diff --git a/lib/invoices/__tests__/peppol-qvalia-transport.test.ts b/lib/invoices/__tests__/peppol-qvalia-transport.test.ts index 749541cc..0efa37f7 100644 --- a/lib/invoices/__tests__/peppol-qvalia-transport.test.ts +++ b/lib/invoices/__tests__/peppol-qvalia-transport.test.ts @@ -271,8 +271,10 @@ describe('Qvalia transport: submit', () => { data: [{ integrationId: 'int-dup', Invoice: { - ID: [{ _: 'F-2026-42' }], - AccountingSupplierParty: [{ Party: [{ EndpointID: [{ _: '556016-0680', schemeID: '0007' }] }] }], + 'cbc:ID': [{ _: 'F-2026-42' }], + 'cac:AccountingSupplierParty': [{ + 'cac:Party': [{ 'cbc:EndpointID': [{ _: '556016-0680', $: { schemeID: '0007' } }] }], + }], }, }], })) @@ -446,6 +448,16 @@ describe('helpers', () => { expect(extractUblJsonSupplierEndpoint({ Invoice: { AccountingSupplierParty: [{ Party: [{ EndpointID: [{ _: '1', schemeID: '0007' }] }] }] }, })).toEqual({ scheme: '0007', identifier: '1' }) + // Qvalia's live shape: prefixed keys, attributes under `$`. + expect(extractUblJsonSupplierEndpoint({ + Invoice: { + $: { xmlns: 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2' }, + 'cac:AccountingSupplierParty': [{ + 'cac:Party': [{ 'cbc:EndpointID': [{ _: '5567321707', $: { schemeID: '0007' } }] }], + }], + }, + integrationId: 'a5845a11-4e5a-4700-bca3-e670a6cd8a79', + })).toEqual({ scheme: '0007', identifier: '5567321707' }) expect(extractUblJsonSupplierEndpoint({ Invoice: {} })).toBeNull() }) diff --git a/lib/invoices/transports/qvalia.ts b/lib/invoices/transports/qvalia.ts index a5f8561d..24926397 100644 --- a/lib/invoices/transports/qvalia.ts +++ b/lib/invoices/transports/qvalia.ts @@ -242,18 +242,32 @@ export function extractUblDocumentId(xml: string): string | null { } /** - * Dig the seller endpoint identifier out of a UBL-JSON invoice as Qvalia - * returns it (OASIS UBL 2.1 JSON: arrays everywhere, text under `_`). + * UBL-JSON as Qvalia returns it (verified live 2026-08-21): xml2js style, so + * element keys keep their namespace prefix (`cac:AccountingSupplierParty`, + * `cbc:EndpointID`), every element is an array, text sits under `_` and + * attributes under `$`. The OASIS UBL-JSON form (unprefixed keys, attributes + * beside `_`) is accepted too. */ +function ublChild(record: Record | null, name: string): unknown { + if (!record) return undefined + return record[name] ?? record[`cac:${name}`] ?? record[`cbc:${name}`] +} + +function ublAttribute(element: Record | null, name: string): string | null { + if (!element) return null + return asString(element[name]) ?? asString(asRecord(element.$)?.[name]) +} + +/** Dig the seller endpoint identifier out of a UBL-JSON invoice message. */ export function extractUblJsonSupplierEndpoint(message: unknown): PeppolParticipant | null { const record = asRecord(message) if (!record) return null - const invoice = asRecord(record.Invoice) ?? record - const supplierParty = firstRecord(invoice.AccountingSupplierParty) - const party = firstRecord(supplierParty?.Party) - const endpoint = firstRecord(party?.EndpointID) + const invoice = asRecord(ublChild(record, 'Invoice')) ?? record + const supplierParty = firstRecord(ublChild(invoice, 'AccountingSupplierParty')) + const party = firstRecord(ublChild(supplierParty, 'Party')) + const endpoint = firstRecord(ublChild(party, 'EndpointID')) const identifier = asString(endpoint?._) - const scheme = asString(endpoint?.schemeID) + const scheme = ublAttribute(endpoint, 'schemeID') if (!identifier || !scheme) return null return { scheme, identifier } } diff --git a/scripts/peppol/qvalia-probe.ts b/scripts/peppol/qvalia-probe.ts index d0744c9d..adeba07a 100644 --- a/scripts/peppol/qvalia-probe.ts +++ b/scripts/peppol/qvalia-probe.ts @@ -16,6 +16,9 @@ * Lists the Peppol identifiers registered on the partner account. * npx tsx scripts/peppol/qvalia-probe.ts outgoing * Lists the three latest outgoing invoice statuses (read-only). + * npx tsx scripts/peppol/qvalia-probe.ts incoming [integrationId] + * Lists the latest incoming invoice statuses, or prints one incoming + * invoice as XML (read-only: uses the non-marking endpoint). * npx tsx scripts/peppol/qvalia-probe.ts send path/to/invoice.xml * Submits a BIS Billing 3 XML through the adapter. Sandbox only: the * script refuses a production base URL. @@ -110,6 +113,22 @@ async function main(): Promise { ) return } + case 'incoming': { + if (argument) { + const response = await fetch( + `${config!.baseUrl}/partner/${partner}/transaction/${account}/invoices/incoming?integrationId=${encodeURIComponent(argument)}&includeRead=true&limit=1`, + { headers: { Authorization: headerFor(config!.authScheme), accept: 'application/xml' } }, + ) + console.log(`HTTP ${response.status}`) + console.log((await response.text()).slice(0, 6000)) + return + } + await show( + 'GET /partner/{p}/transaction/{a}/invoices/incoming/status', + await rawGet(`/partner/${partner}/transaction/${account}/invoices/incoming/status?includeRead=true&limit=5`), + ) + return + } case 'webhook': { await show('GET /partner/{p}/webhook/configure', await rawGet(`/partner/${partner}/webhook/configure`)) return