Files
DiunaBI/DiunaBI.Plugins.PedrolloPL/Processors/PedrolloPLProcessP2.cs

543 lines
20 KiB
C#
Raw Permalink Normal View History

2025-12-05 19:10:28 +01:00
using System.Text;
using DiunaBI.Domain.Entities;
using DiunaBI.Infrastructure.Data;
using DiunaBI.Infrastructure.Plugins;
using Google.Apis.Sheets.v4;
using Google.Apis.Sheets.v4.Data;
using Microsoft.Extensions.Logging;
namespace DiunaBI.Plugins.PedrolloPL.Processors;
public class PedrolloPLProcessP2 : BaseDataProcessor
{
public override string ProcessorType => "PedrolloPL.Process.P2";
private readonly AppDbContext _db;
private readonly ILogger<PedrolloPLProcessP2> _logger;
private readonly SpreadsheetsResource.ValuesResource _googleSheetValues;
// Configuration properties
private string? Year { get; set; }
private bool IsEnabled { get; set; }
private string? GoogleSheetId { get; set; }
private string? GoogleSheetTab { get; set; }
private string? GoogleSheetRange { get; set; }
// Cached data
private Layer? _sourceLayer;
private Layer? _processedLayer;
private Dictionary<string, string>? _codeToRegionMap;
public PedrolloPLProcessP2(
AppDbContext db,
ILogger<PedrolloPLProcessP2> logger,
SpreadsheetsResource.ValuesResource googleSheetValues)
{
_db = db;
_logger = logger;
_googleSheetValues = googleSheetValues;
}
public override void Process(Layer processWorker)
{
try
{
_logger.LogInformation("{ProcessorType}: Starting process for {ProcessWorkerName} ({ProcessWorkerId})",
ProcessorType, processWorker.Name, processWorker.Id);
// Clear cache at start
_sourceLayer = null;
_processedLayer = null;
_codeToRegionMap = null;
LoadConfiguration(processWorker);
ValidateConfiguration();
if (!IsEnabled)
{
_logger.LogInformation("{ProcessorType}: Process disabled for {ProcessWorkerName}",
ProcessorType, processWorker.Name);
return;
}
// Find latest B3 import layer for the configured year
FindSourceLayer();
// Find or create processed layer
FindOrCreateProcessedLayer(processWorker);
// Transform data from source to processed layer
var transformedRecords = TransformData();
// Save records to processed layer
SaveRecordsToLayer(_processedLayer!, transformedRecords);
// Export to Google Sheets if configured
if (!string.IsNullOrEmpty(GoogleSheetId) && !string.IsNullOrEmpty(GoogleSheetTab) && !string.IsNullOrEmpty(GoogleSheetRange))
{
ExportToGoogleSheet();
}
else
{
_logger.LogInformation("{ProcessorType}: Google Sheet export skipped - configuration not provided",
ProcessorType);
}
_logger.LogInformation("{ProcessorType}: Successfully completed process for {ProcessWorkerName} - Processed {RecordCount} records",
ProcessorType, processWorker.Name, transformedRecords.Count);
}
catch (Exception e)
{
_logger.LogError(e, "{ProcessorType}: Failed to process {ProcessWorkerName} ({ProcessWorkerId})",
ProcessorType, processWorker.Name, processWorker.Id);
throw;
}
finally
{
// Clear cache after process
_sourceLayer = null;
_processedLayer = null;
_codeToRegionMap = null;
}
}
private void LoadConfiguration(Layer processWorker)
{
if (processWorker.Records == null) return;
Year = GetRecordValue(processWorker.Records, "Year");
IsEnabled = GetRecordValue(processWorker.Records, "IsEnabled") == "True";
GoogleSheetId = GetRecordValue(processWorker.Records, "GoogleSheetId");
GoogleSheetTab = GetRecordValue(processWorker.Records, "GoogleSheetTab");
GoogleSheetRange = GetRecordValue(processWorker.Records, "GoogleSheetRange");
_logger.LogDebug(
"{ProcessorType}: Configuration loaded - Year: {Year}, Enabled: {IsEnabled}, SheetId: {SheetId}, Tab: {Tab}, Range: {Range}",
ProcessorType, Year, IsEnabled, GoogleSheetId, GoogleSheetTab, GoogleSheetRange);
}
private void ValidateConfiguration()
{
var errors = new List<string>();
if (string.IsNullOrEmpty(Year)) errors.Add("Year is required");
if (errors.Any())
{
throw new InvalidOperationException($"Configuration validation failed: {string.Join(", ", errors)}");
}
_logger.LogDebug("{ProcessorType}: Configuration validated successfully", ProcessorType);
}
private void FindSourceLayer()
{
_logger.LogDebug("{ProcessorType}: Searching for latest B3 import layer for year {Year}",
ProcessorType, Year);
// Find latest B3 import layer matching pattern: L*-I-B3-{Year}-*
var layerNamePattern = $"-I-B3-{Year}-";
_sourceLayer = _db.Layers
.Where(x => x.Name != null && x.Name.Contains(layerNamePattern) && x.Type == LayerType.Import)
.OrderByDescending(x => x.CreatedAt)
.FirstOrDefault();
if (_sourceLayer == null)
{
throw new InvalidOperationException(
$"Source B3 import layer not found for year {Year} (pattern: *{layerNamePattern}*)");
}
_logger.LogInformation("{ProcessorType}: Found source layer - Id: {LayerId}, Name: {LayerName}, CreatedAt: {CreatedAt}",
ProcessorType, _sourceLayer.Id, _sourceLayer.Name, _sourceLayer.CreatedAt);
}
private void FindOrCreateProcessedLayer(Layer processWorker)
{
_logger.LogDebug("{ProcessorType}: Looking for existing processed layer with ParentId={ParentId}",
ProcessorType, processWorker.Id);
// Check if processed layer already exists with ParentId = ProcessWorker.Id
_processedLayer = _db.Layers
.Where(x => x.ParentId == processWorker.Id && x.Type == LayerType.Processed)
.FirstOrDefault();
if (_processedLayer != null)
{
_logger.LogInformation("{ProcessorType}: Found existing processed layer - Id: {LayerId}, Name: {LayerName}",
ProcessorType, _processedLayer.Id, _processedLayer.Name);
}
else
{
_logger.LogInformation("{ProcessorType}: No existing processed layer found, creating new one",
ProcessorType);
_processedLayer = CreateProcessedLayer(processWorker);
}
}
private Layer CreateProcessedLayer(Layer processWorker)
{
var now = DateTime.UtcNow;
var processedLayer = new Layer
{
Id = Guid.NewGuid(),
Number = _db.Layers.Count() + 1,
ParentId = processWorker.Id,
Type = LayerType.Processed,
IsCancelled = false,
CreatedAt = now,
ModifiedAt = now,
CreatedById = Guid.Parse("f392209e-123e-4651-a5a4-0b1d6cf9ff9d"), // System user
ModifiedById = Guid.Parse("f392209e-123e-4651-a5a4-0b1d6cf9ff9d") // System user
};
processedLayer.Name = $"L{processedLayer.Number}-P-P2-{Year}-{now:yyyyMMddHHmm}";
_logger.LogDebug("{ProcessorType}: Creating processed layer '{LayerName}' (Number: {Number})",
ProcessorType, processedLayer.Name, processedLayer.Number);
_db.Layers.Add(processedLayer);
_db.SaveChanges();
_logger.LogInformation("{ProcessorType}: Created processed layer '{LayerName}' with Id: {LayerId}",
ProcessorType, processedLayer.Name, processedLayer.Id);
return processedLayer;
}
private List<Record> TransformData()
{
if (_sourceLayer == null)
{
throw new InvalidOperationException("Source layer not loaded. Call FindSourceLayer first.");
}
_logger.LogDebug("{ProcessorType}: Loading records from source layer {LayerId}",
ProcessorType, _sourceLayer.Id);
// Load all records from source layer
var sourceRecords = _db.Records
.Where(x => x.LayerId == _sourceLayer.Id && !x.IsDeleted)
.ToList();
_logger.LogInformation("{ProcessorType}: Loaded {RecordCount} records from source layer",
ProcessorType, sourceRecords.Count);
// Group records by first 2 digits of Code (region code)
var groupedByRegion = sourceRecords
.Where(x => !string.IsNullOrEmpty(x.Code) && x.Code.Length >= 2)
.GroupBy(x => x.Code!.Substring(0, 2))
.ToList();
_logger.LogDebug("{ProcessorType}: Grouped into {GroupCount} regions",
ProcessorType, groupedByRegion.Count);
var transformedRecords = new List<Record>();
var now = DateTime.UtcNow;
foreach (var regionGroup in groupedByRegion)
{
var regionCode = regionGroup.Key;
// Create array for 12 months (initialize with 0)
var monthValues = new double?[12];
for (int i = 0; i < 12; i++)
{
monthValues[i] = 0;
}
// Fill in values for each month
foreach (var sourceRecord in regionGroup)
{
if (sourceRecord.Code!.Length >= 4)
{
// Extract month from last 2 digits of code (e.g., "0105" -> month 5)
var monthStr = sourceRecord.Code.Substring(2, 2);
if (int.TryParse(monthStr, out var month) && month >= 1 && month <= 12)
{
var monthIndex = month - 1; // Convert to 0-based index
monthValues[monthIndex] = sourceRecord.Value1 ?? 0;
_logger.LogDebug("{ProcessorType}: Region {RegionCode}, Month {Month}: Value = {Value}",
ProcessorType, regionCode, month, sourceRecord.Value1);
}
}
}
// Create transformed record with Code = region code and Value1-12 = monthly values
var record = new Record
{
Id = Guid.NewGuid(),
Code = regionCode,
Value1 = monthValues[0],
Value2 = monthValues[1],
Value3 = monthValues[2],
Value4 = monthValues[3],
Value5 = monthValues[4],
Value6 = monthValues[5],
Value7 = monthValues[6],
Value8 = monthValues[7],
Value9 = monthValues[8],
Value10 = monthValues[9],
Value11 = monthValues[10],
Value12 = monthValues[11],
CreatedAt = now,
ModifiedAt = now
};
transformedRecords.Add(record);
_logger.LogDebug("{ProcessorType}: Transformed region '{RegionCode}' - Values: [{Values}]",
ProcessorType, regionCode,
string.Join(", ", monthValues.Select(v => v?.ToString() ?? "0")));
}
_logger.LogInformation("{ProcessorType}: Successfully transformed {RecordCount} records from {SourceCount} source records",
ProcessorType, transformedRecords.Count, sourceRecords.Count);
return transformedRecords;
}
private void SaveRecordsToLayer(Layer processedLayer, List<Record> records)
{
_logger.LogDebug("{ProcessorType}: Saving {RecordCount} records to layer {LayerId}",
ProcessorType, records.Count, processedLayer.Id);
// Delete any existing records for this layer
var toDelete = _db.Records.Where(x => x.LayerId == processedLayer.Id).ToList();
if (toDelete.Count > 0)
{
_logger.LogInformation("{ProcessorType}: Found {ExistingCount} existing records for layer {LayerId}, removing them",
ProcessorType, toDelete.Count, processedLayer.Id);
_db.Records.RemoveRange(toDelete);
}
// Set all required properties for each record
foreach (var record in records)
{
record.LayerId = processedLayer.Id;
record.CreatedById = Guid.Parse("f392209e-123e-4651-a5a4-0b1d6cf9ff9d"); // System user
record.ModifiedById = Guid.Parse("f392209e-123e-4651-a5a4-0b1d6cf9ff9d"); // System user
_db.Records.Add(record);
}
_db.SaveChanges();
_logger.LogInformation("{ProcessorType}: Successfully saved {RecordCount} records to layer '{LayerName}'",
ProcessorType, records.Count, processedLayer.Name);
}
private void ExportToGoogleSheet()
{
try
{
_logger.LogInformation("{ProcessorType}: Starting Google Sheet export to {SheetId}, Tab: {Tab}, Range: {Range}",
ProcessorType, GoogleSheetId, GoogleSheetTab, GoogleSheetRange);
// Load dictionary for code to region name translation
LoadCodeToRegionDictionary();
// Download current sheet data
var sheetData = DownloadSheetData();
// Update sheet data with processed layer values
var updatedData = UpdateSheetDataWithProcessedValues(sheetData);
// Upload updated data back to sheet
UploadSheetData(updatedData);
_logger.LogInformation("{ProcessorType}: Successfully exported data to Google Sheet",
ProcessorType);
}
catch (Exception e)
{
_logger.LogError(e, "{ProcessorType}: Failed to export to Google Sheet",
ProcessorType);
throw;
}
}
private void LoadCodeToRegionDictionary()
{
const string dictionaryLayerName = "L1-D-P2-CODES";
_logger.LogDebug("{ProcessorType}: Loading code to region mapping from dictionary layer '{DictionaryLayerName}'",
ProcessorType, dictionaryLayerName);
var dictionaryLayer = _db.Layers
.Where(x => x.Name == dictionaryLayerName && x.Type == LayerType.Dictionary)
.FirstOrDefault();
if (dictionaryLayer == null)
{
throw new InvalidOperationException($"Dictionary layer '{dictionaryLayerName}' not found");
}
// Load records for the dictionary layer
var records = _db.Records
.Where(x => x.LayerId == dictionaryLayer.Id)
.ToList();
// Build mapping: Code -> Desc1 (region name)
_codeToRegionMap = records.ToDictionary(
r => r.Code ?? string.Empty,
r => r.Desc1 ?? string.Empty,
StringComparer.OrdinalIgnoreCase);
_logger.LogInformation("{ProcessorType}: Loaded {MappingCount} code to region mappings",
ProcessorType, _codeToRegionMap.Count);
}
private IList<IList<object>> DownloadSheetData()
{
_logger.LogDebug("{ProcessorType}: Downloading sheet data from range {Range}",
ProcessorType, $"{GoogleSheetTab}!{GoogleSheetRange}");
var range = $"{GoogleSheetTab}!{GoogleSheetRange}";
ValueRange? response;
try
{
response = _googleSheetValues.Get(GoogleSheetId, range).Execute();
}
catch (Exception e)
{
_logger.LogError(e, "{ProcessorType}: Failed to download sheet data from {Range}",
ProcessorType, range);
throw new InvalidOperationException($"Failed to download sheet data from {range}", e);
}
if (response?.Values == null || response.Values.Count == 0)
{
throw new InvalidOperationException($"No data found in sheet range {range}");
}
_logger.LogInformation("{ProcessorType}: Downloaded {RowCount} rows from Google Sheet",
ProcessorType, response.Values.Count);
return response.Values;
}
private IList<IList<object>> UpdateSheetDataWithProcessedValues(IList<IList<object>> sheetData)
{
if (_processedLayer == null)
{
throw new InvalidOperationException("Processed layer not loaded");
}
if (_codeToRegionMap == null)
{
throw new InvalidOperationException("Code to region mapping not loaded");
}
_logger.LogDebug("{ProcessorType}: Updating sheet data with processed values from layer {LayerId}",
ProcessorType, _processedLayer.Id);
// Load all records from processed layer
var processedRecords = _db.Records
.Where(x => x.LayerId == _processedLayer.Id && !x.IsDeleted)
.ToList();
_logger.LogDebug("{ProcessorType}: Loaded {RecordCount} records from processed layer",
ProcessorType, processedRecords.Count);
var updatedRowCount = 0;
// Iterate through sheet data and update matching rows
foreach (var row in sheetData)
{
if (row.Count == 0) continue;
// First column (index 0) contains the region name (Kontrola column)
var regionName = row[0]?.ToString()?.Trim();
if (string.IsNullOrEmpty(regionName)) continue;
// Find the code for this region name
var regionCode = _codeToRegionMap
.FirstOrDefault(x => x.Value.Equals(regionName, StringComparison.OrdinalIgnoreCase))
.Key;
if (string.IsNullOrEmpty(regionCode))
{
_logger.LogWarning("{ProcessorType}: No code found for region '{RegionName}' in dictionary - skipping",
ProcessorType, regionName);
continue;
}
// Find the processed record for this code
var processedRecord = processedRecords.FirstOrDefault(x => x.Code == regionCode);
if (processedRecord == null)
{
_logger.LogWarning("{ProcessorType}: No processed record found for code '{RegionCode}' (region: '{RegionName}') - skipping",
ProcessorType, regionCode, regionName);
continue;
}
// Update columns 1-12 (monthly values) in the row
// Column 0 is Kontrola (region name), columns 1-12 are monthly values
// Ensure row has enough columns (13 total: 1 for region + 12 for months)
while (row.Count < 13)
{
row.Add("");
}
// Update monthly values (Value1 through Value12)
row[1] = processedRecord.Value1 ?? 0;
row[2] = processedRecord.Value2 ?? 0;
row[3] = processedRecord.Value3 ?? 0;
row[4] = processedRecord.Value4 ?? 0;
row[5] = processedRecord.Value5 ?? 0;
row[6] = processedRecord.Value6 ?? 0;
row[7] = processedRecord.Value7 ?? 0;
row[8] = processedRecord.Value8 ?? 0;
row[9] = processedRecord.Value9 ?? 0;
row[10] = processedRecord.Value10 ?? 0;
row[11] = processedRecord.Value11 ?? 0;
row[12] = processedRecord.Value12 ?? 0;
updatedRowCount++;
_logger.LogDebug("{ProcessorType}: Updated row for region '{RegionName}' (code: {RegionCode}) with 12 monthly values",
ProcessorType, regionName, regionCode);
}
_logger.LogInformation("{ProcessorType}: Updated {UpdatedRowCount} rows with processed data",
ProcessorType, updatedRowCount);
return sheetData;
}
private void UploadSheetData(IList<IList<object>> data)
{
_logger.LogDebug("{ProcessorType}: Uploading {RowCount} rows to Google Sheet range {Range}",
ProcessorType, data.Count, $"{GoogleSheetTab}!{GoogleSheetRange}");
var range = $"{GoogleSheetTab}!{GoogleSheetRange}";
var valueRange = new ValueRange { Values = data };
try
{
var updateRequest = _googleSheetValues.Update(valueRange, GoogleSheetId, range);
updateRequest.ValueInputOption = SpreadsheetsResource.ValuesResource.UpdateRequest.ValueInputOptionEnum.USERENTERED;
var response = updateRequest.Execute();
_logger.LogInformation("{ProcessorType}: Successfully uploaded data to Google Sheet - Updated {UpdatedCells} cells",
ProcessorType, response.UpdatedCells);
}
catch (Exception e)
{
_logger.LogError(e, "{ProcessorType}: Failed to upload data to Google Sheet range {Range}",
ProcessorType, range);
throw new InvalidOperationException($"Failed to upload data to Google Sheet range {range}", e);
}
}
private string? GetRecordValue(ICollection<Record> records, string code)
{
return records.FirstOrDefault(x => x.Code == code)?.Desc1;
}
}