Schema DB inv
π― Cosa faβ
Definisce il persistence layer della fatturazione: 16 tabelle, 4 viste e 1 funzione
nel schema inv del progetto TrainingHub.Database. Deploy tramite
DACPAC via pipeline Azure.
πΊοΈ Tabelleβ
| Tabella | Ruolo |
|---|---|
inv.issuers | Emittente fattura (cedente/prestatore) con credenziali Aruba |
inv.invoices | Intestazione fattura |
inv.invoiceLines | Righe di dettaglio di una fattura |
inv.invoicePayments | Piano rate di una fattura |
inv.invoiceAppointments | Associazione N:N fattura β appuntamenti formativi |
inv.invoiceStatusHistory | Storico cambi di stato di una fattura |
inv.bankAccounts | Anagrafica conti bancari per emittente |
inv.vatCodes | Codici IVA riutilizzabili (aliquota + natura) |
inv.paymentMethods | Lookup codici metodo di pagamento SDI (MP01βMP23) |
inv.documentTypes | Lookup codici tipo documento SDI (TD01βTD28) |
inv.conventions | Convenzioni commerciali (sconto per categoria corso) |
inv.companyConventions | Associazione N:N azienda β convenzione |
inv.companyPaymentTerms | Piano di pagamento per azienda; companyId NULL = piano predefinito |
π Relazioniβ
Referenze esterne: reg.companies, reg.organizers, edu.categories,
edu.appointments.
ποΈ Dettaglio tabelleβ
inv.issuersβ
Emittente fattura. Una ragione sociale 3SD con credenziali Aruba dedicate.
- PK:
id(GUID, defaultnewid()) - FK:
organizerSlug β reg.organizers(slug) - Campi chiave:
companyName,vatCode,fiscalCode,address,city,zipCode,province,country(default'IT') β anagrafica legalepec,reaOffice,reaNumber,shareCapitalβ extra FatturaPAarubaUsername,arubaPassword(in chiaro),arubaEnvironment('demo'|'production', default'demo')
- Check:
CHK_issuers_arubaEnvironmentsuarubaEnvironment
inv.invoicesβ
Intestazione fattura.
- PK:
id - FK:
issuerId β inv.issuers(id),companyId β reg.companies(id),documentTypeCode β inv.documentTypes(code),relatedInvoiceId β inv.invoices(id)(self-FK, nullable) - Campi chiave:
documentTypeCode(NVARCHAR(8), default'TD01') β codice SDI tipo documento (FatturaPA), es.TD01fattura,TD04nota di creditorelatedInvoiceId(self-FK versoinv.invoices, nullable) β fattura stornata da questa nota di credito; valorizzato solo sui documentiTD04, creato daInvoiceService.BuildCreditNoteHeadercode(renderizzato dal template della serie, es.2026/1),invoiceNumber(progressivo),invoiceYearβ NULL in bozza: assegnati solo all'emissione (vediinv.invoiceCounters)issueDate,status(string, non enum DB β mappato aInvoiceStatuslato C#)taxableAmount,vatAmount,totalAmountnotes- Aruba:
arubaTransmissionId,sdiProtocolNumber,arubaStatus,arubaLastCheckAt,arubaNotificationXml,sentXmlContent - Audit:
createdAt(defaultgetdate()),updatedAt
- Unique:
UQ_invoices_number(issuerId, documentTypeCode, invoiceYear, invoiceNumber)β indice filtratoWHERE invoiceNumber IS NOT NULL: garantisce unicitΓ progressivo per emittente/tipo/anno ammettendo piΓΉ bozze non numerate
inv.invoiceCountersβ
Contatore progressivo per serie di numerazione, sorgente autoritativa (assegnata all'emissione, non in bozza).
- Campi chiave:
issuerId(FK βinv.issuers),yeardocumentTypeCode(FK βinv.documentTypes, nullable) β serie dedicata al tipo documento (es.TD04note di credito);NULL= serie di default valida per tutti i tipi senza contatore specifico (fallback)counterβ ultimo numero assegnato (prossimo =counter + 1)templatingExpressionβ template Handlebars del codice (variabiliprogressive,year,documentTypeCode; default{{year}}/{{progressive}})terminatedβ serie chiusa: non usata per il roll-over dell'anno successivo
- Unique:
UQ_invoiceCounters_series(issuerId, documentTypeCode, year)
inv.invoiceLinesβ
Righe di dettaglio fattura.
- PK:
id - FK:
invoiceId β inv.invoices(id) ON DELETE CASCADE,vatCodeId β inv.vatCodes(id) - Campi chiave:
lineNumber(progressivo da 1)productServiceCode(opzionale),description,quantity,unitOfMeasure,unitPrice,discountPercentagetotalAmount(calcolato lato app:quantity * unitPrice * (1 β discount/100))
- Cascade: elimina riga se fattura eliminata
inv.invoicePaymentsβ
Piano rate di pagamento.
- PK:
id - FK:
invoiceId β inv.invoices(id) ON DELETE CASCADE,paymentMethodCode β inv.paymentMethods(code),bankAccountId β inv.bankAccounts(id)(nullable) - Campi chiave:
lineNumber,paymentMethodCode(codice SDI),paymentDueDate,paymentDate(data ricezione, nullable),bankAccountId(nullable; obbligatorio solo sepaymentMethods.ibanRequired = 1)percentage(somma rate = 100%),amount(calcolato:totalAmount * percentage / 100)
inv.invoiceAppointmentsβ
Associazione N:N tra fattura e appuntamenti formativi.
- PK composita:
(invoiceId, appointmentId) - FK:
invoiceId β inv.invoices(id) ON DELETE CASCADE,appointmentId β edu.appointments(id)
inv.invoiceStatusHistoryβ
Storico cambi di stato di una fattura.
- PK:
id - FK:
invoiceId β inv.invoices(id)(no cascade β audit trail) - Campi chiave:
status,arubaStatus(nullable),notes,createdAt(defaultgetdate()),createdBy
inv.bankAccountsβ
Anagrafica conti bancari per emittente. Riferita da
inv.invoicePayments.bankAccountId per il rendering del nodo
<IBAN> nell'XML FatturaPA.
- PK:
id - FK:
issuerId β inv.issuers(id) - Campi chiave:
label(etichetta breve),iban(NVARCHAR(64), formato SEPA/IT validato lato C# da[ItalianIban]),bankName,bic(NVARCHAR(11)),accountHolderactive(bit, default1) β soft-delete: se0il conto non Γ¨ selezionabile in nuove rate ma resta legato ai pagamenti storiciisDefault(bit, default0) β conto ereditato dalle rate generate dainv.companyPaymentTerms
- Indici:
IX_bankAccounts_issuerId;UQ_bankAccounts_default(unique filtrato suissuerId WHERE isDefault = 1: al massimo un predefinito per emittente, garantito dal DB)
inv.vatCodesβ
Codici IVA riutilizzabili.
- PK:
id - Campi chiave:
label,vatRate,vatNature(se aliquota = 0)
inv.paymentMethodsβ
Lookup dei codici metodo di pagamento SDI (FatturaPA, MP01βMP23).
Riferito da inv.invoicePayments.paymentMethodCode.
- PK:
code(NVARCHAR(8), es.MP05) - Campi chiave:
label(es. Bonifico),descriptionibanRequired(bit) β se1, l'IBAN diventa obbligatorio sulla rata che usa questo metodoactive(bit, default1) β flag soft-delete
- Seed:
Scripts/PostDeploy/01-seed-lookups.sql:8(eseguito dal DACPAC a ogni publish; il vecchioScripts/seed-inv-paymentMethods.sqlè archiviato inScripts/_archive/2026-06/)
inv.documentTypesβ
Lookup dei codici tipo documento SDI (FatturaPA, TD01βTD28).
Riferito da inv.invoices.documentTypeCode.
- PK:
code(NVARCHAR(8), es.TD01) - Campi chiave:
label(es. Fattura, Nota di credito),descriptionactive(bit, default1) β flag soft-delete; i tipi non attivi non sono selezionabili in nuove fatture
- Seed:
Scripts/PostDeploy/01-seed-lookups.sql:51(idem β il vecchioScripts/seed-inv-documentTypes.sqlΓ¨ archiviato)
inv.conventionsβ
Convenzioni commerciali.
- PK:
id - FK:
categoryId β edu.categories(id),organizerSlug β reg.organizers(slug)(entrambe nullable) - Campi chiave:
label,discountPercentagevalidFrom(default'1900-01-01'),validTo(nullable = illimitata)
inv.companyConventionsβ
Associazione N:N azienda β convenzione, con override validitΓ opzionale.
- PK:
id - FK:
companyId β reg.companies(id),conventionId β inv.conventions(id) - Unique:
UQ_companyConventions(companyId, conventionId) - Campi chiave:
validFrom,validTo(entrambi nullable;NULL= usa le date della convenzione)
inv.companyPaymentTermsβ
Piano di pagamento: una riga per rata. Le righe con companyId IS NULL
sono il piano predefinito, applicato alle aziende prive di righe
proprie. Consumato da PaymentTermsService, che ne deriva le
inv.invoicePayments della fattura.
- PK:
id - FK:
companyId β reg.companies(id)(nullable),paymentMethodCode β inv.paymentMethods(code) - Unique:
UQ_companyPaymentTerms(companyId, lineNumber)β SQL Server trattaNULLcome un valore nell'unicitΓ , quindi(NULL,1)e(NULL,2)convivono e un secondo(NULL,1)viene rifiutato: Γ¨ esattamente la semantica voluta per il piano predefinito - Check:
percentage > 0 AND <= 100,daysOffset >= 0 - Campi chiave:
lineNumberβ ordine della rata nel pianodaysOffsetβ giorni di dilazione dalla data fatturaendOfMonth(bit, default0) β la scadenza calcolata slitta all'ultimo giorno del suo mese: i giorni si sommano prima, il fine mese si applica dopo (15/01 + 30 gg f.m. β 14/02 β 28/02)percentageβ quota del totale; le righe di un piano devono sommare 100, altrimenti il piano non viene applicato affatto
- Indice:
IX_companyPaymentTerms_companyId - Seed:
PostDeploy/01-seed-lookups.sqlinserisce il predefinito (MP05, 30 giorni, 100%) solo se non esiste alcuna riga globale
π§ Funzioniβ
inv.fn_activeConventions(@companyId, @issueDate)β
TVF che restituisce le convenzioni attive per un'azienda a una data data. Verifica entrambe le validitΓ (convenzione e associazione):
SELECT c.id AS conventionId, c.categoryId, c.discountPercentage
FROM inv.conventions c
INNER JOIN inv.companyConventions cc ON cc.conventionId = c.id
WHERE cc.companyId = @companyId
AND @issueDate >= c.validFrom
AND (c.validTo IS NULL OR @issueDate <= c.validTo)
AND (cc.validFrom IS NULL OR @issueDate >= cc.validFrom)
AND (cc.validTo IS NULL OR @issueDate <= cc.validTo);
Usata da InvoiceService / wizard fattura per applicare lo sconto in
automatico.
π File chiaveβ
TrainingHub.Database/inv/Tables/*.sqlβ tabelleTrainingHub.Database/inv/Functions/fn_activeConventions.sqlβ funzione TVFTrainingHub.Database/TrainingHub.Database.sqlprojβ progetto SQL
β οΈ Debito tecnicoβ
-
invoices.statuscome stringa anzichΓ© FK a una lookup oCHECKesplicito: oggi validato solo lato C# conEnum.Parse. Valori errati scritti in DB via altri path non sono bloccati. Valutare check constraint o tabella di lookup. -
arubaPasswordin chiaro. Decisione esplicita a design (back- office interno con accesso controllato), ma da rivedere per compliance. - Indici espliciti minimi. Esiste unique su
invoices.number,companyConventions(companyId, conventionId)e index subankAccounts.issuerId. Valutare indici aggiuntivi su FK frequentemente usate in join (es.invoiceLines.invoiceId,invoicePayments.invoiceId,invoiceStatusHistory.invoiceId) se query lente emergono in profilo.