sdi_it — technical overview
FatturaPA / SDI e-invoicing for Italian clinics, phase 1 (issue #133, ADR 0025). Country-gated BillingComplianceHook (IT) in the verifactu shape: own tables, own Alembic branch (sdi_it), nothing added to billing.
The gate
services/tax_ids.is_business_recipient(billing_tax_id): only a valid partita IVA makes an invoice an SDI document. Natural persons (codice fiscale) get compliance_data["IT"] = {"sdi": "not_applicable", "reason": "b2c_healthcare_art_10bis"} and no record — healthcare invoices to persone fisiche may not be electronic (art. 10-bis DL 119/2018).
Flow
hook.on_invoice_issued/on_credit_note_issued→build_record: renders theFPR12file (services/xml_builder.build_fattura, TD01/TD04,Natura N4+RiferimentoNormativofor exempt lines,DatiBolloabove €77.47,CodiceDestinatariofrombilling_address.sdi_code/pecelse0000000), bumps the clinic'sprogressivo_invio, stores ansdi_it_recordsrow inpending.- Manual transport:
GET /records/{id}/xml(download),POST /records/{id}/exported(uploaded to the SDI). POST /receiptsparses the SDI receipt (services/receipts):RC → delivered,MC → undeliverable,NS → rejectedwith the error list; the receipt XML andIdentificativoSdIare stored.POST /records/{id}/requeueafter a scarto: same number and date, new progressivo, old record kept as history.
PEC transport (phase 2)
services/pec_transport.py (stdlib smtplib/imaplib in the default executor, no new dependency): send_file attaches the FPR12 to a message for the SDI mailbox; poll_receipts reads unseen messages from pec.fatturapa.it, extracts receipt attachments (XML or inside a zip), marks them seen and reports the SDI's dedicated reply address. services/submission_queue.process_clinic drains one clinic (rows locked FOR UPDATE SKIP LOCKED, commit before every network call, backoff per row, clinic paused ten minutes on mailbox errors) and applies receipts via services/receipts.apply_receipt, shared with the API import. tasks.py schedules it every 120 s.
Frontend layer (phase 3)
frontend/plugins/settings.client.ts registers two Settings → Billing pages (SdiItSettingsPage.vue, SdiItRecordsPage.vue); frontend/plugins/slots.client.ts mounts InvoiceSdiSlot.vue in invoice.detail.compliance and SdiBadge.vue in invoice.list.row.meta / invoice.detail.header.meta, gated on the clinic country IT or an IT block in compliance_data. composables/useSdiIt.ts wraps the API; the XML download uses useApi().raw (blob). Permissions in frontend/app/config/permissions.ts → sdiIt.
API surface
GET /api/v1/sdi_it/settings,PUT /api/v1/sdi_it/settingsGET /api/v1/sdi_it/recordsGET /api/v1/sdi_it/records/{record_id}/xmlPOST /api/v1/sdi_it/records/{record_id}/exportedPOST /api/v1/sdi_it/records/{record_id}/requeuePOST /api/v1/sdi_it/receiptsGET /api/v1/sdi_it/records/by-invoice/{invoice_id}POST /api/v1/sdi_it/pec/testPOST /api/v1/sdi_it/queue/process-now
Tests
backend/tests/modules/sdi_it/: tax ids, XML builder validated against the vendored official XSD (schemas/), receipt parsing, hook gate and credit notes, router lifecycle, Alembic round-trip uninstall.
Not yet
SdICoop, digital signature, Allegati.
See also
docs/modules/sdi_it.md(user-facing description)- ADR 0025, ADR 0026 (Sistema Tessera Sanitaria, the B2C half)
backend/app/modules/sdi_it/CLAUDE.md