Italy — SDI (FatturaPA) and Documento Commerciale
Research doc for the Italy fiscalization adapter. Internal reference for
engineers maintaining or extending rust/adapters/italy/.
Status: Implemented (legacy Phase-0 wedge). Adapter at
rust/adapters/italy/ (crate italy).
Strategic context: Italy is the largest of our four legacy launch
countries by addressable market and the most architecturally awkward —
the merchant population splits across two regulators-in-one:
Sistema di Interscambio (SDI) for B2B/B2G electronic invoicing, and the
Documento Commerciale (DC) channel via Servizio Software (SSW) for B2C
retail. Both run on Agenzia delle Entrate (AdE) infrastructure but use
different schemas, different endpoints, different auth surfaces, and
different submission windows. The adapter supports both via a single
Adapter struct with a system discriminator. See
docs/architecture/italy-documento-commerciale.md for the deep DC
design and apps/docs-internal/docs/guides/italy.md for the integration
walkthrough.
1. Regulatory scope
Two coexisting regimes
| Regime | Operator | Scope | Trigger |
|---|---|---|---|
| SDI / FatturaPA | Sistema di Interscambio (run by Sogei for AdE) | B2B and B2G electronic invoicing | Invoice issuance under Italian VAT, fatturazione elettronica obbligatoria since 2019-01-01 |
| Documento Commerciale (DC) | AdE Servizio Software (SSW) | B2C retail — replaces the paper scontrino / ricevuta fiscale | Cash-equivalent payment to a final consumer at point-of-sale |
The two are mutually exclusive per transaction: an Italian retail
sale is either a Fattura (goes through SDI) or a Documento Commerciale (goes through SSW). The merchant chooses based on whether
the customer requests an invoice or a commercial-document receipt.
SDI — who must submit
Per Decreto Legislativo 127/2015 and amendments through 2024:
| Effective | Scope |
|---|---|
| 2014-06-06 | B2G (invoicing to Public Administration) — initial |
| 2019-01-01 | All B2B + B2C invoices issued by Italian VAT-resident taxpayers (fatturazione elettronica obbligatoria) |
| 2022-07-01 | Cross-border (intra-EU and extra-EU) supplies via Esterometro / TD17–TD19 codes |
| 2024-01-01 | Forfettario (small taxpayer) regime threshold dropped — all Italian taxpayers in scope, no minimum revenue |
Exemptions: only specific carve-outs (sanitary services subject to patient privacy, certain agriculture under flat-rate regimes, certain diplomatic supplies).
DC — who must submit
Per Legislative Decree 1/2024 and Provvedimento AdE of 2025-03-07:
- Any taxpayer issuing a commercial document in lieu of the previous scontrino (which has been retired since 2020-01-01 in favor of corrispettivi telematici).
- The hardware-RT (Registratore Telematico) path predates SSW; SSW is the new software-only alternative.
- Zyntem's path is SSW (introduced by D.Lgs. 1/2024). Hardware-RT is out of scope for our adapter — those are physical fiscal printers certified by AdE; we don't compete in that space.
Thresholds
- No transaction-amount threshold for either regime. Every invoice / commercial document is in scope regardless of amount.
- Daily transmission cap for DC: file size ≤ 1 MB per submission; ~tens of thousands of receipts in practice fit comfortably below this.
2. API surface
SDI — REST + token
Endpoints (rust/adapters/italy/src/sdi_client.rs:6-7):
| Environment | Base URL |
|---|---|
| Production | https://api.fatturapa.gov.it/servizi/fatturapa/v1 |
| Sandbox | https://api.fatturapa.gov.it/servizi/fatturapa/v1-test |
Operations (sdi_client.rs:11-25):
| Method | Path | Purpose |
|---|---|---|
POST | /fatture | Submit a FatturaPA XML; returns identificativoSdI |
GET | /fatture/{sdi_id}/stato | Poll processing status |
GET | /fatture/{sdi_id}/esito | Retrieve the outcome notification XML |
Wire format: XML body, Content-Type: application/xml, Bearer-token
auth in HTTP header. There's also a legacy SOAP / SDICoop / SDIFTP
profile for high-volume issuers, but our adapter uses the REST path
exclusively.
DC — SSW REST
Endpoints (rust/adapters/italy/src/dc_client.rs:6-7):
| Environment | Base URL |
|---|---|
| Production | https://api.corrispettivi.agenziaentrate.gov.it/v1 |
| Sandbox | https://api.corrispettivi.agenziaentrate.gov.it/v1-test |
Operations (dc_client.rs:18-32):
| Method | Path | Purpose |
|---|---|---|
POST | /corrispettivi | Submit a sealed corrispettivi XML; returns idPresaInCarico |
GET | /corrispettivi/{id}/stato | Poll status (PRONTA / IN_ELABORAZIONE / NON_DISPONIBILE) |
GET | /corrispettivi/{id}/esito | Retrieve outcome XML |
Wire format: binary body (application/octet-stream) — the
sealed XML payload is byte-exact, no Content-Type: application/xml,
no JSON envelope. The seal (PAdES/CAdES electronic seal) is what AdE
verifies, not the XML structure alone.
Schema namespaces
- FatturaPA v1.2.2 (
fatturapa.rs:5):http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2. - DC v1.0 (
dc.rs:5):http://ivaservizi.agenziaentrate.gov.it/docs/xsd/corrispettivi/v1.0. - XMLDSig (both):
http://www.w3.org/2000/09/xmldsig#. - SDI notifications v1.0 (
fatturapa.rs:10):http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.0— used for RC / NS / MC notification XML.
Format-version selection
SDI requires <FormatoTrasmissione> to identify the recipient class
(config.rs:9-10):
| Code | Recipient |
|---|---|
FPA12 | Pubblica Amministrazione (B2G) |
FPR12 | Privati (B2B/B2C — the common case) |
DC uses versione="1.0" on the root <DocumentoCommerciale> element
(dc.rs:13-25). The DSW10 format identifier referenced in the
architecture doc is the SSW software-solution formal-version code; our
emitter uses 1.0 — consistent with the AdE samples and golden
fixtures we conform to.
3. Authentication
SDI — channel credentials + bearer token
Per auth.rs:8-13:
pub struct Credentials {
pub channel_id: String,
pub channel_password: String,
pub api_token: String,
}
The channel_id + channel_password pair authorizes us as a
transmission channel in SDI's registry. The api_token is the
short-lived bearer used on individual REST calls. Channel registration
is a one-time, out-of-band onboarding with AdE (multi-week paperwork);
the bearer is refreshable.
We store credentials on disk per credential_id and load them lazily
through the AuthStore trait (auth.rs:34-101). FileAuthStore
caches loaded creds in memory under a RwLock<HashMap<String, Credentials>> and validates that the supplied credential_id is a
plain filename (no path traversal — auth.rs:73-79):
let clean = Path::new(credential_id)
.file_name()
.and_then(|f| f.to_str())
.unwrap_or("");
if clean != credential_id {
return Err(AuthError::InvalidId(credential_id.to_string()));
}
SDI — XML signing (CAdES-BES)
The FatturaPA production path requires a qualified electronic
signature wrapping the XML — typically CAdES-BES (.p7m) but XAdES is
also accepted. The signing trait is at signature.rs:22-27:
#[async_trait::async_trait]
pub trait Signer: Send + Sync {
async fn sign(&self, xml_data: &[u8]) -> Result<Vec<u8>, SignatureError>;
}
We ship a NoOpSigner (signature.rs:30-38) that returns the input
unchanged — usable for sandbox where SDI accepts unsigned XML, and as
the trait stub for tests. Production deployments must swap in
either a QTSP-backed signer (DocuSign / Aruba PEC / Namirial) or a
local PKCS#12-backed signer.
DC — QTSP electronic seal
Per adapter.rs:306-322:
fn required_certificate_id(&self, config: &serde_json::Value) -> Option<String> {
let system = config.get("system").and_then(|v| v.as_str()).unwrap_or(SYSTEM_SDI);
if system == SYSTEM_DC {
config.get("seal_certificate_id").and_then(|v| v.as_str())...
} else {
None
}
}
DC submissions must be sealed with a sigillo elettronico
qualificato issued by an Italian QTSP (Aruba, Namirial, Infocert,
etc.) — this is seal_certificate_id in Config. The sealed bytes go
on the wire as application/octet-stream. AdE verifies the seal before
accepting the file; an unsealed DC submission is rejected with error
0004 ("digital seal verification failed", errors.rs:14).
This is the single most important deployment constraint for DC —
it forces EmbeddedLocal mode. See §6.
Why credentials and seals live in different places
- SDI:
api_credential_idinConfig(config.rs:33) → loads channel + token fromAuthStore. - DC:
seal_certificate_idinConfig(config.rs:58) → loads a PKCS#12 / HSM handle from the cert store.
There is no shared identity in SDI/DC; they are operated by different AdE teams with different onboarding paths.
4. Payload shape
SDI — FatturaElettronica
Top-level structure built in
rust/adapters/italy/src/fatturapa.rs:14-44:
FatturaElettronica (versione=FPR12 | FPA12)
├── FatturaElettronicaHeader
│ ├── DatiTrasmissione
│ │ ├── IdTrasmittente (IdPaese + IdCodice)
│ │ ├── ProgressivoInvio # 10-char id we mint per submission
│ │ ├── FormatoTrasmissione # FPA12 | FPR12
│ │ └── CodiceDestinatario # 7-char SDI mailbox or "0000000" / "XXXXXXX"
│ ├── CedentePrestatore (supplier)
│ │ ├── DatiAnagrafici (IdFiscaleIVA, CodiceFiscale, Anagrafica, RegimeFiscale)
│ │ └── Sede (address)
│ └── CessionarioCommittente (customer)
│ ├── DatiAnagrafici
│ └── Sede
└── FatturaElettronicaBody
├── DatiGenerali
│ └── DatiGeneraliDocumento
│ ├── TipoDocumento # TD01 / TD04 / TD17–TD27 / etc.
│ ├── Divisa # ISO 4217
│ ├── Data # YYYY-MM-DD
│ ├── Numero
│ └── ImportoTotaleDocumento
├── DatiBeniServizi
│ ├── DettaglioLinee[] # one per line item
│ └── DatiRiepilogo[] # one per VAT rate (taxable / tax / nature / exigibility)
└── DatiPagamento (optional)
└── DettaglioPagamento (ModalitaPagamento, ImportoPagamento, IBAN, ...)
The CodiceDestinatario defaults follow this rule
(fatturapa.rs:79-87):
- Provided by the customer's recipient code: use as-is (7 chars).
- Foreign customer (
country != "IT"):XXXXXXX. - Italian customer with no destination code:
0000000.
DC — DocumentoCommerciale
Top-level structure built in rust/adapters/italy/src/dc.rs:11-33:
DocumentoCommerciale (versione="1.0")
├── DatiConfigurazioneDC
│ ├── FormatoVersione # "1.0"
│ └── Matricola # PEM matricola, NNNN-NNNNNN
├── CedentePrestatore
│ ├── IdFiscaleIVA (IdPaese + IdCodice)
│ ├── CodiceFiscale
│ ├── Denominazione
│ └── Sede (Indirizzo, CAP, Comune, Provincia)
└── Documento
├── DataOraDocumento # ISO 8601 dateTime
├── NumeroProgressivo # NNNN-NNNN, monotonic per matricola
├── ElementiContabili[] # line items (description, qty, unitPrice, totalPrice, vatRate, type=vendita|reso)
├── Riepilogo[] # VAT summary per rate
├── Vendita | ResoAnnullo # totals + cash/electronic/discount/ticket breakdown
├── DatiLotteria (optional) # lottery code or instant-lottery QR
└── CodiceFiscaleCliente (optional)
Document type:
- Vendita (sale): standard receipt with
Venditatotals block. - ResoAnnullo (return / void): includes
TipologiaRA=R(return) |A(annullo) and mandatory reference to the original document (matricola, date, progressive number).
Field formatting (both regimes)
The adapter normalizes minor units into the wire decimals it expects
(adapter.rs:499-519):
fn format_cents(cents: i64) -> String {
format!("{}.{:02}", cents / 100, (cents % 100).unsigned_abs())
}
fn format_decimal(value: i64, decimals: usize) -> String { /* qty in thousandths */ }
fn format_rate(basis_points: i64) -> String {
format!("{}.{:02}", basis_points / 100, (basis_points % 100).unsigned_abs())
}
So LineItem.unit_price = 10000 (cents) → "100.00", quantity = 2000 (thousandths) → "2.000", tax_rate = 2200 (basis points) →
"22.00".
Document-type mapping
Adapter mapping (adapter.rs:521-526):
fn map_sdi_doc_type(tx_type: &str) -> String {
match tx_type {
"refund" | "return" | "credit_note" => "TD04".to_string(),
_ => "TD01".to_string(),
}
}
So consumer-side tx_type=sale → SDI TD01, refund/return/credit_note
→ TD04. The wider TD17–TD27 set (cross-border, reverse charge, asset
transfers, deferred invoices) is documented in
apps/docs-internal/docs/guides/italy.md:60-85 but currently requires
hand-rolled tx_type extension — not something an arbitrary upstream
caller can produce automatically. For autofatture / cross-border we
extend the mapping per merchant onboarding.
5. Ordering and sequencing
SDI: IdentificativoSdI (per submission)
SDI returns one IdentificativoSdI per accepted submission, regardless
of whether the call carried one invoice or N invoices in a single
lotto (lot). Our adapter sends one invoice per call — process_sdi
at adapter.rs:96-143 builds and submits a single FatturapaRequest,
so the returned identificativoSdI is the per-invoice tracking ID
(adapter.rs:127-133):
Ok(AdapterResult {
status: STATUS_PENDING.to_string(),
fiscal_id: resp.identificativo_sdi,
receipt_data: xml_bytes,
content_type: "application/xml".to_string(),
})
The ProgressivoInvio field on the wire is the issuer's tracking
number (10 chars, we mint from tx.id — adapter.rs:376); the SDI ID
is what AdE returns.
Idempotency: SDI deduplicates on (IdTrasmittente.IdCodice, ProgressivoInvio). A retry with the same ProgressivoInvio returns
the original IdentificativoSdI rather than minting a new one — but
only within a 5-day window and only if the prior call reached AdE.
Resubmitting after a timeout is therefore safe; resubmitting weeks
later under the same ProgressivoInvio yields error 00200
("duplicate invoice", errors.rs:80).
DC: idPresaInCarico + NumeroProgressivo
Two ordering keys exist:
idPresaInCarico— UUID returned per submission (dc_client.rs:43-46), opaque tracker.NumeroProgressivo— the printed progressive number, formatNNNN-NNNN, monotonic per PEM matricola (config.rs:54).
The matricola is the Punto di Emissione (emission point) serial. Each cash register gets one matricola at first-use registration with AdE; all that register's documents share it. Within a matricola, the progressive resets at midnight and increments by 1 per receipt.
Adapter rule: the NumeroProgressivo is taken from tx.id
verbatim (adapter.rs:481). Upstream is responsible for the monotonic
property; the adapter does not mint or validate the sequence. If
upstream sends a duplicate, AdE returns error 0010 ("progressive
number already used for this PEM", errors.rs:18) and the document is
rejected.
Retries and idempotency
Both regimes:
- Network failure / 5xx: safe to retry — the same payload bytes produce the same dedup key.
- 4xx with a permanent code (schema, signature, missing matricola): fix and resubmit with a new progressive number; the original number is "burned".
- Timeout after submission: retry — if the prior call reached the authority, we get back the same tracking ID.
6. Submission window and deployment mode
Submission windows
From adapter.rs:226-231:
fn submission_window(&self) -> SubmissionWindow {
match self.system.as_str() {
SYSTEM_DC => SubmissionWindow::Immediate,
_ => SubmissionWindow::Days(12),
}
}
| System | Window | Source |
|---|---|---|
| SDI | 12 days from issuance | DPR 633/1972 Art. 21 — invoice must be issued and submitted within 12 days of the operazione |
| DC | Immediate at point of sale | D.Lgs. 1/2024 — receipt must be transmitted at the moment of issuance (with daily azzeramento / closure transmitted within the same calendar day) |
The DC daily-closure (chiusura giornaliera) is a separate per-day
file that aggregates the day's receipts and is transmitted at random
within 00:00–22:00 local time, deadline 12 days. That file is
generated downstream of the per-receipt flow and isn't yet in the
adapter — it's tracked separately.
Deployment mode
From adapter.rs:233-238:
fn supported_deployment_modes(&self) -> &[DeploymentMode] {
match self.system.as_str() {
SYSTEM_DC => &[DeploymentMode::EmbeddedLocal],
_ => &[DeploymentMode::Cloud, DeploymentMode::EmbeddedLocal],
}
}
- SDI is Cloud-OK. Channel credentials and token can live in our KMS; the QES signing can be brokered to a remote QTSP.
- DC is Embedded-only. The QTSP electronic seal that AdE verifies on every submission must be applied at point-of-sale, per AdE provvedimento — a Cloud-side seal application breaks the "device emits sealed file" mental model and has been rejected in past audits. There may be a path forward for HSM-backed Cloud DC later, but as of 2026-04 we do not deploy DC in Cloud mode.
7. Sandbox access
Two distinct onboarding flows:
SDI sandbox
- Endpoint:
https://api.fatturapa.gov.it/servizi/fatturapa/v1-test. - Onboarding: register a canale di trasmissione via AdE's Servizi Telematici. Requires an Italian VAT number and digital identity (SPID / CIE / CNS).
- Sandbox accepts unsigned XML (the
NoOpSignerworks); production enforces CAdES-BES. - See
apps/docs-internal/docs/countries/italy-sandbox.mdfor the full step-by-step.
DC sandbox
- Endpoint:
https://api.corrispettivi.agenziaentrate.gov.it/v1-test. - Onboarding: separate AdE Fatture e Corrispettivi portal flow.
Register a software identity (CAU code, 4 alnum chars —
config.rs:50) and a test PEM matricola (NNNN-NNNNNN—config.rs:54, 151-158). - Sandbox does verify a test seal certificate. Aruba and Namirial
issue free test seal certs against AdE's test trust roots; capture
the PKCS#12 + password and reference via
seal_certificate_id.
Note: the architecture doc at
docs/architecture/italy-documento-commerciale.md covers the full DC
onboarding sequence including PEM activation.
8. Error model
Two-stage error semantics
Both regimes return an immediate (synchronous) submission response with
a tracking ID, then drive the actual outcome asynchronously through
poll-or-notification. The poll module classifies states
(poll.rs:25-110):
pub const SDI_POLL_POLICY: PollPolicy = PollPolicy::new(
5 * 60, // initial_delay: 5 min
60 * 60, // interval: 1 hour
14 * 24 * 60 * 60, // dead_letter: 14 days
);
pub const DC_POLL_POLICY: PollPolicy = PollPolicy::new(
10, // initial_delay: 10s
60, // interval: 1 min
60 * 60, // dead_letter: 1 hour
);
Polling cadence reflects the asymmetric finalization speeds: SDI can take days to deliver to a recipient PEC; DC finalizes in seconds-to-minutes.
SDI states
Defined at sdi_client.rs:51-54:
| State | Mapping (poll.rs:44-79) | Meaning |
|---|---|---|
IN_ELABORAZIONE | StillPending | AdE is processing the submission |
CONSEGNATA | Accepted | Delivered to recipient PEC — happy path |
NON_CONSEGNATA | Accepted (not Rejected) | AdE accepted but couldn't reach the recipient. The issuer's tax position is fine. Recipient retrieves via "PUT-AT-DISPOSAL" channel. |
SCARTATA | Rejected | Schema / signature / business validation failed; invoice is not legally issued |
| anything else | StillPending (with warning log) | future-proofing for AdE adding new states |
The NON_CONSEGNATA → Accepted mapping is a deliberate design
choice. Many naive implementations report it as failure, which is
wrong — the issuer's invoice has been registered, the only thing
missing is recipient delivery, which AdE makes available through a
secondary channel.
DC states
Defined at dc_client.rs:13-15:
| State | Mapping | Meaning |
|---|---|---|
IN_ELABORAZIONE | StillPending | AdE is processing |
PRONTA | Accepted | Outcome ready and OK |
NON_DISPONIBILE | StillPending (warning) | File not in the "ready-to-serve" cache; transient or hard-fail — the dead-letter window decides |
SDI notifications (RC / NS / MC)
Once CONSEGNATA / NON_CONSEGNATA / SCARTATA is reached, AdE
emits an outcome notification XML on the issuer's PEC
(apps/docs-internal/docs/guides/italy.md:127-131):
| Code | Status |
|---|---|
RC (RicevutaConsegna) | delivered |
NS (NotificaScarto) | rejected — XML enumerates <errori> |
MC (NotificaMancataConsegna) | delivery_error — recipient unreachable, eventually goes to put-at-disposal |
The adapter can build these notification XMLs as well via
FatturapaBuilder::build_notification_xml (fatturapa.rs:37-44) —
used in tests / mocks.
Error code catalogues
errors.rs ships static maps for both SSW (DC, codes 0001–0901) and
SDI (codes 00001–00404). The classifier translates the Italian
description to English so logs and dashboards stay readable.
Selected SSW codes (errors.rs:8-59):
| Code | Class | Translation |
|---|---|---|
0001 | format | file format not recognized |
0002 | size | file exceeds maximum allowed size (1 MB) |
0004 | seal | digital seal verification failed |
0007 | matricola | PEM matricola not registered or not active |
0009 | duplicate | duplicate submission detected |
0010 | duplicate | progressive number already used for this PEM |
0107 | timing | DataOraDocumento is in the future |
0200 | arithmetic | Riepilogo totals do not match ElementiContabili |
0400 | timing | transmission outside allowed window (00:00-22:00) |
0401 | timing | document date exceeds 12-day transmission deadline |
0502 | rate | rate limit exceeded, retry after cooldown |
0900 | server | internal processing error, please retry |
Selected SDI codes (errors.rs:73-86):
| Code | Translation |
|---|---|
00001 | file not conforming to FatturaPA schema |
00002 | transmitter IdCodice not found in registry |
00100 | CodiceDestinatario not valid or not active |
00200 | duplicate invoice (same number/date/sender) |
00301 | buyer IdFiscaleIVA not found in registry |
00400 | digital signature verification failed |
00403 | signing certificate expired |
00404 | signing certificate revoked |
9. Implementation notes
Crate layout
rust/adapters/italy/
├── Cargo.toml # crate "italy", optional `http` feature gates reqwest
└── src/
├── lib.rs # re-exports Adapter, DcBuilder, FatturapaBuilder
├── adapter.rs # Adapter struct + CountryAdapter impl + system routing
├── config.rs # Config (system="sdi"|"dc"), validation per system
├── auth.rs # Credentials, AuthStore, FileAuthStore (channel creds for SDI)
├── signature.rs # Signer trait, NoOpSigner; production signers live elsewhere
├── client.rs # AdEHttpClient (shared reqwest wrapper, error taxonomy)
├── sdi_client.rs # SdiClient trait + HttpSdiClient (FatturaPA REST)
├── dc_client.rs # DcClient trait + HttpDcClient (SSW REST)
├── fatturapa.rs # FatturapaBuilder — FatturaPA XML + notification XML emitters
├── dc.rs # DcBuilder — DocumentoCommerciale XML emitter
├── poll.rs # PollableAdapter impl, classify_sdi_stato / classify_dc_stato
├── errors.rs # SSW + SDI error code translations
└── types.rs # FatturapaRequest, DcRequest, all wire-shape DTOs
Trait fit
CountryAdapter::prepare() is a pure XML-build:
parse_sealed_config()extracts the validatedConfigfromloc.country_config(adapter.rs:332-337).- Routes on
config.effective_system()to eitherDcBuilder::build_dc_xmlorFatturapaBuilder::build_invoice_xml. - Returns
PreparedRecordwithsubmission_payload = xml.into_bytes(),content_type = "application/xml". The signature/seal is not applied atprepare()— that happens at submit time (see below).
CountryAdapter::submit() is a no-op (adapter.rs:289-297):
fn submit(&self, _record: &PreparedRecord) -> Result<SubmitResult, CoreAdapterError> {
// Actual async SDI/DC submission is handled by the retry worker.
Ok(SubmitResult { authority_id: None, authority_response: None })
}
The actual HTTP submission is a separate worker that consumes the
PreparedRecord.submission_payload, signs/seals, posts to the
appropriate client, and reconciles the tracking ID back into the
record. This split exists because:
- Submission is async and error-prone — we want to retry with backoff
without re-running
prepare(). - Signing requires KMS / HSM access; we don't want the prepare hot path to depend on that.
- The poll loop (
PollableAdapterimpl atpoll.rs:120-152) is the third leg of the same pipeline.
Idiosyncrasies and gotchas
Adapterstruct holds both clients asOption<Box<dyn ...>>(adapter.rs:55-62). When the matching client isNone, the adapter returnsSTATUS_PENDING_SUBMISSIONand the worker is expected to fill the gap. This makes prepare() testable without network.fiscal_idis provisional at prepare time:format!("IT-{}", tx.id)(adapter.rs:276). The real SDI ID / DCidPresaInCaricoonly arrives post-submit. Downstream consumers should treatfiscal_idas a stable internal handle and look atfiscal_id_upstream(which the worker fills) for the authority's ID.progressive_numberfor the SDI transmitter istx.idtruncated to 10 chars (adapter.rs:376). Iftx.idis non-alnum or shorter, that may produce aProgressivoInvioAdE rejects. We've not seen a regression from this in practice but it's a sharp edge.- DC matricola has a strict format:
NNNN-NNNNNN(4 + 6 alphanumeric,config.rs:151-158). A bad matricola at config registration is rejected; a matricola that's valid format but not registered with AdE is rejected at submission with0007("PEM matricola not registered or not active"). - SDI deduplicates on
IdTrasmittente + ProgressivoInvio, not on invoice number. Two invoices with the sameNumeroand differentProgressivoInvioare both accepted by SDI. The "duplicate invoice" error (00200) only fires on(supplier, year, invoice_number)collision — a separate check. - Sandbox accepts unsigned FatturaPA, production does not. Don't
wire the
NoOpSignerto a Cloud production deployment. NON_CONSEGNATAis success. If you read the error logs, this looks like a failure — it is not.
10. References
Authority documentation
- D.Lgs. 127/2015 — Trasmissione telematica delle operazioni IVA e di controllo delle cessioni di beni effettuate attraverso distributori automatici. Legal basis for SDI obligation. https://www.gazzettaufficiale.it/eli/id/2015/08/15/15G00141/sg.
- D.Lgs. 1/2024 — Legal basis for the Software Solution (SSW) path for Documento Commerciale.
- AdE Provvedimento 2025-03-07 — Implementing rules for SSW.
- FatturaPA technical specifications — XML schemas + notification schemas at https://www.fatturapa.gov.it/it/lapiattaforma/area-tecnica/.
- DC technical specifications — Specifiche tecniche Corrispettivi Telematici via SSW at https://www.agenziaentrate.gov.it/portale/web/guest/schede/comunicazioni/corrispettivi-telematici.
Adapter source
rust/adapters/italy/Cargo.toml— crate manifest,httpfeature.rust/adapters/italy/src/adapter.rs—Adapter, system routing,CountryAdapterimpl.rust/adapters/italy/src/config.rs—Configschema, validators,SYSTEM_SDI/SYSTEM_DCconstants.rust/adapters/italy/src/auth.rs— channel credentials store.rust/adapters/italy/src/signature.rs—Signertrait,NoOpSigner.rust/adapters/italy/src/sdi_client.rs— REST surface for SDI.rust/adapters/italy/src/dc_client.rs— REST surface for SSW DC.rust/adapters/italy/src/fatturapa.rs— FatturaPA XML emitter + notification XML emitter.rust/adapters/italy/src/dc.rs— DC XML emitter.rust/adapters/italy/src/poll.rs—PollableAdapter,classify_sdi_stato/classify_dc_stato.rust/adapters/italy/src/errors.rs— SSW + SDI error code translations.
Internal docs
docs/architecture/italy-documento-commerciale.md— deep DC design doc (XSD, lottery, daily closure).apps/docs-internal/docs/architecture/italy-documento-commerciale.md— published rendering of the same.apps/docs-internal/docs/guides/italy.md— integration guide (legacy; should migrate to external).apps/docs-internal/docs/countries/italy-sandbox.md— onboarding steps for both sandboxes.apps/docs/docs/guides/italy.md— current customer-facing presentation.