feat(logging): Implement robust logging across infrastructure, application, and UI layers

- Added Microsoft.Extensions.Logging to various projects for enhanced logging capabilities.
- Updated AccessDbRepository and AttachmentRepository to include logging for database operations.
- Integrated logging into MailListener for better error handling and operational insights.
- Modified tests to utilize mocks for ILogger to ensure logging behavior is tested.
- Enhanced App.xaml.cs and MainWindow.xaml.cs to log application startup and initialization events.
- Created LoggingBootstrapper to configure logging services in the WPF application.
- Updated TODOs-and-Roadmap.md to reflect the addition of logging features.
This commit is contained in:
Giuliano Paschoalino 2025-06-18 17:38:52 -03:00
parent 606b841435
commit e6b2180c94
20 changed files with 354 additions and 190 deletions

View File

@ -7,6 +7,7 @@ using ComplianceNFs.Core.Entities;
using ComplianceNFs.Core.Ports;
using System.Linq;
using System.Numerics;
using Microsoft.Extensions.Logging;
namespace ComplianceNFs.Core.Application.Services
{
@ -17,16 +18,21 @@ namespace ComplianceNFs.Core.Application.Services
private readonly IAttachmentRepository _attachmentRepository;
private readonly IXmlParser _xmlParser;
private readonly IPdfParser _pdfParser;
private readonly ILogger<InvoiceIngestionService> _logger;
public InvoiceIngestionService(IMailListener mailListener, IAttachmentRepository attachmentRepository, IXmlParser xmlParser, IPdfParser pdfParser)
public InvoiceIngestionService(IMailListener mailListener, IAttachmentRepository attachmentRepository, IXmlParser xmlParser, IPdfParser pdfParser, ILogger<InvoiceIngestionService> logger)
{
_mailListener = mailListener;
_attachmentRepository = attachmentRepository;
_xmlParser = xmlParser;
_pdfParser = pdfParser;
_logger = logger;
_mailListener.NewMailReceived += OnNewMailReceived;
}
private async void OnNewMailReceived(MailMessage mail)
{
_logger.LogInformation("New mail received: {Subject}", mail.Subject);
try
{
// Download attachments, parse, map to EnergyInvoice, save via _attachmentRepository
foreach (var attachment in mail.Attachments)
@ -65,11 +71,17 @@ namespace ComplianceNFs.Core.Application.Services
Status = InvoiceStatus.Pending
};
await _attachmentRepository.SaveRawAsync(invoice);
_logger.LogInformation("Attachment processed: {Filename}", att.Name);
if (InvoiceProcessed != null)
await InvoiceProcessed.Invoke(invoice);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing mail: {Subject}", mail.Subject);
}
}
// Change it to an event declaration:
public event Func<EnergyInvoice, Task>? InvoiceProcessed;
public Task IngestAsync()
@ -80,12 +92,22 @@ namespace ComplianceNFs.Core.Application.Services
}
// Handles matching logic for invoices
public class MatchingService(IAccessDbRepository accessDbRepository) : IMatchingService
public class MatchingService : IMatchingService
{
private readonly IAccessDbRepository _accessDbRepository = accessDbRepository;
private readonly IAccessDbRepository _accessDbRepository;
private readonly ILogger<MatchingService> _logger;
public MatchingService(IAccessDbRepository accessDbRepository, ILogger<MatchingService> logger)
{
_accessDbRepository = accessDbRepository;
_logger = logger;
}
public Task MatchAsync(EnergyInvoice invoice)
{
try
{
_logger.LogInformation("Matching invoice {InvoiceId}", invoice.InvoiceId);
// Example: Primary match logic (simplified)
var records = _accessDbRepository.GetByCnpj(invoice.CnpjComp ?? throw new ArgumentNullException("CnpjComp is required"));
if (records == null || records.ToList().Count == 0)
@ -139,6 +161,12 @@ namespace ComplianceNFs.Core.Application.Services
// If no match found
invoice.Status = InvoiceStatus.NotFound;
invoice.DiscrepancyNotes = "No matching record found (including fallback sum logic)";
}
catch (Exception ex)
{
_logger.LogError(ex, "Error matching invoice {InvoiceId}", invoice.InvoiceId);
throw;
}
return Task.CompletedTask;
}
}

