@getpeppr/cli 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js DELETED
@@ -1,4466 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/index.ts
4
- import { createRequire } from "module";
5
- import { Command } from "commander";
6
-
7
- // src/utils/file.ts
8
- import { readFileSync, existsSync } from "fs";
9
- import { resolve } from "path";
10
-
11
- // src/utils/errors.ts
12
- function exitWithError(message, code = 2) {
13
- process.stderr.write(message + "\n");
14
- process.exit(code);
15
- }
16
-
17
- // src/utils/file.ts
18
- function readJsonFile(filePath) {
19
- const resolved = resolve(filePath);
20
- if (!existsSync(resolved)) {
21
- return { ok: false, error: `Error: file not found \u2014 ${resolved}` };
22
- }
23
- let content;
24
- try {
25
- content = readFileSync(resolved, "utf-8");
26
- } catch {
27
- return { ok: false, error: `Error: could not read file \u2014 ${resolved}` };
28
- }
29
- try {
30
- const data = JSON.parse(content);
31
- return { ok: true, data };
32
- } catch {
33
- return { ok: false, error: `Error: invalid JSON in file \u2014 ${resolved}` };
34
- }
35
- }
36
- function readAndValidateInvoiceJson(filePath) {
37
- const parseResult = readJsonFile(filePath);
38
- if (!parseResult.ok) {
39
- exitWithError(parseResult.error);
40
- }
41
- if (typeof parseResult.data !== "object" || parseResult.data === null || Array.isArray(parseResult.data)) {
42
- exitWithError(
43
- "Error: JSON file must contain an object, not an array or primitive"
44
- );
45
- }
46
- return parseResult.data;
47
- }
48
-
49
- // src/formatters/validation.ts
50
- import pc from "picocolors";
51
- function sectionHeader(title) {
52
- const pad = 45 - title.length - 4;
53
- return pc.dim(`\u2500\u2500 ${title} ${"\u2500".repeat(Math.max(pad, 3))}`);
54
- }
55
- function formatError(item) {
56
- const ruleId = "ruleId" in item && item.ruleId ? pc.dim(` (${item.ruleId})`) : "";
57
- const field = "field" in item && item.field ? `${item.field} \u2014 ` : "";
58
- return ` ${pc.red("\u2717")} ${field}${item.message}${ruleId}`;
59
- }
60
- function formatWarning(item) {
61
- const ruleId = "ruleId" in item && item.ruleId ? pc.dim(` (${item.ruleId})`) : "";
62
- const field = "field" in item && item.field ? `${item.field} \u2014 ` : "";
63
- return ` ${pc.yellow("\u26A0")} ${field}${item.message}${ruleId}`;
64
- }
65
- function formatSection(title, errors, warnings) {
66
- const lines = [sectionHeader(title)];
67
- if (errors.length === 0 && warnings.length === 0) {
68
- lines.push(` ${pc.green("\u2713")} All rules passed`);
69
- return lines.join("\n");
70
- }
71
- if (errors.length === 0) {
72
- lines.push(` ${pc.green("\u2713")} No errors`);
73
- }
74
- for (const err of errors) {
75
- lines.push(formatError(err));
76
- }
77
- for (const warn2 of warnings) {
78
- lines.push(formatWarning(warn2));
79
- }
80
- return lines.join("\n");
81
- }
82
- function formatValidationResult(filename, result) {
83
- const lines = [];
84
- lines.push(`
85
- Validating: ${pc.bold(filename)}
86
- `);
87
- lines.push(
88
- formatSection(
89
- "Structure",
90
- result.structure.errors,
91
- result.structure.warnings
92
- )
93
- );
94
- lines.push("");
95
- lines.push(
96
- formatSection(
97
- "Business Rules (Peppol BIS 3.0)",
98
- result.schematron.errors,
99
- result.schematron.warnings
100
- )
101
- );
102
- lines.push("");
103
- lines.push(
104
- formatSection(
105
- "Country Rules",
106
- result.countryRules.errors,
107
- result.countryRules.warnings
108
- )
109
- );
110
- lines.push("");
111
- lines.push(sectionHeader("Summary"));
112
- const { totalErrors, totalWarnings, valid } = result;
113
- if (valid && totalWarnings === 0) {
114
- lines.push(` ${pc.green(pc.bold("\u2713 Invoice is valid"))}`);
115
- } else if (valid) {
116
- lines.push(
117
- ` ${pc.green(pc.bold("\u2713 Invoice is valid"))} ${pc.dim(`(${totalWarnings} warning${totalWarnings === 1 ? "" : "s"})`)}`
118
- );
119
- } else {
120
- const parts = [];
121
- parts.push(`${totalErrors} error${totalErrors === 1 ? "" : "s"}`);
122
- if (totalWarnings > 0) {
123
- parts.push(`${totalWarnings} warning${totalWarnings === 1 ? "" : "s"}`);
124
- }
125
- lines.push(
126
- ` ${pc.red(pc.bold(`\u2717 ${parts.join(", ")}`))} \u2014 invoice non-compliant`
127
- );
128
- }
129
- lines.push("");
130
- return lines.join("\n");
131
- }
132
-
133
- // ../sdk/dist/core/ubl-builder.js
134
- var UBL_NS = "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2";
135
- var CAC_NS = "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2";
136
- var CBC_NS = "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2";
137
- var CREDIT_NOTE_NS = "urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2";
138
- var PEPPOL_CUSTOMIZATION_ID = "urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0";
139
- var PEPPOL_PROFILE_ID = "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0";
140
- var DEFAULT_UNIT = "EA";
141
- var DEFAULT_PAYMENT_MEANS = 30;
142
- var UNIT_CODE_MAP = {
143
- each: "EA",
144
- piece: "EA",
145
- pieces: "EA",
146
- hour: "HUR",
147
- hours: "HUR",
148
- day: "DAY",
149
- days: "DAY",
150
- week: "WEE",
151
- weeks: "WEE",
152
- month: "MON",
153
- months: "MON",
154
- year: "ANN",
155
- years: "ANN",
156
- kilogram: "KGM",
157
- kg: "KGM",
158
- meter: "MTR",
159
- metre: "MTR",
160
- liter: "LTR",
161
- litre: "LTR",
162
- unit: "C62",
163
- units: "C62",
164
- set: "SET",
165
- sets: "SET",
166
- pack: "PK",
167
- packs: "PK"
168
- };
169
- function resolveUnitCode(unit) {
170
- return UNIT_CODE_MAP[unit.toLowerCase()] ?? unit;
171
- }
172
- function escapeXml(str) {
173
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
174
- }
175
- function formatDate(dateStr) {
176
- if (!dateStr) {
177
- return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
178
- }
179
- return dateStr.split("T")[0];
180
- }
181
- function formatAmount(amount) {
182
- return amount.toFixed(2);
183
- }
184
- function round2(n) {
185
- return Math.round(n * 100) / 100;
186
- }
187
- function parsePeppolId(peppolId) {
188
- const scheme = peppolId.split(":")[0];
189
- const id = peppolId.split(":").slice(1).join(":");
190
- return { scheme, id };
191
- }
192
- function buildPartyXml(party, role) {
193
- const { scheme: endpointScheme, id: endpointId } = parsePeppolId(party.peppolId);
194
- return `
195
- <cac:${role}>
196
- <cac:Party>
197
- <cbc:EndpointID schemeID="${escapeXml(endpointScheme)}">${escapeXml(endpointId)}</cbc:EndpointID>
198
- <cac:PartyIdentification>
199
- <cbc:ID schemeID="${escapeXml(endpointScheme)}">${escapeXml(endpointId)}</cbc:ID>
200
- </cac:PartyIdentification>
201
- <cac:PartyName>
202
- <cbc:Name>${escapeXml(party.name)}</cbc:Name>
203
- </cac:PartyName>
204
- <cac:PostalAddress>
205
- ${party.street ? `<cbc:StreetName>${escapeXml(party.street)}</cbc:StreetName>` : ""}
206
- ${party.city ? `<cbc:CityName>${escapeXml(party.city)}</cbc:CityName>` : ""}
207
- ${party.postalCode ? `<cbc:PostalZone>${escapeXml(party.postalCode)}</cbc:PostalZone>` : ""}
208
- <cac:Country>
209
- <cbc:IdentificationCode>${escapeXml(party.country)}</cbc:IdentificationCode>
210
- </cac:Country>
211
- </cac:PostalAddress>
212
- ${party.vatNumber ? `<cac:PartyTaxScheme>
213
- <cbc:CompanyID>${escapeXml(party.vatNumber)}</cbc:CompanyID>
214
- <cac:TaxScheme>
215
- <cbc:ID>VAT</cbc:ID>
216
- </cac:TaxScheme>
217
- </cac:PartyTaxScheme>` : ""}
218
- <cac:PartyLegalEntity>
219
- <cbc:RegistrationName>${escapeXml(party.name)}</cbc:RegistrationName>
220
- ${party.companyId ? `<cbc:CompanyID>${escapeXml(party.companyId)}</cbc:CompanyID>` : ""}
221
- </cac:PartyLegalEntity>
222
- ${party.contactName || party.phone || party.email ? `<cac:Contact>
223
- ${party.contactName ? `<cbc:Name>${escapeXml(party.contactName)}</cbc:Name>` : ""}
224
- ${party.phone ? `<cbc:Telephone>${escapeXml(party.phone)}</cbc:Telephone>` : ""}
225
- ${party.email ? `<cbc:ElectronicMail>${escapeXml(party.email)}</cbc:ElectronicMail>` : ""}
226
- </cac:Contact>` : ""}
227
- </cac:Party>
228
- </cac:${role}>`;
229
- }
230
- function buildPayeePartyXml(party) {
231
- const { scheme, id } = parsePeppolId(party.peppolId);
232
- return `
233
- <cac:PayeeParty>
234
- <cac:PartyIdentification>
235
- <cbc:ID schemeID="${escapeXml(scheme)}">${escapeXml(id)}</cbc:ID>
236
- </cac:PartyIdentification>
237
- <cac:PartyName>
238
- <cbc:Name>${escapeXml(party.name)}</cbc:Name>
239
- </cac:PartyName>
240
- ${party.companyId ? `<cac:PartyLegalEntity>
241
- <cbc:RegistrationName>${escapeXml(party.name)}</cbc:RegistrationName>
242
- <cbc:CompanyID>${escapeXml(party.companyId)}</cbc:CompanyID>
243
- </cac:PartyLegalEntity>` : ""}
244
- </cac:PayeeParty>`;
245
- }
246
- function buildTaxRepresentativePartyXml(party) {
247
- const parts = [
248
- " <cac:TaxRepresentativeParty>",
249
- " <cac:PartyName>",
250
- ` <cbc:Name>${escapeXml(party.name)}</cbc:Name>`,
251
- " </cac:PartyName>"
252
- ];
253
- parts.push(" <cac:PostalAddress>");
254
- if (party.street) {
255
- parts.push(` <cbc:StreetName>${escapeXml(party.street)}</cbc:StreetName>`);
256
- }
257
- if (party.city) {
258
- parts.push(` <cbc:CityName>${escapeXml(party.city)}</cbc:CityName>`);
259
- }
260
- if (party.postalCode) {
261
- parts.push(` <cbc:PostalZone>${escapeXml(party.postalCode)}</cbc:PostalZone>`);
262
- }
263
- parts.push(" <cac:Country>");
264
- parts.push(` <cbc:IdentificationCode>${escapeXml(party.country)}</cbc:IdentificationCode>`);
265
- parts.push(" </cac:Country>");
266
- parts.push(" </cac:PostalAddress>");
267
- if (party.vatNumber) {
268
- parts.push(" <cac:PartyTaxScheme>");
269
- parts.push(` <cbc:CompanyID>${escapeXml(party.vatNumber)}</cbc:CompanyID>`);
270
- parts.push(" <cac:TaxScheme>");
271
- parts.push(" <cbc:ID>VAT</cbc:ID>");
272
- parts.push(" </cac:TaxScheme>");
273
- parts.push(" </cac:PartyTaxScheme>");
274
- }
275
- parts.push(" </cac:TaxRepresentativeParty>");
276
- return parts.join("\n");
277
- }
278
- function buildAttachmentXml(attachment) {
279
- const parts = [
280
- "<cac:AdditionalDocumentReference>",
281
- ` <cbc:ID>${escapeXml(attachment.id)}</cbc:ID>`
282
- ];
283
- if (attachment.description) {
284
- parts.push(` <cbc:DocumentDescription>${escapeXml(attachment.description)}</cbc:DocumentDescription>`);
285
- }
286
- if (attachment.content || attachment.url) {
287
- parts.push(" <cac:Attachment>");
288
- if (attachment.content && attachment.mimeType && attachment.filename) {
289
- parts.push(` <cbc:EmbeddedDocumentBinaryObject mimeCode="${escapeXml(attachment.mimeType)}" filename="${escapeXml(attachment.filename)}">${attachment.content}</cbc:EmbeddedDocumentBinaryObject>`);
290
- } else if (attachment.url) {
291
- parts.push(` <cac:ExternalReference>
292
- <cbc:URI>${escapeXml(attachment.url)}</cbc:URI>
293
- </cac:ExternalReference>`);
294
- }
295
- parts.push(" </cac:Attachment>");
296
- }
297
- parts.push("</cac:AdditionalDocumentReference>");
298
- return parts.join("\n ");
299
- }
300
- function buildInvoicePeriodXml(period) {
301
- const parts = ["<cac:InvoicePeriod>"];
302
- if (period.startDate) {
303
- parts.push(` <cbc:StartDate>${formatDate(period.startDate)}</cbc:StartDate>`);
304
- }
305
- if (period.endDate) {
306
- parts.push(` <cbc:EndDate>${formatDate(period.endDate)}</cbc:EndDate>`);
307
- }
308
- parts.push("</cac:InvoicePeriod>");
309
- return parts.join("\n ");
310
- }
311
- function buildDeliveryXml(delivery) {
312
- const parts = ["<cac:Delivery>"];
313
- if (delivery.date) {
314
- parts.push(` <cbc:ActualDeliveryDate>${formatDate(delivery.date)}</cbc:ActualDeliveryDate>`);
315
- }
316
- if (delivery.locationId || delivery.address) {
317
- parts.push(" <cac:DeliveryLocation>");
318
- if (delivery.locationId) {
319
- parts.push(` <cbc:ID>${escapeXml(delivery.locationId)}</cbc:ID>`);
320
- }
321
- if (delivery.address) {
322
- parts.push(" <cac:Address>");
323
- if (delivery.address.street) {
324
- parts.push(` <cbc:StreetName>${escapeXml(delivery.address.street)}</cbc:StreetName>`);
325
- }
326
- if (delivery.address.city) {
327
- parts.push(` <cbc:CityName>${escapeXml(delivery.address.city)}</cbc:CityName>`);
328
- }
329
- if (delivery.address.postalCode) {
330
- parts.push(` <cbc:PostalZone>${escapeXml(delivery.address.postalCode)}</cbc:PostalZone>`);
331
- }
332
- parts.push(` <cac:Country>
333
- <cbc:IdentificationCode>${escapeXml(delivery.address.country)}</cbc:IdentificationCode>
334
- </cac:Country>`);
335
- parts.push(" </cac:Address>");
336
- }
337
- parts.push(" </cac:DeliveryLocation>");
338
- }
339
- parts.push("</cac:Delivery>");
340
- return parts.join("\n ");
341
- }
342
- function buildDocumentAllowanceChargeXml(item, isCharge, currency) {
343
- const vatCategory = item.vatCategory ?? "S";
344
- return `
345
- <cac:AllowanceCharge>
346
- <cbc:ChargeIndicator>${isCharge}</cbc:ChargeIndicator>
347
- <cbc:AllowanceChargeReason>${escapeXml(item.reason)}</cbc:AllowanceChargeReason>
348
- <cbc:Amount currencyID="${escapeXml(currency)}">${formatAmount(item.amount)}</cbc:Amount>
349
- <cac:TaxCategory>
350
- <cbc:ID>${vatCategory}</cbc:ID>
351
- <cbc:Percent>${item.vatRate}</cbc:Percent>
352
- <cac:TaxScheme>
353
- <cbc:ID>VAT</cbc:ID>
354
- </cac:TaxScheme>
355
- </cac:TaxCategory>
356
- </cac:AllowanceCharge>`;
357
- }
358
- function buildLineAllowanceChargeXml(reason, amount, isCharge, currency) {
359
- return `
360
- <cac:AllowanceCharge>
361
- <cbc:ChargeIndicator>${isCharge}</cbc:ChargeIndicator>
362
- <cbc:AllowanceChargeReason>${escapeXml(reason)}</cbc:AllowanceChargeReason>
363
- <cbc:Amount currencyID="${escapeXml(currency)}">${formatAmount(amount)}</cbc:Amount>
364
- </cac:AllowanceCharge>`;
365
- }
366
- function calculateLineExtensionAmount(line) {
367
- const base = line.quantity * line.unitPrice;
368
- const lineAllowances = (line.allowances ?? []).reduce((sum, a) => sum + a.amount, 0);
369
- const lineCharges = (line.charges ?? []).reduce((sum, c) => sum + c.amount, 0);
370
- return base - lineAllowances + lineCharges;
371
- }
372
- function buildDocumentLineXml(line, index, currency, lineTag, qtyTag) {
373
- const lineTotal = calculateLineExtensionAmount(line);
374
- const unit = resolveUnitCode(line.unit ?? DEFAULT_UNIT);
375
- const vatCategory = line.vatCategory ?? "S";
376
- const lineAllowancesXml = (line.allowances ?? []).map((a) => buildLineAllowanceChargeXml(a.reason, a.amount, false, currency)).join("");
377
- const lineChargesXml = (line.charges ?? []).map((c) => buildLineAllowanceChargeXml(c.reason, c.amount, true, currency)).join("");
378
- return `
379
- <cac:${lineTag}>
380
- <cbc:ID>${index + 1}</cbc:ID>
381
- ${line.accountingCost ? `<cbc:AccountingCost>${escapeXml(line.accountingCost)}</cbc:AccountingCost>` : ""}
382
- <cbc:${qtyTag} unitCode="${escapeXml(unit)}">${Number(line.quantity.toFixed(6))}</cbc:${qtyTag}>
383
- <cbc:LineExtensionAmount currencyID="${escapeXml(currency)}">${formatAmount(lineTotal)}</cbc:LineExtensionAmount>
384
- ${lineAllowancesXml}${lineChargesXml}
385
- <cac:Item>
386
- <cbc:Name>${escapeXml(line.description)}</cbc:Name>
387
- ${line.itemId ? `<cac:SellersItemIdentification>
388
- <cbc:ID>${escapeXml(line.itemId)}</cbc:ID>
389
- </cac:SellersItemIdentification>` : ""}
390
- <cac:ClassifiedTaxCategory>
391
- <cbc:ID>${vatCategory}</cbc:ID>
392
- <cbc:Percent>${line.vatRate}</cbc:Percent>
393
- <cac:TaxScheme>
394
- <cbc:ID>VAT</cbc:ID>
395
- </cac:TaxScheme>
396
- </cac:ClassifiedTaxCategory>
397
- ${line.standardItemId ? `<cac:StandardItemIdentification>
398
- <cbc:ID schemeID="${escapeXml(line.standardItemScheme ?? "0160")}">${escapeXml(line.standardItemId)}</cbc:ID>
399
- </cac:StandardItemIdentification>` : ""}
400
- ${line.commodityCode && line.commodityScheme ? `<cac:CommodityClassification>
401
- <cbc:ItemClassificationCode listID="${escapeXml(line.commodityScheme)}">${escapeXml(line.commodityCode)}</cbc:ItemClassificationCode>
402
- </cac:CommodityClassification>` : ""}
403
- ${(line.properties ?? []).map((p) => `<cac:AdditionalItemProperty>
404
- <cbc:Name>${escapeXml(p.name)}</cbc:Name>
405
- <cbc:Value>${escapeXml(p.value)}</cbc:Value>
406
- </cac:AdditionalItemProperty>`).join("\n ")}
407
- </cac:Item>
408
- <cac:Price>
409
- <cbc:PriceAmount currencyID="${escapeXml(currency)}">${formatAmount(line.unitPrice)}</cbc:PriceAmount>
410
- ${line.baseQuantity != null ? `<cbc:BaseQuantity unitCode="${escapeXml(resolveUnitCode(line.baseQuantityUnit ?? line.unit ?? DEFAULT_UNIT))}">${line.baseQuantity}</cbc:BaseQuantity>` : ""}
411
- </cac:Price>
412
- </cac:${lineTag}>`;
413
- }
414
- function buildInvoiceLineXml(line, index, currency) {
415
- return buildDocumentLineXml(line, index, currency, "InvoiceLine", "InvoicedQuantity");
416
- }
417
- function calculateTaxSubtotals(lines, allowances, charges) {
418
- const groups = /* @__PURE__ */ new Map();
419
- function addToGroup(vatCategory, vatRate, amount) {
420
- const key = `${vatCategory}-${vatRate}`;
421
- const existing = groups.get(key);
422
- if (existing) {
423
- existing.taxableAmount = round2(existing.taxableAmount + amount);
424
- } else {
425
- groups.set(key, {
426
- vatRate,
427
- vatCategory,
428
- taxableAmount: amount,
429
- taxAmount: 0
430
- // computed once per group below (BR-CO-17)
431
- });
432
- }
433
- }
434
- for (const line of lines) {
435
- addToGroup(line.vatCategory ?? "S", line.vatRate, calculateLineExtensionAmount(line));
436
- }
437
- for (const a of allowances ?? []) {
438
- addToGroup(a.vatCategory ?? "S", a.vatRate, -a.amount);
439
- }
440
- for (const c of charges ?? []) {
441
- addToGroup(c.vatCategory ?? "S", c.vatRate, c.amount);
442
- }
443
- return Array.from(groups.values()).map((subtotal) => ({
444
- ...subtotal,
445
- taxAmount: round2(subtotal.taxableAmount * (subtotal.vatRate / 100))
446
- }));
447
- }
448
- function calculateDocumentTotals(lines, allowances, charges) {
449
- const taxSubtotals = calculateTaxSubtotals(lines, allowances, charges);
450
- const lineExtensionAmount = lines.reduce((sum, line) => sum + calculateLineExtensionAmount(line), 0);
451
- const allowanceTotalAmount = (allowances ?? []).reduce((sum, a) => sum + a.amount, 0);
452
- const chargeTotalAmount = (charges ?? []).reduce((sum, c) => sum + c.amount, 0);
453
- const taxExclusiveAmount = round2(lineExtensionAmount - allowanceTotalAmount + chargeTotalAmount);
454
- const totalTax = round2(taxSubtotals.reduce((sum, st) => sum + st.taxAmount, 0));
455
- const taxInclusiveAmount = round2(taxExclusiveAmount + totalTax);
456
- return {
457
- lineExtensionAmount,
458
- allowanceTotalAmount,
459
- chargeTotalAmount,
460
- taxExclusiveAmount,
461
- totalTax,
462
- taxInclusiveAmount,
463
- payableAmount: taxInclusiveAmount,
464
- taxSubtotals
465
- };
466
- }
467
- function buildTaxTotalXml(taxSubtotals, totalTax, currency) {
468
- const subtotalsXml = taxSubtotals.map((st) => `
469
- <cac:TaxSubtotal>
470
- <cbc:TaxableAmount currencyID="${escapeXml(currency)}">${formatAmount(st.taxableAmount)}</cbc:TaxableAmount>
471
- <cbc:TaxAmount currencyID="${escapeXml(currency)}">${formatAmount(st.taxAmount)}</cbc:TaxAmount>
472
- <cac:TaxCategory>
473
- <cbc:ID>${st.vatCategory}</cbc:ID>
474
- <cbc:Percent>${st.vatRate}</cbc:Percent>
475
- <cac:TaxScheme>
476
- <cbc:ID>VAT</cbc:ID>
477
- </cac:TaxScheme>
478
- </cac:TaxCategory>
479
- </cac:TaxSubtotal>`).join("");
480
- return `<cac:TaxTotal>
481
- <cbc:TaxAmount currencyID="${escapeXml(currency)}">${formatAmount(totalTax)}</cbc:TaxAmount>
482
- ${subtotalsXml}
483
- </cac:TaxTotal>`;
484
- }
485
- function buildTaxCurrencyTotalXml(totalTax, taxCurrency, rate) {
486
- const convertedAmount = Math.round(totalTax * rate * 100) / 100;
487
- return `<cac:TaxTotal>
488
- <cbc:TaxAmount currencyID="${escapeXml(taxCurrency)}">${formatAmount(convertedAmount)}</cbc:TaxAmount>
489
- </cac:TaxTotal>`;
490
- }
491
- function payableFromTaxInclusive(taxInclusiveAmount, prepaidAmount, roundingAmount) {
492
- return Number((taxInclusiveAmount - (prepaidAmount ?? 0) + (roundingAmount ?? 0)).toFixed(2));
493
- }
494
- function buildLegalMonetaryTotalXml(totals, currency, options) {
495
- const prepaid = options?.prepaidAmount;
496
- const rounding = options?.roundingAmount;
497
- const payableAmount = payableFromTaxInclusive(totals.taxInclusiveAmount, prepaid, rounding);
498
- return `<cac:LegalMonetaryTotal>
499
- <cbc:LineExtensionAmount currencyID="${escapeXml(currency)}">${formatAmount(totals.lineExtensionAmount)}</cbc:LineExtensionAmount>
500
- <cbc:TaxExclusiveAmount currencyID="${escapeXml(currency)}">${formatAmount(totals.taxExclusiveAmount)}</cbc:TaxExclusiveAmount>
501
- <cbc:TaxInclusiveAmount currencyID="${escapeXml(currency)}">${formatAmount(totals.taxInclusiveAmount)}</cbc:TaxInclusiveAmount>
502
- ${totals.allowanceTotalAmount > 0 ? `<cbc:AllowanceTotalAmount currencyID="${escapeXml(currency)}">${formatAmount(totals.allowanceTotalAmount)}</cbc:AllowanceTotalAmount>` : ""}
503
- ${totals.chargeTotalAmount > 0 ? `<cbc:ChargeTotalAmount currencyID="${escapeXml(currency)}">${formatAmount(totals.chargeTotalAmount)}</cbc:ChargeTotalAmount>` : ""}
504
- ${prepaid != null ? `<cbc:PrepaidAmount currencyID="${escapeXml(currency)}">${formatAmount(prepaid)}</cbc:PrepaidAmount>` : ""}
505
- ${rounding != null ? `<cbc:PayableRoundingAmount currencyID="${escapeXml(currency)}">${formatAmount(rounding)}</cbc:PayableRoundingAmount>` : ""}
506
- <cbc:PayableAmount currencyID="${escapeXml(currency)}">${formatAmount(payableAmount)}</cbc:PayableAmount>
507
- </cac:LegalMonetaryTotal>`;
508
- }
509
- function buildPaymentMeansXml(input) {
510
- const paymentMeans = input.paymentMeans ?? DEFAULT_PAYMENT_MEANS;
511
- return `<cac:PaymentMeans>
512
- <cbc:PaymentMeansCode>${paymentMeans}</cbc:PaymentMeansCode>
513
- ${input.paymentReference ? `<cbc:PaymentID>${escapeXml(input.paymentReference)}</cbc:PaymentID>` : ""}
514
- ${input.paymentIban ? `<cac:PayeeFinancialAccount>
515
- <cbc:ID>${escapeXml(input.paymentIban)}</cbc:ID>
516
- ${input.paymentBic ? `<cac:FinancialInstitutionBranch>
517
- <cbc:ID>${escapeXml(input.paymentBic)}</cbc:ID>
518
- </cac:FinancialInstitutionBranch>` : ""}
519
- </cac:PayeeFinancialAccount>` : ""}
520
- </cac:PaymentMeans>`;
521
- }
522
- function buildCreditNoteLineXml(line, index, currency) {
523
- return buildDocumentLineXml(line, index, currency, "CreditNoteLine", "CreditedQuantity");
524
- }
525
- function buildOrderReferenceXml(orderReference, salesOrderReference) {
526
- if (!orderReference && !salesOrderReference)
527
- return "";
528
- const parts = ["<cac:OrderReference>"];
529
- if (orderReference) {
530
- parts.push(`<cbc:ID>${escapeXml(orderReference)}</cbc:ID>`);
531
- }
532
- if (salesOrderReference) {
533
- parts.push(`<cbc:SalesOrderID>${escapeXml(salesOrderReference)}</cbc:SalesOrderID>`);
534
- }
535
- parts.push("</cac:OrderReference>");
536
- return parts.join("");
537
- }
538
- function buildInvoiceXml(input) {
539
- const currency = input.currency ?? "EUR";
540
- const date = formatDate(input.date);
541
- const dueDate = input.dueDate ? formatDate(input.dueDate) : void 0;
542
- const hasTaxCurrency = input.taxCurrency && input.taxCurrency !== currency;
543
- const totals = calculateDocumentTotals(input.lines, input.allowances, input.charges);
544
- const linesXml = input.lines.map((line, i) => buildInvoiceLineXml(line, i, currency)).join("");
545
- return `<?xml version="1.0" encoding="UTF-8"?>
546
- <Invoice xmlns="${UBL_NS}"
547
- xmlns:cac="${CAC_NS}"
548
- xmlns:cbc="${CBC_NS}">
549
- <cbc:CustomizationID>${PEPPOL_CUSTOMIZATION_ID}</cbc:CustomizationID>
550
- <cbc:ProfileID>${PEPPOL_PROFILE_ID}</cbc:ProfileID>
551
- <cbc:ID>${escapeXml(input.number)}</cbc:ID>
552
- <cbc:IssueDate>${date}</cbc:IssueDate>
553
- ${dueDate ? `<cbc:DueDate>${dueDate}</cbc:DueDate>` : ""}
554
- ${input.taxPointDate ? `<cbc:TaxPointDate>${formatDate(input.taxPointDate)}</cbc:TaxPointDate>` : ""}
555
- <cbc:InvoiceTypeCode>${input.invoiceTypeCode ?? (input.isCreditNote ? 381 : 380)}</cbc:InvoiceTypeCode>
556
- ${input.note ? `<cbc:Note>${escapeXml(input.note)}</cbc:Note>` : ""}
557
- ${input.accountingCost ? `<cbc:AccountingCost>${escapeXml(input.accountingCost)}</cbc:AccountingCost>` : ""}
558
- <cbc:DocumentCurrencyCode>${escapeXml(currency)}</cbc:DocumentCurrencyCode>
559
- ${hasTaxCurrency ? `<cbc:TaxCurrencyCode>${escapeXml(input.taxCurrency)}</cbc:TaxCurrencyCode>` : ""}
560
- ${input.buyerReference ? `<cbc:BuyerReference>${escapeXml(input.buyerReference)}</cbc:BuyerReference>` : ""}
561
- ${input.invoicePeriod ? buildInvoicePeriodXml(input.invoicePeriod) : ""}
562
- ${buildOrderReferenceXml(input.orderReference, input.salesOrderReference)}
563
- ${input.despatchReference ? `<cac:DespatchDocumentReference><cbc:ID>${escapeXml(input.despatchReference)}</cbc:ID></cac:DespatchDocumentReference>` : ""}
564
- ${input.receiptReference ? `<cac:ReceiptDocumentReference><cbc:ID>${escapeXml(input.receiptReference)}</cbc:ID></cac:ReceiptDocumentReference>` : ""}
565
- ${input.contractReference ? `<cac:ContractDocumentReference><cbc:ID>${escapeXml(input.contractReference)}</cbc:ID></cac:ContractDocumentReference>` : ""}
566
- ${(input.attachments ?? []).map((a) => buildAttachmentXml(a)).join("\n ")}
567
- ${input.projectReference ? `<cac:ProjectReference><cbc:ID>${escapeXml(input.projectReference)}</cbc:ID></cac:ProjectReference>` : ""}
568
- ${input.from ? buildPartyXml(input.from, "AccountingSupplierParty") : ""}
569
- ${buildPartyXml(input.to, "AccountingCustomerParty")}
570
- ${input.payeeParty ? buildPayeePartyXml(input.payeeParty) : ""}
571
- ${input.taxRepresentative ? buildTaxRepresentativePartyXml(input.taxRepresentative) : ""}
572
- ${input.delivery ? buildDeliveryXml(input.delivery) : ""}
573
- ${buildPaymentMeansXml(input)}
574
- ${input.paymentTerms ? `<cac:PaymentTerms>
575
- <cbc:Note>${escapeXml(input.paymentTerms)}</cbc:Note>
576
- </cac:PaymentTerms>` : ""}
577
- ${(input.allowances ?? []).map((a) => buildDocumentAllowanceChargeXml(a, false, currency)).join("")}
578
- ${(input.charges ?? []).map((c) => buildDocumentAllowanceChargeXml(c, true, currency)).join("")}
579
- ${hasTaxCurrency && input.taxCurrencyRate ? buildTaxCurrencyTotalXml(totals.totalTax, input.taxCurrency, input.taxCurrencyRate) : ""}
580
- ${buildTaxTotalXml(totals.taxSubtotals, totals.totalTax, currency)}
581
- ${buildLegalMonetaryTotalXml(totals, currency, { prepaidAmount: input.prepaidAmount, roundingAmount: input.roundingAmount })}
582
- ${linesXml}
583
- </Invoice>`;
584
- }
585
- function buildCreditNoteXml(input) {
586
- const currency = input.currency ?? "EUR";
587
- const date = formatDate(input.date);
588
- const dueDate = input.dueDate ? formatDate(input.dueDate) : void 0;
589
- const hasTaxCurrency = input.taxCurrency && input.taxCurrency !== currency;
590
- const totals = calculateDocumentTotals(input.lines, input.allowances, input.charges);
591
- const linesXml = input.lines.map((line, i) => buildCreditNoteLineXml(line, i, currency)).join("");
592
- return `<?xml version="1.0" encoding="UTF-8"?>
593
- <CreditNote xmlns="${CREDIT_NOTE_NS}"
594
- xmlns:cac="${CAC_NS}"
595
- xmlns:cbc="${CBC_NS}">
596
- <cbc:CustomizationID>${PEPPOL_CUSTOMIZATION_ID}</cbc:CustomizationID>
597
- <cbc:ProfileID>${PEPPOL_PROFILE_ID}</cbc:ProfileID>
598
- <cbc:ID>${escapeXml(input.number)}</cbc:ID>
599
- <cbc:IssueDate>${date}</cbc:IssueDate>
600
- ${dueDate ? `<cbc:DueDate>${dueDate}</cbc:DueDate>` : ""}
601
- ${input.taxPointDate ? `<cbc:TaxPointDate>${formatDate(input.taxPointDate)}</cbc:TaxPointDate>` : ""}
602
- <cbc:CreditNoteTypeCode>${input.invoiceTypeCode ?? 381}</cbc:CreditNoteTypeCode>
603
- ${input.note ? `<cbc:Note>${escapeXml(input.note)}</cbc:Note>` : ""}
604
- ${input.accountingCost ? `<cbc:AccountingCost>${escapeXml(input.accountingCost)}</cbc:AccountingCost>` : ""}
605
- <cbc:DocumentCurrencyCode>${escapeXml(currency)}</cbc:DocumentCurrencyCode>
606
- ${hasTaxCurrency ? `<cbc:TaxCurrencyCode>${escapeXml(input.taxCurrency)}</cbc:TaxCurrencyCode>` : ""}
607
- ${input.buyerReference ? `<cbc:BuyerReference>${escapeXml(input.buyerReference)}</cbc:BuyerReference>` : ""}
608
- ${input.invoicePeriod ? buildInvoicePeriodXml(input.invoicePeriod) : ""}
609
- ${buildOrderReferenceXml(input.orderReference, input.salesOrderReference)}
610
- <cac:BillingReference><cac:InvoiceDocumentReference><cbc:ID>${escapeXml(input.invoiceReference)}</cbc:ID></cac:InvoiceDocumentReference></cac:BillingReference>
611
- ${input.despatchReference ? `<cac:DespatchDocumentReference><cbc:ID>${escapeXml(input.despatchReference)}</cbc:ID></cac:DespatchDocumentReference>` : ""}
612
- ${input.receiptReference ? `<cac:ReceiptDocumentReference><cbc:ID>${escapeXml(input.receiptReference)}</cbc:ID></cac:ReceiptDocumentReference>` : ""}
613
- ${input.contractReference ? `<cac:ContractDocumentReference><cbc:ID>${escapeXml(input.contractReference)}</cbc:ID></cac:ContractDocumentReference>` : ""}
614
- ${(input.attachments ?? []).map((a) => buildAttachmentXml(a)).join("\n ")}
615
- ${input.projectReference ? `<cac:ProjectReference><cbc:ID>${escapeXml(input.projectReference)}</cbc:ID></cac:ProjectReference>` : ""}
616
- ${input.from ? buildPartyXml(input.from, "AccountingSupplierParty") : ""}
617
- ${buildPartyXml(input.to, "AccountingCustomerParty")}
618
- ${input.payeeParty ? buildPayeePartyXml(input.payeeParty) : ""}
619
- ${input.taxRepresentative ? buildTaxRepresentativePartyXml(input.taxRepresentative) : ""}
620
- ${input.delivery ? buildDeliveryXml(input.delivery) : ""}
621
- ${buildPaymentMeansXml(input)}
622
- ${input.paymentTerms ? `<cac:PaymentTerms>
623
- <cbc:Note>${escapeXml(input.paymentTerms)}</cbc:Note>
624
- </cac:PaymentTerms>` : ""}
625
- ${(input.allowances ?? []).map((a) => buildDocumentAllowanceChargeXml(a, false, currency)).join("")}
626
- ${(input.charges ?? []).map((c) => buildDocumentAllowanceChargeXml(c, true, currency)).join("")}
627
- ${hasTaxCurrency && input.taxCurrencyRate ? buildTaxCurrencyTotalXml(totals.totalTax, input.taxCurrency, input.taxCurrencyRate) : ""}
628
- ${buildTaxTotalXml(totals.taxSubtotals, totals.totalTax, currency)}
629
- ${buildLegalMonetaryTotalXml(totals, currency, { prepaidAmount: input.prepaidAmount, roundingAmount: input.roundingAmount })}
630
- ${linesXml}
631
- </CreditNote>`;
632
- }
633
-
634
- // ../sdk/dist/core/checksums/luhn.js
635
- function isValidLuhn(input) {
636
- if (typeof input !== "string")
637
- return false;
638
- if (!/^\d+$/.test(input))
639
- return false;
640
- let sum = 0;
641
- let alt = false;
642
- for (let i = input.length - 1; i >= 0; i--) {
643
- let n = input.charCodeAt(i) - 48;
644
- if (alt) {
645
- n *= 2;
646
- if (n > 9)
647
- n -= 9;
648
- }
649
- sum += n;
650
- alt = !alt;
651
- }
652
- return sum % 10 === 0;
653
- }
654
- var LA_POSTE_SIREN = "356000000";
655
- function isValidLuhnSiret(input) {
656
- if (typeof input !== "string")
657
- return false;
658
- if (!/^\d{14}$/.test(input))
659
- return false;
660
- const siren = input.slice(0, 9);
661
- if (!isValidLuhn(siren))
662
- return false;
663
- if (isValidLuhn(input))
664
- return true;
665
- if (siren === LA_POSTE_SIREN) {
666
- let sum = 0;
667
- for (let i = 0; i < input.length; i++) {
668
- sum += input.charCodeAt(i) - 48;
669
- }
670
- return sum % 5 === 0;
671
- }
672
- return false;
673
- }
674
-
675
- // ../sdk/dist/core/country-rules.js
676
- function warn(field, message, ruleId) {
677
- return { field, message, ruleId };
678
- }
679
- var BE_STRUCTURED_RE = /^\+{3}\d{3}\/\d{4}\/\d{5}\+{3}$/;
680
- function validateBelgianCheckDigit(reference) {
681
- const digits = reference.replace(/[^0-9]/g, "");
682
- if (digits.length !== 12)
683
- return false;
684
- const base = parseInt(digits.slice(0, 10), 10);
685
- const check = parseInt(digits.slice(10, 12), 10);
686
- const expected = base % 97 === 0 ? 97 : base % 97;
687
- return check === expected;
688
- }
689
- function validateBelgium(input, _errors, warnings) {
690
- const ref = input.paymentReference;
691
- if (ref && BE_STRUCTURED_RE.test(ref)) {
692
- if (!validateBelgianCheckDigit(ref)) {
693
- warnings.push(warn("paymentReference", `Belgian structured communication "${ref}" has an invalid mod-97 checksum. Verify the reference.`, "BE-01"));
694
- }
695
- } else if (!ref) {
696
- warnings.push(warn("paymentReference", "Belgian recipients typically expect a structured communication reference (+++NNN/NNNN/NNNNN+++ format).", "BE-02"));
697
- }
698
- }
699
- function validateBelgiumSeller(input, _errors, warnings) {
700
- const ref = input.paymentReference;
701
- if (ref && BE_STRUCTURED_RE.test(ref)) {
702
- if (!validateBelgianCheckDigit(ref)) {
703
- warnings.push(warn("paymentReference", `Belgian structured communication "${ref}" has an invalid mod-97 checksum. Verify the reference.`, "BE-01"));
704
- }
705
- } else if (!ref) {
706
- warnings.push(warn("paymentReference", "Belgian sellers typically include a structured communication reference (+++NNN/NNNN/NNNNN+++ format).", "BE-02"));
707
- }
708
- }
709
- var FR_SIREN_RE = /^\d{9}$/;
710
- var FR_SIRET_RE = /^\d{14}$/;
711
- var FR_VAT_RE = /^FR[0-9A-HJ-NP-Z]{2}\d{9}$/;
712
- var FR_VAT_NUMERIC_KEY_RE = /^FR(\d{2})(\d{9})$/;
713
- var FR_SIREN_BASED_SCHEMES = ["0002", "0009", "0225"];
714
- function frVatKey(siren) {
715
- return (12 + 3 * (Number(siren) % 97)) % 97;
716
- }
717
- function isValidSirenOrSiret(id) {
718
- if (FR_SIREN_RE.test(id))
719
- return isValidLuhn(id);
720
- if (FR_SIRET_RE.test(id))
721
- return isValidLuhnSiret(id);
722
- return false;
723
- }
724
- function validateFrance(input, _errors, warnings) {
725
- const { companyId, companyIdScheme, vatNumber } = input.to ?? {};
726
- const companyIdIsSiren = !companyIdScheme || FR_SIREN_BASED_SCHEMES.includes(companyIdScheme);
727
- if (companyId != null && companyId !== "" && companyIdIsSiren) {
728
- if (typeof companyId !== "string") {
729
- warnings.push(warn("to.companyId", "French company ID should be a string of 9 (SIREN) or 14 (SIRET) digits.", "FR-01"));
730
- } else if (!FR_SIREN_RE.test(companyId) && !FR_SIRET_RE.test(companyId)) {
731
- warnings.push(warn("to.companyId", `French company ID should be a 9-digit SIREN or 14-digit SIRET, got "${companyId}".`, "FR-01"));
732
- } else if (!isValidSirenOrSiret(companyId)) {
733
- warnings.push(warn("to.companyId", `French company ID "${companyId}" has an invalid checksum. Verify the SIREN/SIRET.`, "FR-01"));
734
- }
735
- }
736
- if (vatNumber != null && vatNumber !== "") {
737
- if (typeof vatNumber !== "string") {
738
- warnings.push(warn("to.vatNumber", "French VAT number should be a string matching FR + 2 characters + 9 digits (SIREN).", "FR-02"));
739
- } else if (!FR_VAT_RE.test(vatNumber)) {
740
- warnings.push(warn("to.vatNumber", `French VAT number should match format FR + 2 characters + 9 digits (SIREN), got "${vatNumber}".`, "FR-02"));
741
- } else {
742
- const numericKey = FR_VAT_NUMERIC_KEY_RE.exec(vatNumber);
743
- if (numericKey) {
744
- const [, key, siren] = numericKey;
745
- if (Number(key) !== frVatKey(siren)) {
746
- warnings.push(warn("to.vatNumber", `French VAT number "${vatNumber}" has an invalid verification key \u2014 expected FR${String(frVatKey(siren)).padStart(2, "0")}${siren}. Likely a typo.`, "FR-03"));
747
- }
748
- }
749
- }
750
- }
751
- }
752
- function validateItaly(input, _errors, warnings) {
753
- if (!input.buyerReference) {
754
- warnings.push(warn("buyerReference", "Italian recipients (SDI) typically require a buyer reference (CIG/CUP code). Consider setting buyerReference.", "IT-01"));
755
- }
756
- const peppolId = input.to?.peppolId;
757
- if (peppolId?.startsWith("0201:")) {
758
- const fiscalCode = peppolId.slice(5);
759
- if (fiscalCode.length !== 11 && fiscalCode.length !== 16) {
760
- warnings.push(warn("to.peppolId", `Italian fiscal code (after 0201:) should be 11 digits (partita IVA) or 16 characters (codice fiscale), got ${fiscalCode.length} characters.`, "IT-02"));
761
- }
762
- }
763
- }
764
- var NL_KVK_RE = /^\d{8}$/;
765
- var NL_VAT_RE = /^NL\d{9}B\d{2}$/;
766
- function validateNetherlands(input, _errors, warnings) {
767
- const { companyId, vatNumber } = input.to ?? {};
768
- if (companyId && !NL_KVK_RE.test(companyId)) {
769
- warnings.push(warn("to.companyId", `Dutch KVK number should be exactly 8 digits, got "${companyId}".`, "NL-01"));
770
- }
771
- if (vatNumber && !NL_VAT_RE.test(vatNumber)) {
772
- warnings.push(warn("to.vatNumber", `Dutch VAT number should match format NL + 9 digits + B + 2 digits, got "${vatNumber}".`, "NL-02"));
773
- }
774
- }
775
- var DE_VAT_RE = /^DE\d{9}$/;
776
- function validateGermany(input, _errors, warnings) {
777
- const { vatNumber } = input.to ?? {};
778
- if (vatNumber && !DE_VAT_RE.test(vatNumber)) {
779
- warnings.push(warn("to.vatNumber", `German VAT number should match format DE + 9 digits, got "${vatNumber}".`, "DE-01"));
780
- }
781
- }
782
- function validateCountryRules(input) {
783
- const errors = [];
784
- const warnings = [];
785
- const buyerCountry = input.to?.country;
786
- const sellerCountry = input.from?.country;
787
- if (buyerCountry) {
788
- switch (buyerCountry) {
789
- case "BE":
790
- validateBelgium(input, errors, warnings);
791
- break;
792
- case "FR":
793
- validateFrance(input, errors, warnings);
794
- break;
795
- case "IT":
796
- validateItaly(input, errors, warnings);
797
- break;
798
- case "NL":
799
- validateNetherlands(input, errors, warnings);
800
- break;
801
- case "DE":
802
- validateGermany(input, errors, warnings);
803
- break;
804
- }
805
- }
806
- if (sellerCountry && sellerCountry !== buyerCountry) {
807
- switch (sellerCountry) {
808
- case "BE":
809
- validateBelgiumSeller(input, errors, warnings);
810
- break;
811
- }
812
- }
813
- return { errors, warnings };
814
- }
815
-
816
- // ../sdk/dist/core/code-lists.js
817
- var CURRENCIES = /* @__PURE__ */ new Map([
818
- ["EUR", { code: "EUR", name: "Euro", minorUnits: 2 }],
819
- ["USD", { code: "USD", name: "US Dollar", minorUnits: 2 }],
820
- ["GBP", { code: "GBP", name: "Pound Sterling", minorUnits: 2 }],
821
- ["CHF", { code: "CHF", name: "Swiss Franc", minorUnits: 2 }],
822
- ["DKK", { code: "DKK", name: "Danish Krone", minorUnits: 2 }],
823
- ["NOK", { code: "NOK", name: "Norwegian Krone", minorUnits: 2 }],
824
- ["SEK", { code: "SEK", name: "Swedish Krona", minorUnits: 2 }],
825
- ["PLN", { code: "PLN", name: "Polish Zloty", minorUnits: 2 }],
826
- ["CZK", { code: "CZK", name: "Czech Koruna", minorUnits: 2 }],
827
- ["HUF", { code: "HUF", name: "Hungarian Forint", minorUnits: 2 }],
828
- ["RON", { code: "RON", name: "Romanian Leu", minorUnits: 2 }],
829
- ["BGN", { code: "BGN", name: "Bulgarian Lev", minorUnits: 2 }],
830
- ["HRK", { code: "HRK", name: "Croatian Kuna", minorUnits: 2 }],
831
- ["ISK", { code: "ISK", name: "Icelandic Krona", minorUnits: 0 }],
832
- ["TRY", { code: "TRY", name: "Turkish Lira", minorUnits: 2 }],
833
- ["JPY", { code: "JPY", name: "Japanese Yen", minorUnits: 0 }],
834
- ["CNY", { code: "CNY", name: "Chinese Yuan", minorUnits: 2 }],
835
- ["KRW", { code: "KRW", name: "South Korean Won", minorUnits: 0 }],
836
- ["INR", { code: "INR", name: "Indian Rupee", minorUnits: 2 }],
837
- ["SGD", { code: "SGD", name: "Singapore Dollar", minorUnits: 2 }],
838
- ["AUD", { code: "AUD", name: "Australian Dollar", minorUnits: 2 }],
839
- ["NZD", { code: "NZD", name: "New Zealand Dollar", minorUnits: 2 }],
840
- ["CAD", { code: "CAD", name: "Canadian Dollar", minorUnits: 2 }],
841
- ["BRL", { code: "BRL", name: "Brazilian Real", minorUnits: 2 }],
842
- ["MXN", { code: "MXN", name: "Mexican Peso", minorUnits: 2 }],
843
- ["ZAR", { code: "ZAR", name: "South African Rand", minorUnits: 2 }],
844
- ["AED", { code: "AED", name: "UAE Dirham", minorUnits: 2 }],
845
- ["SAR", { code: "SAR", name: "Saudi Riyal", minorUnits: 2 }],
846
- ["ILS", { code: "ILS", name: "Israeli Shekel", minorUnits: 2 }],
847
- ["HKD", { code: "HKD", name: "Hong Kong Dollar", minorUnits: 2 }],
848
- ["TWD", { code: "TWD", name: "Taiwan Dollar", minorUnits: 2 }]
849
- ]);
850
- function getCurrency(code) {
851
- return CURRENCIES.get(code.toUpperCase());
852
- }
853
- var EAS_SCHEMES = [
854
- { code: "0002", name: "System Information et Repertoire des Entreprises et des Etablissements (SIRENE)", country: "FR" },
855
- { code: "0007", name: "Organisationsnummer", country: "SE" },
856
- { code: "0009", name: "SIRET-CODE", country: "FR" },
857
- { code: "0088", name: "EAN Location Code (GLN)" },
858
- { code: "0096", name: "Danish Chamber of Commerce (P-nummer)", country: "DK" },
859
- { code: "0184", name: "Danish Central Business Register (CVR)", country: "DK" },
860
- { code: "0190", name: "Dutch Chamber of Commerce (KVK)", country: "NL" },
861
- { code: "0191", name: "Organisatie Identificatie Nummer (OIN)", country: "NL" },
862
- { code: "0192", name: "Danish SE-number (Erhvervsstyrelsen)", country: "DK" },
863
- { code: "0195", name: "Singapore Unique Entity Number (UEN)", country: "SG" },
864
- { code: "0196", name: "Icelandic Kennitala", country: "IS" },
865
- { code: "0198", name: "Danish ERST id (Erhvervsstyrelsen)", country: "DK" },
866
- { code: "0200", name: "Lithuanian Legal Entity Register (GRIS)", country: "LT" },
867
- { code: "0201", name: "Italian Codice Destinatario", country: "IT" },
868
- { code: "0202", name: "Italian Fiscal Code (Codice Fiscale)", country: "IT" },
869
- { code: "0204", name: "German Leitweg-ID", country: "DE" },
870
- { code: "0208", name: "Belgian Enterprise Number (KBO/BCE)", country: "BE" },
871
- { code: "0209", name: "German Creditor Identifier (GS1)", country: "DE" },
872
- { code: "0210", name: "Italian Codice Fiscale (per IPA)", country: "IT" },
873
- { code: "0211", name: "Italian Partita IVA (VAT number)", country: "IT" },
874
- { code: "0212", name: "Finnish OVT code", country: "FI" },
875
- { code: "0213", name: "Finnish OP identifier", country: "FI" },
876
- { code: "0225", name: "FRCTC Electronic Address", country: "FR" },
877
- { code: "9957", name: "French VAT number", country: "FR" }
878
- ];
879
- var EAS_BY_CODE = new Map(EAS_SCHEMES.map((s) => [s.code, s]));
880
- var UNIT_ALIAS_MAP = {
881
- each: "EA",
882
- piece: "EA",
883
- pieces: "EA",
884
- hour: "HUR",
885
- hours: "HUR",
886
- day: "DAY",
887
- days: "DAY",
888
- week: "WEE",
889
- weeks: "WEE",
890
- month: "MON",
891
- months: "MON",
892
- year: "ANN",
893
- years: "ANN",
894
- kilogram: "KGM",
895
- kg: "KGM",
896
- meter: "MTR",
897
- metre: "MTR",
898
- liter: "LTR",
899
- litre: "LTR",
900
- unit: "C62",
901
- units: "C62",
902
- set: "SET",
903
- sets: "SET",
904
- pack: "PK",
905
- packs: "PK",
906
- minute: "MIN",
907
- minutes: "MIN",
908
- second: "SEC",
909
- seconds: "SEC",
910
- tonne: "TNE",
911
- ton: "TNE",
912
- "square metre": "MTK",
913
- "square meter": "MTK",
914
- sqm: "MTK"
915
- };
916
- var UNIT_CODES = /* @__PURE__ */ new Map([
917
- ["EA", "Each"],
918
- ["HUR", "Hour"],
919
- ["DAY", "Day"],
920
- ["WEE", "Week"],
921
- ["MON", "Month"],
922
- ["ANN", "Year"],
923
- ["MIN", "Minute"],
924
- ["SEC", "Second"],
925
- ["KGM", "Kilogram"],
926
- ["MTR", "Metre"],
927
- ["LTR", "Litre"],
928
- ["MTK", "Square metre"],
929
- ["TNE", "Tonne"],
930
- ["C62", "One (unit)"],
931
- ["SET", "Set"],
932
- ["PK", "Pack"]
933
- ]);
934
- function resolveUnit(input) {
935
- return UNIT_ALIAS_MAP[input.toLowerCase()] ?? input;
936
- }
937
- function getAllUnits() {
938
- return Array.from(UNIT_CODES.entries()).map(([code, name]) => ({ code, name })).sort((a, b) => a.code.localeCompare(b.code));
939
- }
940
-
941
- // ../sdk/dist/core/validator.js
942
- function error(field, message, ruleId, suggestion) {
943
- return { field, message, ruleId, suggestion };
944
- }
945
- function warning(field, message, ruleId) {
946
- return { field, message, ruleId };
947
- }
948
- function assertString(value, fieldPath, errors) {
949
- if (typeof value !== "string") {
950
- errors.push(error(fieldPath, `Expected string, received ${value === null ? "null" : typeof value}`, void 0, "Check your payload \u2014 this field must be a text value"));
951
- return false;
952
- }
953
- return true;
954
- }
955
- var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
956
- function validateParty(party, path) {
957
- const errors = [];
958
- if (party.name === void 0 || party.name === null || party.name === "") {
959
- errors.push(error(`${path}.name`, "Business name is required", "BR-06"));
960
- } else if (!assertString(party.name, `${path}.name`, errors)) {
961
- } else if (!party.name.trim()) {
962
- errors.push(error(`${path}.name`, "Business name is required", "BR-06"));
963
- }
964
- if (party.peppolId === void 0 || party.peppolId === null || party.peppolId === "") {
965
- errors.push(error(`${path}.peppolId`, "Peppol participant ID is required", void 0, 'Format: "scheme:id", e.g. "0208:0685660237" for Belgian companies'));
966
- } else if (!assertString(party.peppolId, `${path}.peppolId`, errors)) {
967
- } else if (!party.peppolId.includes(":")) {
968
- errors.push(error(`${path}.peppolId`, `Invalid Peppol ID format: "${party.peppolId}"`, void 0, 'Must be "scheme:id" format. Common schemes: 0208 (Belgium), 0009 (France SIRET), 0204 (Germany Leitweg)'));
969
- }
970
- if (party.country === void 0 || party.country === null || party.country === "") {
971
- errors.push(error(`${path}.country`, "Country code is required", "BR-11"));
972
- } else if (!assertString(party.country, `${path}.country`, errors)) {
973
- } else if (party.country.length !== 2) {
974
- errors.push(error(`${path}.country`, `Invalid country code: "${party.country}"`, void 0, "Must be ISO 3166-1 alpha-2 (e.g., BE, FR, DE, NL)"));
975
- }
976
- return errors;
977
- }
978
- function validateBuyerAddress(party, path) {
979
- const errors = [];
980
- if (party.street === void 0 || party.street === null || party.street === "") {
981
- errors.push(error(`${path}.street`, "Street address is required for the buyer", "BR-50", 'e.g. "123 Business Street"'));
982
- } else if (!assertString(party.street, `${path}.street`, errors)) {
983
- } else if (!party.street.trim()) {
984
- errors.push(error(`${path}.street`, "Street address is required for the buyer", "BR-50", 'e.g. "123 Business Street"'));
985
- }
986
- if (party.city === void 0 || party.city === null || party.city === "") {
987
- errors.push(error(`${path}.city`, "City is required for the buyer", "BR-51", 'e.g. "Brussels"'));
988
- } else if (!assertString(party.city, `${path}.city`, errors)) {
989
- } else if (!party.city.trim()) {
990
- errors.push(error(`${path}.city`, "City is required for the buyer", "BR-51", 'e.g. "Brussels"'));
991
- }
992
- if (party.postalCode === void 0 || party.postalCode === null || party.postalCode === "") {
993
- errors.push(error(`${path}.postalCode`, "Postal code is required for the buyer", "BR-53", 'e.g. "1000"'));
994
- } else if (!assertString(party.postalCode, `${path}.postalCode`, errors)) {
995
- } else if (!party.postalCode.trim()) {
996
- errors.push(error(`${path}.postalCode`, "Postal code is required for the buyer", "BR-53", 'e.g. "1000"'));
997
- }
998
- return errors;
999
- }
1000
- function validateLine(line, index, isCreditNote = false) {
1001
- const errors = [];
1002
- const path = `lines[${index}]`;
1003
- if (line.description === void 0 || line.description === null || line.description === "") {
1004
- errors.push(error(`${path}.description`, "Line item description is required", "BR-25"));
1005
- } else if (!assertString(line.description, `${path}.description`, errors)) {
1006
- } else if (!line.description.trim()) {
1007
- errors.push(error(`${path}.description`, "Line item description is required", "BR-25"));
1008
- }
1009
- if (line.quantity === void 0 || line.quantity === null) {
1010
- errors.push(error(`${path}.quantity`, "Quantity is required", "BR-22"));
1011
- } else if (line.quantity <= 0 && !isCreditNote) {
1012
- errors.push(error(`${path}.quantity`, `Quantity must be positive, got ${line.quantity}`, void 0, "For returns/credits, use a credit note instead"));
1013
- }
1014
- if (line.unitPrice === void 0 || line.unitPrice === null) {
1015
- errors.push(error(`${path}.unitPrice`, "Unit price is required", "BR-26"));
1016
- } else if (line.unitPrice < 0) {
1017
- errors.push(error(`${path}.unitPrice`, `Unit price cannot be negative, got ${line.unitPrice}`, void 0, "For discounts, use a negative quantity or a separate discount line"));
1018
- }
1019
- if (line.vatRate === void 0 || line.vatRate === null) {
1020
- errors.push(error(`${path}.vatRate`, "VAT rate is required", "BR-CO-17"));
1021
- } else if (line.vatRate < 0 || line.vatRate > 100) {
1022
- errors.push(error(`${path}.vatRate`, `VAT rate must be between 0 and 100, got ${line.vatRate}`, void 0, "Use 0 for zero-rated, 21 for standard Belgian VAT, etc."));
1023
- }
1024
- return errors;
1025
- }
1026
- function validateInvoice(input) {
1027
- const errors = [];
1028
- const warnings = [];
1029
- if (input.number === void 0 || input.number === null || input.number === "") {
1030
- errors.push(error("number", "Invoice number is required", "BR-02", "Must be unique per supplier"));
1031
- } else if (!assertString(input.number, "number", errors)) {
1032
- } else if (!input.number.trim()) {
1033
- errors.push(error("number", "Invoice number is required", "BR-02", "Must be unique per supplier"));
1034
- }
1035
- const VALID_TYPE_CODES = [380, 381, 383, 384, 386, 389, 751];
1036
- if (input.invoiceTypeCode != null && !VALID_TYPE_CODES.includes(input.invoiceTypeCode)) {
1037
- errors.push(error("invoiceTypeCode", `Invalid invoice type code: ${input.invoiceTypeCode}`, void 0, "Valid codes: 380, 381, 383, 384, 386, 389, 751"));
1038
- }
1039
- if (input.isCreditNote) {
1040
- const ref = input.invoiceReference;
1041
- if (ref === void 0 || ref === null || ref === "") {
1042
- errors.push(error("invoiceReference", "Reference to the original invoice is required for credit notes", void 0, 'Set invoiceReference to the original invoice number (e.g., "INV-001")'));
1043
- } else if (!assertString(ref, "invoiceReference", errors)) {
1044
- } else if (!ref.trim()) {
1045
- errors.push(error("invoiceReference", "Reference to the original invoice is required for credit notes", void 0, 'Set invoiceReference to the original invoice number (e.g., "INV-001")'));
1046
- }
1047
- }
1048
- if (input.from) {
1049
- warnings.push(warning("from", "Seller info is determined by your API key. The 'from' field is deprecated and ignored."));
1050
- }
1051
- if (!input.to) {
1052
- errors.push(error("to", "Buyer (to) is required", "BR-07"));
1053
- } else {
1054
- errors.push(...validateParty(input.to, "to"));
1055
- errors.push(...validateBuyerAddress(input.to, "to"));
1056
- }
1057
- if (input.payeeParty) {
1058
- const ppName = input.payeeParty.name;
1059
- if (ppName === void 0 || ppName === null || ppName === "") {
1060
- errors.push(error("payeeParty.name", "Payee party name is required", "BR-17"));
1061
- } else if (!assertString(ppName, "payeeParty.name", errors)) {
1062
- } else if (!ppName.trim()) {
1063
- errors.push(error("payeeParty.name", "Payee party name is required", "BR-17"));
1064
- }
1065
- const ppId = input.payeeParty.peppolId;
1066
- if (ppId === void 0 || ppId === null || ppId === "") {
1067
- errors.push(error("payeeParty.peppolId", "Payee party Peppol ID is required", void 0, 'Format: "scheme:id", e.g. "0208:0685660237"'));
1068
- } else if (!assertString(ppId, "payeeParty.peppolId", errors)) {
1069
- } else if (!ppId.includes(":")) {
1070
- errors.push(error("payeeParty.peppolId", `Invalid Peppol ID format: "${ppId}"`, void 0, 'Must be "scheme:id" format'));
1071
- }
1072
- }
1073
- if (!input.lines || input.lines.length === 0) {
1074
- errors.push(error("lines", "At least one line item is required", "BR-16", "Add items to the lines array"));
1075
- } else {
1076
- for (let i = 0; i < input.lines.length; i++) {
1077
- errors.push(...validateLine(input.lines[i], i, input.isCreditNote));
1078
- }
1079
- }
1080
- if (input.date) {
1081
- if (!ISO_DATE_RE.test(input.date)) {
1082
- errors.push(error("date", `Invalid date format: "${input.date}"`, void 0, "Use ISO 8601: YYYY-MM-DD"));
1083
- }
1084
- }
1085
- if (input.dueDate) {
1086
- if (!ISO_DATE_RE.test(input.dueDate)) {
1087
- errors.push(error("dueDate", `Invalid due date format: "${input.dueDate}"`, void 0, "Use ISO 8601: YYYY-MM-DD"));
1088
- }
1089
- }
1090
- if (input.taxPointDate) {
1091
- if (!ISO_DATE_RE.test(input.taxPointDate)) {
1092
- errors.push(error("taxPointDate", `Invalid tax point date format: "${input.taxPointDate}"`, void 0, "Use ISO 8601: YYYY-MM-DD"));
1093
- }
1094
- }
1095
- if (input.roundingAmount !== void 0 && input.roundingAmount !== null) {
1096
- if (input.roundingAmount < -0.99 || input.roundingAmount > 0.99) {
1097
- errors.push(error("roundingAmount", `Rounding amount must be between -0.99 and 0.99, got ${input.roundingAmount}`, void 0, "Rounding is stored as integer cents (\xB199). Use values like 0.50 or -0.25."));
1098
- }
1099
- }
1100
- if (!input.buyerReference && !input.orderReference) {
1101
- warnings.push(warning("buyerReference", "Either buyerReference or orderReference is required by Peppol BIS 3.0 (BT-10).", "BR-10"));
1102
- }
1103
- if (input.taxCurrency && input.taxCurrency !== (input.currency ?? "EUR") && !input.taxCurrencyRate) {
1104
- errors.push(error("taxCurrencyRate", "Tax currency rate is required when taxCurrency differs from document currency", "BR-53", "Set taxCurrencyRate to the exchange rate from document currency to tax currency"));
1105
- }
1106
- if (input.taxCurrency && input.taxCurrency === (input.currency ?? "EUR")) {
1107
- warnings.push(warning("taxCurrency", "Tax currency is the same as document currency \u2014 TaxCurrencyCode will be omitted"));
1108
- }
1109
- if (input.taxCurrencyRate !== void 0 && input.taxCurrencyRate <= 0) {
1110
- errors.push(error("taxCurrencyRate", `Tax currency rate must be positive, got ${input.taxCurrencyRate}`, void 0, "Set to the exchange rate from document currency to tax currency"));
1111
- }
1112
- if (input.currency && !getCurrency(input.currency)) {
1113
- errors.push(error("currency", `Invalid currency code: "${input.currency}"`, void 0, 'Use ISO 4217 (e.g., "EUR", "USD", "GBP", "JPY"). See https://getpeppr.dev/docs/types/#currency'));
1114
- }
1115
- if (input.taxCurrency && !getCurrency(input.taxCurrency)) {
1116
- errors.push(error("taxCurrency", `Invalid tax currency code: "${input.taxCurrency}"`, void 0, 'Use ISO 4217 (e.g., "EUR", "USD")'));
1117
- }
1118
- if (!input.dueDate) {
1119
- warnings.push(warning("dueDate", "No due date specified. Recommended for payment terms.", "BR-09"));
1120
- }
1121
- if (!input.to?.vatNumber) {
1122
- warnings.push(warning("to.vatNumber", "Buyer VAT number not provided. May be required for B2B."));
1123
- }
1124
- if (input.paymentMeans === 30 && !input.paymentIban) {
1125
- warnings.push(warning("paymentIban", "Payment means is credit transfer but no IBAN provided. Buyer won't know where to pay."));
1126
- }
1127
- if (input.from?.peppolId && input.to?.peppolId && input.from.peppolId === input.to.peppolId) {
1128
- errors.push(error("to.peppolId", "Buyer and seller cannot have the same Peppol ID", void 0, "Check that 'from' and 'to' are different parties"));
1129
- }
1130
- const countryResult = validateCountryRules(input);
1131
- warnings.push(...countryResult.warnings);
1132
- return {
1133
- valid: errors.length === 0,
1134
- errors,
1135
- warnings
1136
- };
1137
- }
1138
-
1139
- // ../sdk/dist/core/status-precedence.js
1140
- var STATUS_PRECEDENCE = [
1141
- { status: "failed", family: "terminal-failure" },
1142
- { status: "rejected", family: "terminal-failure" },
1143
- { status: "paid", family: "terminal-success" },
1144
- { status: "partially_paid", family: "progress" },
1145
- { status: "accepted", family: "progress" },
1146
- { status: "conditionally_accepted", family: "progress" },
1147
- { status: "under_query", family: "progress" },
1148
- { status: "in_process", family: "progress" },
1149
- { status: "cleared", family: "progress" },
1150
- { status: "delivered", family: "progress" },
1151
- { status: "acknowledged", family: "progress" },
1152
- // Terminal for developer wait semantics only — stays rank 40 (non-terminal)
1153
- // in the projection guard (§3.12 two-level terminality).
1154
- { status: "no_action", family: "terminal-failure" },
1155
- { status: "submitted", family: "progress" },
1156
- { status: "unknown", family: "fallback" }
1157
- ];
1158
- function statusFamily(status) {
1159
- return STATUS_PRECEDENCE.find((e) => e.status === status)?.family ?? "fallback";
1160
- }
1161
- var TERMINAL_FAILURE_STATUSES = STATUS_PRECEDENCE.filter((e) => e.family === "terminal-failure").map((e) => e.status);
1162
-
1163
- // ../sdk/dist/version.js
1164
- var SDK_VERSION = "4.1.1";
1165
-
1166
- // ../sdk/dist/core/client.js
1167
- function findHeaderCaseInsensitive(headers, name) {
1168
- if (!headers)
1169
- return void 0;
1170
- const target = name.toLowerCase();
1171
- for (const [key, value] of Object.entries(headers)) {
1172
- if (key.toLowerCase() === target)
1173
- return value;
1174
- }
1175
- return void 0;
1176
- }
1177
- var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
1178
- function sleep(ms) {
1179
- return new Promise((resolve4) => setTimeout(resolve4, ms));
1180
- }
1181
- function calculateRetryDelay(attempt, initialDelayMs, maxDelayMs, retryAfterMs) {
1182
- if (retryAfterMs !== void 0)
1183
- return Math.min(retryAfterMs, maxDelayMs);
1184
- const exponentialDelay = initialDelayMs * Math.pow(2, attempt);
1185
- const jitter = Math.random() * initialDelayMs;
1186
- return Math.min(exponentialDelay + jitter, maxDelayMs);
1187
- }
1188
- function parseRetryAfter(headerValue) {
1189
- if (!headerValue)
1190
- return void 0;
1191
- const seconds = Number(headerValue);
1192
- if (Number.isFinite(seconds) && seconds >= 0) {
1193
- return seconds * 1e3;
1194
- }
1195
- const dateMs = Date.parse(headerValue);
1196
- if (!Number.isNaN(dateMs)) {
1197
- const delayMs = dateMs - Date.now();
1198
- return delayMs > 0 ? delayMs : 0;
1199
- }
1200
- return void 0;
1201
- }
1202
- var CONTROL_CHARACTERS = /[\u0000-\u001F\u007F-\u009F]/g;
1203
- function stripControls(value) {
1204
- return value.replace(CONTROL_CHARACTERS, " ");
1205
- }
1206
- function readOwn(source, key) {
1207
- return Object.hasOwn(source, key) ? source[key] : void 0;
1208
- }
1209
- function readSentence(source, key) {
1210
- const value = readOwn(source, key);
1211
- if (typeof value !== "string")
1212
- return null;
1213
- const cleaned = stripControls(value).trim();
1214
- return cleaned === "" ? null : cleaned;
1215
- }
1216
- function safeDocsUrl(value) {
1217
- if (typeof value !== "string")
1218
- return null;
1219
- let parsed;
1220
- try {
1221
- parsed = new URL(value);
1222
- } catch {
1223
- return null;
1224
- }
1225
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
1226
- return null;
1227
- return parsed.href;
1228
- }
1229
- function formatApiErrorMessage(status, rawBody) {
1230
- const verbatim = `getpeppr API error (${status}): ${stripControls(rawBody)}`;
1231
- let parsed;
1232
- try {
1233
- parsed = JSON.parse(rawBody);
1234
- } catch {
1235
- return verbatim;
1236
- }
1237
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1238
- return verbatim;
1239
- }
1240
- const sentence = readSentence(parsed, "message") ?? readSentence(parsed, "error");
1241
- if (sentence === null)
1242
- return verbatim;
1243
- const link = safeDocsUrl(readOwn(parsed, "docs"));
1244
- return `getpeppr API error (${status}): ${sentence}${link ? ` See ${link}` : ""}`;
1245
- }
1246
- function isRetryableError(error2) {
1247
- if (error2 instanceof PeppolApiError) {
1248
- return RETRYABLE_STATUS_CODES.has(error2.statusCode);
1249
- }
1250
- if (error2 instanceof Error && error2.name === "AbortError") {
1251
- return true;
1252
- }
1253
- if (error2 instanceof TypeError && /fetch failed|network|ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT/i.test(error2.message)) {
1254
- return true;
1255
- }
1256
- return false;
1257
- }
1258
- var DEFAULT_BASE_URL = "https://api.getpeppr.dev/v1";
1259
- var GetpepprAdapter = class {
1260
- name = "getpeppr";
1261
- baseUrl;
1262
- apiKey;
1263
- timeout;
1264
- retryConfig;
1265
- onRequest;
1266
- onResponse;
1267
- constructor(config) {
1268
- this.apiKey = config.apiKey;
1269
- this.timeout = config.timeout ?? 3e4;
1270
- this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
1271
- this.retryConfig = {
1272
- maxRetries: config.retry?.maxRetries ?? 3,
1273
- initialDelayMs: config.retry?.initialDelayMs ?? 500,
1274
- maxDelayMs: config.retry?.maxDelayMs ?? 3e4
1275
- };
1276
- this.onRequest = config.onRequest;
1277
- this.onResponse = config.onResponse;
1278
- }
1279
- async request(method, path, body, extraHeaders) {
1280
- const { maxRetries, initialDelayMs, maxDelayMs } = this.retryConfig;
1281
- let lastError;
1282
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
1283
- try {
1284
- return await this.doRequest(method, path, body, extraHeaders);
1285
- } catch (err) {
1286
- lastError = err;
1287
- const is429 = err instanceof PeppolApiError && err.statusCode === 429;
1288
- const isSafeMethod = /^(GET|DELETE|HEAD)$/i.test(method);
1289
- const hasIdempotencyKey = !!findHeaderCaseInsensitive(extraHeaders, "Idempotency-Key");
1290
- const canRetry = is429 || isSafeMethod || hasIdempotencyKey;
1291
- if (attempt < maxRetries && canRetry && isRetryableError(err)) {
1292
- const retryAfterMs = err instanceof PeppolApiError ? err.retryAfterMs : void 0;
1293
- await sleep(calculateRetryDelay(attempt, initialDelayMs, maxDelayMs, retryAfterMs));
1294
- continue;
1295
- }
1296
- throw err;
1297
- }
1298
- }
1299
- throw lastError;
1300
- }
1301
- async doRequest(method, path, body, extraHeaders) {
1302
- const url = `${this.baseUrl}${path}`;
1303
- const controller = new AbortController();
1304
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1305
- const requestHeaders = {
1306
- Authorization: `Bearer ${this.apiKey}`,
1307
- "Content-Type": "application/json",
1308
- Accept: "application/json",
1309
- "User-Agent": `getpeppr-sdk/${SDK_VERSION}`,
1310
- ...extraHeaders
1311
- };
1312
- const startTime = Date.now();
1313
- if (this.onRequest) {
1314
- try {
1315
- this.onRequest({
1316
- method,
1317
- url,
1318
- headers: { ...requestHeaders },
1319
- body,
1320
- timestamp: startTime
1321
- });
1322
- } catch {
1323
- }
1324
- }
1325
- try {
1326
- const response = await fetch(url, {
1327
- method,
1328
- headers: requestHeaders,
1329
- body: body ? JSON.stringify(body) : void 0,
1330
- signal: controller.signal
1331
- });
1332
- if (!response.ok) {
1333
- const errorBody = await response.text().catch(() => "Unknown error");
1334
- const retryAfterMs = response.status === 429 ? parseRetryAfter(response.headers.get("Retry-After")) : void 0;
1335
- if (this.onResponse) {
1336
- try {
1337
- this.onResponse({
1338
- status: response.status,
1339
- headers: Object.fromEntries(response.headers.entries()),
1340
- body: errorBody,
1341
- durationMs: Date.now() - startTime,
1342
- timestamp: Date.now()
1343
- });
1344
- } catch {
1345
- }
1346
- }
1347
- throw new PeppolApiError(formatApiErrorMessage(response.status, errorBody), response.status, errorBody, retryAfterMs);
1348
- }
1349
- if (response.status === 204) {
1350
- if (this.onResponse) {
1351
- try {
1352
- this.onResponse({
1353
- status: response.status,
1354
- headers: Object.fromEntries(response.headers.entries()),
1355
- body: void 0,
1356
- durationMs: Date.now() - startTime,
1357
- timestamp: Date.now()
1358
- });
1359
- } catch {
1360
- }
1361
- }
1362
- return void 0;
1363
- }
1364
- let responseBody;
1365
- try {
1366
- responseBody = await response.json();
1367
- } catch {
1368
- throw new PeppolApiError(`getpeppr API error: unexpected response format (status ${response.status})`, response.status, "Response body is not valid JSON");
1369
- }
1370
- if (this.onResponse) {
1371
- try {
1372
- this.onResponse({
1373
- status: response.status,
1374
- headers: Object.fromEntries(response.headers.entries()),
1375
- // GPR-1061 — a COPY, not the live object. The hook used to receive
1376
- // the very body the parsers then read: a hook that redacts fields
1377
- // before logging them (an entirely reasonable hook) could delete
1378
- // `status` and make the SDK blame the gateway for the omission.
1379
- //
1380
- // The clone is not guaranteed: structuredClone throws RangeError
1381
- // past roughly 3000 levels of nesting. Falling back to the live
1382
- // object would reopen the mutation above, and letting the throw
1383
- // escape would silently drop the log — the surrounding catch
1384
- // swallows everything — so the hook fires with a marker instead.
1385
- body: cloneForHook(responseBody),
1386
- durationMs: Date.now() - startTime,
1387
- timestamp: Date.now()
1388
- });
1389
- } catch {
1390
- }
1391
- }
1392
- return responseBody;
1393
- } finally {
1394
- clearTimeout(timeoutId);
1395
- }
1396
- }
1397
- // NOTE: sendInvoice and createInvoice hit the same SDK endpoint (POST /invoices).
1398
- // The gateway (Tasks 9-10) differentiates them: sendInvoice wraps with
1399
- // { invoice: {...}, send_after_import: true }, createInvoice omits the flag.
1400
- async sendInvoice(input, options) {
1401
- const headers = {};
1402
- if (options?.idempotencyKey) {
1403
- headers["Idempotency-Key"] = options.idempotencyKey;
1404
- }
1405
- if (options?.validateRecipient) {
1406
- headers["X-Validate-Recipient"] = options.validateRecipient === true ? "warn" : String(options.validateRecipient);
1407
- }
1408
- const result = await this.request("POST", "/invoices", input, headers);
1409
- return parseSendResult(result);
1410
- }
1411
- async createInvoice(input, options) {
1412
- const headers = {};
1413
- if (options?.idempotencyKey) {
1414
- headers["Idempotency-Key"] = options.idempotencyKey;
1415
- }
1416
- if (options?.validateRecipient) {
1417
- headers["X-Validate-Recipient"] = options.validateRecipient === true ? "warn" : String(options.validateRecipient);
1418
- }
1419
- const result = await this.request("POST", "/invoices", { ...input, _draft: true }, headers);
1420
- return parseSendResult(result);
1421
- }
1422
- async sendInvoiceById(id) {
1423
- await this.request("POST", `/invoices/send/${id}`);
1424
- }
1425
- async sendCreditNote(input) {
1426
- const result = await this.request("POST", "/credit-notes", input);
1427
- return parseSendResult(result);
1428
- }
1429
- async validateDocument(input) {
1430
- return this.request("POST", "/validate", input);
1431
- }
1432
- async listInvoices(options) {
1433
- const params = new URLSearchParams();
1434
- if (options?.limit != null)
1435
- params.set("limit", String(options.limit));
1436
- if (options?.offset != null)
1437
- params.set("offset", String(options.offset));
1438
- if (options?.includeLines)
1439
- params.set("include", "lines");
1440
- const query = params.toString() ? `?${params.toString()}` : "";
1441
- const result = await this.request("GET", `/invoices${query}`);
1442
- const envelope = requireRecordBody(result, "the invoice list");
1443
- const rows = envelope.invoices ?? envelope.data ?? [];
1444
- if (!Array.isArray(rows)) {
1445
- throw new PeppolProtocolError("The getpeppr API answered the invoice list without an array of invoices. Please report this response to support@getpeppr.dev.", "invoices", boundedBody(envelope));
1446
- }
1447
- const invoices = rows;
1448
- const meta = envelope.meta;
1449
- return {
1450
- data: invoices.map(parseInvoiceSummary),
1451
- meta: {
1452
- totalCount: Number(meta?.total_count ?? invoices.length),
1453
- offset: Number(meta?.offset ?? options?.offset ?? 0),
1454
- limit: Number(meta?.limit ?? options?.limit ?? 25),
1455
- hasMore: meta ? Number(meta.total_count) > Number(meta.offset) + Number(meta.limit) : false,
1456
- truncated: Boolean(meta?.truncated ?? false)
1457
- }
1458
- };
1459
- }
1460
- async getStatus(documentId, options) {
1461
- const query = options?.includeEvidence ? "?include=evidence" : "";
1462
- const result = await this.request("GET", `/invoices/${documentId}${query}`);
1463
- return parseSendResult(result);
1464
- }
1465
- async lookupDirectory(scheme, id) {
1466
- const result = await this.request("GET", `/directory/${scheme}/${id}`);
1467
- return parseDirectoryEntry(result);
1468
- }
1469
- async searchDirectory(params) {
1470
- const query = new URLSearchParams(params).toString();
1471
- const result = await this.request("GET", `/directory/search?${query}`);
1472
- const data = result.data ?? [];
1473
- const meta = result.meta;
1474
- return {
1475
- data,
1476
- meta: {
1477
- totalCount: Number(meta?.total_count ?? meta?.totalCount ?? data.length),
1478
- offset: Number(meta?.offset ?? params.offset ?? 0),
1479
- limit: Number(meta?.limit ?? params.limit ?? 20),
1480
- hasMore: meta?.has_more != null || meta?.hasMore != null ? Boolean(meta.has_more ?? meta.hasMore) : Number(meta?.total_count ?? meta?.totalCount ?? 0) > Number(meta?.offset ?? 0) + Number(meta?.limit ?? 0)
1481
- }
1482
- };
1483
- }
1484
- async getInvoiceAs(id, format) {
1485
- const { maxRetries, initialDelayMs, maxDelayMs } = this.retryConfig;
1486
- let lastError;
1487
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
1488
- try {
1489
- return await this.doRequestBinary(`/invoices/${id}/as/${format}`);
1490
- } catch (err) {
1491
- lastError = err;
1492
- if (attempt < maxRetries && isRetryableError(err)) {
1493
- const retryAfterMs = err instanceof PeppolApiError ? err.retryAfterMs : void 0;
1494
- await sleep(calculateRetryDelay(attempt, initialDelayMs, maxDelayMs, retryAfterMs));
1495
- continue;
1496
- }
1497
- throw err;
1498
- }
1499
- }
1500
- throw lastError;
1501
- }
1502
- async doRequestBinary(path) {
1503
- const url = `${this.baseUrl}${path}`;
1504
- const controller = new AbortController();
1505
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1506
- const requestHeaders = {
1507
- Authorization: `Bearer ${this.apiKey}`
1508
- };
1509
- const startTime = Date.now();
1510
- if (this.onRequest) {
1511
- try {
1512
- this.onRequest({
1513
- method: "GET",
1514
- url,
1515
- headers: { ...requestHeaders },
1516
- timestamp: startTime
1517
- });
1518
- } catch {
1519
- }
1520
- }
1521
- try {
1522
- const response = await fetch(url, {
1523
- method: "GET",
1524
- headers: requestHeaders,
1525
- signal: controller.signal
1526
- });
1527
- if (!response.ok) {
1528
- const errorBody = await response.text().catch(() => "Unknown error");
1529
- const retryAfterMs = response.status === 429 ? parseRetryAfter(response.headers.get("Retry-After")) : void 0;
1530
- if (this.onResponse) {
1531
- try {
1532
- this.onResponse({
1533
- status: response.status,
1534
- headers: Object.fromEntries(response.headers.entries()),
1535
- body: errorBody,
1536
- durationMs: Date.now() - startTime,
1537
- timestamp: Date.now()
1538
- });
1539
- } catch {
1540
- }
1541
- }
1542
- throw new PeppolApiError(formatApiErrorMessage(response.status, errorBody), response.status, errorBody, retryAfterMs);
1543
- }
1544
- const responseBody = await response.arrayBuffer();
1545
- if (this.onResponse) {
1546
- try {
1547
- this.onResponse({
1548
- status: response.status,
1549
- headers: Object.fromEntries(response.headers.entries()),
1550
- body: `[ArrayBuffer: ${responseBody.byteLength} bytes]`,
1551
- durationMs: Date.now() - startTime,
1552
- timestamp: Date.now()
1553
- });
1554
- } catch {
1555
- }
1556
- }
1557
- return responseBody;
1558
- } finally {
1559
- clearTimeout(timeoutId);
1560
- }
1561
- }
1562
- async validateDocumentServer(input) {
1563
- return this.request("POST", "/validate/server", input);
1564
- }
1565
- async listEvents(options) {
1566
- const params = new URLSearchParams();
1567
- if (options?.limit != null)
1568
- params.set("limit", String(options.limit));
1569
- if (options?.offset != null)
1570
- params.set("offset", String(options.offset));
1571
- if (options?.invoiceId)
1572
- params.set("invoiceId", options.invoiceId);
1573
- if (options?.dateFrom)
1574
- params.set("dateFrom", options.dateFrom);
1575
- if (options?.dateTo)
1576
- params.set("dateTo", options.dateTo);
1577
- const query = params.toString() ? `?${params.toString()}` : "";
1578
- const result = await this.request("GET", `/events${query}`);
1579
- const events = result.data ?? [];
1580
- const meta = result.meta;
1581
- return {
1582
- data: events.map((evt) => ({
1583
- id: String(evt.id ?? ""),
1584
- eventType: String(evt.eventType ?? evt.event_type ?? ""),
1585
- documentId: evt.documentId ?? evt.document_id ?? null,
1586
- metadata: evt.metadata ?? null,
1587
- createdAt: String(evt.createdAt ?? evt.created_at ?? "")
1588
- })),
1589
- meta: {
1590
- totalCount: Number(meta?.total_count ?? meta?.totalCount ?? events.length),
1591
- offset: Number(meta?.offset ?? options?.offset ?? 0),
1592
- limit: Number(meta?.limit ?? options?.limit ?? 25),
1593
- // Prefer the gateway's authoritative has_more; fall back to computing it
1594
- // from total_count/offset/limit so listAll() paginates correctly even if
1595
- // a response omits the flag.
1596
- hasMore: meta?.has_more != null || meta?.hasMore != null ? Boolean(meta.has_more ?? meta.hasMore) : Number(meta?.total_count ?? meta?.totalCount ?? 0) > Number(meta?.offset ?? options?.offset ?? 0) + Number(meta?.limit ?? options?.limit ?? 0),
1597
- truncated: Boolean(meta?.truncated ?? false)
1598
- }
1599
- };
1600
- }
1601
- async acknowledgeInvoice(id) {
1602
- const result = await this.request("POST", `/invoices/${id}/ack`);
1603
- return parseSendResult(result);
1604
- }
1605
- async updateInvoice(id, input) {
1606
- const result = await this.request("PUT", `/invoices/${id}`, input);
1607
- return parseSendResult(result);
1608
- }
1609
- async deleteInvoice(id) {
1610
- const result = await this.request("DELETE", `/invoices/${id}`);
1611
- return parseSendResult(result);
1612
- }
1613
- async markInvoiceAs(id, state, options) {
1614
- const body = { state };
1615
- if (options?.commit)
1616
- body.commit = options.commit;
1617
- if (options?.reason)
1618
- body.reason = options.reason;
1619
- const result = await this.request("POST", `/invoices/${id}/mark-as`, body);
1620
- return parseSendResult(result);
1621
- }
1622
- async listContacts(options) {
1623
- const params = new URLSearchParams();
1624
- if (options?.limit != null)
1625
- params.set("limit", String(options.limit));
1626
- if (options?.offset != null)
1627
- params.set("offset", String(options.offset));
1628
- if (options?.name)
1629
- params.set("name", options.name);
1630
- if (options?.isClient != null)
1631
- params.set("isClient", String(options.isClient));
1632
- if (options?.isProvider != null)
1633
- params.set("isProvider", String(options.isProvider));
1634
- const query = params.toString() ? `?${params.toString()}` : "";
1635
- const result = await this.request("GET", `/contacts${query}`);
1636
- const contacts = result.contacts ?? result.data ?? [];
1637
- const meta = result.meta;
1638
- return {
1639
- data: contacts.map(parseContact),
1640
- meta: {
1641
- totalCount: Number(meta?.total_count ?? meta?.totalCount ?? contacts.length),
1642
- offset: Number(meta?.offset ?? options?.offset ?? 0),
1643
- limit: Number(meta?.limit ?? options?.limit ?? 25),
1644
- hasMore: meta ? Number(meta.total_count ?? meta.totalCount) > Number(meta.offset) + Number(meta.limit) : false,
1645
- truncated: Boolean(meta?.truncated ?? false)
1646
- }
1647
- };
1648
- }
1649
- async getContact(id) {
1650
- const result = await this.request("GET", `/contacts/${id}`);
1651
- return parseContact(result);
1652
- }
1653
- async createContact(input) {
1654
- const result = await this.request("POST", "/contacts", input);
1655
- return parseContact(result);
1656
- }
1657
- async updateContact(id, input) {
1658
- const result = await this.request("PUT", `/contacts/${id}`, input);
1659
- return parseContact(result);
1660
- }
1661
- async deleteContact(id) {
1662
- await this.request("DELETE", `/contacts/${id}`);
1663
- }
1664
- async createLegalEntity(input, options) {
1665
- const headers = {};
1666
- if (options?.idempotencyKey)
1667
- headers["Idempotency-Key"] = options.idempotencyKey;
1668
- const result = await this.request("POST", "/legal-entities", input, headers);
1669
- return parseLegalEntity(result);
1670
- }
1671
- async getLegalEntity(id) {
1672
- const result = await this.request("GET", `/legal-entities/${id}`);
1673
- return parseLegalEntity(result);
1674
- }
1675
- async listLegalEntities(options) {
1676
- const params = new URLSearchParams();
1677
- if (options?.limit != null)
1678
- params.set("limit", String(options.limit));
1679
- if (options?.offset != null)
1680
- params.set("offset", String(options.offset));
1681
- const query = params.toString() ? `?${params.toString()}` : "";
1682
- const result = await this.request("GET", `/legal-entities${query}`);
1683
- const rows = result.data ?? [];
1684
- const pagination = result.pagination;
1685
- return {
1686
- data: rows.map(parseLegalEntity),
1687
- meta: {
1688
- totalCount: Number(pagination?.total_count ?? rows.length),
1689
- offset: Number(pagination?.offset ?? options?.offset ?? 0),
1690
- limit: Number(pagination?.limit ?? options?.limit ?? 50),
1691
- hasMore: Boolean(pagination?.has_more ?? false),
1692
- truncated: false
1693
- }
1694
- };
1695
- }
1696
- async archiveLegalEntity(id) {
1697
- const result = await this.request("DELETE", `/legal-entities/${id}`);
1698
- return {
1699
- id: String(result.id ?? id),
1700
- externalId: result.externalId != null ? String(result.externalId) : null,
1701
- status: "archived"
1702
- };
1703
- }
1704
- async requestLegalEntityAttestation(id, input, options) {
1705
- const headers = {};
1706
- if (options?.idempotencyKey)
1707
- headers["Idempotency-Key"] = options.idempotencyKey;
1708
- const result = await this.request("POST", `/legal-entities/${id}/attestation`, input, headers);
1709
- return {
1710
- id: String(result.id ?? id),
1711
- externalId: result.externalId != null ? String(result.externalId) : null,
1712
- status: String(result.status ?? ""),
1713
- expiresAt: String(result.expiresAt ?? "")
1714
- };
1715
- }
1716
- async listBankAccounts(options) {
1717
- const params = new URLSearchParams();
1718
- if (options?.limit != null)
1719
- params.set("limit", String(options.limit));
1720
- if (options?.offset != null)
1721
- params.set("offset", String(options.offset));
1722
- const query = params.toString() ? `?${params.toString()}` : "";
1723
- const result = await this.request("GET", `/bank-accounts${query}`);
1724
- const bankAccounts = result.bankAccounts ?? result.data ?? [];
1725
- const meta = result.meta;
1726
- return {
1727
- data: bankAccounts.map(parseBankAccount),
1728
- meta: {
1729
- totalCount: Number(meta?.total_count ?? meta?.totalCount ?? bankAccounts.length),
1730
- offset: Number(meta?.offset ?? options?.offset ?? 0),
1731
- limit: Number(meta?.limit ?? options?.limit ?? 25),
1732
- hasMore: meta ? Number(meta.total_count ?? meta.totalCount) > Number(meta.offset) + Number(meta.limit) : false,
1733
- truncated: Boolean(meta?.truncated ?? false)
1734
- }
1735
- };
1736
- }
1737
- async getBankAccount(id) {
1738
- const result = await this.request("GET", `/bank-accounts/${id}`);
1739
- return parseBankAccount(result);
1740
- }
1741
- async createBankAccount(input) {
1742
- const result = await this.request("POST", "/bank-accounts", input);
1743
- return parseBankAccount(result);
1744
- }
1745
- async updateBankAccount(id, input) {
1746
- const result = await this.request("PUT", `/bank-accounts/${id}`, input);
1747
- return parseBankAccount(result);
1748
- }
1749
- async deleteBankAccount(id) {
1750
- await this.request("DELETE", `/bank-accounts/${id}`);
1751
- }
1752
- async importInvoice(options) {
1753
- const body = {
1754
- file: arrayBufferToBase64(options.file),
1755
- filename: options.filename,
1756
- mimeType: options.mimeType ?? detectMimeType(options.filename),
1757
- // Declared, never derived from the document: routing decides delivery, and
1758
- // parsing a caller's XML for a destination would put a parse error on the
1759
- // "whose invoice goes where" path.
1760
- to: options.to
1761
- };
1762
- const result = await this.request("POST", "/invoices/import", body);
1763
- return parseSendResult(result);
1764
- }
1765
- async listTransportTypes() {
1766
- const result = await this.request("GET", "/transports/types");
1767
- const types = result.transportTypes ?? result.data ?? [];
1768
- return types.map((t) => ({
1769
- code: String(t.code ?? ""),
1770
- name: String(t.name ?? "")
1771
- }));
1772
- }
1773
- async listTransports() {
1774
- const result = await this.request("GET", "/transports");
1775
- const transports = result.transports ?? result.data ?? [];
1776
- return transports.map(parseTransport);
1777
- }
1778
- async getTransport(code) {
1779
- const result = await this.request("GET", `/transports/${code}`);
1780
- return parseTransport(result);
1781
- }
1782
- async createTransport(input) {
1783
- const result = await this.request("POST", "/transports", input);
1784
- return parseTransport(result);
1785
- }
1786
- async updateTransport(code, input) {
1787
- const result = await this.request("PUT", `/transports/${code}`, input);
1788
- return parseTransport(result);
1789
- }
1790
- async deleteTransport(code) {
1791
- await this.request("DELETE", `/transports/${code}`);
1792
- }
1793
- };
1794
- function arrayBufferToBase64(buffer) {
1795
- const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
1796
- let binary = "";
1797
- for (const byte of bytes) {
1798
- binary += String.fromCharCode(byte);
1799
- }
1800
- return btoa(binary);
1801
- }
1802
- function detectMimeType(filename) {
1803
- const ext = filename.split(".").pop()?.toLowerCase();
1804
- switch (ext) {
1805
- case "xml":
1806
- return "application/xml";
1807
- case "pdf":
1808
- return "application/pdf";
1809
- case "json":
1810
- return "application/json";
1811
- default:
1812
- return "application/octet-stream";
1813
- }
1814
- }
1815
- var PROTOCOL_ERROR_BODY_LIMIT = 2e3;
1816
- function cloneForHook(body) {
1817
- try {
1818
- return structuredClone(body);
1819
- } catch {
1820
- return "[response body could not be copied for logging]";
1821
- }
1822
- }
1823
- function boundedBody(raw) {
1824
- let serialised;
1825
- try {
1826
- serialised = JSON.stringify(raw) ?? String(raw);
1827
- } catch {
1828
- serialised = "[unserialisable response body]";
1829
- }
1830
- if (serialised.length <= PROTOCOL_ERROR_BODY_LIMIT)
1831
- return serialised;
1832
- let head = serialised.slice(0, PROTOCOL_ERROR_BODY_LIMIT);
1833
- if (/[\uD800-\uDBFF]$/.test(head))
1834
- head = head.slice(0, -1);
1835
- return `${head}\u2026 [truncated, ${serialised.length} chars]`;
1836
- }
1837
- function requireRecordBody(raw, surface) {
1838
- if (isRecord(raw))
1839
- return raw;
1840
- throw new PeppolProtocolError(`The getpeppr API answered ${surface} with a body that is not an object. Please report this response to support@getpeppr.dev.`, "body", boundedBody(raw));
1841
- }
1842
- function requireWireStatus(candidate, raw, surface) {
1843
- if (typeof candidate === "string" && candidate.trim() !== "")
1844
- return candidate;
1845
- throw new PeppolProtocolError(`The getpeppr API answered ${surface} without a status. The SDK will not invent one \u2014 please report this response to support@getpeppr.dev.`, "status", boundedBody(raw));
1846
- }
1847
- function optionalWireId(value) {
1848
- return typeof value === "string" && value.trim() !== "" ? value : void 0;
1849
- }
1850
- function parseSendResult(body) {
1851
- const result = requireRecordBody(body, "this request");
1852
- const rawStatus = requireWireStatus(result.status, result, "this request");
1853
- const sendResult = {
1854
- id: String(result.id ?? ""),
1855
- status: mapStatus(rawStatus),
1856
- rawStatus,
1857
- // A TYPE guard like the identifiers below, not a cast. This line used to
1858
- // read `as string | undefined`, which types a number as a string and hands
1859
- // a consumer an AS4 id that never existed.
1860
- peppolMessageId: optionalWireId(result.peppolMessageId ?? result.peppol_message_id),
1861
- ublXml: result.ublXml,
1862
- warnings: Array.isArray(result.warnings) ? result.warnings : void 0
1863
- };
1864
- const createdAt = result.createdAt ?? result.created_at;
1865
- if (createdAt != null)
1866
- sendResult.createdAt = String(createdAt);
1867
- const detail = parseStatusDetail(result.detail);
1868
- if (detail)
1869
- sendResult.detail = detail;
1870
- const rulebook = result.rulebook;
1871
- if (typeof rulebook === "object" && rulebook !== null && typeof rulebook.peppol === "string" && typeof rulebook.verifiedAt === "string") {
1872
- sendResult.rulebook = rulebook;
1873
- }
1874
- const submissionId = optionalWireId(result.submissionId);
1875
- if (submissionId)
1876
- sendResult.submissionId = submissionId;
1877
- const providerDocumentId = optionalWireId(result.providerDocumentId);
1878
- if (providerDocumentId)
1879
- sendResult.providerDocumentId = providerDocumentId;
1880
- return sendResult;
1881
- }
1882
- function parseDirectoryEntry(result) {
1883
- const participant = isRecord(result.participant) ? result.participant : result;
1884
- const scheme = participant.scheme == null ? void 0 : String(participant.scheme);
1885
- const id = participant.id == null ? void 0 : String(participant.id);
1886
- const peppolId = participant.peppolId == null ? formatPeppolId(scheme, id) : String(participant.peppolId);
1887
- return {
1888
- name: String(participant.name ?? ""),
1889
- peppolId,
1890
- country: String(participant.country ?? ""),
1891
- capabilities: Array.isArray(participant.capabilities) ? participant.capabilities.map(String) : [],
1892
- registrationDate: participant.registrationDate == null ? void 0 : String(participant.registrationDate),
1893
- vatNumber: participant.vatNumber == null ? void 0 : String(participant.vatNumber),
1894
- additionalIds: Array.isArray(participant.additionalIds) ? participant.additionalIds.filter(isRecord).map((entry) => ({
1895
- scheme: String(entry.scheme ?? ""),
1896
- value: String(entry.value ?? "")
1897
- })) : void 0,
1898
- contactInfo: isRecord(participant.contactInfo) ? {
1899
- name: participant.contactInfo.name == null ? void 0 : String(participant.contactInfo.name),
1900
- email: participant.contactInfo.email == null ? void 0 : String(participant.contactInfo.email),
1901
- phone: participant.contactInfo.phone == null ? void 0 : String(participant.contactInfo.phone)
1902
- } : void 0,
1903
- website: participant.website == null ? void 0 : String(participant.website)
1904
- };
1905
- }
1906
- function formatPeppolId(scheme, id) {
1907
- if (!id)
1908
- return scheme ? `${scheme}:` : "";
1909
- if (id.includes(":"))
1910
- return id;
1911
- return scheme ? `${scheme}:${id}` : id;
1912
- }
1913
- function isRecord(value) {
1914
- return typeof value === "object" && value !== null && !Array.isArray(value);
1915
- }
1916
- var STATUS_DETAIL_AXES = ["platformFiscal", "delivery", "businessDisposition", "settlement"];
1917
- function parseStatusDetailEntry(raw) {
1918
- if (!isRecord(raw) || typeof raw.axis !== "string" || typeof raw.jurisdiction !== "string" || typeof raw.code !== "string" || typeof raw.label !== "string" || typeof raw.codeSystem !== "string" || typeof raw.codeVersion !== "string") {
1919
- return void 0;
1920
- }
1921
- const entry = {
1922
- axis: raw.axis,
1923
- jurisdiction: raw.jurisdiction,
1924
- code: raw.code,
1925
- label: raw.label,
1926
- codeSystem: raw.codeSystem,
1927
- codeVersion: raw.codeVersion
1928
- };
1929
- if (isRecord(raw.standardCode) && typeof raw.standardCode.system === "string" && typeof raw.standardCode.code === "string") {
1930
- entry.standardCode = { system: raw.standardCode.system, code: raw.standardCode.code };
1931
- }
1932
- if (typeof raw.reason === "string")
1933
- entry.reason = raw.reason;
1934
- if (Array.isArray(raw.warnings) && raw.warnings.every((w) => typeof w === "string")) {
1935
- entry.warnings = [...raw.warnings];
1936
- }
1937
- if (typeof raw.failureCategory === "string") {
1938
- entry.failureCategory = raw.failureCategory;
1939
- }
1940
- if (isRecord(raw.payment) && typeof raw.payment.amount === "number" && typeof raw.payment.currency === "string" && typeof raw.payment.date === "string") {
1941
- entry.payment = { amount: raw.payment.amount, currency: raw.payment.currency, date: raw.payment.date };
1942
- }
1943
- if (typeof raw.paymentSemantics === "string") {
1944
- entry.paymentSemantics = raw.paymentSemantics;
1945
- }
1946
- if (typeof raw.actor === "string")
1947
- entry.actor = raw.actor;
1948
- return entry;
1949
- }
1950
- function parseStatusDetail(raw) {
1951
- if (!isRecord(raw))
1952
- return void 0;
1953
- const detail = {};
1954
- for (const axis of STATUS_DETAIL_AXES) {
1955
- const entry = parseStatusDetailEntry(raw[axis]);
1956
- if (entry)
1957
- detail[axis] = entry;
1958
- }
1959
- return Object.keys(detail).length > 0 ? detail : void 0;
1960
- }
1961
- function parseContact(raw) {
1962
- const contact = {
1963
- id: String(raw.id ?? ""),
1964
- name: String(raw.name ?? "")
1965
- };
1966
- if (raw.peppolId != null)
1967
- contact.peppolId = String(raw.peppolId);
1968
- if (raw.vatNumber != null)
1969
- contact.vatNumber = String(raw.vatNumber);
1970
- if (raw.companyId != null)
1971
- contact.companyId = String(raw.companyId);
1972
- if (raw.street != null)
1973
- contact.street = String(raw.street);
1974
- if (raw.city != null)
1975
- contact.city = String(raw.city);
1976
- if (raw.postalCode != null)
1977
- contact.postalCode = String(raw.postalCode);
1978
- if (raw.country != null)
1979
- contact.country = String(raw.country);
1980
- if (raw.email != null)
1981
- contact.email = String(raw.email);
1982
- if (raw.phone != null)
1983
- contact.phone = String(raw.phone);
1984
- if (raw.isClient != null)
1985
- contact.isClient = Boolean(raw.isClient);
1986
- if (raw.isProvider != null)
1987
- contact.isProvider = Boolean(raw.isProvider);
1988
- if (raw.createdAt != null)
1989
- contact.createdAt = String(raw.createdAt);
1990
- if (raw.updatedAt != null)
1991
- contact.updatedAt = String(raw.updatedAt);
1992
- if (raw.directoryVerified != null)
1993
- contact.directoryVerified = Boolean(raw.directoryVerified);
1994
- if (raw.directoryLastChecked != null)
1995
- contact.directoryLastChecked = String(raw.directoryLastChecked);
1996
- return contact;
1997
- }
1998
- function parseLegalEntity(raw) {
1999
- const idObj = raw.identifier;
2000
- const le = {
2001
- id: String(raw.id ?? ""),
2002
- externalId: raw.externalId != null ? String(raw.externalId) : null,
2003
- companyName: raw.companyName != null ? String(raw.companyName) : null,
2004
- country: raw.country != null ? String(raw.country) : null,
2005
- identifier: idObj && idObj.scheme != null && idObj.value != null ? { scheme: String(idObj.scheme), value: String(idObj.value) } : null,
2006
- status: String(raw.status ?? "pending"),
2007
- environment: String(raw.environment ?? ""),
2008
- createdAt: String(raw.createdAt ?? "")
2009
- };
2010
- if (raw.verificationDetail != null) {
2011
- le.verificationDetail = raw.verificationDetail;
2012
- }
2013
- return le;
2014
- }
2015
- function parseInvoiceSummary(row) {
2016
- const raw = requireRecordBody(row, "this invoice row");
2017
- const rawStatus = requireWireStatus(raw.state ?? raw.status, raw, "this invoice row");
2018
- const summary = {
2019
- id: String(raw.id ?? ""),
2020
- number: String(raw.invoiceNumber ?? raw.number ?? ""),
2021
- status: mapStatus(rawStatus),
2022
- rawStatus
2023
- };
2024
- const detail = parseStatusDetail(raw.detail);
2025
- if (detail)
2026
- summary.detail = detail;
2027
- const submissionId = optionalWireId(raw.submissionId);
2028
- if (submissionId)
2029
- summary.submissionId = submissionId;
2030
- const providerDocumentId = optionalWireId(raw.providerDocumentId);
2031
- if (providerDocumentId)
2032
- summary.providerDocumentId = providerDocumentId;
2033
- if (raw.createdAt != null)
2034
- summary.createdAt = String(raw.createdAt);
2035
- if (typeof raw.isCreditNote === "boolean")
2036
- summary.isCreditNote = raw.isCreditNote;
2037
- if (raw.recipientName != null)
2038
- summary.recipientName = String(raw.recipientName);
2039
- if (raw.totalAmount != null && Number.isFinite(Number(raw.totalAmount))) {
2040
- summary.totalAmount = Number(raw.totalAmount);
2041
- }
2042
- if (raw.currency != null)
2043
- summary.currency = String(raw.currency);
2044
- if (raw.environment != null)
2045
- summary.environment = String(raw.environment);
2046
- return summary;
2047
- }
2048
- function parseBankAccount(raw) {
2049
- const account = {
2050
- id: String(raw.id ?? ""),
2051
- name: String(raw.name ?? ""),
2052
- type: raw.type === "number" ? "number" : "iban"
2053
- };
2054
- if (raw.iban != null)
2055
- account.iban = String(raw.iban);
2056
- if (raw.number != null)
2057
- account.number = String(raw.number);
2058
- if (raw.bic != null)
2059
- account.bic = String(raw.bic);
2060
- if (raw.country != null)
2061
- account.country = String(raw.country);
2062
- if (raw.createdAt != null)
2063
- account.createdAt = String(raw.createdAt);
2064
- if (raw.updatedAt != null)
2065
- account.updatedAt = String(raw.updatedAt);
2066
- return account;
2067
- }
2068
- function parseTransport(raw) {
2069
- return {
2070
- id: String(raw.id ?? ""),
2071
- transportTypeCode: String(raw.transportTypeCode ?? ""),
2072
- name: String(raw.name ?? ""),
2073
- status: raw.status ? String(raw.status) : void 0
2074
- };
2075
- }
2076
- var VALID_STATUSES = /* @__PURE__ */ new Set([
2077
- "submitted",
2078
- "delivered",
2079
- "accepted",
2080
- "rejected",
2081
- "paid",
2082
- "failed",
2083
- "cleared",
2084
- "acknowledged",
2085
- "in_process",
2086
- "under_query",
2087
- "conditionally_accepted",
2088
- "partially_paid",
2089
- "no_action",
2090
- "unknown"
2091
- // explicitly known: gateway may emit this when it can't map a Storecove status
2092
- ]);
2093
- function mapStatus(raw) {
2094
- const s = raw.toLowerCase();
2095
- if (VALID_STATUSES.has(s))
2096
- return s;
2097
- console.warn(`[getpeppr] Unknown gateway status received: "${raw}" \u2014 please report to support@getpeppr.dev. Coercing to "unknown".`);
2098
- return "unknown";
2099
- }
2100
- var PeppolError = class extends Error {
2101
- constructor(message) {
2102
- super(message);
2103
- this.name = "PeppolError";
2104
- }
2105
- };
2106
- var PeppolValidationError = class extends PeppolError {
2107
- validation;
2108
- constructor(message, validation) {
2109
- super(message);
2110
- this.validation = validation;
2111
- this.name = "PeppolValidationError";
2112
- }
2113
- };
2114
- var PeppolProtocolError = class extends PeppolError {
2115
- field;
2116
- responseBody;
2117
- constructor(message, field, responseBody) {
2118
- super(message);
2119
- this.field = field;
2120
- this.responseBody = responseBody;
2121
- this.name = "PeppolProtocolError";
2122
- }
2123
- };
2124
- var PeppolApiError = class extends PeppolError {
2125
- statusCode;
2126
- responseBody;
2127
- /** Parsed Retry-After delay in milliseconds (present on 429 responses) */
2128
- retryAfterMs;
2129
- constructor(message, statusCode, responseBody, retryAfterMs) {
2130
- super(message);
2131
- this.statusCode = statusCode;
2132
- this.responseBody = responseBody;
2133
- this.name = "PeppolApiError";
2134
- this.retryAfterMs = retryAfterMs;
2135
- }
2136
- /**
2137
- * The gateway's machine-readable error code, parsed from the JSON response body
2138
- * (e.g. "le_cap_exceeded", "identifier_immutable", "legal_entity_locked", "forbidden").
2139
- * Returns undefined when the body is not JSON or carries no string `code`.
2140
- */
2141
- get code() {
2142
- try {
2143
- const parsed = JSON.parse(this.responseBody);
2144
- return typeof parsed?.code === "string" ? parsed.code : void 0;
2145
- } catch {
2146
- return void 0;
2147
- }
2148
- }
2149
- };
2150
- var Peppol = class {
2151
- adapter;
2152
- invoices;
2153
- creditNotes;
2154
- directory;
2155
- events;
2156
- contacts;
2157
- bankAccounts;
2158
- transports;
2159
- /**
2160
- * Sub-tenant Legal Entities — **platform accounts only**.
2161
- *
2162
- * Requires a platform account and a **master API key**. With a standard key
2163
- * every call here fails with 403 `master_key_required`.
2164
- *
2165
- * Onboarding your OWN company is not done through this API: your legal entity
2166
- * is managed in the console, on the Peppol identity page. This surface is for
2167
- * platforms that onboard their customers as sub-tenants.
2168
- *
2169
- * **Getting access:** platform mode is enabled by getpeppr on your account —
2170
- * email hello@getpeppr.dev to have it switched on. Once it is, you create the
2171
- * master key yourself at https://console.getpeppr.dev/api-keys.
2172
- *
2173
- * @see https://getpeppr.dev/docs/platform/legal-entities/
2174
- */
2175
- legalEntities;
2176
- constructor(config) {
2177
- if (!config.apiKey) {
2178
- throw new PeppolError("API key is required. Sign up at https://console.getpeppr.dev to get your sandbox key.");
2179
- }
2180
- this.adapter = new GetpepprAdapter(config);
2181
- this.invoices = new InvoiceOperations(this.adapter);
2182
- this.creditNotes = new CreditNoteOperations(this.adapter);
2183
- this.directory = new DirectoryOperations(this.adapter);
2184
- this.events = new EventOperations(this.adapter);
2185
- this.contacts = new ContactOperations(this.adapter);
2186
- this.bankAccounts = new BankAccountOperations(this.adapter);
2187
- this.transports = new TransportOperations(this.adapter);
2188
- this.legalEntities = new LegalEntityOperations(this.adapter);
2189
- }
2190
- /**
2191
- * Validate an invoice without sending it.
2192
- * Useful for pre-flight checks in your UI.
2193
- */
2194
- validate(input) {
2195
- return validateInvoice(input);
2196
- }
2197
- /**
2198
- * Generate UBL XML without sending.
2199
- * Useful for debugging or manual submission.
2200
- */
2201
- toXml(input) {
2202
- const validation = validateInvoice(input);
2203
- if (!validation.valid) {
2204
- throw new PeppolValidationError(`Invoice validation failed: ${validation.errors.map((e) => e.message).join("; ")}`, validation);
2205
- }
2206
- if (input.isCreditNote) {
2207
- return buildCreditNoteXml(input);
2208
- }
2209
- return buildInvoiceXml(input);
2210
- }
2211
- };
2212
- async function* paginate(fetchPage, options) {
2213
- const pageSize = options?.limit ?? 25;
2214
- let offset = 0;
2215
- while (true) {
2216
- const page = await fetchPage(offset, pageSize);
2217
- if (page.data.length === 0)
2218
- break;
2219
- for (const item of page.data) {
2220
- yield item;
2221
- }
2222
- if (!page.meta.hasMore)
2223
- break;
2224
- offset += page.data.length;
2225
- }
2226
- }
2227
- var InvoiceOperations = class {
2228
- adapter;
2229
- constructor(adapter) {
2230
- this.adapter = adapter;
2231
- }
2232
- /**
2233
- * Create a draft invoice without sending it.
2234
- * Validates input client-side, then creates the invoice via the gateway.
2235
- * Use `sendById()` to send the draft when ready.
2236
- *
2237
- * @example
2238
- * ```ts
2239
- * const draft = await peppol.invoices.create({ number: "INV-001", to, lines });
2240
- * // Later, when ready:
2241
- * await peppol.invoices.sendById(draft.id);
2242
- * ```
2243
- */
2244
- async create(input, options) {
2245
- const validation = validateInvoice(input);
2246
- if (!validation.valid) {
2247
- throw new PeppolValidationError(`Invoice validation failed:
2248
- ${validation.errors.map((e) => ` - ${e.field}: ${e.message}${e.suggestion ? ` (${e.suggestion})` : ""}`).join("\n")}`, validation);
2249
- }
2250
- const result = await this.adapter.createInvoice(input, options);
2251
- if (validation.warnings.length > 0) {
2252
- result.warnings = validation.warnings;
2253
- }
2254
- return result;
2255
- }
2256
- /**
2257
- * Send an existing draft invoice by ID.
2258
- * The invoice must have been previously created with `create()`.
2259
- *
2260
- * @example
2261
- * ```ts
2262
- * await peppol.invoices.sendById("inv-1");
2263
- * ```
2264
- * @throws {PeppolApiError} 501 if the gateway provider does not support draft sending
2265
- */
2266
- async sendById(id) {
2267
- return this.adapter.sendInvoiceById(id);
2268
- }
2269
- /**
2270
- * Send an invoice via Peppol.
2271
- *
2272
- * @example
2273
- * ```ts
2274
- * const result = await peppol.invoices.send({
2275
- * number: "INV-001",
2276
- * from: { name: "My Company", peppolId: "0208:0685660237", country: "BE" },
2277
- * to: { name: "Client Co", peppolId: "0208:0685660237", country: "BE" },
2278
- * lines: [
2279
- * { description: "Consulting", quantity: 10, unitPrice: 150, vatRate: 21 }
2280
- * ]
2281
- * });
2282
- * ```
2283
- */
2284
- async send(input, options) {
2285
- const validation = validateInvoice(input);
2286
- if (!validation.valid) {
2287
- throw new PeppolValidationError(`Invoice validation failed:
2288
- ${validation.errors.map((e) => ` - ${e.field}: ${e.message}${e.suggestion ? ` (${e.suggestion})` : ""}`).join("\n")}`, validation);
2289
- }
2290
- const result = await this.adapter.sendInvoice(input, options);
2291
- if (validation.warnings.length > 0) {
2292
- result.warnings = validation.warnings;
2293
- }
2294
- return result;
2295
- }
2296
- /** List invoices with pagination, filtering, and proper metadata */
2297
- async list(options) {
2298
- return this.adapter.listInvoices(options);
2299
- }
2300
- /**
2301
- * Async iterator over all invoices, automatically handling pagination.
2302
- *
2303
- * @example
2304
- * ```ts
2305
- * for await (const invoice of peppol.invoices.listAll()) {
2306
- * console.log(invoice.id, invoice.status);
2307
- * }
2308
- * ```
2309
- */
2310
- listAll(options) {
2311
- return paginate((offset, limit) => this.adapter.listInvoices({ ...options, offset, limit }), options);
2312
- }
2313
- /**
2314
- * Get the status of a sent invoice.
2315
- *
2316
- * @param options.includeEvidence Ask the gateway to read the sending evidence
2317
- * from the Peppol network so the result carries `peppolMessageId`. Costs one
2318
- * provider round trip, so it is off by default; if the document has not gone
2319
- * out yet, or the read fails, the field is simply absent and everything else
2320
- * is unaffected.
2321
- *
2322
- * @example
2323
- * ```ts
2324
- * const status = await peppol.invoices.getStatus(id);
2325
- * const proof = await peppol.invoices.getStatus(id, { includeEvidence: true });
2326
- * ```
2327
- */
2328
- async getStatus(documentId, options) {
2329
- return this.adapter.getStatus(documentId, options);
2330
- }
2331
- /**
2332
- * Export an invoice in a specific format (e.g., PDF, UBL XML).
2333
- * Returns raw binary data as an ArrayBuffer.
2334
- *
2335
- * @example
2336
- * ```ts
2337
- * const pdf = await peppol.invoices.getAs("inv-123", "pdf");
2338
- * fs.writeFileSync("invoice.pdf", Buffer.from(pdf));
2339
- * ```
2340
- */
2341
- async getAs(id, format) {
2342
- return this.adapter.getInvoiceAs(id, format);
2343
- }
2344
- /**
2345
- * Validate an invoice server-side using the getpeppr gateway's offline SDK-backed checks.
2346
- * The gateway runs SDK validation, verifies UBL XML generation, and evaluates offline
2347
- * Peppol business rules without sending the invoice to Storecove.
2348
- *
2349
- * @example
2350
- * ```ts
2351
- * const result = await peppol.invoices.validateServer({
2352
- * number: "INV-001",
2353
- * to: { name: "Acme", peppolId: "0208:0685660237", country: "BE" },
2354
- * lines: [{ description: "Item", quantity: 1, unitPrice: 100, vatRate: 21 }]
2355
- * });
2356
- * console.log(result.valid, result.schematron.errors);
2357
- * ```
2358
- * Validation findings return a structured result with valid=false. Transport, auth,
2359
- * malformed request, and unexpected gateway failures still throw PeppolApiError.
2360
- */
2361
- async validateServer(input) {
2362
- return this.adapter.validateDocumentServer(input);
2363
- }
2364
- /**
2365
- * Send a UBL Invoice or CreditNote you built yourself.
2366
- *
2367
- * getpeppr does not regenerate, normalise, or repair the document — we
2368
- * forward the bytes you supplied, unchanged, to the network. Only UBL Invoice
2369
- * and CreditNote are accepted — a PDF, a CII document, or an XML that is
2370
- * neither is refused. The file is base64-encoded into a JSON body; there is
2371
- * no multipart upload.
2372
- *
2373
- * ⚠️ Byte-for-byte equality is NOT guaranteed, because the network
2374
- * re-serialises the document in transit. Measured 2026-08-18 on a test
2375
- * document: namespace declarations come back reordered, numeric character
2376
- * references are resolved (`&#65;` → `A`), whitespace inside tags is dropped,
2377
- * and no element was added, removed or altered. That is one document and four
2378
- * kinds of difference — indicative, not a warranty of what is preserved.
2379
- * **If you seal your documents, hash a canonical form (C14N) rather than the
2380
- * raw bytes.**
2381
- *
2382
- * `to` is required and is never read from the document. Routing decides
2383
- * delivery, the document travels as payload, and getpeppr will not guess a
2384
- * destination by parsing your XML.
2385
- *
2386
- * Before transmission the document is validated against the complete official
2387
- * OpenPeppol rulebooks. A document violating a `fatal` rule is refused and is
2388
- * NOT sent; the error names the rule.
2389
- *
2390
- * @example
2391
- * ```ts
2392
- * const xmlBytes = fs.readFileSync("invoice.xml");
2393
- * const result = await peppol.invoices.importFile({
2394
- * file: xmlBytes,
2395
- * filename: "invoice.xml",
2396
- * to: { peppolId: "0208:0685660237" },
2397
- * });
2398
- * console.log(result.id, result.status);
2399
- * ```
2400
- *
2401
- * @throws {PeppolApiError} 400 — `invalid_base64`, or a missing `file` /
2402
- * `filename`. `missing_recipient` when `to.peppolId` is absent.
2403
- * @throws {PeppolApiError} 422 — the document was refused and NOT sent. Two
2404
- * families, and they do NOT retry the same way:
2405
- *
2406
- * - **The document was rejected** (`validation_failed`, `not_ubl_document`,
2407
- * `document_too_complex`, `undecodable_document`, `unsupported_encoding`).
2408
- * Terminal: the same bytes fail identically forever. Fix the document —
2409
- * retrying is pure waste, and `validation_failed` names the rule.
2410
- * - **The account may not send right now** (`peppol_identity_incomplete`,
2411
- * `peppol_identity_not_verified`, `platform_billing_not_active`,
2412
- * `production_access_expired`). ⛔ NOT terminal: these describe account
2413
- * state, and account state changes — a verification completes, a contract
2414
- * is activated. The identical document will go through once it does.
2415
- *
2416
- * Treating the second family as terminal costs a customer a real invoice;
2417
- * treating the first as retryable costs an infinite loop. See the API
2418
- * reference for the full list.
2419
- */
2420
- async importFile(options) {
2421
- return this.adapter.importInvoice(options);
2422
- }
2423
- /**
2424
- * Acknowledge a received invoice.
2425
- *
2426
- * @example
2427
- * ```ts
2428
- * const result = await peppol.invoices.acknowledge("inv-123");
2429
- * console.log(result.status); // "accepted"
2430
- * ```
2431
- * @throws {PeppolApiError} 501 if the gateway provider does not support acknowledgement
2432
- */
2433
- async acknowledge(id) {
2434
- return this.adapter.acknowledgeInvoice(id);
2435
- }
2436
- /**
2437
- * Update an existing invoice (draft only).
2438
- * Only include the fields you want to change — partial updates are supported.
2439
- *
2440
- * @example
2441
- * ```ts
2442
- * const updated = await peppol.invoices.update("inv-123", {
2443
- * dueDate: "2026-04-01",
2444
- * lines: [
2445
- * { id: "line-1", quantity: 5 },
2446
- * { id: "line-2", _destroy: true },
2447
- * ],
2448
- * });
2449
- * ```
2450
- * @throws {PeppolApiError} 501 if the gateway provider does not support invoice updates
2451
- */
2452
- async update(id, input) {
2453
- return this.adapter.updateInvoice(id, input);
2454
- }
2455
- /**
2456
- * Delete an invoice.
2457
- *
2458
- * @example
2459
- * ```ts
2460
- * const deleted = await peppol.invoices.delete("inv-123");
2461
- * ```
2462
- * @throws {PeppolApiError} 501 if the gateway provider does not support invoice deletion
2463
- */
2464
- async delete(id) {
2465
- return this.adapter.deleteInvoice(id);
2466
- }
2467
- /**
2468
- * Transition an invoice to a new state.
2469
- * The gateway validates the state machine — invalid transitions return an error.
2470
- *
2471
- * `"paid"` on a French CTC invoice reports the payment collection
2472
- * (« signalement d'encaissement ») to the tax authority via the gateway —
2473
- * a legal obligation of the French mandate for service invoices. The full
2474
- * amount is reported from the invoice's stored tax breakdown (no amount to
2475
- * pass), at most once per invoice: replays return the same report (200),
2476
- * a concurrent report returns 409, a non-French invoice returns 422.
2477
- * The invoice's own status becomes `paid` later, when the network confirms
2478
- * (webhook / polling), not synchronously with this call.
2479
- *
2480
- * @example
2481
- * ```ts
2482
- * // France: report that the customer paid this invoice
2483
- * await peppol.invoices.markAs("inv-123", "paid");
2484
- * ```
2485
- * @throws {PeppolApiError} 422 for "paid" on a non-French-CTC invoice; 501 for states the provider does not support
2486
- */
2487
- async markAs(id, state, options) {
2488
- return this.adapter.markInvoiceAs(id, state, options);
2489
- }
2490
- /**
2491
- * Send multiple invoices in parallel with controlled concurrency.
2492
- * Each invoice is validated and sent individually — failures don't affect other invoices
2493
- * unless `stopOnError: true` is set.
2494
- *
2495
- * The SDK's built-in retry logic (including 429 Retry-After) provides automatic
2496
- * rate-limit handling at the request level.
2497
- *
2498
- * @example
2499
- * ```ts
2500
- * const result = await peppol.invoices.sendBatch([invoice1, invoice2, invoice3], {
2501
- * concurrency: 3,
2502
- * });
2503
- * console.log(`${result.succeeded.length} sent, ${result.failed.length} failed`);
2504
- * ```
2505
- */
2506
- async sendBatch(inputs, options) {
2507
- const concurrency = options?.concurrency ?? 5;
2508
- const stopOnError = options?.stopOnError ?? false;
2509
- const succeeded = [];
2510
- const failed = [];
2511
- let stopped = false;
2512
- for (let i = 0; i < inputs.length; i += concurrency) {
2513
- if (stopped)
2514
- break;
2515
- const chunk = inputs.slice(i, i + concurrency);
2516
- const promises = chunk.map(async (input, j) => {
2517
- const index = i + j;
2518
- if (stopped)
2519
- return;
2520
- try {
2521
- const result = await this.send(input);
2522
- succeeded.push({ index, result });
2523
- } catch (error2) {
2524
- failed.push({ index, input, error: error2 });
2525
- if (stopOnError) {
2526
- stopped = true;
2527
- }
2528
- }
2529
- });
2530
- await Promise.all(promises);
2531
- }
2532
- return { succeeded, failed, total: inputs.length };
2533
- }
2534
- /**
2535
- * Poll until an invoice reaches a target status.
2536
- *
2537
- * @example
2538
- * ```ts
2539
- * const result = await peppol.invoices.waitFor(id, "accepted", { timeout: 60000 });
2540
- * ```
2541
- */
2542
- async waitFor(documentId, targetStatus, options) {
2543
- const timeout = options?.timeout ?? 12e4;
2544
- const interval = options?.interval ?? 5e3;
2545
- const targets = Array.isArray(targetStatus) ? targetStatus : [targetStatus];
2546
- const startTime = Date.now();
2547
- while (true) {
2548
- const result = await this.getStatus(documentId);
2549
- if (targets.includes(result.status)) {
2550
- return result;
2551
- }
2552
- if (TERMINAL_FAILURE_STATUSES.includes(result.status)) {
2553
- throw new PeppolError(`Document ${documentId} reached terminal status "${result.status}" while waiting for "${targets.join('" or "')}"`);
2554
- }
2555
- if (statusFamily(result.status) === "terminal-success") {
2556
- if (targets.every((t) => statusFamily(t) === "progress")) {
2557
- return result;
2558
- }
2559
- throw new PeppolError(`Document ${documentId} reached terminal status "${result.status}" while waiting for "${targets.join('" or "')}"`);
2560
- }
2561
- if (Date.now() - startTime >= timeout) {
2562
- throw new PeppolError(`Timed out waiting for document ${documentId} to reach status "${targets.join('" or "')}" (last: "${result.status}")`);
2563
- }
2564
- await sleep(interval);
2565
- }
2566
- }
2567
- };
2568
- var CreditNoteOperations = class {
2569
- adapter;
2570
- constructor(adapter) {
2571
- this.adapter = adapter;
2572
- }
2573
- /**
2574
- * Send a credit note via Peppol.
2575
- * @deprecated Use peppol.invoices.send({ ...input, isCreditNote: true }) instead.
2576
- */
2577
- async send(input) {
2578
- const invoiceInput = {
2579
- ...input,
2580
- isCreditNote: true,
2581
- invoiceReference: input.invoiceReference
2582
- };
2583
- const validation = validateInvoice(invoiceInput);
2584
- if (!validation.valid) {
2585
- throw new PeppolValidationError(`Credit note validation failed:
2586
- ${validation.errors.map((e) => ` - ${e.field}: ${e.message}`).join("\n")}`, validation);
2587
- }
2588
- return this.adapter.sendInvoice(invoiceInput);
2589
- }
2590
- };
2591
- var DirectoryOperations = class {
2592
- adapter;
2593
- constructor(adapter) {
2594
- this.adapter = adapter;
2595
- }
2596
- /**
2597
- * Look up a Peppol participant in the directory.
2598
- *
2599
- * @example
2600
- * ```ts
2601
- * const entry = await peppol.directory.lookup("0208:0685660237");
2602
- * console.log(entry.name, entry.capabilities);
2603
- * ```
2604
- */
2605
- async lookup(peppolId) {
2606
- const colonIndex = peppolId.indexOf(":");
2607
- if (colonIndex === -1) {
2608
- throw new PeppolError('Invalid Peppol ID format. Expected "scheme:id" (e.g., "0208:0685660237")');
2609
- }
2610
- const scheme = peppolId.slice(0, colonIndex);
2611
- const id = peppolId.slice(colonIndex + 1);
2612
- return this.adapter.lookupDirectory(scheme, id);
2613
- }
2614
- /**
2615
- * Search the Peppol Directory for participants.
2616
- *
2617
- * @example
2618
- * ```ts
2619
- * const result = await peppol.directory.search({ name: "Acme", country: "BE" });
2620
- * console.log(result.data); // DirectoryEntry[]
2621
- * console.log(result.meta.totalCount);
2622
- * ```
2623
- */
2624
- async search(options) {
2625
- if (!options.name && !options.country && !options.vatNumber) {
2626
- throw new PeppolError("At least one search criterion is required (name, country, or vatNumber)");
2627
- }
2628
- if (options.name && options.name.length < 3) {
2629
- throw new PeppolError("Search name must be at least 3 characters");
2630
- }
2631
- if (!this.adapter.searchDirectory) {
2632
- throw new PeppolError("Directory search is not supported by this backend adapter");
2633
- }
2634
- const params = {};
2635
- if (options.name)
2636
- params.name = options.name;
2637
- if (options.country)
2638
- params.country = options.country;
2639
- if (options.vatNumber)
2640
- params.vatNumber = options.vatNumber;
2641
- if (options.limit !== void 0)
2642
- params.limit = String(options.limit);
2643
- if (options.offset !== void 0)
2644
- params.offset = String(options.offset);
2645
- return this.adapter.searchDirectory(params);
2646
- }
2647
- /**
2648
- * Search the Peppol Directory by VAT number.
2649
- * Convenience method — equivalent to `search({ vatNumber })`.
2650
- *
2651
- * @example
2652
- * ```ts
2653
- * const result = await peppol.directory.searchByVat("BE0685660237");
2654
- * ```
2655
- */
2656
- async searchByVat(vatNumber) {
2657
- return this.search({ vatNumber });
2658
- }
2659
- };
2660
- var EventOperations = class {
2661
- adapter;
2662
- constructor(adapter) {
2663
- this.adapter = adapter;
2664
- }
2665
- /**
2666
- * List events with optional filtering and pagination.
2667
- *
2668
- * @example
2669
- * ```ts
2670
- * const result = await peppol.events.list({ limit: 10 });
2671
- * console.log(result.data, result.meta);
2672
- *
2673
- * // Filter by invoice
2674
- * const invoiceEvents = await peppol.events.list({ invoiceId: "inv-123" });
2675
- * ```
2676
- */
2677
- async list(options) {
2678
- return this.adapter.listEvents(options);
2679
- }
2680
- /**
2681
- * Async iterator over all events, automatically handling pagination.
2682
- *
2683
- * @example
2684
- * ```ts
2685
- * for await (const event of peppol.events.listAll({ invoiceId: "inv-123" })) {
2686
- * console.log(event.name, event.createdAt);
2687
- * }
2688
- * ```
2689
- */
2690
- listAll(options) {
2691
- return paginate((offset, limit) => this.adapter.listEvents({ ...options, offset, limit }), options);
2692
- }
2693
- };
2694
- var ContactOperations = class {
2695
- adapter;
2696
- constructor(adapter) {
2697
- this.adapter = adapter;
2698
- }
2699
- /**
2700
- * List contacts with optional filtering and pagination.
2701
- *
2702
- * @example
2703
- * ```ts
2704
- * const result = await peppol.contacts.list({ limit: 10, isClient: true });
2705
- * console.log(result.data, result.meta);
2706
- * ```
2707
- */
2708
- async list(options) {
2709
- return this.adapter.listContacts(options);
2710
- }
2711
- /**
2712
- * Get a single contact by ID.
2713
- *
2714
- * @example
2715
- * ```ts
2716
- * const contact = await peppol.contacts.get("123");
2717
- * console.log(contact.name, contact.peppolId);
2718
- * ```
2719
- */
2720
- async get(id) {
2721
- return this.adapter.getContact(id);
2722
- }
2723
- /**
2724
- * Create a new contact.
2725
- *
2726
- * @example
2727
- * ```ts
2728
- * const contact = await peppol.contacts.create({
2729
- * name: "ACMEDIA",
2730
- * peppolId: "0208:0685660237",
2731
- * country: "BE",
2732
- * isClient: true,
2733
- * });
2734
- * ```
2735
- */
2736
- async create(input) {
2737
- return this.adapter.createContact(input);
2738
- }
2739
- /**
2740
- * Update an existing contact.
2741
- *
2742
- * @example
2743
- * ```ts
2744
- * const updated = await peppol.contacts.update("123", { email: "new@acme.com" });
2745
- * ```
2746
- */
2747
- async update(id, input) {
2748
- return this.adapter.updateContact(id, input);
2749
- }
2750
- /**
2751
- * Delete a contact.
2752
- *
2753
- * @example
2754
- * ```ts
2755
- * await peppol.contacts.delete("123");
2756
- * ```
2757
- */
2758
- async delete(id) {
2759
- return this.adapter.deleteContact(id);
2760
- }
2761
- /**
2762
- * Async iterator over all contacts, automatically handling pagination.
2763
- *
2764
- * @example
2765
- * ```ts
2766
- * for await (const contact of peppol.contacts.listAll({ isClient: true })) {
2767
- * console.log(contact.name, contact.peppolId);
2768
- * }
2769
- * ```
2770
- */
2771
- listAll(options) {
2772
- return paginate((offset, limit) => this.adapter.listContacts({ ...options, offset, limit }), options);
2773
- }
2774
- };
2775
- var LegalEntityOperations = class {
2776
- adapter;
2777
- constructor(adapter) {
2778
- this.adapter = adapter;
2779
- }
2780
- /**
2781
- * Create a sub-tenant Legal Entity for one of your customers.
2782
- *
2783
- * **Platform accounts only — requires a master API key.** Platform mode is
2784
- * enabled by getpeppr: email hello@getpeppr.dev, then create the key at
2785
- * https://console.getpeppr.dev/api-keys.
2786
- *
2787
- * Your own company's legal entity is managed in the console, on the Peppol
2788
- * identity page; this creates an entity for a customer of yours.
2789
- *
2790
- * Idempotent on `externalId`: repeated calls with the same `externalId` return
2791
- * the existing entity (HTTP 200) instead of creating a duplicate. Transient 5xx
2792
- * failures are NOT auto-retried unless you pass `options.idempotencyKey`.
2793
- *
2794
- * @example
2795
- * ```ts
2796
- * const le = await peppol.legalEntities.create({
2797
- * externalId: "tenant-42",
2798
- * companyName: "Acme Health AB",
2799
- * country: "SE",
2800
- * address: { line1: "Storgatan 1", city: "Stockholm", zip: "11122" },
2801
- * identifier: { scheme: "0007", value: "5560000001" },
2802
- * }, { idempotencyKey: "tenant-42-create" });
2803
- * ```
2804
- */
2805
- async create(input, options) {
2806
- return this.adapter.createLegalEntity(input, options);
2807
- }
2808
- /**
2809
- * Fetch a single sub-tenant Legal Entity by id.
2810
- *
2811
- * **Platform accounts only — requires a master API key.** Platform mode is
2812
- * enabled by getpeppr: email hello@getpeppr.dev, then create the key at
2813
- * https://console.getpeppr.dev/api-keys.
2814
- *
2815
- * For production entities the `status` reflects the attestation lifecycle
2816
- * (awaiting_authz → attested → active).
2817
- */
2818
- async get(id) {
2819
- return this.adapter.getLegalEntity(id);
2820
- }
2821
- /**
2822
- * List your sub-tenant Legal Entities, newest first.
2823
- *
2824
- * **Platform accounts only — requires a master API key.** Platform mode is
2825
- * enabled by getpeppr: email hello@getpeppr.dev, then create the key at
2826
- * https://console.getpeppr.dev/api-keys.
2827
- *
2828
- * This lists the customers you have onboarded, never your own legal entity.
2829
- */
2830
- async list(options) {
2831
- return this.adapter.listLegalEntities(options);
2832
- }
2833
- /**
2834
- * Async iterator over all sub-tenant Legal Entities, handling pagination.
2835
- *
2836
- * **Platform accounts only — requires a master API key.** Platform mode is
2837
- * enabled by getpeppr: email hello@getpeppr.dev, then create the key at
2838
- * https://console.getpeppr.dev/api-keys.
2839
- *
2840
- * @example
2841
- * ```ts
2842
- * for await (const le of peppol.legalEntities.listAll()) console.log(le.id, le.status);
2843
- * ```
2844
- */
2845
- listAll(options) {
2846
- return paginate((offset, limit) => this.adapter.listLegalEntities({ ...options, offset, limit }), options);
2847
- }
2848
- /**
2849
- * Archive (soft-delete) a sub-tenant Legal Entity. The id stays resolvable
2850
- * for audit.
2851
- *
2852
- * **Platform accounts only — requires a master API key.** Platform mode is
2853
- * enabled by getpeppr: email hello@getpeppr.dev, then create the key at
2854
- * https://console.getpeppr.dev/api-keys.
2855
- */
2856
- async archive(id) {
2857
- return this.adapter.archiveLegalEntity(id);
2858
- }
2859
- /**
2860
- * Request a sub-tenant attestation (production only). Emails the co-branded
2861
- * confirmation link to the sub-tenant contact and returns the pending status.
2862
- *
2863
- * **Platform accounts only — requires a master API key.** Platform mode is
2864
- * enabled by getpeppr: email hello@getpeppr.dev, then create the key at
2865
- * https://console.getpeppr.dev/api-keys.
2866
- *
2867
- * Transient failures are NOT auto-retried unless you pass `options.idempotencyKey`;
2868
- * re-issuing mints a fresh token, so a retried call is safe.
2869
- *
2870
- * @example
2871
- * ```ts
2872
- * await peppol.legalEntities.requestAttestation(le.id, { contactEmail: "owner@acme.example" });
2873
- * ```
2874
- */
2875
- async requestAttestation(id, input, options) {
2876
- return this.adapter.requestLegalEntityAttestation(id, input, options);
2877
- }
2878
- };
2879
- var BankAccountOperations = class {
2880
- adapter;
2881
- constructor(adapter) {
2882
- this.adapter = adapter;
2883
- }
2884
- /**
2885
- * List bank accounts with optional pagination.
2886
- *
2887
- * @example
2888
- * ```ts
2889
- * const result = await peppol.bankAccounts.list({ limit: 10 });
2890
- * console.log(result.data, result.meta);
2891
- * ```
2892
- */
2893
- async list(options) {
2894
- return this.adapter.listBankAccounts(options);
2895
- }
2896
- /**
2897
- * Get a single bank account by ID.
2898
- *
2899
- * @example
2900
- * ```ts
2901
- * const account = await peppol.bankAccounts.get("123");
2902
- * console.log(account.name, account.iban);
2903
- * ```
2904
- */
2905
- async get(id) {
2906
- return this.adapter.getBankAccount(id);
2907
- }
2908
- /**
2909
- * Create a new bank account.
2910
- *
2911
- * @example
2912
- * ```ts
2913
- * const account = await peppol.bankAccounts.create({
2914
- * name: "Main Account",
2915
- * iban: "BE68539007547034",
2916
- * bic: "BBRUBEBB",
2917
- * country: "BE",
2918
- * });
2919
- * ```
2920
- */
2921
- async create(input) {
2922
- return this.adapter.createBankAccount(input);
2923
- }
2924
- /**
2925
- * Update an existing bank account.
2926
- *
2927
- * @example
2928
- * ```ts
2929
- * const updated = await peppol.bankAccounts.update("123", { name: "Updated Name" });
2930
- * ```
2931
- */
2932
- async update(id, input) {
2933
- return this.adapter.updateBankAccount(id, input);
2934
- }
2935
- /**
2936
- * Delete a bank account.
2937
- *
2938
- * @example
2939
- * ```ts
2940
- * await peppol.bankAccounts.delete("123");
2941
- * ```
2942
- */
2943
- async delete(id) {
2944
- return this.adapter.deleteBankAccount(id);
2945
- }
2946
- /**
2947
- * Async iterator over all bank accounts, automatically handling pagination.
2948
- *
2949
- * @example
2950
- * ```ts
2951
- * for await (const account of peppol.bankAccounts.listAll()) {
2952
- * console.log(account.name, account.iban);
2953
- * }
2954
- * ```
2955
- */
2956
- listAll(options) {
2957
- return paginate((offset, limit) => this.adapter.listBankAccounts({ ...options, offset, limit }), options);
2958
- }
2959
- };
2960
- var TransportOperations = class {
2961
- adapter;
2962
- constructor(adapter) {
2963
- this.adapter = adapter;
2964
- }
2965
- /**
2966
- * List all available transport types in the network.
2967
- * Returns global transport types (not account-scoped).
2968
- *
2969
- * @example
2970
- * ```ts
2971
- * const types = await peppol.transports.listTypes();
2972
- * console.log(types); // [{ code: "peppol", name: "Peppol BIS 3.0" }, ...]
2973
- * ```
2974
- */
2975
- async listTypes() {
2976
- return this.adapter.listTransportTypes();
2977
- }
2978
- /**
2979
- * List configured transports for this account.
2980
- *
2981
- * @example
2982
- * ```ts
2983
- * const transports = await peppol.transports.list();
2984
- * console.log(transports); // [{ id: "t-1", transportTypeCode: "peppol", name: "..." }, ...]
2985
- * ```
2986
- */
2987
- async list() {
2988
- return this.adapter.listTransports();
2989
- }
2990
- /**
2991
- * Get a single transport by code.
2992
- *
2993
- * @example
2994
- * ```ts
2995
- * const transport = await peppol.transports.get("peppol");
2996
- * ```
2997
- */
2998
- async get(code) {
2999
- return this.adapter.getTransport(code);
3000
- }
3001
- /**
3002
- * Create a new transport.
3003
- *
3004
- * @example
3005
- * ```ts
3006
- * const transport = await peppol.transports.create({
3007
- * transportTypeCode: "peppol",
3008
- * email: "billing@acme.com",
3009
- * });
3010
- * ```
3011
- */
3012
- async create(input) {
3013
- return this.adapter.createTransport(input);
3014
- }
3015
- /**
3016
- * Update an existing transport.
3017
- *
3018
- * @example
3019
- * ```ts
3020
- * const transport = await peppol.transports.update("peppol", { email: "new@acme.com" });
3021
- * ```
3022
- */
3023
- async update(code, input) {
3024
- return this.adapter.updateTransport(code, input);
3025
- }
3026
- /**
3027
- * Delete a transport.
3028
- *
3029
- * @example
3030
- * ```ts
3031
- * await peppol.transports.delete("peppol");
3032
- * ```
3033
- */
3034
- async delete(code) {
3035
- return this.adapter.deleteTransport(code);
3036
- }
3037
- };
3038
-
3039
- // ../sdk/dist/core/schematron.js
3040
- function violation(ruleId, severity, message, field) {
3041
- return { ruleId, severity, message, field };
3042
- }
3043
- var _knownUnitCodes;
3044
- function getKnownUnitCodes() {
3045
- if (!_knownUnitCodes) {
3046
- _knownUnitCodes = new Set(getAllUnits().map((u) => u.code));
3047
- }
3048
- return _knownUnitCodes;
3049
- }
3050
- var VALID_VAT_CATEGORIES = /* @__PURE__ */ new Set(["S", "Z", "E", "AE", "K", "G", "O", "L", "M", "B"]);
3051
- var SENDABLE_VAT_CATEGORIES = ["S", "Z", "E", "AE", "K", "G", "O"];
3052
- var UNROUTABLE_VAT_CATEGORIES = /* @__PURE__ */ new Set(["L", "M", "B"]);
3053
- function computeLineNet(line) {
3054
- const baseQty = line.baseQuantity ?? 1;
3055
- if (baseQty === 0)
3056
- return NaN;
3057
- const baseAmount = line.quantity * line.unitPrice / baseQty;
3058
- const chargeTotal = (line.charges ?? []).reduce((sum, c) => sum + c.amount, 0);
3059
- const allowanceTotal = (line.allowances ?? []).reduce((sum, a) => sum + a.amount, 0);
3060
- return baseAmount + chargeTotal - allowanceTotal;
3061
- }
3062
- var br02 = (input) => {
3063
- if (!input.number?.trim()) {
3064
- return [violation("BR-02", "error", "Invoice number is required.", "number")];
3065
- }
3066
- return [];
3067
- };
3068
- var br03 = (input) => {
3069
- if (!input.date) {
3070
- return [
3071
- violation("BR-03", "warning", "Invoice issue date is not set. The SDK will default to today's date.", "date")
3072
- ];
3073
- }
3074
- return [];
3075
- };
3076
- var br06 = (input) => {
3077
- if (input.from && !input.from.vatNumber) {
3078
- return [
3079
- violation("BR-06", "warning", "Seller party has no VAT number. The gateway will use the account's VAT registration.", "from.vatNumber")
3080
- ];
3081
- }
3082
- return [];
3083
- };
3084
- var br07 = (input) => {
3085
- if (!input.to?.name?.trim()) {
3086
- return [violation("BR-07", "error", "Buyer name is required.", "to.name")];
3087
- }
3088
- return [];
3089
- };
3090
- var br08 = (input) => {
3091
- if (!input.lines || input.lines.length === 0) {
3092
- return [violation("BR-08", "error", "Invoice must have at least one line item.", "lines")];
3093
- }
3094
- return [];
3095
- };
3096
- var br09 = (input) => {
3097
- if (!input.dueDate && !input.paymentTerms) {
3098
- return [
3099
- violation("BR-09", "warning", "Neither due date nor payment terms specified. At least one is recommended.", "dueDate")
3100
- ];
3101
- }
3102
- return [];
3103
- };
3104
- var br10 = (input) => {
3105
- if (!input.buyerReference && !input.orderReference) {
3106
- return [
3107
- violation("BR-10", "warning", "Neither buyerReference nor orderReference specified. Peppol BIS 3.0 requires at least one.", "buyerReference")
3108
- ];
3109
- }
3110
- return [];
3111
- };
3112
- var brCo10 = (input) => {
3113
- const violations = [];
3114
- if (!input.lines)
3115
- return violations;
3116
- for (let i = 0; i < input.lines.length; i++) {
3117
- const line = input.lines[i];
3118
- const net = computeLineNet(line);
3119
- if (!Number.isFinite(net)) {
3120
- const baseQty = line.baseQuantity ?? 1;
3121
- const detail = baseQty === 0 ? "baseQuantity is 0, causing division by zero." : "Computed line amount is not a finite number.";
3122
- violations.push(violation("BR-CO-10", "error", `Line ${i}: invalid net amount. ${detail}`, `lines[${i}]`));
3123
- }
3124
- }
3125
- return violations;
3126
- };
3127
- var brCo13 = (input) => {
3128
- if (!input.lines || input.lines.length === 0)
3129
- return [];
3130
- let totalVat = 0;
3131
- for (let i = 0; i < input.lines.length; i++) {
3132
- const line = input.lines[i];
3133
- const net = computeLineNet(line);
3134
- if (!Number.isFinite(net))
3135
- continue;
3136
- totalVat += net * (line.vatRate / 100);
3137
- }
3138
- for (const allowance of input.allowances ?? []) {
3139
- totalVat -= allowance.amount * (allowance.vatRate / 100);
3140
- }
3141
- for (const charge of input.charges ?? []) {
3142
- totalVat += charge.amount * (charge.vatRate / 100);
3143
- }
3144
- if (!Number.isFinite(totalVat)) {
3145
- return [
3146
- violation("BR-CO-13", "error", "Computed total VAT amount is not a finite number. Check line amounts and VAT rates.")
3147
- ];
3148
- }
3149
- if (totalVat < -0.01) {
3150
- return [
3151
- violation("BR-CO-13", "warning", `Computed total VAT is negative (${totalVat.toFixed(2)}). This is unusual for an invoice.`)
3152
- ];
3153
- }
3154
- return [];
3155
- };
3156
- var brCo15 = (input) => {
3157
- if (!input.lines || input.lines.length === 0)
3158
- return [];
3159
- let lineTotal = 0;
3160
- let vatTotal = 0;
3161
- for (const line of input.lines) {
3162
- const net = computeLineNet(line);
3163
- if (!Number.isFinite(net))
3164
- continue;
3165
- lineTotal += net;
3166
- vatTotal += net * (line.vatRate / 100);
3167
- }
3168
- for (const allowance of input.allowances ?? []) {
3169
- lineTotal -= allowance.amount;
3170
- vatTotal -= allowance.amount * (allowance.vatRate / 100);
3171
- }
3172
- for (const charge of input.charges ?? []) {
3173
- lineTotal += charge.amount;
3174
- vatTotal += charge.amount * (charge.vatRate / 100);
3175
- }
3176
- const taxInclusive = lineTotal + vatTotal;
3177
- if (!Number.isFinite(taxInclusive)) {
3178
- return [
3179
- violation("BR-CO-15", "error", "Computed tax-inclusive amount is not a finite number.")
3180
- ];
3181
- }
3182
- if (taxInclusive < -0.01) {
3183
- return [
3184
- violation("BR-CO-15", "warning", `Computed tax-inclusive amount is negative (${taxInclusive.toFixed(2)}). Consider using a credit note instead.`)
3185
- ];
3186
- }
3187
- return [];
3188
- };
3189
- var brCo16 = (input) => {
3190
- if (!input.lines || input.lines.length === 0)
3191
- return [];
3192
- let lineTotal = 0;
3193
- let vatTotal = 0;
3194
- for (const line of input.lines) {
3195
- const net = computeLineNet(line);
3196
- if (!Number.isFinite(net))
3197
- continue;
3198
- lineTotal += net;
3199
- vatTotal += net * (line.vatRate / 100);
3200
- }
3201
- for (const allowance of input.allowances ?? []) {
3202
- lineTotal -= allowance.amount;
3203
- vatTotal -= allowance.amount * (allowance.vatRate / 100);
3204
- }
3205
- for (const charge of input.charges ?? []) {
3206
- lineTotal += charge.amount;
3207
- vatTotal += charge.amount * (charge.vatRate / 100);
3208
- }
3209
- const taxInclusive = lineTotal + vatTotal;
3210
- const prepaid = input.prepaidAmount ?? 0;
3211
- const rounding = input.roundingAmount ?? 0;
3212
- const payable = taxInclusive - prepaid + rounding;
3213
- if (!Number.isFinite(payable)) {
3214
- return [
3215
- violation("BR-CO-16", "error", "Computed payable amount is not a finite number.")
3216
- ];
3217
- }
3218
- if (payable < -0.01) {
3219
- return [
3220
- violation("BR-CO-16", "warning", `Computed payable amount is negative (${payable.toFixed(2)}). Prepaid amount (${prepaid}) exceeds the invoice total.`)
3221
- ];
3222
- }
3223
- return [];
3224
- };
3225
- var brS05 = (input) => {
3226
- const violations = [];
3227
- if (!input.lines)
3228
- return violations;
3229
- for (let i = 0; i < input.lines.length; i++) {
3230
- const line = input.lines[i];
3231
- const category = line.vatCategory ?? "S";
3232
- if (category === "S" && (line.vatRate === void 0 || line.vatRate <= 0)) {
3233
- violations.push(violation("BR-S-05", "error", `Line ${i}: standard rate (S) requires vatRate > 0, got ${line.vatRate ?? "undefined"}.`, `lines[${i}].vatRate`));
3234
- }
3235
- }
3236
- return violations;
3237
- };
3238
- var brZ05 = (input) => {
3239
- const violations = [];
3240
- if (!input.lines)
3241
- return violations;
3242
- for (let i = 0; i < input.lines.length; i++) {
3243
- const line = input.lines[i];
3244
- if (line.vatCategory === "Z" && line.vatRate !== 0) {
3245
- violations.push(violation("BR-Z-05", "error", `Line ${i}: zero-rated (Z) requires vatRate = 0, got ${line.vatRate}.`, `lines[${i}].vatRate`));
3246
- }
3247
- }
3248
- return violations;
3249
- };
3250
- var brE05 = (input) => {
3251
- const violations = [];
3252
- if (!input.lines)
3253
- return violations;
3254
- for (let i = 0; i < input.lines.length; i++) {
3255
- const line = input.lines[i];
3256
- if (line.vatCategory === "E" && line.vatRate !== 0) {
3257
- violations.push(violation("BR-E-05", "error", `Line ${i}: exempt (E) requires vatRate = 0, got ${line.vatRate}.`, `lines[${i}].vatRate`));
3258
- }
3259
- }
3260
- return violations;
3261
- };
3262
- var brAe05 = (input) => {
3263
- const violations = [];
3264
- if (!input.lines)
3265
- return violations;
3266
- for (let i = 0; i < input.lines.length; i++) {
3267
- const line = input.lines[i];
3268
- if (line.vatCategory === "AE" && line.vatRate !== 0) {
3269
- violations.push(violation("BR-AE-05", "error", `Line ${i}: reverse charge (AE) requires vatRate = 0, got ${line.vatRate}.`, `lines[${i}].vatRate`));
3270
- }
3271
- }
3272
- return violations;
3273
- };
3274
- var peppolR004 = (input) => {
3275
- if (!input.to?.peppolId) {
3276
- return [
3277
- violation("PEPPOL-EN16931-R004", "error", "Buyer electronic address (peppolId) is required for Peppol delivery.", "to.peppolId")
3278
- ];
3279
- }
3280
- return [];
3281
- };
3282
- var vatCategoryCodes = (input) => {
3283
- const violations = [];
3284
- const sendable = SENDABLE_VAT_CATEGORIES.join(", ");
3285
- const echo = (v) => v.length <= 16 ? v : `${v.slice(0, 16)}\u2026`;
3286
- const check = (cat, field, label) => {
3287
- if (cat === void 0 || cat === null)
3288
- return;
3289
- if (!VALID_VAT_CATEGORIES.has(cat)) {
3290
- violations.push(violation("BR-CL-17", "error", `${label}: "${echo(cat)}" is not a VAT category code. Use one of: ${sendable}. Codes are case-sensitive \u2014 "AE" is reverse charge, "ae" is not a category.`, field));
3291
- return;
3292
- }
3293
- if (UNROUTABLE_VAT_CATEGORIES.has(cat)) {
3294
- violations.push(violation("unsupported_vat_category", "error", `${label}: VAT category "${echo(cat)}" is valid under EN 16931 but getpeppr cannot route it \u2014 our provider has no vocabulary for it. Sendable categories: ${sendable}.`, field));
3295
- }
3296
- };
3297
- (input.lines ?? []).forEach((line, i) => check(line.vatCategory, `lines[${i}].vatCategory`, `Line ${i}`));
3298
- (input.allowances ?? []).forEach((a, i) => check(a.vatCategory, `allowances[${i}].vatCategory`, `Allowance ${i}`));
3299
- (input.charges ?? []).forEach((c, i) => check(c.vatCategory, `charges[${i}].vatCategory`, `Charge ${i}`));
3300
- return violations;
3301
- };
3302
- var peppolR080 = (input) => {
3303
- const violations = [];
3304
- if (!input.lines)
3305
- return violations;
3306
- const knownCodes = getKnownUnitCodes();
3307
- for (let i = 0; i < input.lines.length; i++) {
3308
- const line = input.lines[i];
3309
- if (line.unit) {
3310
- const resolved = resolveUnit(line.unit);
3311
- if (!knownCodes.has(resolved)) {
3312
- violations.push(violation("PEPPOL-EN16931-R080", "warning", `Line ${i}: unit "${line.unit}" (resolved: "${resolved}") is not a known UN/ECE Rec20 code. Common codes: EA, HUR, DAY, KGM.`, `lines[${i}].unit`));
3313
- }
3314
- }
3315
- }
3316
- return violations;
3317
- };
3318
- var ALL_RULES = [
3319
- // Required fields (BR)
3320
- br02,
3321
- br03,
3322
- br06,
3323
- br07,
3324
- br08,
3325
- br09,
3326
- br10,
3327
- // Calculations (BR-CO)
3328
- brCo10,
3329
- brCo13,
3330
- brCo15,
3331
- brCo16,
3332
- // Tax categories (one family per category)
3333
- brS05,
3334
- brZ05,
3335
- brE05,
3336
- brAe05,
3337
- // Peppol-specific
3338
- peppolR004,
3339
- vatCategoryCodes,
3340
- peppolR080
3341
- ];
3342
- function validateSchematron(input) {
3343
- const errors = [];
3344
- const warnings = [];
3345
- for (const rule of ALL_RULES) {
3346
- const violations = rule(input);
3347
- for (const v of violations) {
3348
- if (v.severity === "error") {
3349
- errors.push(v);
3350
- } else {
3351
- warnings.push(v);
3352
- }
3353
- }
3354
- }
3355
- return {
3356
- valid: errors.length === 0,
3357
- coverage: { rulesChecked: SDK_SCHEMATRON_RULE_IDS.length, ofNetworkFatalRules: "partial" },
3358
- errors,
3359
- warnings
3360
- };
3361
- }
3362
- var SDK_SCHEMATRON_RULE_IDS = [
3363
- "BR-02",
3364
- "BR-03",
3365
- "BR-06",
3366
- "BR-07",
3367
- "BR-08",
3368
- "BR-09",
3369
- "BR-10",
3370
- "BR-CL-17",
3371
- "BR-CO-10",
3372
- "BR-CO-13",
3373
- "BR-CO-15",
3374
- "BR-CO-16",
3375
- "BR-S-05",
3376
- "BR-Z-05",
3377
- "BR-E-05",
3378
- "BR-AE-05",
3379
- "PEPPOL-EN16931-R004",
3380
- "PEPPOL-EN16931-R080"
3381
- ];
3382
-
3383
- // src/commands/validate.ts
3384
- function runValidation(input) {
3385
- const structure = validateInvoice(input);
3386
- const schematron = validateSchematron(input);
3387
- const countryRules = validateCountryRules(input);
3388
- const totalErrors = structure.errors.length + schematron.errors.length + countryRules.errors.length;
3389
- const totalWarnings = structure.warnings.length + schematron.warnings.length + countryRules.warnings.length;
3390
- return {
3391
- structure: { errors: structure.errors, warnings: structure.warnings },
3392
- schematron: { errors: schematron.errors, warnings: schematron.warnings },
3393
- countryRules: {
3394
- errors: countryRules.errors,
3395
- warnings: countryRules.warnings
3396
- },
3397
- totalErrors,
3398
- totalWarnings,
3399
- valid: totalErrors === 0
3400
- };
3401
- }
3402
- function registerValidateCommand(program2) {
3403
- program2.command("validate").description("Validate a Peppol invoice JSON file").argument("<file>", "path to invoice JSON file").option("--json", "output results as JSON").option("--quiet", "exit code only, no output").action(async (file, options) => {
3404
- const input = readAndValidateInvoiceJson(file);
3405
- const result = runValidation(input);
3406
- if (options.quiet) {
3407
- process.exit(result.valid ? 0 : 1);
3408
- }
3409
- if (options.json) {
3410
- console.log(JSON.stringify(result, null, 2));
3411
- process.exit(result.valid ? 0 : 1);
3412
- }
3413
- const output = formatValidationResult(file, result);
3414
- console.log(output);
3415
- process.exit(result.valid ? 0 : 1);
3416
- });
3417
- }
3418
-
3419
- // src/commands/init.ts
3420
- import { existsSync as existsSync2, writeFileSync } from "fs";
3421
- import { resolve as resolve2 } from "path";
3422
- import pc2 from "picocolors";
3423
-
3424
- // src/templates/invoice.ts
3425
- var INVOICE_TEMPLATE = {
3426
- number: "INV-2026-001",
3427
- date: "2026-01-15",
3428
- dueDate: "2026-02-15",
3429
- currency: "EUR",
3430
- buyerReference: "PO-2026-042",
3431
- from: {
3432
- name: "Dupont & Fils SPRL",
3433
- peppolId: "0208:0685660237",
3434
- street: "Avenue Louise 54",
3435
- city: "Bruxelles",
3436
- postalCode: "1050",
3437
- country: "BE"
3438
- },
3439
- // Sandbox test receiver (GPR-828): the only recipient guaranteed reachable on
3440
- // the Storecove test network -- real directory companies make sandbox sends fail.
3441
- to: {
3442
- name: "SPF Economie (test receiver)",
3443
- peppolId: "9925:BE0314595348",
3444
- street: "Rue du Progr\xE8s 50",
3445
- city: "Brussels",
3446
- postalCode: "1210",
3447
- country: "BE"
3448
- },
3449
- lines: [
3450
- {
3451
- description: "Conseil en transformation num\xE9rique",
3452
- quantity: 10,
3453
- unitPrice: 950,
3454
- vatRate: 21
3455
- },
3456
- {
3457
- description: "Software license \u2014 annual subscription",
3458
- quantity: 1,
3459
- unitPrice: 2400,
3460
- vatRate: 0,
3461
- vatCategory: "AE"
3462
- }
3463
- ],
3464
- paymentTerms: "Net 30 days",
3465
- paymentReference: "+++000/0000/00097+++"
3466
- };
3467
-
3468
- // src/templates/credit-note.ts
3469
- var CREDIT_NOTE_TEMPLATE = {
3470
- number: "CN-2026-001",
3471
- date: "2026-02-01",
3472
- currency: "EUR",
3473
- isCreditNote: true,
3474
- invoiceReference: "INV-2026-001",
3475
- from: {
3476
- name: "Dupont & Fils SPRL",
3477
- peppolId: "0208:0685660237",
3478
- street: "Avenue Louise 54",
3479
- city: "Bruxelles",
3480
- postalCode: "1050",
3481
- country: "BE"
3482
- },
3483
- // Sandbox test receiver (GPR-828) \u2014 keep in sync with templates/invoice.ts.
3484
- to: {
3485
- name: "SPF Economie (test receiver)",
3486
- peppolId: "9925:BE0314595348",
3487
- street: "Rue du Progr\xE8s 50",
3488
- city: "Brussels",
3489
- postalCode: "1210",
3490
- country: "BE"
3491
- },
3492
- lines: [
3493
- {
3494
- description: "Avoir partiel \u2014 Conseil en transformation num\xE9rique",
3495
- quantity: 2,
3496
- unitPrice: 950,
3497
- vatRate: 21
3498
- }
3499
- ],
3500
- note: "Avoir pour prestations non r\xE9alis\xE9es \u2014 r\xE9f. INV-2026-001"
3501
- };
3502
-
3503
- // src/commands/init.ts
3504
- function registerInitCommand(program2) {
3505
- program2.command("init").description("Scaffold a starter invoice JSON file").argument("[filename]", "output filename", "invoice.json").option("--credit-note", "generate a credit note template instead").option("--force", "overwrite existing file").action(
3506
- (filename, options) => {
3507
- const resolved = resolve2(filename);
3508
- if (existsSync2(resolved) && !options.force) {
3509
- exitWithError(
3510
- `Error: ${filename} already exists. Use --force to overwrite.`
3511
- );
3512
- }
3513
- const template = options.creditNote ? CREDIT_NOTE_TEMPLATE : INVOICE_TEMPLATE;
3514
- try {
3515
- writeFileSync(
3516
- resolved,
3517
- JSON.stringify(template, null, 2) + "\n",
3518
- "utf-8"
3519
- );
3520
- } catch {
3521
- exitWithError(`Error: could not write file \u2014 ${resolved}`);
3522
- }
3523
- process.stderr.write(`${pc2.green("\u2713")} Created ${filename}
3524
-
3525
- Next steps:
3526
- 1. Edit the file with your invoice data
3527
- 2. Validate: getpeppr validate ${filename}
3528
- 3. Convert to XML: getpeppr convert ${filename}
3529
- 4. Send: getpeppr send ${filename}
3530
-
3531
- ${pc2.dim("Sandbox note:")} this template includes VAT. To send it on a sandbox
3532
- account, first register your VAT on the Peppol identity page, or set each
3533
- line's "vatCategory" to "O" (outside the scope of VAT) for a no-VAT test send.
3534
- `);
3535
- process.exit(0);
3536
- }
3537
- );
3538
- }
3539
-
3540
- // src/commands/convert.ts
3541
- import { writeFileSync as writeFileSync2 } from "fs";
3542
- import pc3 from "picocolors";
3543
- function registerConvertCommand(program2) {
3544
- program2.command("convert").description(
3545
- "Convert a getpeppr JSON invoice to Peppol BIS 3.0 UBL XML"
3546
- ).argument("<file>", "path to invoice JSON file").option("-o, --output <file>", "write XML to file instead of stdout").option("--validate", "validate the invoice before converting").action(
3547
- async (file, options) => {
3548
- const input = readAndValidateInvoiceJson(file);
3549
- if (options.validate) {
3550
- const result = runValidation(input);
3551
- const formatted = formatValidationResult(file, result);
3552
- if (!result.valid) {
3553
- process.stderr.write(formatted + "\n");
3554
- process.exit(1);
3555
- }
3556
- if (result.totalWarnings > 0) {
3557
- process.stderr.write(formatted + "\n");
3558
- }
3559
- }
3560
- const isCreditNote = input.isCreditNote === true;
3561
- let xml;
3562
- try {
3563
- if (isCreditNote) {
3564
- xml = buildCreditNoteXml(input);
3565
- } else {
3566
- xml = buildInvoiceXml(input);
3567
- }
3568
- } catch (err) {
3569
- const message = err instanceof Error ? err.message : "Unknown error";
3570
- exitWithError(`Error: XML generation failed \u2014 ${message}`);
3571
- }
3572
- xml = xml.replace(/^[ \t]*\n/gm, "");
3573
- const docType = isCreditNote ? "UBL 2.1 CreditNote" : "UBL 2.1 Invoice";
3574
- if (options.output) {
3575
- writeFileSync2(options.output, xml, "utf-8");
3576
- process.stderr.write(
3577
- `${pc3.green("\u2713")} Converted to ${options.output} (${docType})
3578
- `
3579
- );
3580
- } else {
3581
- process.stdout.write(xml + "\n");
3582
- }
3583
- }
3584
- );
3585
- }
3586
-
3587
- // src/commands/lookup.ts
3588
- import pc4 from "picocolors";
3589
-
3590
- // src/lib/peppol-directory.ts
3591
- var BASE_URL = "https://directory.peppol.eu/search/1.0/json";
3592
- var DirectoryError = class extends Error {
3593
- status;
3594
- constructor(message, status) {
3595
- super(message);
3596
- this.name = "DirectoryError";
3597
- this.status = status;
3598
- }
3599
- };
3600
- function stripQuotes(name) {
3601
- if (name.startsWith('"') && name.endsWith('"') && name.length >= 2) {
3602
- return name.slice(1, -1);
3603
- }
3604
- return name;
3605
- }
3606
- function pickBestName(names) {
3607
- if (names.length === 0) return "";
3608
- const english = names.find((n) => n.language === "en");
3609
- return (english ?? names[0]).name;
3610
- }
3611
- function mapDocType(urn) {
3612
- if (urn.includes("Invoice-2::Invoice##")) return "invoice";
3613
- if (urn.includes("CreditNote-2::CreditNote##")) return "credit_note";
3614
- if (urn.includes("ApplicationResponse")) return "application_response";
3615
- if (urn.includes("Order-2::Order##")) return "order";
3616
- if (urn.includes("DespatchAdvice")) return "despatch_advice";
3617
- return null;
3618
- }
3619
- function findVatIdentifier(identifiers) {
3620
- const match = identifiers.find((id) => {
3621
- const s = id.scheme.toLowerCase();
3622
- return s.includes("vat") || s.includes("cbe") || s.includes("tax");
3623
- });
3624
- return match?.value;
3625
- }
3626
- function parseMatch(raw) {
3627
- const entity = raw.entities[0];
3628
- const rawName = entity ? pickBestName(entity.name) : "";
3629
- const name = stripQuotes(rawName);
3630
- const country = entity?.countryCode ?? "";
3631
- const capabilities = (raw.docTypes ?? []).map((dt) => mapDocType(dt.value)).filter((c) => c !== null).filter((c, i, arr) => arr.indexOf(c) === i);
3632
- const vatNumber = entity?.identifiers ? findVatIdentifier(entity.identifiers) : void 0;
3633
- const contactEmail = entity?.contacts?.find((c) => c.email)?.email;
3634
- const website = entity?.websites && entity.websites.length > 0 ? entity.websites[0] : void 0;
3635
- return {
3636
- name,
3637
- peppolId: raw.participantID.value,
3638
- country,
3639
- capabilities,
3640
- registrationDate: entity?.regDate,
3641
- vatNumber,
3642
- contactEmail,
3643
- website
3644
- };
3645
- }
3646
- async function lookupParticipant(scheme, id) {
3647
- const participantParam = `iso6523-actorid-upis::${scheme}:${normalizeParticipantIdentifier(scheme, id)}`;
3648
- const url = `${BASE_URL}?participant=${encodeURIComponent(participantParam)}`;
3649
- const response = await fetch(url, {
3650
- signal: AbortSignal.timeout(15e3),
3651
- headers: { "User-Agent": "@getpeppr/cli" }
3652
- });
3653
- if (!response.ok) {
3654
- throw new DirectoryError(
3655
- `Lookup failed: HTTP ${response.status} ${response.statusText}`,
3656
- response.status
3657
- );
3658
- }
3659
- const data = await response.json();
3660
- if (!data.matches || data.matches.length === 0) {
3661
- return null;
3662
- }
3663
- return parseMatch(data.matches[0]);
3664
- }
3665
- function normalizeParticipantIdentifier(scheme, id) {
3666
- if (scheme === "0208") {
3667
- return id.replace(/^BE(?=(?:0|1)\d{9}$)/i, "");
3668
- }
3669
- return id;
3670
- }
3671
- async function searchParticipants(opts) {
3672
- const params = new URLSearchParams();
3673
- if (opts.name) params.set("name", opts.name);
3674
- if (opts.country) params.set("country", opts.country);
3675
- const url = `${BASE_URL}?${params.toString()}`;
3676
- const response = await fetch(url, {
3677
- signal: AbortSignal.timeout(15e3),
3678
- headers: { "User-Agent": "@getpeppr/cli" }
3679
- });
3680
- if (!response.ok) {
3681
- throw new DirectoryError(
3682
- `Search failed: HTTP ${response.status} ${response.statusText}`,
3683
- response.status
3684
- );
3685
- }
3686
- const data = await response.json();
3687
- const allMatches = (data.matches ?? []).map(parseMatch);
3688
- const limit = opts.limit ?? 10;
3689
- const matches = allMatches.slice(0, limit);
3690
- const totalCount = data["total-result-count"] ?? 0;
3691
- const hasMore = totalCount > matches.length;
3692
- return {
3693
- matches,
3694
- totalCount,
3695
- hasMore
3696
- };
3697
- }
3698
-
3699
- // src/commands/lookup.ts
3700
- var COUNTRY_NAMES = {
3701
- AT: "Austria",
3702
- BE: "Belgium",
3703
- BG: "Bulgaria",
3704
- HR: "Croatia",
3705
- CY: "Cyprus",
3706
- CZ: "Czechia",
3707
- DK: "Denmark",
3708
- EE: "Estonia",
3709
- FI: "Finland",
3710
- FR: "France",
3711
- DE: "Germany",
3712
- GR: "Greece",
3713
- HU: "Hungary",
3714
- IS: "Iceland",
3715
- IE: "Ireland",
3716
- IT: "Italy",
3717
- LV: "Latvia",
3718
- LT: "Lithuania",
3719
- LU: "Luxembourg",
3720
- MT: "Malta",
3721
- NL: "Netherlands",
3722
- NO: "Norway",
3723
- PL: "Poland",
3724
- PT: "Portugal",
3725
- RO: "Romania",
3726
- SK: "Slovakia",
3727
- SI: "Slovenia",
3728
- ES: "Spain",
3729
- SE: "Sweden",
3730
- CH: "Switzerland",
3731
- GB: "United Kingdom",
3732
- US: "United States",
3733
- AU: "Australia",
3734
- CA: "Canada",
3735
- SG: "Singapore",
3736
- JP: "Japan",
3737
- NZ: "New Zealand"
3738
- };
3739
- function countryLabel(code) {
3740
- const name = COUNTRY_NAMES[code.toUpperCase()];
3741
- return name ? `${name} (${code})` : code;
3742
- }
3743
- function formatLookupResult(match) {
3744
- const lines = [];
3745
- lines.push(`${pc4.green("\u2713")} ${match.name}`);
3746
- lines.push(` ${pc4.dim("Peppol ID")} ${match.peppolId}`);
3747
- lines.push(` ${pc4.dim("Country")} ${countryLabel(match.country)}`);
3748
- if (match.registrationDate) {
3749
- lines.push(` ${pc4.dim("Registered")} ${match.registrationDate}`);
3750
- }
3751
- if (match.vatNumber) {
3752
- lines.push(` ${pc4.dim("VAT")} ${match.vatNumber}`);
3753
- }
3754
- if (match.capabilities.length > 0) {
3755
- lines.push(
3756
- ` ${pc4.dim("Capabilities")} ${match.capabilities.join(", ")}`
3757
- );
3758
- }
3759
- if (match.contactEmail) {
3760
- lines.push(` ${pc4.dim("Contact")} ${match.contactEmail}`);
3761
- }
3762
- if (match.website) {
3763
- lines.push(` ${pc4.dim("Website")} ${match.website}`);
3764
- }
3765
- return lines.join("\n");
3766
- }
3767
- function formatSearchResults(result) {
3768
- const lines = [];
3769
- const plural = result.totalCount === 1 ? "participant" : "participants";
3770
- lines.push(`Found ${result.totalCount} ${plural}:
3771
- `);
3772
- const nameW = 26;
3773
- const idW = 23;
3774
- const countryW = 9;
3775
- lines.push(
3776
- ` ${"Name".padEnd(nameW)}${"Peppol ID".padEnd(idW)}${"Country".padEnd(countryW)}Capabilities`
3777
- );
3778
- lines.push(` ${"\u2500".repeat(nameW + idW + countryW + 20)}`);
3779
- for (const m of result.matches) {
3780
- const name = m.name.length > nameW - 1 ? m.name.slice(0, nameW - 2) + "\u2026" : m.name;
3781
- const caps = m.capabilities.join(", ");
3782
- lines.push(
3783
- ` ${name.padEnd(nameW)}${m.peppolId.padEnd(idW)}${m.country.padEnd(countryW)}${caps}`
3784
- );
3785
- }
3786
- if (result.hasMore) {
3787
- lines.push(
3788
- `
3789
- ${pc4.dim(`Showing ${result.matches.length} of ${result.totalCount} results.`)}`
3790
- );
3791
- }
3792
- return lines.join("\n");
3793
- }
3794
- function validatePeppolId(raw) {
3795
- const colonIndex = raw.indexOf(":");
3796
- if (colonIndex === -1) {
3797
- return {
3798
- ok: false,
3799
- error: `Invalid Peppol ID format: "${raw}". Expected format: scheme:id (e.g. 0208:0685660237)`
3800
- };
3801
- }
3802
- const scheme = raw.slice(0, colonIndex);
3803
- const id = raw.slice(colonIndex + 1);
3804
- if (!/^\d{4}$/.test(scheme)) {
3805
- return {
3806
- ok: false,
3807
- error: `Invalid scheme "${scheme}". Must be exactly 4 digits (e.g. 0208).`
3808
- };
3809
- }
3810
- if (!/^[A-Za-z0-9:.\-]+$/.test(id)) {
3811
- return {
3812
- ok: false,
3813
- error: `Invalid participant ID "${id}". Only letters, digits, colons, dots and hyphens are allowed.`
3814
- };
3815
- }
3816
- return { ok: true, scheme, id };
3817
- }
3818
- function registerLookupCommand(program2) {
3819
- program2.command("lookup").description("Look up a participant in the Peppol Directory").argument("[peppolId]", "Peppol participant ID (format: scheme:id)").option("--name <name>", "search by company name (min 3 chars)").option("--country <code>", "filter by ISO 2-letter country code").option("--json", "output results as JSON").option("--limit <n>", "max results (default 10)", "10").action(
3820
- async (peppolId, options) => {
3821
- const isSearch = Boolean(options.name);
3822
- const isLookup = Boolean(peppolId);
3823
- if (!isSearch && !isLookup) {
3824
- exitWithError("Provide a Peppol ID or use --name to search.");
3825
- }
3826
- if (options.country) {
3827
- const normalized = options.country.toUpperCase();
3828
- if (!/^[A-Z]{2}$/.test(normalized)) {
3829
- exitWithError(
3830
- `Invalid country code "${options.country}". Must be 2 letters (e.g. BE, DE, FR).`
3831
- );
3832
- }
3833
- options.country = normalized;
3834
- }
3835
- if (isLookup) {
3836
- await handleLookup(peppolId, options);
3837
- } else {
3838
- await handleSearch(options);
3839
- }
3840
- }
3841
- );
3842
- }
3843
- async function handleLookup(peppolId, options) {
3844
- const parsed = validatePeppolId(peppolId);
3845
- if (!parsed.ok) {
3846
- exitWithError(parsed.error);
3847
- }
3848
- let result;
3849
- try {
3850
- result = await lookupParticipant(parsed.scheme, parsed.id);
3851
- } catch (err) {
3852
- if (err instanceof DirectoryError) {
3853
- exitWithError(
3854
- `${pc4.red("\u2717")} Peppol Directory returned an error (HTTP ${err.status ?? "unknown"}). Try again later.`
3855
- );
3856
- }
3857
- exitWithError(
3858
- `${pc4.red("\u2717")} Could not reach Peppol Directory. Check your internet connection.`
3859
- );
3860
- }
3861
- if (!result) {
3862
- if (options.json) {
3863
- console.log(JSON.stringify(null));
3864
- } else {
3865
- process.stderr.write(
3866
- `${pc4.red("\u2717")} Participant not found: ${peppolId}
3867
- `
3868
- );
3869
- }
3870
- process.exit(1);
3871
- }
3872
- if (options.json) {
3873
- console.log(JSON.stringify(result, null, 2));
3874
- } else {
3875
- console.log(formatLookupResult(result));
3876
- }
3877
- process.exit(0);
3878
- }
3879
- async function handleSearch(options) {
3880
- if (options.name && options.name.length < 3) {
3881
- exitWithError(
3882
- `Search name must be at least 3 characters. Got: "${options.name}"`
3883
- );
3884
- }
3885
- const limit = parseInt(options.limit ?? "10", 10);
3886
- let result;
3887
- try {
3888
- result = await searchParticipants({
3889
- name: options.name,
3890
- country: options.country,
3891
- limit
3892
- });
3893
- } catch (err) {
3894
- if (err instanceof DirectoryError) {
3895
- exitWithError(
3896
- `${pc4.red("\u2717")} Peppol Directory returned an error (HTTP ${err.status ?? "unknown"}). Try again later.`
3897
- );
3898
- }
3899
- exitWithError(
3900
- `${pc4.red("\u2717")} Could not reach Peppol Directory. Check your internet connection.`
3901
- );
3902
- }
3903
- if (result.matches.length === 0) {
3904
- if (options.json) {
3905
- console.log(JSON.stringify(result, null, 2));
3906
- } else {
3907
- process.stderr.write("No participants found.\n");
3908
- }
3909
- process.exit(1);
3910
- }
3911
- if (options.json) {
3912
- console.log(JSON.stringify(result, null, 2));
3913
- } else {
3914
- console.log(formatSearchResults(result));
3915
- }
3916
- process.exit(0);
3917
- }
3918
-
3919
- // src/commands/send.ts
3920
- import pc6 from "picocolors";
3921
-
3922
- // src/lib/credentials-store.ts
3923
- import {
3924
- chmodSync,
3925
- existsSync as existsSync3,
3926
- mkdirSync,
3927
- readFileSync as readFileSync2,
3928
- rmSync,
3929
- statSync,
3930
- writeFileSync as writeFileSync3,
3931
- renameSync
3932
- } from "fs";
3933
- import { homedir, platform } from "os";
3934
- import { dirname, join } from "path";
3935
- function getCredentialsPath() {
3936
- if (platform() === "win32") {
3937
- const appdata = process.env.APPDATA ?? join(homedir(), "AppData", "Roaming");
3938
- return join(appdata, "getpeppr", "credentials.json");
3939
- }
3940
- const xdg = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
3941
- return join(xdg, "getpeppr", "credentials.json");
3942
- }
3943
- function readCredentials() {
3944
- const path = getCredentialsPath();
3945
- if (!existsSync3(path)) return null;
3946
- if (platform() !== "win32") {
3947
- const stats = statSync(path);
3948
- const mode = stats.mode & 511;
3949
- if (mode !== 384) {
3950
- process.stderr.write(
3951
- `\u26A0 Config file mode was ${mode.toString(8)}; restoring to 600.
3952
- `
3953
- );
3954
- chmodSync(path, 384);
3955
- }
3956
- }
3957
- let raw;
3958
- try {
3959
- raw = readFileSync2(path, "utf-8");
3960
- } catch {
3961
- process.stderr.write(`\u26A0 Could not read config file: ${path}
3962
- `);
3963
- return null;
3964
- }
3965
- try {
3966
- const data = JSON.parse(raw);
3967
- return data;
3968
- } catch {
3969
- process.stderr.write(`\u26A0 Malformed JSON in config file: ${path}
3970
- `);
3971
- return null;
3972
- }
3973
- }
3974
- function writeCredentials(creds) {
3975
- const path = getCredentialsPath();
3976
- const dir = dirname(path);
3977
- mkdirSync(dir, { recursive: true, mode: 448 });
3978
- const tmpPath = `${path}.tmp`;
3979
- const data = JSON.stringify(creds, null, 2) + "\n";
3980
- try {
3981
- rmSync(tmpPath, { force: true });
3982
- } catch {
3983
- }
3984
- writeFileSync3(tmpPath, data, { mode: 384, flag: "wx" });
3985
- chmodSync(tmpPath, 384);
3986
- try {
3987
- renameSync(tmpPath, path);
3988
- } catch (e) {
3989
- try {
3990
- rmSync(tmpPath, { force: true });
3991
- } catch {
3992
- }
3993
- throw e;
3994
- }
3995
- }
3996
- function deleteCredentials() {
3997
- const path = getCredentialsPath();
3998
- if (!existsSync3(path)) return false;
3999
- rmSync(path);
4000
- return true;
4001
- }
4002
-
4003
- // src/lib/auth.ts
4004
- var AuthError = class extends Error {
4005
- constructor(message) {
4006
- super(message);
4007
- this.name = "AuthError";
4008
- }
4009
- };
4010
- function resolveApiKey(opts) {
4011
- const env = opts.forceProd ? "live" : "sandbox";
4012
- if (opts.flagKey) {
4013
- return { apiKey: opts.flagKey, source: "flag", environment: env };
4014
- }
4015
- const envKey = process.env.GETPEPPR_API_KEY;
4016
- if (envKey) {
4017
- return { apiKey: envKey, source: "env", environment: env };
4018
- }
4019
- const creds = readCredentials();
4020
- if (creds) {
4021
- const key = env === "live" ? creds.live : creds.sandbox;
4022
- if (key) {
4023
- return { apiKey: key, source: "config", environment: env };
4024
- }
4025
- if (env === "live") {
4026
- throw new AuthError(
4027
- `Config has no live API key. Run \`getpeppr login\` again with --live, or pass --key.`
4028
- );
4029
- }
4030
- }
4031
- throw new AuthError(
4032
- `No API key found. Run \`getpeppr login\` or set GETPEPPR_API_KEY.`
4033
- );
4034
- }
4035
-
4036
- // src/lib/send-payload.ts
4037
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
4038
- import { resolve as resolve3 } from "path";
4039
-
4040
- // src/templates/send-default.ts
4041
- var MINIMAL_PDF = `%PDF-1.0
4042
- 1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 3 3]>>endobj
4043
- xref
4044
- 0 4
4045
- 0000000000 65535 f
4046
- 0000000009 00000 n
4047
- 0000000058 00000 n
4048
- 0000000115 00000 n
4049
- trailer<</Size 4/Root 1 0 R>>
4050
- startxref
4051
- 190
4052
- %%EOF`;
4053
- var MINIMAL_TEST_PDF_BASE64 = Buffer.from(MINIMAL_PDF).toString("base64");
4054
- var SCHEME_COUNTRY_DEFAULTS = {
4055
- "0009": "FR",
4056
- "0204": "DE",
4057
- "0208": "BE",
4058
- "9925": "BE"
4059
- };
4060
- function deriveCountryFromPeppolId(peppolId) {
4061
- const [scheme, identifier = ""] = peppolId.split(":");
4062
- const alphaPrefix = identifier.slice(0, 2);
4063
- if (/^[A-Za-z]{2}$/.test(alphaPrefix)) {
4064
- return alphaPrefix.toUpperCase();
4065
- }
4066
- return SCHEME_COUNTRY_DEFAULTS[scheme] ?? "BE";
4067
- }
4068
- function buildDefaultSendPayload(overrides = {}) {
4069
- const today = /* @__PURE__ */ new Date();
4070
- const due = new Date(today.getTime() + 30 * 864e5);
4071
- const isoToday = today.toISOString().slice(0, 10);
4072
- const isoDue = due.toISOString().slice(0, 10);
4073
- const peppolId = overrides.to ?? "9925:BE0314595348";
4074
- const amount = overrides.amount ?? 100;
4075
- const currency = overrides.currency ?? "EUR";
4076
- const description = overrides.description ?? "Test service from getpeppr";
4077
- const randomSuffix = Math.floor(Math.random() * 65536).toString(16).toUpperCase().padStart(4, "0");
4078
- const number = `TEST-${Date.now().toString(36).toUpperCase()}-${randomSuffix}`;
4079
- const country = overrides.country != null ? overrides.country.toUpperCase() : deriveCountryFromPeppolId(peppolId);
4080
- const payload = {
4081
- number,
4082
- date: isoToday,
4083
- dueDate: isoDue,
4084
- currency,
4085
- to: {
4086
- name: peppolId === "9925:BE0314595348" ? "SPF Economie (TEST)" : "Test Recipient",
4087
- peppolId,
4088
- country,
4089
- street: "Rue de la Loi 1",
4090
- city: "Brussels",
4091
- postalCode: "1000"
4092
- },
4093
- lines: [
4094
- {
4095
- description,
4096
- quantity: 1,
4097
- unitPrice: amount,
4098
- vatRate: 0,
4099
- // vatCategory "O" = "Services outside scope of tax" (UBL 2.1 / EN 16931).
4100
- // Public entities (SPF Economie) are VAT-exempt. No `taxExemptReason` field
4101
- // exists in InvoiceLine — Storecove derives exemption from the category code.
4102
- // If sandbox returns 422 on this combination, fallback is vatRate: 21 + vatCategory: "S".
4103
- vatCategory: "O"
4104
- }
4105
- ]
4106
- };
4107
- if (overrides.attachment) {
4108
- payload.attachments = [
4109
- {
4110
- id: "ATT-001",
4111
- description: "Test document",
4112
- filename: "test.pdf",
4113
- mimeType: "application/pdf",
4114
- content: MINIMAL_TEST_PDF_BASE64
4115
- }
4116
- ];
4117
- }
4118
- return payload;
4119
- }
4120
-
4121
- // src/lib/send-payload.ts
4122
- var MutexError = class extends Error {
4123
- constructor() {
4124
- super("Cannot combine custom file with override flags. Use one or the other.");
4125
- this.name = "MutexError";
4126
- }
4127
- };
4128
- function hasOverrides(o) {
4129
- if (!o) return false;
4130
- return Boolean(
4131
- o.to != null && o.to !== "" || o.country != null && o.country !== "" || o.amount != null || o.currency || o.description || o.attachment === true
4132
- );
4133
- }
4134
- function buildPayload(opts) {
4135
- if (opts.file && hasOverrides(opts.overrides)) {
4136
- throw new MutexError();
4137
- }
4138
- if (opts.file) {
4139
- const absPath = resolve3(opts.file);
4140
- if (!existsSync4(absPath)) {
4141
- throw new Error(`Error: file not found \u2014 ${absPath}`);
4142
- }
4143
- let raw;
4144
- try {
4145
- raw = readFileSync3(absPath, "utf-8");
4146
- } catch {
4147
- throw new Error(`Error: could not read file \u2014 ${absPath}`);
4148
- }
4149
- try {
4150
- return JSON.parse(raw);
4151
- } catch {
4152
- throw new Error(`Error: invalid JSON in file \u2014 ${absPath}`);
4153
- }
4154
- }
4155
- return buildDefaultSendPayload(opts.overrides);
4156
- }
4157
-
4158
- // src/lib/confirm.ts
4159
- import { createInterface } from "readline";
4160
- async function confirmInteractive(opts) {
4161
- const stdin = opts.stdin ?? process.stdin;
4162
- const stdout = opts.stdout ?? process.stdout;
4163
- if (stdin.isTTY !== true) {
4164
- return opts.defaultYes;
4165
- }
4166
- const suffix = opts.defaultYes ? "[Y/n]" : "[y/N]";
4167
- return new Promise((resolve4) => {
4168
- const rl = createInterface({ input: stdin, output: stdout });
4169
- let settled = false;
4170
- rl.question(`${opts.prompt} ${suffix} `, (answer) => {
4171
- settled = true;
4172
- rl.close();
4173
- const trimmed = answer.trim().toLowerCase();
4174
- if (trimmed === "") return resolve4(opts.defaultYes);
4175
- if (trimmed === "y" || trimmed === "yes") return resolve4(true);
4176
- if (trimmed === "n" || trimmed === "no") return resolve4(false);
4177
- return resolve4(opts.defaultYes);
4178
- });
4179
- rl.once("close", () => {
4180
- if (!settled) resolve4(opts.defaultYes);
4181
- });
4182
- });
4183
- }
4184
-
4185
- // src/lib/watch.ts
4186
- var TERMINAL_STATES = /* @__PURE__ */ new Set([
4187
- "delivered",
4188
- "accepted",
4189
- "rejected",
4190
- "failed",
4191
- "no_action"
4192
- ]);
4193
- var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
4194
- async function pollUntilTerminal(client, documentId, options = {}) {
4195
- const intervalMs = options.intervalMs ?? 2e3;
4196
- const timeoutMs = options.timeoutMs ?? 6e4;
4197
- const start = Date.now();
4198
- let lastStatus = "";
4199
- while (Date.now() - start < timeoutMs) {
4200
- const { status } = await client.invoices.getStatus(documentId);
4201
- if (status !== lastStatus) {
4202
- options.onTransition?.(status);
4203
- lastStatus = status;
4204
- }
4205
- if (TERMINAL_STATES.has(status)) {
4206
- return { finalStatus: status, timedOut: false };
4207
- }
4208
- await sleep2(intervalMs);
4209
- }
4210
- return { finalStatus: lastStatus, timedOut: true };
4211
- }
4212
-
4213
- // src/formatters/send-result.ts
4214
- import pc5 from "picocolors";
4215
- function formatSendResult(result, mode) {
4216
- if (mode === "quiet") return "";
4217
- if (mode === "json") {
4218
- return JSON.stringify(result, null, 2);
4219
- }
4220
- const lines = [];
4221
- lines.push(`${pc5.green("\u2713")} Sent ${pc5.bold(result.number)}`);
4222
- lines.push(` id: ${result.id}`);
4223
- lines.push(` Status: ${pc5.cyan(result.status)}`);
4224
- lines.push(` Track: ${pc5.dim(result.dashboardUrl)}`);
4225
- const wCount = result.warnings?.length ?? 0;
4226
- if (wCount > 0) {
4227
- lines.push(` ${pc5.yellow(`${wCount} warning${wCount === 1 ? "" : "s"}`)}`);
4228
- for (const w of result.warnings ?? []) {
4229
- lines.push(` ${pc5.yellow("\u26A0")} ${w.message}`);
4230
- }
4231
- }
4232
- return lines.join("\n");
4233
- }
4234
-
4235
- // src/commands/send.ts
4236
- var API_BASE = "https://api.getpeppr.dev/v1";
4237
- var LOCAL_BASE = "http://localhost:3001/api/v1";
4238
- var DASHBOARD_BASE = "https://console.getpeppr.dev/invoices";
4239
- function registerSendCommand(program2) {
4240
- program2.command("send").description("Send an invoice to the Peppol network via getpeppr API").argument("[file]", "optional path to invoice JSON (mutex with --to/--amount/...)").option("--prod", "target production (live keys + confirmation)").option("--local", "target localhost:3001 dev server").option("--key <key>", "override API key \u2014 for CI/scripted use only; visible in `ps` and shell history. Prefer GETPEPPR_API_KEY env var.").option("--to <peppol-id>", "recipient peppol id (e.g., 9925:BE0314595348)").option("--country <iso>", "recipient ISO 3166-1 alpha-2 country override (e.g., BE)").option("--amount <number>", "line amount in major currency units (decimal allowed)").option("--currency <iso>", "ISO 4217 currency (default EUR)").option("--desc <text>", "line description").option("--attachment", "attach the test PDF").option("--watch", "poll status until a terminal state (60s timeout)").option("-y, --yes", "skip --prod confirmation prompt").option("--no-validate", "skip pre-validation locally").option("--json", "output JSON").option("--quiet", "exit code only, no output").action(async (file, flags) => {
4241
- let auth;
4242
- try {
4243
- auth = resolveApiKey({
4244
- flagKey: flags.key,
4245
- forceProd: Boolean(flags.prod),
4246
- forceLocal: Boolean(flags.local)
4247
- });
4248
- } catch (e) {
4249
- if (e instanceof AuthError) {
4250
- exitWithError(e.message);
4251
- return;
4252
- }
4253
- throw e;
4254
- }
4255
- const overrides = {
4256
- to: flags.to,
4257
- country: flags.country,
4258
- amount: flags.amount != null ? Number(flags.amount) : void 0,
4259
- currency: flags.currency,
4260
- description: flags.desc,
4261
- attachment: flags.attachment
4262
- };
4263
- let payload;
4264
- try {
4265
- payload = buildPayload({ file, overrides });
4266
- } catch (e) {
4267
- if (e instanceof MutexError) {
4268
- exitWithError(e.message);
4269
- return;
4270
- }
4271
- if (e instanceof Error) {
4272
- exitWithError(e.message);
4273
- return;
4274
- }
4275
- throw e;
4276
- }
4277
- if (flags.validate !== false) {
4278
- const result2 = runValidation(payload);
4279
- if (!result2.valid) {
4280
- process.stderr.write(
4281
- `${pc6.red("\u2717")} Pre-validation failed (${result2.totalErrors} errors). Use --no-validate to skip.
4282
- `
4283
- );
4284
- process.exit(2);
4285
- }
4286
- }
4287
- if (flags.prod && !flags.yes) {
4288
- const confirmed = await confirmInteractive({
4289
- prompt: pc6.yellow("\u26A0 About to send a REAL invoice on the Peppol network. Continue?"),
4290
- defaultYes: false
4291
- });
4292
- if (!confirmed) {
4293
- if (!flags.quiet) process.stderr.write("Cancelled.\n");
4294
- process.exit(0);
4295
- }
4296
- }
4297
- const baseUrl = flags.local ? LOCAL_BASE : API_BASE;
4298
- const client = new Peppol({ apiKey: auth.apiKey, baseUrl });
4299
- let result;
4300
- try {
4301
- result = await client.invoices.send(payload);
4302
- } catch (e) {
4303
- const msg = e instanceof Error ? e.message : String(e);
4304
- process.stderr.write(`${pc6.red("\u2717")} ${msg}
4305
- `);
4306
- process.exit(1);
4307
- }
4308
- const dashboardUrl = `${DASHBOARD_BASE}/${result.id}`;
4309
- let finalStatus = result.status;
4310
- let timedOut = false;
4311
- let watchFailed = false;
4312
- if (flags.watch) {
4313
- const onTransition = (s) => {
4314
- if (!flags.quiet && !flags.json) {
4315
- process.stderr.write(` ${pc6.cyan("\u2192")} ${s}
4316
- `);
4317
- }
4318
- };
4319
- try {
4320
- const w = await pollUntilTerminal(client, result.id, {
4321
- intervalMs: 2e3,
4322
- timeoutMs: 6e4,
4323
- onTransition
4324
- });
4325
- finalStatus = w.finalStatus;
4326
- timedOut = w.timedOut;
4327
- } catch (e) {
4328
- const msg = e instanceof Error ? e.message : String(e);
4329
- process.stderr.write(`${pc6.yellow("\u26A0")} Watch error: ${msg}
4330
- `);
4331
- watchFailed = true;
4332
- }
4333
- if (timedOut) {
4334
- process.stderr.write(
4335
- `${pc6.yellow("\u26A0")} Timeout \u2014 invoice was sent but delivery not confirmed in 60s.
4336
- `
4337
- );
4338
- }
4339
- }
4340
- const mode = flags.quiet ? "quiet" : flags.json ? "json" : "formatted";
4341
- const output = formatSendResult(
4342
- {
4343
- id: result.id,
4344
- number: payload.number,
4345
- status: finalStatus,
4346
- warnings: result.warnings,
4347
- dashboardUrl
4348
- },
4349
- mode
4350
- );
4351
- if (output) process.stdout.write(output + "\n");
4352
- if (watchFailed || finalStatus === "rejected" || finalStatus === "failed" || finalStatus === "no_action") {
4353
- process.exit(1);
4354
- }
4355
- process.exit(0);
4356
- });
4357
- }
4358
-
4359
- // src/commands/login.ts
4360
- import { createInterface as createInterface2 } from "readline";
4361
- import pc7 from "picocolors";
4362
- async function promptMaskedKey(envLabel) {
4363
- if (process.stdin.isTTY !== true) {
4364
- exitWithError("Error: --key flag required when stdin is not a TTY (CI mode).");
4365
- }
4366
- process.stdout.write(`Paste your ${envLabel} API key (input hidden): `);
4367
- return new Promise((resolve4) => {
4368
- let buffer = "";
4369
- const onData = (chunk) => {
4370
- const c = chunk.toString("utf-8");
4371
- if (c === "\n" || c === "\r" || c === "\r\n") {
4372
- process.stdin.setRawMode(false);
4373
- process.stdin.removeListener("data", onData);
4374
- process.stdin.pause();
4375
- process.stdout.write("\n");
4376
- resolve4(buffer);
4377
- return;
4378
- }
4379
- if (c === "") {
4380
- process.stdin.setRawMode(false);
4381
- process.stdin.removeListener("data", onData);
4382
- process.stdin.pause();
4383
- process.stdout.write("\n");
4384
- process.exit(130);
4385
- }
4386
- if (c === "\x7F" || c === "\b") {
4387
- buffer = buffer.slice(0, -1);
4388
- return;
4389
- }
4390
- buffer += c;
4391
- };
4392
- process.stdin.setRawMode(true);
4393
- process.stdin.resume();
4394
- process.stdin.on("data", onData);
4395
- });
4396
- }
4397
- async function promptEnvironment() {
4398
- if (process.stdin.isTTY !== true) return "sandbox";
4399
- return new Promise((resolve4) => {
4400
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
4401
- rl.question("Environment? (s)andbox / (l)ive [sandbox]: ", (answer) => {
4402
- rl.close();
4403
- const a = answer.trim().toLowerCase();
4404
- if (a === "l" || a === "live") return resolve4("live");
4405
- return resolve4("sandbox");
4406
- });
4407
- });
4408
- }
4409
- function registerLoginCommand(program2) {
4410
- program2.command("login").description("Save a getpeppr API key to ~/.config/getpeppr/credentials.json").option("--key <key>", "API key \u2014 for CI/scripted use only; visible in `ps` and shell history. Prefer the interactive prompt or GETPEPPR_API_KEY env var.").option("--sandbox", "store as sandbox key (default)").option("--live", "store as live (production) key").action(async (flags) => {
4411
- if (!flags.live && !flags.sandbox && process.stdin.isTTY !== true) {
4412
- exitWithError("Error: --sandbox or --live required when stdin is not a TTY (CI mode).");
4413
- }
4414
- let env;
4415
- if (flags.live) env = "live";
4416
- else if (flags.sandbox) env = "sandbox";
4417
- else env = await promptEnvironment();
4418
- let key;
4419
- if (flags.key) {
4420
- key = flags.key;
4421
- } else {
4422
- key = (await promptMaskedKey(env)).trim();
4423
- if (!key) exitWithError("Error: empty API key.");
4424
- }
4425
- const existing = readCredentials() ?? {};
4426
- const next = { ...existing, [env]: key };
4427
- writeCredentials(next);
4428
- const path = getCredentialsPath();
4429
- process.stderr.write(
4430
- `${pc7.green("\u2713")} Saved ${env} key to ${path} (mode 600)
4431
- `
4432
- );
4433
- });
4434
- }
4435
-
4436
- // src/commands/logout.ts
4437
- import pc8 from "picocolors";
4438
- function registerLogoutCommand(program2) {
4439
- program2.command("logout").description("Remove ~/.config/getpeppr/credentials.json").action(() => {
4440
- const path = getCredentialsPath();
4441
- const removed = deleteCredentials();
4442
- if (removed) {
4443
- process.stderr.write(`${pc8.green("\u2713")} Removed ${path}
4444
- `);
4445
- } else {
4446
- process.stderr.write(`No credentials to remove (${path})
4447
- `);
4448
- }
4449
- process.exit(0);
4450
- });
4451
- }
4452
-
4453
- // src/index.ts
4454
- var require2 = createRequire(import.meta.url);
4455
- var { version } = require2("../package.json");
4456
- var program = new Command();
4457
- program.name("getpeppr").description("CLI tool for Peppol e-invoice validation and development").version(version);
4458
- registerValidateCommand(program);
4459
- registerInitCommand(program);
4460
- registerConvertCommand(program);
4461
- registerLookupCommand(program);
4462
- registerSendCommand(program);
4463
- registerLoginCommand(program);
4464
- registerLogoutCommand(program);
4465
- program.parse();
4466
- //# sourceMappingURL=index.js.map