fiskaltrust / fiskaltrust/middleware

RFC: move eInvoice document generation (EN 16931 / ZUGFeRD / Factur-X) from the Receipt API /issue into the Middleware /sign response

Open
#778 0 comments 0 reactions 0 assignees View on GitHub
area-interface area-sign category-rfc market-eu market-fr status-needs-design
Dominant language
C#
Stars
20
Forks
11
Avg merge
1d 18h
Merged PRs (30d)
13

Description

# RFC: eInvoice document generation moves from the Receipt API `/issue` into the Middleware `/sign` response

Discussion issue. Goal: reach the design decisions needed before any implementation PR, on three topics:

1. How the eInvoice **format** is chosen (EN 16931 UBL / CII, ZUGFeRD / Factur-X, XRechnung, Peppol BIS ...).
2. How the **recipient's electronic address** (Peppol participant ID, French SIREN / PPF routing, e-mail ...) is obtained.
3. Where the **seller enrichment** that the Receipt API pulls from platform / portal master data today has to move: POSSystemAPI, or the Middleware's queue / SCU itself.

Related: #662 (request validation levels; its "prerequisite for: E-Invoice Issue" placeholder is this issue).

---

## 1. Where we are

### Target state (stated, not built)

"The `/sign` response carries the eInvoice document." Nothing in the Middleware, the interface package or the public docs implements or specifies this yet. The public docs page *Delivery (/issue Endpoint)* already frames `/sign` as the integration point ("any merchant already integrated with `/sign` is ready to use it"), while stating that "the actual delivery to the recipient ... is handled outside the Middleware". This RFC keeps that split: **generation** moves into the Middleware, **delivery** stays behind `/issue`.

### Current state (Receipt API)

The eInvoice is generated **after** fiscalization, from the stored `ReceiptRequest` / `ReceiptResponse` pair the POS posts to `/issue`:

| Aspect | Today |
|---|---|
| Trigger | POS (or POSSystemAPI on its behalf) posts the signed pair to `POST /v2/issue`, then asks for a document. Only v2 receipts with an `Invoice*` receipt case (`0x1000`..`0x1003`) are accepted for eInvoice rendering. |
| Format selection | A free-form `Format` string on `Action: download` (or a format segment in the download URL): `en16931` (UBL, EN 16931 core + Peppol BIS Billing 3.0 customization), `zugferd` (CII embedded in a PDF/A via a ZUGFeRD library, profile `basic` or `en16931` as a query parameter), `facturx` as an alias of `zugferd`. No XRechnung. Both formats are currently sandbox-only. |
| Delivery | `Action: send` with `Target.Scheme` = `email` / `sms` / `peppol`. Anything else is rejected. |
| Peppol recipient | Supplied by the caller in `Target.Address` as `scheme:value`, passed through unchanged. No directory / SMP lookup, no customer master data. The document itself does not carry a buyer electronic address (BT-49) except through the French extension. |
| Peppol sender | Read from per-queue receipt settings maintained in the portal. Document type / process are hard-coded to Peppol BIS Billing 3.0 Invoice / CreditNote. |
| Seller party (BT-27..BT-40) | Enriched from portal master data at render time: outlet name, street, zip, city, country, VAT ID. For France additionally company legal form, RCS, NAF, VAT payment option (BT-8) and the payment-terms notes, with a documented precedence *signed payload → master data → request*. SIRET is deliberately taken from the **signed** QR payload or the request, never from master data. |
| Buyer party (BT-44..BT-49) | `cbCustomer` parsed as the DSFinV-K style object (`CustomerName`, `CustomerVATId`, `CustomerStreet`, `CustomerZip`, `CustomerCity`, `CustomerCountry`), plus `ftReceiptCaseData.FR.Customer*` (SIRET) for France. |
| Lines / totals / payments | From `cbChargeItems`, `cbPayItems`, `ftReceiptCase` (refund → CreditNote), `cbPreviousReceiptReference` (BT-25), `ftTypeOfService` chain for BT-23. Currency hard-coded to EUR. |
| Validation | XSD + Schematron (EN 16931, Peppol; for FR the BR-FR Flux 2 and EXTENDED-CTC-FR rulesets). Failure → 422 at download time, i.e. **after** the receipt was fiscalized. |
| France | EXTENDED-CTC-FR customization, B1 / S1 / M1..M8 business process type derived from the receipt case, SIREN / SIRET identifiers (schemes 0002 / 0009 / 0225), issue-time 422 when seller SIRET or legal form is missing. |

