Passa al contenuto principale

Servizi β€” dominio inv

🎯 Cosa fa​

Il package TrainingHub.BackOffice.Services.Invoicing contiene la logica applicativa del dominio fatturazione: numerazione, invio SDI via Aruba, generazione XML FatturaPA, piano rate e storico stati. Tre servizi principali, tutti mockabili per testing.

πŸ—ΊοΈ Servizi​

ServizioResponsabilitΓ 
IInvoiceService / InvoiceServiceOrchestrazione fattura (numerazione, send, refresh, rate, storico)
IArubaSdiService / ArubaSdiServiceIntegrazione HTTP con API Aruba (invio + polling stato)
IFatturaPaXmlGenerator / FatturaPaXmlGeneratorGenerazione dell'XML FatturaPA 1.2

Helper statico di mapping:

HelperRuolo
InvoiceStatusExtensionsMappa codici notifica SDI (RC, NS, ecc.) a InvoiceStatus
InvoiceActionRunnerPattern condiviso try β†’ action β†’ toast successo β†’ reload β†’ catch toast errore. Usato da Invoice.razor.cs (grid) e da InvoiceFormPopup.StepSummary.cs per Send/Refresh/PreviewXml

Tutti registrati in Program.cs come scoped/transient per DI.

πŸ”§ API pubblica​

IInvoiceService​

Task<int> GetNextInvoiceNumberAsync(Guid issuerId, int year, CancellationToken ct);
static string BuildInvoiceCode(int year, int number, string? sezionale = null);
Task SendToArubaAsync(Guid invoiceId, CancellationToken ct);
Task RefreshStatusFromArubaAsync(Guid invoiceId, CancellationToken ct);
Task AddStatusHistoryAsync(Guid invoiceId, InvoiceStatus status, string? arubaStatus, string? notes, string? createdBy, CancellationToken ct);
Task<IEnumerable<invoiceStatusHistory>> GetStatusHistoryAsync(Guid invoiceId, CancellationToken ct);
Task<IEnumerable<invoicePayment>> GetPaymentsAsync(Guid invoiceId, CancellationToken ct);
Task SavePaymentsAsync(Guid invoiceId, IEnumerable<invoicePayment> payments, CancellationToken ct);

Helper statici puri (testabili senza DB):

public static int ComputeNextNumber(IEnumerable<invoice> existing, int year);
public static decimal ComputePaymentAmount(decimal totalAmount, decimal percentage);

IArubaSdiService​

Task<string> SendInvoiceAsync(issuer issuer, string xmlContent, string fileName, CancellationToken ct);
Task<ArubaStatusResult> CheckStatusAsync(issuer issuer, string transmissionId, CancellationToken ct);

public record ArubaStatusResult(
InvoiceStatus Status,
string? SdiProtocolNumber,
string? RawArubaStatus,
string? NotificationXml);

L'interfaccia accetta l'oggetto issuer perchΓ© le credenziali Aruba (username, password, ambiente) sono per-emittente: lo stesso tenant puΓ² avere emittenti con account Aruba diversi.

IFatturaPaXmlGenerator​

string Generate(FatturaPaXmlInput input);
string GetFileName(issuer issuer, int invoiceNumber);
// formato: IT{vatCode}_{progressivo5cifre}.xml β†’ "IT01234567890_00001.xml"

public record FatturaPaXmlInput(
issuer Issuer,
invoice Invoice,
IReadOnlyList<invoiceLine> Lines,
company Company,
IReadOnlyList<invoicePayment> Payments,
IReadOnlyList<vatCode> VatCodes);

Genera XML conforme al tracciato FatturaPA 1.2 pronto per SDI.

InvoiceStatusExtensions​

public static SdiNotificationCode? ParseSdiCode(string? code);
public static InvoiceStatus FromArubaNotification(SdiNotificationCode? code);

Mapping:

Codice SDI→ InvoiceStatus
RC, ATAccepted
NS, MCDiscarded
ECRejected
SE, DTUndeliverableReceipt
(nullo / non riconosciuto)Sent

🧩 Pattern chiave​

Flusso SendToArubaAsync​

  1. Carica invoice dal DB β†’ lancia se non esiste.
  2. Verifica che InvoiceStatus == Draft β†’ lancia altrimenti.
  3. Carica issuer, company, lines, payments, vatCodes.
  4. Valida che ci siano rate (altrimenti lancia).
  5. Costruisce FatturaPaXmlInput e genera l'XML via IFatturaPaXmlGenerator.
  6. Chiama IArubaSdiService.SendInvoiceAsync β†’ ottiene transmissionId.
  7. Aggiorna invoice: status = Sent, arubaTransmissionId = ..., sentXmlContent = xml, updatedAt = now.
  8. Inserisce riga in invoiceStatusHistory.

Ogni step fallisce fast con InvalidOperationException dal messaggio descrittivo (catturato dal code-behind UI e mostrato via toast).

Flusso RefreshStatusFromArubaAsync​

  1. Carica invoice, verifica presenza arubaTransmissionId.
  2. Carica issuer (per credenziali Aruba).
  3. Chiama IArubaSdiService.CheckStatusAsync(issuer, transmissionId).
  4. Aggiorna stato invoice da ArubaStatusResult (via InvoiceStatusExtensions.FromArubaNotification).
  5. Registra cambio in invoiceStatusHistory con arubaStatus grezzo.

