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,58 +18,69 @@ 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)
|
||||
{
|
||||
// Download attachments, parse, map to EnergyInvoice, save via _attachmentRepository
|
||||
foreach (var attachment in mail.Attachments)
|
||||
_logger.LogInformation("New mail received: {Subject}", mail.Subject);
|
||||
try
|
||||
{
|
||||
if (attachment is System.Net.Mail.Attachment att && att.Name != null)
|
||||
// Download attachments, parse, map to EnergyInvoice, save via _attachmentRepository
|
||||
foreach (var attachment in mail.Attachments)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
att.ContentStream.CopyTo(stream);
|
||||
stream.Position = 0;
|
||||
ParsedInvoice parsed = new();
|
||||
if (att.Name.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
|
||||
parsed = _xmlParser.Parse(stream);
|
||||
else if (att.Name.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
|
||||
parsed = _pdfParser.Parse(stream);
|
||||
else
|
||||
continue;
|
||||
var invoice = new EnergyInvoice
|
||||
if (attachment is System.Net.Mail.Attachment att && att.Name != null)
|
||||
{
|
||||
MailId = mail.Headers?["Message-ID"] ?? string.Empty,
|
||||
ConversationId = mail.Headers?["Conversation-ID"] ?? string.Empty,
|
||||
SupplierEmail = mail.From != null ? mail.From.Address : string.Empty,
|
||||
ReceivedDate = !string.IsNullOrEmpty(mail.Headers?["Date"]) && DateTime.TryParse(mail.Headers["Date"], out var dt) ? dt : DateTime.Now,
|
||||
InvoiceId = int.TryParse(parsed.NumeroNF, out var nf) ? nf : 0,
|
||||
Filename = att.Name,
|
||||
CnpjComp = parsed.CnpjComp,
|
||||
CnpjVend = parsed.CnpjVend,
|
||||
MontNF = parsed.MontNF,
|
||||
PrecNF = parsed.PrecNF,
|
||||
ValorFinalComImpostos = parsed.ValorFinalComImpostos,
|
||||
RsComp = parsed.RsComp,
|
||||
RsVend = parsed.RsVend,
|
||||
NumeroNF = parsed.NumeroNF,
|
||||
IcmsNF = parsed.IcmsNF,
|
||||
UfComp = parsed.UfComp,
|
||||
UfVend = parsed.UfVend,
|
||||
Status = InvoiceStatus.Pending
|
||||
};
|
||||
await _attachmentRepository.SaveRawAsync(invoice);
|
||||
if (InvoiceProcessed != null)
|
||||
await InvoiceProcessed.Invoke(invoice);
|
||||
using var stream = new MemoryStream();
|
||||
att.ContentStream.CopyTo(stream);
|
||||
stream.Position = 0;
|
||||
ParsedInvoice parsed = new();
|
||||
if (att.Name.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
|
||||
parsed = _xmlParser.Parse(stream);
|
||||
else if (att.Name.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
|
||||
parsed = _pdfParser.Parse(stream);
|
||||
else
|
||||
continue;
|
||||
var invoice = new EnergyInvoice
|
||||
{
|
||||
MailId = mail.Headers?["Message-ID"] ?? string.Empty,
|
||||
ConversationId = mail.Headers?["Conversation-ID"] ?? string.Empty,
|
||||
SupplierEmail = mail.From != null ? mail.From.Address : string.Empty,
|
||||
ReceivedDate = !string.IsNullOrEmpty(mail.Headers?["Date"]) && DateTime.TryParse(mail.Headers["Date"], out var dt) ? dt : DateTime.Now,
|
||||
InvoiceId = int.TryParse(parsed.NumeroNF, out var nf) ? nf : 0,
|
||||
Filename = att.Name,
|
||||
CnpjComp = parsed.CnpjComp,
|
||||
CnpjVend = parsed.CnpjVend,
|
||||
MontNF = parsed.MontNF,
|
||||
PrecNF = parsed.PrecNF,
|
||||
ValorFinalComImpostos = parsed.ValorFinalComImpostos,
|
||||
RsComp = parsed.RsComp,
|
||||
RsVend = parsed.RsVend,
|
||||
NumeroNF = parsed.NumeroNF,
|
||||
IcmsNF = parsed.IcmsNF,
|
||||
UfComp = parsed.UfComp,
|
||||
UfVend = parsed.UfVend,
|
||||
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;
|
||||
@ -80,65 +92,81 @@ 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)
|
||||
{
|
||||
// Example: Primary match logic (simplified)
|
||||
var records = _accessDbRepository.GetByCnpj(invoice.CnpjComp ?? throw new ArgumentNullException("CnpjComp is required"));
|
||||
if (records == null || records.ToList().Count == 0)
|
||||
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)
|
||||
{
|
||||
invoice.Status = InvoiceStatus.NotFound;
|
||||
invoice.DiscrepancyNotes = "No records found for matching";
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
foreach (var record in records)
|
||||
{
|
||||
if (record.CnpjComp == invoice.CnpjComp && record.CnpjVend == invoice.CnpjVend)
|
||||
{
|
||||
var volMatch = Math.Abs(record.MontLO - invoice.MontNF ?? 0) / (record.MontLO == 0 ? 1 : record.MontLO) <= 0.01m;
|
||||
var priceMatch = Math.Abs(record.PrecLO - invoice.PrecNF ?? 0) / (record.PrecLO == 0 ? 1 : record.PrecLO) <= 0.005m;
|
||||
if (volMatch && priceMatch)
|
||||
{
|
||||
invoice.MatchedCodTE = record.CodTE;
|
||||
invoice.Status = InvoiceStatus.Matched;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback: try to match by summing multiple records (multi-invoice sum logic)
|
||||
// Only consider records with same CnpjComp and CnpjVend
|
||||
var candidateRecords = new List<BuyingRecord>();
|
||||
foreach (var record in records)
|
||||
{
|
||||
if (record.CnpjComp == invoice.CnpjComp && record.CnpjVend == invoice.CnpjVend)
|
||||
{
|
||||
candidateRecords.Add(record);
|
||||
}
|
||||
}
|
||||
// Try all combinations of 2 records (can be extended to more)
|
||||
for (int i = 0; i < candidateRecords.Count; i++)
|
||||
{
|
||||
for (int j = i + 1; j < candidateRecords.Count; j++)
|
||||
{
|
||||
var sumVol = (candidateRecords[i].MontLO ?? 0) + (candidateRecords[j].MontLO ?? 0);
|
||||
var sumPrice = (candidateRecords[i].PrecLO ?? 0) + (candidateRecords[j].PrecLO ?? 0);
|
||||
var volMatch = Math.Abs(sumVol - (invoice.MontNF ?? 0)) / (sumVol == 0 ? 1 : sumVol) <= 0.01m;
|
||||
var priceMatch = Math.Abs(sumPrice - (invoice.PrecNF ?? 0)) / (sumPrice == 0 ? 1 : sumPrice) <= 0.005m;
|
||||
if (volMatch && priceMatch)
|
||||
{
|
||||
invoice.MatchedCodTE = candidateRecords[i].CodTE; // or store both if needed
|
||||
invoice.Status = InvoiceStatus.FallbackMatched;
|
||||
invoice.DiscrepancyNotes = $"Matched by sum of records {candidateRecords[i].CodTE} and {candidateRecords[j].CodTE}";
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If no match found
|
||||
invoice.Status = InvoiceStatus.NotFound;
|
||||
invoice.DiscrepancyNotes = "No records found for matching";
|
||||
return Task.CompletedTask;
|
||||
invoice.DiscrepancyNotes = "No matching record found (including fallback sum logic)";
|
||||
}
|
||||
foreach (var record in records)
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (record.CnpjComp == invoice.CnpjComp && record.CnpjVend == invoice.CnpjVend)
|
||||
{
|
||||
var volMatch = Math.Abs(record.MontLO - invoice.MontNF ?? 0) / (record.MontLO == 0 ? 1 : record.MontLO) <= 0.01m;
|
||||
var priceMatch = Math.Abs(record.PrecLO - invoice.PrecNF ?? 0) / (record.PrecLO == 0 ? 1 : record.PrecLO) <= 0.005m;
|
||||
if (volMatch && priceMatch)
|
||||
{
|
||||
invoice.MatchedCodTE = record.CodTE;
|
||||
invoice.Status = InvoiceStatus.Matched;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
_logger.LogError(ex, "Error matching invoice {InvoiceId}", invoice.InvoiceId);
|
||||
throw;
|
||||
}
|
||||
// Fallback: try to match by summing multiple records (multi-invoice sum logic)
|
||||
// Only consider records with same CnpjComp and CnpjVend
|
||||
var candidateRecords = new List<BuyingRecord>();
|
||||
foreach (var record in records)
|
||||
{
|
||||
if (record.CnpjComp == invoice.CnpjComp && record.CnpjVend == invoice.CnpjVend)
|
||||
{
|
||||
candidateRecords.Add(record);
|
||||
}
|
||||
}
|
||||
// Try all combinations of 2 records (can be extended to more)
|
||||
for (int i = 0; i < candidateRecords.Count; i++)
|
||||
{
|
||||
for (int j = i + 1; j < candidateRecords.Count; j++)
|
||||
{
|
||||
var sumVol = (candidateRecords[i].MontLO ?? 0) + (candidateRecords[j].MontLO ?? 0);
|
||||
var sumPrice = (candidateRecords[i].PrecLO ?? 0) + (candidateRecords[j].PrecLO ?? 0);
|
||||
var volMatch = Math.Abs(sumVol - (invoice.MontNF ?? 0)) / (sumVol == 0 ? 1 : sumVol) <= 0.01m;
|
||||
var priceMatch = Math.Abs(sumPrice - (invoice.PrecNF ?? 0)) / (sumPrice == 0 ? 1 : sumPrice) <= 0.005m;
|
||||
if (volMatch && priceMatch)
|
||||
{
|
||||
invoice.MatchedCodTE = candidateRecords[i].CodTE; // or store both if needed
|
||||
invoice.Status = InvoiceStatus.FallbackMatched;
|
||||
invoice.DiscrepancyNotes = $"Matched by sum of records {candidateRecords[i].CodTE} and {candidateRecords[j].CodTE}";
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If no match found
|
||||
invoice.Status = InvoiceStatus.NotFound;
|
||||
invoice.DiscrepancyNotes = "No matching record found (including fallback sum logic)";
|
||||
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,17 +53,27 @@ namespace ComplianceNFs.Infrastructure.Repositories
|
||||
public IEnumerable<BuyingRecord> GetByCnpj(string CNPJ_comp)
|
||||
{
|
||||
var results = new List<BuyingRecord>();
|
||||
using (var conn = new OleDbConnection(_connectionString))
|
||||
try
|
||||
{
|
||||
conn.Open();
|
||||
var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = $@"SELECT {BuyingRecordColumns} FROM Dados_TE WHERE ((Dados_TE.CNPJ_comp)=@CNPJ_comp);";
|
||||
cmd.Parameters.AddWithValue("@CNPJ_comp", CNPJ_comp);
|
||||
using var reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
_logger?.LogInformation("Querying Access DB for CNPJ_comp={CNPJ_comp}", CNPJ_comp);
|
||||
using (var conn = new OleDbConnection(_connectionString))
|
||||
{
|
||||
results.Add(MapBuyingRecord(reader));
|
||||
conn.Open();
|
||||
var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = $@"SELECT {BuyingRecordColumns} FROM Dados_TE WHERE ((Dados_TE.CNPJ_comp)=@CNPJ_comp);";
|
||||
cmd.Parameters.AddWithValue("@CNPJ_comp", CNPJ_comp);
|
||||
using var reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
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;
|
||||
}
|
||||
@ -69,18 +81,28 @@ namespace ComplianceNFs.Infrastructure.Repositories
|
||||
public IEnumerable<BuyingRecord> GetByCnpjAndMonth(string CNPJ_comp, int refMonth)
|
||||
{
|
||||
var results = new List<BuyingRecord>();
|
||||
using (var conn = new OleDbConnection(_connectionString))
|
||||
try
|
||||
{
|
||||
conn.Open();
|
||||
var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = $@"SELECT {BuyingRecordColumns} FROM Dados_TE WHERE ((Dados_TE.CNPJ_comp)=@CNPJ_comp) AND ((Dados_TE.Mes)=@MesRef);";
|
||||
cmd.Parameters.AddWithValue("@CNPJ_comp", CNPJ_comp);
|
||||
cmd.Parameters.AddWithValue("@MesRef", refMonth);
|
||||
using var reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
_logger?.LogInformation("Querying Access DB for CNPJ_comp={CNPJ_comp}, Mes={MesRef}", CNPJ_comp, refMonth);
|
||||
using (var conn = new OleDbConnection(_connectionString))
|
||||
{
|
||||
results.Add(MapBuyingRecord(reader));
|
||||
conn.Open();
|
||||
var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = $@"SELECT {BuyingRecordColumns} FROM Dados_TE WHERE ((Dados_TE.CNPJ_comp)=@CNPJ_comp) AND ((Dados_TE.Mes)=@MesRef);";
|
||||
cmd.Parameters.AddWithValue("@CNPJ_comp", CNPJ_comp);
|
||||
cmd.Parameters.AddWithValue("@MesRef", refMonth);
|
||||
using var reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
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,59 +5,85 @@ 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)
|
||||
{
|
||||
using var conn = new NpgsqlConnection(_connectionString);
|
||||
await conn.OpenAsync();
|
||||
var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"INSERT INTO attachments (
|
||||
filename, supplier_email, conversation_id, received_date, md5, cnpj_comp, cnpj_vend, mont_nf, prec_nf, valor_sem_imp, valor_com_imp, rs_comp, rs_vend, numero_nf, icms_nf, uf_comp, uf_vend, matched_cod_te, status, discrepancy, metadata
|
||||
) VALUES (
|
||||
@filename, @supplier_email, @conversation_id, @received_date, @md5, @cnpj_comp, @cnpj_vend, @mont_nf, @prec_nf, @valor_sem_imp, @valor_com_imp, @rs_comp, @rs_vend, @numero_nf, @icms_nf, @uf_comp, @uf_vend, @matched_cod_te, @status, @discrepancy, @metadata
|
||||
)";
|
||||
cmd.Parameters.AddWithValue("@filename", invoice.Filename);
|
||||
cmd.Parameters.AddWithValue("@supplier_email", invoice.SupplierEmail);
|
||||
cmd.Parameters.AddWithValue("@conversation_id", (object?)invoice.ConversationId ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@received_date", invoice.ReceivedDate);
|
||||
cmd.Parameters.AddWithValue("@md5", (object?)invoice.Md5 ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@cnpj_comp", (object?)invoice.CnpjComp ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@cnpj_vend", (object?)invoice.CnpjVend ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@mont_nf", (object?)invoice.MontNF ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@prec_nf", (object?)invoice.PrecNF ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@valor_sem_imp", (object?)invoice.ValorSemImpostos ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@valor_com_imp", (object?)invoice.ValorFinalComImpostos ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@rs_comp", (object?)invoice.RsComp ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@rs_vend", (object?)invoice.RsVend ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@numero_nf", (object?)invoice.NumeroNF ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@icms_nf", (object?)invoice.IcmsNF ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@uf_comp", (object?)invoice.UfComp ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@uf_vend", (object?)invoice.UfVend ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@matched_cod_te", (object?)invoice.MatchedCodTE ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@status", invoice.Status.ToString());
|
||||
cmd.Parameters.AddWithValue("@discrepancy", (object?)invoice.DiscrepancyNotes ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@metadata", Newtonsoft.Json.JsonConvert.SerializeObject(invoice));
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
try
|
||||
{
|
||||
using var conn = new NpgsqlConnection(_connectionString);
|
||||
await conn.OpenAsync();
|
||||
var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"INSERT INTO attachments (
|
||||
filename, supplier_email, conversation_id, received_date, md5, cnpj_comp, cnpj_vend, mont_nf, prec_nf, valor_sem_imp, valor_com_imp, rs_comp, rs_vend, numero_nf, icms_nf, uf_comp, uf_vend, matched_cod_te, status, discrepancy, metadata
|
||||
) VALUES (
|
||||
@filename, @supplier_email, @conversation_id, @received_date, @md5, @cnpj_comp, @cnpj_vend, @mont_nf, @prec_nf, @valor_sem_imp, @valor_com_imp, @rs_comp, @rs_vend, @numero_nf, @icms_nf, @uf_comp, @uf_vend, @matched_cod_te, @status, @discrepancy, @metadata
|
||||
)";
|
||||
cmd.Parameters.AddWithValue("@filename", invoice.Filename);
|
||||
cmd.Parameters.AddWithValue("@supplier_email", invoice.SupplierEmail);
|
||||
cmd.Parameters.AddWithValue("@conversation_id", (object?)invoice.ConversationId ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@received_date", invoice.ReceivedDate);
|
||||
cmd.Parameters.AddWithValue("@md5", (object?)invoice.Md5 ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@cnpj_comp", (object?)invoice.CnpjComp ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@cnpj_vend", (object?)invoice.CnpjVend ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@mont_nf", (object?)invoice.MontNF ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@prec_nf", (object?)invoice.PrecNF ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@valor_sem_imp", (object?)invoice.ValorSemImpostos ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@valor_com_imp", (object?)invoice.ValorFinalComImpostos ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@rs_comp", (object?)invoice.RsComp ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@rs_vend", (object?)invoice.RsVend ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@numero_nf", (object?)invoice.NumeroNF ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@icms_nf", (object?)invoice.IcmsNF ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@uf_comp", (object?)invoice.UfComp ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@uf_vend", (object?)invoice.UfVend ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@matched_cod_te", (object?)invoice.MatchedCodTE ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@status", invoice.Status.ToString());
|
||||
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)
|
||||
{
|
||||
using var conn = new NpgsqlConnection(_connectionString);
|
||||
await conn.OpenAsync();
|
||||
var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"UPDATE attachments SET matched_cod_te = @matched_cod_te, status = @status, discrepancy = @discrepancy WHERE invoice_id = @invoice_id";
|
||||
cmd.Parameters.AddWithValue("@matched_cod_te", matchedCodTE);
|
||||
cmd.Parameters.AddWithValue("@status", status.ToString());
|
||||
cmd.Parameters.AddWithValue("@discrepancy", (object?)notes ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@invoice_id", invoiceId);
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
try
|
||||
{
|
||||
using var conn = new NpgsqlConnection(_connectionString);
|
||||
await conn.OpenAsync();
|
||||
var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"UPDATE attachments SET matched_cod_te = @matched_cod_te, status = @status, discrepancy = @discrepancy WHERE invoice_id = @invoice_id";
|
||||
cmd.Parameters.AddWithValue("@matched_cod_te", matchedCodTE);
|
||||
cmd.Parameters.AddWithValue("@status", status.ToString());
|
||||
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,32 +21,48 @@ public class Worker(ILogger<Worker> logger,
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("Worker starting at: {time}", DateTimeOffset.Now);
|
||||
// Start mail ingestion (starts listening for new mail)
|
||||
await _ingestionService.IngestAsync();
|
||||
// Subscribe to new invoice events and orchestrate workflow
|
||||
if (_ingestionService is InvoiceIngestionService ingestionImpl)
|
||||
try
|
||||
{
|
||||
ingestionImpl.InvoiceProcessed += async invoice =>
|
||||
// Start mail ingestion (starts listening for new mail)
|
||||
await _ingestionService.IngestAsync();
|
||||
// Subscribe to new invoice events and orchestrate workflow
|
||||
if (_ingestionService is InvoiceIngestionService ingestionImpl)
|
||||
{
|
||||
// 1. Match invoice
|
||||
await _matchingService.MatchAsync(invoice);
|
||||
// 2. Compliance validation
|
||||
await _complianceService.ValidateAsync(invoice);
|
||||
// 3. Notify if needed
|
||||
if (invoice.Status == InvoiceStatus.TaxMismatch || invoice.Status == InvoiceStatus.VolumeMismatch || invoice.Status == InvoiceStatus.PriceMismatch)
|
||||
ingestionImpl.InvoiceProcessed += async invoice =>
|
||||
{
|
||||
await _notificationService.NotifyAsync(invoice, invoice.DiscrepancyNotes ?? "Discrepancy detected");
|
||||
}
|
||||
// 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);
|
||||
};
|
||||
try
|
||||
{
|
||||
// 1. Match invoice
|
||||
await _matchingService.MatchAsync(invoice);
|
||||
// 2. Compliance validation
|
||||
await _complianceService.ValidateAsync(invoice);
|
||||
// 3. Notify if needed
|
||||
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
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(1000, stoppingToken);
|
||||
}
|
||||
}
|
||||
// Keep the worker alive
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Task.Delay(1000, stoppingToken);
|
||||
_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