@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/CHANGELOG.md +47 -0
- package/LICENSE +21 -0
- package/README.md +3 -2
- package/dist/index.js +561 -96
- package/dist/index.js.map +1 -1
- package/package.json +7 -8
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
|
|
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
|
|
743
|
-
|
|
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 (
|
|
767
|
-
// contre la liste ISO 6523 ICD (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1055
|
-
taxAmount
|
|
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 =
|
|
1064
|
-
|
|
1065
|
-
|
|
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 =
|
|
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
|
-
|
|
1441
|
-
|
|
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
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
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`,
|
|
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 = "
|
|
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
|
|
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
|
|
2340
|
-
const
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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) {
|