- 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.
220 lines
10 KiB
C#
220 lines
10 KiB
C#
using System;
|
|
using System.Threading.Tasks;
|
|
using System.Net.Mail;
|
|
using System.IO;
|
|
using System.Collections.Generic;
|
|
using ComplianceNFs.Core.Entities;
|
|
using ComplianceNFs.Core.Ports;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace ComplianceNFs.Core.Application.Services
|
|
{
|
|
// Handles ingestion of invoices from mail attachments
|
|
public class InvoiceIngestionService : IInvoiceIngestionService
|
|
{
|
|
private readonly IMailListener _mailListener;
|
|
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, 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)
|
|
{
|
|
if (attachment is System.Net.Mail.Attachment att && att.Name != null)
|
|
{
|
|
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;
|
|
public Task IngestAsync()
|
|
{
|
|
_mailListener.StartListening();
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
// Handles matching logic for invoices
|
|
public class MatchingService : IMatchingService
|
|
{
|
|
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)
|
|
{
|
|
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 matching record found (including fallback sum logic)";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error matching invoice {InvoiceId}", invoice.InvoiceId);
|
|
throw;
|
|
}
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
// Handles compliance validation
|
|
public class ComplianceService : IComplianceService
|
|
{
|
|
public Task ValidateAsync(EnergyInvoice invoice)
|
|
{
|
|
// Example: Tax compliance check
|
|
if (invoice.Status == InvoiceStatus.Matched || invoice.Status == InvoiceStatus.FallbackMatched)
|
|
{
|
|
decimal? impliedTax = 1 - (invoice.ValorSemImpostos / (invoice.ValorFinalComImpostos == 0 ? 1 : invoice.ValorFinalComImpostos));
|
|
if (Math.Abs(impliedTax - invoice.IcmsNF ?? 0) > 0.01m)
|
|
{
|
|
invoice.Status = InvoiceStatus.TaxMismatch;
|
|
invoice.DiscrepancyNotes = $"Tax mismatch: imp={impliedTax:P2}, exp={invoice.IcmsNF:P2}";
|
|
}
|
|
else
|
|
{
|
|
invoice.Status = InvoiceStatus.Validated;
|
|
}
|
|
}
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
// Handles notifications for mismatches
|
|
public class NotificationService : INotificationService
|
|
{
|
|
public Task NotifyAsync(EnergyInvoice invoice, string message)
|
|
{
|
|
// Example: Send notification (placeholder)
|
|
// In production, use SMTP or other email service
|
|
Console.WriteLine($"Notify {invoice.SupplierEmail}: {message}");
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
// Handles archiving of files
|
|
public class ArchivingService(IFileArchiver fileArchiver) : IArchivingService
|
|
{
|
|
private readonly IFileArchiver _fileArchiver = fileArchiver;
|
|
|
|
public Task ArchiveAsync(EnergyInvoice invoice, byte[] rawFile)
|
|
{
|
|
return _fileArchiver.ArchiveAsync(invoice, rawFile);
|
|
}
|
|
}
|
|
}
|