* 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>
* feat(peppol): receive e-invoices via Qvalia: registration, inbound archive, inbox delivery
Second Peppol slice (#546). Qvalia confirmed that sending needs no
per-company account, so receiving keeps the consolidated partner account:
each company publishes its 0007:orgnr on our account and inbound documents
are routed by the AccountingCustomerParty endpoint.
- PeppolTransport grows optional receiving methods (registerRecipient,
unregisterRecipient, listInboundDocuments, fetchInboundDocumentXml); the
Qvalia adapter implements them (PUT/DELETE /peppol/{id}, readinvoices /
readcreditnotes, exact XML fetch).
- lib/invoices/peppol-inbound-ubl.ts reads the provider's UBL-JSON
(xml2js-style prefixed keys, verified against Qvalia's real inbound test
invoice, kept as a fixture) into a neutral document: parties, payment
means with SE:BANKGIRO/SE:PLUSGIRO/IBAN, totals, VAT subtotals, lines,
embedded attachments, credit notes.
- Migration 20260821170000: peppol_registrations (one live row per company
and participant), peppol_inbound_documents (exact XML immutable and
undeletable, routed once), invoice_inbox_items.source gains 'peppol' with a
per-channel dedupe index; pg-real test covers RLS, uniqueness, immutability
and routing.
- POST/DELETE/GET /api/settings/peppol + "E-faktura via Peppol" switch in
Settings > Fakturering; personnummer-based companies are refused until 0088
GLN exists; sandbox refused.
- GET /api/peppol/inbound/cron every 10 minutes: archive, route, deliver.
lib/invoices/peppol-inbox-delivery.ts archives the XML as a WORM document
(upload_source e_invoice, extractionOwner none), an embedded PDF when
present, and creates the inbox row with the extraction filled from the UBL
(confidence 1, no model pass), matching the supplier by org number. The
existing inbox review/convert flow takes over.
- document-service accepts application/xml for the archive; inbox list shows
a Peppol icon.
Refs #546
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqFpxeWqbpR7bcwUJLRERQ
* test(peppol): archive contract, pg fixture and phantom-column ceiling for the receiving tables
The two new tables are räkenskapsinformation and join MASTER_DATA_DUMP_TABLES;
the pg fixture for a deregistered row now carries deregistered_at as the
status-shape constraint requires; the archive insert is an inline literal and
the one generic processing-state updater is accounted for in the ceiling.
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>