commit 690ab131aa49b292da3265f7a99a4af3ef0b7f79 Author: Giuliano Paschoalino Date: Thu Jun 5 14:47:28 2025 -0300 feat: Initialize ComplianceNFs project structure with core, infrastructure, service, and monitor components - 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1802cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# .NET build artifacts +bin/ +obj/ +*.user +*.suo +*.userosscache +*.sln.docstates + +# VS Code +.vscode/ + +# Test results +TestResults/ + +# Build logs +*.log + +# OS generated files +Thumbs.db +.DS_Store + +# Rider/JetBrains +.idea/ +*.sln.iml + +# Local settings +appsettings.Development.json + +# Backup/history +.history/ + +# Global.json lock +project.lock.json + +# Others +*.ncrunch* +*.localhistory +*.vs/ + +# Ignore all Debug/Release folders +**/bin/ +**/obj/ +**/Debug/ +**/Release/ +.fake \ No newline at end of file diff --git a/ComplianceNFs.Core/Application/ApplicationInterfaces.cs b/ComplianceNFs.Core/Application/ApplicationInterfaces.cs new file mode 100644 index 0000000..066cd33 --- /dev/null +++ b/ComplianceNFs.Core/Application/ApplicationInterfaces.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using ComplianceNFs.Core.Entities; + +namespace ComplianceNFs.Core.Application +{ + // Handles ingestion of invoices from mail attachments + public interface IInvoiceIngestionService + { + Task IngestAsync(); + } + + // Handles matching logic for invoices + public interface IMatchingService + { + Task MatchAsync(EnergyInvoice invoice); + } + + // Handles compliance validation + public interface IComplianceService + { + Task ValidateAsync(EnergyInvoice invoice); + } + + // Handles notifications for mismatches + public interface INotificationService + { + Task NotifyAsync(EnergyInvoice invoice, string message); + } + + // Handles archiving of files + public interface IArchivingService + { + Task ArchiveAsync(EnergyInvoice invoice, byte[] rawFile); + } + + // For streaming invoice status updates (for Monitor) + public interface IInvoiceStatusStream + { + event Action StatusUpdated; + IEnumerable GetRecent(int count = 100); + } +} diff --git a/ComplianceNFs.Core/Application/Services/ApplicationServices.cs b/ComplianceNFs.Core/Application/Services/ApplicationServices.cs new file mode 100644 index 0000000..77ba8b4 --- /dev/null +++ b/ComplianceNFs.Core/Application/Services/ApplicationServices.cs @@ -0,0 +1,151 @@ +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); + } + } +} diff --git a/ComplianceNFs.Core/ComplianceNFs.Core.csproj b/ComplianceNFs.Core/ComplianceNFs.Core.csproj new file mode 100644 index 0000000..85c043b --- /dev/null +++ b/ComplianceNFs.Core/ComplianceNFs.Core.csproj @@ -0,0 +1,12 @@ + + + + net9.0 + enable + + + + + + + diff --git a/ComplianceNFs.Core/Entities/.keep b/ComplianceNFs.Core/Entities/.keep new file mode 100644 index 0000000..b720bb6 --- /dev/null +++ b/ComplianceNFs.Core/Entities/.keep @@ -0,0 +1 @@ +// Contains domain entities for ComplianceNFs diff --git a/ComplianceNFs.Core/Entities/BuyingRecord.cs b/ComplianceNFs.Core/Entities/BuyingRecord.cs new file mode 100644 index 0000000..6e58567 --- /dev/null +++ b/ComplianceNFs.Core/Entities/BuyingRecord.cs @@ -0,0 +1,17 @@ +using System; + +namespace ComplianceNFs.Core.Entities +{ + public class BuyingRecord + { + public int CodTE { get; set; } + public string CodSmartUnidade { get; set; } + public int Mes { get; set; } // month as integer + public int Ano { get; set; } // year as integer + public string CnpjComp { get; set; } + public string CnpjVend { get; set; } + public decimal MontLO { get; set; } // expected volume + public decimal PrecLO { get; set; } // expected unit price + // … other client fields omitted + } +} diff --git a/ComplianceNFs.Core/Entities/EnergyInvoice.cs b/ComplianceNFs.Core/Entities/EnergyInvoice.cs new file mode 100644 index 0000000..322ac8d --- /dev/null +++ b/ComplianceNFs.Core/Entities/EnergyInvoice.cs @@ -0,0 +1,42 @@ +using System; + +namespace ComplianceNFs.Core.Entities +{ + public class EnergyInvoice + { + public int InvoiceId { get; set; } // PK + public string Filename { get; set; } + public string SupplierEmail { get; set; } + public string ConversationId { get; set; } + public DateTime ReceivedDate { get; set; } + public string Md5 { get; set; } + public string CnpjComp { get; set; } + public string CnpjVend { get; set; } + public decimal MontNF { get; set; } + public decimal PrecNF { get; set; } + public decimal ValorSemImpostos { get; set; } + public decimal ValorFinalComImpostos { get; set; } + public string RsComp { get; set; } + public string RsVend { get; set; } + public string NumeroNF { get; set; } + public decimal IcmsNF { get; set; } + public string UfComp { get; set; } + public string UfVend { get; set; } + public int? MatchedCodTE { get; set; } // FK to BuyingRecord + public InvoiceStatus Status { get; set; } + public string DiscrepancyNotes { get; set; } + } + + public enum InvoiceStatus + { + Pending, + Matched, + FallbackMatched, + VolumeMismatch, + PriceMismatch, + TaxMismatch, + NotFound, + Error, + Validated + } +} diff --git a/ComplianceNFs.Core/Entities/ParsedInvoice.cs b/ComplianceNFs.Core/Entities/ParsedInvoice.cs new file mode 100644 index 0000000..2f67b8a --- /dev/null +++ b/ComplianceNFs.Core/Entities/ParsedInvoice.cs @@ -0,0 +1,18 @@ +namespace ComplianceNFs.Core.Entities +{ + public class ParsedInvoice + { + public string CnpjComp { get; set; } + public string CnpjVend { get; set; } + public decimal MontNF { get; set; } + public decimal PrecNF { get; set; } + public decimal ValorSemImpostos { get; set; } + public decimal ValorFinalComImpostos { get; set; } + public string RsComp { get; set; } + public string RsVend { get; set; } + public string NumeroNF { get; set; } + public decimal IcmsNF { get; set; } + public string UfComp { get; set; } + public string UfVend { get; set; } + } +} diff --git a/ComplianceNFs.Core/Ports/.keep b/ComplianceNFs.Core/Ports/.keep new file mode 100644 index 0000000..d301b73 --- /dev/null +++ b/ComplianceNFs.Core/Ports/.keep @@ -0,0 +1 @@ +// Contains domain interfaces (ports) for ComplianceNFs diff --git a/ComplianceNFs.Core/Ports/DomainInterfaces.cs b/ComplianceNFs.Core/Ports/DomainInterfaces.cs new file mode 100644 index 0000000..b42cb9b --- /dev/null +++ b/ComplianceNFs.Core/Ports/DomainInterfaces.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Mail; +using System.Threading.Tasks; + +namespace ComplianceNFs.Core.Ports +{ + public interface IMailListener + { + void StartListening(); + event Action NewMailReceived; + } + + public interface IXmlParser + { + Entities.ParsedInvoice Parse(Stream xmlStream); + } + + public interface IPdfParser + { + Entities.ParsedInvoice Parse(Stream pdfStream); + } + + public interface IAccessDbRepository + { + IEnumerable GetByUnidade(string codSmartUnidade); + IEnumerable GetByUnidadeAndMonth(string codSmartUnidade, int month, int year); + } + + public interface IAttachmentRepository + { + Task SaveRawAsync(Entities.EnergyInvoice invoice); + Task UpdateMatchAsync(int invoiceId, int matchedCodTE, Entities.InvoiceStatus status, string notes); + } + + public interface IFileArchiver + { + Task ArchiveAsync(Entities.EnergyInvoice invoice, byte[] rawFile); + } +} diff --git a/ComplianceNFs.Infrastructure.Tests/ComplianceNFs.Infrastructure.Tests.csproj b/ComplianceNFs.Infrastructure.Tests/ComplianceNFs.Infrastructure.Tests.csproj new file mode 100644 index 0000000..0ed2a0c --- /dev/null +++ b/ComplianceNFs.Infrastructure.Tests/ComplianceNFs.Infrastructure.Tests.csproj @@ -0,0 +1,27 @@ + + + + net9.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/ComplianceNFs.Infrastructure.Tests/UnitTest1.cs b/ComplianceNFs.Infrastructure.Tests/UnitTest1.cs new file mode 100644 index 0000000..26cf183 --- /dev/null +++ b/ComplianceNFs.Infrastructure.Tests/UnitTest1.cs @@ -0,0 +1,76 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using ComplianceNFs.Core.Entities; +using ComplianceNFs.Infrastructure.Archiving; +using Xunit; + +namespace ComplianceNFs.Infrastructure.Tests +{ + public class FileArchiverTests : IDisposable + { + private readonly string _testBasePath; + public FileArchiverTests() + { + _testBasePath = Path.Combine(Path.GetTempPath(), "ComplianceNFsTestArchive"); + if (Directory.Exists(_testBasePath)) + Directory.Delete(_testBasePath, true); + } + + [Fact] + public async Task ArchiveAsync_CreatesFolderAndWritesFile() + { + var archiver = new FileArchiver(_testBasePath); + var invoice = new EnergyInvoice + { + Filename = "testfile.txt", + Status = InvoiceStatus.Validated + }; + var data = new byte[] { 1, 2, 3, 4 }; + + await archiver.ArchiveAsync(invoice, data); + + var expectedFolder = Path.Combine(_testBasePath, "Validated"); + var expectedFile = Path.Combine(expectedFolder, "testfile.txt"); + Assert.True(Directory.Exists(expectedFolder)); + Assert.True(File.Exists(expectedFile)); + var fileData = await File.ReadAllBytesAsync(expectedFile); + Assert.Equal(data, fileData); + } + + [Fact] + public async Task ArchiveAsync_OverwritesExistingFile() + { + var archiver = new FileArchiver(_testBasePath); + var invoice = new EnergyInvoice + { + Filename = "testfile.txt", + Status = InvoiceStatus.Validated + }; + var data1 = new byte[] { 1, 2, 3 }; + var data2 = new byte[] { 9, 8, 7 }; + + await archiver.ArchiveAsync(invoice, data1); + await archiver.ArchiveAsync(invoice, data2); + + var expectedFile = Path.Combine(_testBasePath, "Validated", "testfile.txt"); + var fileData = await File.ReadAllBytesAsync(expectedFile); + Assert.Equal(data2, fileData); + } + + public void Dispose() + { + if (Directory.Exists(_testBasePath)) + Directory.Delete(_testBasePath, true); + } + } + + public class UnitTest1 + { + [Fact] + public void Test1() + { + + } + } +} \ No newline at end of file diff --git a/ComplianceNFs.Infrastructure/Archiving/FileArchiver.cs b/ComplianceNFs.Infrastructure/Archiving/FileArchiver.cs new file mode 100644 index 0000000..cd4ffd1 --- /dev/null +++ b/ComplianceNFs.Infrastructure/Archiving/FileArchiver.cs @@ -0,0 +1,33 @@ +using System.IO; +using System.Threading.Tasks; +using ComplianceNFs.Core.Entities; +using ComplianceNFs.Core.Ports; +using System; +using System.IO; + +namespace ComplianceNFs.Infrastructure.Archiving +{ + // Moves files to archive folders by status + public class FileArchiver : IFileArchiver + { + private readonly string _basePath; + public FileArchiver(string basePath) + { + _basePath = basePath; + } + + public async Task ArchiveAsync(EnergyInvoice invoice, byte[] rawFile) + { + // Create subfolder for invoice.Status + var statusFolder = Path.Combine(_basePath, invoice.Status.ToString()); + if (!Directory.Exists(statusFolder)) + { + Directory.CreateDirectory(statusFolder); + } + // Build file path + var filePath = Path.Combine(statusFolder, invoice.Filename); + // Write file (overwrite if exists) + await File.WriteAllBytesAsync(filePath, rawFile); + } + } +} diff --git a/ComplianceNFs.Infrastructure/ComplianceNFs.Infrastructure.csproj b/ComplianceNFs.Infrastructure/ComplianceNFs.Infrastructure.csproj new file mode 100644 index 0000000..3eb4b3e --- /dev/null +++ b/ComplianceNFs.Infrastructure/ComplianceNFs.Infrastructure.csproj @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + net9.0 + enable + enable + + + diff --git a/ComplianceNFs.Infrastructure/Mail/MailListener.cs b/ComplianceNFs.Infrastructure/Mail/MailListener.cs new file mode 100644 index 0000000..247e1f3 --- /dev/null +++ b/ComplianceNFs.Infrastructure/Mail/MailListener.cs @@ -0,0 +1,57 @@ +using System; +using System.Net.Mail; +using System.Collections.Generic; +using System.Threading.Tasks; +using ComplianceNFs.Core.Ports; +using Microsoft.Extensions.Configuration; +using MailKit.Net.Imap; +using MailKit.Security; +using MimeKit; + +namespace ComplianceNFs.Infrastructure.Mail +{ + public class MailListener : IMailListener + { + public event Action NewMailReceived; + private readonly IConfiguration _config; + private ImapClient _client; + private readonly List _allowList; + private bool _listening; + + public MailListener(IConfiguration config) + { + _config = config; + _allowList = _config.GetSection("Mail:SupplierAllowList").Get>() ?? new List(); + } + + public void StartListening() + { + if (_listening) return; + _listening = true; + Task.Run(async () => + { + _client = new ImapClient(); + await _client.ConnectAsync(_config["Mail:Host"], int.Parse(_config["Mail:Port"] ?? "0"), SecureSocketOptions.SslOnConnect); + await _client.AuthenticateAsync(_config["Mail:User"], _config["Mail:Password"]); + await _client.Inbox.OpenAsync(MailKit.FolderAccess.ReadOnly); + foreach (var uid in await _client.Inbox.SearchAsync(MailKit.Search.SearchQuery.NotSeen)) + { + var message = await _client.Inbox.GetMessageAsync(uid); + var senderAddress = message.From.Mailboxes.FirstOrDefault()?.Address; + + if (!string.IsNullOrEmpty(senderAddress) && _allowList.Contains(senderAddress)) + { + var mailMsg = new MailMessage + { + From = new MailAddress(senderAddress), // Fix for CS1922 and CS8670 + Subject = message.Subject, + Body = message.TextBody + }; + NewMailReceived?.Invoke(mailMsg); + } + } + // For demo: not a real polling loop. Add timer/polling for production. + }); + } + } +} diff --git a/ComplianceNFs.Infrastructure/Parsers/PdfParser.cs b/ComplianceNFs.Infrastructure/Parsers/PdfParser.cs new file mode 100644 index 0000000..24a161d --- /dev/null +++ b/ComplianceNFs.Infrastructure/Parsers/PdfParser.cs @@ -0,0 +1,31 @@ +using System.IO; +using System.Text.RegularExpressions; +using ComplianceNFs.Core.Entities; +using ComplianceNFs.Core.Ports; + +namespace ComplianceNFs.Infrastructure.Parsers +{ + public class PdfParser : IPdfParser + { + public ParsedInvoice Parse(Stream pdfStream) + { + // Minimal demo: just read bytes as text (replace with real PDF parsing in production) + using var ms = new MemoryStream(); + pdfStream.CopyTo(ms); + var text = System.Text.Encoding.UTF8.GetString(ms.ToArray()); + // Example: extract CNPJ and values using regex (replace with real patterns) + var cnpjComp = Regex.Match(text, @"CNPJComp: (\d{14})").Groups[1].Value; + var cnpjVend = Regex.Match(text, @"CNPJVend: (\d{14})").Groups[1].Value; + var montNF = decimal.TryParse(Regex.Match(text, @"MontNF: ([\d,.]+)").Groups[1].Value, out var m) ? m : 0; + var precNF = decimal.TryParse(Regex.Match(text, @"PrecNF: ([\d,.]+)").Groups[1].Value, out var p) ? p : 0; + return new ParsedInvoice + { + CnpjComp = cnpjComp, + CnpjVend = cnpjVend, + MontNF = montNF, + PrecNF = precNF + // ...fill other fields as needed + }; + } + } +} diff --git a/ComplianceNFs.Infrastructure/Parsers/XmlParser.cs b/ComplianceNFs.Infrastructure/Parsers/XmlParser.cs new file mode 100644 index 0000000..67cce20 --- /dev/null +++ b/ComplianceNFs.Infrastructure/Parsers/XmlParser.cs @@ -0,0 +1,33 @@ +using System.IO; +using ComplianceNFs.Core.Entities; +using ComplianceNFs.Core.Ports; + +namespace ComplianceNFs.Infrastructure.Parsers +{ + // Placeholder: fill in actual XML parsing logic + public class XmlParser : IXmlParser + { + public ParsedInvoice Parse(Stream xmlStream) + { + // Use System.Xml to parse known elements + var doc = new System.Xml.XmlDocument(); + doc.Load(xmlStream); + var invoice = new ParsedInvoice + { + CnpjComp = doc.SelectSingleNode("//CNPJComp")?.InnerText, + CnpjVend = doc.SelectSingleNode("//CNPJVend")?.InnerText, + MontNF = decimal.TryParse(doc.SelectSingleNode("//MontNF")?.InnerText, out var mont) ? mont : 0, + PrecNF = decimal.TryParse(doc.SelectSingleNode("//PrecNF")?.InnerText, out var prec) ? prec : 0, + ValorSemImpostos = decimal.TryParse(doc.SelectSingleNode("//ValorSemImpostos")?.InnerText, out var vsi) ? vsi : 0, + ValorFinalComImpostos = decimal.TryParse(doc.SelectSingleNode("//ValorFinalComImpostos")?.InnerText, out var vfi) ? vfi : 0, + RsComp = doc.SelectSingleNode("//RsComp")?.InnerText, + RsVend = doc.SelectSingleNode("//RsVend")?.InnerText, + NumeroNF = doc.SelectSingleNode("//NumeroNF")?.InnerText, + IcmsNF = decimal.TryParse(doc.SelectSingleNode("//IcmsNF")?.InnerText, out var icms) ? icms : 0, + UfComp = doc.SelectSingleNode("//UfComp")?.InnerText, + UfVend = doc.SelectSingleNode("//UfVend")?.InnerText + }; + return invoice; + } + } +} diff --git a/ComplianceNFs.Infrastructure/Repositories/AccessDbRepository.cs b/ComplianceNFs.Infrastructure/Repositories/AccessDbRepository.cs new file mode 100644 index 0000000..5818689 --- /dev/null +++ b/ComplianceNFs.Infrastructure/Repositories/AccessDbRepository.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.OleDb; +using ComplianceNFs.Core.Entities; +using ComplianceNFs.Core.Ports; + +namespace ComplianceNFs.Infrastructure.Repositories +{ + // Placeholder: fill in actual SQL and mapping logic + public class AccessDbRepository : IAccessDbRepository + { + private readonly string _connectionString; + public AccessDbRepository(string connectionString) + { + _connectionString = connectionString; + } + + public IEnumerable GetByUnidade(string codSmartUnidade) + { + var results = new List(); + using (var conn = new OleDbConnection(_connectionString)) + { + conn.Open(); + var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT Cod_TE, Cod_Smart_unidade, Mes, Ano, CNPJ_comp, CNPJ_vend, Mont_LO, Prec_LO FROM Dados_TE WHERE Cod_Smart_unidade = ?"; + cmd.Parameters.AddWithValue("@CodSmartUnidade", codSmartUnidade); + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + results.Add(new BuyingRecord + { + CodTE = reader.GetInt32(0), + CodSmartUnidade = reader.GetString(1), + Mes = reader.GetInt32(2), + Ano = reader.GetInt32(3), + CnpjComp = reader.GetString(4), + CnpjVend = reader.GetString(5), + MontLO = reader.GetDecimal(6), + PrecLO = reader.GetDecimal(7) + }); + } + } + } + return results; + } + + public IEnumerable GetByUnidadeAndMonth(string codSmartUnidade, int month, int year) + { + var results = new List(); + using (var conn = new OleDbConnection(_connectionString)) + { + conn.Open(); + var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT Cod_TE, Cod_Smart_unidade, Mes, Ano, CNPJ_comp, CNPJ_vend, Mont_LO, Prec_LO FROM Dados_TE WHERE Cod_Smart_unidade = ? AND Mes = ? AND Ano = ?"; + cmd.Parameters.AddWithValue("@CodSmartUnidade", codSmartUnidade); + cmd.Parameters.AddWithValue("@Mes", month); + cmd.Parameters.AddWithValue("@Ano", year); + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + results.Add(new BuyingRecord + { + CodTE = reader.GetInt32(0), + CodSmartUnidade = reader.GetString(1), + Mes = reader.GetInt32(2), + Ano = reader.GetInt32(3), + CnpjComp = reader.GetString(4), + CnpjVend = reader.GetString(5), + MontLO = reader.GetDecimal(6), + PrecLO = reader.GetDecimal(7) + }); + } + } + } + return results; + } + } +} diff --git a/ComplianceNFs.Infrastructure/Repositories/AttachmentRepository.cs b/ComplianceNFs.Infrastructure/Repositories/AttachmentRepository.cs new file mode 100644 index 0000000..78f7ddd --- /dev/null +++ b/ComplianceNFs.Infrastructure/Repositories/AttachmentRepository.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading.Tasks; +using ComplianceNFs.Core.Entities; +using ComplianceNFs.Core.Ports; +using Npgsql; +using Newtonsoft.Json; + +namespace ComplianceNFs.Infrastructure.Repositories +{ + // Placeholder: fill in actual SQL and mapping logic + public class AttachmentRepository : IAttachmentRepository + { + private readonly string _connectionString; + public AttachmentRepository(string connectionString) + { + _connectionString = connectionString; + } + + public async Task SaveRawAsync(EnergyInvoice invoice) + { + using (var conn = new NpgsqlConnection(_connectionString)) + { + await conn.OpenAsync(); + var cmd = conn.CreateCommand(); + cmd.CommandText = @"INSERT INTO attachments ( + filename, supplier_email, conversation_id, received_date, md5, cnpj_comp, cnpj_vend, mont_nf, prec_nf, valor_sem_imp, valor_com_imp, rs_comp, rs_vend, numero_nf, icms_nf, uf_comp, uf_vend, matched_cod_te, status, discrepancy, metadata + ) VALUES ( + @filename, @supplier_email, @conversation_id, @received_date, @md5, @cnpj_comp, @cnpj_vend, @mont_nf, @prec_nf, @valor_sem_imp, @valor_com_imp, @rs_comp, @rs_vend, @numero_nf, @icms_nf, @uf_comp, @uf_vend, @matched_cod_te, @status, @discrepancy, @metadata + )"; + cmd.Parameters.AddWithValue("@filename", invoice.Filename); + cmd.Parameters.AddWithValue("@supplier_email", invoice.SupplierEmail); + cmd.Parameters.AddWithValue("@conversation_id", (object?)invoice.ConversationId ?? DBNull.Value); + cmd.Parameters.AddWithValue("@received_date", invoice.ReceivedDate); + cmd.Parameters.AddWithValue("@md5", invoice.Md5); + cmd.Parameters.AddWithValue("@cnpj_comp", (object?)invoice.CnpjComp ?? DBNull.Value); + cmd.Parameters.AddWithValue("@cnpj_vend", (object?)invoice.CnpjVend ?? DBNull.Value); + cmd.Parameters.AddWithValue("@mont_nf", invoice.MontNF); + cmd.Parameters.AddWithValue("@prec_nf", invoice.PrecNF); + cmd.Parameters.AddWithValue("@valor_sem_imp", invoice.ValorSemImpostos); + cmd.Parameters.AddWithValue("@valor_com_imp", invoice.ValorFinalComImpostos); + cmd.Parameters.AddWithValue("@rs_comp", (object?)invoice.RsComp ?? DBNull.Value); + cmd.Parameters.AddWithValue("@rs_vend", (object?)invoice.RsVend ?? DBNull.Value); + cmd.Parameters.AddWithValue("@numero_nf", (object?)invoice.NumeroNF ?? DBNull.Value); + cmd.Parameters.AddWithValue("@icms_nf", invoice.IcmsNF); + cmd.Parameters.AddWithValue("@uf_comp", (object?)invoice.UfComp ?? DBNull.Value); + cmd.Parameters.AddWithValue("@uf_vend", (object?)invoice.UfVend ?? DBNull.Value); + cmd.Parameters.AddWithValue("@matched_cod_te", (object?)invoice.MatchedCodTE ?? DBNull.Value); + cmd.Parameters.AddWithValue("@status", invoice.Status.ToString()); + cmd.Parameters.AddWithValue("@discrepancy", (object?)invoice.DiscrepancyNotes ?? DBNull.Value); + cmd.Parameters.AddWithValue("@metadata", Newtonsoft.Json.JsonConvert.SerializeObject(invoice)); + await cmd.ExecuteNonQueryAsync(); + } + } + + public async Task UpdateMatchAsync(int invoiceId, int matchedCodTE, InvoiceStatus status, string notes) + { + using (var conn = new NpgsqlConnection(_connectionString)) + { + await conn.OpenAsync(); + var cmd = conn.CreateCommand(); + cmd.CommandText = @"UPDATE attachments SET matched_cod_te = @matched_cod_te, status = @status, discrepancy = @discrepancy WHERE invoice_id = @invoice_id"; + cmd.Parameters.AddWithValue("@matched_cod_te", matchedCodTE); + cmd.Parameters.AddWithValue("@status", status.ToString()); + cmd.Parameters.AddWithValue("@discrepancy", (object?)notes ?? DBNull.Value); + cmd.Parameters.AddWithValue("@invoice_id", invoiceId); + await cmd.ExecuteNonQueryAsync(); + } + } + } +} diff --git a/ComplianceNFs.Monitor/App.xaml b/ComplianceNFs.Monitor/App.xaml new file mode 100644 index 0000000..33c9570 --- /dev/null +++ b/ComplianceNFs.Monitor/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/ComplianceNFs.Monitor/App.xaml.cs b/ComplianceNFs.Monitor/App.xaml.cs new file mode 100644 index 0000000..a9e7300 --- /dev/null +++ b/ComplianceNFs.Monitor/App.xaml.cs @@ -0,0 +1,13 @@ +using System.Configuration; +using System.Data; +using System.Windows; + +namespace ComplianceNFs.Monitor; + +/// +/// Interaction logic for App.xaml +/// +public partial class App : Application +{ +} + diff --git a/ComplianceNFs.Monitor/AssemblyInfo.cs b/ComplianceNFs.Monitor/AssemblyInfo.cs new file mode 100644 index 0000000..cc29e7f --- /dev/null +++ b/ComplianceNFs.Monitor/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly:ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/ComplianceNFs.Monitor/ComplianceNFs.Monitor.csproj b/ComplianceNFs.Monitor/ComplianceNFs.Monitor.csproj new file mode 100644 index 0000000..6a5e122 --- /dev/null +++ b/ComplianceNFs.Monitor/ComplianceNFs.Monitor.csproj @@ -0,0 +1,19 @@ + + + + + + + + + + + + WinExe + net9.0-windows + enable + enable + true + + + diff --git a/ComplianceNFs.Monitor/MainWindow.xaml b/ComplianceNFs.Monitor/MainWindow.xaml new file mode 100644 index 0000000..ceed9e3 --- /dev/null +++ b/ComplianceNFs.Monitor/MainWindow.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + +