Giuliano Paschoalino e49192dac1 Refatoração e atualizações de dependências
- Modificado `InvoiceIngestionService` para usar `IServiceScopeFactory`, permitindo melhor gerenciamento do ciclo de vida de dependências.
- Atualizações de pacotes em diversos projetos (`ComplianceNFs.Core`, `ComplianceNFs.Infrastructure`, `ComplianceNFs.Service`, `ComplianceNFs.Monitor`, `ComplianceNFs.Infrastructure.Tests`).
- Alterado namespace de `AuditComplianceNFsTest.cs` para `ComplianceNFs.Infrastructure.Tests`.
- Refatorações no `AttachmentRepository` e `ComplianceNFsDbContext` para simplificar código e melhorar legibilidade.
- Atualizado teste `InvoiceIngestionServiceTests` para refletir mudanças no uso de escopos de serviço.
- Melhorias gerais na arquitetura e alinhamento com boas práticas.
2025-10-01 09:10:18 -03:00

214 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;
using Microsoft.Extensions.DependencyInjection;
namespace ComplianceNFs.Core.Application.Services
{
// Handles ingestion of invoices from mail attachments
public class InvoiceIngestionService : IInvoiceIngestionService
{
private readonly IMailListener _mailListener;
private readonly IXmlParser _xmlParser;
private readonly IPdfParser _pdfParser;
private readonly ILogger<InvoiceIngestionService> _logger;
private readonly IServiceScopeFactory _scopeFactory;
public InvoiceIngestionService(IMailListener mailListener, IServiceScopeFactory scopeFactory, IXmlParser xmlParser, IPdfParser pdfParser, ILogger<InvoiceIngestionService> logger)
{
_mailListener = mailListener;
_scopeFactory = scopeFactory;
_xmlParser = xmlParser;
_pdfParser = pdfParser;
_logger = logger;
_mailListener.NewMailReceived += OnNewMailReceived;
}
private async void OnNewMailReceived(MailMessage mail)
{
using var scope = _scopeFactory.CreateScope();
var _attachmentRepository = scope.ServiceProvider.GetRequiredService<IAttachmentRepository>();
_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(IAccessDbRepository accessDbRepository, ILogger<MatchingService> logger) : IMatchingService
{
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(null, nameof(invoice.CnpjComp)));
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)
{
_fileArchiver.ArchiveAsync(invoice);
return Task.CompletedTask;
}
}
}