Giuliano Paschoalino 606b841435 feat: Add ComplianceNFs.Infrastructure.Tests project and implement unit tests for various services
- Added ComplianceNFs.Infrastructure.Tests project to the solution.
- Implemented unit tests for AccessDbRepository, ArchivingService, AttachmentRepository, InvoiceIngestionService, MailListener, MonitorViewModel, and Worker.
- Enhanced existing tests with additional assertions and mock setups.
- Updated TODOs and roadmap documentation to reflect changes in service implementations and testing coverage.
- Modified ComplianceNFs.sln to include new test project and adjust solution properties.
2025-06-18 16:34:44 -03:00

192 lines
8.6 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;
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;
public InvoiceIngestionService(IMailListener mailListener, IAttachmentRepository attachmentRepository, IXmlParser xmlParser, IPdfParser pdfParser)
{
_mailListener = mailListener;
_attachmentRepository = attachmentRepository;
_xmlParser = xmlParser;
_pdfParser = pdfParser;
_mailListener.NewMailReceived += OnNewMailReceived;
}
private async void OnNewMailReceived(MailMessage mail)
{
// 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);
if (InvoiceProcessed != null)
await InvoiceProcessed.Invoke(invoice);
}
}
}
// 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) : IMatchingService
{
private readonly IAccessDbRepository _accessDbRepository = accessDbRepository;
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)
{
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)";
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);
}
}
}