@getpeppr/cli 0.9.0 → 0.11.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
@@ -623,7 +623,10 @@ var ICD_CODES = /* @__PURE__ */ new Set([
623
623
  "0242",
624
624
  "0243",
625
625
  "0244",
626
- "0245"
626
+ "0245",
627
+ "0246",
628
+ "0247",
629
+ "0248"
627
630
  ]);
628
631
  var ISO6523_ICD_CODES = Object.freeze({
629
632
  has: (v) => ICD_CODES.has(v),
@@ -717,10 +720,41 @@ function formatVatRate(vatRate, field) {
717
720
  }
718
721
  return String(vatRate);
719
722
  }
720
- function formatBaseQuantity(baseQuantity, field) {
723
+ function assertValidBaseQuantity(baseQuantity, field) {
721
724
  if (typeof baseQuantity !== "number" || !Number.isFinite(baseQuantity) || baseQuantity <= 0) {
722
725
  throw new UblBuilderInputError("baseQuantity must be a finite number greater than zero.", field, "PEPPOL-EN16931-R121");
723
726
  }
727
+ }
728
+ var ADJUSTMENT_CONTRACT_RULE = "GETPEPPR-ALLOWANCE-CHARGE-AMOUNT";
729
+ var ADJUSTMENT_AMOUNT_MESSAGE = "Allowance and charge amounts must be finite numbers greater than or equal to zero. An allowance reduces the amount and a charge increases it; encode the direction in the field, never in the sign.";
730
+ var DERIVED_AMOUNT_CONTRACT_RULE = "GETPEPPR-DERIVED-AMOUNT";
731
+ var NON_FINITE_DERIVED_AMOUNT_MESSAGE = "Derived amount is not finite (overflow). Reduce quantities, prices or adjustment amounts so every total stays representable.";
732
+ function survivesCents(value) {
733
+ return Number.isFinite(value) && Number.isFinite(value * 100);
734
+ }
735
+ function assertValidAdjustmentAmounts(items, field) {
736
+ if (items === void 0)
737
+ return;
738
+ if (!Array.isArray(items)) {
739
+ throw new UblBuilderInputError(`${field} must be an array \u2014 omit the field instead of sending ${items === null ? "null" : typeof items}`, field, ADJUSTMENT_CONTRACT_RULE);
740
+ }
741
+ for (const [i, item] of items.entries()) {
742
+ const amount = item?.amount;
743
+ if (item === null || typeof item !== "object" || typeof amount !== "number" || !Number.isFinite(amount) || amount < 0) {
744
+ throw new UblBuilderInputError(ADJUSTMENT_AMOUNT_MESSAGE, `${field}[${i}].amount`, ADJUSTMENT_CONTRACT_RULE);
745
+ }
746
+ }
747
+ }
748
+ function assertDocumentAdjustmentAmounts(input) {
749
+ for (const [i, line] of input.lines.entries()) {
750
+ assertValidAdjustmentAmounts(line.allowances, `lines[${i}].allowances`);
751
+ assertValidAdjustmentAmounts(line.charges, `lines[${i}].charges`);
752
+ }
753
+ assertValidAdjustmentAmounts(input.allowances, "allowances");
754
+ assertValidAdjustmentAmounts(input.charges, "charges");
755
+ }
756
+ function formatBaseQuantity(baseQuantity, field) {
757
+ assertValidBaseQuantity(baseQuantity, field);
724
758
  const numeric = String(baseQuantity);
725
759
  const exponentMarker = numeric.search(/[eE]/);
726
760
  if (exponentMarker === -1)
@@ -739,8 +773,9 @@ function formatBaseQuantity(baseQuantity, field) {
739
773
  }
740
774
  return `${digits.slice(0, outputPoint)}.${digits.slice(outputPoint)}`;
741
775
  }
742
- function round2(n) {
743
- return Math.round(n * 100) / 100;
776
+ function roundUblCurrencyAmount(value) {
777
+ const rounded = Math.round((value + Math.sign(value) * Number.EPSILON) * 100) / 100;
778
+ return Object.is(rounded, -0) ? 0 : rounded;
744
779
  }
745
780
  function normalizedTaxExemptReason(vatCategory, reason) {
746
781
  if (!EXEMPTION_REASON_CATEGORIES.has(vatCategory) || typeof reason !== "string") {
@@ -763,8 +798,8 @@ function buildPartyXml(party, role) {
763
798
  <cac:Party>
764
799
  <cbc:EndpointID schemeID="${escapeXml(endpointScheme)}">${escapeXml(endpointId)}</cbc:EndpointID>
765
800
  ${// ⛔ DEUX listes, pas une. `BR-CL-25` juge l'EndpointID ci-dessus contre la
766
- // liste EAS (101 valeurs, jusqu'à `9959`) ; `BR-CL-10` juge CE champ-ci
767
- // contre la liste ISO 6523 ICD (240 valeurs, `0002`–`0245`). `9932` (GB),
801
+ // liste EAS (104 valeurs, jusqu'à `9959`) ; `BR-CL-10` juge CE champ-ci
802
+ // contre la liste ISO 6523 ICD (243 valeurs, `0002`–`0248`). `9932` (GB),
768
803
  // `9935` (IE) et `9930` (DE) sont légaux là-haut et FATALS ici.
769
804
  //
770
805
  // Le champ (BT-29) est optionnel : l'omettre est la seule écriture
@@ -952,14 +987,21 @@ function buildLineAllowanceChargeXml(reason, amount, isCharge, currency) {
952
987
  <cbc:Amount currencyID="${escapeXml(currency)}">${formatAmount(amount)}</cbc:Amount>
953
988
  </cac:AllowanceCharge>`;
954
989
  }
955
- function calculateLineExtensionAmount(line) {
956
- const base = line.quantity * line.unitPrice;
990
+ function calculateLineExtensionAmount(line, lineIndex) {
991
+ if (line.baseQuantity !== void 0) {
992
+ assertValidBaseQuantity(line.baseQuantity, `lines[${lineIndex}].baseQuantity`);
993
+ }
994
+ const base = line.quantity * line.unitPrice / (line.baseQuantity ?? 1);
957
995
  const lineAllowances = (line.allowances ?? []).reduce((sum, a) => sum + a.amount, 0);
958
996
  const lineCharges = (line.charges ?? []).reduce((sum, c) => sum + c.amount, 0);
959
- return base - lineAllowances + lineCharges;
997
+ const total = base - lineAllowances + lineCharges;
998
+ if (!survivesCents(total)) {
999
+ throw new UblBuilderInputError(NON_FINITE_DERIVED_AMOUNT_MESSAGE, `lines[${lineIndex}]`, DERIVED_AMOUNT_CONTRACT_RULE);
1000
+ }
1001
+ return total;
960
1002
  }
961
1003
  function buildDocumentLineXml(line, index, currency, lineTag, qtyTag) {
962
- const lineTotal = calculateLineExtensionAmount(line);
1004
+ const lineTotal = calculateLineExtensionAmount(line, index);
963
1005
  const unit = resolveUnitCode(line.unit ?? DEFAULT_UNIT);
964
1006
  const vatCategory = line.vatCategory ?? "S";
965
1007
  const lineAllowancesXml = (line.allowances ?? []).map((a) => buildLineAllowanceChargeXml(a.reason, a.amount, false, currency)).join("");
@@ -1022,7 +1064,7 @@ function calculateTaxSubtotals(lines, allowances, charges, options = {}) {
1022
1064
  throw new UblBuilderInputError(`Conflicting taxExemptReason values for VAT group ${vatCategory}/${effectiveVatRate}.`, "taxExemptReason");
1023
1065
  }
1024
1066
  existing.taxExemptReason ??= reason;
1025
- existing.taxableAmount = round2(existing.taxableAmount + amount);
1067
+ existing.taxableAmount += amount;
1026
1068
  } else {
1027
1069
  groups.set(key, {
1028
1070
  vatRate: effectiveVatRate,
@@ -1035,7 +1077,7 @@ function calculateTaxSubtotals(lines, allowances, charges, options = {}) {
1035
1077
  }
1036
1078
  }
1037
1079
  for (const [index, line] of lines.entries()) {
1038
- addToGroup(line.vatCategory ?? "S", line.vatRate, calculateLineExtensionAmount(line), line.taxExemptReason, `lines[${index}]`);
1080
+ addToGroup(line.vatCategory ?? "S", line.vatRate, calculateLineExtensionAmount(line, index), line.taxExemptReason, `lines[${index}]`);
1039
1081
  }
1040
1082
  for (const [index, a] of (allowances ?? []).entries()) {
1041
1083
  addToGroup(a.vatCategory ?? "S", a.vatRate, -a.amount, a.taxExemptReason, `allowances[${index}]`);
@@ -1050,19 +1092,29 @@ function calculateTaxSubtotals(lines, allowances, charges, options = {}) {
1050
1092
  }
1051
1093
  }
1052
1094
  }
1053
- return Array.from(groups.values()).map((subtotal) => ({
1054
- ...subtotal,
1055
- taxAmount: round2(subtotal.taxableAmount * (subtotal.vatRate / 100))
1056
- }));
1095
+ return Array.from(groups.values()).map((subtotal) => {
1096
+ const taxableAmount = roundUblCurrencyAmount(subtotal.taxableAmount);
1097
+ const taxAmount = roundUblCurrencyAmount(taxableAmount * (subtotal.vatRate / 100));
1098
+ if (!survivesCents(taxableAmount) || !survivesCents(taxAmount)) {
1099
+ throw new UblBuilderInputError(NON_FINITE_DERIVED_AMOUNT_MESSAGE, "totals", DERIVED_AMOUNT_CONTRACT_RULE);
1100
+ }
1101
+ return { ...subtotal, taxableAmount, taxAmount };
1102
+ });
1057
1103
  }
1058
1104
  function calculateDocumentTotals(lines, allowances, charges, options = {}) {
1059
1105
  const taxSubtotals = calculateTaxSubtotals(lines, allowances, charges, options);
1060
- const lineExtensionAmount = lines.reduce((sum, line) => sum + calculateLineExtensionAmount(line), 0);
1106
+ const lineExtensionAmount = lines.reduce((sum, line, index) => sum + calculateLineExtensionAmount(line, index), 0);
1061
1107
  const allowanceTotalAmount = (allowances ?? []).reduce((sum, a) => sum + a.amount, 0);
1062
1108
  const chargeTotalAmount = (charges ?? []).reduce((sum, c) => sum + c.amount, 0);
1063
- const taxExclusiveAmount = round2(lineExtensionAmount - allowanceTotalAmount + chargeTotalAmount);
1064
- const totalTax = round2(taxSubtotals.reduce((sum, st) => sum + st.taxAmount, 0));
1065
- const taxInclusiveAmount = round2(taxExclusiveAmount + totalTax);
1109
+ const taxExclusiveAmount = roundUblCurrencyAmount(lineExtensionAmount - allowanceTotalAmount + chargeTotalAmount);
1110
+ if (!survivesCents(allowanceTotalAmount) || !survivesCents(chargeTotalAmount) || !survivesCents(taxExclusiveAmount)) {
1111
+ throw new UblBuilderInputError(NON_FINITE_DERIVED_AMOUNT_MESSAGE, "totals", DERIVED_AMOUNT_CONTRACT_RULE);
1112
+ }
1113
+ const totalTax = roundUblCurrencyAmount(taxSubtotals.reduce((sum, st) => sum + st.taxAmount, 0));
1114
+ const taxInclusiveAmount = roundUblCurrencyAmount(taxExclusiveAmount + totalTax);
1115
+ if (!survivesCents(totalTax) || !survivesCents(taxInclusiveAmount)) {
1116
+ throw new UblBuilderInputError(NON_FINITE_DERIVED_AMOUNT_MESSAGE, "totals", DERIVED_AMOUNT_CONTRACT_RULE);
1117
+ }
1066
1118
  return {
1067
1119
  lineExtensionAmount,
1068
1120
  allowanceTotalAmount,
@@ -1094,7 +1146,7 @@ function buildTaxTotalXml(taxSubtotals, totalTax, currency) {
1094
1146
  </cac:TaxTotal>`;
1095
1147
  }
1096
1148
  function buildTaxCurrencyTotalXml(totalTax, taxCurrency, rate) {
1097
- const convertedAmount = Math.round(totalTax * rate * 100) / 100;
1149
+ const convertedAmount = roundUblCurrencyAmount(totalTax * rate);
1098
1150
  return `<cac:TaxTotal>
1099
1151
  <cbc:TaxAmount currencyID="${escapeXml(taxCurrency)}">${formatAmount(convertedAmount)}</cbc:TaxAmount>
1100
1152
  </cac:TaxTotal>`;
@@ -1147,6 +1199,7 @@ function buildOrderReferenceXml(orderReference, salesOrderReference) {
1147
1199
  return parts.join("");
1148
1200
  }
1149
1201
  function buildInvoiceXml(input) {
1202
+ assertDocumentAdjustmentAmounts(input);
1150
1203
  const currency = input.currency ?? "EUR";
1151
1204
  const date = formatDate(input.date);
1152
1205
  const dueDate = input.dueDate ? formatDate(input.dueDate) : void 0;
@@ -1194,6 +1247,7 @@ function buildInvoiceXml(input) {
1194
1247
  </Invoice>`;
1195
1248
  }
1196
1249
  function buildCreditNoteXml(input) {
1250
+ assertDocumentAdjustmentAmounts(input);
1197
1251
  const currency = input.currency ?? "EUR";
1198
1252
  const date = formatDate(input.date);
1199
1253
  const dueDate = input.dueDate ? formatDate(input.dueDate) : void 0;
@@ -1437,8 +1491,13 @@ var CURRENCIES = /* @__PURE__ */ new Map([
1437
1491
  ["CZK", { code: "CZK", name: "Czech Koruna", minorUnits: 2 }],
1438
1492
  ["HUF", { code: "HUF", name: "Hungarian Forint", minorUnits: 2 }],
1439
1493
  ["RON", { code: "RON", name: "Romanian Leu", minorUnits: 2 }],
1440
- ["BGN", { code: "BGN", name: "Bulgarian Lev", minorUnits: 2 }],
1441
- ["HRK", { code: "HRK", name: "Croatian Kuna", minorUnits: 2 }],
1494
+ // ⛔ BGN and HRK are GONE (GPR-1205). Neither is in BR-CL-04/BR-CL-05 of the
1495
+ // graved 3.0.21 rulebooks Bulgaria and Croatia both joined the euro — so
1496
+ // accepting them here would wave through a document the network refuses, which
1497
+ // is the exact failure this release closes. XCG replaces ANG (never carried
1498
+ // here) for the Dutch Caribbean. `currency-codes-network-parity.test.ts` reds
1499
+ // if this table ever readmits a code the rulebooks reject.
1500
+ ["XCG", { code: "XCG", name: "Caribbean Guilder", minorUnits: 2 }],
1442
1501
  ["ISK", { code: "ISK", name: "Icelandic Krona", minorUnits: 0 }],
1443
1502
  ["TRY", { code: "TRY", name: "Turkish Lira", minorUnits: 2 }],
1444
1503
  ["JPY", { code: "JPY", name: "Japanese Yen", minorUnits: 0 }],
@@ -1548,6 +1607,79 @@ function resolveUnit(input) {
1548
1607
  function getAllUnits() {
1549
1608
  return Array.from(UNIT_CODES.entries()).map(([code, name]) => ({ code, name })).sort((a, b) => a.code.localeCompare(b.code));
1550
1609
  }
1610
+ var INVOICE_TYPE_CODES = [
1611
+ 71,
1612
+ 80,
1613
+ 81,
1614
+ 82,
1615
+ 84,
1616
+ 102,
1617
+ 130,
1618
+ 202,
1619
+ 203,
1620
+ 204,
1621
+ 211,
1622
+ 218,
1623
+ 219,
1624
+ 295,
1625
+ 325,
1626
+ 326,
1627
+ 331,
1628
+ 380,
1629
+ 382,
1630
+ 383,
1631
+ 384,
1632
+ 385,
1633
+ 386,
1634
+ 387,
1635
+ 388,
1636
+ 389,
1637
+ 390,
1638
+ 393,
1639
+ 394,
1640
+ 395,
1641
+ 456,
1642
+ 457,
1643
+ 471,
1644
+ 472,
1645
+ 473,
1646
+ 500,
1647
+ 501,
1648
+ 527,
1649
+ 553,
1650
+ 575,
1651
+ 623,
1652
+ 633,
1653
+ 751,
1654
+ 780,
1655
+ 817,
1656
+ 870,
1657
+ 875,
1658
+ 876,
1659
+ 877,
1660
+ 935
1661
+ ];
1662
+ var CREDIT_NOTE_TYPE_CODES = [
1663
+ 81,
1664
+ 83,
1665
+ 261,
1666
+ 262,
1667
+ 296,
1668
+ 308,
1669
+ 381,
1670
+ 396,
1671
+ 420,
1672
+ 458,
1673
+ 502,
1674
+ 503,
1675
+ 532
1676
+ ];
1677
+ function getInvoiceTypeCodes() {
1678
+ return [...INVOICE_TYPE_CODES];
1679
+ }
1680
+ function getCreditNoteTypeCodes() {
1681
+ return [...CREDIT_NOTE_TYPE_CODES];
1682
+ }
1551
1683
 
1552
1684
  // ../sdk/dist/core/validator.js
1553
1685
  function error(field, message, ruleId, suggestion) {
@@ -1564,6 +1696,11 @@ function assertString(value, fieldPath, errors) {
1564
1696
  return true;
1565
1697
  }
1566
1698
  var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
1699
+ var ALLOWANCE_CHARGE_CONTRACT_RULE = "GETPEPPR-ALLOWANCE-CHARGE-AMOUNT";
1700
+ var DERIVED_AMOUNT_CONTRACT_RULE2 = "GETPEPPR-DERIVED-AMOUNT";
1701
+ function survivesCents2(value) {
1702
+ return Number.isFinite(value) && Number.isFinite(value * 100);
1703
+ }
1567
1704
  function validateParty(party, path) {
1568
1705
  const errors = [];
1569
1706
  if (party.name === void 0 || party.name === null || party.name === "") {
@@ -1637,6 +1774,31 @@ function validateLine(line, index, isCreditNote = false) {
1637
1774
  } else if (line.vatRate < 0 || line.vatRate > 100) {
1638
1775
  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."));
1639
1776
  }
1777
+ const adjustmentItems = {
1778
+ allowances: Array.isArray(line.allowances) ? line.allowances : [],
1779
+ charges: Array.isArray(line.charges) ? line.charges : []
1780
+ };
1781
+ for (const kind of ["allowances", "charges"]) {
1782
+ const items = line[kind];
1783
+ if (items === void 0)
1784
+ continue;
1785
+ if (!Array.isArray(items)) {
1786
+ errors.push(error(`${path}.${kind}`, `${kind} must be an array \u2014 omit the field instead of sending ${items === null ? "null" : typeof items}`, ALLOWANCE_CHARGE_CONTRACT_RULE));
1787
+ continue;
1788
+ }
1789
+ for (const [i, item] of items.entries()) {
1790
+ const amount = item?.amount;
1791
+ if (typeof amount !== "number" || !Number.isFinite(amount) || amount < 0) {
1792
+ errors.push(error(`${path}.${kind}[${i}].amount`, `Allowance and charge amounts must be zero or positive finite numbers, got ${String(amount)}`, ALLOWANCE_CHARGE_CONTRACT_RULE, "An allowance reduces the amount and a charge increases it \u2014 encode the direction in the field, never in the sign."));
1793
+ }
1794
+ }
1795
+ }
1796
+ const bq = typeof line.baseQuantity === "number" && line.baseQuantity > 0 ? line.baseQuantity : 1;
1797
+ const derivedNet = (line.quantity ?? 0) * (line.unitPrice ?? 0) / bq - adjustmentItems.allowances.reduce((sum, a) => sum + (a?.amount ?? 0), 0) + adjustmentItems.charges.reduce((sum, c) => sum + (c?.amount ?? 0), 0);
1798
+ const derivedVat = derivedNet * ((typeof line.vatRate === "number" ? line.vatRate : 0) / 100);
1799
+ if (!survivesCents2(derivedNet) || !survivesCents2(derivedVat)) {
1800
+ errors.push(error(path, "Derived amount is not finite (overflow). Reduce quantities, prices or adjustment amounts so every total stays representable.", DERIVED_AMOUNT_CONTRACT_RULE2));
1801
+ }
1640
1802
  return errors;
1641
1803
  }
1642
1804
  function validateInvoice(input) {
@@ -1648,9 +1810,12 @@ function validateInvoice(input) {
1648
1810
  } else if (!input.number.trim()) {
1649
1811
  errors.push(error("number", "Invoice number is required", "BR-02", "Must be unique per supplier"));
1650
1812
  }
1651
- const VALID_TYPE_CODES = [380, 381, 383, 384, 386, 389, 751];
1652
- if (input.invoiceTypeCode != null && !VALID_TYPE_CODES.includes(input.invoiceTypeCode)) {
1653
- errors.push(error("invoiceTypeCode", `Invalid invoice type code: ${input.invoiceTypeCode}`, void 0, "Valid codes: 380, 381, 383, 384, 386, 389, 751"));
1813
+ if (input.invoiceTypeCode != null) {
1814
+ const isCreditNote = input.isCreditNote === true;
1815
+ const legalCodes = isCreditNote ? getCreditNoteTypeCodes() : getInvoiceTypeCodes();
1816
+ if (!legalCodes.includes(input.invoiceTypeCode)) {
1817
+ errors.push(error("invoiceTypeCode", `Invalid ${isCreditNote ? "credit note" : "invoice"} type code: ${input.invoiceTypeCode}`, "BR-CL-01", `Valid ${isCreditNote ? "credit note" : "invoice"} type codes: ${legalCodes.join(", ")}`));
1818
+ }
1654
1819
  }
1655
1820
  if (input.isCreditNote) {
1656
1821
  const ref = input.invoiceReference;
@@ -1713,15 +1878,52 @@ function validateInvoice(input) {
1713
1878
  if (value === void 0)
1714
1879
  continue;
1715
1880
  if (!Array.isArray(value)) {
1716
- errors.push(error(field, `${field} must be an array`, void 0));
1881
+ errors.push(error(field, `${field} must be an array \u2014 omit the field instead of sending ${value === null ? "null" : typeof value}`, ALLOWANCE_CHARGE_CONTRACT_RULE));
1717
1882
  continue;
1718
1883
  }
1719
1884
  for (const [index, item] of value.entries()) {
1720
1885
  if (item === null || typeof item !== "object") {
1721
1886
  errors.push(error(`${field}[${index}]`, `${field}[${index}] must be an object`, void 0));
1887
+ continue;
1888
+ }
1889
+ const amount = item.amount;
1890
+ if (typeof amount !== "number" || !Number.isFinite(amount) || amount < 0) {
1891
+ errors.push(error(`${field}[${index}].amount`, `Allowance and charge amounts must be zero or positive finite numbers, got ${String(amount)}`, ALLOWANCE_CHARGE_CONTRACT_RULE, "An allowance reduces the amount and a charge increases it \u2014 encode the direction in the field, never in the sign."));
1722
1892
  }
1723
1893
  }
1724
1894
  }
1895
+ if (Array.isArray(input.lines)) {
1896
+ const itemsOf = (value) => Array.isArray(value) ? value : [];
1897
+ const amountOf = (item) => typeof item?.amount === "number" ? item.amount : 0;
1898
+ const lineNets = input.lines.reduce((sum, line) => {
1899
+ if (typeof line !== "object" || line === null)
1900
+ return sum;
1901
+ const lbq = typeof line.baseQuantity === "number" && line.baseQuantity > 0 ? line.baseQuantity : 1;
1902
+ return sum + (line.quantity ?? 0) * (line.unitPrice ?? 0) / lbq - itemsOf(line.allowances).reduce((s, a) => s + amountOf(a), 0) + itemsOf(line.charges).reduce((s, c) => s + amountOf(c), 0);
1903
+ }, 0);
1904
+ const allowanceTotal = itemsOf(input.allowances).reduce((s, a) => s + amountOf(a), 0);
1905
+ const chargeTotal = itemsOf(input.charges).reduce((s, c) => s + amountOf(c), 0);
1906
+ const vatOf = (items, sign) => {
1907
+ if (!Array.isArray(items))
1908
+ return 0;
1909
+ return items.reduce((s, it) => {
1910
+ const rec = it;
1911
+ const amount = typeof rec?.amount === "number" ? rec.amount : 0;
1912
+ const rate = typeof rec?.vatRate === "number" ? rec.vatRate : 0;
1913
+ return s + sign * amount * (rate / 100);
1914
+ }, 0);
1915
+ };
1916
+ const docVat = vatOf(input.allowances, -1) + vatOf(input.charges, 1) + input.lines.reduce((s, line) => {
1917
+ if (typeof line !== "object" || line === null)
1918
+ return s;
1919
+ const lbq = typeof line.baseQuantity === "number" && line.baseQuantity > 0 ? line.baseQuantity : 1;
1920
+ const net = (line.quantity ?? 0) * (line.unitPrice ?? 0) / lbq;
1921
+ return s + net * ((typeof line.vatRate === "number" ? line.vatRate : 0) / 100);
1922
+ }, 0);
1923
+ if (!survivesCents2(lineNets - allowanceTotal + chargeTotal) || !survivesCents2(docVat)) {
1924
+ errors.push(error("totals", "Derived amount is not finite (overflow). Reduce quantities, prices or adjustment amounts so every total stays representable.", DERIVED_AMOUNT_CONTRACT_RULE2));
1925
+ }
1926
+ }
1725
1927
  if (input.date) {
1726
1928
  if (!ISO_DATE_RE.test(input.date)) {
1727
1929
  errors.push(error("date", `Invalid date format: "${input.date}"`, void 0, "Use ISO 8601: YYYY-MM-DD"));
@@ -2331,18 +2533,171 @@ function statusFamily(status) {
2331
2533
  var TERMINAL_FAILURE_STATUSES = STATUS_PRECEDENCE.filter((e) => e.family === "terminal-failure").map((e) => e.status);
2332
2534
 
2333
2535
  // ../sdk/dist/version.js
2334
- var SDK_VERSION = "4.8.0";
2536
+ var SDK_VERSION = "5.1.0";
2537
+
2538
+ // ../sdk/dist/core/api-result.js
2539
+ var API_RESULT_HEADER_NAMES = {
2540
+ requestId: "Getpeppr-Request-Id",
2541
+ resultCode: "Getpeppr-Result-Code",
2542
+ resultMessage: "Getpeppr-Result-Message",
2543
+ retryable: "Getpeppr-Retryable",
2544
+ remediation: "Getpeppr-Remediation",
2545
+ docs: "Getpeppr-Result-Docs"
2546
+ };
2547
+ var CONTROL_CHARACTERS = /[\u0000-\u001F\u007F-\u009F]/g;
2548
+ function stripControls(value) {
2549
+ return value.replace(CONTROL_CHARACTERS, " ");
2550
+ }
2551
+ function safeDocsUrl(value) {
2552
+ if (typeof value !== "string")
2553
+ return null;
2554
+ let parsed;
2555
+ try {
2556
+ parsed = new URL(value);
2557
+ } catch {
2558
+ return null;
2559
+ }
2560
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
2561
+ return null;
2562
+ return parsed.href;
2563
+ }
2564
+ var MAX_BYTES = {
2565
+ requestId: 256,
2566
+ code: 256,
2567
+ message: 1024,
2568
+ remediation: 64,
2569
+ docs: 2048,
2570
+ // ⚠️ A COST bound, not a correctness one — and the distinction is measured.
2571
+ // When this field was still trimmed, a huge padded value could normalise into
2572
+ // `true`, so the ceiling changed the verdict. Now that the token is compared
2573
+ // verbatim, no string can be both over-long and equal to "true"/"false": a
2574
+ // mutation removing this ceiling SURVIVES the suite, and correctly so. It
2575
+ // stays to bound the whitespace/control scan on an absurd value, and it is
2576
+ // the one entry here with no test — deliberately, since any assertion would
2577
+ // be satisfied by both answers.
2578
+ retryable: 32
2579
+ };
2580
+ var STRICT_HTTPS_URL = /^https:\/\/[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?(?::\d{1,5})?(?:\/[^\s\\]*)?$/i;
2581
+ var TEXT_ENCODER = new TextEncoder();
2582
+ function withinBudget(raw, maxBytes) {
2583
+ if (raw === null)
2584
+ return void 0;
2585
+ return TEXT_ENCODER.encode(raw).length > maxBytes ? void 0 : raw;
2586
+ }
2587
+ function readSentence(raw, maxBytes) {
2588
+ const bounded = withinBudget(raw, maxBytes);
2589
+ if (bounded === void 0)
2590
+ return void 0;
2591
+ const cleaned = stripControls(bounded).trim();
2592
+ return cleaned === "" ? void 0 : cleaned;
2593
+ }
2594
+ function readMachineToken(raw, maxBytes) {
2595
+ const bounded = withinBudget(raw, maxBytes);
2596
+ if (bounded === void 0)
2597
+ return void 0;
2598
+ if (bounded === "")
2599
+ return void 0;
2600
+ if (new RegExp(CONTROL_CHARACTERS.source).test(bounded))
2601
+ return void 0;
2602
+ if (/\s/u.test(bounded))
2603
+ return void 0;
2604
+ return bounded;
2605
+ }
2606
+ function readBoolean(raw) {
2607
+ const token = readMachineToken(raw, MAX_BYTES.retryable);
2608
+ if (token === void 0)
2609
+ return void 0;
2610
+ if (token === "true")
2611
+ return true;
2612
+ if (token === "false")
2613
+ return false;
2614
+ return void 0;
2615
+ }
2616
+ function readDocsUrl(raw) {
2617
+ const token = readMachineToken(raw, MAX_BYTES.docs);
2618
+ if (token === void 0)
2619
+ return void 0;
2620
+ if (!STRICT_HTTPS_URL.test(token))
2621
+ return void 0;
2622
+ const parsed = safeDocsUrl(token);
2623
+ if (parsed === null)
2624
+ return void 0;
2625
+ const url = new URL(parsed);
2626
+ if (url.protocol !== "https:")
2627
+ return void 0;
2628
+ if (url.username !== "" || url.password !== "")
2629
+ return void 0;
2630
+ return url.href;
2631
+ }
2632
+ function parseApiResultHeaders(headers) {
2633
+ const requestId = readMachineToken(headers.get(API_RESULT_HEADER_NAMES.requestId), MAX_BYTES.requestId);
2634
+ const code = readMachineToken(headers.get(API_RESULT_HEADER_NAMES.resultCode), MAX_BYTES.code);
2635
+ const message = readSentence(headers.get(API_RESULT_HEADER_NAMES.resultMessage), MAX_BYTES.message);
2636
+ const retryable = readBoolean(headers.get(API_RESULT_HEADER_NAMES.retryable));
2637
+ const remediation = readMachineToken(headers.get(API_RESULT_HEADER_NAMES.remediation), MAX_BYTES.remediation);
2638
+ const docs = readDocsUrl(headers.get(API_RESULT_HEADER_NAMES.docs));
2639
+ const result = {};
2640
+ if (requestId !== void 0)
2641
+ result.requestId = requestId;
2642
+ if (code !== void 0)
2643
+ result.code = code;
2644
+ if (message !== void 0)
2645
+ result.message = message;
2646
+ if (retryable !== void 0)
2647
+ result.retryable = retryable;
2648
+ if (remediation !== void 0)
2649
+ result.remediation = remediation;
2650
+ if (docs !== void 0)
2651
+ result.docs = docs;
2652
+ return Object.keys(result).length === 0 ? void 0 : result;
2653
+ }
2335
2654
 
2336
2655
  // ../sdk/dist/core/client.js
2337
- function findHeaderCaseInsensitive(headers, name) {
2656
+ function normalizeHeaderValue(value) {
2657
+ return value.replace(/^[\t\n\r ]+|[\t\n\r ]+$/g, "");
2658
+ }
2659
+ function isCarriableHeaderValue(value) {
2660
+ for (let i = 0; i < value.length; i++) {
2661
+ const code = value.charCodeAt(i);
2662
+ const carriable = code === 9 || code >= 32 && code <= 126 || code >= 128 && code <= 255;
2663
+ if (!carriable)
2664
+ return false;
2665
+ }
2666
+ return true;
2667
+ }
2668
+ function idempotencyKeyRefusal(message) {
2669
+ return new PeppolValidationError(`Invalid idempotency key: ${message}`, {
2670
+ valid: false,
2671
+ errors: [{ field: "idempotencyKey", message }],
2672
+ warnings: []
2673
+ });
2674
+ }
2675
+ function applyIdempotencyKey(headers, options) {
2676
+ const key = options?.idempotencyKey;
2677
+ if (key === void 0 || key === null)
2678
+ return;
2679
+ if (typeof key !== "string") {
2680
+ throw idempotencyKeyRefusal(`expected a string, received ${Array.isArray(key) ? "an array" : `a ${typeof key}`}.`);
2681
+ }
2682
+ const normalized = normalizeHeaderValue(key);
2683
+ if (normalized === "") {
2684
+ throw idempotencyKeyRefusal("the key is blank once HTTP whitespace is stripped, so it would reach the gateway empty and protect nothing. Pass a non-blank key, or omit the option.");
2685
+ }
2686
+ if (!isCarriableHeaderValue(normalized)) {
2687
+ throw idempotencyKeyRefusal("the key contains a character no HTTP header can carry. A header value may hold only HTAB, space, U+0021-U+007E and U+0080-U+00FF (RFC 9110 field-value) \u2014 so every control character other than the tab, plus DEL and anything above U+00FF, is refused by the transport itself.");
2688
+ }
2689
+ headers["Idempotency-Key"] = normalized;
2690
+ }
2691
+ function carriesUsableIdempotencyKey(headers) {
2338
2692
  if (!headers)
2339
- return void 0;
2340
- const target = name.toLowerCase();
2341
- for (const [key, value] of Object.entries(headers)) {
2342
- if (key.toLowerCase() === target)
2343
- return value;
2693
+ return false;
2694
+ for (const [name, value] of Object.entries(headers)) {
2695
+ if (name.toLowerCase() !== "idempotency-key")
2696
+ continue;
2697
+ if (typeof value === "string" && normalizeHeaderValue(value) !== "")
2698
+ return true;
2344
2699
  }
2345
- return void 0;
2700
+ return false;
2346
2701
  }
2347
2702
  var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
2348
2703
  function sleep(ms) {
@@ -2369,33 +2724,16 @@ function parseRetryAfter(headerValue) {
2369
2724
  }
2370
2725
  return void 0;
2371
2726
  }
2372
- var CONTROL_CHARACTERS = /[\u0000-\u001F\u007F-\u009F]/g;
2373
- function stripControls(value) {
2374
- return value.replace(CONTROL_CHARACTERS, " ");
2375
- }
2376
2727
  function readOwn(source, key) {
2377
2728
  return Object.hasOwn(source, key) ? source[key] : void 0;
2378
2729
  }
2379
- function readSentence(source, key) {
2730
+ function readSentence2(source, key) {
2380
2731
  const value = readOwn(source, key);
2381
2732
  if (typeof value !== "string")
2382
2733
  return null;
2383
2734
  const cleaned = stripControls(value).trim();
2384
2735
  return cleaned === "" ? null : cleaned;
2385
2736
  }
2386
- function safeDocsUrl(value) {
2387
- if (typeof value !== "string")
2388
- return null;
2389
- let parsed;
2390
- try {
2391
- parsed = new URL(value);
2392
- } catch {
2393
- return null;
2394
- }
2395
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
2396
- return null;
2397
- return parsed.href;
2398
- }
2399
2737
  function formatApiErrorMessage(status, rawBody) {
2400
2738
  const verbatim = `getpeppr API error (${status}): ${stripControls(rawBody)}`;
2401
2739
  let parsed;
@@ -2407,7 +2745,7 @@ function formatApiErrorMessage(status, rawBody) {
2407
2745
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2408
2746
  return verbatim;
2409
2747
  }
2410
- const sentence = readSentence(parsed, "message") ?? readSentence(parsed, "error");
2748
+ const sentence = readSentence2(parsed, "message") ?? readSentence2(parsed, "error");
2411
2749
  if (sentence === null)
2412
2750
  return verbatim;
2413
2751
  const link = safeDocsUrl(readOwn(parsed, "docs"));
@@ -2415,6 +2753,8 @@ function formatApiErrorMessage(status, rawBody) {
2415
2753
  }
2416
2754
  function isRetryableError(error2) {
2417
2755
  if (error2 instanceof PeppolApiError) {
2756
+ if (error2.retryable !== void 0)
2757
+ return error2.retryable;
2418
2758
  return RETRYABLE_STATUS_CODES.has(error2.statusCode);
2419
2759
  }
2420
2760
  if (error2 instanceof Error && error2.name === "AbortError") {
@@ -2456,7 +2796,7 @@ var GetpepprAdapter = class {
2456
2796
  lastError = err;
2457
2797
  const is429 = err instanceof PeppolApiError && err.statusCode === 429;
2458
2798
  const isSafeMethod = /^(GET|DELETE|HEAD)$/i.test(method);
2459
- const hasIdempotencyKey = !!findHeaderCaseInsensitive(extraHeaders, "Idempotency-Key");
2799
+ const hasIdempotencyKey = carriesUsableIdempotencyKey(extraHeaders);
2460
2800
  const canRetry = is429 || isSafeMethod || hasIdempotencyKey;
2461
2801
  if (attempt < maxRetries && canRetry && isRetryableError(err)) {
2462
2802
  const retryAfterMs = err instanceof PeppolApiError ? err.retryAfterMs : void 0;
@@ -2499,6 +2839,7 @@ var GetpepprAdapter = class {
2499
2839
  body: body ? JSON.stringify(body) : void 0,
2500
2840
  signal: controller.signal
2501
2841
  });
2842
+ const result = parseApiResultHeaders(response.headers);
2502
2843
  if (!response.ok) {
2503
2844
  const errorBody = await response.text().catch(() => "Unknown error");
2504
2845
  const retryAfterMs = response.status === 429 ? parseRetryAfter(response.headers.get("Retry-After")) : void 0;
@@ -2509,12 +2850,13 @@ var GetpepprAdapter = class {
2509
2850
  headers: Object.fromEntries(response.headers.entries()),
2510
2851
  body: errorBody,
2511
2852
  durationMs: Date.now() - startTime,
2512
- timestamp: Date.now()
2853
+ timestamp: Date.now(),
2854
+ result: cloneResultForHook(result)
2513
2855
  });
2514
2856
  } catch {
2515
2857
  }
2516
2858
  }
2517
- throw new PeppolApiError(formatApiErrorMessage(response.status, errorBody), response.status, errorBody, retryAfterMs);
2859
+ throw new PeppolApiError(formatApiErrorMessage(response.status, errorBody), response.status, errorBody, retryAfterMs, result);
2518
2860
  }
2519
2861
  if (response.status === 204) {
2520
2862
  if (this.onResponse) {
@@ -2524,7 +2866,8 @@ var GetpepprAdapter = class {
2524
2866
  headers: Object.fromEntries(response.headers.entries()),
2525
2867
  body: void 0,
2526
2868
  durationMs: Date.now() - startTime,
2527
- timestamp: Date.now()
2869
+ timestamp: Date.now(),
2870
+ result: cloneResultForHook(result)
2528
2871
  });
2529
2872
  } catch {
2530
2873
  }
@@ -2535,7 +2878,20 @@ var GetpepprAdapter = class {
2535
2878
  try {
2536
2879
  responseBody = await response.json();
2537
2880
  } catch {
2538
- throw new PeppolApiError(`getpeppr API error: unexpected response format (status ${response.status})`, response.status, "Response body is not valid JSON");
2881
+ if (this.onResponse) {
2882
+ try {
2883
+ this.onResponse({
2884
+ status: response.status,
2885
+ headers: Object.fromEntries(response.headers.entries()),
2886
+ body: void 0,
2887
+ durationMs: Date.now() - startTime,
2888
+ timestamp: Date.now(),
2889
+ result: cloneResultForHook(result)
2890
+ });
2891
+ } catch {
2892
+ }
2893
+ }
2894
+ throw new PeppolApiError(`getpeppr API error: unexpected response format (status ${response.status})`, response.status, "Response body is not valid JSON", void 0, result);
2539
2895
  }
2540
2896
  if (this.onResponse) {
2541
2897
  try {
@@ -2554,7 +2910,8 @@ var GetpepprAdapter = class {
2554
2910
  // swallows everything — so the hook fires with a marker instead.
2555
2911
  body: cloneForHook(responseBody),
2556
2912
  durationMs: Date.now() - startTime,
2557
- timestamp: Date.now()
2913
+ timestamp: Date.now(),
2914
+ result: cloneResultForHook(result)
2558
2915
  });
2559
2916
  } catch {
2560
2917
  }
@@ -2568,9 +2925,7 @@ var GetpepprAdapter = class {
2568
2925
  // legacy `_draft` marker, which the current gateway rejects explicitly.
2569
2926
  async sendInvoice(input, options) {
2570
2927
  const headers = {};
2571
- if (options?.idempotencyKey) {
2572
- headers["Idempotency-Key"] = options.idempotencyKey;
2573
- }
2928
+ applyIdempotencyKey(headers, options);
2574
2929
  if (options?.validateRecipient) {
2575
2930
  headers["X-Validate-Recipient"] = options.validateRecipient === true ? "warn" : String(options.validateRecipient);
2576
2931
  }
@@ -2579,17 +2934,17 @@ var GetpepprAdapter = class {
2579
2934
  }
2580
2935
  async createInvoice(input, options) {
2581
2936
  const headers = {};
2582
- if (options?.idempotencyKey) {
2583
- headers["Idempotency-Key"] = options.idempotencyKey;
2584
- }
2937
+ applyIdempotencyKey(headers, options);
2585
2938
  if (options?.validateRecipient) {
2586
2939
  headers["X-Validate-Recipient"] = options.validateRecipient === true ? "warn" : String(options.validateRecipient);
2587
2940
  }
2588
2941
  const result = await this.request("POST", "/invoices", { ...input, _draft: true }, headers);
2589
2942
  return parseSendResult(result);
2590
2943
  }
2591
- async sendInvoiceById(id) {
2592
- await this.request("POST", `/invoices/send/${id}`);
2944
+ async sendInvoiceById(id, options) {
2945
+ const headers = {};
2946
+ applyIdempotencyKey(headers, options);
2947
+ await this.request("POST", `/invoices/send/${id}`, void 0, headers);
2593
2948
  }
2594
2949
  async sendCreditNote(input) {
2595
2950
  const result = await this.request("POST", "/credit-notes", input);
@@ -2695,6 +3050,7 @@ var GetpepprAdapter = class {
2695
3050
  headers: requestHeaders,
2696
3051
  signal: controller.signal
2697
3052
  });
3053
+ const result = parseApiResultHeaders(response.headers);
2698
3054
  if (!response.ok) {
2699
3055
  const errorBody = await response.text().catch(() => "Unknown error");
2700
3056
  const retryAfterMs = response.status === 429 ? parseRetryAfter(response.headers.get("Retry-After")) : void 0;
@@ -2705,12 +3061,13 @@ var GetpepprAdapter = class {
2705
3061
  headers: Object.fromEntries(response.headers.entries()),
2706
3062
  body: errorBody,
2707
3063
  durationMs: Date.now() - startTime,
2708
- timestamp: Date.now()
3064
+ timestamp: Date.now(),
3065
+ result: cloneResultForHook(result)
2709
3066
  });
2710
3067
  } catch {
2711
3068
  }
2712
3069
  }
2713
- throw new PeppolApiError(formatApiErrorMessage(response.status, errorBody), response.status, errorBody, retryAfterMs);
3070
+ throw new PeppolApiError(formatApiErrorMessage(response.status, errorBody), response.status, errorBody, retryAfterMs, result);
2714
3071
  }
2715
3072
  const responseBody = await response.arrayBuffer();
2716
3073
  if (this.onResponse) {
@@ -2720,7 +3077,8 @@ var GetpepprAdapter = class {
2720
3077
  headers: Object.fromEntries(response.headers.entries()),
2721
3078
  body: `[ArrayBuffer: ${responseBody.byteLength} bytes]`,
2722
3079
  durationMs: Date.now() - startTime,
2723
- timestamp: Date.now()
3080
+ timestamp: Date.now(),
3081
+ result: cloneResultForHook(result)
2724
3082
  });
2725
3083
  } catch {
2726
3084
  }
@@ -2771,8 +3129,10 @@ var GetpepprAdapter = class {
2771
3129
  }
2772
3130
  };
2773
3131
  }
2774
- async acknowledgeInvoice(id) {
2775
- const result = await this.request("POST", `/invoices/${id}/ack`);
3132
+ async acknowledgeInvoice(id, options) {
3133
+ const headers = {};
3134
+ applyIdempotencyKey(headers, options);
3135
+ const result = await this.request("POST", `/invoices/${id}/ack`, void 0, headers);
2776
3136
  return parseSendResult(result);
2777
3137
  }
2778
3138
  async updateInvoice(id, input) {
@@ -2823,8 +3183,10 @@ var GetpepprAdapter = class {
2823
3183
  const result = await this.request("GET", `/contacts/${id}`);
2824
3184
  return parseContact(result);
2825
3185
  }
2826
- async createContact(input) {
2827
- const result = await this.request("POST", "/contacts", input);
3186
+ async createContact(input, options) {
3187
+ const headers = {};
3188
+ applyIdempotencyKey(headers, options);
3189
+ const result = await this.request("POST", "/contacts", input, headers);
2828
3190
  return parseContact(result);
2829
3191
  }
2830
3192
  async updateContact(id, input) {
@@ -2836,8 +3198,7 @@ var GetpepprAdapter = class {
2836
3198
  }
2837
3199
  async createLegalEntity(input, options) {
2838
3200
  const headers = {};
2839
- if (options?.idempotencyKey)
2840
- headers["Idempotency-Key"] = options.idempotencyKey;
3201
+ applyIdempotencyKey(headers, options);
2841
3202
  const result = await this.request("POST", "/legal-entities", input, headers);
2842
3203
  return parseLegalEntity(result);
2843
3204
  }
@@ -2876,8 +3237,7 @@ var GetpepprAdapter = class {
2876
3237
  }
2877
3238
  async requestLegalEntityAttestation(id, input, options) {
2878
3239
  const headers = {};
2879
- if (options?.idempotencyKey)
2880
- headers["Idempotency-Key"] = options.idempotencyKey;
3240
+ applyIdempotencyKey(headers, options);
2881
3241
  const result = await this.request("POST", `/legal-entities/${id}/attestation`, input, headers);
2882
3242
  return {
2883
3243
  id: String(result.id ?? id),
@@ -2915,8 +3275,10 @@ var GetpepprAdapter = class {
2915
3275
  const result = await this.request("GET", `/bank-accounts/${id}`);
2916
3276
  return parseBankAccount(result);
2917
3277
  }
2918
- async createBankAccount(input) {
2919
- const result = await this.request("POST", "/bank-accounts", input);
3278
+ async createBankAccount(input, options) {
3279
+ const headers = {};
3280
+ applyIdempotencyKey(headers, options);
3281
+ const result = await this.request("POST", "/bank-accounts", input, headers);
2920
3282
  return parseBankAccount(result);
2921
3283
  }
2922
3284
  async updateBankAccount(id, input) {
@@ -2953,7 +3315,9 @@ var GetpepprAdapter = class {
2953
3315
  // tout en salissant le corps des envois standards.
2954
3316
  ...options.sender ? { sender: options.sender } : {}
2955
3317
  };
2956
- const result = await this.request("POST", "/invoices/import", body);
3318
+ const headers = {};
3319
+ applyIdempotencyKey(headers, options);
3320
+ const result = await this.request("POST", "/invoices/import", body, headers);
2957
3321
  return parseSendResult(result);
2958
3322
  }
2959
3323
  async listTransportTypes() {
@@ -3007,6 +3371,9 @@ function detectMimeType(filename) {
3007
3371
  }
3008
3372
  }
3009
3373
  var PROTOCOL_ERROR_BODY_LIMIT = 2e3;
3374
+ function cloneResultForHook(result) {
3375
+ return result === void 0 ? void 0 : { ...result };
3376
+ }
3010
3377
  function cloneForHook(body) {
3011
3378
  try {
3012
3379
  return structuredClone(body);
@@ -3209,12 +3576,24 @@ function parseLegalEntity(raw) {
3209
3576
  country: raw.country != null ? String(raw.country) : null,
3210
3577
  identifier: idObj && idObj.scheme != null && idObj.value != null ? { scheme: String(idObj.scheme), value: String(idObj.value) } : null,
3211
3578
  status: String(raw.status ?? "pending"),
3579
+ networkDiscovery: raw.networkDiscovery && typeof raw.networkDiscovery === "object" ? raw.networkDiscovery : { state: "pending", attempts: 0 },
3212
3580
  environment: String(raw.environment ?? ""),
3213
3581
  createdAt: String(raw.createdAt ?? "")
3214
3582
  };
3215
3583
  if (raw.verificationDetail != null) {
3216
3584
  le.verificationDetail = raw.verificationDetail;
3217
3585
  }
3586
+ if (raw.registrationDetail && typeof raw.registrationDetail === "object") {
3587
+ const reason = raw.registrationDetail.reason;
3588
+ const safeReasons = [
3589
+ "already_registered",
3590
+ "invalid_format",
3591
+ "provider_error"
3592
+ ];
3593
+ le.registrationDetail = {
3594
+ reason: safeReasons.includes(reason) ? reason : "provider_error"
3595
+ };
3596
+ }
3218
3597
  return le;
3219
3598
  }
3220
3599
  function parseAccountIdentity(raw) {
@@ -3437,14 +3816,95 @@ var PeppolProtocolError = class extends PeppolError {
3437
3816
  var PeppolApiError = class extends PeppolError {
3438
3817
  statusCode;
3439
3818
  responseBody;
3440
- /** Parsed Retry-After delay in milliseconds (present on 429 responses) */
3819
+ /**
3820
+ * Parsed `Retry-After` delay in milliseconds.
3821
+ *
3822
+ * `undefined` unless this response is a **429** AND carried a readable
3823
+ * `Retry-After`. No other status reads that header, whatever its remediation
3824
+ * says — measured, all 22 `retry_after` entries in the catalogue are 429s.
3825
+ * The gateway does not attach the header to every throttled answer either.
3826
+ */
3441
3827
  retryAfterMs;
3442
- constructor(message, statusCode, responseBody, retryAfterMs) {
3828
+ /**
3829
+ * The canonical result the gateway declared for this response, read from its
3830
+ * six headers — no body parsing required.
3831
+ *
3832
+ * `undefined` against a gateway that has not activated the result catalogue,
3833
+ * and behind any hop that strips unknown headers. The flattened accessors
3834
+ * below all read from here, so they are `undefined` together.
3835
+ */
3836
+ result;
3837
+ constructor(message, statusCode, responseBody, retryAfterMs, result) {
3443
3838
  super(message);
3444
3839
  this.statusCode = statusCode;
3445
3840
  this.responseBody = responseBody;
3446
3841
  this.name = "PeppolApiError";
3447
3842
  this.retryAfterMs = retryAfterMs;
3843
+ this.result = result;
3844
+ }
3845
+ /**
3846
+ * Stable getpeppr result code for this failure (e.g. `"auth.api_key_invalid"`).
3847
+ *
3848
+ * ⛔ NOT the same field as {@link code}, and they can both be present with
3849
+ * different values: this one is the catalogue's global code, `code` is the
3850
+ * route's own sub-reason from the body.
3851
+ *
3852
+ * `undefined` when the gateway sent no result headers.
3853
+ */
3854
+ get resultCode() {
3855
+ return this.result?.code;
3856
+ }
3857
+ /**
3858
+ * The catalogue's sentence for {@link resultCode}.
3859
+ *
3860
+ * ⚠️ Usually SHORTER on detail than `.message`, which is built from the
3861
+ * response body and can name the offending field or rule. Show `.message` to
3862
+ * a human; use this one when you want the stable phrasing.
3863
+ *
3864
+ * `undefined` when the gateway sent no result headers.
3865
+ */
3866
+ get resultMessage() {
3867
+ return this.result?.message;
3868
+ }
3869
+ /**
3870
+ * Server-generated correlation id for this exact request. Quote it to support.
3871
+ *
3872
+ * `undefined` when the gateway sent no result headers — which includes every
3873
+ * response from a deployment predating the catalogue.
3874
+ */
3875
+ get requestId() {
3876
+ return this.result?.requestId;
3877
+ }
3878
+ /**
3879
+ * Whether retrying this same request can succeed, per the catalogue.
3880
+ *
3881
+ * ⚠️ `undefined` means "the gateway did not say", NOT "no" — the SDK then
3882
+ * falls back to its historic status policy. A `false` here is an explicit
3883
+ * refusal and the SDK will not retry, whatever the status.
3884
+ */
3885
+ get retryable() {
3886
+ return this.result?.retryable;
3887
+ }
3888
+ /**
3889
+ * What to do about it: `"none"`, `"fix_request"`, `"authenticate"`,
3890
+ * `"retry"`, `"retry_after"`, `"wait"` or `"contact_support"` today.
3891
+ *
3892
+ * Typed open — a value added server-side reaches you rather than vanishing.
3893
+ * `undefined` when the gateway sent no result headers.
3894
+ */
3895
+ get remediation() {
3896
+ return this.result?.remediation;
3897
+ }
3898
+ /**
3899
+ * Documentation link for {@link resultCode}, when the catalogue provides one.
3900
+ *
3901
+ * `undefined` when the gateway sent no result headers, when the catalogue
3902
+ * entry has no docs link, or when the value was not a plain `https://` URL
3903
+ * (`http:`, credentials in the authority, and anything the URL parser would
3904
+ * have to repair are all refused).
3905
+ */
3906
+ get docs() {
3907
+ return this.result?.docs;
3448
3908
  }
3449
3909
  /**
3450
3910
  * The gateway's machine-readable error code, parsed from the JSON response body
@@ -3617,8 +4077,8 @@ ${validation.errors.map((e) => ` - ${e.field}: ${e.message}${e.suggestion ? ` (
3617
4077
  * always returns 501. Submit the final document with `invoices.send()`.
3618
4078
  * @throws {PeppolApiError} 501 with the current gateway provider
3619
4079
  */
3620
- async sendById(id) {
3621
- return this.adapter.sendInvoiceById(id);
4080
+ async sendById(id, options) {
4081
+ return this.adapter.sendInvoiceById(id, options);
3622
4082
  }
3623
4083
  /**
3624
4084
  * Send an invoice via Peppol.
@@ -3792,8 +4252,8 @@ ${validation.errors.map((e) => ` - ${e.field}: ${e.message}${e.suggestion ? ` (
3792
4252
  * acknowledgement and always returns 501.
3793
4253
  * @throws {PeppolApiError} 501 with the current gateway provider
3794
4254
  */
3795
- async acknowledge(id) {
3796
- return this.adapter.acknowledgeInvoice(id);
4255
+ async acknowledge(id, options) {
4256
+ return this.adapter.acknowledgeInvoice(id, options);
3797
4257
  }
3798
4258
  /**
3799
4259
  * Request an update to an existing invoice.
@@ -4083,8 +4543,8 @@ var ContactOperations = class {
4083
4543
  * });
4084
4544
  * ```
4085
4545
  */
4086
- async create(input) {
4087
- return this.adapter.createContact(input);
4546
+ async create(input, options) {
4547
+ return this.adapter.createContact(input, options);
4088
4548
  }
4089
4549
  /**
4090
4550
  * Update an existing contact.
@@ -4309,8 +4769,8 @@ var BankAccountOperations = class {
4309
4769
  * });
4310
4770
  * ```
4311
4771
  */
4312
- async create(input) {
4313
- return this.adapter.createBankAccount(input);
4772
+ async create(input, options) {
4773
+ return this.adapter.createBankAccount(input, options);
4314
4774
  }
4315
4775
  /**
4316
4776
  * Update an existing bank account.
@@ -5261,6 +5721,12 @@ async function pollUntilTerminal(client, documentId, options = {}) {
5261
5721
  return { finalStatus: lastStatus, timedOut: true };
5262
5722
  }
5263
5723
 
5724
+ // src/lib/dashboard-url.ts
5725
+ var DASHBOARD_INVOICES_BASE = "https://console.getpeppr.dev/invoices";
5726
+ function dashboardUrlForSendResult(result) {
5727
+ return `${DASHBOARD_INVOICES_BASE}/${result.id}`;
5728
+ }
5729
+
5264
5730
  // src/formatters/send-result.ts
5265
5731
  import pc5 from "picocolors";
5266
5732
  function formatSendResult(result, mode) {
@@ -5286,7 +5752,6 @@ function formatSendResult(result, mode) {
5286
5752
  // src/commands/send.ts
5287
5753
  var API_BASE = "https://api.getpeppr.dev/v1";
5288
5754
  var LOCAL_BASE = "http://localhost:3001/api/v1";
5289
- var DASHBOARD_BASE = "https://console.getpeppr.dev/invoices";
5290
5755
  function registerSendCommand(program2) {
5291
5756
  program2.command("send").description("Send an invoice to the Peppol network via getpeppr API").argument("[file]", "optional path to invoice JSON (mutex with --to/--amount/...)").option("--prod", "target production (live keys + confirmation)").option("--local", "target localhost:3001 dev server").option("--key <key>", "override API key \u2014 for CI/scripted use only; visible in `ps` and shell history. Prefer GETPEPPR_API_KEY env var.").option("--to <peppol-id>", "recipient peppol id (e.g., 9925:BE0314595348)").option("--country <iso>", "recipient ISO 3166-1 alpha-2 country override (e.g., BE)").option("--amount <number>", "line amount in major currency units (decimal allowed)").option("--currency <iso>", "ISO 4217 currency (default EUR)").option("--desc <text>", "line description").option("--attachment", "attach the test PDF").option("--watch", "poll status until a terminal state (60s timeout)").option("-y, --yes", "skip --prod confirmation prompt").option("--no-validate", "skip pre-validation locally").option("--json", "output JSON").option("--quiet", "exit code only, no output").action(async (file, flags) => {
5292
5757
  let auth;
@@ -5390,7 +5855,7 @@ function registerSendCommand(program2) {
5390
5855
  `);
5391
5856
  process.exit(1);
5392
5857
  }
5393
- const dashboardUrl = `${DASHBOARD_BASE}/${result.id}`;
5858
+ const dashboardUrl = dashboardUrlForSendResult(result);
5394
5859
  let finalStatus = result.status;
5395
5860
  let timedOut = false;
5396
5861
  let watchFailed = false;
@@ -5492,7 +5957,7 @@ async function promptEnvironment() {
5492
5957
  });
5493
5958
  }
5494
5959
  function registerLoginCommand(program2) {
5495
- 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) => {
5960
+ program2.command("login").description("Save a getpeppr API key to the credentials file ($XDG_CONFIG_HOME/getpeppr, %APPDATA%\\getpeppr on Windows)").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) => {
5496
5961
  if (!flags.live && !flags.sandbox && process.stdin.isTTY !== true) {
5497
5962
  exitWithError("Error: --sandbox or --live required when stdin is not a TTY (CI mode).");
5498
5963
  }
@@ -5624,7 +6089,7 @@ function registerWhoamiCommand(program2) {
5624
6089
  // src/commands/logout.ts
5625
6090
  import pc9 from "picocolors";
5626
6091
  function registerLogoutCommand(program2) {
5627
- program2.command("logout").description("Remove ~/.config/getpeppr/credentials.json").action(() => {
6092
+ program2.command("logout").description("Remove the stored credentials file").action(() => {
5628
6093
  const path = getCredentialsPath();
5629
6094
  const removed = deleteCredentials();
5630
6095
  if (removed) {