Numerazione atomica con fallback DB​

GetNextInvoiceNumberAsync usa ComputeNextNumber statico su una query WHERE issuerId = @id AND invoiceYear = @year. Il calcolo puΓ² teoricamente assegnare lo stesso numero a due chiamate concorrenti, ma il vincolo unique DB (UQ_invoices_number) blocca l'insert duplicato: la seconda insert fallisce e va ritentata. Accettabile per carichi di fatturazione (bassa concorrenza tipica).

SavePaymentsAsync: delete-then-insert​

Il metodo cancella tutte le rate esistenti e reinserisce quelle fornite. Non Γ¨ un upsert. Side effect: muta invoiceId, lineNumber, amount degli oggetti in input.

int lineNumber = 1;
foreach (var p in payments)
{
p.invoiceId = invoiceId;
p.lineNumber = lineNumber++;
p.amount = ComputePaymentAmount(invoice.totalAmount, p.percentage);
await _db.InsertAsync<...>(p, ct);
}

Implicazione: gli ID delle rate cambiano a ogni salvataggio. Non referenziare rate per ID da fuori della fattura.

History sempre scritto​

Ogni cambio di stato passa per AddStatusHistoryAsync, chiamato da SendToArubaAsync e RefreshStatusFromArubaAsync. Non esistono path che cambiano status senza registrarlo in invoiceStatusHistory.

πŸ“¦ Dipendenze​

InvoiceService dipende da:

  • ISimpleCRUDService (Brighela.SimpleCRUD) β€” accesso DB generico (GetAsync/GetListAsync/InsertAsync/UpdateAsync/DeleteAsync con parametri SQL).
  • IFatturaPaXmlGenerator β€” generazione XML (injectable, mockabile).
  • IArubaSdiService β€” integrazione HTTP Aruba (injectable, mockabile).

Registrazione DI in Program.cs:

builder.Services.AddScoped<IInvoiceService, InvoiceService>();
builder.Services.AddScoped<IArubaSdiService, ArubaSdiService>();
builder.Services.AddScoped<IFatturaPaXmlGenerator, FatturaPaXmlGenerator>();

πŸ“ File chiave​

  • Services/Invoicing/IInvoiceService.cs + InvoiceService.cs β€” core
  • Services/Invoicing/IArubaSdiService.cs + ArubaSdiService.cs β€” integrazione HTTP Aruba
  • Services/Invoicing/IFatturaPaXmlGenerator.cs + FatturaPaXmlGenerator.cs β€” generazione XML 1.2
  • Services/Invoicing/InvoiceStatusExtensions.cs β€” mapping codici SDI
  • TrainingHub.Tests/Services/Invoicing/InvoiceServiceTests.cs β€” copertura unit test

πŸ”Œ Estensione tipica​

Aggiungere un metodo al servizio​

  1. Aggiungere signature in IInvoiceService.
  2. Implementare in InvoiceService.
  3. Scrivere test in InvoiceServiceTests (con mock dei 3 dependency).
  4. Chiamare dal code-behind UI via [Inject] IInvoiceService invoiceService.

Cambiare il mapping stati SDI​

Modificare InvoiceStatusExtensions.FromArubaNotification. Helper puro, testabile in isolation.

Mockare Aruba in test​

Implementare una stub di IArubaSdiService che restituisce transmissionId fisso per SendInvoiceAsync e un ArubaStatusResult configurato per CheckStatusAsync. Iniettare nella testbench senza DB reale.

⚠️ Debito tecnico​

  • SendToArubaAsync monolitico. Il metodo fa validazione + caricamento dati + generazione XML + invio + update + history. Valutare split in metodi privati o pipeline step-per-step per leggibilitΓ /test.
  • ComputeNextNumber non atomico. In caso di alta concorrenza (es. piΓΉ wizard aperti in parallelo che salvano bozza) si rischiano collisioni. Alternative: OUTPUT INSERTED con increment in SQL, sequence esplicita per emittente/anno.
  • Messaggi di errore hardcoded in italiano. throw new InvalidOperationException("La fattura non ha rate di pagamento...") non Γ¨ localizzato. Spostare in risorse se la UI in futuro mostra testo raw. Oggi mitigato dal toast che incapsula il messaggio.
  • SavePaymentsAsync mutates input. Side-effect non dichiarato nei parametri. Considerare record/immutable e restituire le nuove rate, o rendere esplicito nel nome (SaveAndAssignAsync).
  • Nessun retry/backoff su IArubaSdiService.SendInvoiceAsync in caso di timeout di rete. Una chiamata in timeout che in realtΓ  Γ¨ arrivata a destinazione puΓ² causare doppi invii al ritentativo manuale. Valutare idempotency key o stato intermedio "Sending".
  • GetPaymentsAsync inclusa in IInvoiceService ma Γ¨ una pura query. Se la superficie dell'interfaccia cresce, valutare split (IInvoiceQueries vs IInvoiceCommands).

πŸ”— Vedi anche​