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
This commit is contained in:
commit
690ab131aa
45
.gitignore
vendored
Normal file
45
.gitignore
vendored
Normal file
@ -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
|
||||
44
ComplianceNFs.Core/Application/ApplicationInterfaces.cs
Normal file
44
ComplianceNFs.Core/Application/ApplicationInterfaces.cs
Normal file
@ -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<EnergyInvoice> StatusUpdated;
|
||||
IEnumerable<EnergyInvoice> GetRecent(int count = 100);
|
||||
}
|
||||
}
|
||||
151
ComplianceNFs.Core/Application/Services/ApplicationServices.cs
Normal file
151
ComplianceNFs.Core/Application/Services/ApplicationServices.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
ComplianceNFs.Core/ComplianceNFs.Core.csproj
Normal file
12
ComplianceNFs.Core/ComplianceNFs.Core.csproj
Normal file
@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0-preview.4.25258.110" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
1
ComplianceNFs.Core/Entities/.keep
Normal file
1
ComplianceNFs.Core/Entities/.keep
Normal file
@ -0,0 +1 @@
|
||||
// Contains domain entities for ComplianceNFs
|
||||
17
ComplianceNFs.Core/Entities/BuyingRecord.cs
Normal file
17
ComplianceNFs.Core/Entities/BuyingRecord.cs
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
42
ComplianceNFs.Core/Entities/EnergyInvoice.cs
Normal file
42
ComplianceNFs.Core/Entities/EnergyInvoice.cs
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
18
ComplianceNFs.Core/Entities/ParsedInvoice.cs
Normal file
18
ComplianceNFs.Core/Entities/ParsedInvoice.cs
Normal file
@ -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; }
|
||||
}
|
||||
}
|
||||
1
ComplianceNFs.Core/Ports/.keep
Normal file
1
ComplianceNFs.Core/Ports/.keep
Normal file
@ -0,0 +1 @@
|
||||
// Contains domain interfaces (ports) for ComplianceNFs
|
||||
41
ComplianceNFs.Core/Ports/DomainInterfaces.cs
Normal file
41
ComplianceNFs.Core/Ports/DomainInterfaces.cs
Normal file
@ -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<MailMessage> NewMailReceived;
|
||||
}
|
||||
|
||||
public interface IXmlParser
|
||||
{
|
||||
Entities.ParsedInvoice Parse(Stream xmlStream);
|
||||
}
|
||||
|
||||
public interface IPdfParser
|
||||
{
|
||||
Entities.ParsedInvoice Parse(Stream pdfStream);
|
||||
}
|
||||
|
||||
public interface IAccessDbRepository
|
||||
{
|
||||
IEnumerable<Entities.BuyingRecord> GetByUnidade(string codSmartUnidade);
|
||||
IEnumerable<Entities.BuyingRecord> 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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="3.2.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.1" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ComplianceNFs.Infrastructure\ComplianceNFs.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
76
ComplianceNFs.Infrastructure.Tests/UnitTest1.cs
Normal file
76
ComplianceNFs.Infrastructure.Tests/UnitTest1.cs
Normal file
@ -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()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
33
ComplianceNFs.Infrastructure/Archiving/FileArchiver.cs
Normal file
33
ComplianceNFs.Infrastructure/Archiving/FileArchiver.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ComplianceNFs.Core\ComplianceNFs.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MailKit" Version="4.12.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0-preview.4.25258.110" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0-preview.4.25258.110" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Npgsql" Version="9.0.3" />
|
||||
<PackageReference Include="System.Data.OleDb" Version="10.0.0-preview.4.25258.110" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
57
ComplianceNFs.Infrastructure/Mail/MailListener.cs
Normal file
57
ComplianceNFs.Infrastructure/Mail/MailListener.cs
Normal file
@ -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<MailMessage> NewMailReceived;
|
||||
private readonly IConfiguration _config;
|
||||
private ImapClient _client;
|
||||
private readonly List<string> _allowList;
|
||||
private bool _listening;
|
||||
|
||||
public MailListener(IConfiguration config)
|
||||
{
|
||||
_config = config;
|
||||
_allowList = _config.GetSection("Mail:SupplierAllowList").Get<List<string>>() ?? new List<string>();
|
||||
}
|
||||
|
||||
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.
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
31
ComplianceNFs.Infrastructure/Parsers/PdfParser.cs
Normal file
31
ComplianceNFs.Infrastructure/Parsers/PdfParser.cs
Normal file
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
33
ComplianceNFs.Infrastructure/Parsers/XmlParser.cs
Normal file
33
ComplianceNFs.Infrastructure/Parsers/XmlParser.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<BuyingRecord> GetByUnidade(string codSmartUnidade)
|
||||
{
|
||||
var results = new List<BuyingRecord>();
|
||||
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<BuyingRecord> GetByUnidadeAndMonth(string codSmartUnidade, int month, int year)
|
||||
{
|
||||
var results = new List<BuyingRecord>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
ComplianceNFs.Monitor/App.xaml
Normal file
9
ComplianceNFs.Monitor/App.xaml
Normal file
@ -0,0 +1,9 @@
|
||||
<Application x:Class="ComplianceNFs.Monitor.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:ComplianceNFs.Monitor"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
13
ComplianceNFs.Monitor/App.xaml.cs
Normal file
13
ComplianceNFs.Monitor/App.xaml.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Windows;
|
||||
|
||||
namespace ComplianceNFs.Monitor;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
}
|
||||
|
||||
10
ComplianceNFs.Monitor/AssemblyInfo.cs
Normal file
10
ComplianceNFs.Monitor/AssemblyInfo.cs
Normal file
@ -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)
|
||||
)]
|
||||
19
ComplianceNFs.Monitor/ComplianceNFs.Monitor.csproj
Normal file
19
ComplianceNFs.Monitor/ComplianceNFs.Monitor.csproj
Normal file
@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0-preview.4.25258.110" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ComplianceNFs.Core\ComplianceNFs.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
25
ComplianceNFs.Monitor/MainWindow.xaml
Normal file
25
ComplianceNFs.Monitor/MainWindow.xaml
Normal file
@ -0,0 +1,25 @@
|
||||
<Window x:Class="ComplianceNFs.Monitor.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:ComplianceNFs.Monitor"
|
||||
mc:Ignorable="d"
|
||||
Title="ComplianceNFs Monitor" Height="450" Width="800">
|
||||
<Grid Margin="10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<ListView x:Name="LogList" Grid.Row="0" ItemsSource="{Binding RecentInvoices}">
|
||||
<ListView.View>
|
||||
<GridView>
|
||||
<GridViewColumn Header="Timestamp" DisplayMemberBinding="{Binding ReceivedDate}" Width="150"/>
|
||||
<GridViewColumn Header="Filename" DisplayMemberBinding="{Binding Filename}" Width="250"/>
|
||||
<GridViewColumn Header="Status" DisplayMemberBinding="{Binding Status}" Width="120"/>
|
||||
</GridView>
|
||||
</ListView.View>
|
||||
</ListView>
|
||||
<Button Grid.Row="1" Content="Force Scan" Command="{Binding ForceScanCommand}" Height="32" Width="120" HorizontalAlignment="Right" Margin="0,10,0,0"/>
|
||||
</Grid>
|
||||
</Window>
|
||||
24
ComplianceNFs.Monitor/MainWindow.xaml.cs
Normal file
24
ComplianceNFs.Monitor/MainWindow.xaml.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace ComplianceNFs.Monitor;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new MonitorViewModel();
|
||||
}
|
||||
}
|
||||
47
ComplianceNFs.Monitor/MonitorViewModel.cs
Normal file
47
ComplianceNFs.Monitor/MonitorViewModel.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows.Input;
|
||||
using ComplianceNFs.Core.Entities;
|
||||
|
||||
namespace ComplianceNFs.Monitor
|
||||
{
|
||||
public class MonitorViewModel : INotifyPropertyChanged
|
||||
{
|
||||
public ObservableCollection<EnergyInvoice> RecentInvoices { get; } = new();
|
||||
public ICommand ForceScanCommand { get; }
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public MonitorViewModel()
|
||||
{
|
||||
// TODO: Inject IInvoiceStatusStream and subscribe to updates
|
||||
ForceScanCommand = new RelayCommand(_ => ForceScan());
|
||||
}
|
||||
|
||||
private void ForceScan()
|
||||
{
|
||||
// TODO: Call service to force ingestion cycle
|
||||
}
|
||||
|
||||
protected void OnPropertyChanged([CallerMemberName] string name = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
}
|
||||
|
||||
public class RelayCommand : ICommand
|
||||
{
|
||||
private readonly Action<object> _execute;
|
||||
private readonly Func<object, bool> _canExecute;
|
||||
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
|
||||
{
|
||||
_execute = execute;
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter);
|
||||
public void Execute(object parameter) => _execute(parameter);
|
||||
public event EventHandler CanExecuteChanged { add { } remove { } }
|
||||
}
|
||||
}
|
||||
19
ComplianceNFs.Service/ComplianceNFs.Service.csproj
Normal file
19
ComplianceNFs.Service/ComplianceNFs.Service.csproj
Normal file
@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>dotnet-ComplianceNFs.Service-386f44bf-03c8-490c-96b9-3852d8c3d01a</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0-preview.4.25258.110" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-preview.4.25258.110" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ComplianceNFs.Core\ComplianceNFs.Core.csproj" />
|
||||
<ProjectReference Include="..\ComplianceNFs.Infrastructure\ComplianceNFs.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
36
ComplianceNFs.Service/Program.cs
Normal file
36
ComplianceNFs.Service/Program.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using ComplianceNFs.Service;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using ComplianceNFs.Core.Ports;
|
||||
using ComplianceNFs.Infrastructure.Repositories;
|
||||
using ComplianceNFs.Infrastructure.Mail;
|
||||
using ComplianceNFs.Infrastructure.Parsers;
|
||||
using ComplianceNFs.Infrastructure.Archiving;
|
||||
|
||||
IHost host = Host.CreateDefaultBuilder(args)
|
||||
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||
{
|
||||
config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
|
||||
})
|
||||
.ConfigureServices((context, services) =>
|
||||
{
|
||||
var config = context.Configuration;
|
||||
// Register infrastructure
|
||||
services.AddSingleton<IAccessDbRepository>(sp => new AccessDbRepository(config["AccessConnectionString"]));
|
||||
services.AddSingleton<IAttachmentRepository>(sp => new AttachmentRepository(config["PostgresConnectionString"]));
|
||||
services.AddSingleton<IMailListener, MailListener>();
|
||||
services.AddSingleton<IXmlParser, XmlParser>();
|
||||
services.AddSingleton<IPdfParser, PdfParser>();
|
||||
services.AddSingleton<IFileArchiver>(sp => new FileArchiver(config["ArchiveBasePath"]));
|
||||
// Register application services
|
||||
services.AddSingleton<ComplianceNFs.Core.Application.Services.InvoiceIngestionService>();
|
||||
services.AddSingleton<ComplianceNFs.Core.Application.Services.MatchingService>();
|
||||
services.AddSingleton<ComplianceNFs.Core.Application.Services.ComplianceService>();
|
||||
services.AddSingleton<ComplianceNFs.Core.Application.Services.NotificationService>();
|
||||
services.AddSingleton<ComplianceNFs.Core.Application.Services.ArchivingService>();
|
||||
services.AddHostedService<Worker>();
|
||||
})
|
||||
.Build();
|
||||
|
||||
host.Run();
|
||||
11
ComplianceNFs.Service/Properties/launchSettings.json
Normal file
11
ComplianceNFs.Service/Properties/launchSettings.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"profiles": {
|
||||
"ComplianceNFs.Service": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
ComplianceNFs.Service/Worker.cs
Normal file
20
ComplianceNFs.Service/Worker.cs
Normal file
@ -0,0 +1,20 @@
|
||||
namespace ComplianceNFs.Service;
|
||||
|
||||
public class Worker : BackgroundService
|
||||
{
|
||||
private readonly ILogger<Worker> _logger;
|
||||
|
||||
public Worker(ILogger<Worker> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
|
||||
await Task.Delay(1000, stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
ComplianceNFs.Service/appsettings.json
Normal file
24
ComplianceNFs.Service/appsettings.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"AccessConnectionString": "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=X:\\Back\\Controle NFs\\Dados.accdb;",
|
||||
"PostgresConnectionString": "Host=…;Port=5432;Database=…;Username=…;Password=…;",
|
||||
"Mail": {
|
||||
"Host": "outlook.office365.com",
|
||||
"Port": 993,
|
||||
"User": "service@yourcompany.com",
|
||||
"Password": "…",
|
||||
"SupplierAllowList": [ "faturamento@…", "nfe@…" ]
|
||||
},
|
||||
"PollingIntervalMinutes": 2,
|
||||
"Tolerances": {
|
||||
"VolumePercent": 1.0,
|
||||
"PricePercent": 0.5,
|
||||
"TaxPercent": 1.0
|
||||
},
|
||||
"ArchiveBasePath": "X:\\Back\\Controle NFs\\Fs\\",
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
40
ComplianceNFs.sln
Normal file
40
ComplianceNFs.sln
Normal file
@ -0,0 +1,40 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ComplianceNFs.Core", "ComplianceNFs.Core\ComplianceNFs.Core.csproj", "{43625B9E-D806-490C-A65C-DEC88263B563}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ComplianceNFs.Infrastructure", "ComplianceNFs.Infrastructure\ComplianceNFs.Infrastructure.csproj", "{86962CF6-BC05-4167-972B-1F24AC9348D4}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ComplianceNFs.Monitor", "ComplianceNFs.Monitor\ComplianceNFs.Monitor.csproj", "{03F7AF44-2394-410A-98ED-4973B7F66FF4}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ComplianceNFs.Service", "ComplianceNFs.Service\ComplianceNFs.Service.csproj", "{72BFBD25-0814-420D-8524-21448479A771}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{43625B9E-D806-490C-A65C-DEC88263B563}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{43625B9E-D806-490C-A65C-DEC88263B563}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{43625B9E-D806-490C-A65C-DEC88263B563}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{43625B9E-D806-490C-A65C-DEC88263B563}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{86962CF6-BC05-4167-972B-1F24AC9348D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{86962CF6-BC05-4167-972B-1F24AC9348D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{86962CF6-BC05-4167-972B-1F24AC9348D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{86962CF6-BC05-4167-972B-1F24AC9348D4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{03F7AF44-2394-410A-98ED-4973B7F66FF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{03F7AF44-2394-410A-98ED-4973B7F66FF4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{03F7AF44-2394-410A-98ED-4973B7F66FF4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{03F7AF44-2394-410A-98ED-4973B7F66FF4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{72BFBD25-0814-420D-8524-21448479A771}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{72BFBD25-0814-420D-8524-21448479A771}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{72BFBD25-0814-420D-8524-21448479A771}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{72BFBD25-0814-420D-8524-21448479A771}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
101
TODOs-and-Roadmap.md
Normal file
101
TODOs-and-Roadmap.md
Normal file
@ -0,0 +1,101 @@
|
||||
# ComplianceNFs TODOs and Roadmap
|
||||
|
||||
## TODOs in Codebase
|
||||
|
||||
### ComplianceNFs.Infrastructure/Archiving/FileArchiver.cs
|
||||
|
||||
- [x] FileArchiver.ArchiveAsync: Create subfolder for invoice.Status, move file
|
||||
|
||||
### ComplianceNFs.Infrastructure/Mail/MailListener.cs
|
||||
|
||||
- [x] MailListener.StartListening: Connect to mailbox, filter by allowlist, raise NewMailReceived
|
||||
|
||||
### ComplianceNFs.Infrastructure/Parsers/PdfParser.cs
|
||||
|
||||
- [x] PdfParser.Parse: Use iTextSharp or PdfSharp to extract text/numbers via regex
|
||||
|
||||
### ComplianceNFs.Infrastructure/Parsers/XmlParser.cs
|
||||
|
||||
- [x] XmlParser.Parse: Use System.Xml to parse known elements
|
||||
|
||||
### ComplianceNFs.Infrastructure/Repositories/AccessDbRepository.cs
|
||||
|
||||
- [x] AccessDbRepository.GetByUnidade: Implement actual query to Access DB
|
||||
|
||||
- [x] AccessDbRepository.GetByUnidadeAndMonth: Implement actual query to Access DB
|
||||
|
||||
### ComplianceNFs.Infrastructure/Repositories/AttachmentRepository.cs
|
||||
|
||||
- [x] AttachmentRepository.SaveRawAsync: Implement actual insert into Postgres attachments table
|
||||
|
||||
- [x] AttachmentRepository.UpdateMatchAsync: Implement actual update in Postgres
|
||||
|
||||
### ComplianceNFs.Core/Application/Services
|
||||
|
||||
- [x] InvoiceIngestionService: Implement ingestion logic and subscribe to NewMailReceived
|
||||
|
||||
- [x] MatchingService: Implement invoice matching logic
|
||||
|
||||
- [x] ComplianceService: Implement compliance validation logic
|
||||
|
||||
- [x] NotificationService: Implement notification logic for mismatches
|
||||
|
||||
- [x] ArchivingService: Implement archiving logic for final status
|
||||
|
||||
### ComplianceNFs.Monitor/MonitorViewModel.cs
|
||||
|
||||
- [ ] MonitorViewModel: Inject IInvoiceStatusStream and subscribe to updates
|
||||
|
||||
- [ ] MonitorViewModel.ForceScan: Call service to force ingestion cycle
|
||||
|
||||
### ComplianceNFs.Service/Program.cs
|
||||
|
||||
- [ ] Register application services (InvoiceIngestionService, MatchingService, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Roadmap to Finish the App
|
||||
|
||||
### 1. Infrastructure Layer
|
||||
|
||||
- [x] **FileArchiver**: Implement logic to create subfolders by `InvoiceStatus` and move files accordingly.
|
||||
- [x] **MailListener**: Use MailKit to connect to IMAP/Exchange, filter by allowlist, and raise `NewMailReceived` event.
|
||||
- [x] **PdfParser**: Integrate iTextSharp or PdfSharp, extract invoice data using regex.
|
||||
- [x] **XmlParser**: Use System.Xml to parse invoice XMLs and map to `ParsedInvoice`.
|
||||
- [x] **AccessDbRepository**: Implement queries to Access DB for buying records.
|
||||
- [x] **AttachmentRepository**: Implement Postgres insert/update for invoice attachments.
|
||||
|
||||
### 2. Application Layer
|
||||
|
||||
- [x] Implement and register:
|
||||
- InvoiceIngestionService
|
||||
- MatchingService
|
||||
- ComplianceService
|
||||
- NotificationService
|
||||
- ArchivingService
|
||||
|
||||
- [ ] Wire up these services in DI in `Program.cs`.
|
||||
|
||||
### 3. Service Host
|
||||
|
||||
- [ ] Ensure all services are registered and started in the Worker.
|
||||
- [ ] Implement polling and retry logic as per configuration.
|
||||
|
||||
### 4. WPF Monitor
|
||||
|
||||
- [ ] Inject and subscribe to `IInvoiceStatusStream` in `MonitorViewModel`.
|
||||
- [ ] Implement `ForceScan` to trigger ingestion from UI.
|
||||
- [ ] Bind UI to show recent invoice status updates.
|
||||
|
||||
### 5. Configuration & Testing
|
||||
|
||||
- [ ] Test all configuration values from `appsettings.json`.
|
||||
- [ ] Add error handling, logging, and validation.
|
||||
- [ ] Write integration tests for end-to-end flow.
|
||||
|
||||
---
|
||||
|
||||
**Tip:**
|
||||
|
||||
- Tackle infrastructure TODOs first, then application services, then UI and orchestration.
|
||||
- Use comments in code for any business logic or mapping details that need clarification.
|
||||
5
global.json
Normal file
5
global.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "9.0.101"
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user