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.Threading.Tasks;
|
||||||
using System.Net.Mail;
|
using System.Net.Mail;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Collections.Generic;
|
||||||
using ComplianceNFs.Core.Entities;
|
using ComplianceNFs.Core.Entities;
|
||||||
using ComplianceNFs.Core.Ports;
|
using ComplianceNFs.Core.Ports;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
namespace ComplianceNFs.Core.Application.Services
|
namespace ComplianceNFs.Core.Application.Services
|
||||||
{
|
{
|
||||||
@ -14,6 +17,7 @@ namespace ComplianceNFs.Core.Application.Services
|
|||||||
private readonly IAttachmentRepository _attachmentRepository;
|
private readonly IAttachmentRepository _attachmentRepository;
|
||||||
private readonly IXmlParser _xmlParser;
|
private readonly IXmlParser _xmlParser;
|
||||||
private readonly IPdfParser _pdfParser;
|
private readonly IPdfParser _pdfParser;
|
||||||
|
|
||||||
public InvoiceIngestionService(IMailListener mailListener, IAttachmentRepository attachmentRepository, IXmlParser xmlParser, IPdfParser pdfParser)
|
public InvoiceIngestionService(IMailListener mailListener, IAttachmentRepository attachmentRepository, IXmlParser xmlParser, IPdfParser pdfParser)
|
||||||
{
|
{
|
||||||
_mailListener = mailListener;
|
_mailListener = mailListener;
|
||||||
@ -32,7 +36,7 @@ namespace ComplianceNFs.Core.Application.Services
|
|||||||
using var stream = new MemoryStream();
|
using var stream = new MemoryStream();
|
||||||
att.ContentStream.CopyTo(stream);
|
att.ContentStream.CopyTo(stream);
|
||||||
stream.Position = 0;
|
stream.Position = 0;
|
||||||
ParsedInvoice parsed = new ParsedInvoice();
|
ParsedInvoice parsed = new();
|
||||||
if (att.Name.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
|
if (att.Name.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
|
||||||
parsed = _xmlParser.Parse(stream);
|
parsed = _xmlParser.Parse(stream);
|
||||||
else if (att.Name.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
|
else if (att.Name.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
|
||||||
@ -41,14 +45,16 @@ namespace ComplianceNFs.Core.Application.Services
|
|||||||
continue;
|
continue;
|
||||||
var invoice = new EnergyInvoice
|
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,
|
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,
|
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,
|
CnpjComp = parsed.CnpjComp,
|
||||||
CnpjVend = parsed.CnpjVend,
|
CnpjVend = parsed.CnpjVend,
|
||||||
MontNF = parsed.MontNF,
|
MontNF = parsed.MontNF,
|
||||||
PrecNF = parsed.PrecNF,
|
PrecNF = parsed.PrecNF,
|
||||||
ValorSemImpostos = parsed.ValorSemImpostos,
|
|
||||||
ValorFinalComImpostos = parsed.ValorFinalComImpostos,
|
ValorFinalComImpostos = parsed.ValorFinalComImpostos,
|
||||||
RsComp = parsed.RsComp,
|
RsComp = parsed.RsComp,
|
||||||
RsVend = parsed.RsVend,
|
RsVend = parsed.RsVend,
|
||||||
@ -59,9 +65,13 @@ namespace ComplianceNFs.Core.Application.Services
|
|||||||
Status = InvoiceStatus.Pending
|
Status = InvoiceStatus.Pending
|
||||||
};
|
};
|
||||||
await _attachmentRepository.SaveRawAsync(invoice);
|
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()
|
public Task IngestAsync()
|
||||||
{
|
{
|
||||||
_mailListener.StartListening();
|
_mailListener.StartListening();
|
||||||
@ -70,32 +80,65 @@ namespace ComplianceNFs.Core.Application.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handles matching logic for invoices
|
// Handles matching logic for invoices
|
||||||
public class MatchingService : IMatchingService
|
public class MatchingService(IAccessDbRepository accessDbRepository) : IMatchingService
|
||||||
{
|
{
|
||||||
private readonly IAccessDbRepository _accessDbRepository;
|
private readonly IAccessDbRepository _accessDbRepository = accessDbRepository;
|
||||||
public MatchingService(IAccessDbRepository accessDbRepository)
|
|
||||||
{
|
|
||||||
_accessDbRepository = accessDbRepository;
|
|
||||||
}
|
|
||||||
public Task MatchAsync(EnergyInvoice invoice)
|
public Task MatchAsync(EnergyInvoice invoice)
|
||||||
{
|
{
|
||||||
// Example: Primary match logic (simplified)
|
// 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)
|
foreach (var record in records)
|
||||||
{
|
{
|
||||||
if (record.CnpjComp == invoice.CnpjComp && record.CnpjVend == invoice.CnpjVend)
|
if (record.CnpjComp == invoice.CnpjComp && record.CnpjVend == invoice.CnpjVend)
|
||||||
{
|
{
|
||||||
var volMatch = Math.Abs(record.MontLO - invoice.MontNF) / record.MontLO <= 0.01m;
|
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) / record.PrecLO <= 0.005m;
|
var priceMatch = Math.Abs(record.PrecLO - invoice.PrecNF ?? 0) / (record.PrecLO == 0 ? 1 : record.PrecLO) <= 0.005m;
|
||||||
if (volMatch && priceMatch)
|
if (volMatch && priceMatch)
|
||||||
{
|
{
|
||||||
invoice.MatchedCodTE = record.CodTE;
|
invoice.MatchedCodTE = record.CodTE;
|
||||||
invoice.Status = InvoiceStatus.Matched;
|
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;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,11 +151,11 @@ namespace ComplianceNFs.Core.Application.Services
|
|||||||
// Example: Tax compliance check
|
// Example: Tax compliance check
|
||||||
if (invoice.Status == InvoiceStatus.Matched || invoice.Status == InvoiceStatus.FallbackMatched)
|
if (invoice.Status == InvoiceStatus.Matched || invoice.Status == InvoiceStatus.FallbackMatched)
|
||||||
{
|
{
|
||||||
var impliedTax = invoice.ValorFinalComImpostos / (invoice.ValorSemImpostos == 0 ? 1 : invoice.ValorSemImpostos) - 1;
|
decimal? impliedTax = 1 - (invoice.ValorSemImpostos / (invoice.ValorFinalComImpostos == 0 ? 1 : invoice.ValorFinalComImpostos));
|
||||||
if (Math.Abs(impliedTax - invoice.IcmsNF) > 0.01m)
|
if (Math.Abs(impliedTax - invoice.IcmsNF ?? 0) > 0.01m)
|
||||||
{
|
{
|
||||||
invoice.Status = InvoiceStatus.TaxMismatch;
|
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
|
else
|
||||||
{
|
{
|
||||||
@ -136,13 +179,10 @@ namespace ComplianceNFs.Core.Application.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handles archiving of files
|
// Handles archiving of files
|
||||||
public class ArchivingService : IArchivingService
|
public class ArchivingService(IFileArchiver fileArchiver) : IArchivingService
|
||||||
{
|
{
|
||||||
private readonly IFileArchiver _fileArchiver;
|
private readonly IFileArchiver _fileArchiver = fileArchiver;
|
||||||
public ArchivingService(IFileArchiver fileArchiver)
|
|
||||||
{
|
|
||||||
_fileArchiver = fileArchiver;
|
|
||||||
}
|
|
||||||
public Task ArchiveAsync(EnergyInvoice invoice, byte[] rawFile)
|
public Task ArchiveAsync(EnergyInvoice invoice, byte[] rawFile)
|
||||||
{
|
{
|
||||||
return _fileArchiver.ArchiveAsync(invoice, rawFile);
|
return _fileArchiver.ArchiveAsync(invoice, rawFile);
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0-preview.4.25258.110" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0-preview.4.25258.110" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@ -1,17 +1,41 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
namespace ComplianceNFs.Core.Entities
|
namespace ComplianceNFs.Core.Entities
|
||||||
{
|
{
|
||||||
public class BuyingRecord
|
public class BuyingRecord
|
||||||
{
|
{
|
||||||
public int CodTE { get; set; }
|
public BigInteger CodTE { get; set; }
|
||||||
public string CodSmartUnidade { get; set; }
|
public BigInteger? CodSmartUnidade { get; set; }
|
||||||
public int Mes { get; set; } // month as integer
|
public int Mes { get; set; }
|
||||||
public int Ano { get; set; } // year as integer
|
public DateTime? Hora_LO { get; set; }
|
||||||
public string CnpjComp { get; set; }
|
public string? Operacao { get; set; }
|
||||||
public string CnpjVend { get; set; }
|
public string? Tipo { get; set; }
|
||||||
public decimal MontLO { get; set; } // expected volume
|
public DateTime? Hora_NF { get; set; }
|
||||||
public decimal PrecLO { get; set; } // expected unit price
|
public decimal? Tempo_NF { get; set; }
|
||||||
// … other client fields omitted
|
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;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
namespace ComplianceNFs.Core.Entities
|
namespace ComplianceNFs.Core.Entities
|
||||||
{
|
{
|
||||||
public class EnergyInvoice
|
public class EnergyInvoice
|
||||||
{
|
{
|
||||||
public int InvoiceId { get; set; } // PK
|
public required string MailId { get; set; }
|
||||||
public string Filename { get; set; }
|
public required string ConversationId { get; set; }
|
||||||
public string SupplierEmail { get; set; }
|
public required string SupplierEmail { get; set; }
|
||||||
public string ConversationId { get; set; }
|
|
||||||
public DateTime ReceivedDate { get; set; }
|
public DateTime ReceivedDate { get; set; }
|
||||||
public string Md5 { get; set; }
|
public int InvoiceId { get; set; }
|
||||||
public string CnpjComp { get; set; }
|
public required string Filename { get; set; }
|
||||||
public string CnpjVend { get; set; }
|
public string? Md5 { get; set; }
|
||||||
public decimal MontNF { get; set; }
|
public string? CnpjComp { get; set; }
|
||||||
public decimal PrecNF { get; set; }
|
public string? CnpjVend { get; set; }
|
||||||
public decimal ValorSemImpostos { get; set; }
|
public decimal? MontNF { get; set; }
|
||||||
public decimal ValorFinalComImpostos { get; set; }
|
public decimal? PrecNF { get; set; }
|
||||||
public string RsComp { get; set; }
|
public decimal? ValorSemImpostos { get; set; }
|
||||||
public string RsVend { get; set; }
|
public decimal? ValorFinalComImpostos { get; set; }
|
||||||
public string NumeroNF { get; set; }
|
public string? RsComp { get; set; }
|
||||||
public decimal IcmsNF { get; set; }
|
public string? RsVend { get; set; }
|
||||||
public string UfComp { get; set; }
|
public string? NumeroNF { get; set; }
|
||||||
public string UfVend { get; set; }
|
public decimal? IcmsNF { get; set; }
|
||||||
public int? MatchedCodTE { get; set; } // FK to BuyingRecord
|
public string? UfComp { get; set; }
|
||||||
|
public string? UfVend { get; set; }
|
||||||
|
public BigInteger? MatchedCodTE { get; set; }
|
||||||
public InvoiceStatus Status { get; set; }
|
public InvoiceStatus Status { get; set; }
|
||||||
public string DiscrepancyNotes { get; set; }
|
public string? DiscrepancyNotes { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum InvoiceStatus
|
public enum InvoiceStatus
|
||||||
|
|||||||
@ -2,17 +2,16 @@ namespace ComplianceNFs.Core.Entities
|
|||||||
{
|
{
|
||||||
public class ParsedInvoice
|
public class ParsedInvoice
|
||||||
{
|
{
|
||||||
public string CnpjComp { get; set; }
|
public string? CnpjComp { get; set; }
|
||||||
public string CnpjVend { get; set; }
|
public string? CnpjVend { get; set; }
|
||||||
public decimal MontNF { get; set; }
|
public decimal? MontNF { get; set; }
|
||||||
public decimal PrecNF { get; set; }
|
public decimal? PrecNF { get; set; }
|
||||||
public decimal ValorSemImpostos { get; set; }
|
public decimal? ValorFinalComImpostos { get; set; }
|
||||||
public decimal ValorFinalComImpostos { get; set; }
|
public string? RsComp { get; set; }
|
||||||
public string RsComp { get; set; }
|
public string? RsVend { get; set; }
|
||||||
public string RsVend { get; set; }
|
public string? NumeroNF { get; set; }
|
||||||
public string NumeroNF { get; set; }
|
public decimal? IcmsNF { get; set; }
|
||||||
public decimal IcmsNF { get; set; }
|
public string? UfComp { get; set; }
|
||||||
public string UfComp { get; set; }
|
public string? UfVend { get; set; }
|
||||||
public string UfVend { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,8 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net.Mail;
|
using System.Net.Mail;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace ComplianceNFs.Core.Ports
|
namespace ComplianceNFs.Core.Ports
|
||||||
@ -24,14 +26,14 @@ namespace ComplianceNFs.Core.Ports
|
|||||||
|
|
||||||
public interface IAccessDbRepository
|
public interface IAccessDbRepository
|
||||||
{
|
{
|
||||||
IEnumerable<Entities.BuyingRecord> GetByUnidade(string codSmartUnidade);
|
IEnumerable<Entities.BuyingRecord> GetByCnpj(string codSmartUnidade);
|
||||||
IEnumerable<Entities.BuyingRecord> GetByUnidadeAndMonth(string codSmartUnidade, int month, int year);
|
IEnumerable<Entities.BuyingRecord> GetByCnpjAndMonth(string codSmartUnidade, int refMonth);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IAttachmentRepository
|
public interface IAttachmentRepository
|
||||||
{
|
{
|
||||||
Task SaveRawAsync(Entities.EnergyInvoice invoice);
|
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
|
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">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
@ -12,6 +12,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" Version="3.2.0" />
|
<PackageReference Include="coverlet.collector" Version="3.2.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.1" />
|
<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" Version="2.4.2" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@ -22,6 +23,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ComplianceNFs.Infrastructure\ComplianceNFs.Infrastructure.csproj" />
|
<ProjectReference Include="..\ComplianceNFs.Infrastructure\ComplianceNFs.Infrastructure.csproj" />
|
||||||
|
<ProjectReference Include="..\ComplianceNFs.Monitor\ComplianceNFs.Monitor.csproj" />
|
||||||
|
<ProjectReference Include="..\ComplianceNFs.Service\ComplianceNFs.Service.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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 archiver = new FileArchiver(_testBasePath);
|
||||||
var invoice = new EnergyInvoice
|
var invoice = new EnergyInvoice
|
||||||
{
|
{
|
||||||
|
MailId = "test-mail-id",
|
||||||
|
ConversationId = "test-conv-id",
|
||||||
|
SupplierEmail = "test@supplier.com",
|
||||||
Filename = "testfile.txt",
|
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 };
|
var data = new byte[] { 1, 2, 3, 4 };
|
||||||
|
|
||||||
@ -44,8 +50,14 @@ namespace ComplianceNFs.Infrastructure.Tests
|
|||||||
var archiver = new FileArchiver(_testBasePath);
|
var archiver = new FileArchiver(_testBasePath);
|
||||||
var invoice = new EnergyInvoice
|
var invoice = new EnergyInvoice
|
||||||
{
|
{
|
||||||
|
MailId = "test-mail-id",
|
||||||
|
ConversationId = "test-conv-id",
|
||||||
|
SupplierEmail = "test@supplier.com",
|
||||||
Filename = "testfile.txt",
|
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 data1 = new byte[] { 1, 2, 3 };
|
||||||
var data2 = new byte[] { 9, 8, 7 };
|
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
|
namespace ComplianceNFs.Infrastructure.Archiving
|
||||||
{
|
{
|
||||||
// Moves files to archive folders by status
|
// Moves files to archive folders by status
|
||||||
public class FileArchiver : IFileArchiver
|
public class FileArchiver(string basePath) : IFileArchiver
|
||||||
{
|
{
|
||||||
private readonly string _basePath;
|
private readonly string _basePath = basePath;
|
||||||
public FileArchiver(string basePath)
|
|
||||||
{
|
|
||||||
_basePath = basePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ArchiveAsync(EnergyInvoice invoice, byte[] rawFile)
|
public async Task ArchiveAsync(EnergyInvoice invoice, byte[] rawFile)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -8,13 +8,15 @@
|
|||||||
<PackageReference Include="MailKit" Version="4.12.1" />
|
<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.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.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="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="Npgsql" Version="9.0.3" />
|
<PackageReference Include="Npgsql" Version="9.0.3" />
|
||||||
<PackageReference Include="System.Data.OleDb" Version="10.0.0-preview.4.25258.110" />
|
<PackageReference Include="System.Data.OleDb" Version="10.0.0-preview.4.25258.110" />
|
||||||
|
<PackageReference Include="Unimake.DFe" Version="20250610.1145.39" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@ -4,54 +4,106 @@ using System.Collections.Generic;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ComplianceNFs.Core.Ports;
|
using ComplianceNFs.Core.Ports;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using MailKit.Net.Imap;
|
using Outlook = Microsoft.Office.Interop.Outlook;
|
||||||
using MailKit.Security;
|
|
||||||
using MimeKit;
|
|
||||||
|
|
||||||
namespace ComplianceNFs.Infrastructure.Mail
|
namespace ComplianceNFs.Infrastructure.Mail
|
||||||
{
|
{
|
||||||
public class MailListener : IMailListener
|
public class MailListener : IMailListener
|
||||||
{
|
{
|
||||||
public event Action<MailMessage> NewMailReceived;
|
public event Action<MailMessage> NewMailReceived = delegate { };
|
||||||
private readonly IConfiguration _config;
|
private readonly IConfiguration _config;
|
||||||
private ImapClient _client;
|
|
||||||
private readonly List<string> _allowList;
|
private readonly List<string> _allowList;
|
||||||
private bool _listening;
|
private bool _listening;
|
||||||
|
|
||||||
public MailListener(IConfiguration config)
|
public MailListener(IConfiguration config)
|
||||||
{
|
{
|
||||||
_config = 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()
|
public void StartListening()
|
||||||
{
|
{
|
||||||
if (_listening) return;
|
if (_listening) return;
|
||||||
_listening = true;
|
_listening = true;
|
||||||
Task.Run(async () =>
|
Task.Run(() =>
|
||||||
{
|
{
|
||||||
_client = new ImapClient();
|
try
|
||||||
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 outlookApp = new Outlook.Application();
|
||||||
var senderAddress = message.From.Mailboxes.FirstOrDefault()?.Address;
|
Outlook.NameSpace outlookNs = outlookApp.GetNamespace("MAPI");
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(senderAddress) && _allowList.Contains(senderAddress))
|
// 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
|
||||||
{
|
{
|
||||||
var mailMsg = new MailMessage
|
rootFolder = outlookNs.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox).Parent as Outlook.MAPIFolder;
|
||||||
{
|
|
||||||
From = new MailAddress(senderAddress), // Fix for CS1922 and CS8670
|
|
||||||
Subject = message.Subject,
|
|
||||||
Body = message.TextBody
|
|
||||||
};
|
|
||||||
NewMailReceived?.Invoke(mailMsg);
|
|
||||||
}
|
}
|
||||||
|
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),
|
||||||
|
Subject = item.Subject,
|
||||||
|
Body = item.Body
|
||||||
|
};
|
||||||
|
NewMailReceived?.Invoke(mailMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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}");
|
||||||
}
|
}
|
||||||
// For demo: not a real polling loop. Add timer/polling for production.
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
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)
|
public ParsedInvoice Parse(Stream pdfStream)
|
||||||
{
|
{
|
||||||
// Minimal demo: just read bytes as text (replace with real PDF parsing in production)
|
// 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);
|
pdfStream.CopyTo(ms);
|
||||||
var text = System.Text.Encoding.UTF8.GetString(ms.ToArray());
|
var text = System.Text.Encoding.UTF8.GetString(ms.ToArray());
|
||||||
// Example: extract CNPJ and values using regex (replace with real patterns)
|
// Example: extract CNPJ and values using regex (replace with real patterns)
|
||||||
var cnpjComp = Regex.Match(text, @"CNPJComp: (\d{14})").Groups[1].Value;
|
var cnpjComp = CnpjCompRegex.Match(text).Groups[1].Value;
|
||||||
var cnpjVend = Regex.Match(text, @"CNPJVend: (\d{14})").Groups[1].Value;
|
var cnpjVend = CnpjVendRegex.Match(text).Groups[1].Value;
|
||||||
var montNF = decimal.TryParse(Regex.Match(text, @"MontNF: ([\d,.]+)").Groups[1].Value, out var m) ? m : 0;
|
var montNF = decimal.TryParse(MontNFRegex.Match(text).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 precNF = decimal.TryParse(PrecNFRegex.Match(text).Groups[1].Value, out var p) ? p : 0;
|
||||||
return new ParsedInvoice
|
return new ParsedInvoice
|
||||||
{
|
{
|
||||||
CnpjComp = cnpjComp,
|
CnpjComp = cnpjComp,
|
||||||
@ -26,6 +31,12 @@ namespace ComplianceNFs.Infrastructure.Parsers
|
|||||||
PrecNF = precNF
|
PrecNF = precNF
|
||||||
// ...fill other fields as needed
|
// ...fill other fields as needed
|
||||||
};
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Regex GeneratedRegex(string pattern)
|
||||||
|
{
|
||||||
|
return new Regex(pattern);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,33 +1,133 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
using ComplianceNFs.Core.Entities;
|
using ComplianceNFs.Core.Entities;
|
||||||
using ComplianceNFs.Core.Ports;
|
using ComplianceNFs.Core.Ports;
|
||||||
|
using Unimake.Business.DFe.Xml.NFe;
|
||||||
|
|
||||||
namespace ComplianceNFs.Infrastructure.Parsers
|
namespace ComplianceNFs.Infrastructure.Parsers
|
||||||
{
|
{
|
||||||
// Placeholder: fill in actual XML parsing logic
|
|
||||||
public class XmlParser : IXmlParser
|
public class XmlParser : IXmlParser
|
||||||
{
|
{
|
||||||
public ParsedInvoice Parse(Stream xmlStream)
|
public ParsedInvoice Parse(Stream xmlStream)
|
||||||
{
|
{
|
||||||
// Use System.Xml to parse known elements
|
string xml = ReadXmlFromStream(xmlStream);
|
||||||
var doc = new System.Xml.XmlDocument();
|
var nfeProc = Unimake.Business.DFe.Utility.XMLUtility.Deserializar<NfeProc>(xml);
|
||||||
doc.Load(xmlStream);
|
var infNFe = nfeProc.NFe.InfNFe.First();
|
||||||
var invoice = new ParsedInvoice
|
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,
|
CnpjComp = FormatCnpjOrCpf(comprador.CNPJ ?? comprador.CPF),
|
||||||
CnpjVend = doc.SelectSingleNode("//CNPJVend")?.InnerText,
|
CnpjVend = FormatCnpjOrCpf(vendedor.CNPJ),
|
||||||
MontNF = decimal.TryParse(doc.SelectSingleNode("//MontNF")?.InnerText, out var mont) ? mont : 0,
|
MontNF = montanteOperacao,
|
||||||
PrecNF = decimal.TryParse(doc.SelectSingleNode("//PrecNF")?.InnerText, out var prec) ? prec : 0,
|
PrecNF = valorUnitario,
|
||||||
ValorSemImpostos = decimal.TryParse(doc.SelectSingleNode("//ValorSemImpostos")?.InnerText, out var vsi) ? vsi : 0,
|
ValorFinalComImpostos = valorFinalComImpostos,
|
||||||
ValorFinalComImpostos = decimal.TryParse(doc.SelectSingleNode("//ValorFinalComImpostos")?.InnerText, out var vfi) ? vfi : 0,
|
RsComp = DecodeHtml(comprador?.XNome),
|
||||||
RsComp = doc.SelectSingleNode("//RsComp")?.InnerText,
|
RsVend = DecodeHtml(vendedor?.XNome),
|
||||||
RsVend = doc.SelectSingleNode("//RsVend")?.InnerText,
|
NumeroNF = infNFe.Ide.NNF.ToString(),
|
||||||
NumeroNF = doc.SelectSingleNode("//NumeroNF")?.InnerText,
|
IcmsNF = icmsNF,
|
||||||
IcmsNF = decimal.TryParse(doc.SelectSingleNode("//IcmsNF")?.InnerText, out var icms) ? icms : 0,
|
UfComp = comprador?.EnderDest?.UF.ToString(),
|
||||||
UfComp = doc.SelectSingleNode("//UfComp")?.InnerText,
|
UfVend = vendedor?.EnderEmit?.UF.ToString()
|
||||||
UfVend = doc.SelectSingleNode("//UfVend")?.InnerText
|
|
||||||
};
|
};
|
||||||
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.Collections.Generic;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Data.OleDb;
|
using System.Data.OleDb;
|
||||||
|
using System.Numerics;
|
||||||
using ComplianceNFs.Core.Entities;
|
using ComplianceNFs.Core.Entities;
|
||||||
using ComplianceNFs.Core.Ports;
|
using ComplianceNFs.Core.Ports;
|
||||||
|
|
||||||
namespace ComplianceNFs.Infrastructure.Repositories
|
namespace ComplianceNFs.Infrastructure.Repositories
|
||||||
{
|
{
|
||||||
// Placeholder: fill in actual SQL and mapping logic
|
// Placeholder: fill in actual SQL and mapping logic
|
||||||
public class AccessDbRepository : IAccessDbRepository
|
public class AccessDbRepository(string connectionString) : IAccessDbRepository
|
||||||
{
|
{
|
||||||
private readonly string _connectionString;
|
private readonly string _connectionString = connectionString;
|
||||||
public AccessDbRepository(string connectionString)
|
|
||||||
{
|
|
||||||
_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>();
|
var results = new List<BuyingRecord>();
|
||||||
using (var conn = new OleDbConnection(_connectionString))
|
using (var conn = new OleDbConnection(_connectionString))
|
||||||
{
|
{
|
||||||
conn.Open();
|
conn.Open();
|
||||||
var cmd = conn.CreateCommand();
|
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.CommandText = $@"SELECT {BuyingRecordColumns} FROM Dados_TE WHERE ((Dados_TE.CNPJ_comp)=@CNPJ_comp);";
|
||||||
cmd.Parameters.AddWithValue("@CodSmartUnidade", codSmartUnidade);
|
cmd.Parameters.AddWithValue("@CNPJ_comp", CNPJ_comp);
|
||||||
using (var reader = cmd.ExecuteReader())
|
using var reader = cmd.ExecuteReader();
|
||||||
|
while (reader.Read())
|
||||||
{
|
{
|
||||||
while (reader.Read())
|
results.Add(MapBuyingRecord(reader));
|
||||||
{
|
|
||||||
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;
|
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>();
|
var results = new List<BuyingRecord>();
|
||||||
using (var conn = new OleDbConnection(_connectionString))
|
using (var conn = new OleDbConnection(_connectionString))
|
||||||
{
|
{
|
||||||
conn.Open();
|
conn.Open();
|
||||||
var cmd = conn.CreateCommand();
|
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.CommandText = $@"SELECT {BuyingRecordColumns} FROM Dados_TE WHERE ((Dados_TE.CNPJ_comp)=@CNPJ_comp) AND ((Dados_TE.Mes)=@MesRef);";
|
||||||
cmd.Parameters.AddWithValue("@CodSmartUnidade", codSmartUnidade);
|
cmd.Parameters.AddWithValue("@CNPJ_comp", CNPJ_comp);
|
||||||
cmd.Parameters.AddWithValue("@Mes", month);
|
cmd.Parameters.AddWithValue("@MesRef", refMonth);
|
||||||
cmd.Parameters.AddWithValue("@Ano", year);
|
using var reader = cmd.ExecuteReader();
|
||||||
using (var reader = cmd.ExecuteReader())
|
while (reader.Read())
|
||||||
{
|
{
|
||||||
while (reader.Read())
|
results.Add(MapBuyingRecord(reader));
|
||||||
{
|
|
||||||
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;
|
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,67 +4,60 @@ using ComplianceNFs.Core.Entities;
|
|||||||
using ComplianceNFs.Core.Ports;
|
using ComplianceNFs.Core.Ports;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
namespace ComplianceNFs.Infrastructure.Repositories
|
namespace ComplianceNFs.Infrastructure.Repositories
|
||||||
{
|
{
|
||||||
// Placeholder: fill in actual SQL and mapping logic
|
// Placeholder: fill in actual SQL and mapping logic
|
||||||
public class AttachmentRepository : IAttachmentRepository
|
public class AttachmentRepository(string connectionString) : IAttachmentRepository
|
||||||
{
|
{
|
||||||
private readonly string _connectionString;
|
private readonly string _connectionString = connectionString;
|
||||||
public AttachmentRepository(string connectionString)
|
|
||||||
{
|
|
||||||
_connectionString = connectionString;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SaveRawAsync(EnergyInvoice invoice)
|
public async Task SaveRawAsync(EnergyInvoice invoice)
|
||||||
{
|
{
|
||||||
using (var conn = new NpgsqlConnection(_connectionString))
|
using var conn = new NpgsqlConnection(_connectionString);
|
||||||
{
|
await conn.OpenAsync();
|
||||||
await conn.OpenAsync();
|
var cmd = conn.CreateCommand();
|
||||||
var cmd = conn.CreateCommand();
|
cmd.CommandText = @"INSERT INTO attachments (
|
||||||
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
|
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 (
|
) 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
|
@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("@filename", invoice.Filename);
|
||||||
cmd.Parameters.AddWithValue("@supplier_email", invoice.SupplierEmail);
|
cmd.Parameters.AddWithValue("@supplier_email", invoice.SupplierEmail);
|
||||||
cmd.Parameters.AddWithValue("@conversation_id", (object?)invoice.ConversationId ?? DBNull.Value);
|
cmd.Parameters.AddWithValue("@conversation_id", (object?)invoice.ConversationId ?? DBNull.Value);
|
||||||
cmd.Parameters.AddWithValue("@received_date", invoice.ReceivedDate);
|
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_comp", (object?)invoice.CnpjComp ?? DBNull.Value);
|
||||||
cmd.Parameters.AddWithValue("@cnpj_vend", (object?)invoice.CnpjVend ?? DBNull.Value);
|
cmd.Parameters.AddWithValue("@cnpj_vend", (object?)invoice.CnpjVend ?? DBNull.Value);
|
||||||
cmd.Parameters.AddWithValue("@mont_nf", invoice.MontNF);
|
cmd.Parameters.AddWithValue("@mont_nf", (object?)invoice.MontNF ?? DBNull.Value);
|
||||||
cmd.Parameters.AddWithValue("@prec_nf", invoice.PrecNF);
|
cmd.Parameters.AddWithValue("@prec_nf", (object?)invoice.PrecNF ?? DBNull.Value);
|
||||||
cmd.Parameters.AddWithValue("@valor_sem_imp", invoice.ValorSemImpostos);
|
cmd.Parameters.AddWithValue("@valor_sem_imp", (object?)invoice.ValorSemImpostos ?? DBNull.Value);
|
||||||
cmd.Parameters.AddWithValue("@valor_com_imp", invoice.ValorFinalComImpostos);
|
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_comp", (object?)invoice.RsComp ?? DBNull.Value);
|
||||||
cmd.Parameters.AddWithValue("@rs_vend", (object?)invoice.RsVend ?? 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("@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_comp", (object?)invoice.UfComp ?? DBNull.Value);
|
||||||
cmd.Parameters.AddWithValue("@uf_vend", (object?)invoice.UfVend ?? 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("@matched_cod_te", (object?)invoice.MatchedCodTE ?? DBNull.Value);
|
||||||
cmd.Parameters.AddWithValue("@status", invoice.Status.ToString());
|
cmd.Parameters.AddWithValue("@status", invoice.Status.ToString());
|
||||||
cmd.Parameters.AddWithValue("@discrepancy", (object?)invoice.DiscrepancyNotes ?? DBNull.Value);
|
cmd.Parameters.AddWithValue("@discrepancy", (object?)invoice.DiscrepancyNotes ?? DBNull.Value);
|
||||||
cmd.Parameters.AddWithValue("@metadata", Newtonsoft.Json.JsonConvert.SerializeObject(invoice));
|
cmd.Parameters.AddWithValue("@metadata", Newtonsoft.Json.JsonConvert.SerializeObject(invoice));
|
||||||
await cmd.ExecuteNonQueryAsync();
|
await cmd.ExecuteNonQueryAsync();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateMatchAsync(int invoiceId, int matchedCodTE, InvoiceStatus status, string notes)
|
public async Task UpdateMatchAsync(int invoiceId, BigInteger matchedCodTE, InvoiceStatus status, string notes)
|
||||||
{
|
{
|
||||||
using (var conn = new NpgsqlConnection(_connectionString))
|
using var conn = new NpgsqlConnection(_connectionString);
|
||||||
{
|
await conn.OpenAsync();
|
||||||
await conn.OpenAsync();
|
var cmd = conn.CreateCommand();
|
||||||
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.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("@matched_cod_te", matchedCodTE);
|
cmd.Parameters.AddWithValue("@status", status.ToString());
|
||||||
cmd.Parameters.AddWithValue("@status", status.ToString());
|
cmd.Parameters.AddWithValue("@discrepancy", (object?)notes ?? DBNull.Value);
|
||||||
cmd.Parameters.AddWithValue("@discrepancy", (object?)notes ?? DBNull.Value);
|
cmd.Parameters.AddWithValue("@invoice_id", invoiceId);
|
||||||
cmd.Parameters.AddWithValue("@invoice_id", invoiceId);
|
await cmd.ExecuteNonQueryAsync();
|
||||||
await cmd.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0-preview.4.25258.110" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0-preview.4.25258.110" />
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<Window x:Class="ComplianceNFs.Monitor.MainWindow"
|
<Window x:Class="ComplianceNFs.Monitor.MainWindow"
|
||||||
|
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
@ -17,6 +18,8 @@
|
|||||||
<GridViewColumn Header="Timestamp" DisplayMemberBinding="{Binding ReceivedDate}" Width="150"/>
|
<GridViewColumn Header="Timestamp" DisplayMemberBinding="{Binding ReceivedDate}" Width="150"/>
|
||||||
<GridViewColumn Header="Filename" DisplayMemberBinding="{Binding Filename}" Width="250"/>
|
<GridViewColumn Header="Filename" DisplayMemberBinding="{Binding Filename}" Width="250"/>
|
||||||
<GridViewColumn Header="Status" DisplayMemberBinding="{Binding Status}" Width="120"/>
|
<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>
|
</GridView>
|
||||||
</ListView.View>
|
</ListView.View>
|
||||||
</ListView>
|
</ListView>
|
||||||
|
|||||||
@ -8,17 +8,26 @@ using System.Windows.Media;
|
|||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
using System.Windows.Navigation;
|
using System.Windows.Navigation;
|
||||||
using System.Windows.Shapes;
|
using System.Windows.Shapes;
|
||||||
|
using ComplianceNFs.Core.Application;
|
||||||
|
|
||||||
namespace ComplianceNFs.Monitor;
|
namespace ComplianceNFs.Monitor
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Interaction logic for MainWindow.xaml
|
|
||||||
/// </summary>
|
|
||||||
public partial class MainWindow : Window
|
|
||||||
{
|
{
|
||||||
public MainWindow()
|
/// <summary>
|
||||||
|
/// Interaction logic for MainWindow.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
public MainWindow()
|
||||||
DataContext = new MonitorViewModel();
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
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.ComponentModel;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
using ComplianceNFs.Core.Application;
|
||||||
using ComplianceNFs.Core.Entities;
|
using ComplianceNFs.Core.Entities;
|
||||||
|
|
||||||
namespace ComplianceNFs.Monitor
|
namespace ComplianceNFs.Monitor
|
||||||
{
|
{
|
||||||
public class MonitorViewModel : INotifyPropertyChanged
|
public class MonitorViewModel : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
public ObservableCollection<EnergyInvoice> RecentInvoices { get; } = new();
|
public ObservableCollection<EnergyInvoice> RecentInvoices { get; } = [];
|
||||||
public ICommand ForceScanCommand { 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());
|
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
|
// 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;
|
public bool CanExecute(object? parameter) => parameter != null && (canExecute == null || canExecute(parameter));
|
||||||
private readonly Func<object, bool> _canExecute;
|
public void Execute(object? parameter)
|
||||||
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
|
|
||||||
{
|
{
|
||||||
_execute = execute;
|
if (parameter != null) { execute(parameter); }
|
||||||
_canExecute = canExecute;
|
|
||||||
}
|
}
|
||||||
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">
|
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<UserSecretsId>dotnet-ComplianceNFs.Service-386f44bf-03c8-490c-96b9-3852d8c3d01a</UserSecretsId>
|
<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.Mail;
|
||||||
using ComplianceNFs.Infrastructure.Parsers;
|
using ComplianceNFs.Infrastructure.Parsers;
|
||||||
using ComplianceNFs.Infrastructure.Archiving;
|
using ComplianceNFs.Infrastructure.Archiving;
|
||||||
|
using ComplianceNFs.Core.Application.Services;
|
||||||
|
using ComplianceNFs.Core.Application;
|
||||||
|
|
||||||
IHost host = Host.CreateDefaultBuilder(args)
|
IHost host = Host.CreateDefaultBuilder(args)
|
||||||
.ConfigureAppConfiguration((hostingContext, config) =>
|
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||||
@ -17,18 +19,35 @@ IHost host = Host.CreateDefaultBuilder(args)
|
|||||||
{
|
{
|
||||||
var config = context.Configuration;
|
var config = context.Configuration;
|
||||||
// Register infrastructure
|
// Register infrastructure
|
||||||
services.AddSingleton<IAccessDbRepository>(sp => new AccessDbRepository(config["AccessConnectionString"]));
|
services.AddSingleton<IAccessDbRepository>(sp =>
|
||||||
services.AddSingleton<IAttachmentRepository>(sp => new AttachmentRepository(config["PostgresConnectionString"]));
|
{
|
||||||
|
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<IMailListener, MailListener>();
|
||||||
services.AddSingleton<IXmlParser, XmlParser>();
|
services.AddSingleton<IXmlParser, XmlParser>();
|
||||||
services.AddSingleton<IPdfParser, PdfParser>();
|
services.AddSingleton<IPdfParser, PdfParser>();
|
||||||
services.AddSingleton<IFileArchiver>(sp => new FileArchiver(config["ArchiveBasePath"]));
|
services.AddSingleton<IFileArchiver>(sp =>
|
||||||
// Register application services
|
{
|
||||||
services.AddSingleton<ComplianceNFs.Core.Application.Services.InvoiceIngestionService>();
|
var archiveBasePath = config["ArchiveBasePath"];
|
||||||
services.AddSingleton<ComplianceNFs.Core.Application.Services.MatchingService>();
|
if (string.IsNullOrWhiteSpace(archiveBasePath))
|
||||||
services.AddSingleton<ComplianceNFs.Core.Application.Services.ComplianceService>();
|
throw new InvalidOperationException("ArchiveBasePath is missing in configuration.");
|
||||||
services.AddSingleton<ComplianceNFs.Core.Application.Services.NotificationService>();
|
return new FileArchiver(archiveBasePath);
|
||||||
services.AddSingleton<ComplianceNFs.Core.Application.Services.ArchivingService>();
|
});
|
||||||
|
services.AddSingleton<IInvoiceIngestionService, InvoiceIngestionService>();
|
||||||
|
services.AddSingleton<IMatchingService, MatchingService>();
|
||||||
|
services.AddSingleton<IComplianceService, ComplianceService>();
|
||||||
|
services.AddSingleton<INotificationService, NotificationService>();
|
||||||
|
services.AddSingleton<IArchivingService, ArchivingService>();
|
||||||
services.AddHostedService<Worker>();
|
services.AddHostedService<Worker>();
|
||||||
})
|
})
|
||||||
.Build();
|
.Build();
|
||||||
|
|||||||
@ -1,19 +1,51 @@
|
|||||||
|
using ComplianceNFs.Core.Entities;
|
||||||
|
using ComplianceNFs.Core.Application;
|
||||||
|
using ComplianceNFs.Core.Application.Services;
|
||||||
|
|
||||||
namespace ComplianceNFs.Service;
|
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;
|
private readonly ILogger<Worker> _logger = logger;
|
||||||
|
private readonly IInvoiceIngestionService _ingestionService = ingestionService;
|
||||||
public Worker(ILogger<Worker> logger)
|
private readonly IMatchingService _matchingService = matchingService;
|
||||||
{
|
private readonly IComplianceService _complianceService = complianceService;
|
||||||
_logger = logger;
|
private readonly INotificationService _notificationService = notificationService;
|
||||||
}
|
private readonly IArchivingService _archivingService = archivingService;
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
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)
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
|
|
||||||
await Task.Delay(1000, stoppingToken);
|
await Task.Delay(1000, stoppingToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,92 @@
|
|||||||
{
|
{
|
||||||
"AccessConnectionString": "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=X:\\Back\\Controle NFs\\Dados.accdb;",
|
"AccessConnectionString": "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=X:\\Middle\\Informativo Setorial\\Modelo Word\\BD1_dados cadastrais e faturas.accdb;",
|
||||||
"PostgresConnectionString": "Host=…;Port=5432;Database=…;Username=…;Password=…;",
|
"PostgresConnectionString": "Host=192.168.10.248;Port=5432;Database=pipefy_move_cards;User Id=postgres;Password=gds21;",
|
||||||
"Mail": {
|
"Mail": {
|
||||||
"Host": "outlook.office365.com",
|
"SupplierAllowList": [
|
||||||
"Port": 993,
|
"faturamento.energia@2wenergia.com.br",
|
||||||
"User": "service@yourcompany.com",
|
"faturamento.energia@2wecobank.com.br",
|
||||||
"Password": "…",
|
"jaqueliny.oliveira@2wenergia.com.br",
|
||||||
"SupplierAllowList": [ "faturamento@…", "nfe@…" ]
|
"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,
|
"PollingIntervalMinutes": 2,
|
||||||
"Tolerances": {
|
"Tolerances": {
|
||||||
@ -14,7 +94,7 @@
|
|||||||
"PricePercent": 0.5,
|
"PricePercent": 0.5,
|
||||||
"TaxPercent": 1.0
|
"TaxPercent": 1.0
|
||||||
},
|
},
|
||||||
"ArchiveBasePath": "X:\\Back\\Controle NFs\\Fs\\",
|
"ArchiveBasePath": "X:\\Back\\Controle NFs\\NFs\\",
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
|
|||||||
@ -11,14 +11,13 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ComplianceNFs.Monitor", "Co
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ComplianceNFs.Service", "ComplianceNFs.Service\ComplianceNFs.Service.csproj", "{72BFBD25-0814-420D-8524-21448479A771}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ComplianceNFs.Service", "ComplianceNFs.Service\ComplianceNFs.Service.csproj", "{72BFBD25-0814-420D-8524-21448479A771}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ComplianceNFs.Infrastructure.Tests", "ComplianceNFs.Infrastructure.Tests\ComplianceNFs.Infrastructure.Tests.csproj", "{E25FC800-6D77-465C-AD05-95D947D472E9}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
|
||||||
HideSolutionNode = FALSE
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{43625B9E-D806-490C-A65C-DEC88263B563}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{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}.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}.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.ActiveCfg = Release|Any CPU
|
||||||
{72BFBD25-0814-420D-8524-21448479A771}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@ -8,7 +8,8 @@
|
|||||||
|
|
||||||
### ComplianceNFs.Infrastructure/Mail/MailListener.cs
|
### 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
|
### ComplianceNFs.Infrastructure/Parsers/PdfParser.cs
|
||||||
|
|
||||||
@ -35,6 +36,7 @@
|
|||||||
- [x] InvoiceIngestionService: Implement ingestion logic and subscribe to NewMailReceived
|
- [x] InvoiceIngestionService: Implement ingestion logic and subscribe to NewMailReceived
|
||||||
|
|
||||||
- [x] MatchingService: Implement invoice matching logic
|
- [x] MatchingService: Implement invoice matching logic
|
||||||
|
- [x] Add fallback and multi-invoice sum logic in MatchAsync
|
||||||
|
|
||||||
- [x] ComplianceService: Implement compliance validation logic
|
- [x] ComplianceService: Implement compliance validation logic
|
||||||
|
|
||||||
@ -50,7 +52,7 @@
|
|||||||
|
|
||||||
### ComplianceNFs.Service/Program.cs
|
### ComplianceNFs.Service/Program.cs
|
||||||
|
|
||||||
- [ ] Register application services (InvoiceIngestionService, MatchingService, etc.)
|
- [x] Register application services (InvoiceIngestionService, MatchingService, etc.)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -59,11 +61,12 @@
|
|||||||
### 1. Infrastructure Layer
|
### 1. Infrastructure Layer
|
||||||
|
|
||||||
- [x] **FileArchiver**: Implement logic to create subfolders by `InvoiceStatus` and move files accordingly.
|
- [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] **PdfParser**: Integrate iTextSharp or PdfSharp, extract invoice data using regex.
|
||||||
- [x] **XmlParser**: Use System.Xml to parse invoice XMLs and map to `ParsedInvoice`.
|
- [x] **XmlParser**: Use System.Xml to parse invoice XMLs and map to `ParsedInvoice`.
|
||||||
- [x] **AccessDbRepository**: Implement queries to Access DB for buying records.
|
- [x] **AccessDbRepository**: Implement queries to Access DB for buying records.
|
||||||
- [x] **AttachmentRepository**: Implement Postgres insert/update for invoice attachments.
|
- [x] **AttachmentRepository**: Implement Postgres insert/update for invoice attachments.
|
||||||
|
- [ ] Add robust logging and error handling for all infrastructure components.
|
||||||
|
|
||||||
### 2. Application Layer
|
### 2. Application Layer
|
||||||
|
|
||||||
@ -74,11 +77,12 @@
|
|||||||
- NotificationService
|
- NotificationService
|
||||||
- ArchivingService
|
- 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
|
### 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.
|
- [ ] Implement polling and retry logic as per configuration.
|
||||||
|
|
||||||
### 4. WPF Monitor
|
### 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