@getpeppr/cli 0.8.4 → 0.10.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 +53 -4
- package/README.md +2 -2
- package/dist/index.js +576 -111
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -130,7 +130,7 @@ Validating: ${pc.bold(filename)}
|
|
|
130
130
|
return lines.join("\n");
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
//
|
|
133
|
+
// ../sdk/dist/core/canonical-schemes.js
|
|
134
134
|
var ALIAS_TO_EAS = /* @__PURE__ */ new Map([
|
|
135
135
|
["AD:VAT", "9922"],
|
|
136
136
|
["AE:TIN", "0235"],
|
|
@@ -356,7 +356,7 @@ function countryForScheme(scheme) {
|
|
|
356
356
|
var CANONICAL_SCHEME_COUNT = ALIAS_TO_EAS.size;
|
|
357
357
|
var SCHEME_COUNTRY_COUNT = EAS_TO_COUNTRY.size;
|
|
358
358
|
|
|
359
|
-
//
|
|
359
|
+
// ../sdk/dist/core/peppol-id.js
|
|
360
360
|
function isWellFormedPeppolId(peppolId) {
|
|
361
361
|
if (!peppolId.includes(":"))
|
|
362
362
|
return false;
|
|
@@ -382,7 +382,7 @@ function parsePeppolId(peppolId) {
|
|
|
382
382
|
};
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
-
//
|
|
385
|
+
// ../sdk/dist/core/iso6523-icd-codes.js
|
|
386
386
|
var ICD_CODES = /* @__PURE__ */ new Set([
|
|
387
387
|
"0002",
|
|
388
388
|
"0003",
|
|
@@ -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),
|
|
@@ -642,7 +645,7 @@ function canCarryPartyIdentification(scheme, context) {
|
|
|
642
645
|
return scheme === "SEPA" && context !== "AccountingCustomerParty";
|
|
643
646
|
}
|
|
644
647
|
|
|
645
|
-
//
|
|
648
|
+
// ../sdk/dist/core/ubl-builder.js
|
|
646
649
|
var UBL_NS = "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2";
|
|
647
650
|
var CAC_NS = "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2";
|
|
648
651
|
var CBC_NS = "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2";
|
|
@@ -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)
|
|
@@ -763,8 +797,8 @@ function buildPartyXml(party, role) {
|
|
|
763
797
|
<cac:Party>
|
|
764
798
|
<cbc:EndpointID schemeID="${escapeXml(endpointScheme)}">${escapeXml(endpointId)}</cbc:EndpointID>
|
|
765
799
|
${// ⛔ DEUX listes, pas une. `BR-CL-25` juge l'EndpointID ci-dessus contre la
|
|
766
|
-
// liste EAS (
|
|
767
|
-
// contre la liste ISO 6523 ICD (
|
|
800
|
+
// liste EAS (104 valeurs, jusqu'à `9959`) ; `BR-CL-10` juge CE champ-ci
|
|
801
|
+
// contre la liste ISO 6523 ICD (243 valeurs, `0002`–`0248`). `9932` (GB),
|
|
768
802
|
// `9935` (IE) et `9930` (DE) sont légaux là-haut et FATALS ici.
|
|
769
803
|
//
|
|
770
804
|
// Le champ (BT-29) est optionnel : l'omettre est la seule écriture
|
|
@@ -952,14 +986,21 @@ function buildLineAllowanceChargeXml(reason, amount, isCharge, currency) {
|
|
|
952
986
|
<cbc:Amount currencyID="${escapeXml(currency)}">${formatAmount(amount)}</cbc:Amount>
|
|
953
987
|
</cac:AllowanceCharge>`;
|
|
954
988
|
}
|
|
955
|
-
function calculateLineExtensionAmount(line) {
|
|
956
|
-
|
|
989
|
+
function calculateLineExtensionAmount(line, lineIndex) {
|
|
990
|
+
if (line.baseQuantity !== void 0) {
|
|
991
|
+
assertValidBaseQuantity(line.baseQuantity, `lines[${lineIndex}].baseQuantity`);
|
|
992
|
+
}
|
|
993
|
+
const base = line.quantity * line.unitPrice / (line.baseQuantity ?? 1);
|
|
957
994
|
const lineAllowances = (line.allowances ?? []).reduce((sum, a) => sum + a.amount, 0);
|
|
958
995
|
const lineCharges = (line.charges ?? []).reduce((sum, c) => sum + c.amount, 0);
|
|
959
|
-
|
|
996
|
+
const total = base - lineAllowances + lineCharges;
|
|
997
|
+
if (!survivesCents(total)) {
|
|
998
|
+
throw new UblBuilderInputError(NON_FINITE_DERIVED_AMOUNT_MESSAGE, `lines[${lineIndex}]`, DERIVED_AMOUNT_CONTRACT_RULE);
|
|
999
|
+
}
|
|
1000
|
+
return total;
|
|
960
1001
|
}
|
|
961
1002
|
function buildDocumentLineXml(line, index, currency, lineTag, qtyTag) {
|
|
962
|
-
const lineTotal = calculateLineExtensionAmount(line);
|
|
1003
|
+
const lineTotal = calculateLineExtensionAmount(line, index);
|
|
963
1004
|
const unit = resolveUnitCode(line.unit ?? DEFAULT_UNIT);
|
|
964
1005
|
const vatCategory = line.vatCategory ?? "S";
|
|
965
1006
|
const lineAllowancesXml = (line.allowances ?? []).map((a) => buildLineAllowanceChargeXml(a.reason, a.amount, false, currency)).join("");
|
|
@@ -1035,7 +1076,7 @@ function calculateTaxSubtotals(lines, allowances, charges, options = {}) {
|
|
|
1035
1076
|
}
|
|
1036
1077
|
}
|
|
1037
1078
|
for (const [index, line] of lines.entries()) {
|
|
1038
|
-
addToGroup(line.vatCategory ?? "S", line.vatRate, calculateLineExtensionAmount(line), line.taxExemptReason, `lines[${index}]`);
|
|
1079
|
+
addToGroup(line.vatCategory ?? "S", line.vatRate, calculateLineExtensionAmount(line, index), line.taxExemptReason, `lines[${index}]`);
|
|
1039
1080
|
}
|
|
1040
1081
|
for (const [index, a] of (allowances ?? []).entries()) {
|
|
1041
1082
|
addToGroup(a.vatCategory ?? "S", a.vatRate, -a.amount, a.taxExemptReason, `allowances[${index}]`);
|
|
@@ -1050,19 +1091,29 @@ function calculateTaxSubtotals(lines, allowances, charges, options = {}) {
|
|
|
1050
1091
|
}
|
|
1051
1092
|
}
|
|
1052
1093
|
}
|
|
1053
|
-
return Array.from(groups.values()).map((subtotal) =>
|
|
1054
|
-
|
|
1055
|
-
taxAmount
|
|
1056
|
-
|
|
1094
|
+
return Array.from(groups.values()).map((subtotal) => {
|
|
1095
|
+
const taxableAmount = round2(subtotal.taxableAmount);
|
|
1096
|
+
const taxAmount = round2(taxableAmount * (subtotal.vatRate / 100));
|
|
1097
|
+
if (!survivesCents(taxableAmount) || !survivesCents(taxAmount)) {
|
|
1098
|
+
throw new UblBuilderInputError(NON_FINITE_DERIVED_AMOUNT_MESSAGE, "totals", DERIVED_AMOUNT_CONTRACT_RULE);
|
|
1099
|
+
}
|
|
1100
|
+
return { ...subtotal, taxableAmount, taxAmount };
|
|
1101
|
+
});
|
|
1057
1102
|
}
|
|
1058
1103
|
function calculateDocumentTotals(lines, allowances, charges, options = {}) {
|
|
1059
1104
|
const taxSubtotals = calculateTaxSubtotals(lines, allowances, charges, options);
|
|
1060
|
-
const lineExtensionAmount = lines.reduce((sum, line) => sum + calculateLineExtensionAmount(line), 0);
|
|
1105
|
+
const lineExtensionAmount = lines.reduce((sum, line, index) => sum + calculateLineExtensionAmount(line, index), 0);
|
|
1061
1106
|
const allowanceTotalAmount = (allowances ?? []).reduce((sum, a) => sum + a.amount, 0);
|
|
1062
1107
|
const chargeTotalAmount = (charges ?? []).reduce((sum, c) => sum + c.amount, 0);
|
|
1063
1108
|
const taxExclusiveAmount = round2(lineExtensionAmount - allowanceTotalAmount + chargeTotalAmount);
|
|
1109
|
+
if (!survivesCents(allowanceTotalAmount) || !survivesCents(chargeTotalAmount) || !survivesCents(taxExclusiveAmount)) {
|
|
1110
|
+
throw new UblBuilderInputError(NON_FINITE_DERIVED_AMOUNT_MESSAGE, "totals", DERIVED_AMOUNT_CONTRACT_RULE);
|
|
1111
|
+
}
|
|
1064
1112
|
const totalTax = round2(taxSubtotals.reduce((sum, st) => sum + st.taxAmount, 0));
|
|
1065
1113
|
const taxInclusiveAmount = round2(taxExclusiveAmount + totalTax);
|
|
1114
|
+
if (!survivesCents(totalTax) || !survivesCents(taxInclusiveAmount)) {
|
|
1115
|
+
throw new UblBuilderInputError(NON_FINITE_DERIVED_AMOUNT_MESSAGE, "totals", DERIVED_AMOUNT_CONTRACT_RULE);
|
|
1116
|
+
}
|
|
1066
1117
|
return {
|
|
1067
1118
|
lineExtensionAmount,
|
|
1068
1119
|
allowanceTotalAmount,
|
|
@@ -1147,6 +1198,7 @@ function buildOrderReferenceXml(orderReference, salesOrderReference) {
|
|
|
1147
1198
|
return parts.join("");
|
|
1148
1199
|
}
|
|
1149
1200
|
function buildInvoiceXml(input) {
|
|
1201
|
+
assertDocumentAdjustmentAmounts(input);
|
|
1150
1202
|
const currency = input.currency ?? "EUR";
|
|
1151
1203
|
const date = formatDate(input.date);
|
|
1152
1204
|
const dueDate = input.dueDate ? formatDate(input.dueDate) : void 0;
|
|
@@ -1194,6 +1246,7 @@ function buildInvoiceXml(input) {
|
|
|
1194
1246
|
</Invoice>`;
|
|
1195
1247
|
}
|
|
1196
1248
|
function buildCreditNoteXml(input) {
|
|
1249
|
+
assertDocumentAdjustmentAmounts(input);
|
|
1197
1250
|
const currency = input.currency ?? "EUR";
|
|
1198
1251
|
const date = formatDate(input.date);
|
|
1199
1252
|
const dueDate = input.dueDate ? formatDate(input.dueDate) : void 0;
|
|
@@ -1242,7 +1295,7 @@ function buildCreditNoteXml(input) {
|
|
|
1242
1295
|
</CreditNote>`;
|
|
1243
1296
|
}
|
|
1244
1297
|
|
|
1245
|
-
//
|
|
1298
|
+
// ../sdk/dist/core/checksums/luhn.js
|
|
1246
1299
|
function isValidLuhn(input) {
|
|
1247
1300
|
if (typeof input !== "string")
|
|
1248
1301
|
return false;
|
|
@@ -1283,7 +1336,7 @@ function isValidLuhnSiret(input) {
|
|
|
1283
1336
|
return false;
|
|
1284
1337
|
}
|
|
1285
1338
|
|
|
1286
|
-
//
|
|
1339
|
+
// ../sdk/dist/core/country-rules.js
|
|
1287
1340
|
function warn(field, message, ruleId) {
|
|
1288
1341
|
return { field, message, ruleId };
|
|
1289
1342
|
}
|
|
@@ -1424,7 +1477,7 @@ function validateCountryRules(input) {
|
|
|
1424
1477
|
return { errors, warnings };
|
|
1425
1478
|
}
|
|
1426
1479
|
|
|
1427
|
-
//
|
|
1480
|
+
// ../sdk/dist/core/code-lists.js
|
|
1428
1481
|
var CURRENCIES = /* @__PURE__ */ new Map([
|
|
1429
1482
|
["EUR", { code: "EUR", name: "Euro", minorUnits: 2 }],
|
|
1430
1483
|
["USD", { code: "USD", name: "US Dollar", minorUnits: 2 }],
|
|
@@ -1437,8 +1490,13 @@ var CURRENCIES = /* @__PURE__ */ new Map([
|
|
|
1437
1490
|
["CZK", { code: "CZK", name: "Czech Koruna", minorUnits: 2 }],
|
|
1438
1491
|
["HUF", { code: "HUF", name: "Hungarian Forint", minorUnits: 2 }],
|
|
1439
1492
|
["RON", { code: "RON", name: "Romanian Leu", minorUnits: 2 }],
|
|
1440
|
-
|
|
1441
|
-
|
|
1493
|
+
// ⛔ BGN and HRK are GONE (GPR-1205). Neither is in BR-CL-04/BR-CL-05 of the
|
|
1494
|
+
// graved 3.0.21 rulebooks — Bulgaria and Croatia both joined the euro — so
|
|
1495
|
+
// accepting them here would wave through a document the network refuses, which
|
|
1496
|
+
// is the exact failure this release closes. XCG replaces ANG (never carried
|
|
1497
|
+
// here) for the Dutch Caribbean. `currency-codes-network-parity.test.ts` reds
|
|
1498
|
+
// if this table ever readmits a code the rulebooks reject.
|
|
1499
|
+
["XCG", { code: "XCG", name: "Caribbean Guilder", minorUnits: 2 }],
|
|
1442
1500
|
["ISK", { code: "ISK", name: "Icelandic Krona", minorUnits: 0 }],
|
|
1443
1501
|
["TRY", { code: "TRY", name: "Turkish Lira", minorUnits: 2 }],
|
|
1444
1502
|
["JPY", { code: "JPY", name: "Japanese Yen", minorUnits: 0 }],
|
|
@@ -1549,7 +1607,7 @@ function getAllUnits() {
|
|
|
1549
1607
|
return Array.from(UNIT_CODES.entries()).map(([code, name]) => ({ code, name })).sort((a, b) => a.code.localeCompare(b.code));
|
|
1550
1608
|
}
|
|
1551
1609
|
|
|
1552
|
-
//
|
|
1610
|
+
// ../sdk/dist/core/validator.js
|
|
1553
1611
|
function error(field, message, ruleId, suggestion) {
|
|
1554
1612
|
return { field, message, ruleId, suggestion };
|
|
1555
1613
|
}
|
|
@@ -1564,6 +1622,11 @@ function assertString(value, fieldPath, errors) {
|
|
|
1564
1622
|
return true;
|
|
1565
1623
|
}
|
|
1566
1624
|
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
1625
|
+
var ALLOWANCE_CHARGE_CONTRACT_RULE = "GETPEPPR-ALLOWANCE-CHARGE-AMOUNT";
|
|
1626
|
+
var DERIVED_AMOUNT_CONTRACT_RULE2 = "GETPEPPR-DERIVED-AMOUNT";
|
|
1627
|
+
function survivesCents2(value) {
|
|
1628
|
+
return Number.isFinite(value) && Number.isFinite(value * 100);
|
|
1629
|
+
}
|
|
1567
1630
|
function validateParty(party, path) {
|
|
1568
1631
|
const errors = [];
|
|
1569
1632
|
if (party.name === void 0 || party.name === null || party.name === "") {
|
|
@@ -1637,6 +1700,31 @@ function validateLine(line, index, isCreditNote = false) {
|
|
|
1637
1700
|
} else if (line.vatRate < 0 || line.vatRate > 100) {
|
|
1638
1701
|
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
1702
|
}
|
|
1703
|
+
const adjustmentItems = {
|
|
1704
|
+
allowances: Array.isArray(line.allowances) ? line.allowances : [],
|
|
1705
|
+
charges: Array.isArray(line.charges) ? line.charges : []
|
|
1706
|
+
};
|
|
1707
|
+
for (const kind of ["allowances", "charges"]) {
|
|
1708
|
+
const items = line[kind];
|
|
1709
|
+
if (items === void 0)
|
|
1710
|
+
continue;
|
|
1711
|
+
if (!Array.isArray(items)) {
|
|
1712
|
+
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));
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
for (const [i, item] of items.entries()) {
|
|
1716
|
+
const amount = item?.amount;
|
|
1717
|
+
if (typeof amount !== "number" || !Number.isFinite(amount) || amount < 0) {
|
|
1718
|
+
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."));
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
const bq = typeof line.baseQuantity === "number" && line.baseQuantity > 0 ? line.baseQuantity : 1;
|
|
1723
|
+
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);
|
|
1724
|
+
const derivedVat = derivedNet * ((typeof line.vatRate === "number" ? line.vatRate : 0) / 100);
|
|
1725
|
+
if (!survivesCents2(derivedNet) || !survivesCents2(derivedVat)) {
|
|
1726
|
+
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));
|
|
1727
|
+
}
|
|
1640
1728
|
return errors;
|
|
1641
1729
|
}
|
|
1642
1730
|
function validateInvoice(input) {
|
|
@@ -1713,15 +1801,52 @@ function validateInvoice(input) {
|
|
|
1713
1801
|
if (value === void 0)
|
|
1714
1802
|
continue;
|
|
1715
1803
|
if (!Array.isArray(value)) {
|
|
1716
|
-
errors.push(error(field, `${field} must be an array`,
|
|
1804
|
+
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
1805
|
continue;
|
|
1718
1806
|
}
|
|
1719
1807
|
for (const [index, item] of value.entries()) {
|
|
1720
1808
|
if (item === null || typeof item !== "object") {
|
|
1721
1809
|
errors.push(error(`${field}[${index}]`, `${field}[${index}] must be an object`, void 0));
|
|
1810
|
+
continue;
|
|
1811
|
+
}
|
|
1812
|
+
const amount = item.amount;
|
|
1813
|
+
if (typeof amount !== "number" || !Number.isFinite(amount) || amount < 0) {
|
|
1814
|
+
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
1815
|
}
|
|
1723
1816
|
}
|
|
1724
1817
|
}
|
|
1818
|
+
if (Array.isArray(input.lines)) {
|
|
1819
|
+
const itemsOf = (value) => Array.isArray(value) ? value : [];
|
|
1820
|
+
const amountOf = (item) => typeof item?.amount === "number" ? item.amount : 0;
|
|
1821
|
+
const lineNets = input.lines.reduce((sum, line) => {
|
|
1822
|
+
if (typeof line !== "object" || line === null)
|
|
1823
|
+
return sum;
|
|
1824
|
+
const lbq = typeof line.baseQuantity === "number" && line.baseQuantity > 0 ? line.baseQuantity : 1;
|
|
1825
|
+
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);
|
|
1826
|
+
}, 0);
|
|
1827
|
+
const allowanceTotal = itemsOf(input.allowances).reduce((s, a) => s + amountOf(a), 0);
|
|
1828
|
+
const chargeTotal = itemsOf(input.charges).reduce((s, c) => s + amountOf(c), 0);
|
|
1829
|
+
const vatOf = (items, sign) => {
|
|
1830
|
+
if (!Array.isArray(items))
|
|
1831
|
+
return 0;
|
|
1832
|
+
return items.reduce((s, it) => {
|
|
1833
|
+
const rec = it;
|
|
1834
|
+
const amount = typeof rec?.amount === "number" ? rec.amount : 0;
|
|
1835
|
+
const rate = typeof rec?.vatRate === "number" ? rec.vatRate : 0;
|
|
1836
|
+
return s + sign * amount * (rate / 100);
|
|
1837
|
+
}, 0);
|
|
1838
|
+
};
|
|
1839
|
+
const docVat = vatOf(input.allowances, -1) + vatOf(input.charges, 1) + input.lines.reduce((s, line) => {
|
|
1840
|
+
if (typeof line !== "object" || line === null)
|
|
1841
|
+
return s;
|
|
1842
|
+
const lbq = typeof line.baseQuantity === "number" && line.baseQuantity > 0 ? line.baseQuantity : 1;
|
|
1843
|
+
const net = (line.quantity ?? 0) * (line.unitPrice ?? 0) / lbq;
|
|
1844
|
+
return s + net * ((typeof line.vatRate === "number" ? line.vatRate : 0) / 100);
|
|
1845
|
+
}, 0);
|
|
1846
|
+
if (!survivesCents2(lineNets - allowanceTotal + chargeTotal) || !survivesCents2(docVat)) {
|
|
1847
|
+
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));
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1725
1850
|
if (input.date) {
|
|
1726
1851
|
if (!ISO_DATE_RE.test(input.date)) {
|
|
1727
1852
|
errors.push(error("date", `Invalid date format: "${input.date}"`, void 0, "Use ISO 8601: YYYY-MM-DD"));
|
|
@@ -1781,7 +1906,7 @@ function validateInvoice(input) {
|
|
|
1781
1906
|
};
|
|
1782
1907
|
}
|
|
1783
1908
|
|
|
1784
|
-
//
|
|
1909
|
+
// ../sdk/dist/core/schematron.js
|
|
1785
1910
|
function violation(ruleId, severity, message, field) {
|
|
1786
1911
|
return { ruleId, severity, message, field };
|
|
1787
1912
|
}
|
|
@@ -2306,7 +2431,7 @@ var SDK_SCHEMATRON_RULE_IDS = [
|
|
|
2306
2431
|
"PEPPOL-EN16931-R080"
|
|
2307
2432
|
];
|
|
2308
2433
|
|
|
2309
|
-
//
|
|
2434
|
+
// ../sdk/dist/core/status-precedence.js
|
|
2310
2435
|
var STATUS_PRECEDENCE = [
|
|
2311
2436
|
{ status: "failed", family: "terminal-failure" },
|
|
2312
2437
|
{ status: "rejected", family: "terminal-failure" },
|
|
@@ -2330,20 +2455,173 @@ function statusFamily(status) {
|
|
|
2330
2455
|
}
|
|
2331
2456
|
var TERMINAL_FAILURE_STATUSES = STATUS_PRECEDENCE.filter((e) => e.family === "terminal-failure").map((e) => e.status);
|
|
2332
2457
|
|
|
2333
|
-
//
|
|
2334
|
-
var SDK_VERSION = "
|
|
2458
|
+
// ../sdk/dist/version.js
|
|
2459
|
+
var SDK_VERSION = "5.0.0";
|
|
2335
2460
|
|
|
2336
|
-
//
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2461
|
+
// ../sdk/dist/core/api-result.js
|
|
2462
|
+
var API_RESULT_HEADER_NAMES = {
|
|
2463
|
+
requestId: "Getpeppr-Request-Id",
|
|
2464
|
+
resultCode: "Getpeppr-Result-Code",
|
|
2465
|
+
resultMessage: "Getpeppr-Result-Message",
|
|
2466
|
+
retryable: "Getpeppr-Retryable",
|
|
2467
|
+
remediation: "Getpeppr-Remediation",
|
|
2468
|
+
docs: "Getpeppr-Result-Docs"
|
|
2469
|
+
};
|
|
2470
|
+
var CONTROL_CHARACTERS = /[\u0000-\u001F\u007F-\u009F]/g;
|
|
2471
|
+
function stripControls(value) {
|
|
2472
|
+
return value.replace(CONTROL_CHARACTERS, " ");
|
|
2473
|
+
}
|
|
2474
|
+
function safeDocsUrl(value) {
|
|
2475
|
+
if (typeof value !== "string")
|
|
2476
|
+
return null;
|
|
2477
|
+
let parsed;
|
|
2478
|
+
try {
|
|
2479
|
+
parsed = new URL(value);
|
|
2480
|
+
} catch {
|
|
2481
|
+
return null;
|
|
2344
2482
|
}
|
|
2483
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
2484
|
+
return null;
|
|
2485
|
+
return parsed.href;
|
|
2486
|
+
}
|
|
2487
|
+
var MAX_BYTES = {
|
|
2488
|
+
requestId: 256,
|
|
2489
|
+
code: 256,
|
|
2490
|
+
message: 1024,
|
|
2491
|
+
remediation: 64,
|
|
2492
|
+
docs: 2048,
|
|
2493
|
+
// ⚠️ A COST bound, not a correctness one — and the distinction is measured.
|
|
2494
|
+
// When this field was still trimmed, a huge padded value could normalise into
|
|
2495
|
+
// `true`, so the ceiling changed the verdict. Now that the token is compared
|
|
2496
|
+
// verbatim, no string can be both over-long and equal to "true"/"false": a
|
|
2497
|
+
// mutation removing this ceiling SURVIVES the suite, and correctly so. It
|
|
2498
|
+
// stays to bound the whitespace/control scan on an absurd value, and it is
|
|
2499
|
+
// the one entry here with no test — deliberately, since any assertion would
|
|
2500
|
+
// be satisfied by both answers.
|
|
2501
|
+
retryable: 32
|
|
2502
|
+
};
|
|
2503
|
+
var STRICT_HTTPS_URL = /^https:\/\/[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?(?::\d{1,5})?(?:\/[^\s\\]*)?$/i;
|
|
2504
|
+
var TEXT_ENCODER = new TextEncoder();
|
|
2505
|
+
function withinBudget(raw, maxBytes) {
|
|
2506
|
+
if (raw === null)
|
|
2507
|
+
return void 0;
|
|
2508
|
+
return TEXT_ENCODER.encode(raw).length > maxBytes ? void 0 : raw;
|
|
2509
|
+
}
|
|
2510
|
+
function readSentence(raw, maxBytes) {
|
|
2511
|
+
const bounded = withinBudget(raw, maxBytes);
|
|
2512
|
+
if (bounded === void 0)
|
|
2513
|
+
return void 0;
|
|
2514
|
+
const cleaned = stripControls(bounded).trim();
|
|
2515
|
+
return cleaned === "" ? void 0 : cleaned;
|
|
2516
|
+
}
|
|
2517
|
+
function readMachineToken(raw, maxBytes) {
|
|
2518
|
+
const bounded = withinBudget(raw, maxBytes);
|
|
2519
|
+
if (bounded === void 0)
|
|
2520
|
+
return void 0;
|
|
2521
|
+
if (bounded === "")
|
|
2522
|
+
return void 0;
|
|
2523
|
+
if (new RegExp(CONTROL_CHARACTERS.source).test(bounded))
|
|
2524
|
+
return void 0;
|
|
2525
|
+
if (/\s/u.test(bounded))
|
|
2526
|
+
return void 0;
|
|
2527
|
+
return bounded;
|
|
2528
|
+
}
|
|
2529
|
+
function readBoolean(raw) {
|
|
2530
|
+
const token = readMachineToken(raw, MAX_BYTES.retryable);
|
|
2531
|
+
if (token === void 0)
|
|
2532
|
+
return void 0;
|
|
2533
|
+
if (token === "true")
|
|
2534
|
+
return true;
|
|
2535
|
+
if (token === "false")
|
|
2536
|
+
return false;
|
|
2345
2537
|
return void 0;
|
|
2346
2538
|
}
|
|
2539
|
+
function readDocsUrl(raw) {
|
|
2540
|
+
const token = readMachineToken(raw, MAX_BYTES.docs);
|
|
2541
|
+
if (token === void 0)
|
|
2542
|
+
return void 0;
|
|
2543
|
+
if (!STRICT_HTTPS_URL.test(token))
|
|
2544
|
+
return void 0;
|
|
2545
|
+
const parsed = safeDocsUrl(token);
|
|
2546
|
+
if (parsed === null)
|
|
2547
|
+
return void 0;
|
|
2548
|
+
const url = new URL(parsed);
|
|
2549
|
+
if (url.protocol !== "https:")
|
|
2550
|
+
return void 0;
|
|
2551
|
+
if (url.username !== "" || url.password !== "")
|
|
2552
|
+
return void 0;
|
|
2553
|
+
return url.href;
|
|
2554
|
+
}
|
|
2555
|
+
function parseApiResultHeaders(headers) {
|
|
2556
|
+
const requestId = readMachineToken(headers.get(API_RESULT_HEADER_NAMES.requestId), MAX_BYTES.requestId);
|
|
2557
|
+
const code = readMachineToken(headers.get(API_RESULT_HEADER_NAMES.resultCode), MAX_BYTES.code);
|
|
2558
|
+
const message = readSentence(headers.get(API_RESULT_HEADER_NAMES.resultMessage), MAX_BYTES.message);
|
|
2559
|
+
const retryable = readBoolean(headers.get(API_RESULT_HEADER_NAMES.retryable));
|
|
2560
|
+
const remediation = readMachineToken(headers.get(API_RESULT_HEADER_NAMES.remediation), MAX_BYTES.remediation);
|
|
2561
|
+
const docs = readDocsUrl(headers.get(API_RESULT_HEADER_NAMES.docs));
|
|
2562
|
+
const result = {};
|
|
2563
|
+
if (requestId !== void 0)
|
|
2564
|
+
result.requestId = requestId;
|
|
2565
|
+
if (code !== void 0)
|
|
2566
|
+
result.code = code;
|
|
2567
|
+
if (message !== void 0)
|
|
2568
|
+
result.message = message;
|
|
2569
|
+
if (retryable !== void 0)
|
|
2570
|
+
result.retryable = retryable;
|
|
2571
|
+
if (remediation !== void 0)
|
|
2572
|
+
result.remediation = remediation;
|
|
2573
|
+
if (docs !== void 0)
|
|
2574
|
+
result.docs = docs;
|
|
2575
|
+
return Object.keys(result).length === 0 ? void 0 : result;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
// ../sdk/dist/core/client.js
|
|
2579
|
+
function normalizeHeaderValue(value) {
|
|
2580
|
+
return value.replace(/^[\t\n\r ]+|[\t\n\r ]+$/g, "");
|
|
2581
|
+
}
|
|
2582
|
+
function isCarriableHeaderValue(value) {
|
|
2583
|
+
for (let i = 0; i < value.length; i++) {
|
|
2584
|
+
const code = value.charCodeAt(i);
|
|
2585
|
+
const carriable = code === 9 || code >= 32 && code <= 126 || code >= 128 && code <= 255;
|
|
2586
|
+
if (!carriable)
|
|
2587
|
+
return false;
|
|
2588
|
+
}
|
|
2589
|
+
return true;
|
|
2590
|
+
}
|
|
2591
|
+
function idempotencyKeyRefusal(message) {
|
|
2592
|
+
return new PeppolValidationError(`Invalid idempotency key: ${message}`, {
|
|
2593
|
+
valid: false,
|
|
2594
|
+
errors: [{ field: "idempotencyKey", message }],
|
|
2595
|
+
warnings: []
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
function applyIdempotencyKey(headers, options) {
|
|
2599
|
+
const key = options?.idempotencyKey;
|
|
2600
|
+
if (key === void 0 || key === null)
|
|
2601
|
+
return;
|
|
2602
|
+
if (typeof key !== "string") {
|
|
2603
|
+
throw idempotencyKeyRefusal(`expected a string, received ${Array.isArray(key) ? "an array" : `a ${typeof key}`}.`);
|
|
2604
|
+
}
|
|
2605
|
+
const normalized = normalizeHeaderValue(key);
|
|
2606
|
+
if (normalized === "") {
|
|
2607
|
+
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.");
|
|
2608
|
+
}
|
|
2609
|
+
if (!isCarriableHeaderValue(normalized)) {
|
|
2610
|
+
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.");
|
|
2611
|
+
}
|
|
2612
|
+
headers["Idempotency-Key"] = normalized;
|
|
2613
|
+
}
|
|
2614
|
+
function carriesUsableIdempotencyKey(headers) {
|
|
2615
|
+
if (!headers)
|
|
2616
|
+
return false;
|
|
2617
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
2618
|
+
if (name.toLowerCase() !== "idempotency-key")
|
|
2619
|
+
continue;
|
|
2620
|
+
if (typeof value === "string" && normalizeHeaderValue(value) !== "")
|
|
2621
|
+
return true;
|
|
2622
|
+
}
|
|
2623
|
+
return false;
|
|
2624
|
+
}
|
|
2347
2625
|
var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
2348
2626
|
function sleep(ms) {
|
|
2349
2627
|
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
@@ -2369,33 +2647,16 @@ function parseRetryAfter(headerValue) {
|
|
|
2369
2647
|
}
|
|
2370
2648
|
return void 0;
|
|
2371
2649
|
}
|
|
2372
|
-
var CONTROL_CHARACTERS = /[\u0000-\u001F\u007F-\u009F]/g;
|
|
2373
|
-
function stripControls(value) {
|
|
2374
|
-
return value.replace(CONTROL_CHARACTERS, " ");
|
|
2375
|
-
}
|
|
2376
2650
|
function readOwn(source, key) {
|
|
2377
2651
|
return Object.hasOwn(source, key) ? source[key] : void 0;
|
|
2378
2652
|
}
|
|
2379
|
-
function
|
|
2653
|
+
function readSentence2(source, key) {
|
|
2380
2654
|
const value = readOwn(source, key);
|
|
2381
2655
|
if (typeof value !== "string")
|
|
2382
2656
|
return null;
|
|
2383
2657
|
const cleaned = stripControls(value).trim();
|
|
2384
2658
|
return cleaned === "" ? null : cleaned;
|
|
2385
2659
|
}
|
|
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
2660
|
function formatApiErrorMessage(status, rawBody) {
|
|
2400
2661
|
const verbatim = `getpeppr API error (${status}): ${stripControls(rawBody)}`;
|
|
2401
2662
|
let parsed;
|
|
@@ -2407,7 +2668,7 @@ function formatApiErrorMessage(status, rawBody) {
|
|
|
2407
2668
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2408
2669
|
return verbatim;
|
|
2409
2670
|
}
|
|
2410
|
-
const sentence =
|
|
2671
|
+
const sentence = readSentence2(parsed, "message") ?? readSentence2(parsed, "error");
|
|
2411
2672
|
if (sentence === null)
|
|
2412
2673
|
return verbatim;
|
|
2413
2674
|
const link = safeDocsUrl(readOwn(parsed, "docs"));
|
|
@@ -2415,6 +2676,8 @@ function formatApiErrorMessage(status, rawBody) {
|
|
|
2415
2676
|
}
|
|
2416
2677
|
function isRetryableError(error2) {
|
|
2417
2678
|
if (error2 instanceof PeppolApiError) {
|
|
2679
|
+
if (error2.retryable !== void 0)
|
|
2680
|
+
return error2.retryable;
|
|
2418
2681
|
return RETRYABLE_STATUS_CODES.has(error2.statusCode);
|
|
2419
2682
|
}
|
|
2420
2683
|
if (error2 instanceof Error && error2.name === "AbortError") {
|
|
@@ -2456,7 +2719,7 @@ var GetpepprAdapter = class {
|
|
|
2456
2719
|
lastError = err;
|
|
2457
2720
|
const is429 = err instanceof PeppolApiError && err.statusCode === 429;
|
|
2458
2721
|
const isSafeMethod = /^(GET|DELETE|HEAD)$/i.test(method);
|
|
2459
|
-
const hasIdempotencyKey =
|
|
2722
|
+
const hasIdempotencyKey = carriesUsableIdempotencyKey(extraHeaders);
|
|
2460
2723
|
const canRetry = is429 || isSafeMethod || hasIdempotencyKey;
|
|
2461
2724
|
if (attempt < maxRetries && canRetry && isRetryableError(err)) {
|
|
2462
2725
|
const retryAfterMs = err instanceof PeppolApiError ? err.retryAfterMs : void 0;
|
|
@@ -2499,6 +2762,7 @@ var GetpepprAdapter = class {
|
|
|
2499
2762
|
body: body ? JSON.stringify(body) : void 0,
|
|
2500
2763
|
signal: controller.signal
|
|
2501
2764
|
});
|
|
2765
|
+
const result = parseApiResultHeaders(response.headers);
|
|
2502
2766
|
if (!response.ok) {
|
|
2503
2767
|
const errorBody = await response.text().catch(() => "Unknown error");
|
|
2504
2768
|
const retryAfterMs = response.status === 429 ? parseRetryAfter(response.headers.get("Retry-After")) : void 0;
|
|
@@ -2509,12 +2773,13 @@ var GetpepprAdapter = class {
|
|
|
2509
2773
|
headers: Object.fromEntries(response.headers.entries()),
|
|
2510
2774
|
body: errorBody,
|
|
2511
2775
|
durationMs: Date.now() - startTime,
|
|
2512
|
-
timestamp: Date.now()
|
|
2776
|
+
timestamp: Date.now(),
|
|
2777
|
+
result: cloneResultForHook(result)
|
|
2513
2778
|
});
|
|
2514
2779
|
} catch {
|
|
2515
2780
|
}
|
|
2516
2781
|
}
|
|
2517
|
-
throw new PeppolApiError(formatApiErrorMessage(response.status, errorBody), response.status, errorBody, retryAfterMs);
|
|
2782
|
+
throw new PeppolApiError(formatApiErrorMessage(response.status, errorBody), response.status, errorBody, retryAfterMs, result);
|
|
2518
2783
|
}
|
|
2519
2784
|
if (response.status === 204) {
|
|
2520
2785
|
if (this.onResponse) {
|
|
@@ -2524,7 +2789,8 @@ var GetpepprAdapter = class {
|
|
|
2524
2789
|
headers: Object.fromEntries(response.headers.entries()),
|
|
2525
2790
|
body: void 0,
|
|
2526
2791
|
durationMs: Date.now() - startTime,
|
|
2527
|
-
timestamp: Date.now()
|
|
2792
|
+
timestamp: Date.now(),
|
|
2793
|
+
result: cloneResultForHook(result)
|
|
2528
2794
|
});
|
|
2529
2795
|
} catch {
|
|
2530
2796
|
}
|
|
@@ -2535,7 +2801,20 @@ var GetpepprAdapter = class {
|
|
|
2535
2801
|
try {
|
|
2536
2802
|
responseBody = await response.json();
|
|
2537
2803
|
} catch {
|
|
2538
|
-
|
|
2804
|
+
if (this.onResponse) {
|
|
2805
|
+
try {
|
|
2806
|
+
this.onResponse({
|
|
2807
|
+
status: response.status,
|
|
2808
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
2809
|
+
body: void 0,
|
|
2810
|
+
durationMs: Date.now() - startTime,
|
|
2811
|
+
timestamp: Date.now(),
|
|
2812
|
+
result: cloneResultForHook(result)
|
|
2813
|
+
});
|
|
2814
|
+
} catch {
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
throw new PeppolApiError(`getpeppr API error: unexpected response format (status ${response.status})`, response.status, "Response body is not valid JSON", void 0, result);
|
|
2539
2818
|
}
|
|
2540
2819
|
if (this.onResponse) {
|
|
2541
2820
|
try {
|
|
@@ -2554,7 +2833,8 @@ var GetpepprAdapter = class {
|
|
|
2554
2833
|
// swallows everything — so the hook fires with a marker instead.
|
|
2555
2834
|
body: cloneForHook(responseBody),
|
|
2556
2835
|
durationMs: Date.now() - startTime,
|
|
2557
|
-
timestamp: Date.now()
|
|
2836
|
+
timestamp: Date.now(),
|
|
2837
|
+
result: cloneResultForHook(result)
|
|
2558
2838
|
});
|
|
2559
2839
|
} catch {
|
|
2560
2840
|
}
|
|
@@ -2568,9 +2848,7 @@ var GetpepprAdapter = class {
|
|
|
2568
2848
|
// legacy `_draft` marker, which the current gateway rejects explicitly.
|
|
2569
2849
|
async sendInvoice(input, options) {
|
|
2570
2850
|
const headers = {};
|
|
2571
|
-
|
|
2572
|
-
headers["Idempotency-Key"] = options.idempotencyKey;
|
|
2573
|
-
}
|
|
2851
|
+
applyIdempotencyKey(headers, options);
|
|
2574
2852
|
if (options?.validateRecipient) {
|
|
2575
2853
|
headers["X-Validate-Recipient"] = options.validateRecipient === true ? "warn" : String(options.validateRecipient);
|
|
2576
2854
|
}
|
|
@@ -2579,17 +2857,17 @@ var GetpepprAdapter = class {
|
|
|
2579
2857
|
}
|
|
2580
2858
|
async createInvoice(input, options) {
|
|
2581
2859
|
const headers = {};
|
|
2582
|
-
|
|
2583
|
-
headers["Idempotency-Key"] = options.idempotencyKey;
|
|
2584
|
-
}
|
|
2860
|
+
applyIdempotencyKey(headers, options);
|
|
2585
2861
|
if (options?.validateRecipient) {
|
|
2586
2862
|
headers["X-Validate-Recipient"] = options.validateRecipient === true ? "warn" : String(options.validateRecipient);
|
|
2587
2863
|
}
|
|
2588
2864
|
const result = await this.request("POST", "/invoices", { ...input, _draft: true }, headers);
|
|
2589
2865
|
return parseSendResult(result);
|
|
2590
2866
|
}
|
|
2591
|
-
async sendInvoiceById(id) {
|
|
2592
|
-
|
|
2867
|
+
async sendInvoiceById(id, options) {
|
|
2868
|
+
const headers = {};
|
|
2869
|
+
applyIdempotencyKey(headers, options);
|
|
2870
|
+
await this.request("POST", `/invoices/send/${id}`, void 0, headers);
|
|
2593
2871
|
}
|
|
2594
2872
|
async sendCreditNote(input) {
|
|
2595
2873
|
const result = await this.request("POST", "/credit-notes", input);
|
|
@@ -2604,6 +2882,8 @@ var GetpepprAdapter = class {
|
|
|
2604
2882
|
params.set("limit", String(options.limit));
|
|
2605
2883
|
if (options?.offset != null)
|
|
2606
2884
|
params.set("offset", String(options.offset));
|
|
2885
|
+
if (options?.number != null)
|
|
2886
|
+
params.set("number", options.number);
|
|
2607
2887
|
if (options?.includeLines)
|
|
2608
2888
|
params.set("include", "lines");
|
|
2609
2889
|
const query = params.toString() ? `?${params.toString()}` : "";
|
|
@@ -2693,6 +2973,7 @@ var GetpepprAdapter = class {
|
|
|
2693
2973
|
headers: requestHeaders,
|
|
2694
2974
|
signal: controller.signal
|
|
2695
2975
|
});
|
|
2976
|
+
const result = parseApiResultHeaders(response.headers);
|
|
2696
2977
|
if (!response.ok) {
|
|
2697
2978
|
const errorBody = await response.text().catch(() => "Unknown error");
|
|
2698
2979
|
const retryAfterMs = response.status === 429 ? parseRetryAfter(response.headers.get("Retry-After")) : void 0;
|
|
@@ -2703,12 +2984,13 @@ var GetpepprAdapter = class {
|
|
|
2703
2984
|
headers: Object.fromEntries(response.headers.entries()),
|
|
2704
2985
|
body: errorBody,
|
|
2705
2986
|
durationMs: Date.now() - startTime,
|
|
2706
|
-
timestamp: Date.now()
|
|
2987
|
+
timestamp: Date.now(),
|
|
2988
|
+
result: cloneResultForHook(result)
|
|
2707
2989
|
});
|
|
2708
2990
|
} catch {
|
|
2709
2991
|
}
|
|
2710
2992
|
}
|
|
2711
|
-
throw new PeppolApiError(formatApiErrorMessage(response.status, errorBody), response.status, errorBody, retryAfterMs);
|
|
2993
|
+
throw new PeppolApiError(formatApiErrorMessage(response.status, errorBody), response.status, errorBody, retryAfterMs, result);
|
|
2712
2994
|
}
|
|
2713
2995
|
const responseBody = await response.arrayBuffer();
|
|
2714
2996
|
if (this.onResponse) {
|
|
@@ -2718,7 +3000,8 @@ var GetpepprAdapter = class {
|
|
|
2718
3000
|
headers: Object.fromEntries(response.headers.entries()),
|
|
2719
3001
|
body: `[ArrayBuffer: ${responseBody.byteLength} bytes]`,
|
|
2720
3002
|
durationMs: Date.now() - startTime,
|
|
2721
|
-
timestamp: Date.now()
|
|
3003
|
+
timestamp: Date.now(),
|
|
3004
|
+
result: cloneResultForHook(result)
|
|
2722
3005
|
});
|
|
2723
3006
|
} catch {
|
|
2724
3007
|
}
|
|
@@ -2737,7 +3020,9 @@ var GetpepprAdapter = class {
|
|
|
2737
3020
|
params.set("limit", String(options.limit));
|
|
2738
3021
|
if (options?.offset != null)
|
|
2739
3022
|
params.set("offset", String(options.offset));
|
|
2740
|
-
if (options?.
|
|
3023
|
+
if (options?.documentId != null)
|
|
3024
|
+
params.set("documentId", options.documentId);
|
|
3025
|
+
if (options?.invoiceId != null)
|
|
2741
3026
|
params.set("invoiceId", options.invoiceId);
|
|
2742
3027
|
if (options?.dateFrom)
|
|
2743
3028
|
params.set("dateFrom", options.dateFrom);
|
|
@@ -2767,8 +3052,10 @@ var GetpepprAdapter = class {
|
|
|
2767
3052
|
}
|
|
2768
3053
|
};
|
|
2769
3054
|
}
|
|
2770
|
-
async acknowledgeInvoice(id) {
|
|
2771
|
-
const
|
|
3055
|
+
async acknowledgeInvoice(id, options) {
|
|
3056
|
+
const headers = {};
|
|
3057
|
+
applyIdempotencyKey(headers, options);
|
|
3058
|
+
const result = await this.request("POST", `/invoices/${id}/ack`, void 0, headers);
|
|
2772
3059
|
return parseSendResult(result);
|
|
2773
3060
|
}
|
|
2774
3061
|
async updateInvoice(id, input) {
|
|
@@ -2819,8 +3106,10 @@ var GetpepprAdapter = class {
|
|
|
2819
3106
|
const result = await this.request("GET", `/contacts/${id}`);
|
|
2820
3107
|
return parseContact(result);
|
|
2821
3108
|
}
|
|
2822
|
-
async createContact(input) {
|
|
2823
|
-
const
|
|
3109
|
+
async createContact(input, options) {
|
|
3110
|
+
const headers = {};
|
|
3111
|
+
applyIdempotencyKey(headers, options);
|
|
3112
|
+
const result = await this.request("POST", "/contacts", input, headers);
|
|
2824
3113
|
return parseContact(result);
|
|
2825
3114
|
}
|
|
2826
3115
|
async updateContact(id, input) {
|
|
@@ -2832,8 +3121,7 @@ var GetpepprAdapter = class {
|
|
|
2832
3121
|
}
|
|
2833
3122
|
async createLegalEntity(input, options) {
|
|
2834
3123
|
const headers = {};
|
|
2835
|
-
|
|
2836
|
-
headers["Idempotency-Key"] = options.idempotencyKey;
|
|
3124
|
+
applyIdempotencyKey(headers, options);
|
|
2837
3125
|
const result = await this.request("POST", "/legal-entities", input, headers);
|
|
2838
3126
|
return parseLegalEntity(result);
|
|
2839
3127
|
}
|
|
@@ -2872,8 +3160,7 @@ var GetpepprAdapter = class {
|
|
|
2872
3160
|
}
|
|
2873
3161
|
async requestLegalEntityAttestation(id, input, options) {
|
|
2874
3162
|
const headers = {};
|
|
2875
|
-
|
|
2876
|
-
headers["Idempotency-Key"] = options.idempotencyKey;
|
|
3163
|
+
applyIdempotencyKey(headers, options);
|
|
2877
3164
|
const result = await this.request("POST", `/legal-entities/${id}/attestation`, input, headers);
|
|
2878
3165
|
return {
|
|
2879
3166
|
id: String(result.id ?? id),
|
|
@@ -2911,8 +3198,10 @@ var GetpepprAdapter = class {
|
|
|
2911
3198
|
const result = await this.request("GET", `/bank-accounts/${id}`);
|
|
2912
3199
|
return parseBankAccount(result);
|
|
2913
3200
|
}
|
|
2914
|
-
async createBankAccount(input) {
|
|
2915
|
-
const
|
|
3201
|
+
async createBankAccount(input, options) {
|
|
3202
|
+
const headers = {};
|
|
3203
|
+
applyIdempotencyKey(headers, options);
|
|
3204
|
+
const result = await this.request("POST", "/bank-accounts", input, headers);
|
|
2916
3205
|
return parseBankAccount(result);
|
|
2917
3206
|
}
|
|
2918
3207
|
async updateBankAccount(id, input) {
|
|
@@ -2949,7 +3238,9 @@ var GetpepprAdapter = class {
|
|
|
2949
3238
|
// tout en salissant le corps des envois standards.
|
|
2950
3239
|
...options.sender ? { sender: options.sender } : {}
|
|
2951
3240
|
};
|
|
2952
|
-
const
|
|
3241
|
+
const headers = {};
|
|
3242
|
+
applyIdempotencyKey(headers, options);
|
|
3243
|
+
const result = await this.request("POST", "/invoices/import", body, headers);
|
|
2953
3244
|
return parseSendResult(result);
|
|
2954
3245
|
}
|
|
2955
3246
|
async listTransportTypes() {
|
|
@@ -3003,6 +3294,9 @@ function detectMimeType(filename) {
|
|
|
3003
3294
|
}
|
|
3004
3295
|
}
|
|
3005
3296
|
var PROTOCOL_ERROR_BODY_LIMIT = 2e3;
|
|
3297
|
+
function cloneResultForHook(result) {
|
|
3298
|
+
return result === void 0 ? void 0 : { ...result };
|
|
3299
|
+
}
|
|
3006
3300
|
function cloneForHook(body) {
|
|
3007
3301
|
try {
|
|
3008
3302
|
return structuredClone(body);
|
|
@@ -3205,12 +3499,24 @@ function parseLegalEntity(raw) {
|
|
|
3205
3499
|
country: raw.country != null ? String(raw.country) : null,
|
|
3206
3500
|
identifier: idObj && idObj.scheme != null && idObj.value != null ? { scheme: String(idObj.scheme), value: String(idObj.value) } : null,
|
|
3207
3501
|
status: String(raw.status ?? "pending"),
|
|
3502
|
+
networkDiscovery: raw.networkDiscovery && typeof raw.networkDiscovery === "object" ? raw.networkDiscovery : { state: "pending", attempts: 0 },
|
|
3208
3503
|
environment: String(raw.environment ?? ""),
|
|
3209
3504
|
createdAt: String(raw.createdAt ?? "")
|
|
3210
3505
|
};
|
|
3211
3506
|
if (raw.verificationDetail != null) {
|
|
3212
3507
|
le.verificationDetail = raw.verificationDetail;
|
|
3213
3508
|
}
|
|
3509
|
+
if (raw.registrationDetail && typeof raw.registrationDetail === "object") {
|
|
3510
|
+
const reason = raw.registrationDetail.reason;
|
|
3511
|
+
const safeReasons = [
|
|
3512
|
+
"already_registered",
|
|
3513
|
+
"invalid_format",
|
|
3514
|
+
"provider_error"
|
|
3515
|
+
];
|
|
3516
|
+
le.registrationDetail = {
|
|
3517
|
+
reason: safeReasons.includes(reason) ? reason : "provider_error"
|
|
3518
|
+
};
|
|
3519
|
+
}
|
|
3214
3520
|
return le;
|
|
3215
3521
|
}
|
|
3216
3522
|
function parseAccountIdentity(raw) {
|
|
@@ -3244,9 +3550,49 @@ function parseAccountIdentity(raw) {
|
|
|
3244
3550
|
return {
|
|
3245
3551
|
environment,
|
|
3246
3552
|
legalEntity: parseAccountIdentityLegalEntity(readOwn(result, "legalEntity"), result),
|
|
3247
|
-
identifiers
|
|
3553
|
+
identifiers,
|
|
3554
|
+
sandboxFirstSend: parseSandboxFirstSendProfile(readOwn(result, "sandboxFirstSend"), result)
|
|
3248
3555
|
};
|
|
3249
3556
|
}
|
|
3557
|
+
function parseSandboxFirstSendProfile(raw, body) {
|
|
3558
|
+
if (raw === null)
|
|
3559
|
+
return null;
|
|
3560
|
+
if (!isRecord(raw)) {
|
|
3561
|
+
throw new PeppolProtocolError("The getpeppr API answered the identity request without a valid sandboxFirstSend field. Please report this response to support@getpeppr.dev.", "sandboxFirstSend", boundedBody(body));
|
|
3562
|
+
}
|
|
3563
|
+
const status = readOwn(raw, "status");
|
|
3564
|
+
if (status === "blocked") {
|
|
3565
|
+
const code = readOwn(raw, "code");
|
|
3566
|
+
const message = readOwn(raw, "message");
|
|
3567
|
+
if (typeof code === "string" && code.trim() !== "" && typeof message === "string" && message.trim() !== "") {
|
|
3568
|
+
return { status, code, message };
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3571
|
+
if (status === "ready") {
|
|
3572
|
+
const taxMode = readOwn(raw, "taxMode");
|
|
3573
|
+
const line = readOwn(raw, "line");
|
|
3574
|
+
if ((taxMode === "outside_scope" || taxMode === "reverse_charge") && isRecord(line)) {
|
|
3575
|
+
const vatRate = readOwn(line, "vatRate");
|
|
3576
|
+
const vatCategory = readOwn(line, "vatCategory");
|
|
3577
|
+
const taxExemptReason = readOwn(line, "taxExemptReason");
|
|
3578
|
+
if (vatRate === 0 && typeof taxExemptReason === "string" && taxExemptReason.trim() !== "" && taxMode === "outside_scope" && vatCategory === "O") {
|
|
3579
|
+
return {
|
|
3580
|
+
status,
|
|
3581
|
+
taxMode,
|
|
3582
|
+
line: { vatRate, vatCategory, taxExemptReason }
|
|
3583
|
+
};
|
|
3584
|
+
}
|
|
3585
|
+
if (vatRate === 0 && typeof taxExemptReason === "string" && taxExemptReason.trim() !== "" && taxMode === "reverse_charge" && vatCategory === "AE") {
|
|
3586
|
+
return {
|
|
3587
|
+
status,
|
|
3588
|
+
taxMode,
|
|
3589
|
+
line: { vatRate, vatCategory, taxExemptReason }
|
|
3590
|
+
};
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
throw new PeppolProtocolError("The getpeppr API answered the identity request with a malformed sandboxFirstSend profile. Please report this response to support@getpeppr.dev.", "sandboxFirstSend", boundedBody(body));
|
|
3595
|
+
}
|
|
3250
3596
|
function parseAccountIdentityLegalEntity(raw, body) {
|
|
3251
3597
|
if (raw === null)
|
|
3252
3598
|
return null;
|
|
@@ -3393,14 +3739,95 @@ var PeppolProtocolError = class extends PeppolError {
|
|
|
3393
3739
|
var PeppolApiError = class extends PeppolError {
|
|
3394
3740
|
statusCode;
|
|
3395
3741
|
responseBody;
|
|
3396
|
-
/**
|
|
3742
|
+
/**
|
|
3743
|
+
* Parsed `Retry-After` delay in milliseconds.
|
|
3744
|
+
*
|
|
3745
|
+
* `undefined` unless this response is a **429** AND carried a readable
|
|
3746
|
+
* `Retry-After`. No other status reads that header, whatever its remediation
|
|
3747
|
+
* says — measured, all 22 `retry_after` entries in the catalogue are 429s.
|
|
3748
|
+
* The gateway does not attach the header to every throttled answer either.
|
|
3749
|
+
*/
|
|
3397
3750
|
retryAfterMs;
|
|
3398
|
-
|
|
3751
|
+
/**
|
|
3752
|
+
* The canonical result the gateway declared for this response, read from its
|
|
3753
|
+
* six headers — no body parsing required.
|
|
3754
|
+
*
|
|
3755
|
+
* `undefined` against a gateway that has not activated the result catalogue,
|
|
3756
|
+
* and behind any hop that strips unknown headers. The flattened accessors
|
|
3757
|
+
* below all read from here, so they are `undefined` together.
|
|
3758
|
+
*/
|
|
3759
|
+
result;
|
|
3760
|
+
constructor(message, statusCode, responseBody, retryAfterMs, result) {
|
|
3399
3761
|
super(message);
|
|
3400
3762
|
this.statusCode = statusCode;
|
|
3401
3763
|
this.responseBody = responseBody;
|
|
3402
3764
|
this.name = "PeppolApiError";
|
|
3403
3765
|
this.retryAfterMs = retryAfterMs;
|
|
3766
|
+
this.result = result;
|
|
3767
|
+
}
|
|
3768
|
+
/**
|
|
3769
|
+
* Stable getpeppr result code for this failure (e.g. `"auth.api_key_invalid"`).
|
|
3770
|
+
*
|
|
3771
|
+
* ⛔ NOT the same field as {@link code}, and they can both be present with
|
|
3772
|
+
* different values: this one is the catalogue's global code, `code` is the
|
|
3773
|
+
* route's own sub-reason from the body.
|
|
3774
|
+
*
|
|
3775
|
+
* `undefined` when the gateway sent no result headers.
|
|
3776
|
+
*/
|
|
3777
|
+
get resultCode() {
|
|
3778
|
+
return this.result?.code;
|
|
3779
|
+
}
|
|
3780
|
+
/**
|
|
3781
|
+
* The catalogue's sentence for {@link resultCode}.
|
|
3782
|
+
*
|
|
3783
|
+
* ⚠️ Usually SHORTER on detail than `.message`, which is built from the
|
|
3784
|
+
* response body and can name the offending field or rule. Show `.message` to
|
|
3785
|
+
* a human; use this one when you want the stable phrasing.
|
|
3786
|
+
*
|
|
3787
|
+
* `undefined` when the gateway sent no result headers.
|
|
3788
|
+
*/
|
|
3789
|
+
get resultMessage() {
|
|
3790
|
+
return this.result?.message;
|
|
3791
|
+
}
|
|
3792
|
+
/**
|
|
3793
|
+
* Server-generated correlation id for this exact request. Quote it to support.
|
|
3794
|
+
*
|
|
3795
|
+
* `undefined` when the gateway sent no result headers — which includes every
|
|
3796
|
+
* response from a deployment predating the catalogue.
|
|
3797
|
+
*/
|
|
3798
|
+
get requestId() {
|
|
3799
|
+
return this.result?.requestId;
|
|
3800
|
+
}
|
|
3801
|
+
/**
|
|
3802
|
+
* Whether retrying this same request can succeed, per the catalogue.
|
|
3803
|
+
*
|
|
3804
|
+
* ⚠️ `undefined` means "the gateway did not say", NOT "no" — the SDK then
|
|
3805
|
+
* falls back to its historic status policy. A `false` here is an explicit
|
|
3806
|
+
* refusal and the SDK will not retry, whatever the status.
|
|
3807
|
+
*/
|
|
3808
|
+
get retryable() {
|
|
3809
|
+
return this.result?.retryable;
|
|
3810
|
+
}
|
|
3811
|
+
/**
|
|
3812
|
+
* What to do about it: `"none"`, `"fix_request"`, `"authenticate"`,
|
|
3813
|
+
* `"retry"`, `"retry_after"`, `"wait"` or `"contact_support"` today.
|
|
3814
|
+
*
|
|
3815
|
+
* Typed open — a value added server-side reaches you rather than vanishing.
|
|
3816
|
+
* `undefined` when the gateway sent no result headers.
|
|
3817
|
+
*/
|
|
3818
|
+
get remediation() {
|
|
3819
|
+
return this.result?.remediation;
|
|
3820
|
+
}
|
|
3821
|
+
/**
|
|
3822
|
+
* Documentation link for {@link resultCode}, when the catalogue provides one.
|
|
3823
|
+
*
|
|
3824
|
+
* `undefined` when the gateway sent no result headers, when the catalogue
|
|
3825
|
+
* entry has no docs link, or when the value was not a plain `https://` URL
|
|
3826
|
+
* (`http:`, credentials in the authority, and anything the URL parser would
|
|
3827
|
+
* have to repair are all refused).
|
|
3828
|
+
*/
|
|
3829
|
+
get docs() {
|
|
3830
|
+
return this.result?.docs;
|
|
3404
3831
|
}
|
|
3405
3832
|
/**
|
|
3406
3833
|
* The gateway's machine-readable error code, parsed from the JSON response body
|
|
@@ -3573,8 +4000,8 @@ ${validation.errors.map((e) => ` - ${e.field}: ${e.message}${e.suggestion ? ` (
|
|
|
3573
4000
|
* always returns 501. Submit the final document with `invoices.send()`.
|
|
3574
4001
|
* @throws {PeppolApiError} 501 with the current gateway provider
|
|
3575
4002
|
*/
|
|
3576
|
-
async sendById(id) {
|
|
3577
|
-
return this.adapter.sendInvoiceById(id);
|
|
4003
|
+
async sendById(id, options) {
|
|
4004
|
+
return this.adapter.sendInvoiceById(id, options);
|
|
3578
4005
|
}
|
|
3579
4006
|
/**
|
|
3580
4007
|
* Send an invoice via Peppol.
|
|
@@ -3748,8 +4175,8 @@ ${validation.errors.map((e) => ` - ${e.field}: ${e.message}${e.suggestion ? ` (
|
|
|
3748
4175
|
* acknowledgement and always returns 501.
|
|
3749
4176
|
* @throws {PeppolApiError} 501 with the current gateway provider
|
|
3750
4177
|
*/
|
|
3751
|
-
async acknowledge(id) {
|
|
3752
|
-
return this.adapter.acknowledgeInvoice(id);
|
|
4178
|
+
async acknowledge(id, options) {
|
|
4179
|
+
return this.adapter.acknowledgeInvoice(id, options);
|
|
3753
4180
|
}
|
|
3754
4181
|
/**
|
|
3755
4182
|
* Request an update to an existing invoice.
|
|
@@ -3976,8 +4403,8 @@ var EventOperations = class {
|
|
|
3976
4403
|
* const result = await peppol.events.list({ limit: 10 });
|
|
3977
4404
|
* console.log(result.data, result.meta);
|
|
3978
4405
|
*
|
|
3979
|
-
* // Filter by
|
|
3980
|
-
* const invoiceEvents = await peppol.events.list({
|
|
4406
|
+
* // Filter by provider document ID or getpeppr submission ID
|
|
4407
|
+
* const invoiceEvents = await peppol.events.list({ documentId: "inv-123" });
|
|
3981
4408
|
* ```
|
|
3982
4409
|
*/
|
|
3983
4410
|
async list(options) {
|
|
@@ -3988,7 +4415,7 @@ var EventOperations = class {
|
|
|
3988
4415
|
*
|
|
3989
4416
|
* @example
|
|
3990
4417
|
* ```ts
|
|
3991
|
-
* for await (const event of peppol.events.listAll({
|
|
4418
|
+
* for await (const event of peppol.events.listAll({ documentId: "inv-123" })) {
|
|
3992
4419
|
* console.log(event.name, event.createdAt);
|
|
3993
4420
|
* }
|
|
3994
4421
|
* ```
|
|
@@ -4039,8 +4466,8 @@ var ContactOperations = class {
|
|
|
4039
4466
|
* });
|
|
4040
4467
|
* ```
|
|
4041
4468
|
*/
|
|
4042
|
-
async create(input) {
|
|
4043
|
-
return this.adapter.createContact(input);
|
|
4469
|
+
async create(input, options) {
|
|
4470
|
+
return this.adapter.createContact(input, options);
|
|
4044
4471
|
}
|
|
4045
4472
|
/**
|
|
4046
4473
|
* Update an existing contact.
|
|
@@ -4265,8 +4692,8 @@ var BankAccountOperations = class {
|
|
|
4265
4692
|
* });
|
|
4266
4693
|
* ```
|
|
4267
4694
|
*/
|
|
4268
|
-
async create(input) {
|
|
4269
|
-
return this.adapter.createBankAccount(input);
|
|
4695
|
+
async create(input, options) {
|
|
4696
|
+
return this.adapter.createBankAccount(input, options);
|
|
4270
4697
|
}
|
|
4271
4698
|
/**
|
|
4272
4699
|
* Update an existing bank account.
|
|
@@ -4536,9 +4963,9 @@ function registerInitCommand(program2) {
|
|
|
4536
4963
|
3. Convert to XML: getpeppr convert ${filename}
|
|
4537
4964
|
4. Send: getpeppr send ${filename}
|
|
4538
4965
|
|
|
4539
|
-
${pc2.dim("Sandbox note:")}
|
|
4540
|
-
|
|
4541
|
-
|
|
4966
|
+
${pc2.dim("Sandbox note:")} this offline template starts with O/0 tax lines.
|
|
4967
|
+
On send, the CLI checks GET /v1/identity and refuses a conflicting sender
|
|
4968
|
+
profile before anything reaches the provider.
|
|
4542
4969
|
`);
|
|
4543
4970
|
process.exit(0);
|
|
4544
4971
|
}
|
|
@@ -5102,11 +5529,10 @@ function buildDefaultSendPayload(overrides = {}) {
|
|
|
5102
5529
|
unitPrice: amount,
|
|
5103
5530
|
vatRate: 0,
|
|
5104
5531
|
// vatCategory "O" = outside the scope of VAT (UBL 2.1 / EN 16931).
|
|
5105
|
-
//
|
|
5106
|
-
//
|
|
5107
|
-
//
|
|
5108
|
-
// derive its own provider
|
|
5109
|
-
// Do not fall back to VAT-bearing category "S": a fresh sandbox sender has no VAT number.
|
|
5532
|
+
// This offline fixture starts at O/0. Before sending, the CLI reads
|
|
5533
|
+
// GET /identity and replaces these tax fields with the sender-specific
|
|
5534
|
+
// O/0 or AE/0 first-send profile. The SDK transport strips the
|
|
5535
|
+
// builder-only reason so Storecove can derive its own provider text.
|
|
5110
5536
|
vatCategory: "O",
|
|
5111
5537
|
taxExemptReason: "Not subject to VAT"
|
|
5112
5538
|
}
|
|
@@ -5218,6 +5644,12 @@ async function pollUntilTerminal(client, documentId, options = {}) {
|
|
|
5218
5644
|
return { finalStatus: lastStatus, timedOut: true };
|
|
5219
5645
|
}
|
|
5220
5646
|
|
|
5647
|
+
// src/lib/dashboard-url.ts
|
|
5648
|
+
var DASHBOARD_INVOICES_BASE = "https://console.getpeppr.dev/invoices";
|
|
5649
|
+
function dashboardUrlForSendResult(result) {
|
|
5650
|
+
return `${DASHBOARD_INVOICES_BASE}/${result.id}`;
|
|
5651
|
+
}
|
|
5652
|
+
|
|
5221
5653
|
// src/formatters/send-result.ts
|
|
5222
5654
|
import pc5 from "picocolors";
|
|
5223
5655
|
function formatSendResult(result, mode) {
|
|
@@ -5243,7 +5675,6 @@ function formatSendResult(result, mode) {
|
|
|
5243
5675
|
// src/commands/send.ts
|
|
5244
5676
|
var API_BASE = "https://api.getpeppr.dev/v1";
|
|
5245
5677
|
var LOCAL_BASE = "http://localhost:3001/api/v1";
|
|
5246
|
-
var DASHBOARD_BASE = "https://console.getpeppr.dev/invoices";
|
|
5247
5678
|
function registerSendCommand(program2) {
|
|
5248
5679
|
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) => {
|
|
5249
5680
|
let auth;
|
|
@@ -5282,6 +5713,42 @@ function registerSendCommand(program2) {
|
|
|
5282
5713
|
}
|
|
5283
5714
|
throw e;
|
|
5284
5715
|
}
|
|
5716
|
+
const baseUrl = flags.local ? LOCAL_BASE : API_BASE;
|
|
5717
|
+
const client = new Peppol({ apiKey: auth.apiKey, baseUrl });
|
|
5718
|
+
if (auth.environment === "sandbox") {
|
|
5719
|
+
let profile;
|
|
5720
|
+
try {
|
|
5721
|
+
profile = (await client.identity.get()).sandboxFirstSend;
|
|
5722
|
+
} catch (e) {
|
|
5723
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
5724
|
+
process.stderr.write(`${pc6.red("\u2722")} Could not verify the sandbox sender profile: ${msg}
|
|
5725
|
+
`);
|
|
5726
|
+
process.exit(1);
|
|
5727
|
+
return;
|
|
5728
|
+
}
|
|
5729
|
+
if (!profile || profile.status !== "ready") {
|
|
5730
|
+
const message = profile?.status === "blocked" ? profile.message : "The sandbox first-send profile is unavailable.";
|
|
5731
|
+
process.stderr.write(`${pc6.red("\u2722")} ${message}
|
|
5732
|
+
`);
|
|
5733
|
+
process.exit(1);
|
|
5734
|
+
return;
|
|
5735
|
+
}
|
|
5736
|
+
const taxEntries = [payload.lines, payload.allowances, payload.charges].filter(Array.isArray).flat();
|
|
5737
|
+
const payloadOutsideScope = taxEntries.length > 0 && taxEntries.every((entry) => entry.vatCategory === "O");
|
|
5738
|
+
const profileOutsideScope = profile.taxMode === "outside_scope";
|
|
5739
|
+
if (file) {
|
|
5740
|
+
if (payloadOutsideScope !== profileOutsideScope) {
|
|
5741
|
+
process.stderr.write(
|
|
5742
|
+
`${pc6.red("\u2722")} This file's tax mode does not match the sandbox sender. GET /v1/identity recommends ${profile.line.vatCategory}/0. Nothing was sent.
|
|
5743
|
+
`
|
|
5744
|
+
);
|
|
5745
|
+
process.exit(1);
|
|
5746
|
+
return;
|
|
5747
|
+
}
|
|
5748
|
+
} else {
|
|
5749
|
+
payload.lines = payload.lines.map((line) => ({ ...line, ...profile.line }));
|
|
5750
|
+
}
|
|
5751
|
+
}
|
|
5285
5752
|
if (flags.validate !== false) {
|
|
5286
5753
|
const result2 = runValidation(payload);
|
|
5287
5754
|
if (!result2.valid) {
|
|
@@ -5302,8 +5769,6 @@ function registerSendCommand(program2) {
|
|
|
5302
5769
|
process.exit(0);
|
|
5303
5770
|
}
|
|
5304
5771
|
}
|
|
5305
|
-
const baseUrl = flags.local ? LOCAL_BASE : API_BASE;
|
|
5306
|
-
const client = new Peppol({ apiKey: auth.apiKey, baseUrl });
|
|
5307
5772
|
let result;
|
|
5308
5773
|
try {
|
|
5309
5774
|
result = await client.invoices.send(payload);
|
|
@@ -5313,7 +5778,7 @@ function registerSendCommand(program2) {
|
|
|
5313
5778
|
`);
|
|
5314
5779
|
process.exit(1);
|
|
5315
5780
|
}
|
|
5316
|
-
const dashboardUrl =
|
|
5781
|
+
const dashboardUrl = dashboardUrlForSendResult(result);
|
|
5317
5782
|
let finalStatus = result.status;
|
|
5318
5783
|
let timedOut = false;
|
|
5319
5784
|
let watchFailed = false;
|