- Created ComplianceNFs.Core project with domain entities and ports - Implemented BuyingRecord, EnergyInvoice, ParsedInvoice entities - Defined domain interfaces for mail listening, XML and PDF parsing, and repository access - Established ComplianceNFs.Infrastructure project with file archiving, mail listening, and data access implementations - Developed PDF and XML parsers for invoice data extraction - Set up AccessDbRepository and AttachmentRepository for data retrieval and storage - Created ComplianceNFs.Service project for background processing and service orchestration - Implemented Worker service for periodic tasks - Established ComplianceNFs.Monitor project with WPF UI for monitoring invoice statuses - Added ViewModel for UI data binding and command handling - Configured project files for .NET 9.0 and necessary package references - Created initial appsettings.json for configuration management - Added TODOs and roadmap for future development
152 lines
6.2 KiB
C#
152 lines
6.2 KiB
C#
using System;
|
|
using System.Threading.Tasks;
|
|
using System.Net.Mail;
|
|
using System.IO;
|
|
using ComplianceNFs.Core.Entities;
|
|
using ComplianceNFs.Core.Ports;
|
|
|
|
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 ParsedInvoice();
|
|
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
|
|
{
|
|
Filename = att.Name,
|
|
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,
|
|
CnpjComp = parsed.CnpjComp,
|
|
CnpjVend = parsed.CnpjVend,
|
|
MontNF = parsed.MontNF,
|
|
PrecNF = parsed.PrecNF,
|
|
ValorSemImpostos = parsed.ValorSemImpostos,
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
public Task IngestAsync()
|
|
{
|
|
_mailListener.StartListening();
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
// Handles matching logic for invoices
|
|
public class MatchingService : IMatchingService
|
|
{
|
|
private readonly IAccessDbRepository _accessDbRepository;
|
|
public MatchingService(IAccessDbRepository accessDbRepository)
|
|
{
|
|
_accessDbRepository = accessDbRepository;
|
|
}
|
|
public Task MatchAsync(EnergyInvoice invoice)
|
|
{
|
|
// Example: Primary match logic (simplified)
|
|
var records = _accessDbRepository.GetByUnidade(invoice.CnpjComp);
|
|
foreach (var record in records)
|
|
{
|
|
if (record.CnpjComp == invoice.CnpjComp && record.CnpjVend == invoice.CnpjVend)
|
|
{
|
|
var volMatch = Math.Abs(record.MontLO - invoice.MontNF) / record.MontLO <= 0.01m;
|
|
var priceMatch = Math.Abs(record.PrecLO - invoice.PrecNF) / record.PrecLO <= 0.005m;
|
|
if (volMatch && priceMatch)
|
|
{
|
|
invoice.MatchedCodTE = record.CodTE;
|
|
invoice.Status = InvoiceStatus.Matched;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// TODO: Add fallback and multi-invoice 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)
|
|
{
|
|
var impliedTax = invoice.ValorFinalComImpostos / (invoice.ValorSemImpostos == 0 ? 1 : invoice.ValorSemImpostos) - 1;
|
|
if (Math.Abs(impliedTax - invoice.IcmsNF) > 0.01m)
|
|
{
|
|
invoice.Status = InvoiceStatus.TaxMismatch;
|
|
invoice.DiscrepancyNotes = $"Tax mismatch: implied={impliedTax:P2}, expected={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 : IArchivingService
|
|
{
|
|
private readonly IFileArchiver _fileArchiver;
|
|
public ArchivingService(IFileArchiver fileArchiver)
|
|
{
|
|
_fileArchiver = fileArchiver;
|
|
}
|
|
public Task ArchiveAsync(EnergyInvoice invoice, byte[] rawFile)
|
|
{
|
|
return _fileArchiver.ArchiveAsync(invoice, rawFile);
|
|
}
|
|
}
|
|
}
|