@getpeppr/cli 0.2.1 → 0.3.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 CHANGED
@@ -7,6 +7,14 @@ import { Command } from "commander";
7
7
  // src/utils/file.ts
8
8
  import { readFileSync, existsSync } from "fs";
9
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
10
18
  function readJsonFile(filePath) {
11
19
  const resolved = resolve(filePath);
12
20
  if (!existsSync(resolved)) {
@@ -18,104 +26,2619 @@ function readJsonFile(filePath) {
18
26
  } catch {
19
27
  return { ok: false, error: `Error: could not read file \u2014 ${resolved}` };
20
28
  }
21
- try {
22
- const data = JSON.parse(content);
23
- return { ok: true, data };
24
- } catch {
25
- return { ok: false, error: `Error: invalid JSON in file \u2014 ${resolved}` };
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 lineTax = round2(amount * (vatRate / 100));
422
+ const existing = groups.get(key);
423
+ if (existing) {
424
+ existing.taxableAmount = round2(existing.taxableAmount + amount);
425
+ existing.taxAmount = round2(existing.taxAmount + lineTax);
426
+ } else {
427
+ groups.set(key, {
428
+ vatRate,
429
+ vatCategory,
430
+ taxableAmount: amount,
431
+ taxAmount: lineTax
432
+ });
433
+ }
434
+ }
435
+ for (const line of lines) {
436
+ addToGroup(line.vatCategory ?? "S", line.vatRate, calculateLineExtensionAmount(line));
437
+ }
438
+ for (const a of allowances ?? []) {
439
+ addToGroup(a.vatCategory ?? "S", a.vatRate, -a.amount);
440
+ }
441
+ for (const c of charges ?? []) {
442
+ addToGroup(c.vatCategory ?? "S", c.vatRate, c.amount);
443
+ }
444
+ return Array.from(groups.values());
445
+ }
446
+ function calculateDocumentTotals(lines, allowances, charges) {
447
+ const taxSubtotals = calculateTaxSubtotals(lines, allowances, charges);
448
+ const lineExtensionAmount = lines.reduce((sum, line) => sum + calculateLineExtensionAmount(line), 0);
449
+ const allowanceTotalAmount = (allowances ?? []).reduce((sum, a) => sum + a.amount, 0);
450
+ const chargeTotalAmount = (charges ?? []).reduce((sum, c) => sum + c.amount, 0);
451
+ const taxExclusiveAmount = round2(lineExtensionAmount - allowanceTotalAmount + chargeTotalAmount);
452
+ const totalTax = round2(taxSubtotals.reduce((sum, st) => sum + st.taxAmount, 0));
453
+ const taxInclusiveAmount = round2(taxExclusiveAmount + totalTax);
454
+ return {
455
+ lineExtensionAmount,
456
+ allowanceTotalAmount,
457
+ chargeTotalAmount,
458
+ taxExclusiveAmount,
459
+ totalTax,
460
+ taxInclusiveAmount,
461
+ payableAmount: taxInclusiveAmount,
462
+ taxSubtotals
463
+ };
464
+ }
465
+ function buildTaxTotalXml(taxSubtotals, totalTax, currency) {
466
+ const subtotalsXml = taxSubtotals.map((st) => `
467
+ <cac:TaxSubtotal>
468
+ <cbc:TaxableAmount currencyID="${escapeXml(currency)}">${formatAmount(st.taxableAmount)}</cbc:TaxableAmount>
469
+ <cbc:TaxAmount currencyID="${escapeXml(currency)}">${formatAmount(st.taxAmount)}</cbc:TaxAmount>
470
+ <cac:TaxCategory>
471
+ <cbc:ID>${st.vatCategory}</cbc:ID>
472
+ <cbc:Percent>${st.vatRate}</cbc:Percent>
473
+ <cac:TaxScheme>
474
+ <cbc:ID>VAT</cbc:ID>
475
+ </cac:TaxScheme>
476
+ </cac:TaxCategory>
477
+ </cac:TaxSubtotal>`).join("");
478
+ return `<cac:TaxTotal>
479
+ <cbc:TaxAmount currencyID="${escapeXml(currency)}">${formatAmount(totalTax)}</cbc:TaxAmount>
480
+ ${subtotalsXml}
481
+ </cac:TaxTotal>`;
482
+ }
483
+ function buildTaxCurrencyTotalXml(totalTax, taxCurrency, rate) {
484
+ const convertedAmount = Math.round(totalTax * rate * 100) / 100;
485
+ return `<cac:TaxTotal>
486
+ <cbc:TaxAmount currencyID="${escapeXml(taxCurrency)}">${formatAmount(convertedAmount)}</cbc:TaxAmount>
487
+ </cac:TaxTotal>`;
488
+ }
489
+ function buildLegalMonetaryTotalXml(totals, currency, options) {
490
+ const prepaid = options?.prepaidAmount;
491
+ const rounding = options?.roundingAmount;
492
+ const payableAmount = totals.taxInclusiveAmount - (prepaid ?? 0) + (rounding ?? 0);
493
+ return `<cac:LegalMonetaryTotal>
494
+ <cbc:LineExtensionAmount currencyID="${escapeXml(currency)}">${formatAmount(totals.lineExtensionAmount)}</cbc:LineExtensionAmount>
495
+ <cbc:TaxExclusiveAmount currencyID="${escapeXml(currency)}">${formatAmount(totals.taxExclusiveAmount)}</cbc:TaxExclusiveAmount>
496
+ <cbc:TaxInclusiveAmount currencyID="${escapeXml(currency)}">${formatAmount(totals.taxInclusiveAmount)}</cbc:TaxInclusiveAmount>
497
+ ${totals.allowanceTotalAmount > 0 ? `<cbc:AllowanceTotalAmount currencyID="${escapeXml(currency)}">${formatAmount(totals.allowanceTotalAmount)}</cbc:AllowanceTotalAmount>` : ""}
498
+ ${totals.chargeTotalAmount > 0 ? `<cbc:ChargeTotalAmount currencyID="${escapeXml(currency)}">${formatAmount(totals.chargeTotalAmount)}</cbc:ChargeTotalAmount>` : ""}
499
+ ${prepaid != null ? `<cbc:PrepaidAmount currencyID="${escapeXml(currency)}">${formatAmount(prepaid)}</cbc:PrepaidAmount>` : ""}
500
+ ${rounding != null ? `<cbc:PayableRoundingAmount currencyID="${escapeXml(currency)}">${formatAmount(rounding)}</cbc:PayableRoundingAmount>` : ""}
501
+ <cbc:PayableAmount currencyID="${escapeXml(currency)}">${formatAmount(payableAmount)}</cbc:PayableAmount>
502
+ </cac:LegalMonetaryTotal>`;
503
+ }
504
+ function buildPaymentMeansXml(input) {
505
+ const paymentMeans = input.paymentMeans ?? DEFAULT_PAYMENT_MEANS;
506
+ return `<cac:PaymentMeans>
507
+ <cbc:PaymentMeansCode>${paymentMeans}</cbc:PaymentMeansCode>
508
+ ${input.paymentReference ? `<cbc:PaymentID>${escapeXml(input.paymentReference)}</cbc:PaymentID>` : ""}
509
+ ${input.paymentIban ? `<cac:PayeeFinancialAccount>
510
+ <cbc:ID>${escapeXml(input.paymentIban)}</cbc:ID>
511
+ ${input.paymentBic ? `<cac:FinancialInstitutionBranch>
512
+ <cbc:ID>${escapeXml(input.paymentBic)}</cbc:ID>
513
+ </cac:FinancialInstitutionBranch>` : ""}
514
+ </cac:PayeeFinancialAccount>` : ""}
515
+ </cac:PaymentMeans>`;
516
+ }
517
+ function buildCreditNoteLineXml(line, index, currency) {
518
+ return buildDocumentLineXml(line, index, currency, "CreditNoteLine", "CreditedQuantity");
519
+ }
520
+ function buildOrderReferenceXml(orderReference, salesOrderReference) {
521
+ if (!orderReference && !salesOrderReference)
522
+ return "";
523
+ const parts = ["<cac:OrderReference>"];
524
+ if (orderReference) {
525
+ parts.push(`<cbc:ID>${escapeXml(orderReference)}</cbc:ID>`);
526
+ }
527
+ if (salesOrderReference) {
528
+ parts.push(`<cbc:SalesOrderID>${escapeXml(salesOrderReference)}</cbc:SalesOrderID>`);
529
+ }
530
+ parts.push("</cac:OrderReference>");
531
+ return parts.join("");
532
+ }
533
+ function buildInvoiceXml(input) {
534
+ const currency = input.currency ?? "EUR";
535
+ const date = formatDate(input.date);
536
+ const dueDate = input.dueDate ? formatDate(input.dueDate) : void 0;
537
+ const hasTaxCurrency = input.taxCurrency && input.taxCurrency !== currency;
538
+ const totals = calculateDocumentTotals(input.lines, input.allowances, input.charges);
539
+ const linesXml = input.lines.map((line, i) => buildInvoiceLineXml(line, i, currency)).join("");
540
+ return `<?xml version="1.0" encoding="UTF-8"?>
541
+ <Invoice xmlns="${UBL_NS}"
542
+ xmlns:cac="${CAC_NS}"
543
+ xmlns:cbc="${CBC_NS}">
544
+ <cbc:CustomizationID>${PEPPOL_CUSTOMIZATION_ID}</cbc:CustomizationID>
545
+ <cbc:ProfileID>${PEPPOL_PROFILE_ID}</cbc:ProfileID>
546
+ <cbc:ID>${escapeXml(input.number)}</cbc:ID>
547
+ <cbc:IssueDate>${date}</cbc:IssueDate>
548
+ ${dueDate ? `<cbc:DueDate>${dueDate}</cbc:DueDate>` : ""}
549
+ ${input.taxPointDate ? `<cbc:TaxPointDate>${formatDate(input.taxPointDate)}</cbc:TaxPointDate>` : ""}
550
+ <cbc:InvoiceTypeCode>${input.invoiceTypeCode ?? (input.isCreditNote ? 381 : 380)}</cbc:InvoiceTypeCode>
551
+ ${input.note ? `<cbc:Note>${escapeXml(input.note)}</cbc:Note>` : ""}
552
+ ${input.accountingCost ? `<cbc:AccountingCost>${escapeXml(input.accountingCost)}</cbc:AccountingCost>` : ""}
553
+ <cbc:DocumentCurrencyCode>${escapeXml(currency)}</cbc:DocumentCurrencyCode>
554
+ ${hasTaxCurrency ? `<cbc:TaxCurrencyCode>${escapeXml(input.taxCurrency)}</cbc:TaxCurrencyCode>` : ""}
555
+ ${input.buyerReference ? `<cbc:BuyerReference>${escapeXml(input.buyerReference)}</cbc:BuyerReference>` : ""}
556
+ ${input.invoicePeriod ? buildInvoicePeriodXml(input.invoicePeriod) : ""}
557
+ ${buildOrderReferenceXml(input.orderReference, input.salesOrderReference)}
558
+ ${input.despatchReference ? `<cac:DespatchDocumentReference><cbc:ID>${escapeXml(input.despatchReference)}</cbc:ID></cac:DespatchDocumentReference>` : ""}
559
+ ${input.receiptReference ? `<cac:ReceiptDocumentReference><cbc:ID>${escapeXml(input.receiptReference)}</cbc:ID></cac:ReceiptDocumentReference>` : ""}
560
+ ${input.contractReference ? `<cac:ContractDocumentReference><cbc:ID>${escapeXml(input.contractReference)}</cbc:ID></cac:ContractDocumentReference>` : ""}
561
+ ${(input.attachments ?? []).map((a) => buildAttachmentXml(a)).join("\n ")}
562
+ ${input.projectReference ? `<cac:ProjectReference><cbc:ID>${escapeXml(input.projectReference)}</cbc:ID></cac:ProjectReference>` : ""}
563
+ ${input.from ? buildPartyXml(input.from, "AccountingSupplierParty") : ""}
564
+ ${buildPartyXml(input.to, "AccountingCustomerParty")}
565
+ ${input.payeeParty ? buildPayeePartyXml(input.payeeParty) : ""}
566
+ ${input.taxRepresentative ? buildTaxRepresentativePartyXml(input.taxRepresentative) : ""}
567
+ ${input.delivery ? buildDeliveryXml(input.delivery) : ""}
568
+ ${buildPaymentMeansXml(input)}
569
+ ${input.paymentTerms ? `<cac:PaymentTerms>
570
+ <cbc:Note>${escapeXml(input.paymentTerms)}</cbc:Note>
571
+ </cac:PaymentTerms>` : ""}
572
+ ${(input.allowances ?? []).map((a) => buildDocumentAllowanceChargeXml(a, false, currency)).join("")}
573
+ ${(input.charges ?? []).map((c) => buildDocumentAllowanceChargeXml(c, true, currency)).join("")}
574
+ ${hasTaxCurrency && input.taxCurrencyRate ? buildTaxCurrencyTotalXml(totals.totalTax, input.taxCurrency, input.taxCurrencyRate) : ""}
575
+ ${buildTaxTotalXml(totals.taxSubtotals, totals.totalTax, currency)}
576
+ ${buildLegalMonetaryTotalXml(totals, currency, { prepaidAmount: input.prepaidAmount, roundingAmount: input.roundingAmount })}
577
+ ${linesXml}
578
+ </Invoice>`;
579
+ }
580
+ function buildCreditNoteXml(input) {
581
+ const currency = input.currency ?? "EUR";
582
+ const date = formatDate(input.date);
583
+ const dueDate = input.dueDate ? formatDate(input.dueDate) : void 0;
584
+ const hasTaxCurrency = input.taxCurrency && input.taxCurrency !== currency;
585
+ const totals = calculateDocumentTotals(input.lines, input.allowances, input.charges);
586
+ const linesXml = input.lines.map((line, i) => buildCreditNoteLineXml(line, i, currency)).join("");
587
+ return `<?xml version="1.0" encoding="UTF-8"?>
588
+ <CreditNote xmlns="${CREDIT_NOTE_NS}"
589
+ xmlns:cac="${CAC_NS}"
590
+ xmlns:cbc="${CBC_NS}">
591
+ <cbc:CustomizationID>${PEPPOL_CUSTOMIZATION_ID}</cbc:CustomizationID>
592
+ <cbc:ProfileID>${PEPPOL_PROFILE_ID}</cbc:ProfileID>
593
+ <cbc:ID>${escapeXml(input.number)}</cbc:ID>
594
+ <cbc:IssueDate>${date}</cbc:IssueDate>
595
+ ${dueDate ? `<cbc:DueDate>${dueDate}</cbc:DueDate>` : ""}
596
+ ${input.taxPointDate ? `<cbc:TaxPointDate>${formatDate(input.taxPointDate)}</cbc:TaxPointDate>` : ""}
597
+ <cbc:CreditNoteTypeCode>${input.invoiceTypeCode ?? 381}</cbc:CreditNoteTypeCode>
598
+ ${input.note ? `<cbc:Note>${escapeXml(input.note)}</cbc:Note>` : ""}
599
+ ${input.accountingCost ? `<cbc:AccountingCost>${escapeXml(input.accountingCost)}</cbc:AccountingCost>` : ""}
600
+ <cbc:DocumentCurrencyCode>${escapeXml(currency)}</cbc:DocumentCurrencyCode>
601
+ ${hasTaxCurrency ? `<cbc:TaxCurrencyCode>${escapeXml(input.taxCurrency)}</cbc:TaxCurrencyCode>` : ""}
602
+ ${input.buyerReference ? `<cbc:BuyerReference>${escapeXml(input.buyerReference)}</cbc:BuyerReference>` : ""}
603
+ ${input.invoicePeriod ? buildInvoicePeriodXml(input.invoicePeriod) : ""}
604
+ ${buildOrderReferenceXml(input.orderReference, input.salesOrderReference)}
605
+ <cac:BillingReference><cac:InvoiceDocumentReference><cbc:ID>${escapeXml(input.invoiceReference)}</cbc:ID></cac:InvoiceDocumentReference></cac:BillingReference>
606
+ ${input.despatchReference ? `<cac:DespatchDocumentReference><cbc:ID>${escapeXml(input.despatchReference)}</cbc:ID></cac:DespatchDocumentReference>` : ""}
607
+ ${input.receiptReference ? `<cac:ReceiptDocumentReference><cbc:ID>${escapeXml(input.receiptReference)}</cbc:ID></cac:ReceiptDocumentReference>` : ""}
608
+ ${input.contractReference ? `<cac:ContractDocumentReference><cbc:ID>${escapeXml(input.contractReference)}</cbc:ID></cac:ContractDocumentReference>` : ""}
609
+ ${(input.attachments ?? []).map((a) => buildAttachmentXml(a)).join("\n ")}
610
+ ${input.projectReference ? `<cac:ProjectReference><cbc:ID>${escapeXml(input.projectReference)}</cbc:ID></cac:ProjectReference>` : ""}
611
+ ${input.from ? buildPartyXml(input.from, "AccountingSupplierParty") : ""}
612
+ ${buildPartyXml(input.to, "AccountingCustomerParty")}
613
+ ${input.payeeParty ? buildPayeePartyXml(input.payeeParty) : ""}
614
+ ${input.taxRepresentative ? buildTaxRepresentativePartyXml(input.taxRepresentative) : ""}
615
+ ${input.delivery ? buildDeliveryXml(input.delivery) : ""}
616
+ ${buildPaymentMeansXml(input)}
617
+ ${input.paymentTerms ? `<cac:PaymentTerms>
618
+ <cbc:Note>${escapeXml(input.paymentTerms)}</cbc:Note>
619
+ </cac:PaymentTerms>` : ""}
620
+ ${(input.allowances ?? []).map((a) => buildDocumentAllowanceChargeXml(a, false, currency)).join("")}
621
+ ${(input.charges ?? []).map((c) => buildDocumentAllowanceChargeXml(c, true, currency)).join("")}
622
+ ${hasTaxCurrency && input.taxCurrencyRate ? buildTaxCurrencyTotalXml(totals.totalTax, input.taxCurrency, input.taxCurrencyRate) : ""}
623
+ ${buildTaxTotalXml(totals.taxSubtotals, totals.totalTax, currency)}
624
+ ${buildLegalMonetaryTotalXml(totals, currency, { prepaidAmount: input.prepaidAmount, roundingAmount: input.roundingAmount })}
625
+ ${linesXml}
626
+ </CreditNote>`;
627
+ }
628
+
629
+ // ../sdk/dist/core/country-rules.js
630
+ function warn(field, message, ruleId) {
631
+ return { field, message, ruleId };
632
+ }
633
+ var BE_STRUCTURED_RE = /^\+{3}\d{3}\/\d{4}\/\d{5}\+{3}$/;
634
+ function validateBelgianCheckDigit(reference) {
635
+ const digits = reference.replace(/[^0-9]/g, "");
636
+ if (digits.length !== 12)
637
+ return false;
638
+ const base = parseInt(digits.slice(0, 10), 10);
639
+ const check = parseInt(digits.slice(10, 12), 10);
640
+ const expected = base % 97 === 0 ? 97 : base % 97;
641
+ return check === expected;
642
+ }
643
+ function validateBelgium(input, _errors, warnings) {
644
+ const ref = input.paymentReference;
645
+ if (ref && BE_STRUCTURED_RE.test(ref)) {
646
+ if (!validateBelgianCheckDigit(ref)) {
647
+ warnings.push(warn("paymentReference", `Belgian structured communication "${ref}" has an invalid mod-97 checksum. Verify the reference.`, "BE-01"));
648
+ }
649
+ } else if (!ref) {
650
+ warnings.push(warn("paymentReference", "Belgian recipients typically expect a structured communication reference (+++NNN/NNNN/NNNNN+++ format).", "BE-02"));
651
+ }
652
+ }
653
+ function validateBelgiumSeller(input, _errors, warnings) {
654
+ const ref = input.paymentReference;
655
+ if (ref && BE_STRUCTURED_RE.test(ref)) {
656
+ if (!validateBelgianCheckDigit(ref)) {
657
+ warnings.push(warn("paymentReference", `Belgian structured communication "${ref}" has an invalid mod-97 checksum. Verify the reference.`, "BE-01"));
658
+ }
659
+ } else if (!ref) {
660
+ warnings.push(warn("paymentReference", "Belgian sellers typically include a structured communication reference (+++NNN/NNNN/NNNNN+++ format).", "BE-02"));
661
+ }
662
+ }
663
+ var FR_SIRET_RE = /^\d{14}$/;
664
+ var FR_VAT_RE = /^FR[A-Z0-9]{2}\d{9}$/;
665
+ function validateFrance(input, _errors, warnings) {
666
+ const { companyId, vatNumber } = input.to ?? {};
667
+ if (companyId && !FR_SIRET_RE.test(companyId)) {
668
+ warnings.push(warn("to.companyId", `French company ID (SIRET) should be exactly 14 digits, got "${companyId}".`, "FR-01"));
669
+ }
670
+ if (vatNumber && !FR_VAT_RE.test(vatNumber)) {
671
+ warnings.push(warn("to.vatNumber", `French VAT number should match format FR + 2 characters + 9 digits (SIREN), got "${vatNumber}".`, "FR-02"));
672
+ }
673
+ }
674
+ function validateItaly(input, _errors, warnings) {
675
+ if (!input.buyerReference) {
676
+ warnings.push(warn("buyerReference", "Italian recipients (SDI) typically require a buyer reference (CIG/CUP code). Consider setting buyerReference.", "IT-01"));
677
+ }
678
+ const peppolId = input.to?.peppolId;
679
+ if (peppolId?.startsWith("0201:")) {
680
+ const fiscalCode = peppolId.slice(5);
681
+ if (fiscalCode.length !== 11 && fiscalCode.length !== 16) {
682
+ 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"));
683
+ }
684
+ }
685
+ }
686
+ var NL_KVK_RE = /^\d{8}$/;
687
+ var NL_VAT_RE = /^NL\d{9}B\d{2}$/;
688
+ function validateNetherlands(input, _errors, warnings) {
689
+ const { companyId, vatNumber } = input.to ?? {};
690
+ if (companyId && !NL_KVK_RE.test(companyId)) {
691
+ warnings.push(warn("to.companyId", `Dutch KVK number should be exactly 8 digits, got "${companyId}".`, "NL-01"));
692
+ }
693
+ if (vatNumber && !NL_VAT_RE.test(vatNumber)) {
694
+ warnings.push(warn("to.vatNumber", `Dutch VAT number should match format NL + 9 digits + B + 2 digits, got "${vatNumber}".`, "NL-02"));
695
+ }
696
+ }
697
+ var DE_VAT_RE = /^DE\d{9}$/;
698
+ function validateGermany(input, _errors, warnings) {
699
+ const { vatNumber } = input.to ?? {};
700
+ if (vatNumber && !DE_VAT_RE.test(vatNumber)) {
701
+ warnings.push(warn("to.vatNumber", `German VAT number should match format DE + 9 digits, got "${vatNumber}".`, "DE-01"));
702
+ }
703
+ }
704
+ function validateCountryRules(input) {
705
+ const errors = [];
706
+ const warnings = [];
707
+ const buyerCountry = input.to?.country;
708
+ const sellerCountry = input.from?.country;
709
+ if (buyerCountry) {
710
+ switch (buyerCountry) {
711
+ case "BE":
712
+ validateBelgium(input, errors, warnings);
713
+ break;
714
+ case "FR":
715
+ validateFrance(input, errors, warnings);
716
+ break;
717
+ case "IT":
718
+ validateItaly(input, errors, warnings);
719
+ break;
720
+ case "NL":
721
+ validateNetherlands(input, errors, warnings);
722
+ break;
723
+ case "DE":
724
+ validateGermany(input, errors, warnings);
725
+ break;
726
+ }
727
+ }
728
+ if (sellerCountry && sellerCountry !== buyerCountry) {
729
+ switch (sellerCountry) {
730
+ case "BE":
731
+ validateBelgiumSeller(input, errors, warnings);
732
+ break;
733
+ }
734
+ }
735
+ return { errors, warnings };
736
+ }
737
+
738
+ // ../sdk/dist/core/validator.js
739
+ function error(field, message, ruleId, suggestion) {
740
+ return { field, message, ruleId, suggestion };
741
+ }
742
+ function warning(field, message, ruleId) {
743
+ return { field, message, ruleId };
744
+ }
745
+ var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
746
+ function validateParty(party, path) {
747
+ const errors = [];
748
+ if (!party.name?.trim()) {
749
+ errors.push(error(`${path}.name`, "Business name is required", "BR-06"));
750
+ }
751
+ if (!party.peppolId) {
752
+ errors.push(error(`${path}.peppolId`, "Peppol participant ID is required", void 0, 'Format: "scheme:id", e.g. "0208:BE0123456789" for Belgian companies'));
753
+ } else if (!party.peppolId.includes(":")) {
754
+ 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)'));
755
+ }
756
+ if (!party.country) {
757
+ errors.push(error(`${path}.country`, "Country code is required", "BR-11"));
758
+ } else if (party.country.length !== 2) {
759
+ 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)"));
760
+ }
761
+ return errors;
762
+ }
763
+ function validateBuyerAddress(party, path) {
764
+ const errors = [];
765
+ if (!party.street?.trim()) {
766
+ errors.push(error(`${path}.street`, "Street address is required for the buyer", "BR-50", 'e.g. "123 Business Street"'));
767
+ }
768
+ if (!party.city?.trim()) {
769
+ errors.push(error(`${path}.city`, "City is required for the buyer", "BR-51", 'e.g. "Brussels"'));
770
+ }
771
+ if (!party.postalCode?.trim()) {
772
+ errors.push(error(`${path}.postalCode`, "Postal code is required for the buyer", "BR-53", 'e.g. "1000"'));
773
+ }
774
+ return errors;
775
+ }
776
+ function validateLine(line, index, isCreditNote = false) {
777
+ const errors = [];
778
+ const path = `lines[${index}]`;
779
+ if (!line.description?.trim()) {
780
+ errors.push(error(`${path}.description`, "Line item description is required", "BR-25"));
781
+ }
782
+ if (line.quantity === void 0 || line.quantity === null) {
783
+ errors.push(error(`${path}.quantity`, "Quantity is required", "BR-22"));
784
+ } else if (line.quantity <= 0 && !isCreditNote) {
785
+ errors.push(error(`${path}.quantity`, `Quantity must be positive, got ${line.quantity}`, void 0, "For returns/credits, use a credit note instead"));
786
+ }
787
+ if (line.unitPrice === void 0 || line.unitPrice === null) {
788
+ errors.push(error(`${path}.unitPrice`, "Unit price is required", "BR-26"));
789
+ } else if (line.unitPrice < 0) {
790
+ 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"));
791
+ }
792
+ if (line.vatRate === void 0 || line.vatRate === null) {
793
+ errors.push(error(`${path}.vatRate`, "VAT rate is required", "BR-CO-17"));
794
+ } else if (line.vatRate < 0 || line.vatRate > 100) {
795
+ 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."));
796
+ }
797
+ return errors;
798
+ }
799
+ function validateInvoice(input) {
800
+ const errors = [];
801
+ const warnings = [];
802
+ if (!input.number?.trim()) {
803
+ errors.push(error("number", "Invoice number is required", "BR-02", "Must be unique per supplier"));
804
+ }
805
+ const VALID_TYPE_CODES = [380, 381, 383, 384, 386, 389, 751];
806
+ if (input.invoiceTypeCode != null && !VALID_TYPE_CODES.includes(input.invoiceTypeCode)) {
807
+ errors.push(error("invoiceTypeCode", `Invalid invoice type code: ${input.invoiceTypeCode}`, void 0, "Valid codes: 380, 381, 383, 384, 386, 389, 751"));
808
+ }
809
+ if (input.isCreditNote && !input.invoiceReference?.trim()) {
810
+ 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")'));
811
+ }
812
+ if (input.from) {
813
+ warnings.push(warning("from", "Seller info is determined by your API key. The 'from' field is deprecated and ignored."));
814
+ }
815
+ if (!input.to) {
816
+ errors.push(error("to", "Buyer (to) is required", "BR-07"));
817
+ } else {
818
+ errors.push(...validateParty(input.to, "to"));
819
+ errors.push(...validateBuyerAddress(input.to, "to"));
820
+ }
821
+ if (input.payeeParty) {
822
+ if (!input.payeeParty.name?.trim()) {
823
+ errors.push(error("payeeParty.name", "Payee party name is required", "BR-17"));
824
+ }
825
+ if (!input.payeeParty.peppolId) {
826
+ errors.push(error("payeeParty.peppolId", "Payee party Peppol ID is required", void 0, 'Format: "scheme:id", e.g. "0208:BE0123456789"'));
827
+ } else if (!input.payeeParty.peppolId.includes(":")) {
828
+ errors.push(error("payeeParty.peppolId", `Invalid Peppol ID format: "${input.payeeParty.peppolId}"`, void 0, 'Must be "scheme:id" format'));
829
+ }
830
+ }
831
+ if (!input.lines || input.lines.length === 0) {
832
+ errors.push(error("lines", "At least one line item is required", "BR-16", "Add items to the lines array"));
833
+ } else {
834
+ for (let i = 0; i < input.lines.length; i++) {
835
+ errors.push(...validateLine(input.lines[i], i, input.isCreditNote));
836
+ }
837
+ }
838
+ if (input.date) {
839
+ if (!ISO_DATE_RE.test(input.date)) {
840
+ errors.push(error("date", `Invalid date format: "${input.date}"`, void 0, "Use ISO 8601: YYYY-MM-DD"));
841
+ }
842
+ }
843
+ if (input.dueDate) {
844
+ if (!ISO_DATE_RE.test(input.dueDate)) {
845
+ errors.push(error("dueDate", `Invalid due date format: "${input.dueDate}"`, void 0, "Use ISO 8601: YYYY-MM-DD"));
846
+ }
847
+ }
848
+ if (input.taxPointDate) {
849
+ if (!ISO_DATE_RE.test(input.taxPointDate)) {
850
+ errors.push(error("taxPointDate", `Invalid tax point date format: "${input.taxPointDate}"`, void 0, "Use ISO 8601: YYYY-MM-DD"));
851
+ }
852
+ }
853
+ if (input.roundingAmount !== void 0 && input.roundingAmount !== null) {
854
+ if (input.roundingAmount < -0.99 || input.roundingAmount > 0.99) {
855
+ errors.push(error("roundingAmount", `Rounding amount must be between -0.99 and 0.99, got ${input.roundingAmount}`, void 0, "B2BRouter stores rounding as integer cents (\xB199). Use values like 0.50 or -0.25."));
856
+ }
857
+ }
858
+ if (!input.buyerReference && !input.orderReference) {
859
+ warnings.push(warning("buyerReference", "Either buyerReference or orderReference is required by Peppol BIS 3.0 (BT-10).", "BR-10"));
860
+ }
861
+ if (input.taxCurrency && input.taxCurrency !== (input.currency ?? "EUR") && !input.taxCurrencyRate) {
862
+ 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"));
863
+ }
864
+ if (input.taxCurrency && input.taxCurrency === (input.currency ?? "EUR")) {
865
+ warnings.push(warning("taxCurrency", "Tax currency is the same as document currency \u2014 TaxCurrencyCode will be omitted"));
866
+ }
867
+ if (input.taxCurrencyRate !== void 0 && input.taxCurrencyRate <= 0) {
868
+ 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"));
869
+ }
870
+ if (!input.dueDate) {
871
+ warnings.push(warning("dueDate", "No due date specified. Recommended for payment terms.", "BR-09"));
872
+ }
873
+ if (!input.to?.vatNumber) {
874
+ warnings.push(warning("to.vatNumber", "Buyer VAT number not provided. May be required for B2B."));
875
+ }
876
+ if (input.paymentMeans === 30 && !input.paymentIban) {
877
+ warnings.push(warning("paymentIban", "Payment means is credit transfer but no IBAN provided. Buyer won't know where to pay."));
878
+ }
879
+ if (input.from?.peppolId && input.to?.peppolId && input.from.peppolId === input.to.peppolId) {
880
+ errors.push(error("to.peppolId", "Buyer and seller cannot have the same Peppol ID", void 0, "Check that 'from' and 'to' are different parties"));
881
+ }
882
+ const countryResult = validateCountryRules(input);
883
+ warnings.push(...countryResult.warnings);
884
+ return {
885
+ valid: errors.length === 0,
886
+ errors,
887
+ warnings
888
+ };
889
+ }
890
+
891
+ // ../sdk/dist/version.js
892
+ var SDK_VERSION = "1.3.0";
893
+
894
+ // ../sdk/dist/core/client.js
895
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
896
+ function sleep(ms) {
897
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
898
+ }
899
+ function calculateRetryDelay(attempt, initialDelayMs, maxDelayMs, retryAfterMs) {
900
+ if (retryAfterMs !== void 0)
901
+ return Math.min(retryAfterMs, maxDelayMs);
902
+ const exponentialDelay = initialDelayMs * Math.pow(2, attempt);
903
+ const jitter = Math.random() * initialDelayMs;
904
+ return Math.min(exponentialDelay + jitter, maxDelayMs);
905
+ }
906
+ function parseRetryAfter(headerValue) {
907
+ if (!headerValue)
908
+ return void 0;
909
+ const seconds = Number(headerValue);
910
+ if (Number.isFinite(seconds) && seconds >= 0) {
911
+ return seconds * 1e3;
912
+ }
913
+ const dateMs = Date.parse(headerValue);
914
+ if (!Number.isNaN(dateMs)) {
915
+ const delayMs = dateMs - Date.now();
916
+ return delayMs > 0 ? delayMs : 0;
917
+ }
918
+ return void 0;
919
+ }
920
+ function isRetryableError(error2) {
921
+ if (error2 instanceof PeppolApiError) {
922
+ return RETRYABLE_STATUS_CODES.has(error2.statusCode);
923
+ }
924
+ if (error2 instanceof Error && error2.name === "AbortError") {
925
+ return true;
926
+ }
927
+ if (error2 instanceof TypeError && /fetch failed|network|ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT/i.test(error2.message)) {
928
+ return true;
929
+ }
930
+ return false;
931
+ }
932
+ var DEFAULT_BASE_URL = "https://api.getpeppr.dev/v1";
933
+ var GetpepprAdapter = class {
934
+ name = "getpeppr";
935
+ baseUrl;
936
+ apiKey;
937
+ timeout;
938
+ retryConfig;
939
+ onRequest;
940
+ onResponse;
941
+ constructor(config) {
942
+ this.apiKey = config.apiKey;
943
+ this.timeout = config.timeout ?? 3e4;
944
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
945
+ this.retryConfig = {
946
+ maxRetries: config.retry?.maxRetries ?? 3,
947
+ initialDelayMs: config.retry?.initialDelayMs ?? 500,
948
+ maxDelayMs: config.retry?.maxDelayMs ?? 3e4
949
+ };
950
+ this.onRequest = config.onRequest;
951
+ this.onResponse = config.onResponse;
952
+ }
953
+ async request(method, path, body, extraHeaders) {
954
+ const { maxRetries, initialDelayMs, maxDelayMs } = this.retryConfig;
955
+ let lastError;
956
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
957
+ try {
958
+ return await this.doRequest(method, path, body, extraHeaders);
959
+ } catch (err) {
960
+ lastError = err;
961
+ const is429 = err instanceof PeppolApiError && err.statusCode === 429;
962
+ const isSafeMethod = /^(GET|DELETE|HEAD)$/i.test(method);
963
+ const hasIdempotencyKey = !!extraHeaders?.["Idempotency-Key"];
964
+ const canRetry = is429 || isSafeMethod || hasIdempotencyKey;
965
+ if (attempt < maxRetries && canRetry && isRetryableError(err)) {
966
+ const retryAfterMs = err instanceof PeppolApiError ? err.retryAfterMs : void 0;
967
+ await sleep(calculateRetryDelay(attempt, initialDelayMs, maxDelayMs, retryAfterMs));
968
+ continue;
969
+ }
970
+ throw err;
971
+ }
972
+ }
973
+ throw lastError;
974
+ }
975
+ async doRequest(method, path, body, extraHeaders) {
976
+ const url = `${this.baseUrl}${path}`;
977
+ const controller = new AbortController();
978
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
979
+ const requestHeaders = {
980
+ Authorization: `Bearer ${this.apiKey}`,
981
+ "Content-Type": "application/json",
982
+ Accept: "application/json",
983
+ "User-Agent": `getpeppr-sdk/${SDK_VERSION}`,
984
+ ...extraHeaders
985
+ };
986
+ const startTime = Date.now();
987
+ if (this.onRequest) {
988
+ try {
989
+ this.onRequest({
990
+ method,
991
+ url,
992
+ headers: { ...requestHeaders },
993
+ body,
994
+ timestamp: startTime
995
+ });
996
+ } catch {
997
+ }
998
+ }
999
+ try {
1000
+ const response = await fetch(url, {
1001
+ method,
1002
+ headers: requestHeaders,
1003
+ body: body ? JSON.stringify(body) : void 0,
1004
+ signal: controller.signal
1005
+ });
1006
+ if (!response.ok) {
1007
+ const errorBody = await response.text().catch(() => "Unknown error");
1008
+ const retryAfterMs = response.status === 429 ? parseRetryAfter(response.headers.get("Retry-After")) : void 0;
1009
+ if (this.onResponse) {
1010
+ try {
1011
+ this.onResponse({
1012
+ status: response.status,
1013
+ headers: Object.fromEntries(response.headers.entries()),
1014
+ body: errorBody,
1015
+ durationMs: Date.now() - startTime,
1016
+ timestamp: Date.now()
1017
+ });
1018
+ } catch {
1019
+ }
1020
+ }
1021
+ throw new PeppolApiError(`getpeppr API error (${response.status}): ${errorBody}`, response.status, errorBody, retryAfterMs);
1022
+ }
1023
+ if (response.status === 204) {
1024
+ if (this.onResponse) {
1025
+ try {
1026
+ this.onResponse({
1027
+ status: response.status,
1028
+ headers: Object.fromEntries(response.headers.entries()),
1029
+ body: void 0,
1030
+ durationMs: Date.now() - startTime,
1031
+ timestamp: Date.now()
1032
+ });
1033
+ } catch {
1034
+ }
1035
+ }
1036
+ return void 0;
1037
+ }
1038
+ let responseBody;
1039
+ try {
1040
+ responseBody = await response.json();
1041
+ } catch {
1042
+ throw new PeppolApiError(`getpeppr API error: unexpected response format (status ${response.status})`, response.status, "Response body is not valid JSON");
1043
+ }
1044
+ if (this.onResponse) {
1045
+ try {
1046
+ this.onResponse({
1047
+ status: response.status,
1048
+ headers: Object.fromEntries(response.headers.entries()),
1049
+ body: responseBody,
1050
+ durationMs: Date.now() - startTime,
1051
+ timestamp: Date.now()
1052
+ });
1053
+ } catch {
1054
+ }
1055
+ }
1056
+ return responseBody;
1057
+ } finally {
1058
+ clearTimeout(timeoutId);
1059
+ }
1060
+ }
1061
+ // NOTE: sendInvoice and createInvoice hit the same SDK endpoint (POST /invoices).
1062
+ // The gateway (Tasks 9-10) differentiates them: sendInvoice wraps with
1063
+ // { invoice: {...}, send_after_import: true }, createInvoice omits the flag.
1064
+ async sendInvoice(input, options) {
1065
+ const headers = {};
1066
+ if (options?.idempotencyKey) {
1067
+ headers["Idempotency-Key"] = options.idempotencyKey;
1068
+ }
1069
+ if (options?.validateRecipient) {
1070
+ headers["X-Validate-Recipient"] = options.validateRecipient === true ? "warn" : String(options.validateRecipient);
1071
+ }
1072
+ const result = await this.request("POST", "/invoices", input, headers);
1073
+ return parseSendResult(result);
1074
+ }
1075
+ async createInvoice(input, options) {
1076
+ const headers = {};
1077
+ if (options?.idempotencyKey) {
1078
+ headers["Idempotency-Key"] = options.idempotencyKey;
1079
+ }
1080
+ if (options?.validateRecipient) {
1081
+ headers["X-Validate-Recipient"] = options.validateRecipient === true ? "warn" : String(options.validateRecipient);
1082
+ }
1083
+ const result = await this.request("POST", "/invoices", { ...input, _draft: true }, headers);
1084
+ return parseSendResult(result);
1085
+ }
1086
+ async sendInvoiceById(id) {
1087
+ await this.request("POST", `/invoices/send/${id}`);
1088
+ }
1089
+ async sendCreditNote(input) {
1090
+ const result = await this.request("POST", "/credit-notes", input);
1091
+ return parseSendResult(result);
1092
+ }
1093
+ async validateDocument(input) {
1094
+ return this.request("POST", "/validate", input);
1095
+ }
1096
+ async listInvoices(options) {
1097
+ const params = new URLSearchParams();
1098
+ if (options?.limit != null)
1099
+ params.set("limit", String(options.limit));
1100
+ if (options?.offset != null)
1101
+ params.set("offset", String(options.offset));
1102
+ if (options?.includeLines)
1103
+ params.set("include", "lines");
1104
+ const query = params.toString() ? `?${params.toString()}` : "";
1105
+ const result = await this.request("GET", `/invoices${query}`);
1106
+ const invoices = result.invoices ?? result.data ?? [];
1107
+ const meta = result.meta;
1108
+ return {
1109
+ data: invoices.map((inv) => ({
1110
+ id: String(inv.id ?? ""),
1111
+ number: String(inv.number ?? ""),
1112
+ status: mapStatus(String(inv.state ?? inv.status ?? "submitted")),
1113
+ createdAt: inv.created_at ? String(inv.created_at) : void 0
1114
+ })),
1115
+ meta: {
1116
+ totalCount: Number(meta?.total_count ?? invoices.length),
1117
+ offset: Number(meta?.offset ?? options?.offset ?? 0),
1118
+ limit: Number(meta?.limit ?? options?.limit ?? 25),
1119
+ hasMore: meta ? Number(meta.total_count) > Number(meta.offset) + Number(meta.limit) : false,
1120
+ truncated: Boolean(meta?.truncated ?? false)
1121
+ }
1122
+ };
1123
+ }
1124
+ async getStatus(documentId) {
1125
+ const result = await this.request("GET", `/invoices/${documentId}`);
1126
+ return parseSendResult(result);
1127
+ }
1128
+ async lookupDirectory(scheme, id) {
1129
+ return this.request("GET", `/directory/${scheme}/${id}`);
1130
+ }
1131
+ async searchDirectory(params) {
1132
+ const query = new URLSearchParams(params).toString();
1133
+ return this.request("GET", `/directory/search?${query}`);
1134
+ }
1135
+ async getInvoiceAs(id, format) {
1136
+ const { maxRetries, initialDelayMs, maxDelayMs } = this.retryConfig;
1137
+ let lastError;
1138
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1139
+ try {
1140
+ return await this.doRequestBinary(`/invoices/${id}/as/${format}`);
1141
+ } catch (err) {
1142
+ lastError = err;
1143
+ if (attempt < maxRetries && isRetryableError(err)) {
1144
+ const retryAfterMs = err instanceof PeppolApiError ? err.retryAfterMs : void 0;
1145
+ await sleep(calculateRetryDelay(attempt, initialDelayMs, maxDelayMs, retryAfterMs));
1146
+ continue;
1147
+ }
1148
+ throw err;
1149
+ }
1150
+ }
1151
+ throw lastError;
1152
+ }
1153
+ async doRequestBinary(path) {
1154
+ const url = `${this.baseUrl}${path}`;
1155
+ const controller = new AbortController();
1156
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1157
+ const requestHeaders = {
1158
+ Authorization: `Bearer ${this.apiKey}`
1159
+ };
1160
+ const startTime = Date.now();
1161
+ if (this.onRequest) {
1162
+ try {
1163
+ this.onRequest({
1164
+ method: "GET",
1165
+ url,
1166
+ headers: { ...requestHeaders },
1167
+ timestamp: startTime
1168
+ });
1169
+ } catch {
1170
+ }
1171
+ }
1172
+ try {
1173
+ const response = await fetch(url, {
1174
+ method: "GET",
1175
+ headers: requestHeaders,
1176
+ signal: controller.signal
1177
+ });
1178
+ if (!response.ok) {
1179
+ const errorBody = await response.text().catch(() => "Unknown error");
1180
+ const retryAfterMs = response.status === 429 ? parseRetryAfter(response.headers.get("Retry-After")) : void 0;
1181
+ if (this.onResponse) {
1182
+ try {
1183
+ this.onResponse({
1184
+ status: response.status,
1185
+ headers: Object.fromEntries(response.headers.entries()),
1186
+ body: errorBody,
1187
+ durationMs: Date.now() - startTime,
1188
+ timestamp: Date.now()
1189
+ });
1190
+ } catch {
1191
+ }
1192
+ }
1193
+ throw new PeppolApiError(`getpeppr API error (${response.status}): ${errorBody}`, response.status, errorBody, retryAfterMs);
1194
+ }
1195
+ const responseBody = await response.arrayBuffer();
1196
+ if (this.onResponse) {
1197
+ try {
1198
+ this.onResponse({
1199
+ status: response.status,
1200
+ headers: Object.fromEntries(response.headers.entries()),
1201
+ body: `[ArrayBuffer: ${responseBody.byteLength} bytes]`,
1202
+ durationMs: Date.now() - startTime,
1203
+ timestamp: Date.now()
1204
+ });
1205
+ } catch {
1206
+ }
1207
+ }
1208
+ return responseBody;
1209
+ } finally {
1210
+ clearTimeout(timeoutId);
1211
+ }
1212
+ }
1213
+ async validateDocumentServer(input) {
1214
+ return this.request("POST", "/validate/server", input);
1215
+ }
1216
+ async listEvents(options) {
1217
+ const params = new URLSearchParams();
1218
+ if (options?.limit != null)
1219
+ params.set("limit", String(options.limit));
1220
+ if (options?.offset != null)
1221
+ params.set("offset", String(options.offset));
1222
+ if (options?.invoiceId)
1223
+ params.set("invoiceId", options.invoiceId);
1224
+ if (options?.dateFrom)
1225
+ params.set("dateFrom", options.dateFrom);
1226
+ if (options?.dateTo)
1227
+ params.set("dateTo", options.dateTo);
1228
+ const query = params.toString() ? `?${params.toString()}` : "";
1229
+ const result = await this.request("GET", `/events${query}`);
1230
+ const events = result.events ?? result.data ?? [];
1231
+ const meta = result.meta;
1232
+ return {
1233
+ data: events.map((evt) => ({
1234
+ name: String(evt.name ?? ""),
1235
+ text: String(evt.text ?? ""),
1236
+ notes: evt.notes ? String(evt.notes) : void 0,
1237
+ createdAt: String(evt.createdAt ?? evt.created_at ?? (/* @__PURE__ */ new Date()).toISOString()),
1238
+ invoice: evt.invoice,
1239
+ contact: evt.contact
1240
+ })),
1241
+ meta: {
1242
+ totalCount: Number(meta?.total_count ?? meta?.totalCount ?? events.length),
1243
+ offset: Number(meta?.offset ?? options?.offset ?? 0),
1244
+ limit: Number(meta?.limit ?? options?.limit ?? 25),
1245
+ hasMore: meta ? Number(meta.total_count ?? meta.totalCount) > Number(meta.offset) + Number(meta.limit) : false,
1246
+ truncated: Boolean(meta?.truncated ?? false)
1247
+ }
1248
+ };
1249
+ }
1250
+ async acknowledgeInvoice(id) {
1251
+ const result = await this.request("POST", `/invoices/${id}/ack`);
1252
+ return parseSendResult(result);
1253
+ }
1254
+ async updateInvoice(id, input) {
1255
+ const result = await this.request("PUT", `/invoices/${id}`, input);
1256
+ return parseSendResult(result);
1257
+ }
1258
+ async deleteInvoice(id) {
1259
+ const result = await this.request("DELETE", `/invoices/${id}`);
1260
+ return parseSendResult(result);
1261
+ }
1262
+ async markInvoiceAs(id, state, options) {
1263
+ const body = { state };
1264
+ if (options?.commit)
1265
+ body.commit = options.commit;
1266
+ if (options?.reason)
1267
+ body.reason = options.reason;
1268
+ const result = await this.request("POST", `/invoices/${id}/mark-as`, body);
1269
+ return parseSendResult(result);
1270
+ }
1271
+ async listContacts(options) {
1272
+ const params = new URLSearchParams();
1273
+ if (options?.limit != null)
1274
+ params.set("limit", String(options.limit));
1275
+ if (options?.offset != null)
1276
+ params.set("offset", String(options.offset));
1277
+ if (options?.name)
1278
+ params.set("name", options.name);
1279
+ if (options?.isClient != null)
1280
+ params.set("isClient", String(options.isClient));
1281
+ if (options?.isProvider != null)
1282
+ params.set("isProvider", String(options.isProvider));
1283
+ const query = params.toString() ? `?${params.toString()}` : "";
1284
+ const result = await this.request("GET", `/contacts${query}`);
1285
+ const contacts = result.contacts ?? result.data ?? [];
1286
+ const meta = result.meta;
1287
+ return {
1288
+ data: contacts.map(parseContact),
1289
+ meta: {
1290
+ totalCount: Number(meta?.total_count ?? meta?.totalCount ?? contacts.length),
1291
+ offset: Number(meta?.offset ?? options?.offset ?? 0),
1292
+ limit: Number(meta?.limit ?? options?.limit ?? 25),
1293
+ hasMore: meta ? Number(meta.total_count ?? meta.totalCount) > Number(meta.offset) + Number(meta.limit) : false,
1294
+ truncated: Boolean(meta?.truncated ?? false)
1295
+ }
1296
+ };
1297
+ }
1298
+ async getContact(id) {
1299
+ const result = await this.request("GET", `/contacts/${id}`);
1300
+ return parseContact(result);
1301
+ }
1302
+ async createContact(input) {
1303
+ const result = await this.request("POST", "/contacts", input);
1304
+ return parseContact(result);
1305
+ }
1306
+ async updateContact(id, input) {
1307
+ const result = await this.request("PUT", `/contacts/${id}`, input);
1308
+ return parseContact(result);
1309
+ }
1310
+ async deleteContact(id) {
1311
+ await this.request("DELETE", `/contacts/${id}`);
1312
+ }
1313
+ async listBankAccounts(options) {
1314
+ const params = new URLSearchParams();
1315
+ if (options?.limit != null)
1316
+ params.set("limit", String(options.limit));
1317
+ if (options?.offset != null)
1318
+ params.set("offset", String(options.offset));
1319
+ const query = params.toString() ? `?${params.toString()}` : "";
1320
+ const result = await this.request("GET", `/bank-accounts${query}`);
1321
+ const bankAccounts = result.bankAccounts ?? result.data ?? [];
1322
+ const meta = result.meta;
1323
+ return {
1324
+ data: bankAccounts.map(parseBankAccount),
1325
+ meta: {
1326
+ totalCount: Number(meta?.total_count ?? meta?.totalCount ?? bankAccounts.length),
1327
+ offset: Number(meta?.offset ?? options?.offset ?? 0),
1328
+ limit: Number(meta?.limit ?? options?.limit ?? 25),
1329
+ hasMore: meta ? Number(meta.total_count ?? meta.totalCount) > Number(meta.offset) + Number(meta.limit) : false,
1330
+ truncated: Boolean(meta?.truncated ?? false)
1331
+ }
1332
+ };
1333
+ }
1334
+ async getBankAccount(id) {
1335
+ const result = await this.request("GET", `/bank-accounts/${id}`);
1336
+ return parseBankAccount(result);
1337
+ }
1338
+ async createBankAccount(input) {
1339
+ const result = await this.request("POST", "/bank-accounts", input);
1340
+ return parseBankAccount(result);
1341
+ }
1342
+ async updateBankAccount(id, input) {
1343
+ const result = await this.request("PUT", `/bank-accounts/${id}`, input);
1344
+ return parseBankAccount(result);
1345
+ }
1346
+ async deleteBankAccount(id) {
1347
+ await this.request("DELETE", `/bank-accounts/${id}`);
1348
+ }
1349
+ async importInvoice(options) {
1350
+ const body = {
1351
+ file: arrayBufferToBase64(options.file),
1352
+ filename: options.filename,
1353
+ mimeType: options.mimeType ?? detectMimeType(options.filename)
1354
+ };
1355
+ const result = await this.request("POST", "/invoices/import", body);
1356
+ return parseSendResult(result);
1357
+ }
1358
+ async listTransportTypes() {
1359
+ const result = await this.request("GET", "/transports/types");
1360
+ const types = result.transportTypes ?? result.data ?? [];
1361
+ return types.map((t) => ({
1362
+ code: String(t.code ?? ""),
1363
+ name: String(t.name ?? "")
1364
+ }));
1365
+ }
1366
+ async listTransports() {
1367
+ const result = await this.request("GET", "/transports");
1368
+ const transports = result.transports ?? result.data ?? [];
1369
+ return transports.map(parseTransport);
1370
+ }
1371
+ async getTransport(code) {
1372
+ const result = await this.request("GET", `/transports/${code}`);
1373
+ return parseTransport(result);
1374
+ }
1375
+ async createTransport(input) {
1376
+ const result = await this.request("POST", "/transports", input);
1377
+ return parseTransport(result);
1378
+ }
1379
+ async updateTransport(code, input) {
1380
+ const result = await this.request("PUT", `/transports/${code}`, input);
1381
+ return parseTransport(result);
1382
+ }
1383
+ async deleteTransport(code) {
1384
+ await this.request("DELETE", `/transports/${code}`);
1385
+ }
1386
+ };
1387
+ function arrayBufferToBase64(buffer) {
1388
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
1389
+ let binary = "";
1390
+ for (const byte of bytes) {
1391
+ binary += String.fromCharCode(byte);
1392
+ }
1393
+ return btoa(binary);
1394
+ }
1395
+ function detectMimeType(filename) {
1396
+ const ext = filename.split(".").pop()?.toLowerCase();
1397
+ switch (ext) {
1398
+ case "xml":
1399
+ return "application/xml";
1400
+ case "pdf":
1401
+ return "application/pdf";
1402
+ case "json":
1403
+ return "application/json";
1404
+ default:
1405
+ return "application/octet-stream";
1406
+ }
1407
+ }
1408
+ function parseSendResult(result) {
1409
+ return {
1410
+ id: String(result.id ?? ""),
1411
+ status: mapStatus(String(result.status ?? "submitted")),
1412
+ peppolMessageId: result.peppolMessageId ?? result.peppol_message_id,
1413
+ createdAt: String(result.createdAt ?? result.updatedAt ?? result.created_at ?? (/* @__PURE__ */ new Date()).toISOString()),
1414
+ ublXml: result.ublXml,
1415
+ warnings: Array.isArray(result.warnings) ? result.warnings : void 0
1416
+ };
1417
+ }
1418
+ function parseContact(raw) {
1419
+ const contact = {
1420
+ id: String(raw.id ?? ""),
1421
+ name: String(raw.name ?? "")
1422
+ };
1423
+ if (raw.peppolId != null)
1424
+ contact.peppolId = String(raw.peppolId);
1425
+ if (raw.vatNumber != null)
1426
+ contact.vatNumber = String(raw.vatNumber);
1427
+ if (raw.companyId != null)
1428
+ contact.companyId = String(raw.companyId);
1429
+ if (raw.street != null)
1430
+ contact.street = String(raw.street);
1431
+ if (raw.city != null)
1432
+ contact.city = String(raw.city);
1433
+ if (raw.postalCode != null)
1434
+ contact.postalCode = String(raw.postalCode);
1435
+ if (raw.country != null)
1436
+ contact.country = String(raw.country);
1437
+ if (raw.email != null)
1438
+ contact.email = String(raw.email);
1439
+ if (raw.phone != null)
1440
+ contact.phone = String(raw.phone);
1441
+ if (raw.isClient != null)
1442
+ contact.isClient = Boolean(raw.isClient);
1443
+ if (raw.isProvider != null)
1444
+ contact.isProvider = Boolean(raw.isProvider);
1445
+ if (raw.createdAt != null)
1446
+ contact.createdAt = String(raw.createdAt);
1447
+ if (raw.updatedAt != null)
1448
+ contact.updatedAt = String(raw.updatedAt);
1449
+ if (raw.directoryVerified != null)
1450
+ contact.directoryVerified = Boolean(raw.directoryVerified);
1451
+ if (raw.directoryLastChecked != null)
1452
+ contact.directoryLastChecked = String(raw.directoryLastChecked);
1453
+ return contact;
1454
+ }
1455
+ function parseBankAccount(raw) {
1456
+ const account = {
1457
+ id: String(raw.id ?? ""),
1458
+ name: String(raw.name ?? ""),
1459
+ type: raw.type === "number" ? "number" : "iban"
1460
+ };
1461
+ if (raw.iban != null)
1462
+ account.iban = String(raw.iban);
1463
+ if (raw.number != null)
1464
+ account.number = String(raw.number);
1465
+ if (raw.bic != null)
1466
+ account.bic = String(raw.bic);
1467
+ if (raw.country != null)
1468
+ account.country = String(raw.country);
1469
+ if (raw.createdAt != null)
1470
+ account.createdAt = String(raw.createdAt);
1471
+ if (raw.updatedAt != null)
1472
+ account.updatedAt = String(raw.updatedAt);
1473
+ return account;
1474
+ }
1475
+ function parseTransport(raw) {
1476
+ return {
1477
+ id: String(raw.id ?? ""),
1478
+ transportTypeCode: String(raw.transportTypeCode ?? ""),
1479
+ name: String(raw.name ?? ""),
1480
+ status: raw.status ? String(raw.status) : void 0
1481
+ };
1482
+ }
1483
+ var VALID_STATUSES = /* @__PURE__ */ new Set([
1484
+ "submitted",
1485
+ "delivered",
1486
+ "accepted",
1487
+ "rejected",
1488
+ "paid",
1489
+ "failed",
1490
+ "cleared",
1491
+ "acknowledged",
1492
+ "in_process",
1493
+ "under_query",
1494
+ "conditionally_accepted",
1495
+ "partially_paid",
1496
+ "no_action"
1497
+ ]);
1498
+ function mapStatus(raw) {
1499
+ const s = raw.toLowerCase();
1500
+ return VALID_STATUSES.has(s) ? s : "unknown";
1501
+ }
1502
+ var PeppolError = class extends Error {
1503
+ constructor(message) {
1504
+ super(message);
1505
+ this.name = "PeppolError";
1506
+ }
1507
+ };
1508
+ var PeppolValidationError = class extends PeppolError {
1509
+ validation;
1510
+ constructor(message, validation) {
1511
+ super(message);
1512
+ this.validation = validation;
1513
+ this.name = "PeppolValidationError";
1514
+ }
1515
+ };
1516
+ var PeppolApiError = class extends PeppolError {
1517
+ statusCode;
1518
+ responseBody;
1519
+ /** Parsed Retry-After delay in milliseconds (present on 429 responses) */
1520
+ retryAfterMs;
1521
+ constructor(message, statusCode, responseBody, retryAfterMs) {
1522
+ super(message);
1523
+ this.statusCode = statusCode;
1524
+ this.responseBody = responseBody;
1525
+ this.name = "PeppolApiError";
1526
+ this.retryAfterMs = retryAfterMs;
1527
+ }
1528
+ };
1529
+ var Peppol = class {
1530
+ adapter;
1531
+ invoices;
1532
+ creditNotes;
1533
+ directory;
1534
+ events;
1535
+ contacts;
1536
+ bankAccounts;
1537
+ transports;
1538
+ constructor(config) {
1539
+ if (!config.apiKey) {
1540
+ throw new PeppolError("API key is required. Sign up at https://console.getpeppr.dev to get your sandbox key.");
1541
+ }
1542
+ this.adapter = new GetpepprAdapter(config);
1543
+ this.invoices = new InvoiceOperations(this.adapter);
1544
+ this.creditNotes = new CreditNoteOperations(this.adapter);
1545
+ this.directory = new DirectoryOperations(this.adapter);
1546
+ this.events = new EventOperations(this.adapter);
1547
+ this.contacts = new ContactOperations(this.adapter);
1548
+ this.bankAccounts = new BankAccountOperations(this.adapter);
1549
+ this.transports = new TransportOperations(this.adapter);
1550
+ }
1551
+ /**
1552
+ * Validate an invoice without sending it.
1553
+ * Useful for pre-flight checks in your UI.
1554
+ */
1555
+ validate(input) {
1556
+ return validateInvoice(input);
1557
+ }
1558
+ /**
1559
+ * Generate UBL XML without sending.
1560
+ * Useful for debugging or manual submission.
1561
+ */
1562
+ toXml(input) {
1563
+ const validation = validateInvoice(input);
1564
+ if (!validation.valid) {
1565
+ throw new PeppolValidationError(`Invoice validation failed: ${validation.errors.map((e) => e.message).join("; ")}`, validation);
1566
+ }
1567
+ if (input.isCreditNote) {
1568
+ return buildCreditNoteXml(input);
1569
+ }
1570
+ return buildInvoiceXml(input);
1571
+ }
1572
+ };
1573
+ async function* paginate(fetchPage, options) {
1574
+ const pageSize = options?.limit ?? 25;
1575
+ let offset = 0;
1576
+ while (true) {
1577
+ const page = await fetchPage(offset, pageSize);
1578
+ for (const item of page.data) {
1579
+ yield item;
1580
+ }
1581
+ if (!page.meta.hasMore)
1582
+ break;
1583
+ offset += pageSize;
1584
+ }
1585
+ }
1586
+ var InvoiceOperations = class {
1587
+ adapter;
1588
+ constructor(adapter) {
1589
+ this.adapter = adapter;
1590
+ }
1591
+ /**
1592
+ * Create a draft invoice without sending it.
1593
+ * Validates input client-side, then creates the invoice on B2BRouter.
1594
+ * Use `sendById()` to send the draft when ready.
1595
+ *
1596
+ * @example
1597
+ * ```ts
1598
+ * const draft = await peppol.invoices.create({ number: "INV-001", to, lines });
1599
+ * // Later, when ready:
1600
+ * await peppol.invoices.sendById(draft.id);
1601
+ * ```
1602
+ */
1603
+ async create(input, options) {
1604
+ const validation = validateInvoice(input);
1605
+ if (!validation.valid) {
1606
+ throw new PeppolValidationError(`Invoice validation failed:
1607
+ ${validation.errors.map((e) => ` - ${e.field}: ${e.message}${e.suggestion ? ` (${e.suggestion})` : ""}`).join("\n")}`, validation);
1608
+ }
1609
+ const result = await this.adapter.createInvoice(input, options);
1610
+ if (validation.warnings.length > 0) {
1611
+ result.warnings = validation.warnings;
1612
+ }
1613
+ return result;
1614
+ }
1615
+ /**
1616
+ * Send an existing draft invoice by ID.
1617
+ * The invoice must have been previously created with `create()`.
1618
+ *
1619
+ * @example
1620
+ * ```ts
1621
+ * await peppol.invoices.sendById("inv-1");
1622
+ * ```
1623
+ * @throws {PeppolApiError} 501 if the gateway provider does not support draft sending
1624
+ */
1625
+ async sendById(id) {
1626
+ return this.adapter.sendInvoiceById(id);
1627
+ }
1628
+ /**
1629
+ * Send an invoice via Peppol.
1630
+ *
1631
+ * @example
1632
+ * ```ts
1633
+ * const result = await peppol.invoices.send({
1634
+ * number: "INV-001",
1635
+ * from: { name: "My Company", peppolId: "0208:BE0123456789", country: "BE" },
1636
+ * to: { name: "Client Co", peppolId: "0208:BE9876543210", country: "BE" },
1637
+ * lines: [
1638
+ * { description: "Consulting", quantity: 10, unitPrice: 150, vatRate: 21 }
1639
+ * ]
1640
+ * });
1641
+ * ```
1642
+ */
1643
+ async send(input, options) {
1644
+ const validation = validateInvoice(input);
1645
+ if (!validation.valid) {
1646
+ throw new PeppolValidationError(`Invoice validation failed:
1647
+ ${validation.errors.map((e) => ` - ${e.field}: ${e.message}${e.suggestion ? ` (${e.suggestion})` : ""}`).join("\n")}`, validation);
1648
+ }
1649
+ const result = await this.adapter.sendInvoice(input, options);
1650
+ if (validation.warnings.length > 0) {
1651
+ result.warnings = validation.warnings;
1652
+ }
1653
+ return result;
1654
+ }
1655
+ /** List invoices with pagination, filtering, and proper metadata */
1656
+ async list(options) {
1657
+ return this.adapter.listInvoices(options);
1658
+ }
1659
+ /**
1660
+ * Async iterator over all invoices, automatically handling pagination.
1661
+ *
1662
+ * @example
1663
+ * ```ts
1664
+ * for await (const invoice of peppol.invoices.listAll()) {
1665
+ * console.log(invoice.id, invoice.status);
1666
+ * }
1667
+ * ```
1668
+ */
1669
+ listAll(options) {
1670
+ return paginate((offset, limit) => this.adapter.listInvoices({ ...options, offset, limit }), options);
1671
+ }
1672
+ /** Get the status of a sent invoice */
1673
+ async getStatus(documentId) {
1674
+ return this.adapter.getStatus(documentId);
1675
+ }
1676
+ /**
1677
+ * Export an invoice in a specific format (e.g., PDF, UBL XML).
1678
+ * Returns raw binary data as an ArrayBuffer.
1679
+ *
1680
+ * @example
1681
+ * ```ts
1682
+ * const pdf = await peppol.invoices.getAs("inv-123", "pdf");
1683
+ * fs.writeFileSync("invoice.pdf", Buffer.from(pdf));
1684
+ * ```
1685
+ */
1686
+ async getAs(id, format) {
1687
+ return this.adapter.getInvoiceAs(id, format);
1688
+ }
1689
+ /**
1690
+ * Validate an invoice server-side using XSD and Schematron rules.
1691
+ * Builds UBL XML from the input and sends it to the server for validation.
1692
+ * More thorough than client-side validation — catches XML-level issues.
1693
+ *
1694
+ * @example
1695
+ * ```ts
1696
+ * const result = await peppol.invoices.validateServer({
1697
+ * number: "INV-001",
1698
+ * to: { name: "Acme", peppolId: "0208:BE0123456789", country: "BE" },
1699
+ * lines: [{ description: "Item", quantity: 1, unitPrice: 100, vatRate: 21 }]
1700
+ * });
1701
+ * console.log(result.valid, result.schematron.errors);
1702
+ * ```
1703
+ * @throws {PeppolApiError} 501 if the gateway provider does not support server-side validation
1704
+ */
1705
+ async validateServer(input) {
1706
+ return this.adapter.validateDocumentServer(input);
1707
+ }
1708
+ /**
1709
+ * Import an invoice from a file (XML, PDF, JSON).
1710
+ * The file is base64-encoded and sent to the gateway, which forwards it
1711
+ * as multipart/form-data to B2BRouter's import endpoint.
1712
+ *
1713
+ * @example
1714
+ * ```ts
1715
+ * const xmlBytes = fs.readFileSync("invoice.xml");
1716
+ * const result = await peppol.invoices.importFile({
1717
+ * file: xmlBytes,
1718
+ * filename: "invoice.xml",
1719
+ * });
1720
+ * console.log(result.id, result.status);
1721
+ * ```
1722
+ * @throws {PeppolApiError} 501 if the gateway provider does not support file import
1723
+ */
1724
+ async importFile(options) {
1725
+ return this.adapter.importInvoice(options);
1726
+ }
1727
+ /**
1728
+ * Acknowledge a received invoice.
1729
+ *
1730
+ * @example
1731
+ * ```ts
1732
+ * const result = await peppol.invoices.acknowledge("inv-123");
1733
+ * console.log(result.status); // "accepted"
1734
+ * ```
1735
+ * @throws {PeppolApiError} 501 if the gateway provider does not support acknowledgement
1736
+ */
1737
+ async acknowledge(id) {
1738
+ return this.adapter.acknowledgeInvoice(id);
1739
+ }
1740
+ /**
1741
+ * Update an existing invoice (draft only).
1742
+ * Only include the fields you want to change — partial updates are supported.
1743
+ *
1744
+ * @example
1745
+ * ```ts
1746
+ * const updated = await peppol.invoices.update("inv-123", {
1747
+ * dueDate: "2026-04-01",
1748
+ * lines: [
1749
+ * { id: "line-1", quantity: 5 },
1750
+ * { id: "line-2", _destroy: true },
1751
+ * ],
1752
+ * });
1753
+ * ```
1754
+ * @throws {PeppolApiError} 501 if the gateway provider does not support invoice updates
1755
+ */
1756
+ async update(id, input) {
1757
+ return this.adapter.updateInvoice(id, input);
1758
+ }
1759
+ /**
1760
+ * Delete an invoice.
1761
+ *
1762
+ * @example
1763
+ * ```ts
1764
+ * const deleted = await peppol.invoices.delete("inv-123");
1765
+ * ```
1766
+ * @throws {PeppolApiError} 501 if the gateway provider does not support invoice deletion
1767
+ */
1768
+ async delete(id) {
1769
+ return this.adapter.deleteInvoice(id);
1770
+ }
1771
+ /**
1772
+ * Transition an invoice to a new state.
1773
+ * B2BRouter validates the state machine — invalid transitions return an error.
1774
+ *
1775
+ * @example
1776
+ * ```ts
1777
+ * await peppol.invoices.markAs("inv-123", "accepted");
1778
+ * await peppol.invoices.markAs("inv-123", "paid", {
1779
+ * commit: "with_mail",
1780
+ * reason: "Payment received",
1781
+ * });
1782
+ * ```
1783
+ * @throws {PeppolApiError} 501 if the gateway provider does not support state transitions
1784
+ */
1785
+ async markAs(id, state, options) {
1786
+ return this.adapter.markInvoiceAs(id, state, options);
1787
+ }
1788
+ /**
1789
+ * Send multiple invoices in parallel with controlled concurrency.
1790
+ * Each invoice is validated and sent individually — failures don't affect other invoices
1791
+ * unless `stopOnError: true` is set.
1792
+ *
1793
+ * The SDK's built-in retry logic (including 429 Retry-After) provides automatic
1794
+ * rate-limit handling at the request level.
1795
+ *
1796
+ * @example
1797
+ * ```ts
1798
+ * const result = await peppol.invoices.sendBatch([invoice1, invoice2, invoice3], {
1799
+ * concurrency: 3,
1800
+ * });
1801
+ * console.log(`${result.succeeded.length} sent, ${result.failed.length} failed`);
1802
+ * ```
1803
+ */
1804
+ async sendBatch(inputs, options) {
1805
+ const concurrency = options?.concurrency ?? 5;
1806
+ const stopOnError = options?.stopOnError ?? false;
1807
+ const succeeded = [];
1808
+ const failed = [];
1809
+ let stopped = false;
1810
+ for (let i = 0; i < inputs.length; i += concurrency) {
1811
+ if (stopped)
1812
+ break;
1813
+ const chunk = inputs.slice(i, i + concurrency);
1814
+ const promises = chunk.map(async (input, j) => {
1815
+ const index = i + j;
1816
+ if (stopped)
1817
+ return;
1818
+ try {
1819
+ const result = await this.send(input);
1820
+ succeeded.push({ index, result });
1821
+ } catch (error2) {
1822
+ failed.push({ index, input, error: error2 });
1823
+ if (stopOnError) {
1824
+ stopped = true;
1825
+ }
1826
+ }
1827
+ });
1828
+ await Promise.all(promises);
1829
+ }
1830
+ return { succeeded, failed, total: inputs.length };
1831
+ }
1832
+ /**
1833
+ * Poll until an invoice reaches a target status.
1834
+ *
1835
+ * @example
1836
+ * ```ts
1837
+ * const result = await peppol.invoices.waitFor(id, "accepted", { timeout: 60000 });
1838
+ * ```
1839
+ */
1840
+ async waitFor(documentId, targetStatus, options) {
1841
+ const timeout = options?.timeout ?? 12e4;
1842
+ const interval = options?.interval ?? 5e3;
1843
+ const targets = Array.isArray(targetStatus) ? targetStatus : [targetStatus];
1844
+ const terminalFailures = ["failed", "rejected"];
1845
+ const startTime = Date.now();
1846
+ while (true) {
1847
+ const result = await this.getStatus(documentId);
1848
+ if (targets.includes(result.status)) {
1849
+ return result;
1850
+ }
1851
+ if (terminalFailures.includes(result.status)) {
1852
+ throw new PeppolError(`Document ${documentId} reached terminal status "${result.status}" while waiting for "${targets.join('" or "')}"`);
1853
+ }
1854
+ if (Date.now() - startTime >= timeout) {
1855
+ throw new PeppolError(`Timed out waiting for document ${documentId} to reach status "${targets.join('" or "')}" (last: "${result.status}")`);
1856
+ }
1857
+ await sleep(interval);
1858
+ }
1859
+ }
1860
+ };
1861
+ var CreditNoteOperations = class {
1862
+ adapter;
1863
+ constructor(adapter) {
1864
+ this.adapter = adapter;
1865
+ }
1866
+ /**
1867
+ * Send a credit note via Peppol.
1868
+ * @deprecated Use peppol.invoices.send({ ...input, isCreditNote: true }) instead.
1869
+ */
1870
+ async send(input) {
1871
+ const invoiceInput = {
1872
+ ...input,
1873
+ isCreditNote: true,
1874
+ invoiceReference: input.invoiceReference
1875
+ };
1876
+ const validation = validateInvoice(invoiceInput);
1877
+ if (!validation.valid) {
1878
+ throw new PeppolValidationError(`Credit note validation failed:
1879
+ ${validation.errors.map((e) => ` - ${e.field}: ${e.message}`).join("\n")}`, validation);
1880
+ }
1881
+ return this.adapter.sendInvoice(invoiceInput);
1882
+ }
1883
+ };
1884
+ var DirectoryOperations = class {
1885
+ adapter;
1886
+ constructor(adapter) {
1887
+ this.adapter = adapter;
1888
+ }
1889
+ /**
1890
+ * Look up a Peppol participant in the directory.
1891
+ *
1892
+ * @example
1893
+ * ```ts
1894
+ * const entry = await peppol.directory.lookup("0208:BE0123456789");
1895
+ * console.log(entry.name, entry.capabilities);
1896
+ * ```
1897
+ */
1898
+ async lookup(peppolId) {
1899
+ const colonIndex = peppolId.indexOf(":");
1900
+ if (colonIndex === -1) {
1901
+ throw new PeppolError('Invalid Peppol ID format. Expected "scheme:id" (e.g., "0208:BE0123456789")');
1902
+ }
1903
+ const scheme = peppolId.slice(0, colonIndex);
1904
+ const id = peppolId.slice(colonIndex + 1);
1905
+ return this.adapter.lookupDirectory(scheme, id);
1906
+ }
1907
+ /**
1908
+ * Search the Peppol Directory for participants.
1909
+ *
1910
+ * @example
1911
+ * ```ts
1912
+ * const result = await peppol.directory.search({ name: "Acme", country: "BE" });
1913
+ * console.log(result.data); // DirectoryEntry[]
1914
+ * console.log(result.meta.totalCount);
1915
+ * ```
1916
+ */
1917
+ async search(options) {
1918
+ if (!options.name && !options.country && !options.vatNumber) {
1919
+ throw new PeppolError("At least one search criterion is required (name, country, or vatNumber)");
1920
+ }
1921
+ if (options.name && options.name.length < 3) {
1922
+ throw new PeppolError("Search name must be at least 3 characters");
1923
+ }
1924
+ if (!this.adapter.searchDirectory) {
1925
+ throw new PeppolError("Directory search is not supported by this backend adapter");
1926
+ }
1927
+ const params = {};
1928
+ if (options.name)
1929
+ params.name = options.name;
1930
+ if (options.country)
1931
+ params.country = options.country;
1932
+ if (options.vatNumber)
1933
+ params.vatNumber = options.vatNumber;
1934
+ if (options.limit !== void 0)
1935
+ params.limit = String(options.limit);
1936
+ if (options.offset !== void 0)
1937
+ params.offset = String(options.offset);
1938
+ return this.adapter.searchDirectory(params);
1939
+ }
1940
+ /**
1941
+ * Search the Peppol Directory by VAT number.
1942
+ * Convenience method — equivalent to `search({ vatNumber })`.
1943
+ *
1944
+ * @example
1945
+ * ```ts
1946
+ * const result = await peppol.directory.searchByVat("BE0123456789");
1947
+ * ```
1948
+ */
1949
+ async searchByVat(vatNumber) {
1950
+ return this.search({ vatNumber });
1951
+ }
1952
+ };
1953
+ var EventOperations = class {
1954
+ adapter;
1955
+ constructor(adapter) {
1956
+ this.adapter = adapter;
1957
+ }
1958
+ /**
1959
+ * List events with optional filtering and pagination.
1960
+ *
1961
+ * @example
1962
+ * ```ts
1963
+ * const result = await peppol.events.list({ limit: 10 });
1964
+ * console.log(result.data, result.meta);
1965
+ *
1966
+ * // Filter by invoice
1967
+ * const invoiceEvents = await peppol.events.list({ invoiceId: "inv-123" });
1968
+ * ```
1969
+ */
1970
+ async list(options) {
1971
+ return this.adapter.listEvents(options);
1972
+ }
1973
+ /**
1974
+ * Async iterator over all events, automatically handling pagination.
1975
+ *
1976
+ * @example
1977
+ * ```ts
1978
+ * for await (const event of peppol.events.listAll({ invoiceId: "inv-123" })) {
1979
+ * console.log(event.name, event.createdAt);
1980
+ * }
1981
+ * ```
1982
+ */
1983
+ listAll(options) {
1984
+ return paginate((offset, limit) => this.adapter.listEvents({ ...options, offset, limit }), options);
1985
+ }
1986
+ };
1987
+ var ContactOperations = class {
1988
+ adapter;
1989
+ constructor(adapter) {
1990
+ this.adapter = adapter;
1991
+ }
1992
+ /**
1993
+ * List contacts with optional filtering and pagination.
1994
+ *
1995
+ * @example
1996
+ * ```ts
1997
+ * const result = await peppol.contacts.list({ limit: 10, isClient: true });
1998
+ * console.log(result.data, result.meta);
1999
+ * ```
2000
+ */
2001
+ async list(options) {
2002
+ return this.adapter.listContacts(options);
2003
+ }
2004
+ /**
2005
+ * Get a single contact by ID.
2006
+ *
2007
+ * @example
2008
+ * ```ts
2009
+ * const contact = await peppol.contacts.get("123");
2010
+ * console.log(contact.name, contact.peppolId);
2011
+ * ```
2012
+ */
2013
+ async get(id) {
2014
+ return this.adapter.getContact(id);
2015
+ }
2016
+ /**
2017
+ * Create a new contact.
2018
+ *
2019
+ * @example
2020
+ * ```ts
2021
+ * const contact = await peppol.contacts.create({
2022
+ * name: "Acme Corp",
2023
+ * peppolId: "0208:BE0123456789",
2024
+ * country: "BE",
2025
+ * isClient: true,
2026
+ * });
2027
+ * ```
2028
+ */
2029
+ async create(input) {
2030
+ return this.adapter.createContact(input);
2031
+ }
2032
+ /**
2033
+ * Update an existing contact.
2034
+ *
2035
+ * @example
2036
+ * ```ts
2037
+ * const updated = await peppol.contacts.update("123", { email: "new@acme.com" });
2038
+ * ```
2039
+ */
2040
+ async update(id, input) {
2041
+ return this.adapter.updateContact(id, input);
2042
+ }
2043
+ /**
2044
+ * Delete a contact.
2045
+ *
2046
+ * @example
2047
+ * ```ts
2048
+ * await peppol.contacts.delete("123");
2049
+ * ```
2050
+ */
2051
+ async delete(id) {
2052
+ return this.adapter.deleteContact(id);
2053
+ }
2054
+ /**
2055
+ * Async iterator over all contacts, automatically handling pagination.
2056
+ *
2057
+ * @example
2058
+ * ```ts
2059
+ * for await (const contact of peppol.contacts.listAll({ isClient: true })) {
2060
+ * console.log(contact.name, contact.peppolId);
2061
+ * }
2062
+ * ```
2063
+ */
2064
+ listAll(options) {
2065
+ return paginate((offset, limit) => this.adapter.listContacts({ ...options, offset, limit }));
2066
+ }
2067
+ };
2068
+ var BankAccountOperations = class {
2069
+ adapter;
2070
+ constructor(adapter) {
2071
+ this.adapter = adapter;
2072
+ }
2073
+ /**
2074
+ * List bank accounts with optional pagination.
2075
+ *
2076
+ * @example
2077
+ * ```ts
2078
+ * const result = await peppol.bankAccounts.list({ limit: 10 });
2079
+ * console.log(result.data, result.meta);
2080
+ * ```
2081
+ */
2082
+ async list(options) {
2083
+ return this.adapter.listBankAccounts(options);
2084
+ }
2085
+ /**
2086
+ * Get a single bank account by ID.
2087
+ *
2088
+ * @example
2089
+ * ```ts
2090
+ * const account = await peppol.bankAccounts.get("123");
2091
+ * console.log(account.name, account.iban);
2092
+ * ```
2093
+ */
2094
+ async get(id) {
2095
+ return this.adapter.getBankAccount(id);
2096
+ }
2097
+ /**
2098
+ * Create a new bank account.
2099
+ *
2100
+ * @example
2101
+ * ```ts
2102
+ * const account = await peppol.bankAccounts.create({
2103
+ * name: "Main Account",
2104
+ * iban: "BE68539007547034",
2105
+ * bic: "BBRUBEBB",
2106
+ * country: "BE",
2107
+ * });
2108
+ * ```
2109
+ */
2110
+ async create(input) {
2111
+ return this.adapter.createBankAccount(input);
2112
+ }
2113
+ /**
2114
+ * Update an existing bank account.
2115
+ *
2116
+ * @example
2117
+ * ```ts
2118
+ * const updated = await peppol.bankAccounts.update("123", { name: "Updated Name" });
2119
+ * ```
2120
+ */
2121
+ async update(id, input) {
2122
+ return this.adapter.updateBankAccount(id, input);
2123
+ }
2124
+ /**
2125
+ * Delete a bank account.
2126
+ *
2127
+ * @example
2128
+ * ```ts
2129
+ * await peppol.bankAccounts.delete("123");
2130
+ * ```
2131
+ */
2132
+ async delete(id) {
2133
+ return this.adapter.deleteBankAccount(id);
2134
+ }
2135
+ /**
2136
+ * Async iterator over all bank accounts, automatically handling pagination.
2137
+ *
2138
+ * @example
2139
+ * ```ts
2140
+ * for await (const account of peppol.bankAccounts.listAll()) {
2141
+ * console.log(account.name, account.iban);
2142
+ * }
2143
+ * ```
2144
+ */
2145
+ listAll(options) {
2146
+ return paginate((offset, limit) => this.adapter.listBankAccounts({ ...options, offset, limit }));
2147
+ }
2148
+ };
2149
+ var TransportOperations = class {
2150
+ adapter;
2151
+ constructor(adapter) {
2152
+ this.adapter = adapter;
2153
+ }
2154
+ /**
2155
+ * List all available transport types in the network.
2156
+ * Returns global transport types (not account-scoped).
2157
+ *
2158
+ * @example
2159
+ * ```ts
2160
+ * const types = await peppol.transports.listTypes();
2161
+ * console.log(types); // [{ code: "peppol", name: "Peppol BIS 3.0" }, ...]
2162
+ * ```
2163
+ */
2164
+ async listTypes() {
2165
+ return this.adapter.listTransportTypes();
2166
+ }
2167
+ /**
2168
+ * List configured transports for this account.
2169
+ *
2170
+ * @example
2171
+ * ```ts
2172
+ * const transports = await peppol.transports.list();
2173
+ * console.log(transports); // [{ id: "t-1", transportTypeCode: "peppol", name: "..." }, ...]
2174
+ * ```
2175
+ */
2176
+ async list() {
2177
+ return this.adapter.listTransports();
2178
+ }
2179
+ /**
2180
+ * Get a single transport by code.
2181
+ *
2182
+ * @example
2183
+ * ```ts
2184
+ * const transport = await peppol.transports.get("peppol");
2185
+ * ```
2186
+ */
2187
+ async get(code) {
2188
+ return this.adapter.getTransport(code);
2189
+ }
2190
+ /**
2191
+ * Create a new transport.
2192
+ *
2193
+ * @example
2194
+ * ```ts
2195
+ * const transport = await peppol.transports.create({
2196
+ * transportTypeCode: "peppol",
2197
+ * email: "billing@acme.com",
2198
+ * });
2199
+ * ```
2200
+ */
2201
+ async create(input) {
2202
+ return this.adapter.createTransport(input);
2203
+ }
2204
+ /**
2205
+ * Update an existing transport.
2206
+ *
2207
+ * @example
2208
+ * ```ts
2209
+ * const transport = await peppol.transports.update("peppol", { email: "new@acme.com" });
2210
+ * ```
2211
+ */
2212
+ async update(code, input) {
2213
+ return this.adapter.updateTransport(code, input);
2214
+ }
2215
+ /**
2216
+ * Delete a transport.
2217
+ *
2218
+ * @example
2219
+ * ```ts
2220
+ * await peppol.transports.delete("peppol");
2221
+ * ```
2222
+ */
2223
+ async delete(code) {
2224
+ return this.adapter.deleteTransport(code);
2225
+ }
2226
+ };
2227
+
2228
+ // ../sdk/dist/core/code-lists.js
2229
+ var EAS_SCHEMES = [
2230
+ { code: "0002", name: "System Information et Repertoire des Entreprises et des Etablissements (SIRENE)", country: "FR" },
2231
+ { code: "0007", name: "Organisationsnummer", country: "SE" },
2232
+ { code: "0009", name: "SIRET-CODE", country: "FR" },
2233
+ { code: "0088", name: "EAN Location Code (GLN)" },
2234
+ { code: "0096", name: "Danish Chamber of Commerce (P-nummer)", country: "DK" },
2235
+ { code: "0184", name: "Danish Central Business Register (CVR)", country: "DK" },
2236
+ { code: "0190", name: "Dutch Chamber of Commerce (KVK)", country: "NL" },
2237
+ { code: "0191", name: "Organisatie Identificatie Nummer (OIN)", country: "NL" },
2238
+ { code: "0192", name: "Danish SE-number (Erhvervsstyrelsen)", country: "DK" },
2239
+ { code: "0195", name: "Singapore Unique Entity Number (UEN)", country: "SG" },
2240
+ { code: "0196", name: "Icelandic Kennitala", country: "IS" },
2241
+ { code: "0198", name: "Danish ERST id (Erhvervsstyrelsen)", country: "DK" },
2242
+ { code: "0200", name: "Lithuanian Legal Entity Register (GRIS)", country: "LT" },
2243
+ { code: "0201", name: "Italian Codice Destinatario", country: "IT" },
2244
+ { code: "0202", name: "Italian Fiscal Code (Codice Fiscale)", country: "IT" },
2245
+ { code: "0204", name: "German Leitweg-ID", country: "DE" },
2246
+ { code: "0208", name: "Belgian Enterprise Number (KBO/BCE)", country: "BE" },
2247
+ { code: "0209", name: "German Creditor Identifier (GS1)", country: "DE" },
2248
+ { code: "0210", name: "Italian Codice Fiscale (per IPA)", country: "IT" },
2249
+ { code: "0211", name: "Italian Partita IVA (VAT number)", country: "IT" },
2250
+ { code: "0212", name: "Finnish OVT code", country: "FI" },
2251
+ { code: "0213", name: "Finnish OP identifier", country: "FI" }
2252
+ ];
2253
+ var EAS_BY_CODE = new Map(EAS_SCHEMES.map((s) => [s.code, s]));
2254
+ var UNIT_ALIAS_MAP = {
2255
+ each: "EA",
2256
+ piece: "EA",
2257
+ pieces: "EA",
2258
+ hour: "HUR",
2259
+ hours: "HUR",
2260
+ day: "DAY",
2261
+ days: "DAY",
2262
+ week: "WEE",
2263
+ weeks: "WEE",
2264
+ month: "MON",
2265
+ months: "MON",
2266
+ year: "ANN",
2267
+ years: "ANN",
2268
+ kilogram: "KGM",
2269
+ kg: "KGM",
2270
+ meter: "MTR",
2271
+ metre: "MTR",
2272
+ liter: "LTR",
2273
+ litre: "LTR",
2274
+ unit: "C62",
2275
+ units: "C62",
2276
+ set: "SET",
2277
+ sets: "SET",
2278
+ pack: "PK",
2279
+ packs: "PK",
2280
+ minute: "MIN",
2281
+ minutes: "MIN",
2282
+ second: "SEC",
2283
+ seconds: "SEC",
2284
+ tonne: "TNE",
2285
+ ton: "TNE",
2286
+ "square metre": "MTK",
2287
+ "square meter": "MTK",
2288
+ sqm: "MTK"
2289
+ };
2290
+ var UNIT_CODES = /* @__PURE__ */ new Map([
2291
+ ["EA", "Each"],
2292
+ ["HUR", "Hour"],
2293
+ ["DAY", "Day"],
2294
+ ["WEE", "Week"],
2295
+ ["MON", "Month"],
2296
+ ["ANN", "Year"],
2297
+ ["MIN", "Minute"],
2298
+ ["SEC", "Second"],
2299
+ ["KGM", "Kilogram"],
2300
+ ["MTR", "Metre"],
2301
+ ["LTR", "Litre"],
2302
+ ["MTK", "Square metre"],
2303
+ ["TNE", "Tonne"],
2304
+ ["C62", "One (unit)"],
2305
+ ["SET", "Set"],
2306
+ ["PK", "Pack"]
2307
+ ]);
2308
+ function resolveUnit(input) {
2309
+ return UNIT_ALIAS_MAP[input.toLowerCase()] ?? input;
2310
+ }
2311
+ function getAllUnits() {
2312
+ return Array.from(UNIT_CODES.entries()).map(([code, name]) => ({ code, name })).sort((a, b) => a.code.localeCompare(b.code));
2313
+ }
2314
+
2315
+ // ../sdk/dist/core/schematron.js
2316
+ function violation(ruleId, severity, message, field) {
2317
+ return { ruleId, severity, message, field };
2318
+ }
2319
+ var _knownUnitCodes;
2320
+ function getKnownUnitCodes() {
2321
+ if (!_knownUnitCodes) {
2322
+ _knownUnitCodes = new Set(getAllUnits().map((u) => u.code));
2323
+ }
2324
+ return _knownUnitCodes;
2325
+ }
2326
+ var VALID_VAT_CATEGORIES = /* @__PURE__ */ new Set(["S", "Z", "E", "AE", "K", "G", "O", "L", "M"]);
2327
+ function computeLineNet(line) {
2328
+ const baseQty = line.baseQuantity ?? 1;
2329
+ if (baseQty === 0)
2330
+ return NaN;
2331
+ const baseAmount = line.quantity * line.unitPrice / baseQty;
2332
+ const chargeTotal = (line.charges ?? []).reduce((sum, c) => sum + c.amount, 0);
2333
+ const allowanceTotal = (line.allowances ?? []).reduce((sum, a) => sum + a.amount, 0);
2334
+ return baseAmount + chargeTotal - allowanceTotal;
2335
+ }
2336
+ var br02 = (input) => {
2337
+ if (!input.number?.trim()) {
2338
+ return [violation("BR-02", "error", "Invoice number is required.", "number")];
2339
+ }
2340
+ return [];
2341
+ };
2342
+ var br03 = (input) => {
2343
+ if (!input.date) {
2344
+ return [
2345
+ violation("BR-03", "warning", "Invoice issue date is not set. The SDK will default to today's date.", "date")
2346
+ ];
2347
+ }
2348
+ return [];
2349
+ };
2350
+ var br06 = (input) => {
2351
+ if (input.from && !input.from.vatNumber) {
2352
+ return [
2353
+ violation("BR-06", "warning", "Seller party has no VAT number. The gateway will use the account's VAT registration.", "from.vatNumber")
2354
+ ];
2355
+ }
2356
+ return [];
2357
+ };
2358
+ var br07 = (input) => {
2359
+ if (!input.to?.name?.trim()) {
2360
+ return [violation("BR-07", "error", "Buyer name is required.", "to.name")];
2361
+ }
2362
+ return [];
2363
+ };
2364
+ var br08 = (input) => {
2365
+ if (!input.lines || input.lines.length === 0) {
2366
+ return [violation("BR-08", "error", "Invoice must have at least one line item.", "lines")];
2367
+ }
2368
+ return [];
2369
+ };
2370
+ var br09 = (input) => {
2371
+ if (!input.dueDate && !input.paymentTerms) {
2372
+ return [
2373
+ violation("BR-09", "warning", "Neither due date nor payment terms specified. At least one is recommended.", "dueDate")
2374
+ ];
2375
+ }
2376
+ return [];
2377
+ };
2378
+ var br10 = (input) => {
2379
+ if (!input.buyerReference && !input.orderReference) {
2380
+ return [
2381
+ violation("BR-10", "warning", "Neither buyerReference nor orderReference specified. Peppol BIS 3.0 requires at least one.", "buyerReference")
2382
+ ];
2383
+ }
2384
+ return [];
2385
+ };
2386
+ var brCo10 = (input) => {
2387
+ const violations = [];
2388
+ if (!input.lines)
2389
+ return violations;
2390
+ for (let i = 0; i < input.lines.length; i++) {
2391
+ const line = input.lines[i];
2392
+ const net = computeLineNet(line);
2393
+ if (!Number.isFinite(net)) {
2394
+ const baseQty = line.baseQuantity ?? 1;
2395
+ const detail = baseQty === 0 ? "baseQuantity is 0, causing division by zero." : "Computed line amount is not a finite number.";
2396
+ violations.push(violation("BR-CO-10", "error", `Line ${i}: invalid net amount. ${detail}`, `lines[${i}]`));
2397
+ }
2398
+ }
2399
+ return violations;
2400
+ };
2401
+ var brCo13 = (input) => {
2402
+ if (!input.lines || input.lines.length === 0)
2403
+ return [];
2404
+ let totalVat = 0;
2405
+ for (let i = 0; i < input.lines.length; i++) {
2406
+ const line = input.lines[i];
2407
+ const net = computeLineNet(line);
2408
+ if (!Number.isFinite(net))
2409
+ continue;
2410
+ totalVat += net * (line.vatRate / 100);
2411
+ }
2412
+ for (const allowance of input.allowances ?? []) {
2413
+ totalVat -= allowance.amount * (allowance.vatRate / 100);
2414
+ }
2415
+ for (const charge of input.charges ?? []) {
2416
+ totalVat += charge.amount * (charge.vatRate / 100);
2417
+ }
2418
+ if (!Number.isFinite(totalVat)) {
2419
+ return [
2420
+ violation("BR-CO-13", "error", "Computed total VAT amount is not a finite number. Check line amounts and VAT rates.")
2421
+ ];
2422
+ }
2423
+ if (totalVat < -0.01) {
2424
+ return [
2425
+ violation("BR-CO-13", "warning", `Computed total VAT is negative (${totalVat.toFixed(2)}). This is unusual for an invoice.`)
2426
+ ];
2427
+ }
2428
+ return [];
2429
+ };
2430
+ var brCo15 = (input) => {
2431
+ if (!input.lines || input.lines.length === 0)
2432
+ return [];
2433
+ let lineTotal = 0;
2434
+ let vatTotal = 0;
2435
+ for (const line of input.lines) {
2436
+ const net = computeLineNet(line);
2437
+ if (!Number.isFinite(net))
2438
+ continue;
2439
+ lineTotal += net;
2440
+ vatTotal += net * (line.vatRate / 100);
2441
+ }
2442
+ for (const allowance of input.allowances ?? []) {
2443
+ lineTotal -= allowance.amount;
2444
+ vatTotal -= allowance.amount * (allowance.vatRate / 100);
2445
+ }
2446
+ for (const charge of input.charges ?? []) {
2447
+ lineTotal += charge.amount;
2448
+ vatTotal += charge.amount * (charge.vatRate / 100);
2449
+ }
2450
+ const taxInclusive = lineTotal + vatTotal;
2451
+ if (!Number.isFinite(taxInclusive)) {
2452
+ return [
2453
+ violation("BR-CO-15", "error", "Computed tax-inclusive amount is not a finite number.")
2454
+ ];
2455
+ }
2456
+ if (taxInclusive < -0.01) {
2457
+ return [
2458
+ violation("BR-CO-15", "warning", `Computed tax-inclusive amount is negative (${taxInclusive.toFixed(2)}). Consider using a credit note instead.`)
2459
+ ];
2460
+ }
2461
+ return [];
2462
+ };
2463
+ var brCo16 = (input) => {
2464
+ if (!input.lines || input.lines.length === 0)
2465
+ return [];
2466
+ let lineTotal = 0;
2467
+ let vatTotal = 0;
2468
+ for (const line of input.lines) {
2469
+ const net = computeLineNet(line);
2470
+ if (!Number.isFinite(net))
2471
+ continue;
2472
+ lineTotal += net;
2473
+ vatTotal += net * (line.vatRate / 100);
2474
+ }
2475
+ for (const allowance of input.allowances ?? []) {
2476
+ lineTotal -= allowance.amount;
2477
+ vatTotal -= allowance.amount * (allowance.vatRate / 100);
2478
+ }
2479
+ for (const charge of input.charges ?? []) {
2480
+ lineTotal += charge.amount;
2481
+ vatTotal += charge.amount * (charge.vatRate / 100);
2482
+ }
2483
+ const taxInclusive = lineTotal + vatTotal;
2484
+ const prepaid = input.prepaidAmount ?? 0;
2485
+ const rounding = input.roundingAmount ?? 0;
2486
+ const payable = taxInclusive - prepaid + rounding;
2487
+ if (!Number.isFinite(payable)) {
2488
+ return [
2489
+ violation("BR-CO-16", "error", "Computed payable amount is not a finite number.")
2490
+ ];
2491
+ }
2492
+ if (payable < -0.01) {
2493
+ return [
2494
+ violation("BR-CO-16", "warning", `Computed payable amount is negative (${payable.toFixed(2)}). Prepaid amount (${prepaid}) exceeds the invoice total.`)
2495
+ ];
2496
+ }
2497
+ return [];
2498
+ };
2499
+ var brS01 = (input) => {
2500
+ const violations = [];
2501
+ if (!input.lines)
2502
+ return violations;
2503
+ for (let i = 0; i < input.lines.length; i++) {
2504
+ const line = input.lines[i];
2505
+ const category = line.vatCategory ?? "S";
2506
+ if (category === "S" && (line.vatRate === void 0 || line.vatRate <= 0)) {
2507
+ violations.push(violation("BR-S-01", "error", `Line ${i}: standard rate (S) requires vatRate > 0, got ${line.vatRate ?? "undefined"}.`, `lines[${i}].vatRate`));
2508
+ }
2509
+ }
2510
+ return violations;
2511
+ };
2512
+ var brS05 = (input) => {
2513
+ const violations = [];
2514
+ if (!input.lines)
2515
+ return violations;
2516
+ for (let i = 0; i < input.lines.length; i++) {
2517
+ const line = input.lines[i];
2518
+ if (line.vatCategory === "Z" && line.vatRate !== 0) {
2519
+ violations.push(violation("BR-S-05", "error", `Line ${i}: zero-rated (Z) requires vatRate = 0, got ${line.vatRate}.`, `lines[${i}].vatRate`));
2520
+ }
2521
+ }
2522
+ return violations;
2523
+ };
2524
+ var brS06 = (input) => {
2525
+ const violations = [];
2526
+ if (!input.lines)
2527
+ return violations;
2528
+ for (let i = 0; i < input.lines.length; i++) {
2529
+ const line = input.lines[i];
2530
+ if (line.vatCategory === "E" && line.vatRate !== 0) {
2531
+ violations.push(violation("BR-S-06", "error", `Line ${i}: exempt (E) requires vatRate = 0, got ${line.vatRate}.`, `lines[${i}].vatRate`));
2532
+ }
2533
+ }
2534
+ return violations;
2535
+ };
2536
+ var brS08 = (input) => {
2537
+ const violations = [];
2538
+ if (!input.lines)
2539
+ return violations;
2540
+ for (let i = 0; i < input.lines.length; i++) {
2541
+ const line = input.lines[i];
2542
+ if (line.vatCategory === "AE" && line.vatRate !== 0) {
2543
+ violations.push(violation("BR-S-08", "error", `Line ${i}: reverse charge (AE) requires vatRate = 0, got ${line.vatRate}.`, `lines[${i}].vatRate`));
2544
+ }
2545
+ }
2546
+ return violations;
2547
+ };
2548
+ var peppolR004 = (input) => {
2549
+ if (!input.to?.peppolId) {
2550
+ return [
2551
+ violation("PEPPOL-EN16931-R004", "error", "Buyer electronic address (peppolId) is required for Peppol delivery.", "to.peppolId")
2552
+ ];
26
2553
  }
27
- }
28
-
29
- // src/formatters/validation.ts
30
- import pc from "picocolors";
31
- function sectionHeader(title) {
32
- const pad = 45 - title.length - 4;
33
- return pc.dim(`\u2500\u2500 ${title} ${"\u2500".repeat(Math.max(pad, 3))}`);
34
- }
35
- function formatError(item) {
36
- const ruleId = "ruleId" in item && item.ruleId ? pc.dim(` (${item.ruleId})`) : "";
37
- const field = "field" in item && item.field ? `${item.field} \u2014 ` : "";
38
- return ` ${pc.red("\u2717")} ${field}${item.message}${ruleId}`;
39
- }
40
- function formatWarning(item) {
41
- const ruleId = "ruleId" in item && item.ruleId ? pc.dim(` (${item.ruleId})`) : "";
42
- const field = "field" in item && item.field ? `${item.field} \u2014 ` : "";
43
- return ` ${pc.yellow("\u26A0")} ${field}${item.message}${ruleId}`;
44
- }
45
- function formatSection(title, errors, warnings) {
46
- const lines = [sectionHeader(title)];
47
- if (errors.length === 0 && warnings.length === 0) {
48
- lines.push(` ${pc.green("\u2713")} All rules passed`);
49
- return lines.join("\n");
2554
+ return [];
2555
+ };
2556
+ var peppolR006 = (input) => {
2557
+ const violations = [];
2558
+ if (!input.lines)
2559
+ return violations;
2560
+ for (let i = 0; i < input.lines.length; i++) {
2561
+ const line = input.lines[i];
2562
+ const cat = line.vatCategory;
2563
+ if (cat !== void 0 && !VALID_VAT_CATEGORIES.has(cat)) {
2564
+ violations.push(violation("PEPPOL-EN16931-R006", "error", `Line ${i}: invalid VAT category "${cat}". Must be one of: S, Z, E, AE, K, G, O, L, M.`, `lines[${i}].vatCategory`));
2565
+ }
50
2566
  }
51
- if (errors.length === 0) {
52
- lines.push(` ${pc.green("\u2713")} No errors`);
2567
+ for (let i = 0; i < (input.allowances ?? []).length; i++) {
2568
+ const cat = input.allowances[i].vatCategory;
2569
+ if (cat !== void 0 && !VALID_VAT_CATEGORIES.has(cat)) {
2570
+ violations.push(violation("PEPPOL-EN16931-R006", "error", `Allowance ${i}: invalid VAT category "${cat}".`, `allowances[${i}].vatCategory`));
2571
+ }
53
2572
  }
54
- for (const err of errors) {
55
- lines.push(formatError(err));
2573
+ for (let i = 0; i < (input.charges ?? []).length; i++) {
2574
+ const cat = input.charges[i].vatCategory;
2575
+ if (cat !== void 0 && !VALID_VAT_CATEGORIES.has(cat)) {
2576
+ violations.push(violation("PEPPOL-EN16931-R006", "error", `Charge ${i}: invalid VAT category "${cat}".`, `charges[${i}].vatCategory`));
2577
+ }
56
2578
  }
57
- for (const warn of warnings) {
58
- lines.push(formatWarning(warn));
2579
+ return violations;
2580
+ };
2581
+ var peppolR080 = (input) => {
2582
+ const violations = [];
2583
+ if (!input.lines)
2584
+ return violations;
2585
+ const knownCodes = getKnownUnitCodes();
2586
+ for (let i = 0; i < input.lines.length; i++) {
2587
+ const line = input.lines[i];
2588
+ if (line.unit) {
2589
+ const resolved = resolveUnit(line.unit);
2590
+ if (!knownCodes.has(resolved)) {
2591
+ 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`));
2592
+ }
2593
+ }
59
2594
  }
60
- return lines.join("\n");
61
- }
62
- function formatValidationResult(filename, result) {
63
- const lines = [];
64
- lines.push(`
65
- Validating: ${pc.bold(filename)}
66
- `);
67
- lines.push(
68
- formatSection(
69
- "Structure",
70
- result.structure.errors,
71
- result.structure.warnings
72
- )
73
- );
74
- lines.push("");
75
- lines.push(
76
- formatSection(
77
- "Business Rules (Peppol BIS 3.0)",
78
- result.schematron.errors,
79
- result.schematron.warnings
80
- )
81
- );
82
- lines.push("");
83
- lines.push(
84
- formatSection(
85
- "Country Rules",
86
- result.countryRules.errors,
87
- result.countryRules.warnings
88
- )
89
- );
90
- lines.push("");
91
- lines.push(sectionHeader("Summary"));
92
- const { totalErrors, totalWarnings, valid } = result;
93
- if (valid && totalWarnings === 0) {
94
- lines.push(` ${pc.green(pc.bold("\u2713 Invoice is valid"))}`);
95
- } else if (valid) {
96
- lines.push(
97
- ` ${pc.green(pc.bold("\u2713 Invoice is valid"))} ${pc.dim(`(${totalWarnings} warning${totalWarnings === 1 ? "" : "s"})`)}`
98
- );
99
- } else {
100
- const parts = [];
101
- parts.push(`${totalErrors} error${totalErrors === 1 ? "" : "s"}`);
102
- if (totalWarnings > 0) {
103
- parts.push(`${totalWarnings} warning${totalWarnings === 1 ? "" : "s"}`);
2595
+ return violations;
2596
+ };
2597
+ var ALL_RULES = [
2598
+ // Required fields (BR)
2599
+ br02,
2600
+ br03,
2601
+ br06,
2602
+ br07,
2603
+ br08,
2604
+ br09,
2605
+ br10,
2606
+ // Calculations (BR-CO)
2607
+ brCo10,
2608
+ brCo13,
2609
+ brCo15,
2610
+ brCo16,
2611
+ // Tax categories (BR-S)
2612
+ brS01,
2613
+ brS05,
2614
+ brS06,
2615
+ brS08,
2616
+ // Peppol-specific
2617
+ peppolR004,
2618
+ peppolR006,
2619
+ peppolR080
2620
+ ];
2621
+ function validateSchematron(input) {
2622
+ const errors = [];
2623
+ const warnings = [];
2624
+ for (const rule of ALL_RULES) {
2625
+ const violations = rule(input);
2626
+ for (const v of violations) {
2627
+ if (v.severity === "error") {
2628
+ errors.push(v);
2629
+ } else {
2630
+ warnings.push(v);
2631
+ }
104
2632
  }
105
- lines.push(
106
- ` ${pc.red(pc.bold(`\u2717 ${parts.join(", ")}`))} \u2014 invoice non-compliant`
107
- );
108
2633
  }
109
- lines.push("");
110
- return lines.join("\n");
2634
+ return {
2635
+ valid: errors.length === 0,
2636
+ errors,
2637
+ warnings
2638
+ };
111
2639
  }
112
2640
 
113
2641
  // src/commands/validate.ts
114
- import {
115
- validateInvoice,
116
- validateSchematron,
117
- validateCountryRules
118
- } from "@getpeppr/sdk";
119
2642
  function runValidation(input) {
120
2643
  const structure = validateInvoice(input);
121
2644
  const schematron = validateSchematron(input);
@@ -136,18 +2659,7 @@ function runValidation(input) {
136
2659
  }
137
2660
  function registerValidateCommand(program2) {
138
2661
  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) => {
139
- const parseResult = readJsonFile(file);
140
- if (!parseResult.ok) {
141
- process.stderr.write(parseResult.error + "\n");
142
- process.exit(2);
143
- }
144
- if (typeof parseResult.data !== "object" || parseResult.data === null || Array.isArray(parseResult.data)) {
145
- process.stderr.write(
146
- "Error: JSON file must contain an object, not an array or primitive\n"
147
- );
148
- process.exit(2);
149
- }
150
- const input = parseResult.data;
2662
+ const input = readAndValidateInvoiceJson(file);
151
2663
  const result = runValidation(input);
152
2664
  if (options.quiet) {
153
2665
  process.exit(result.valid ? 0 : 1);
@@ -249,10 +2761,9 @@ function registerInitCommand(program2) {
249
2761
  (filename, options) => {
250
2762
  const resolved = resolve2(filename);
251
2763
  if (existsSync2(resolved) && !options.force) {
252
- console.error(
2764
+ exitWithError(
253
2765
  `Error: ${filename} already exists. Use --force to overwrite.`
254
2766
  );
255
- process.exit(2);
256
2767
  }
257
2768
  const template = options.creditNote ? CREDIT_NOTE_TEMPLATE : INVOICE_TEMPLATE;
258
2769
  try {
@@ -262,15 +2773,15 @@ function registerInitCommand(program2) {
262
2773
  "utf-8"
263
2774
  );
264
2775
  } catch {
265
- console.error(`Error: could not write file \u2014 ${resolved}`);
266
- process.exit(2);
2776
+ exitWithError(`Error: could not write file \u2014 ${resolved}`);
267
2777
  }
268
- console.error(`${pc2.green("\u2713")} Created ${filename}
2778
+ process.stderr.write(`${pc2.green("\u2713")} Created ${filename}
269
2779
 
270
2780
  Next steps:
271
2781
  1. Edit the file with your invoice data
272
2782
  2. Validate: getpeppr validate ${filename}
273
- 3. Convert to XML: getpeppr convert ${filename}`);
2783
+ 3. Convert to XML: getpeppr convert ${filename}
2784
+ `);
274
2785
  process.exit(0);
275
2786
  }
276
2787
  );
@@ -279,27 +2790,12 @@ function registerInitCommand(program2) {
279
2790
  // src/commands/convert.ts
280
2791
  import { writeFileSync as writeFileSync2 } from "fs";
281
2792
  import pc3 from "picocolors";
282
- import {
283
- buildInvoiceXml,
284
- buildCreditNoteXml
285
- } from "@getpeppr/sdk";
286
2793
  function registerConvertCommand(program2) {
287
2794
  program2.command("convert").description(
288
2795
  "Convert a getpeppr JSON invoice to Peppol BIS 3.0 UBL XML"
289
2796
  ).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(
290
2797
  async (file, options) => {
291
- const parseResult = readJsonFile(file);
292
- if (!parseResult.ok) {
293
- process.stderr.write(parseResult.error + "\n");
294
- process.exit(2);
295
- }
296
- if (typeof parseResult.data !== "object" || parseResult.data === null || Array.isArray(parseResult.data)) {
297
- process.stderr.write(
298
- "Error: JSON file must contain an object, not an array or primitive\n"
299
- );
300
- process.exit(2);
301
- }
302
- const input = parseResult.data;
2798
+ const input = readAndValidateInvoiceJson(file);
303
2799
  if (options.validate) {
304
2800
  const result = runValidation(input);
305
2801
  const formatted = formatValidationResult(file, result);
@@ -321,9 +2817,7 @@ function registerConvertCommand(program2) {
321
2817
  }
322
2818
  } catch (err) {
323
2819
  const message = err instanceof Error ? err.message : "Unknown error";
324
- process.stderr.write(`Error: XML generation failed \u2014 ${message}
325
- `);
326
- process.exit(2);
2820
+ exitWithError(`Error: XML generation failed \u2014 ${message}`);
327
2821
  }
328
2822
  xml = xml.replace(/^[ \t]*\n/gm, "");
329
2823
  const docType = isCreditNote ? "UBL 2.1 CreditNote" : "UBL 2.1 Invoice";
@@ -545,19 +3039,14 @@ function registerLookupCommand(program2) {
545
3039
  const isSearch = Boolean(options.name);
546
3040
  const isLookup = Boolean(peppolId);
547
3041
  if (!isSearch && !isLookup) {
548
- process.stderr.write(
549
- "Provide a Peppol ID or use --name to search.\n"
550
- );
551
- process.exit(2);
3042
+ exitWithError("Provide a Peppol ID or use --name to search.");
552
3043
  }
553
3044
  if (options.country) {
554
3045
  const normalized = options.country.toUpperCase();
555
3046
  if (!/^[A-Z]{2}$/.test(normalized)) {
556
- process.stderr.write(
557
- `Invalid country code "${options.country}". Must be 2 letters (e.g. BE, DE, FR).
558
- `
3047
+ exitWithError(
3048
+ `Invalid country code "${options.country}". Must be 2 letters (e.g. BE, DE, FR).`
559
3049
  );
560
- process.exit(2);
561
3050
  }
562
3051
  options.country = normalized;
563
3052
  }
@@ -572,18 +3061,15 @@ function registerLookupCommand(program2) {
572
3061
  async function handleLookup(peppolId, options) {
573
3062
  const parsed = validatePeppolId(peppolId);
574
3063
  if (!parsed.ok) {
575
- process.stderr.write(parsed.error + "\n");
576
- process.exit(2);
3064
+ exitWithError(parsed.error);
577
3065
  }
578
3066
  let result;
579
3067
  try {
580
3068
  result = await lookupParticipant(parsed.scheme, parsed.id);
581
3069
  } catch {
582
- process.stderr.write(
583
- `${pc4.red("\u2717")} Could not reach Peppol Directory. Check your internet connection.
584
- `
3070
+ exitWithError(
3071
+ `${pc4.red("\u2717")} Could not reach Peppol Directory. Check your internet connection.`
585
3072
  );
586
- process.exit(2);
587
3073
  }
588
3074
  if (!result) {
589
3075
  if (options.json) {
@@ -605,11 +3091,9 @@ async function handleLookup(peppolId, options) {
605
3091
  }
606
3092
  async function handleSearch(options) {
607
3093
  if (options.name && options.name.length < 3) {
608
- process.stderr.write(
609
- `Search name must be at least 3 characters. Got: "${options.name}"
610
- `
3094
+ exitWithError(
3095
+ `Search name must be at least 3 characters. Got: "${options.name}"`
611
3096
  );
612
- process.exit(2);
613
3097
  }
614
3098
  const limit = parseInt(options.limit ?? "10", 10);
615
3099
  let result;
@@ -620,11 +3104,9 @@ async function handleSearch(options) {
620
3104
  limit
621
3105
  });
622
3106
  } catch {
623
- process.stderr.write(
624
- `${pc4.red("\u2717")} Could not reach Peppol Directory. Check your internet connection.
625
- `
3107
+ exitWithError(
3108
+ `${pc4.red("\u2717")} Could not reach Peppol Directory. Check your internet connection.`
626
3109
  );
627
- process.exit(2);
628
3110
  }
629
3111
  if (result.matches.length === 0) {
630
3112
  if (options.json) {
@@ -642,6 +3124,522 @@ async function handleSearch(options) {
642
3124
  process.exit(0);
643
3125
  }
644
3126
 
3127
+ // src/commands/send.ts
3128
+ import pc6 from "picocolors";
3129
+
3130
+ // src/lib/credentials-store.ts
3131
+ import {
3132
+ chmodSync,
3133
+ existsSync as existsSync3,
3134
+ mkdirSync,
3135
+ readFileSync as readFileSync2,
3136
+ rmSync,
3137
+ statSync,
3138
+ writeFileSync as writeFileSync3,
3139
+ renameSync
3140
+ } from "fs";
3141
+ import { homedir, platform } from "os";
3142
+ import { dirname, join } from "path";
3143
+ function getCredentialsPath() {
3144
+ if (platform() === "win32") {
3145
+ const appdata = process.env.APPDATA ?? join(homedir(), "AppData", "Roaming");
3146
+ return join(appdata, "getpeppr", "credentials.json");
3147
+ }
3148
+ const xdg = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
3149
+ return join(xdg, "getpeppr", "credentials.json");
3150
+ }
3151
+ function readCredentials() {
3152
+ const path = getCredentialsPath();
3153
+ if (!existsSync3(path)) return null;
3154
+ if (platform() !== "win32") {
3155
+ const stats = statSync(path);
3156
+ const mode = stats.mode & 511;
3157
+ if (mode !== 384) {
3158
+ process.stderr.write(
3159
+ `\u26A0 Config file mode was ${mode.toString(8)}; restoring to 600.
3160
+ `
3161
+ );
3162
+ chmodSync(path, 384);
3163
+ }
3164
+ }
3165
+ let raw;
3166
+ try {
3167
+ raw = readFileSync2(path, "utf-8");
3168
+ } catch {
3169
+ process.stderr.write(`\u26A0 Could not read config file: ${path}
3170
+ `);
3171
+ return null;
3172
+ }
3173
+ try {
3174
+ const data = JSON.parse(raw);
3175
+ return data;
3176
+ } catch {
3177
+ process.stderr.write(`\u26A0 Malformed JSON in config file: ${path}
3178
+ `);
3179
+ return null;
3180
+ }
3181
+ }
3182
+ function writeCredentials(creds) {
3183
+ const path = getCredentialsPath();
3184
+ const dir = dirname(path);
3185
+ mkdirSync(dir, { recursive: true, mode: 448 });
3186
+ const tmpPath = `${path}.tmp`;
3187
+ const data = JSON.stringify(creds, null, 2) + "\n";
3188
+ try {
3189
+ rmSync(tmpPath, { force: true });
3190
+ } catch {
3191
+ }
3192
+ writeFileSync3(tmpPath, data, { mode: 384, flag: "wx" });
3193
+ chmodSync(tmpPath, 384);
3194
+ try {
3195
+ renameSync(tmpPath, path);
3196
+ } catch (e) {
3197
+ try {
3198
+ rmSync(tmpPath, { force: true });
3199
+ } catch {
3200
+ }
3201
+ throw e;
3202
+ }
3203
+ }
3204
+ function deleteCredentials() {
3205
+ const path = getCredentialsPath();
3206
+ if (!existsSync3(path)) return false;
3207
+ rmSync(path);
3208
+ return true;
3209
+ }
3210
+
3211
+ // src/lib/auth.ts
3212
+ var AuthError = class extends Error {
3213
+ constructor(message) {
3214
+ super(message);
3215
+ this.name = "AuthError";
3216
+ }
3217
+ };
3218
+ function resolveApiKey(opts) {
3219
+ const env = opts.forceProd ? "live" : "sandbox";
3220
+ if (opts.flagKey) {
3221
+ return { apiKey: opts.flagKey, source: "flag", environment: env };
3222
+ }
3223
+ const envKey = process.env.GETPEPPR_API_KEY;
3224
+ if (envKey) {
3225
+ return { apiKey: envKey, source: "env", environment: env };
3226
+ }
3227
+ const creds = readCredentials();
3228
+ if (creds) {
3229
+ const key = env === "live" ? creds.live : creds.sandbox;
3230
+ if (key) {
3231
+ return { apiKey: key, source: "config", environment: env };
3232
+ }
3233
+ if (env === "live") {
3234
+ throw new AuthError(
3235
+ `Config has no live API key. Run \`getpeppr login\` again with --live, or pass --key.`
3236
+ );
3237
+ }
3238
+ }
3239
+ throw new AuthError(
3240
+ `No API key found. Run \`getpeppr login\` or set GETPEPPR_API_KEY.`
3241
+ );
3242
+ }
3243
+
3244
+ // src/lib/send-payload.ts
3245
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
3246
+ import { resolve as resolve3 } from "path";
3247
+
3248
+ // src/templates/send-default.ts
3249
+ var MINIMAL_PDF = `%PDF-1.0
3250
+ 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
3251
+ xref
3252
+ 0 4
3253
+ 0000000000 65535 f
3254
+ 0000000009 00000 n
3255
+ 0000000058 00000 n
3256
+ 0000000115 00000 n
3257
+ trailer<</Size 4/Root 1 0 R>>
3258
+ startxref
3259
+ 190
3260
+ %%EOF`;
3261
+ var MINIMAL_TEST_PDF_BASE64 = Buffer.from(MINIMAL_PDF).toString("base64");
3262
+ function buildDefaultSendPayload(overrides = {}) {
3263
+ const today = /* @__PURE__ */ new Date();
3264
+ const due = new Date(today.getTime() + 30 * 864e5);
3265
+ const isoToday = today.toISOString().slice(0, 10);
3266
+ const isoDue = due.toISOString().slice(0, 10);
3267
+ const peppolId = overrides.to ?? "9925:BE0314595348";
3268
+ const amount = overrides.amount ?? 100;
3269
+ const currency = overrides.currency ?? "EUR";
3270
+ const description = overrides.description ?? "Test service from getpeppr";
3271
+ const randomSuffix = Math.floor(Math.random() * 65536).toString(16).toUpperCase().padStart(4, "0");
3272
+ const number = `TEST-${Date.now().toString(36).toUpperCase()}-${randomSuffix}`;
3273
+ const country = peppolId.split(":")[1]?.slice(0, 2) ?? "BE";
3274
+ const payload = {
3275
+ number,
3276
+ date: isoToday,
3277
+ dueDate: isoDue,
3278
+ currency,
3279
+ to: {
3280
+ name: peppolId === "9925:BE0314595348" ? "SPF Economie (TEST)" : "Test Recipient",
3281
+ peppolId,
3282
+ country,
3283
+ street: "Rue de la Loi 1",
3284
+ city: "Brussels",
3285
+ postalCode: "1000"
3286
+ },
3287
+ lines: [
3288
+ {
3289
+ description,
3290
+ quantity: 1,
3291
+ unitPrice: amount,
3292
+ vatRate: 0,
3293
+ // vatCategory "O" = "Services outside scope of tax" (UBL 2.1 / EN 16931).
3294
+ // Public entities (SPF Economie) are VAT-exempt. No `taxExemptReason` field
3295
+ // exists in InvoiceLine — Storecove derives exemption from the category code.
3296
+ // If sandbox returns 422 on this combination, fallback is vatRate: 21 + vatCategory: "S".
3297
+ vatCategory: "O"
3298
+ }
3299
+ ]
3300
+ };
3301
+ if (overrides.attachment) {
3302
+ payload.attachments = [
3303
+ {
3304
+ id: "ATT-001",
3305
+ description: "Test document",
3306
+ filename: "test.pdf",
3307
+ mimeType: "application/pdf",
3308
+ content: MINIMAL_TEST_PDF_BASE64
3309
+ }
3310
+ ];
3311
+ }
3312
+ return payload;
3313
+ }
3314
+
3315
+ // src/lib/send-payload.ts
3316
+ var MutexError = class extends Error {
3317
+ constructor() {
3318
+ super("Cannot combine custom file with override flags. Use one or the other.");
3319
+ this.name = "MutexError";
3320
+ }
3321
+ };
3322
+ function hasOverrides(o) {
3323
+ if (!o) return false;
3324
+ return Boolean(
3325
+ o.to != null && o.to !== "" || o.amount != null || o.currency || o.description || o.attachment === true
3326
+ );
3327
+ }
3328
+ function buildPayload(opts) {
3329
+ if (opts.file && hasOverrides(opts.overrides)) {
3330
+ throw new MutexError();
3331
+ }
3332
+ if (opts.file) {
3333
+ const absPath = resolve3(opts.file);
3334
+ if (!existsSync4(absPath)) {
3335
+ throw new Error(`Error: file not found \u2014 ${absPath}`);
3336
+ }
3337
+ let raw;
3338
+ try {
3339
+ raw = readFileSync3(absPath, "utf-8");
3340
+ } catch {
3341
+ throw new Error(`Error: could not read file \u2014 ${absPath}`);
3342
+ }
3343
+ try {
3344
+ return JSON.parse(raw);
3345
+ } catch {
3346
+ throw new Error(`Error: invalid JSON in file \u2014 ${absPath}`);
3347
+ }
3348
+ }
3349
+ return buildDefaultSendPayload(opts.overrides);
3350
+ }
3351
+
3352
+ // src/lib/confirm.ts
3353
+ import { createInterface } from "readline";
3354
+ async function confirmInteractive(opts) {
3355
+ const stdin = opts.stdin ?? process.stdin;
3356
+ const stdout = opts.stdout ?? process.stdout;
3357
+ if (stdin.isTTY !== true) {
3358
+ return opts.defaultYes;
3359
+ }
3360
+ const suffix = opts.defaultYes ? "[Y/n]" : "[y/N]";
3361
+ return new Promise((resolve4) => {
3362
+ const rl = createInterface({ input: stdin, output: stdout });
3363
+ let settled = false;
3364
+ rl.question(`${opts.prompt} ${suffix} `, (answer) => {
3365
+ settled = true;
3366
+ rl.close();
3367
+ const trimmed = answer.trim().toLowerCase();
3368
+ if (trimmed === "") return resolve4(opts.defaultYes);
3369
+ if (trimmed === "y" || trimmed === "yes") return resolve4(true);
3370
+ if (trimmed === "n" || trimmed === "no") return resolve4(false);
3371
+ return resolve4(opts.defaultYes);
3372
+ });
3373
+ rl.once("close", () => {
3374
+ if (!settled) resolve4(opts.defaultYes);
3375
+ });
3376
+ });
3377
+ }
3378
+
3379
+ // src/lib/watch.ts
3380
+ var TERMINAL_STATES = /* @__PURE__ */ new Set([
3381
+ "delivered",
3382
+ "accepted",
3383
+ "rejected",
3384
+ "failed"
3385
+ ]);
3386
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
3387
+ async function pollUntilTerminal(client, documentId, options = {}) {
3388
+ const intervalMs = options.intervalMs ?? 2e3;
3389
+ const timeoutMs = options.timeoutMs ?? 6e4;
3390
+ const start = Date.now();
3391
+ let lastStatus = "";
3392
+ while (Date.now() - start < timeoutMs) {
3393
+ const { status } = await client.invoices.getStatus(documentId);
3394
+ if (status !== lastStatus) {
3395
+ options.onTransition?.(status);
3396
+ lastStatus = status;
3397
+ }
3398
+ if (TERMINAL_STATES.has(status)) {
3399
+ return { finalStatus: status, timedOut: false };
3400
+ }
3401
+ await sleep2(intervalMs);
3402
+ }
3403
+ return { finalStatus: lastStatus, timedOut: true };
3404
+ }
3405
+
3406
+ // src/formatters/send-result.ts
3407
+ import pc5 from "picocolors";
3408
+ function formatSendResult(result, mode) {
3409
+ if (mode === "quiet") return "";
3410
+ if (mode === "json") {
3411
+ return JSON.stringify(result, null, 2);
3412
+ }
3413
+ const lines = [];
3414
+ lines.push(`${pc5.green("\u2713")} Sent ${pc5.bold(result.number)}`);
3415
+ lines.push(` id: ${result.id}`);
3416
+ lines.push(` Status: ${pc5.cyan(result.status)}`);
3417
+ lines.push(` Track: ${pc5.dim(result.dashboardUrl)}`);
3418
+ const wCount = result.warnings?.length ?? 0;
3419
+ if (wCount > 0) {
3420
+ lines.push(` ${pc5.yellow(`${wCount} warning${wCount === 1 ? "" : "s"}`)}`);
3421
+ for (const w of result.warnings ?? []) {
3422
+ lines.push(` ${pc5.yellow("\u26A0")} ${w.message}`);
3423
+ }
3424
+ }
3425
+ return lines.join("\n");
3426
+ }
3427
+
3428
+ // src/commands/send.ts
3429
+ var API_BASE = "https://api.getpeppr.dev/v1";
3430
+ var LOCAL_BASE = "http://localhost:3001/api/v1";
3431
+ var DASHBOARD_BASE = "https://console.getpeppr.dev/invoices";
3432
+ function registerSendCommand(program2) {
3433
+ 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., 0208:BE0314595348)").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 delivered (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) => {
3434
+ let auth;
3435
+ try {
3436
+ auth = resolveApiKey({
3437
+ flagKey: flags.key,
3438
+ forceProd: Boolean(flags.prod),
3439
+ forceLocal: Boolean(flags.local)
3440
+ });
3441
+ } catch (e) {
3442
+ if (e instanceof AuthError) {
3443
+ exitWithError(e.message);
3444
+ return;
3445
+ }
3446
+ throw e;
3447
+ }
3448
+ const overrides = {
3449
+ to: flags.to,
3450
+ amount: flags.amount != null ? Number(flags.amount) : void 0,
3451
+ currency: flags.currency,
3452
+ description: flags.desc,
3453
+ attachment: flags.attachment
3454
+ };
3455
+ let payload;
3456
+ try {
3457
+ payload = buildPayload({ file, overrides });
3458
+ } catch (e) {
3459
+ if (e instanceof MutexError) {
3460
+ exitWithError(e.message);
3461
+ return;
3462
+ }
3463
+ if (e instanceof Error) {
3464
+ exitWithError(e.message);
3465
+ return;
3466
+ }
3467
+ throw e;
3468
+ }
3469
+ if (flags.validate !== false) {
3470
+ const result2 = runValidation(payload);
3471
+ if (!result2.valid) {
3472
+ process.stderr.write(
3473
+ `${pc6.red("\u2717")} Pre-validation failed (${result2.totalErrors} errors). Use --no-validate to skip.
3474
+ `
3475
+ );
3476
+ process.exit(2);
3477
+ }
3478
+ }
3479
+ if (flags.prod && !flags.yes) {
3480
+ const confirmed = await confirmInteractive({
3481
+ prompt: pc6.yellow("\u26A0 About to send a REAL invoice on the Peppol network. Continue?"),
3482
+ defaultYes: false
3483
+ });
3484
+ if (!confirmed) {
3485
+ if (!flags.quiet) process.stderr.write("Cancelled.\n");
3486
+ process.exit(0);
3487
+ }
3488
+ }
3489
+ const baseUrl = flags.local ? LOCAL_BASE : API_BASE;
3490
+ const client = new Peppol({ apiKey: auth.apiKey, baseUrl });
3491
+ let result;
3492
+ try {
3493
+ result = await client.invoices.send(payload);
3494
+ } catch (e) {
3495
+ const msg = e instanceof Error ? e.message : String(e);
3496
+ process.stderr.write(`${pc6.red("\u2717")} ${msg}
3497
+ `);
3498
+ process.exit(1);
3499
+ }
3500
+ const dashboardUrl = `${DASHBOARD_BASE}/${result.id}`;
3501
+ let finalStatus = result.status;
3502
+ let timedOut = false;
3503
+ if (flags.watch) {
3504
+ const onTransition = (s) => {
3505
+ if (!flags.quiet && !flags.json) {
3506
+ process.stderr.write(` ${pc6.cyan("\u2192")} ${s}
3507
+ `);
3508
+ }
3509
+ };
3510
+ try {
3511
+ const w = await pollUntilTerminal(client, result.id, {
3512
+ intervalMs: 2e3,
3513
+ timeoutMs: 6e4,
3514
+ onTransition
3515
+ });
3516
+ finalStatus = w.finalStatus;
3517
+ timedOut = w.timedOut;
3518
+ } catch (e) {
3519
+ const msg = e instanceof Error ? e.message : String(e);
3520
+ process.stderr.write(`${pc6.yellow("\u26A0")} Watch error: ${msg}
3521
+ `);
3522
+ }
3523
+ if (timedOut) {
3524
+ process.stderr.write(
3525
+ `${pc6.yellow("\u26A0")} Timeout \u2014 invoice was sent but delivery not confirmed in 60s.
3526
+ `
3527
+ );
3528
+ }
3529
+ }
3530
+ const mode = flags.quiet ? "quiet" : flags.json ? "json" : "formatted";
3531
+ const output = formatSendResult(
3532
+ {
3533
+ id: result.id,
3534
+ number: payload.number,
3535
+ status: finalStatus,
3536
+ warnings: result.warnings,
3537
+ dashboardUrl
3538
+ },
3539
+ mode
3540
+ );
3541
+ if (output) process.stdout.write(output + "\n");
3542
+ if (finalStatus === "rejected" || finalStatus === "failed") {
3543
+ process.exit(1);
3544
+ }
3545
+ process.exit(0);
3546
+ });
3547
+ }
3548
+
3549
+ // src/commands/login.ts
3550
+ import { createInterface as createInterface2 } from "readline";
3551
+ import pc7 from "picocolors";
3552
+ async function promptMaskedKey(envLabel) {
3553
+ if (process.stdin.isTTY !== true) {
3554
+ exitWithError("Error: --key flag required when stdin is not a TTY (CI mode).");
3555
+ }
3556
+ process.stdout.write(`Paste your ${envLabel} API key (input hidden): `);
3557
+ return new Promise((resolve4) => {
3558
+ let buffer = "";
3559
+ const onData = (chunk) => {
3560
+ const c = chunk.toString("utf-8");
3561
+ if (c === "\n" || c === "\r" || c === "\r\n") {
3562
+ process.stdin.setRawMode(false);
3563
+ process.stdin.removeListener("data", onData);
3564
+ process.stdin.pause();
3565
+ process.stdout.write("\n");
3566
+ resolve4(buffer);
3567
+ return;
3568
+ }
3569
+ if (c === "") {
3570
+ process.stdin.setRawMode(false);
3571
+ process.stdin.removeListener("data", onData);
3572
+ process.stdin.pause();
3573
+ process.stdout.write("\n");
3574
+ process.exit(130);
3575
+ }
3576
+ if (c === "\x7F" || c === "\b") {
3577
+ buffer = buffer.slice(0, -1);
3578
+ return;
3579
+ }
3580
+ buffer += c;
3581
+ };
3582
+ process.stdin.setRawMode(true);
3583
+ process.stdin.resume();
3584
+ process.stdin.on("data", onData);
3585
+ });
3586
+ }
3587
+ async function promptEnvironment() {
3588
+ if (process.stdin.isTTY !== true) return "sandbox";
3589
+ return new Promise((resolve4) => {
3590
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
3591
+ rl.question("Environment? (s)andbox / (l)ive [sandbox]: ", (answer) => {
3592
+ rl.close();
3593
+ const a = answer.trim().toLowerCase();
3594
+ if (a === "l" || a === "live") return resolve4("live");
3595
+ return resolve4("sandbox");
3596
+ });
3597
+ });
3598
+ }
3599
+ function registerLoginCommand(program2) {
3600
+ 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) => {
3601
+ if (!flags.live && !flags.sandbox && process.stdin.isTTY !== true) {
3602
+ exitWithError("Error: --sandbox or --live required when stdin is not a TTY (CI mode).");
3603
+ }
3604
+ let env;
3605
+ if (flags.live) env = "live";
3606
+ else if (flags.sandbox) env = "sandbox";
3607
+ else env = await promptEnvironment();
3608
+ let key;
3609
+ if (flags.key) {
3610
+ key = flags.key;
3611
+ } else {
3612
+ key = (await promptMaskedKey(env)).trim();
3613
+ if (!key) exitWithError("Error: empty API key.");
3614
+ }
3615
+ const existing = readCredentials() ?? {};
3616
+ const next = { ...existing, [env]: key };
3617
+ writeCredentials(next);
3618
+ const path = getCredentialsPath();
3619
+ process.stderr.write(
3620
+ `${pc7.green("\u2713")} Saved ${env} key to ${path} (mode 600)
3621
+ `
3622
+ );
3623
+ });
3624
+ }
3625
+
3626
+ // src/commands/logout.ts
3627
+ import pc8 from "picocolors";
3628
+ function registerLogoutCommand(program2) {
3629
+ program2.command("logout").description("Remove ~/.config/getpeppr/credentials.json").action(() => {
3630
+ const path = getCredentialsPath();
3631
+ const removed = deleteCredentials();
3632
+ if (removed) {
3633
+ process.stderr.write(`${pc8.green("\u2713")} Removed ${path}
3634
+ `);
3635
+ } else {
3636
+ process.stderr.write(`No credentials to remove (${path})
3637
+ `);
3638
+ }
3639
+ process.exit(0);
3640
+ });
3641
+ }
3642
+
645
3643
  // src/index.ts
646
3644
  var require2 = createRequire(import.meta.url);
647
3645
  var { version } = require2("../package.json");
@@ -651,5 +3649,8 @@ registerValidateCommand(program);
651
3649
  registerInitCommand(program);
652
3650
  registerConvertCommand(program);
653
3651
  registerLookupCommand(program);
3652
+ registerSendCommand(program);
3653
+ registerLoginCommand(program);
3654
+ registerLogoutCommand(program);
654
3655
  program.parse();
655
3656
  //# sourceMappingURL=index.js.map