feat(peppol): receive e-invoices via Qvalia: registration, inbound archive, inbox delivery (#1789)
* 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>
This commit is contained in:
co-authored by
Claude Fable 5
Jakob Wennberg
parent
99a872987e
commit
f93152c397
@@ -1294,6 +1294,42 @@ const INVOICE: Record<string, StructuredErrorEntry> = {
|
||||
message_sv: 'Peppol-operatören kunde inte nås just nu. Fakturan har inte skickats; försök igen om en stund.',
|
||||
message_en: 'The Peppol access point could not be reached. The invoice has not been sent; try again shortly.',
|
||||
},
|
||||
// /api/settings/peppol: publishing a company's identifier for receiving.
|
||||
PEPPOL_RECEIVING_UNSUPPORTED: {
|
||||
httpStatus: 503,
|
||||
message_sv: 'Den konfigurerade Peppol-operatören stöder inte mottagning av e-fakturor.',
|
||||
message_en: 'The configured Peppol access point does not support receiving e-invoices.',
|
||||
},
|
||||
PEPPOL_SANDBOX_NOT_ALLOWED: {
|
||||
httpStatus: 403,
|
||||
message_sv: 'Peppol-registrering är inte tillgänglig i demobolaget. Skapa ett riktigt konto för att ta emot e-fakturor.',
|
||||
message_en: 'Peppol registration is not available in the demo company. Create a real account to receive e-invoices.',
|
||||
},
|
||||
PEPPOL_REGISTRATION_ORG_NUMBER_REQUIRED: {
|
||||
httpStatus: 422,
|
||||
message_sv: 'Bolaget behöver ett giltigt organisationsnummer i företagsinställningarna innan det kan ta emot e-fakturor via Peppol.',
|
||||
message_en: 'The company needs a valid organisation number in company settings before it can receive e-invoices via Peppol.',
|
||||
},
|
||||
PEPPOL_REGISTRATION_PERSONAL_NUMBER: {
|
||||
httpStatus: 422,
|
||||
message_sv: 'Enskild firma med personnummer kan ännu inte registreras för Peppol: det skulle publicera personuppgifter i Peppol-katalogen. Stöd för GLN-nummer kommer.',
|
||||
message_en: 'A sole trader identified by a personal identity number cannot be registered for Peppol yet: it would publish personal data in the Peppol directory. GLN support is coming.',
|
||||
},
|
||||
PEPPOL_REGISTRATION_COMPANY_NAME_REQUIRED: {
|
||||
httpStatus: 422,
|
||||
message_sv: 'Bolaget behöver ett företagsnamn i företagsinställningarna innan det kan registreras för Peppol.',
|
||||
message_en: 'The company needs a company name in company settings before it can be registered for Peppol.',
|
||||
},
|
||||
PEPPOL_REGISTRATION_FAILED: {
|
||||
httpStatus: 502,
|
||||
message_sv: 'Peppol-operatören kunde inte genomföra registreringen. Försök igen om en stund.',
|
||||
message_en: 'The Peppol access point could not complete the registration. Try again shortly.',
|
||||
},
|
||||
PEPPOL_REGISTRATION_NOT_FOUND: {
|
||||
httpStatus: 404,
|
||||
message_sv: 'Bolaget är inte registrerat för Peppol-mottagning.',
|
||||
message_en: 'The company is not registered for Peppol receiving.',
|
||||
},
|
||||
}
|
||||
|
||||
const SUPPLIER_INVOICE: Record<string, StructuredErrorEntry> = {
|
||||
|
||||
Reference in New Issue
Block a user