feat: Add ComplianceNFs.Infrastructure.Tests project and implement unit tests for various services
- Added ComplianceNFs.Infrastructure.Tests project to the solution. - Implemented unit tests for AccessDbRepository, ArchivingService, AttachmentRepository, InvoiceIngestionService, MailListener, MonitorViewModel, and Worker. - Enhanced existing tests with additional assertions and mock setups. - Updated TODOs and roadmap documentation to reflect changes in service implementations and testing coverage. - Modified ComplianceNFs.sln to include new test project and adjust solution properties.
This commit is contained in:
parent
690ab131aa
commit
606b841435
@ -2,8 +2,11 @@ using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Net.Mail;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using ComplianceNFs.Core.Entities;
|
||||
using ComplianceNFs.Core.Ports;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
namespace ComplianceNFs.Core.Application.Services
|
||||
{
|
||||
@ -14,6 +17,7 @@ namespace ComplianceNFs.Core.Application.Services
|
||||
private readonly IAttachmentRepository _attachmentRepository;
|
||||
private readonly IXmlParser _xmlParser;
|
||||
private readonly IPdfParser _pdfParser;
|
||||
|
||||
public InvoiceIngestionService(IMailListener mailListener, IAttachmentRepository attachmentRepository, IXmlParser xmlParser, IPdfParser pdfParser)
|
||||
{
|
||||
_mailListener = mailListener;
|
||||
@ -32,7 +36,7 @@ namespace ComplianceNFs.Core.Application.Services
|
||||
using var stream = new MemoryStream();
|
||||
att.ContentStream.CopyTo(stream);
|
||||
stream.Position = 0;
|
||||
ParsedInvoice parsed = new ParsedInvoice();
|
||||
ParsedInvoice parsed = new();
|
||||
if (att.Name.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
|
||||
parsed = _xmlParser.Parse(stream);
|
||||
else if (att.Name.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
|
||||
@ -41,14 +45,16 @@ namespace ComplianceNFs.Core.Application.Services
|
||||
continue;
|
||||
var invoice = new EnergyInvoice
|
||||
{
|
||||
Filename = att.Name,
|
||||
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,
|
||||
ValorSemImpostos = parsed.ValorSemImpostos,
|
||||
ValorFinalComImpostos = parsed.ValorFinalComImpostos,
|
||||
RsComp = parsed.RsComp,
|
||||
RsVend = parsed.RsVend,
|
||||
@ -59,9 +65,13 @@ namespace ComplianceNFs.Core.Application.Services
|
||||
Status = InvoiceStatus.Pending
|
||||
};
|
||||
await _attachmentRepository.SaveRawAsync(invoice);
|
||||
if (InvoiceProcessed != null)
|
||||
await InvoiceProcessed.Invoke(invoice);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Change it to an event declaration:
|
||||
public event Func<EnergyInvoice, Task>? InvoiceProcessed;
|
||||
public Task IngestAsync()
|
||||
{
|
||||
_mailListener.StartListening();
|
||||
@ -70,32 +80,65 @@ namespace ComplianceNFs.Core.Application.Services
|
||||
}
|
||||
|
||||
// Handles matching logic for invoices
|
||||
public class MatchingService : IMatchingService
|
||||
public class MatchingService(IAccessDbRepository accessDbRepository) : IMatchingService
|
||||
{
|
||||
private readonly IAccessDbRepository _accessDbRepository;
|
||||
public MatchingService(IAccessDbRepository accessDbRepository)
|
||||
{
|
||||
_accessDbRepository = accessDbRepository;
|
||||
}
|
||||
private readonly IAccessDbRepository _accessDbRepository = accessDbRepository;
|
||||
|
||||
public Task MatchAsync(EnergyInvoice invoice)
|
||||
{
|
||||
// Example: Primary match logic (simplified)
|
||||
var records = _accessDbRepository.GetByUnidade(invoice.CnpjComp);
|
||||
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) / record.MontLO <= 0.01m;
|
||||
var priceMatch = Math.Abs(record.PrecLO - invoice.PrecNF) / record.PrecLO <= 0.005m;
|
||||
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;
|
||||
break;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Add fallback and multi-invoice sum logic
|
||||
// Fallback: try to match by summing multiple records (multi-invoice sum logic)
|
||||
// Only consider records with same CnpjComp and CnpjVend
|
||||
var candidateRecords = new List<BuyingRecord>();
|
||||
foreach (var record in records)
|
||||
{
|
||||
if (record.CnpjComp == invoice.CnpjComp && record.CnpjVend == invoice.CnpjVend)
|
||||
{
|
||||
candidateRecords.Add(record);
|
||||
}
|
||||
}
|
||||
// Try all combinations of 2 records (can be extended to more)
|
||||
for (int i = 0; i < candidateRecords.Count; i++)
|
||||
{
|
||||
for (int j = i + 1; j < candidateRecords.Count; j++)
|
||||
{
|
||||
var sumVol = (candidateRecords[i].MontLO ?? 0) + (candidateRecords[j].MontLO ?? 0);
|
||||
var sumPrice = (candidateRecords[i].PrecLO ?? 0) + (candidateRecords[j].PrecLO ?? 0);
|
||||
var volMatch = Math.Abs(sumVol - (invoice.MontNF ?? 0)) / (sumVol == 0 ? 1 : sumVol) <= 0.01m;
|
||||
var priceMatch = Math.Abs(sumPrice - (invoice.PrecNF ?? 0)) / (sumPrice == 0 ? 1 : sumPrice) <= 0.005m;
|
||||
if (volMatch && priceMatch)
|
||||
{
|
||||
invoice.MatchedCodTE = candidateRecords[i].CodTE; // or store both if needed
|
||||
invoice.Status = InvoiceStatus.FallbackMatched;
|
||||
invoice.DiscrepancyNotes = $"Matched by sum of records {candidateRecords[i].CodTE} and {candidateRecords[j].CodTE}";
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If no match found
|
||||
invoice.Status = InvoiceStatus.NotFound;
|
||||
invoice.DiscrepancyNotes = "No matching record found (including fallback sum logic)";
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@ -108,11 +151,11 @@ namespace ComplianceNFs.Core.Application.Services
|
||||
// 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)
|
||||
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: implied={impliedTax:P2}, expected={invoice.IcmsNF:P2}";
|
||||
invoice.DiscrepancyNotes = $"Tax mismatch: imp={impliedTax:P2}, exp={invoice.IcmsNF:P2}";
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -136,13 +179,10 @@ namespace ComplianceNFs.Core.Application.Services
|
||||
}
|
||||
|
||||
// Handles archiving of files
|
||||
public class ArchivingService : IArchivingService
|
||||
public class ArchivingService(IFileArchiver fileArchiver) : IArchivingService
|
||||
{
|
||||
private readonly IFileArchiver _fileArchiver;
|
||||
public ArchivingService(IFileArchiver fileArchiver)
|
||||
{
|
||||
_fileArchiver = fileArchiver;
|
||||
}
|
||||
private readonly IFileArchiver _fileArchiver = fileArchiver;
|
||||
|
||||
public Task ArchiveAsync(EnergyInvoice invoice, byte[] rawFile)
|
||||
{
|
||||
return _fileArchiver.ArchiveAsync(invoice, rawFile);
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0-preview.4.25258.110" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -1,17 +1,41 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
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
|
||||
public BigInteger CodTE { get; set; }
|
||||
public BigInteger? CodSmartUnidade { get; set; }
|
||||
public int Mes { get; set; }
|
||||
public DateTime? Hora_LO { get; set; }
|
||||
public string? Operacao { get; set; }
|
||||
public string? Tipo { get; set; }
|
||||
public DateTime? Hora_NF { get; set; }
|
||||
public decimal? Tempo_NF { get; set; }
|
||||
public string? Contraparte_NF { get; set; }
|
||||
public string? Energia { get; set; }
|
||||
public decimal? Montante_NF { get; set; }
|
||||
public decimal? Preco_NF { get; set; }
|
||||
public decimal? Desconto_NF { get; set; }
|
||||
public decimal? NF_c_ICMS { get; set; }
|
||||
public bool NF_recebida { get; set; }
|
||||
public bool NF_Correta { get; set; }
|
||||
public string? Numero_NF { get; set; }
|
||||
public string? Chave_acesso { get; set; }
|
||||
public bool Lanc_autom { get; set; }
|
||||
public decimal? Revend_Mont { get; set; }
|
||||
public decimal? Revend_Prec { get; set; }
|
||||
public string? CnpjComp { get; set; }
|
||||
public string? CnpjVend { get; set; }
|
||||
public decimal? MontLO { get; set; }
|
||||
public decimal? PrecLO { get; set; }
|
||||
public string? Contrato_CliqCCEE { get; set; }
|
||||
public string? Vig_ini_CliqCCEE { get; set; }
|
||||
public string? Vig_fim_CliqCCEE { get; set; }
|
||||
public string? Submercado { get; set; }
|
||||
public string? Consolidado { get; set; }
|
||||
public string? PerfilCliqCCEE { get; set; }
|
||||
public string? Perfil_Contr { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,30 +1,32 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
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 required string MailId { get; set; }
|
||||
public required string ConversationId { get; set; }
|
||||
public required string SupplierEmail { 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 int InvoiceId { get; set; }
|
||||
public required string Filename { 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 BigInteger? MatchedCodTE { get; set; }
|
||||
public InvoiceStatus Status { get; set; }
|
||||
public string DiscrepancyNotes { get; set; }
|
||||
public string? DiscrepancyNotes { get; set; }
|
||||
}
|
||||
|
||||
public enum InvoiceStatus
|
||||
|
||||
@ -2,17 +2,16 @@ 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; }
|
||||
public string? CnpjComp { get; set; }
|
||||
public string? CnpjVend { get; set; }
|
||||
public decimal? MontNF { get; set; }
|
||||
public decimal? PrecNF { 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; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Mail;
|
||||
using System.Numerics;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComplianceNFs.Core.Ports
|
||||
@ -24,14 +26,14 @@ namespace ComplianceNFs.Core.Ports
|
||||
|
||||
public interface IAccessDbRepository
|
||||
{
|
||||
IEnumerable<Entities.BuyingRecord> GetByUnidade(string codSmartUnidade);
|
||||
IEnumerable<Entities.BuyingRecord> GetByUnidadeAndMonth(string codSmartUnidade, int month, int year);
|
||||
IEnumerable<Entities.BuyingRecord> GetByCnpj(string codSmartUnidade);
|
||||
IEnumerable<Entities.BuyingRecord> GetByCnpjAndMonth(string codSmartUnidade, int refMonth);
|
||||
}
|
||||
|
||||
public interface IAttachmentRepository
|
||||
{
|
||||
Task SaveRawAsync(Entities.EnergyInvoice invoice);
|
||||
Task UpdateMatchAsync(int invoiceId, int matchedCodTE, Entities.InvoiceStatus status, string notes);
|
||||
Task UpdateMatchAsync(int invoiceId, BigInteger matchedCodTE, Entities.InvoiceStatus status, string notes);
|
||||
}
|
||||
|
||||
public interface IFileArchiver
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
using ComplianceNFs.Infrastructure.Repositories;
|
||||
using ComplianceNFs.Core.Entities;
|
||||
using Xunit;
|
||||
using Moq;
|
||||
|
||||
namespace ComplianceNFs.Infrastructure.Tests
|
||||
{
|
||||
public class AccessDbRepositoryTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetByCnpj_ReturnsExpectedRecords()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new List<BuyingRecord> {
|
||||
new BuyingRecord { CodTE = 180310221018240701, CnpjComp = "06272575007403", CnpjVend = "13777004000122", MontLO = 24.72m, PrecLO = 147.29m }
|
||||
};
|
||||
var CaminhoDB = "X:\\Middle\\Informativo Setorial\\Modelo Word\\BD1_dados cadastrais e faturas.accdb";
|
||||
var repo = new AccessDbRepository(@"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + CaminhoDB + ";Jet OLEDB:Database Password=gds21");
|
||||
// Act
|
||||
var result = repo.GetByCnpj("06272575007403");
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("06272575007403", result.First().CnpjComp);
|
||||
Assert.Equal("13777004000122", result.First().CnpjVend);
|
||||
Assert.Equal(24.72m, result.First().MontLO);
|
||||
Assert.Equal(147.29m, result.First().PrecLO);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
ComplianceNFs.Infrastructure.Tests/ArchivingServiceTests.cs
Normal file
38
ComplianceNFs.Infrastructure.Tests/ArchivingServiceTests.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using ComplianceNFs.Core.Entities;
|
||||
using ComplianceNFs.Core.Application.Services;
|
||||
using ComplianceNFs.Core.Ports;
|
||||
using Xunit;
|
||||
using Moq;
|
||||
|
||||
namespace ComplianceNFs.Infrastructure.Tests
|
||||
{
|
||||
public class ArchivingServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ArchiveAsync_CallsFileArchiver()
|
||||
{
|
||||
// Arrange
|
||||
var mockArchiver = new Mock<IFileArchiver>();
|
||||
var service = new ArchivingService(mockArchiver.Object);
|
||||
var invoice = new EnergyInvoice
|
||||
{
|
||||
MailId = "mailid",
|
||||
ConversationId = "convid",
|
||||
SupplierEmail = "test@supplier.com",
|
||||
ReceivedDate = DateTime.Now,
|
||||
InvoiceId = 1,
|
||||
Filename = "file.xml",
|
||||
Status = InvoiceStatus.Validated
|
||||
};
|
||||
var fileBytes = new byte[] { 1, 2, 3 };
|
||||
|
||||
// Act
|
||||
await service.ArchiveAsync(invoice, fileBytes);
|
||||
|
||||
// Assert
|
||||
mockArchiver.Verify(a => a.ArchiveAsync(invoice, fileBytes), Times.Once);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using ComplianceNFs.Infrastructure.Repositories;
|
||||
using ComplianceNFs.Core.Entities;
|
||||
using Xunit;
|
||||
using Moq;
|
||||
using Npgsql;
|
||||
|
||||
namespace ComplianceNFs.Infrastructure.Tests
|
||||
{
|
||||
public class AttachmentRepositoryTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SaveRawAsync_DoesNotThrow_WithValidInvoice()
|
||||
{
|
||||
// Arrange
|
||||
var repo = new AttachmentRepository("Host=localhost;Port=5432;Database=test;Username=test;Password=test");
|
||||
var invoice = new EnergyInvoice
|
||||
{
|
||||
MailId = "mailid",
|
||||
ConversationId = "convid",
|
||||
SupplierEmail = "test@supplier.com",
|
||||
ReceivedDate = DateTime.Now,
|
||||
InvoiceId = 1,
|
||||
Filename = "file.xml",
|
||||
Status = InvoiceStatus.Validated
|
||||
};
|
||||
// This is a placeholder: in a real test, use a test DB or mock NpgsqlConnection/Command
|
||||
// For demonstration, we'll just check that the method can be called without throwing
|
||||
await Assert.ThrowsAnyAsync<Exception>(async () => await repo.SaveRawAsync(invoice));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="3.2.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.1" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
|
||||
</ItemGroup>
|
||||
@ -22,6 +23,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ComplianceNFs.Infrastructure\ComplianceNFs.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\ComplianceNFs.Monitor\ComplianceNFs.Monitor.csproj" />
|
||||
<ProjectReference Include="..\ComplianceNFs.Service\ComplianceNFs.Service.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Net.Mail;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Xunit;
|
||||
using Moq;
|
||||
using ComplianceNFs.Core.Application.Services;
|
||||
using ComplianceNFs.Core.Ports;
|
||||
using ComplianceNFs.Core.Entities;
|
||||
|
||||
namespace ComplianceNFs.Infrastructure.Tests
|
||||
{
|
||||
public class InvoiceIngestionServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void OnNewMailReceived_ParsesXmlAttachmentAndSavesInvoice()
|
||||
{
|
||||
// Arrange
|
||||
var mockMailListener = new Mock<IMailListener>();
|
||||
var mockAttachmentRepo = new Mock<IAttachmentRepository>();
|
||||
var mockXmlParser = new Mock<IXmlParser>();
|
||||
var mockPdfParser = new Mock<IPdfParser>();
|
||||
|
||||
var testParsed = new ParsedInvoice { CnpjComp = "123", NumeroNF = "456" };
|
||||
mockXmlParser.Setup(x => x.Parse(It.IsAny<Stream>())).Returns(testParsed);
|
||||
|
||||
var service = new InvoiceIngestionService(
|
||||
mockMailListener.Object,
|
||||
mockAttachmentRepo.Object,
|
||||
mockXmlParser.Object,
|
||||
mockPdfParser.Object
|
||||
);
|
||||
|
||||
var mail = new MailMessage
|
||||
{
|
||||
From = new MailAddress("test@supplier.com"),
|
||||
Subject = "Test Invoice",
|
||||
Headers = { ["Message-ID"] = "msgid", ["Date"] = DateTime.Now.ToString(), ["Conversation-ID"] = "conv-id" }
|
||||
};
|
||||
var xmlContent = new MemoryStream(System.Text.Encoding.UTF8.GetBytes("<xml></xml>"));
|
||||
var attachment = new Attachment(xmlContent, "invoice.xml");
|
||||
mail.Attachments.Add(attachment);
|
||||
|
||||
// Act
|
||||
// Simulate event
|
||||
mockMailListener.Raise(m => m.NewMailReceived += null, mail);
|
||||
|
||||
// Assert
|
||||
mockXmlParser.Verify(x => x.Parse(It.IsAny<Stream>()), Times.Once);
|
||||
mockAttachmentRepo.Verify(x => x.SaveRawAsync(It.Is<EnergyInvoice>(inv => inv.CnpjComp == "123" && inv.Filename == "invoice.xml")), Times.Once);
|
||||
}
|
||||
}
|
||||
}
|
||||
53
ComplianceNFs.Infrastructure.Tests/MailListenerTests.cs
Normal file
53
ComplianceNFs.Infrastructure.Tests/MailListenerTests.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Net.Mail;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ComplianceNFs.Infrastructure.Mail;
|
||||
using ComplianceNFs.Core.Ports;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Xunit;
|
||||
using Moq;
|
||||
|
||||
namespace ComplianceNFs.Infrastructure.Tests
|
||||
{
|
||||
public class MailListenerTests
|
||||
{
|
||||
[Fact]
|
||||
public void StartListening_RaisesNewMailReceived_ForAllowListedSender()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
{"Mail:SupplierAllowList:0", "allowed@sender.com"}
|
||||
})
|
||||
.Build();
|
||||
var listener = new TestableMailListener(config);
|
||||
bool eventRaised = false;
|
||||
listener.NewMailReceived += (mail) =>
|
||||
{
|
||||
eventRaised = true;
|
||||
Assert.NotNull(mail.From);
|
||||
Assert.Equal("allowed@sender.com", mail.From.Address);
|
||||
Assert.Equal("Test Subject", mail.Subject);
|
||||
Assert.Equal("Test Body", mail.Body);
|
||||
};
|
||||
// Use the protected test hook to raise the event
|
||||
var mailMsg = new MailMessage
|
||||
{
|
||||
From = new MailAddress("allowed@sender.com"),
|
||||
Subject = "Test Subject",
|
||||
Body = "Test Body"
|
||||
};
|
||||
listener.RaiseNewMailReceivedForTest(mailMsg);
|
||||
Assert.True(eventRaised);
|
||||
}
|
||||
|
||||
// Expose protected method for test
|
||||
private class TestableMailListener : MailListener
|
||||
{
|
||||
public TestableMailListener(IConfiguration config) : base(config) { }
|
||||
public new void RaiseNewMailReceivedForTest(MailMessage mail) => base.RaiseNewMailReceivedForTest(mail);
|
||||
}
|
||||
}
|
||||
}
|
||||
62
ComplianceNFs.Infrastructure.Tests/MonitorViewModelTests.cs
Normal file
62
ComplianceNFs.Infrastructure.Tests/MonitorViewModelTests.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using ComplianceNFs.Monitor;
|
||||
using ComplianceNFs.Core.Entities;
|
||||
using ComplianceNFs.Core.Application;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace ComplianceNFs.Infrastructure.Tests
|
||||
{
|
||||
public class MonitorViewModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SubscribesToStatusStreamAndPopulatesRecentInvoices()
|
||||
{
|
||||
// Arrange
|
||||
var mockStream = new Mock<IInvoiceStatusStream>();
|
||||
var testInvoice = new EnergyInvoice
|
||||
{
|
||||
MailId = "mailid",
|
||||
ConversationId = "convid",
|
||||
SupplierEmail = "test@supplier.com",
|
||||
ReceivedDate = DateTime.Now,
|
||||
InvoiceId = 1,
|
||||
Filename = "file.xml",
|
||||
Status = InvoiceStatus.Validated
|
||||
};
|
||||
mockStream.Setup(s => s.GetRecent(It.IsAny<int>())).Returns(new[] { testInvoice });
|
||||
var viewModel = new MonitorViewModel(mockStream.Object);
|
||||
|
||||
// Assert
|
||||
Assert.Single(viewModel.RecentInvoices);
|
||||
Assert.Equal("mailid", viewModel.RecentInvoices[0].MailId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StatusUpdated_Event_AddsInvoiceToRecentInvoices()
|
||||
{
|
||||
// Arrange
|
||||
var mockStream = new Mock<IInvoiceStatusStream>();
|
||||
mockStream.Setup(s => s.GetRecent(It.IsAny<int>())).Returns(Array.Empty<EnergyInvoice>());
|
||||
var viewModel = new MonitorViewModel(mockStream.Object);
|
||||
var newInvoice = new EnergyInvoice
|
||||
{
|
||||
MailId = "newmailid",
|
||||
ConversationId = "convid",
|
||||
SupplierEmail = "test@supplier.com",
|
||||
ReceivedDate = DateTime.Now,
|
||||
InvoiceId = 2,
|
||||
Filename = "file2.xml",
|
||||
Status = InvoiceStatus.Validated
|
||||
};
|
||||
|
||||
// Act
|
||||
mockStream.Raise(s => s.StatusUpdated += null, newInvoice);
|
||||
|
||||
// Assert
|
||||
Assert.Single(viewModel.RecentInvoices);
|
||||
Assert.Equal("newmailid", viewModel.RecentInvoices[0].MailId);
|
||||
}
|
||||
}
|
||||
}
|
||||
173
ComplianceNFs.Infrastructure.Tests/ServiceLogicTests.cs
Normal file
173
ComplianceNFs.Infrastructure.Tests/ServiceLogicTests.cs
Normal file
@ -0,0 +1,173 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
using Moq;
|
||||
using ComplianceNFs.Core.Application.Services;
|
||||
using ComplianceNFs.Core.Ports;
|
||||
using ComplianceNFs.Core.Entities;
|
||||
using ComplianceNFs.Infrastructure.Repositories;
|
||||
|
||||
namespace ComplianceNFs.Infrastructure.Tests
|
||||
{
|
||||
public class MatchingServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task MatchAsync_SetsMatchedStatus_WhenSingleRecordMatches()
|
||||
{
|
||||
// Arrange
|
||||
var invoice = new EnergyInvoice {
|
||||
CnpjComp = "02696252000122",
|
||||
CnpjVend = "06981176000158",
|
||||
MontNF = 19.845m,
|
||||
PrecNF = 248.76m,
|
||||
MailId = "m",
|
||||
ConversationId = "c",
|
||||
SupplierEmail = "s",
|
||||
ReceivedDate = DateTime.Now,
|
||||
InvoiceId = 359630,
|
||||
Filename = "f.xml"
|
||||
};
|
||||
var CaminhoDB = "X:\\Middle\\Informativo Setorial\\Modelo Word\\BD1_dados cadastrais e faturas.accdb";
|
||||
var repo = new AccessDbRepository(@"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + CaminhoDB + ";Jet OLEDB:Database Password=gds21");
|
||||
// Act
|
||||
var result = repo.GetByCnpj(invoice.CnpjComp);
|
||||
var service = new MatchingService(repo);
|
||||
// Act
|
||||
await service.MatchAsync(invoice);
|
||||
// Debug output
|
||||
System.Diagnostics.Debug.WriteLine($"Invoice status after match: {invoice.Status}");
|
||||
// Assert
|
||||
Assert.Equal(result.First().CnpjComp, invoice.CnpjComp);
|
||||
Assert.Equal(result.First().CnpjVend, invoice.CnpjVend);
|
||||
Assert.Equal(result.First().MontLO, invoice.MontNF);
|
||||
Assert.Equal(result.First().PrecLO, invoice.PrecNF);
|
||||
Assert.Equal(InvoiceStatus.Matched, invoice.Status);
|
||||
Assert.Equal(240712110001250501, (Int64)invoice.MatchedCodTE!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MatchAsync_SetsFallbackMatched_WhenSumOfTwoRecordsMatches()
|
||||
{
|
||||
var mockRepo = new Mock<IAccessDbRepository>();
|
||||
var invoice = new EnergyInvoice { CnpjComp = "123", CnpjVend = "456", MontNF = 300, PrecNF = 600, MailId = "m", ConversationId = "c", SupplierEmail = "s", ReceivedDate = DateTime.Now, InvoiceId = 1, Filename = "f.xml" };
|
||||
var records = new List<BuyingRecord> {
|
||||
new BuyingRecord { CnpjComp = "123", CnpjVend = "456", MontLO = 100, PrecLO = 200, CodTE = 1 },
|
||||
new BuyingRecord { CnpjComp = "123", CnpjVend = "456", MontLO = 200, PrecLO = 400, CodTE = 2 }
|
||||
};
|
||||
mockRepo.Setup(r => r.GetByCnpj("123")).Returns(records);
|
||||
var service = new MatchingService(mockRepo.Object);
|
||||
|
||||
await service.MatchAsync(invoice);
|
||||
|
||||
Assert.Equal(InvoiceStatus.FallbackMatched, invoice.Status);
|
||||
Assert.Equal(1, invoice.MatchedCodTE); // or null, depending on your logic
|
||||
Assert.Contains("Matched by sum", invoice.DiscrepancyNotes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MatchAsync_SetsNotFound_WhenNoMatch()
|
||||
{
|
||||
// Arrange
|
||||
var mockRepo = new Mock<IAccessDbRepository>();
|
||||
var invoice = new EnergyInvoice {
|
||||
CnpjComp = "123",
|
||||
CnpjVend = "456",
|
||||
MontNF = 999,
|
||||
PrecNF = 999,
|
||||
MailId = "m",
|
||||
ConversationId = "c",
|
||||
SupplierEmail = "s",
|
||||
ReceivedDate = DateTime.Now,
|
||||
InvoiceId = 1,
|
||||
Filename = "f.xml"
|
||||
};
|
||||
var records = new List<BuyingRecord> {
|
||||
new BuyingRecord { CnpjComp = "123", CnpjVend = "456", MontLO = 100, PrecLO = 200, CodTE = 1 },
|
||||
new BuyingRecord { CnpjComp = "123", CnpjVend = "456", MontLO = 200, PrecLO = 400, CodTE = 2 }
|
||||
};
|
||||
mockRepo.Setup(r => r.GetByCnpj("123")).Returns(records);
|
||||
var service = new MatchingService(mockRepo.Object);
|
||||
// Act
|
||||
await service.MatchAsync(invoice);
|
||||
// Assert
|
||||
Assert.Equal(InvoiceStatus.NotFound, invoice.Status);
|
||||
Assert.Null(invoice.MatchedCodTE);
|
||||
}
|
||||
}
|
||||
|
||||
public class ComplianceServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ValidateAsync_SetsValidated_WhenTaxMatches()
|
||||
{
|
||||
var service = new ComplianceService();
|
||||
var invoice = new EnergyInvoice
|
||||
{
|
||||
MailId = "m",
|
||||
ConversationId = "c",
|
||||
SupplierEmail = "s",
|
||||
ReceivedDate = DateTime.Now,
|
||||
InvoiceId = 1,
|
||||
Filename = "f.xml",
|
||||
Status = InvoiceStatus.Matched,
|
||||
ValorSemImpostos = 100,
|
||||
ValorFinalComImpostos = 125m,
|
||||
IcmsNF = 0.2m, // implied tax = 0.1
|
||||
};
|
||||
|
||||
await service.ValidateAsync(invoice);
|
||||
|
||||
// Debug output
|
||||
System.Diagnostics.Debug.WriteLine($"Invoice status after validate: {invoice.Status}");
|
||||
Assert.Null(invoice.DiscrepancyNotes);
|
||||
Assert.Equal(InvoiceStatus.Validated, invoice.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_SetsTaxMismatch_WhenTaxDiffers()
|
||||
{
|
||||
// Arrange
|
||||
var invoice = new EnergyInvoice {
|
||||
MailId = "m",
|
||||
ConversationId = "c",
|
||||
SupplierEmail = "s",
|
||||
ReceivedDate = DateTime.Now,
|
||||
InvoiceId = 1,
|
||||
Filename = "f.xml",
|
||||
IcmsNF = 100,
|
||||
Status = InvoiceStatus.Matched
|
||||
};
|
||||
var service = new ComplianceService();
|
||||
// Act
|
||||
invoice.IcmsNF = 100;
|
||||
invoice.ValorFinalComImpostos = 110;
|
||||
invoice.ValorSemImpostos = 100;
|
||||
await service.ValidateAsync(invoice);
|
||||
// Assert
|
||||
Assert.Equal(InvoiceStatus.TaxMismatch, invoice.Status);
|
||||
}
|
||||
}
|
||||
|
||||
public class NotificationServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task NotifyAsync_WritesToConsole()
|
||||
{
|
||||
// Arrange
|
||||
var invoice = new EnergyInvoice {
|
||||
MailId = "m",
|
||||
ConversationId = "c",
|
||||
SupplierEmail = "s",
|
||||
ReceivedDate = DateTime.Now,
|
||||
InvoiceId = 1,
|
||||
Filename = "f.xml",
|
||||
Status = InvoiceStatus.Validated
|
||||
};
|
||||
var service = new NotificationService();
|
||||
// Act & Assert
|
||||
var ex = await Record.ExceptionAsync(() => service.NotifyAsync(invoice, "Test message"));
|
||||
Assert.Null(ex); // Should not throw
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -23,8 +23,14 @@ namespace ComplianceNFs.Infrastructure.Tests
|
||||
var archiver = new FileArchiver(_testBasePath);
|
||||
var invoice = new EnergyInvoice
|
||||
{
|
||||
MailId = "test-mail-id",
|
||||
ConversationId = "test-conv-id",
|
||||
SupplierEmail = "test@supplier.com",
|
||||
Filename = "testfile.txt",
|
||||
Status = InvoiceStatus.Validated
|
||||
Status = InvoiceStatus.Validated,
|
||||
// Add required fields for null safety
|
||||
ReceivedDate = DateTime.Now,
|
||||
InvoiceId = 1
|
||||
};
|
||||
var data = new byte[] { 1, 2, 3, 4 };
|
||||
|
||||
@ -44,8 +50,14 @@ namespace ComplianceNFs.Infrastructure.Tests
|
||||
var archiver = new FileArchiver(_testBasePath);
|
||||
var invoice = new EnergyInvoice
|
||||
{
|
||||
MailId = "test-mail-id",
|
||||
ConversationId = "test-conv-id",
|
||||
SupplierEmail = "test@supplier.com",
|
||||
Filename = "testfile.txt",
|
||||
Status = InvoiceStatus.Validated
|
||||
Status = InvoiceStatus.Validated,
|
||||
// Add required fields for null safety
|
||||
ReceivedDate = DateTime.Now,
|
||||
InvoiceId = 1
|
||||
};
|
||||
var data1 = new byte[] { 1, 2, 3 };
|
||||
var data2 = new byte[] { 9, 8, 7 };
|
||||
|
||||
46
ComplianceNFs.Infrastructure.Tests/WorkerTests.cs
Normal file
46
ComplianceNFs.Infrastructure.Tests/WorkerTests.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ComplianceNFs.Service;
|
||||
using ComplianceNFs.Core.Application;
|
||||
using ComplianceNFs.Core.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace ComplianceNFs.Infrastructure.Tests
|
||||
{
|
||||
public class WorkerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_StartsIngestionAndOrchestratesWorkflow()
|
||||
{
|
||||
// Arrange
|
||||
var loggerMock = new Mock<ILogger<Worker>>();
|
||||
var ingestionMock = new Mock<IInvoiceIngestionService>();
|
||||
var matchingMock = new Mock<IMatchingService>();
|
||||
var complianceMock = new Mock<IComplianceService>();
|
||||
var notificationMock = new Mock<INotificationService>();
|
||||
var archivingMock = new Mock<IArchivingService>();
|
||||
|
||||
var worker = new Worker(
|
||||
loggerMock.Object,
|
||||
ingestionMock.Object,
|
||||
matchingMock.Object,
|
||||
complianceMock.Object,
|
||||
notificationMock.Object,
|
||||
archivingMock.Object
|
||||
);
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(100); // Cancel quickly for test
|
||||
|
||||
// Act
|
||||
var task = worker.StartAsync(cts.Token);
|
||||
await Task.Delay(200); // Give it time to start
|
||||
|
||||
// Assert
|
||||
ingestionMock.Verify(i => i.IngestAsync(), Times.Once);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
ComplianceNFs.Infrastructure.Tests/XmlParserTests.cs
Normal file
35
ComplianceNFs.Infrastructure.Tests/XmlParserTests.cs
Normal file
File diff suppressed because one or more lines are too long
@ -8,13 +8,9 @@ using System.IO;
|
||||
namespace ComplianceNFs.Infrastructure.Archiving
|
||||
{
|
||||
// Moves files to archive folders by status
|
||||
public class FileArchiver : IFileArchiver
|
||||
public class FileArchiver(string basePath) : IFileArchiver
|
||||
{
|
||||
private readonly string _basePath;
|
||||
public FileArchiver(string basePath)
|
||||
{
|
||||
_basePath = basePath;
|
||||
}
|
||||
private readonly string _basePath = basePath;
|
||||
|
||||
public async Task ArchiveAsync(EnergyInvoice invoice, byte[] rawFile)
|
||||
{
|
||||
|
||||
@ -8,13 +8,15 @@
|
||||
<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="Microsoft.Office.Interop.Outlook" Version="15.0.4797.1004" />
|
||||
<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" />
|
||||
<PackageReference Include="Unimake.DFe" Version="20250610.1145.39" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
@ -4,54 +4,106 @@ 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;
|
||||
using Outlook = Microsoft.Office.Interop.Outlook;
|
||||
|
||||
namespace ComplianceNFs.Infrastructure.Mail
|
||||
{
|
||||
public class MailListener : IMailListener
|
||||
{
|
||||
public event Action<MailMessage> NewMailReceived;
|
||||
public event Action<MailMessage> NewMailReceived = delegate { };
|
||||
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>();
|
||||
_allowList = _config.GetSection("Mail:SupplierAllowList").Get<List<string>>() ?? [];
|
||||
}
|
||||
|
||||
public void StartListening()
|
||||
{
|
||||
if (_listening) return;
|
||||
_listening = true;
|
||||
Task.Run(async () =>
|
||||
Task.Run(() =>
|
||||
{
|
||||
_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))
|
||||
try
|
||||
{
|
||||
var message = await _client.Inbox.GetMessageAsync(uid);
|
||||
var senderAddress = message.From.Mailboxes.FirstOrDefault()?.Address;
|
||||
var outlookApp = new Outlook.Application();
|
||||
Outlook.NameSpace outlookNs = outlookApp.GetNamespace("MAPI");
|
||||
|
||||
// Get default account (first in Accounts collection)
|
||||
Outlook.Account? defaultAccount = null;
|
||||
if (outlookNs.Accounts != null && outlookNs.Accounts.Count > 0)
|
||||
defaultAccount = _config["Mail:FolderName"] is null ? outlookNs.Accounts[1] : outlookNs.Accounts[_config["Mail:FolderName"]];
|
||||
|
||||
// Get root folder (parent of Inbox) or fallback to Inbox
|
||||
Outlook.MAPIFolder? rootFolder = null;
|
||||
try
|
||||
{
|
||||
rootFolder = outlookNs.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox).Parent as Outlook.MAPIFolder;
|
||||
}
|
||||
catch { }
|
||||
rootFolder ??= outlookNs.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox);
|
||||
|
||||
// Read folder name from config or default to Inbox
|
||||
string folderName = _config["Mail:FolderName"] ?? "Caixa de Entrada";
|
||||
Outlook.MAPIFolder? selectedFolder = null;
|
||||
try
|
||||
{
|
||||
selectedFolder = rootFolder.Folders[folderName];
|
||||
}
|
||||
catch { }
|
||||
selectedFolder ??= rootFolder;
|
||||
|
||||
// Helper method to recursively process all subfolders
|
||||
void ProcessFolder(Outlook.MAPIFolder folder)
|
||||
{
|
||||
if (folder == null) return;
|
||||
foreach (object itemObj in folder.Items)
|
||||
{
|
||||
if (itemObj is Outlook.MailItem item && item.UnRead)
|
||||
{
|
||||
var senderAddress = item.SenderEmailAddress;
|
||||
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
|
||||
From = new MailAddress(senderAddress),
|
||||
Subject = item.Subject,
|
||||
Body = item.Body
|
||||
};
|
||||
NewMailReceived?.Invoke(mailMsg);
|
||||
}
|
||||
}
|
||||
// For demo: not a real polling loop. Add timer/polling for production.
|
||||
}
|
||||
// Recursively process subfolders
|
||||
foreach (Outlook.MAPIFolder subfolder in folder.Folders)
|
||||
{
|
||||
ProcessFolder(subfolder);
|
||||
}
|
||||
}
|
||||
|
||||
// Start processing from the selected folder
|
||||
ProcessFolder(selectedFolder);
|
||||
// Log success
|
||||
Console.WriteLine($"[MailListener] Started processing folder: {selectedFolder?.Name}");
|
||||
}
|
||||
catch (System.Runtime.InteropServices.COMException comEx)
|
||||
{
|
||||
Console.Error.WriteLine($"[MailListener][ERROR] Outlook Interop COMException: {comEx.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[MailListener][ERROR] Unexpected: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Protected method to raise the event for testing
|
||||
protected void RaiseNewMailReceivedForTest(MailMessage mail)
|
||||
{
|
||||
NewMailReceived?.Invoke(mail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,11 @@ namespace ComplianceNFs.Infrastructure.Parsers
|
||||
{
|
||||
public class PdfParser : IPdfParser
|
||||
{
|
||||
private readonly Regex CnpjCompRegex = GeneratedRegex("CNPJComp: (\\d{14})");
|
||||
private readonly Regex CnpjVendRegex = GeneratedRegex("CNPJVend: (\\d{14})");
|
||||
private readonly Regex MontNFRegex = GeneratedRegex("MontNF: ([\\d,.]+)");
|
||||
private readonly Regex PrecNFRegex = GeneratedRegex("PrecNF: ([\\d,.]+)");
|
||||
|
||||
public ParsedInvoice Parse(Stream pdfStream)
|
||||
{
|
||||
// Minimal demo: just read bytes as text (replace with real PDF parsing in production)
|
||||
@ -14,10 +19,10 @@ namespace ComplianceNFs.Infrastructure.Parsers
|
||||
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;
|
||||
var cnpjComp = CnpjCompRegex.Match(text).Groups[1].Value;
|
||||
var cnpjVend = CnpjVendRegex.Match(text).Groups[1].Value;
|
||||
var montNF = decimal.TryParse(MontNFRegex.Match(text).Groups[1].Value, out var m) ? m : 0;
|
||||
var precNF = decimal.TryParse(PrecNFRegex.Match(text).Groups[1].Value, out var p) ? p : 0;
|
||||
return new ParsedInvoice
|
||||
{
|
||||
CnpjComp = cnpjComp,
|
||||
@ -26,6 +31,12 @@ namespace ComplianceNFs.Infrastructure.Parsers
|
||||
PrecNF = precNF
|
||||
// ...fill other fields as needed
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
public static Regex GeneratedRegex(string pattern)
|
||||
{
|
||||
return new Regex(pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,33 +1,133 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using ComplianceNFs.Core.Entities;
|
||||
using ComplianceNFs.Core.Ports;
|
||||
using Unimake.Business.DFe.Xml.NFe;
|
||||
|
||||
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
|
||||
string xml = ReadXmlFromStream(xmlStream);
|
||||
var nfeProc = Unimake.Business.DFe.Utility.XMLUtility.Deserializar<NfeProc>(xml);
|
||||
var infNFe = nfeProc.NFe.InfNFe.First();
|
||||
var comprador = infNFe.Dest;
|
||||
var vendedor = infNFe.Emit;
|
||||
var produtos = infNFe.Det;
|
||||
var detalhes = produtos.First();
|
||||
var impostos = detalhes.Imposto;
|
||||
|
||||
decimal somaProdutos = CalcularSomaProdutos(produtos);
|
||||
decimal montanteOperacao = CalcularMontanteOperacao(produtos);
|
||||
decimal valorFinalComImpostos = CalcularValorFinalComImpostos(infNFe);
|
||||
decimal icmsNF = CalcularICMS(impostos.ICMS, somaProdutos, valorFinalComImpostos);
|
||||
decimal valorUnitario = CalcularValorUnitario(comprador, vendedor, somaProdutos, montanteOperacao, icmsNF);
|
||||
|
||||
return 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
|
||||
CnpjComp = FormatCnpjOrCpf(comprador.CNPJ ?? comprador.CPF),
|
||||
CnpjVend = FormatCnpjOrCpf(vendedor.CNPJ),
|
||||
MontNF = montanteOperacao,
|
||||
PrecNF = valorUnitario,
|
||||
ValorFinalComImpostos = valorFinalComImpostos,
|
||||
RsComp = DecodeHtml(comprador?.XNome),
|
||||
RsVend = DecodeHtml(vendedor?.XNome),
|
||||
NumeroNF = infNFe.Ide.NNF.ToString(),
|
||||
IcmsNF = icmsNF,
|
||||
UfComp = comprador?.EnderDest?.UF.ToString(),
|
||||
UfVend = vendedor?.EnderEmit?.UF.ToString()
|
||||
};
|
||||
return invoice;
|
||||
}
|
||||
|
||||
private static string ReadXmlFromStream(Stream xmlStream)
|
||||
{
|
||||
using var reader = new StreamReader(xmlStream, Encoding.UTF8, true, 1024, leaveOpen: true);
|
||||
string xml = reader.ReadToEnd();
|
||||
xmlStream.Position = 0;
|
||||
return xml;
|
||||
}
|
||||
|
||||
private static decimal CalcularSomaProdutos(System.Collections.Generic.List<Det> produtos)
|
||||
{
|
||||
return produtos.Sum(prod =>
|
||||
(prod.Prod.UCom == "KWH" ? prod.Prod.QCom / 1000M : prod.Prod.QCom) *
|
||||
(prod.Prod.UCom == "KWH" ? prod.Prod.VUnCom * 1000M : prod.Prod.VUnCom)
|
||||
);
|
||||
}
|
||||
|
||||
private static decimal CalcularMontanteOperacao(System.Collections.Generic.List<Det> produtos)
|
||||
{
|
||||
return produtos.Sum(prod => prod.Prod.UCom == "KWH" ? prod.Prod.QCom / 1000M : prod.Prod.QCom);
|
||||
}
|
||||
|
||||
private static decimal CalcularValorFinalComImpostos(InfNFe infNFe)
|
||||
{
|
||||
if (infNFe.Pag?.DetPag?.Sum(pag => pag.VPag) > 0)
|
||||
return (decimal)infNFe.Pag.DetPag.Sum(pag => pag.VPag);
|
||||
if (infNFe.Cobr?.Fat?.VLiq > 0)
|
||||
return (decimal)infNFe.Cobr.Fat.VLiq;
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static decimal CalcularICMS(ICMS icms, decimal somaProdutos, decimal valorFinalComImpostos)
|
||||
{
|
||||
decimal icmsValue = GetICMS(icms);
|
||||
if (icmsValue == 0 && valorFinalComImpostos != 0)
|
||||
return 1 - (somaProdutos / valorFinalComImpostos);
|
||||
return icmsValue;
|
||||
}
|
||||
|
||||
private static decimal CalcularValorUnitario(Dest comprador, Emit vendedor, decimal somaProdutos, decimal montanteOperacao, decimal icmsNF)
|
||||
{
|
||||
if (comprador.EnderDest.UF.ToString() == "SP" && vendedor.EnderEmit.UF.ToString() == "SP")
|
||||
{
|
||||
return montanteOperacao == 0 ? 0 : somaProdutos / montanteOperacao * (1 - icmsNF);
|
||||
}
|
||||
else
|
||||
{
|
||||
return montanteOperacao == 0 ? 0 : somaProdutos / montanteOperacao;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? FormatCnpjOrCpf(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||
if (long.TryParse(value, out var num))
|
||||
return num.ToString("00000000000000");
|
||||
return value;
|
||||
}
|
||||
|
||||
private static string? DecodeHtml(string? value)
|
||||
{
|
||||
return value != null ? WebUtility.HtmlDecode(value) : null;
|
||||
}
|
||||
|
||||
public static decimal GetICMS(ICMS icms)
|
||||
{
|
||||
var propICMS = icms.GetType()
|
||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.Name.StartsWith("ICMS"))
|
||||
.ToList();
|
||||
var primeiraProp = propICMS
|
||||
.Select(p => new { Prop = p, Valor = p.GetValue(icms) })
|
||||
.FirstOrDefault(x => x.Valor != null);
|
||||
if (primeiraProp == null || primeiraProp.Valor == null) return 0;
|
||||
var tipo = primeiraProp.Valor.GetType();
|
||||
var valor = Convert.ChangeType(primeiraProp.Valor, tipo);
|
||||
var listICMSReal = valor.GetType()
|
||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.Name.StartsWith("PICMS"))
|
||||
.Select(p => p.GetValue(valor))
|
||||
.FirstOrDefault();
|
||||
decimal icmsReal = 0;
|
||||
if (listICMSReal != null && decimal.TryParse(listICMSReal.ToString(), out icmsReal))
|
||||
icmsReal /= 100m;
|
||||
return icmsReal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,80 +2,171 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.OleDb;
|
||||
using System.Numerics;
|
||||
using ComplianceNFs.Core.Entities;
|
||||
using ComplianceNFs.Core.Ports;
|
||||
|
||||
namespace ComplianceNFs.Infrastructure.Repositories
|
||||
{
|
||||
// Placeholder: fill in actual SQL and mapping logic
|
||||
public class AccessDbRepository : IAccessDbRepository
|
||||
public class AccessDbRepository(string connectionString) : IAccessDbRepository
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
public AccessDbRepository(string connectionString)
|
||||
{
|
||||
_connectionString = connectionString;
|
||||
}
|
||||
private readonly string _connectionString = connectionString;
|
||||
|
||||
public IEnumerable<BuyingRecord> GetByUnidade(string codSmartUnidade)
|
||||
private const string BuyingRecordColumns = @"
|
||||
Dados_TE.Cod_TE,
|
||||
Dados_TE.Cod_Smart_unidade,
|
||||
Dados_TE.Mes,
|
||||
Dados_TE.Hora_LO,
|
||||
Dados_TE.Operacao,
|
||||
Dados_TE.Tipo,
|
||||
Dados_TE.Hora_NF,
|
||||
Dados_TE.Tempo_NF,
|
||||
Dados_TE.Contraparte_NF,
|
||||
Dados_TE.Energia,
|
||||
Dados_TE.Montante_NF,
|
||||
Dados_TE.Preco_NF,
|
||||
Dados_TE.Desconto_NF,
|
||||
Dados_TE.NF_c_ICMS,
|
||||
Dados_TE.NF_recebida,
|
||||
Dados_TE.NF_Correta,
|
||||
Dados_TE.Numero_NF,
|
||||
Dados_TE.Chave_acesso,
|
||||
Dados_TE.Lanc_autom,
|
||||
Dados_TE.Revend_Mont,
|
||||
Dados_TE.Revend_Prec,
|
||||
Dados_TE.CNPJ_comp,
|
||||
Dados_TE.CNPJ_vend,
|
||||
Dados_TE.Mont_LO,
|
||||
Dados_TE.Prec_LO,
|
||||
Dados_TE.Contrato_CliqCCEE,
|
||||
Dados_TE.Vig_ini_CliqCCEE,
|
||||
Dados_TE.Vig_fim_CliqCCEE,
|
||||
Dados_TE.Submercado,
|
||||
Dados_TE.Consolidado,
|
||||
Dados_TE.PerfilCliqCCEE,
|
||||
Dados_TE.Perfil_Contr
|
||||
";
|
||||
|
||||
public IEnumerable<BuyingRecord> GetByCnpj(string CNPJ_comp)
|
||||
{
|
||||
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())
|
||||
{
|
||||
cmd.CommandText = $@"SELECT {BuyingRecordColumns} FROM Dados_TE WHERE ((Dados_TE.CNPJ_comp)=@CNPJ_comp);";
|
||||
cmd.Parameters.AddWithValue("@CNPJ_comp", CNPJ_comp);
|
||||
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)
|
||||
});
|
||||
}
|
||||
results.Add(MapBuyingRecord(reader));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
public IEnumerable<BuyingRecord> GetByUnidadeAndMonth(string codSmartUnidade, int month, int year)
|
||||
public IEnumerable<BuyingRecord> GetByCnpjAndMonth(string CNPJ_comp, int refMonth)
|
||||
{
|
||||
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())
|
||||
{
|
||||
cmd.CommandText = $@"SELECT {BuyingRecordColumns} FROM Dados_TE WHERE ((Dados_TE.CNPJ_comp)=@CNPJ_comp) AND ((Dados_TE.Mes)=@MesRef);";
|
||||
cmd.Parameters.AddWithValue("@CNPJ_comp", CNPJ_comp);
|
||||
cmd.Parameters.AddWithValue("@MesRef", refMonth);
|
||||
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)
|
||||
});
|
||||
}
|
||||
results.Add(MapBuyingRecord(reader));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private static BuyingRecord MapBuyingRecord(OleDbDataReader reader)
|
||||
{
|
||||
return new BuyingRecord
|
||||
{
|
||||
CodTE = ToBigInteger(reader, "Cod_TE"),
|
||||
CodSmartUnidade = ToBigIntegerOrNull(reader, "Cod_Smart_unidade"),
|
||||
Mes = ToInt(reader, "Mes"),
|
||||
Hora_LO = ToDateTimeOrNull(reader, "Hora_LO"),
|
||||
Operacao = ToStringOrNull(reader, "Operacao"),
|
||||
Tipo = ToStringOrNull(reader, "Tipo"),
|
||||
Hora_NF = ToDateTimeOrNull(reader, "Hora_NF"),
|
||||
Tempo_NF = ToDecimalOrNull(reader, "Tempo_NF"),
|
||||
Contraparte_NF = ToStringOrNull(reader, "Contraparte_NF"),
|
||||
Energia = ToStringOrNull(reader, "Energia"),
|
||||
Montante_NF = ToDecimalOrNull(reader, "Montante_NF"),
|
||||
Preco_NF = ToDecimalOrNull(reader, "Preco_NF"),
|
||||
Desconto_NF = ToDecimalOrNull(reader, "Desconto_NF"),
|
||||
NF_c_ICMS = ToDecimalOrNull(reader, "NF_c_ICMS"),
|
||||
NF_recebida = ToBool(reader, "NF_recebida"),
|
||||
NF_Correta = ToBool(reader, "NF_Correta"),
|
||||
Numero_NF = ToStringOrNull(reader, "Numero_NF"),
|
||||
Chave_acesso = ToStringOrNull(reader, "Chave_acesso"),
|
||||
Lanc_autom = ToBool(reader, "Lanc_autom"),
|
||||
Revend_Mont = ToDecimalOrNull(reader, "Revend_Mont"),
|
||||
Revend_Prec = ToDecimalOrNull(reader, "Revend_Prec"),
|
||||
CnpjComp = ToStringOrNull(reader, "CNPJ_comp"),
|
||||
CnpjVend = ToStringOrNull(reader, "CNPJ_vend"),
|
||||
MontLO = ToDecimalOrNull(reader, "Mont_LO"),
|
||||
PrecLO = ToDecimalOrNull(reader, "Prec_LO"),
|
||||
Contrato_CliqCCEE = ToStringOrNull(reader, "Contrato_CliqCCEE"),
|
||||
Vig_ini_CliqCCEE = ToStringOrNull(reader, "Vig_ini_CliqCCEE"),
|
||||
Vig_fim_CliqCCEE = ToStringOrNull(reader, "Vig_fim_CliqCCEE"),
|
||||
Submercado = ToStringOrNull(reader, "Submercado"),
|
||||
Consolidado = ToStringOrNull(reader, "Consolidado"),
|
||||
PerfilCliqCCEE = ToStringOrNull(reader, "PerfilCliqCCEE"),
|
||||
Perfil_Contr = ToStringOrNull(reader, "Perfil_Contr"),
|
||||
};
|
||||
}
|
||||
#region Helpers
|
||||
private static BigInteger ToBigInteger(OleDbDataReader r, string col)
|
||||
{
|
||||
// Assumes non-null, integral
|
||||
var v = r[col];
|
||||
return v is BigInteger bi
|
||||
? bi
|
||||
: new BigInteger(Convert.ToInt64(v));
|
||||
}
|
||||
|
||||
private static BigInteger? ToBigIntegerOrNull(OleDbDataReader r, string col)
|
||||
{
|
||||
if (r.IsDBNull(r.GetOrdinal(col))) return null;
|
||||
return new BigInteger(Convert.ToInt64(r[col]));
|
||||
}
|
||||
|
||||
private static int ToInt(OleDbDataReader r, string col)
|
||||
{
|
||||
return Convert.ToInt32(r[col]);
|
||||
}
|
||||
|
||||
private static DateTime? ToDateTimeOrNull(OleDbDataReader r, string col)
|
||||
{
|
||||
if (r.IsDBNull(r.GetOrdinal(col))) return null;
|
||||
return Convert.ToDateTime(r[col]);
|
||||
}
|
||||
|
||||
private static decimal? ToDecimalOrNull(OleDbDataReader r, string col)
|
||||
{
|
||||
if (r.IsDBNull(r.GetOrdinal(col))) return null;
|
||||
return Convert.ToDecimal(r[col]);
|
||||
}
|
||||
|
||||
private static bool ToBool(OleDbDataReader r, string col)
|
||||
{
|
||||
return Convert.ToBoolean(r[col]);
|
||||
}
|
||||
|
||||
private static string? ToStringOrNull(OleDbDataReader r, string col)
|
||||
{
|
||||
return r.IsDBNull(r.GetOrdinal(col))
|
||||
? null
|
||||
: r.GetString(r.GetOrdinal(col));
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,22 +4,18 @@ using ComplianceNFs.Core.Entities;
|
||||
using ComplianceNFs.Core.Ports;
|
||||
using Npgsql;
|
||||
using Newtonsoft.Json;
|
||||
using System.Numerics;
|
||||
|
||||
namespace ComplianceNFs.Infrastructure.Repositories
|
||||
{
|
||||
// Placeholder: fill in actual SQL and mapping logic
|
||||
public class AttachmentRepository : IAttachmentRepository
|
||||
public class AttachmentRepository(string connectionString) : IAttachmentRepository
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
public AttachmentRepository(string connectionString)
|
||||
{
|
||||
_connectionString = connectionString;
|
||||
}
|
||||
private readonly string _connectionString = connectionString;
|
||||
|
||||
public async Task SaveRawAsync(EnergyInvoice invoice)
|
||||
{
|
||||
using (var conn = new NpgsqlConnection(_connectionString))
|
||||
{
|
||||
using var conn = new NpgsqlConnection(_connectionString);
|
||||
await conn.OpenAsync();
|
||||
var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"INSERT INTO attachments (
|
||||
@ -31,17 +27,17 @@ namespace ComplianceNFs.Infrastructure.Repositories
|
||||
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("@md5", (object?)invoice.Md5 ?? DBNull.Value);
|
||||
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("@mont_nf", (object?)invoice.MontNF ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@prec_nf", (object?)invoice.PrecNF ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@valor_sem_imp", (object?)invoice.ValorSemImpostos ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@valor_com_imp", (object?)invoice.ValorFinalComImpostos ?? DBNull.Value);
|
||||
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("@icms_nf", (object?)invoice.IcmsNF ?? DBNull.Value);
|
||||
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);
|
||||
@ -50,12 +46,10 @@ namespace ComplianceNFs.Infrastructure.Repositories
|
||||
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))
|
||||
public async Task UpdateMatchAsync(int invoiceId, BigInteger 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";
|
||||
@ -66,5 +60,4 @@ namespace ComplianceNFs.Infrastructure.Repositories
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0-preview.4.25258.110" />
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<Window x:Class="ComplianceNFs.Monitor.MainWindow"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
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"
|
||||
@ -17,6 +18,8 @@
|
||||
<GridViewColumn Header="Timestamp" DisplayMemberBinding="{Binding ReceivedDate}" Width="150"/>
|
||||
<GridViewColumn Header="Filename" DisplayMemberBinding="{Binding Filename}" Width="250"/>
|
||||
<GridViewColumn Header="Status" DisplayMemberBinding="{Binding Status}" Width="120"/>
|
||||
<GridViewColumn Header="Supplier Email" DisplayMemberBinding="{Binding SupplierEmail}" Width="200"/>
|
||||
<GridViewColumn Header="CNPJ Comp" DisplayMemberBinding="{Binding CnpjComp}" Width="150"/>
|
||||
</GridView>
|
||||
</ListView.View>
|
||||
</ListView>
|
||||
|
||||
@ -8,17 +8,26 @@ using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using ComplianceNFs.Core.Application;
|
||||
|
||||
namespace ComplianceNFs.Monitor;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
namespace ComplianceNFs.Monitor
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new MonitorViewModel();
|
||||
DataContext = new MonitorViewModel(new DummyStatusStream());
|
||||
}
|
||||
}
|
||||
|
||||
// Dummy implementation for design/runtime
|
||||
public class DummyStatusStream : IInvoiceStatusStream
|
||||
{
|
||||
public event Action<Core.Entities.EnergyInvoice>? StatusUpdated { add { } remove { } }
|
||||
public IEnumerable<Core.Entities.EnergyInvoice> GetRecent(int count = 100) => Array.Empty<Core.Entities.EnergyInvoice>();
|
||||
}
|
||||
}
|
||||
@ -3,45 +3,54 @@ using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows.Input;
|
||||
using ComplianceNFs.Core.Application;
|
||||
using ComplianceNFs.Core.Entities;
|
||||
|
||||
namespace ComplianceNFs.Monitor
|
||||
{
|
||||
public class MonitorViewModel : INotifyPropertyChanged
|
||||
{
|
||||
public ObservableCollection<EnergyInvoice> RecentInvoices { get; } = new();
|
||||
public ObservableCollection<EnergyInvoice> RecentInvoices { get; } = [];
|
||||
public ICommand ForceScanCommand { get; }
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public MonitorViewModel()
|
||||
public IInvoiceStatusStream? StatusStream { get; }
|
||||
|
||||
public MonitorViewModel(IInvoiceStatusStream statusStream)
|
||||
{
|
||||
// TODO: Inject IInvoiceStatusStream and subscribe to updates
|
||||
StatusStream = statusStream;
|
||||
foreach (var inv in statusStream.GetRecent(100))
|
||||
RecentInvoices.Add(inv);
|
||||
statusStream.StatusUpdated += OnStatusUpdated;
|
||||
ForceScanCommand = new RelayCommand(_ => ForceScan());
|
||||
}
|
||||
|
||||
private void ForceScan()
|
||||
private void OnStatusUpdated(EnergyInvoice invoice)
|
||||
{
|
||||
RecentInvoices.Add(invoice);
|
||||
OnPropertyChanged(nameof(RecentInvoices));
|
||||
}
|
||||
|
||||
private static void ForceScan()
|
||||
{
|
||||
// TODO: Call service to force ingestion cycle
|
||||
}
|
||||
|
||||
protected void OnPropertyChanged([CallerMemberName] string name = null)
|
||||
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
if (name != null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); }
|
||||
}
|
||||
}
|
||||
|
||||
public class RelayCommand : ICommand
|
||||
public class RelayCommand(Action<object> execute, Func<object, bool>? canExecute = null) : ICommand
|
||||
{
|
||||
private readonly Action<object> _execute;
|
||||
private readonly Func<object, bool> _canExecute;
|
||||
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
|
||||
public bool CanExecute(object? parameter) => parameter != null && (canExecute == null || canExecute(parameter));
|
||||
public void Execute(object? parameter)
|
||||
{
|
||||
_execute = execute;
|
||||
_canExecute = canExecute;
|
||||
if (parameter != null) { execute(parameter); }
|
||||
}
|
||||
public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter);
|
||||
public void Execute(object parameter) => _execute(parameter);
|
||||
public event EventHandler CanExecuteChanged { add { } remove { } }
|
||||
|
||||
public event EventHandler? CanExecuteChanged { add { } remove { } }
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>dotnet-ComplianceNFs.Service-386f44bf-03c8-490c-96b9-3852d8c3d01a</UserSecretsId>
|
||||
|
||||
@ -7,6 +7,8 @@ using ComplianceNFs.Infrastructure.Repositories;
|
||||
using ComplianceNFs.Infrastructure.Mail;
|
||||
using ComplianceNFs.Infrastructure.Parsers;
|
||||
using ComplianceNFs.Infrastructure.Archiving;
|
||||
using ComplianceNFs.Core.Application.Services;
|
||||
using ComplianceNFs.Core.Application;
|
||||
|
||||
IHost host = Host.CreateDefaultBuilder(args)
|
||||
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||
@ -17,18 +19,35 @@ IHost host = Host.CreateDefaultBuilder(args)
|
||||
{
|
||||
var config = context.Configuration;
|
||||
// Register infrastructure
|
||||
services.AddSingleton<IAccessDbRepository>(sp => new AccessDbRepository(config["AccessConnectionString"]));
|
||||
services.AddSingleton<IAttachmentRepository>(sp => new AttachmentRepository(config["PostgresConnectionString"]));
|
||||
services.AddSingleton<IAccessDbRepository>(sp =>
|
||||
{
|
||||
var connectionString = config["AccessConnectionString"];
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
throw new InvalidOperationException("AccessConnectionString is missing in configuration.");
|
||||
return new AccessDbRepository(connectionString);
|
||||
});
|
||||
services.AddSingleton<IAttachmentRepository>(sp =>
|
||||
{
|
||||
var pgConnectionString = config["PostgresConnectionString"];
|
||||
if (string.IsNullOrWhiteSpace(pgConnectionString))
|
||||
throw new InvalidOperationException("PostgresConnectionString is missing in configuration.");
|
||||
return new AttachmentRepository(pgConnectionString);
|
||||
});
|
||||
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.AddSingleton<IFileArchiver>(sp =>
|
||||
{
|
||||
var archiveBasePath = config["ArchiveBasePath"];
|
||||
if (string.IsNullOrWhiteSpace(archiveBasePath))
|
||||
throw new InvalidOperationException("ArchiveBasePath is missing in configuration.");
|
||||
return new FileArchiver(archiveBasePath);
|
||||
});
|
||||
services.AddSingleton<IInvoiceIngestionService, InvoiceIngestionService>();
|
||||
services.AddSingleton<IMatchingService, MatchingService>();
|
||||
services.AddSingleton<IComplianceService, ComplianceService>();
|
||||
services.AddSingleton<INotificationService, NotificationService>();
|
||||
services.AddSingleton<IArchivingService, ArchivingService>();
|
||||
services.AddHostedService<Worker>();
|
||||
})
|
||||
.Build();
|
||||
|
||||
@ -1,19 +1,51 @@
|
||||
using ComplianceNFs.Core.Entities;
|
||||
using ComplianceNFs.Core.Application;
|
||||
using ComplianceNFs.Core.Application.Services;
|
||||
|
||||
namespace ComplianceNFs.Service;
|
||||
|
||||
public class Worker : BackgroundService
|
||||
public class Worker(ILogger<Worker> logger,
|
||||
IInvoiceIngestionService ingestionService,
|
||||
IMatchingService matchingService,
|
||||
IComplianceService complianceService,
|
||||
INotificationService notificationService,
|
||||
IArchivingService archivingService) : BackgroundService
|
||||
{
|
||||
private readonly ILogger<Worker> _logger;
|
||||
|
||||
public Worker(ILogger<Worker> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
private readonly ILogger<Worker> _logger = logger;
|
||||
private readonly IInvoiceIngestionService _ingestionService = ingestionService;
|
||||
private readonly IMatchingService _matchingService = matchingService;
|
||||
private readonly IComplianceService _complianceService = complianceService;
|
||||
private readonly INotificationService _notificationService = notificationService;
|
||||
private readonly IArchivingService _archivingService = archivingService;
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("Worker starting at: {time}", DateTimeOffset.Now);
|
||||
// Start mail ingestion (starts listening for new mail)
|
||||
await _ingestionService.IngestAsync();
|
||||
// Subscribe to new invoice events and orchestrate workflow
|
||||
if (_ingestionService is InvoiceIngestionService ingestionImpl)
|
||||
{
|
||||
ingestionImpl.InvoiceProcessed += async invoice =>
|
||||
{
|
||||
// 1. Match invoice
|
||||
await _matchingService.MatchAsync(invoice);
|
||||
// 2. Compliance validation
|
||||
await _complianceService.ValidateAsync(invoice);
|
||||
// 3. Notify if needed
|
||||
if (invoice.Status == InvoiceStatus.TaxMismatch || invoice.Status == InvoiceStatus.VolumeMismatch || invoice.Status == InvoiceStatus.PriceMismatch)
|
||||
{
|
||||
await _notificationService.NotifyAsync(invoice, invoice.DiscrepancyNotes ?? "Discrepancy detected");
|
||||
}
|
||||
// 4. Archive
|
||||
// (Assume raw file is available or can be loaded if needed)
|
||||
// await _archivingService.ArchiveAsync(invoice, rawFile);
|
||||
_logger.LogInformation("Invoice {NumeroNF} processed with status: {Status}", invoice.NumeroNF, invoice.Status);
|
||||
};
|
||||
}
|
||||
// Keep the worker alive
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
|
||||
await Task.Delay(1000, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,92 @@
|
||||
{
|
||||
"AccessConnectionString": "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=X:\\Back\\Controle NFs\\Dados.accdb;",
|
||||
"PostgresConnectionString": "Host=…;Port=5432;Database=…;Username=…;Password=…;",
|
||||
"AccessConnectionString": "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=X:\\Middle\\Informativo Setorial\\Modelo Word\\BD1_dados cadastrais e faturas.accdb;",
|
||||
"PostgresConnectionString": "Host=192.168.10.248;Port=5432;Database=pipefy_move_cards;User Id=postgres;Password=gds21;",
|
||||
"Mail": {
|
||||
"Host": "outlook.office365.com",
|
||||
"Port": 993,
|
||||
"User": "service@yourcompany.com",
|
||||
"Password": "…",
|
||||
"SupplierAllowList": [ "faturamento@…", "nfe@…" ]
|
||||
"SupplierAllowList": [
|
||||
"faturamento.energia@2wenergia.com.br",
|
||||
"faturamento.energia@2wecobank.com.br",
|
||||
"jaqueliny.oliveira@2wenergia.com.br",
|
||||
"nfe@americaenergia.com.br",
|
||||
"andreia.aoyama@atmoenergia.com.br",
|
||||
"lais.marques@atmoenergia.com.br",
|
||||
"noreply@omie.com.br",
|
||||
"no-reply@uolinc.com",
|
||||
"no-reply@notanet.uol.com.br",
|
||||
"mayara.souza@capitaleenergia.com.br",
|
||||
"faturamento@capitaleenergia.com.br",
|
||||
"nfe@ceienergetica.com.br",
|
||||
"nfe@necenergia.com.br",
|
||||
"faturaat@cemig.com.br",
|
||||
"faturas.comercializacao@copel.com",
|
||||
"com.backoffice@copel.com",
|
||||
"lbredariol@cpfl.com.br",
|
||||
"noemi.rocha@cpfl.com.br",
|
||||
"backoffice@cpflsolucoes.com.br",
|
||||
"noreply@omie.com.br",
|
||||
"noreply@omie.com.br",
|
||||
"marjorie@desttra.com.br",
|
||||
"noreply@omie.com.br",
|
||||
"noreply@omie.com.br",
|
||||
"leonardo@emeweenergia.com.br",
|
||||
"alexando@emeweenergia.com.br",
|
||||
"arthur@emeweenergia.com.br",
|
||||
"nfe@enel.com",
|
||||
"danilo.marchiotto@enel.com",
|
||||
"energisa.comercializadora@energisa.com.br",
|
||||
"faturamento@energisa.com.br",
|
||||
"nfesede.brenergia@engie.com",
|
||||
"nfesede.brenergia@rpost.engie.com.br",
|
||||
"faturamento@evolutionenergia.com.br",
|
||||
"noreply@omie.com.br",
|
||||
"nfe@ceienergetica.com.br",
|
||||
"nfe@necenergia.com.br",
|
||||
"faturamento@pactoenergia.com.br",
|
||||
"financeiro@pactoenergia.com.br",
|
||||
"camila.silva@pactoenergia.com.br",
|
||||
"back1@smartenergia.com.br",
|
||||
"back4@smartenergia.com.br",
|
||||
"fernando@smartenergia.com.br",
|
||||
"contratos@smartenergia.com.br",
|
||||
"nfe.energia@omegaenergia.com.br",
|
||||
"VE.FINANCEIRO_CAR@VENERGIA.COM.BR",
|
||||
"no-reply@venergia.com.br",
|
||||
"marcelo.fuliotti@w7energia.com.br",
|
||||
"faturas.edpcom@edpbr.com.br",
|
||||
"backoffice@cesp.com.br",
|
||||
"noreply@aurenenergia.com.br",
|
||||
"naoresponder@oobj-dfe.com.br",
|
||||
"faturamento@comerc.com.br",
|
||||
"aarboleda@quantageracao.com.br",
|
||||
"hpaixao@quantageracao.com.br",
|
||||
"comercializadora@celesc.com.br",
|
||||
"etrm@neoenergiacomercializacao.com.br",
|
||||
"nfe.noreply@dfe.mastersaf.com.br",
|
||||
"faturamento@mercattoenergia.com.br",
|
||||
"nao_responda@raizen.com",
|
||||
"nao_responda@cosan.com",
|
||||
"smtp-noreply@grupobcenergia.com.br",
|
||||
"faturamento@lightcom.com.br",
|
||||
"NFE@CASADOSVENTOS.COM.BR",
|
||||
"noreply.br@statkraft.com",
|
||||
"gestao.ccee@smartenergia.com.br",
|
||||
"solucao.fiscalnfe@eletrobras.com",
|
||||
"hegon.goncalves@aurenenergia.com.br",
|
||||
"fechamento@echoenergia.com.br",
|
||||
"sistema@brennandenergia.com.br",
|
||||
"bruna.costa@aurenenergia.com.br",
|
||||
"financeiro@libertyenergy.com.br",
|
||||
"fatura@grupowtz.com.br",
|
||||
"faturamento@eletronorte.gov.br",
|
||||
"faturamento@echoenergia.com.br",
|
||||
"nfe.varejo@casadosventos.com.br",
|
||||
"faturamento-nf@eletrobras.com",
|
||||
"faturamento-nf.chesf@eletrobras.com",
|
||||
"faturamento-nf.enorte@eletrobras.com",
|
||||
"faturamento-nf.esul@eletrobras.com",
|
||||
"faturamento-nf@eletrobras.com"
|
||||
],
|
||||
"AccountName": "gestao.ccee@energiasmart.com.br",
|
||||
"FolderName": "Caixa de Entrada"
|
||||
},
|
||||
"PollingIntervalMinutes": 2,
|
||||
"Tolerances": {
|
||||
@ -14,7 +94,7 @@
|
||||
"PricePercent": 0.5,
|
||||
"TaxPercent": 1.0
|
||||
},
|
||||
"ArchiveBasePath": "X:\\Back\\Controle NFs\\Fs\\",
|
||||
"ArchiveBasePath": "X:\\Back\\Controle NFs\\NFs\\",
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
|
||||
@ -11,14 +11,13 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ComplianceNFs.Monitor", "Co
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ComplianceNFs.Service", "ComplianceNFs.Service\ComplianceNFs.Service.csproj", "{72BFBD25-0814-420D-8524-21448479A771}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ComplianceNFs.Infrastructure.Tests", "ComplianceNFs.Infrastructure.Tests\ComplianceNFs.Infrastructure.Tests.csproj", "{E25FC800-6D77-465C-AD05-95D947D472E9}"
|
||||
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
|
||||
@ -36,5 +35,12 @@ Global
|
||||
{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
|
||||
{E25FC800-6D77-465C-AD05-95D947D472E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E25FC800-6D77-465C-AD05-95D947D472E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E25FC800-6D77-465C-AD05-95D947D472E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E25FC800-6D77-465C-AD05-95D947D472E9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@ -8,7 +8,8 @@
|
||||
|
||||
### ComplianceNFs.Infrastructure/Mail/MailListener.cs
|
||||
|
||||
- [x] MailListener.StartListening: Connect to mailbox, filter by allowlist, raise NewMailReceived
|
||||
- [x] MailListener.StartListening: Connect to Outlook app, filter by allowlist, support account/folder selection, recursively process subfolders, raise NewMailReceived
|
||||
- [x] Add logging and error handling for Outlook interop failures
|
||||
|
||||
### ComplianceNFs.Infrastructure/Parsers/PdfParser.cs
|
||||
|
||||
@ -35,6 +36,7 @@
|
||||
- [x] InvoiceIngestionService: Implement ingestion logic and subscribe to NewMailReceived
|
||||
|
||||
- [x] MatchingService: Implement invoice matching logic
|
||||
- [x] Add fallback and multi-invoice sum logic in MatchAsync
|
||||
|
||||
- [x] ComplianceService: Implement compliance validation logic
|
||||
|
||||
@ -50,7 +52,7 @@
|
||||
|
||||
### ComplianceNFs.Service/Program.cs
|
||||
|
||||
- [ ] Register application services (InvoiceIngestionService, MatchingService, etc.)
|
||||
- [x] Register application services (InvoiceIngestionService, MatchingService, etc.)
|
||||
|
||||
---
|
||||
|
||||
@ -59,11 +61,12 @@
|
||||
### 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] **MailListener**: Use Outlook Interop to connect to local Outlook, filter by allowlist, support account/folder selection, recursively process subfolders, 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.
|
||||
- [ ] Add robust logging and error handling for all infrastructure components.
|
||||
|
||||
### 2. Application Layer
|
||||
|
||||
@ -74,11 +77,12 @@
|
||||
- NotificationService
|
||||
- ArchivingService
|
||||
|
||||
- [ ] Wire up these services in DI in `Program.cs`.
|
||||
- [x] Wire up these services in DI in `Program.cs`.
|
||||
- [x] Add fallback and multi-invoice sum logic in `MatchingService.MatchAsync`.
|
||||
|
||||
### 3. Service Host
|
||||
|
||||
- [ ] Ensure all services are registered and started in the Worker.
|
||||
- [x] Ensure all services are registered and started in the Worker.
|
||||
- [ ] Implement polling and retry logic as per configuration.
|
||||
|
||||
### 4. WPF Monitor
|
||||
|
||||
17
testEnvironments.json
Normal file
17
testEnvironments.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"version": "1",
|
||||
"environments": [
|
||||
//Consulte https://aka.ms/remotetesting para obter mais detalhe
|
||||
//sobre como configurar ambientes remotos.
|
||||
//{
|
||||
// "name": "WSL Ubuntu",
|
||||
// "type": "wsl",
|
||||
// "wslDistribution": "Ubuntu"
|
||||
//},
|
||||
//{
|
||||
// "name": "Docker dotnet/sdk",
|
||||
// "type": "docker",
|
||||
// "dockerImage": "mcr.microsoft.com/dotnet/sdk"
|
||||
//}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user