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β
| Servizio | ResponsabilitΓ |
|---|---|
IInvoiceService / InvoiceService | Orchestrazione fattura (numerazione, send, refresh, rate, storico) |
IArubaSdiService / ArubaSdiService | Integrazione HTTP con API Aruba (invio + polling stato) |
IFatturaPaXmlGenerator / FatturaPaXmlGenerator | Generazione dell'XML FatturaPA 1.2 |
Helper statico di mapping:
| Helper | Ruolo |
|---|---|
InvoiceStatusExtensions | Mappa codici notifica SDI (RC, NS, ecc.) a InvoiceStatus |
InvoiceActionRunner | Pattern 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, AT | Accepted |
NS, MC | Discarded |
EC | Rejected |
SE, DT | UndeliverableReceipt |
| (nullo / non riconosciuto) | Sent |
π§© Pattern chiaveβ
Flusso SendToArubaAsyncβ
- Carica
invoicedal DB β lancia se non esiste. - Verifica che
InvoiceStatus == Draftβ lancia altrimenti. - Carica
issuer,company,lines,payments,vatCodes. - Valida che ci siano rate (altrimenti lancia).
- Costruisce
FatturaPaXmlInpute genera l'XML viaIFatturaPaXmlGenerator. - Chiama
IArubaSdiService.SendInvoiceAsyncβ ottienetransmissionId. - Aggiorna
invoice:status = Sent,arubaTransmissionId = ...,sentXmlContent = xml,updatedAt = now. - Inserisce riga in
invoiceStatusHistory.
Ogni step fallisce fast con InvalidOperationException dal messaggio
descrittivo (catturato dal code-behind UI e mostrato via toast).
Flusso RefreshStatusFromArubaAsyncβ
- Carica
invoice, verifica presenzaarubaTransmissionId. - Carica
issuer(per credenziali Aruba). - Chiama
IArubaSdiService.CheckStatusAsync(issuer, transmissionId). - Aggiorna stato invoice da
ArubaStatusResult(viaInvoiceStatusExtensions.FromArubaNotification). - Registra cambio in
invoiceStatusHistoryconarubaStatusgrezzo.
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β coreServices/Invoicing/IArubaSdiService.cs+ArubaSdiService.csβ integrazione HTTP ArubaServices/Invoicing/IFatturaPaXmlGenerator.cs+FatturaPaXmlGenerator.csβ generazione XML 1.2Services/Invoicing/InvoiceStatusExtensions.csβ mapping codici SDITrainingHub.Tests/Services/Invoicing/InvoiceServiceTests.csβ copertura unit test
π Estensione tipicaβ
Aggiungere un metodo al servizioβ
- Aggiungere signature in
IInvoiceService. - Implementare in
InvoiceService. - Scrivere test in
InvoiceServiceTests(con mock dei 3 dependency). - 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β
-
SendToArubaAsyncmonolitico. Il metodo fa validazione + caricamento dati + generazione XML + invio + update + history. Valutare split in metodi privati o pipeline step-per-step per leggibilitΓ /test. -
ComputeNextNumbernon atomico. In caso di alta concorrenza (es. piΓΉ wizard aperti in parallelo che salvano bozza) si rischiano collisioni. Alternative:OUTPUT INSERTEDcon 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. -
SavePaymentsAsyncmutates input. Side-effect non dichiarato nei parametri. Considerarerecord/immutable e restituire le nuove rate, o rendere esplicito nel nome (SaveAndAssignAsync). - Nessun retry/backoff su
IArubaSdiService.SendInvoiceAsyncin 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". -
GetPaymentsAsyncinclusa inIInvoiceServicema Γ¨ una pura query. Se la superficie dell'interfaccia cresce, valutare split (IInvoiceQueriesvsIInvoiceCommands).