Two things about the current implementation matter for the migration:

- The generator libraries are already namespaced as Middleware components (`fiskaltrust.Middleware.Invoices.Format.EN16931`, `...ZUGFeRD`). They were written to be lifted.
- The BT-23 gap ("`ftTypeOfService` is planned for the v2 Middleware response") is now closed on the FR side by f3a7c798 on the `market-fr` branch (QueueFR.v2 exposes `ftTypeOfService` S / B / M). The Receipt API still reads it from case data because it stores the v1 shape of the pair.

### What the Middleware and the interface offer today

- Receipt cases `InvoiceUnknown0x1000`, `InvoiceB2C0x1001`, `InvoiceB2B0x1002`, `InvoiceB2G0x1003` and flags `ReceiverIsBusiness`, `ReceiverIsKnown`. They classify the **document**, not the **syntax**.
- `ReceiptCaseFlags.ReceiptRequested` (`0x8000_0000`) is the idempotent re-send flag ([SignProcessor.cs:51](https://github.com/fiskaltrust/middleware/blob/main/queue/src/fiskaltrust.Middleware.Localization.v2/SignProcessor.cs#L51)). It must not be reused as "eInvoice requested". The docs also document gggg bit `0200` as *InvoiceProcessingOnly / InvoiceDelivery* while the enum assigns `0200_0000` to `AdditionalInformationRequested`. That discrepancy needs to be settled as part of this design.
- `cbCustomer` is `object?`. The Middleware's typed view is [MiddlewareCustomer.cs](https://github.com/fiskaltrust/middleware/blob/main/queue/src/fiskaltrust.Middleware.Localization.v2/Models/MiddlewareCustomer.cs): name, id, type, street, zip, city, country, VAT ID. **No electronic address, no identifier scheme.**
- `ReceiptResponse.ftSignatures` is the only container for a returned document. `SignatureFormat.Base64 (0x000D)` and `SignatureTypeFlags.DigitalReceiptOnly` / `DontVisualize` exist. Greece already returns the full myDATA XML as signature type `010`, so there is a precedent for a document inside the response.
- The documented maximum of `SignatureItem.Data` is 1023 characters. An EN 16931 XML does not fit; a hybrid PDF certainly does not.
- The Middleware has no PDF renderer. The Receipt API builds the ZUGFeRD / Factur-X PDF/A by rendering the receipt to PDF first and embedding the CII.

---

## 2. Topic 1: how is the format chosen

Distinguish three layers, because they are chosen by different parties:

| Layer | Examples | Who should decide |
|---|---|---|
| Semantic model | EN 16931 core; national CIUS / extensions (EXTENDED-CTC-FR, XRechnung) | Market (queue country) |
| Syntax | UBL 2.1, UN/CEFACT CII | Recipient / channel (Peppol BIS wants UBL, Factur-X wants CII) |
| Container | bare XML, hybrid PDF/A (ZUGFeRD / Factur-X), signed envelope | Consumer of the document |

Options for the request contract:

- **A. Flag only, market default.** A new `ftReceiptCase` flag "eInvoice requested" (or simply every `InvoiceB2B` / `InvoiceB2G` case) makes the queue emit the market's canonical document: FR → EN 16931 with EXTENDED-CTC-FR, DE → XRechnung / ZUGFeRD EN 16931 profile, other EU → EN 16931 + Peppol BIS. Simple for the POS, no way to ask for a second syntax.
- **B. Explicit selector in the request.** `ftReceiptCaseData.eInvoice = { format, profile }` (or a small typed block). Matches today's `Format` string, but moves a rendering choice into the fiscal request and into the signed / archived data.
- **C. Queue configuration with request override.** The portal / configuration package sets the queue's default (format, profile, syntax); the POS can override per receipt via B. Master data and format policy live together.
- **D. Middleware emits the semantic model, `/issue` renders syntaxes.** The queue returns one canonical XML (EN 16931 in the market's CIUS, one syntax). Any other syntax or the hybrid PDF stays a `/issue` rendering derived from that XML.

Questions to settle:

1. Is generation triggered by the receipt case alone (`0x1002` / `0x1003`), by an explicit flag, or by queue configuration? What about `InvoiceB2C0x1001` (France: B2C invoices are e-reporting, not e-invoicing)?
2. Does the Middleware ever produce more than one syntax for the same receipt? If yes, both go into the archived response.
3. The hybrid PDF (ZUGFeRD / Factur-X) needs a visual rendering. Proposal: it is **not** produced by the Middleware; the Middleware produces the XML, `/issue` embeds it. Agree?
4. Schematron validation before signing turns a `/issue`-time 422 into a `/sign`-time rejection (or a signed receipt with an error signature). This is the coupling with #662 level 2 / level 4. Which behaviour do we want when the receipt is fiscally valid but not eInvoice-complete: sign and flag, or reject?

Proposal to start the discussion: **C + D**. Generation is switched on per queue in configuration, defaulted from the market, overridable per request. The Middleware emits exactly one XML per receipt (market CIUS, market default syntax). Hybrid PDF, alternative syntaxes and delivery remain `/issue` concerns.

---

## 3. Topic 2: how is the recipient's electronic address obtained

The address is needed in two places: **inside the document** (BT-49 buyer electronic address + scheme, BT-34 seller electronic address) and **for routing** (`/issue` `Target.Address` today). They should be the same value, provided once.

Constraints:

- A VAT ID is not a Peppol participant ID. SMP lookup can verify that a given participant ID is registered and which document types it accepts; it cannot discover the ID from a VAT ID. Peppol Directory search is best effort.
- France routes by SIREN (+ optional routing code) through the PPF directory (annuaire), not by Peppol participant ID. The scheme is `0225` (SIREN-based French electronic address) or `0002` / `0009`.
- The Middleware may be offline and must not depend on a directory call to sign. The Receipt API currently does no lookup either.
- The POS is the party that knows the customer. Today the POS already sends the address on `/issue`; moving it to `/sign` only changes when it is sent.

Options:

- **A. Typed extension of `cbCustomer`.** Add `CustomerElectronicAddress` + `CustomerElectronicAddressScheme` (ISO 6523 ICD / EAS code, e.g. `0088`, `0208`, `0225`, `9930`) to the documented `cbCustomer` object and to `MiddlewareCustomer`. Cross-market, ends up in the DSFinV-K style record as well. Also settles the `CustomerCountry` alpha-2 vs alpha-3 inconsistency between the docs and the delivery examples, and the undocumented `CustomerEmail` used there.
- **B. Market case data.** Keep it in `ftReceiptCaseData.FR.Customer*` (SIRET already lives there) and equivalents per market. Already the pattern for France, but not portable, and DE / EU would each need their own block.
- **C. Resolution by the Middleware.** Derive from VAT ID / SIRET via lookup at sign time. Rejected for the offline reason above; verification (not discovery) could be an optional #662 level 3 check.
- **D. Portal customer master data.** A customer directory in the portal keyed by `CustomerId`. Does not exist today and would move customer data ownership to fiskaltrust.

Questions to settle:

1. A or B as the carrier? (Proposal: A for the address and scheme, B only for identifiers that are truly market-specific such as SIRET, with the FR block reading A first.)
2. Does the Middleware validate the address at all (syntax of the scheme, checksum), or only echo it into BT-49? If validated, at which #662 level?
3. Should `/issue` `Target.Address` for `peppol` become optional, defaulting to BT-49 of the archived document, so the address is provided exactly once on `/sign`?
4. What does a B2B invoice without a recipient address produce: a valid EN 16931 document without BT-49 (allowed by the core, rejected by Peppol BIS), a warning signature, or a rejection?

---

## 4. Topic 3: where does the seller enrichment move

Today the seller party, the FR company data and the Peppol sender ID are fetched from portal master data by the Receipt API **at render time**, with the explicit note that this is a stopgap "until the SCU carries all master data fields". If the document is produced at `/sign`, that data has to be available to the queue when it signs.

Options:

- **A. POSSystemAPI enriches the request before `/sign`.** The POSSystemAPI already fills `ftCashBoxID`, `ftPosSystemId`, `cbTerminalID` and the country code, so adding seller data is mechanically easy. But: direct integrators bypass the POSSystemAPI; seller identity would appear in the request as if the POS had asserted it; the Middleware would have to trust request data for legally binding seller identifiers, which the FR implementation deliberately does not do (SIRET from the signed payload only). It also does not work offline.
- **B. Queue / SCU configuration.** The configuration package pushed to the cashbox carries the seller master data (legal name, trading name, address, VAT ID, tax number, legal registration ID + scheme, electronic address / Peppol sender participant ID, market-specific fields such as legal form, RCS, NAF, VAT payment option, payment terms). The FR queue / SCU already carries SIRET and signs it into the QR payload, so this extends an existing pattern rather than inventing one. The Middleware signs the document with data it owns.
- **C. Hybrid.** Identity that must be bound to the signature (B) lives in queue / SCU configuration; purely presentational data (logo, contact e-mail, phone, website, free text) stays in receipt settings and is applied by `/issue` when rendering PDF / HTML. Master data edits in the portal reach the queue through a configuration update, as for any other configuration change.

Questions to settle:

1. B or C, and which fields exactly are "signature-bound" versus "presentational"? Proposal: everything that lands in BT-27..BT-40 and BT-8 / BT-20 is signature-bound.
2. Propagation: how does a master data change in the portal reach a running queue (configuration reload, restart, next daily closing)? What is the behaviour for receipts signed between the edit and the reload?
3. Validation at configuration time: a queue configured for eInvoice generation with incomplete seller data should fail loudly at start-up, not produce an invalid document per receipt. Today, missing settings fall back to a default seller at render time, which would silently produce a wrong seller on a legal document once generation moves into the signing path.
4. Does the Peppol **sender** participant ID belong to the queue (per outlet, as receipt settings have it today) or to the account?
5. Is there anything the POSSystemAPI should still add? Proposal: nothing seller-related; it stays a pass-through for the format token and the send target.

---

## 5. Cross-cutting decisions

- **Response carrier.** New global signature type for "eInvoice document" (free space in the `sss` table; only GR uses `010` for myDATA XML), `SignatureFormat.Base64` or a new `Xml` format, `DigitalReceiptOnly | ArchivingRequired` flags. The documented 1023-character limit on `Data` has to be lifted, or the response carries a `Link` and the document is stored next to the queue item. Inline has the advantage that the eInvoice becomes part of the signed, archived queue item; the cost is journal size.
- **Archiving and re-issue.** If the document is inside the response, a `ReceiptRequested` re-send returns the same document (good). `/issue` `download` then becomes a lookup, not a render.
- **One mapper.** The Receipt API has a receipt-to-EN 16931 mapper, and the France e-invoicing platform work has a second mapping of ifPOS v2 receipts to Factur-X CII. The Middleware should end up with one mapper both consume, not a third.
- **Docs.** The *Delivery (/issue Endpoint)* page and the `cbCustomer` data structure page need updating; the `0200` / `0400` / `0800` gggg discrepancy between docs and the interface enum needs a decision.
- **France timeline.** The reception obligation and the emission obligation for large and mid-size companies started on 1 September 2026; SMEs follow on 1 September 2027. Whatever we decide here is on the critical path for FR B2B customers.

---

## 6. Proposed next steps

1. Agree on the answers to the numbered questions in sections 2 to 4 in this thread.
2. Write the interface change (new flag / case data block, typed `cbCustomer` fields, signature type / format) as a PR against the interface package and docs.
3. Lift the existing EN 16931 generator into a Middleware package and wire it behind the QueueFR.v2 processor first (the market with the mandate and the richest current implementation), then DE.
4. Reduce `/issue` to lookup + rendering + delivery for eInvoice formats.

Contributor guide

Open the contributing guide

Research direction

Start with SignProcessor.cs, MiddlewareCustomer.cs, and the ReceiptResponse/signature definitions, then compare the current `/issue` documentation and the linked #662 validation levels. Done means the RFC resolves the format trigger, recipient-address carrier, seller-enrichment ownership, and validation behavior well enough to define an implementation contract before any PR.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
api, backend-api-design
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.