feat(peppol): poll outbound delivery status + cap receiving registrations (#1793)

* feat(peppol): poll outbound delivery status from the access point

Qvalia's webhook API answers 404 on its production host (the sandbox answers
204), so without this the prod lifecycle would stop at submission_accepted.
The transport gains pollDeliveryStatus(); the Qvalia adapter reads
/invoices/outgoing/status and maps the message-log status through the same
tolerant mapping as a document_delivery webhook, with the same dedupe key, so
a later webhook for the same transition is a harmless duplicate. A cron four
times an hour walks the open deliveries of the last 45 days, records the
answer through the append-only lifecycle RPC and fetches evidence once a
delivery reaches transport or a terminal state. Kept as the safety net for a
missed webhook once Qvalia ships them to prod.

Refs #546

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqFpxeWqbpR7bcwUJLRERQ

* feat(peppol): cap receiving registrations at the contracted tenant count

The Qvalia partner contract is priced per tenant (10 to start), so the
registration refuses the next company with PEPPOL_REGISTRATION_CAP_REACHED
once PEPPOL_RECEIVING_MAX_REGISTRATIONS live registrations exist, instead of
silently exceeding the contract. A company that already holds a live row is
never counted twice; unset means no cap (own provider account).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqFpxeWqbpR7bcwUJLRERQ

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-21 17:06:57 +02:00
committed by GitHub
co-authored by Claude Fable 5 Jakob Wennberg
parent a2c9a12cfc
commit 9ef7de861f
15 changed files with 576 additions and 48 deletions
+1
View File
@@ -100,6 +100,7 @@ v1 uses Qvalia's consolidated setup (every company's documents under Accounted's
- `peppol_registrations`: one live row per company and per participant; written by `POST/DELETE /api/settings/peppol` (service role after the membership check) through `lib/invoices/peppol-registration.ts`, which publishes `0007:orgnr` with the company's business card and the BIS Billing 3 Invoice + CreditNote document types via `transport.registerRecipient()`. Personnummer-based identifiers are refused (`0088` GLN pending). The switch lives in Settings > Fakturering ("E-faktura via Peppol").
- `peppol_inbound_documents`: every document the Access Point hands us, with the exact XML (immutable, undeletable) and the provider's UBL-JSON; routed to a company by the `AccountingCustomerParty` endpoint through the registrations; states `received`, `routed`, `unrouted`, `converted`, `ignored`, `failed`.
- `GET /api/peppol/inbound/cron` every 10 minutes: `lib/invoices/peppol-inbound.ts` lists unread invoices and credit notes, archives (`archiveInboundPeppolMessage`), routes and delivers; `lib/invoices/peppol-inbox-delivery.ts` archives the XML as a WORM document (`upload_source: 'e_invoice'`, no AI extraction), the embedded PDF when present, and creates the `invoice_inbox_items` row (`source: 'peppol'`) with the extraction filled from the structured UBL (`lib/invoices/peppol-inbound-ubl.ts`, confidence 1). The existing inbox review and convert flows take over from there.
- Outbound status without webhooks: `GET /api/peppol/outbound/status/cron` (four times an hour) asks the Access Point about every open delivery (`transport.pollDeliveryStatus`, Qvalia: `/invoices/outgoing/status`) and records the answer through the same lifecycle RPC a webhook uses, with the same dedupe key, so a later webhook for the same transition is a harmless duplicate. Needed because Qvalia's webhook API answers 404 on its production host (2026-08-21); kept as the safety net afterwards.
- Still open: the Qvalia `new_document` webhook for inbound (today polled), credit-note conversion from the inbox, `0088` GLN for enskild firma, the release-pinned validation stack, and a UI surface for `unrouted` documents.
### Storecove versus Qvalia (historical, pre-contract)