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:
parent
606b841435
commit
e6b2180c94
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
22
ComplianceNFs.Monitor/LoggingBootstrapper.cs
Normal file
22
ComplianceNFs.Monitor/LoggingBootstrapper.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user