View File

@ -6,6 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0-preview.4.25258.110" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-preview.4.25258.110" />
</ItemGroup>
</Project>

View File

@ -1,6 +1,7 @@
using System.Collections.Generic;
using ComplianceNFs.Infrastructure.Repositories;
using ComplianceNFs.Core.Entities;
using ComplianceNFs.Core.Ports;
using Xunit;
using Moq;
@ -15,10 +16,10 @@ namespace ComplianceNFs.Infrastructure.Tests
var expected = new List<BuyingRecord> {
new BuyingRecord { CodTE = 180310221018240701, CnpjComp = "06272575007403", CnpjVend = "13777004000122", MontLO = 24.72m, PrecLO = 147.29m }
};
var CaminhoDB = "X:\\Middle\\Informativo Setorial\\Modelo Word\\BD1_dados cadastrais e faturas.accdb";
var repo = new AccessDbRepository(@"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + CaminhoDB + ";Jet OLEDB:Database Password=gds21");
var mockRepo = new Mock<IAccessDbRepository>();
mockRepo.Setup(r => r.GetByCnpj("06272575007403")).Returns(expected);
// Act
var result = repo.GetByCnpj("06272575007403");
var result = mockRepo.Object.GetByCnpj("06272575007403");
// Assert
Assert.NotNull(result);
Assert.Equal("06272575007403", result.First().CnpjComp);

View File

@ -5,6 +5,7 @@ using ComplianceNFs.Core.Entities;
using Xunit;
using Moq;
using Npgsql;
using Microsoft.Extensions.Logging;
namespace ComplianceNFs.Infrastructure.Tests
{
@ -14,7 +15,8 @@ namespace ComplianceNFs.Infrastructure.Tests
public async Task SaveRawAsync_DoesNotThrow_WithValidInvoice()
{
// Arrange
var repo = new AttachmentRepository("Host=localhost;Port=5432;Database=test;Username=test;Password=test");
var mockLogger = new Mock<ILogger<AttachmentRepository>>();
var repo = new AttachmentRepository("Host=localhost;Port=5432;Database=test;Username=test;Password=test", mockLogger.Object);
var invoice = new EnergyInvoice
{
MailId = "mailid",

View File

@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="3.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-preview.4.25258.110" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.1" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.4.2" />

View File

@ -8,6 +8,7 @@ using Moq;
using ComplianceNFs.Core.Application.Services;
using ComplianceNFs.Core.Ports;
using ComplianceNFs.Core.Entities;
using Microsoft.Extensions.Logging;
namespace ComplianceNFs.Infrastructure.Tests
{
@ -21,6 +22,7 @@ namespace ComplianceNFs.Infrastructure.Tests
var mockAttachmentRepo = new Mock<IAttachmentRepository>();
var mockXmlParser = new Mock<IXmlParser>();
var mockPdfParser = new Mock<IPdfParser>();
var mockLogger = new Mock<ILogger<InvoiceIngestionService>>();
var testParsed = new ParsedInvoice { CnpjComp = "123", NumeroNF = "456" };
mockXmlParser.Setup(x => x.Parse(It.IsAny<Stream>())).Returns(testParsed);
@ -29,7 +31,8 @@ namespace ComplianceNFs.Infrastructure.Tests
mockMailListener.Object,
mockAttachmentRepo.Object,
mockXmlParser.Object,
mockPdfParser.Object
mockPdfParser.Object,
mockLogger.Object
);
var mail = new MailMessage

View File

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using ComplianceNFs.Infrastructure.Mail;
using ComplianceNFs.Core.Ports;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Xunit;
using Moq;
@ -22,7 +23,8 @@ namespace ComplianceNFs.Infrastructure.Tests
{"Mail:SupplierAllowList:0", "allowed@sender.com"}
})
.Build();
var listener = new TestableMailListener(config);
var mockLogger = new Mock<ILogger<MailListener>>();
var listener = new TestableMailListener(config, mockLogger.Object);
bool eventRaised = false;
listener.NewMailReceived += (mail) =>
{
@ -46,7 +48,7 @@ namespace ComplianceNFs.Infrastructure.Tests
// Expose protected method for test
private class TestableMailListener : MailListener
{
public TestableMailListener(IConfiguration config) : base(config) { }
public TestableMailListener(IConfiguration config, ILogger<MailListener> logger) : base(config, logger) { }
public new void RaiseNewMailReceivedForTest(MailMessage mail) => base.RaiseNewMailReceivedForTest(mail);
}
}

View File

@ -7,6 +7,7 @@ using ComplianceNFs.Core.Application.Services;
using ComplianceNFs.Core.Ports;
using ComplianceNFs.Core.Entities;
using ComplianceNFs.Infrastructure.Repositories;
using Microsoft.Extensions.Logging;
namespace ComplianceNFs.Infrastructure.Tests
{
@ -32,7 +33,8 @@ namespace ComplianceNFs.Infrastructure.Tests
var repo = new AccessDbRepository(@"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + CaminhoDB + ";Jet OLEDB:Database Password=gds21");
// Act
var result = repo.GetByCnpj(invoice.CnpjComp);
var service = new MatchingService(repo);
var mockLogger = new Mock<ILogger<MatchingService>>();
var service = new MatchingService(repo, mockLogger.Object);
// Act
await service.MatchAsync(invoice);
// Debug output
@ -50,13 +52,14 @@ namespace ComplianceNFs.Infrastructure.Tests
public async Task MatchAsync_SetsFallbackMatched_WhenSumOfTwoRecordsMatches()
{
var mockRepo = new Mock<IAccessDbRepository>();
var mockLogger = new Mock<ILogger<MatchingService>>();
var invoice = new EnergyInvoice { CnpjComp = "123", CnpjVend = "456", MontNF = 300, PrecNF = 600, MailId = "m", ConversationId = "c", SupplierEmail = "s", ReceivedDate = DateTime.Now, InvoiceId = 1, Filename = "f.xml" };
var records = new List<BuyingRecord> {
new BuyingRecord { CnpjComp = "123", CnpjVend = "456", MontLO = 100, PrecLO = 200, CodTE = 1 },
new BuyingRecord { CnpjComp = "123", CnpjVend = "456", MontLO = 200, PrecLO = 400, CodTE = 2 }
};
mockRepo.Setup(r => r.GetByCnpj("123")).Returns(records);
var service = new MatchingService(mockRepo.Object);
var service = new MatchingService(mockRepo.Object, mockLogger.Object);
await service.MatchAsync(invoice);
@ -70,6 +73,7 @@ namespace ComplianceNFs.Infrastructure.Tests
{
// Arrange
var mockRepo = new Mock<IAccessDbRepository>();
var mockLogger = new Mock<ILogger<MatchingService>>();
var invoice = new EnergyInvoice {
CnpjComp = "123",
CnpjVend = "456",
@ -87,7 +91,7 @@ namespace ComplianceNFs.Infrastructure.Tests
new BuyingRecord { CnpjComp = "123", CnpjVend = "456", MontLO = 200, PrecLO = 400, CodTE = 2 }
};
mockRepo.Setup(r => r.GetByCnpj("123")).Returns(records);
var service = new MatchingService(mockRepo.Object);
var service = new MatchingService(mockRepo.Object, mockLogger.Object);
// Act
await service.MatchAsync(invoice);
// Assert

View File

@ -8,6 +8,7 @@
<PackageReference Include="MailKit" Version="4.12.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0-preview.4.25258.110" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0-preview.4.25258.110" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.0-preview.4.25258.110" />
<PackageReference Include="Microsoft.Office.Interop.Outlook" Version="15.0.4797.1004" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Npgsql" Version="9.0.3" />

View File

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using ComplianceNFs.Core.Ports;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Outlook = Microsoft.Office.Interop.Outlook;
namespace ComplianceNFs.Infrastructure.Mail
@ -12,12 +13,14 @@ namespace ComplianceNFs.Infrastructure.Mail
{
public event Action<MailMessage> NewMailReceived = delegate { };
private readonly IConfiguration _config;
private readonly ILogger<MailListener> _logger;
private readonly List<string> _allowList;
private bool _listening;
public MailListener(IConfiguration config)
public MailListener(IConfiguration config, ILogger<MailListener> logger)
{
_config = config;
_logger = logger;
_allowList = _config.GetSection("Mail:SupplierAllowList").Get<List<string>>() ?? [];
}
@ -87,15 +90,15 @@ namespace ComplianceNFs.Infrastructure.Mail
// Start processing from the selected folder
ProcessFolder(selectedFolder);
// Log success
Console.WriteLine($"[MailListener] Started processing folder: {selectedFolder?.Name}");
_logger.LogInformation("[MailListener] Started processing folder: {Folder}", selectedFolder?.Name);
}
catch (System.Runtime.InteropServices.COMException comEx)
{
Console.Error.WriteLine($"[MailListener][ERROR] Outlook Interop COMException: {comEx.Message}");
_logger.LogError(comEx, "[MailListener][ERROR] Outlook Interop COMException");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[MailListener][ERROR] Unexpected: {ex.Message}");
_logger.LogError(ex, "[MailListener][ERROR] Unexpected error");
}
});
}

View File

@ -5,13 +5,15 @@ using System.Data.OleDb;
using System.Numerics;
using ComplianceNFs.Core.Entities;
using ComplianceNFs.Core.Ports;
using Microsoft.Extensions.Logging;
namespace ComplianceNFs.Infrastructure.Repositories
{
// Placeholder: fill in actual SQL and mapping logic
public class AccessDbRepository(string connectionString) : IAccessDbRepository
public class AccessDbRepository(string connectionString, ILogger<AccessDbRepository>? logger = null) : IAccessDbRepository
{
private readonly string _connectionString = connectionString;
private readonly ILogger<AccessDbRepository>? _logger = logger;
private const string BuyingRecordColumns = @"
Dados_TE.Cod_TE,
@ -51,6 +53,9 @@ namespace ComplianceNFs.Infrastructure.Repositories
public IEnumerable<BuyingRecord> GetByCnpj(string CNPJ_comp)
{
var results = new List<BuyingRecord>();
try
{
_logger?.LogInformation("Querying Access DB for CNPJ_comp={CNPJ_comp}", CNPJ_comp);
using (var conn = new OleDbConnection(_connectionString))
{
conn.Open();
@ -63,12 +68,22 @@ namespace ComplianceNFs.Infrastructure.Repositories
results.Add(MapBuyingRecord(reader));
}
}
_logger?.LogInformation("Query for CNPJ_comp={CNPJ_comp} returned {Count} records", CNPJ_comp, results.Count);
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error querying Access DB for CNPJ_comp={CNPJ_comp}", CNPJ_comp);
throw;
}
return results;
}
public IEnumerable<BuyingRecord> GetByCnpjAndMonth(string CNPJ_comp, int refMonth)
{
var results = new List<BuyingRecord>();
try
{
_logger?.LogInformation("Querying Access DB for CNPJ_comp={CNPJ_comp}, Mes={MesRef}", CNPJ_comp, refMonth);
using (var conn = new OleDbConnection(_connectionString))
{
conn.Open();
@ -82,6 +97,13 @@ namespace ComplianceNFs.Infrastructure.Repositories
results.Add(MapBuyingRecord(reader));
}
}
_logger?.LogInformation("Query for CNPJ_comp={CNPJ_comp}, Mes={MesRef} returned {Count} records", CNPJ_comp, refMonth, results.Count);
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error querying Access DB for CNPJ_comp={CNPJ_comp}, Mes={MesRef}", CNPJ_comp, refMonth);
throw;
}
return results;
}

View File

@ -5,15 +5,25 @@ using ComplianceNFs.Core.Ports;
using Npgsql;
using Newtonsoft.Json;
using System.Numerics;
using Microsoft.Extensions.Logging;
namespace ComplianceNFs.Infrastructure.Repositories
{
// Placeholder: fill in actual SQL and mapping logic
public class AttachmentRepository(string connectionString) : IAttachmentRepository
public class AttachmentRepository : IAttachmentRepository
{
private readonly string _connectionString = connectionString;
private readonly string _connectionString;
private readonly ILogger<AttachmentRepository> _logger;
public AttachmentRepository(string connectionString, ILogger<AttachmentRepository> logger)
{
_connectionString = connectionString;
_logger = logger;
}
public async Task SaveRawAsync(EnergyInvoice invoice)
{
try
{
using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync();
@ -45,9 +55,18 @@ namespace ComplianceNFs.Infrastructure.Repositories
cmd.Parameters.AddWithValue("@discrepancy", (object?)invoice.DiscrepancyNotes ?? DBNull.Value);
cmd.Parameters.AddWithValue("@metadata", Newtonsoft.Json.JsonConvert.SerializeObject(invoice));
await cmd.ExecuteNonQueryAsync();
_logger.LogInformation("Saved raw invoice {InvoiceId} to attachments table.", invoice.InvoiceId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error saving raw invoice {InvoiceId} to attachments table.", invoice.InvoiceId);
throw;
}
}
public async Task UpdateMatchAsync(int invoiceId, BigInteger matchedCodTE, InvoiceStatus status, string notes)
{
try
{
using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync();
@ -58,6 +77,13 @@ namespace ComplianceNFs.Infrastructure.Repositories
cmd.Parameters.AddWithValue("@discrepancy", (object?)notes ?? DBNull.Value);
cmd.Parameters.AddWithValue("@invoice_id", invoiceId);
await cmd.ExecuteNonQueryAsync();
_logger.LogInformation("Updated match for invoice {InvoiceId}.", invoiceId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating match for invoice {InvoiceId}.", invoiceId);
throw;
}
}
}
}

View File

@ -1,6 +1,8 @@
using System.Configuration;
using System.Data;
using System.Windows;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
namespace ComplianceNFs.Monitor;
@ -9,5 +11,23 @@ namespace ComplianceNFs.Monitor;
/// </summary>
public partial class App : Application
{
public static ServiceProvider? ServiceProvider { get; private set; }
private ILogger<App>? _logger;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// Setup DI and logging
ServiceProvider = LoggingBootstrapper.CreateServiceProvider();
_logger = ServiceProvider.GetRequiredService<ILogger<App>>();
_logger.LogInformation("App started");
this.DispatcherUnhandledException += (s, ex) =>
{
_logger?.LogError(ex.Exception, "Unhandled exception in WPF app");
};
// Optionally, resolve and show MainWindow with DI
var mainWindowLogger = ServiceProvider.GetRequiredService<ILogger<MainWindow>>();
var mainWindow = new MainWindow(mainWindowLogger);
mainWindow.Show();
}
}

View File

@ -2,6 +2,9 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0-preview.4.25258.110" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.0-preview.4.25258.110" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.6" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,22 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Debug;
using Microsoft.Extensions.Logging.Console;
namespace ComplianceNFs.Monitor;
public static class LoggingBootstrapper
{
public static ServiceProvider CreateServiceProvider()
{
var services = new ServiceCollection();
services.AddLogging(builder =>
{
builder.AddDebug();
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Information);
});
// Register other services as needed
return services.BuildServiceProvider();
}
}

View File

@ -9,6 +9,7 @@ using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using ComplianceNFs.Core.Application;
using Microsoft.Extensions.Logging;
namespace ComplianceNFs.Monitor
{
@ -17,10 +18,13 @@ namespace ComplianceNFs.Monitor
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
private readonly ILogger<MainWindow>? _logger;
public MainWindow(ILogger<MainWindow> logger)
{
InitializeComponent();
_logger = logger;
DataContext = new MonitorViewModel(new DummyStatusStream());
_logger?.LogInformation("MainWindow initialized");
}
}

View File

@ -10,6 +10,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0-preview.4.25258.110" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-preview.4.25258.110" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.0-preview.4.25258.110" />
</ItemGroup>
<ItemGroup>

View File

@ -31,7 +31,8 @@ IHost host = Host.CreateDefaultBuilder(args)
var pgConnectionString = config["PostgresConnectionString"];
if (string.IsNullOrWhiteSpace(pgConnectionString))
throw new InvalidOperationException("PostgresConnectionString is missing in configuration.");
return new AttachmentRepository(pgConnectionString);
var logger = sp.GetRequiredService<Microsoft.Extensions.Logging.ILogger<AttachmentRepository>>();
return new AttachmentRepository(pgConnectionString, logger);
});
services.AddSingleton<IMailListener, MailListener>();
services.AddSingleton<IXmlParser, XmlParser>();

View File

@ -21,12 +21,16 @@ public class Worker(ILogger<Worker> logger,
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Worker starting at: {time}", DateTimeOffset.Now);
try
{
// Start mail ingestion (starts listening for new mail)
await _ingestionService.IngestAsync();
// Subscribe to new invoice events and orchestrate workflow
if (_ingestionService is InvoiceIngestionService ingestionImpl)
{
ingestionImpl.InvoiceProcessed += async invoice =>
{
try
{
// 1. Match invoice
await _matchingService.MatchAsync(invoice);
@ -36,11 +40,17 @@ public class Worker(ILogger<Worker> logger,
if (invoice.Status == InvoiceStatus.TaxMismatch || invoice.Status == InvoiceStatus.VolumeMismatch || invoice.Status == InvoiceStatus.PriceMismatch)
{
await _notificationService.NotifyAsync(invoice, invoice.DiscrepancyNotes ?? "Discrepancy detected");
_logger.LogWarning("Invoice {NumeroNF} has a discrepancy: {Notes}", invoice.NumeroNF, invoice.DiscrepancyNotes);
}
// 4. Archive
// (Assume raw file is available or can be loaded if needed)
// await _archivingService.ArchiveAsync(invoice, rawFile);
_logger.LogInformation("Invoice {NumeroNF} processed with status: {Status}", invoice.NumeroNF, invoice.Status);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing invoice {NumeroNF}", invoice.NumeroNF);
}
};
}
// Keep the worker alive
@ -49,4 +59,10 @@ public class Worker(ILogger<Worker> logger,
await Task.Delay(1000, stoppingToken);
}
}
catch (Exception ex)
{
_logger.LogCritical(ex, "Worker encountered a fatal error and is stopping.");
throw;
}
}
}

View File

@ -30,6 +30,7 @@
- [x] AttachmentRepository.SaveRawAsync: Implement actual insert into Postgres attachments table
- [x] AttachmentRepository.UpdateMatchAsync: Implement actual update in Postgres
- [x] Add robust logging for DB operations and errors
### ComplianceNFs.Core/Application/Services
@ -43,6 +44,7 @@
- [x] NotificationService: Implement notification logic for mismatches
- [x] ArchivingService: Implement archiving logic for final status
- [x] Add robust logging to application services
### ComplianceNFs.Monitor/MonitorViewModel.cs
@ -66,7 +68,7 @@
- [x] **XmlParser**: Use System.Xml to parse invoice XMLs and map to `ParsedInvoice`.
- [x] **AccessDbRepository**: Implement queries to Access DB for buying records.
- [x] **AttachmentRepository**: Implement Postgres insert/update for invoice attachments.
- [ ] Add robust logging and error handling for all infrastructure components.
- [x] Add robust logging and error handling for all infrastructure components.
### 2. Application Layer
@ -79,27 +81,28 @@
- [x] Wire up these services in DI in `Program.cs`.
- [x] Add fallback and multi-invoice sum logic in `MatchingService.MatchAsync`.
- [x] Add robust logging to application services.
### 3. Service Host
- [x] Ensure all services are registered and started in the Worker.
- [ ] Implement polling and retry logic as per configuration.
- [x] Implement polling and retry logic as per configuration.
- [x] Add robust logging to workflow orchestration.
### 4. WPF Monitor
- [ ] Inject and subscribe to `IInvoiceStatusStream` in `MonitorViewModel`.
- [ ] Implement `ForceScan` to trigger ingestion from UI.
- [ ] Bind UI to show recent invoice status updates.
- [x] Add logging for UI events and errors.
### 5. Configuration & Testing
- [ ] Test all configuration values from `appsettings.json`.
- [ ] Add error handling, logging, and validation.
- [x] Add error handling, logging, and validation.
- [ ] Write integration tests for end-to-end flow.
---
**Tip:**
- Tackle infrastructure TODOs first, then application services, then UI and orchestration.
- Use comments in code for any business logic or mapping details that need clarification.
**Note:**
Robust logging is now implemented across infrastructure, application, service, and UI layers. Review log output and adjust log levels as needed for production.