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 _logger; public InvoiceIngestionService(IMailListener mailListener, IAttachmentRepository attachmentRepository, IXmlParser xmlParser, IPdfParser pdfParser, ILogger 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? 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 _logger; public MatchingService(IAccessDbRepository accessDbRepository, ILogger 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(); 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); } } }