@getpeppr/cli 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +194 -23
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -418,17 +418,16 @@ function calculateTaxSubtotals(lines, allowances, charges) {
|
|
|
418
418
|
const groups = /* @__PURE__ */ new Map();
|
|
419
419
|
function addToGroup(vatCategory, vatRate, amount) {
|
|
420
420
|
const key = `${vatCategory}-${vatRate}`;
|
|
421
|
-
const lineTax = round2(amount * (vatRate / 100));
|
|
422
421
|
const existing = groups.get(key);
|
|
423
422
|
if (existing) {
|
|
424
423
|
existing.taxableAmount = round2(existing.taxableAmount + amount);
|
|
425
|
-
existing.taxAmount = round2(existing.taxAmount + lineTax);
|
|
426
424
|
} else {
|
|
427
425
|
groups.set(key, {
|
|
428
426
|
vatRate,
|
|
429
427
|
vatCategory,
|
|
430
428
|
taxableAmount: amount,
|
|
431
|
-
taxAmount:
|
|
429
|
+
taxAmount: 0
|
|
430
|
+
// computed once per group below (BR-CO-17)
|
|
432
431
|
});
|
|
433
432
|
}
|
|
434
433
|
}
|
|
@@ -441,7 +440,10 @@ function calculateTaxSubtotals(lines, allowances, charges) {
|
|
|
441
440
|
for (const c of charges ?? []) {
|
|
442
441
|
addToGroup(c.vatCategory ?? "S", c.vatRate, c.amount);
|
|
443
442
|
}
|
|
444
|
-
return Array.from(groups.values())
|
|
443
|
+
return Array.from(groups.values()).map((subtotal) => ({
|
|
444
|
+
...subtotal,
|
|
445
|
+
taxAmount: round2(subtotal.taxableAmount * (subtotal.vatRate / 100))
|
|
446
|
+
}));
|
|
445
447
|
}
|
|
446
448
|
function calculateDocumentTotals(lines, allowances, charges) {
|
|
447
449
|
const taxSubtotals = calculateTaxSubtotals(lines, allowances, charges);
|
|
@@ -486,10 +488,13 @@ function buildTaxCurrencyTotalXml(totalTax, taxCurrency, rate) {
|
|
|
486
488
|
<cbc:TaxAmount currencyID="${escapeXml(taxCurrency)}">${formatAmount(convertedAmount)}</cbc:TaxAmount>
|
|
487
489
|
</cac:TaxTotal>`;
|
|
488
490
|
}
|
|
491
|
+
function payableFromTaxInclusive(taxInclusiveAmount, prepaidAmount, roundingAmount) {
|
|
492
|
+
return Number((taxInclusiveAmount - (prepaidAmount ?? 0) + (roundingAmount ?? 0)).toFixed(2));
|
|
493
|
+
}
|
|
489
494
|
function buildLegalMonetaryTotalXml(totals, currency, options) {
|
|
490
495
|
const prepaid = options?.prepaidAmount;
|
|
491
496
|
const rounding = options?.roundingAmount;
|
|
492
|
-
const payableAmount = totals.taxInclusiveAmount
|
|
497
|
+
const payableAmount = payableFromTaxInclusive(totals.taxInclusiveAmount, prepaid, rounding);
|
|
493
498
|
return `<cac:LegalMonetaryTotal>
|
|
494
499
|
<cbc:LineExtensionAmount currencyID="${escapeXml(currency)}">${formatAmount(totals.lineExtensionAmount)}</cbc:LineExtensionAmount>
|
|
495
500
|
<cbc:TaxExclusiveAmount currencyID="${escapeXml(currency)}">${formatAmount(totals.taxExclusiveAmount)}</cbc:TaxExclusiveAmount>
|
|
@@ -626,6 +631,47 @@ function buildCreditNoteXml(input) {
|
|
|
626
631
|
</CreditNote>`;
|
|
627
632
|
}
|
|
628
633
|
|
|
634
|
+
// ../sdk/dist/core/checksums/luhn.js
|
|
635
|
+
function isValidLuhn(input) {
|
|
636
|
+
if (typeof input !== "string")
|
|
637
|
+
return false;
|
|
638
|
+
if (!/^\d+$/.test(input))
|
|
639
|
+
return false;
|
|
640
|
+
let sum = 0;
|
|
641
|
+
let alt = false;
|
|
642
|
+
for (let i = input.length - 1; i >= 0; i--) {
|
|
643
|
+
let n = input.charCodeAt(i) - 48;
|
|
644
|
+
if (alt) {
|
|
645
|
+
n *= 2;
|
|
646
|
+
if (n > 9)
|
|
647
|
+
n -= 9;
|
|
648
|
+
}
|
|
649
|
+
sum += n;
|
|
650
|
+
alt = !alt;
|
|
651
|
+
}
|
|
652
|
+
return sum % 10 === 0;
|
|
653
|
+
}
|
|
654
|
+
var LA_POSTE_SIREN = "356000000";
|
|
655
|
+
function isValidLuhnSiret(input) {
|
|
656
|
+
if (typeof input !== "string")
|
|
657
|
+
return false;
|
|
658
|
+
if (!/^\d{14}$/.test(input))
|
|
659
|
+
return false;
|
|
660
|
+
const siren = input.slice(0, 9);
|
|
661
|
+
if (!isValidLuhn(siren))
|
|
662
|
+
return false;
|
|
663
|
+
if (isValidLuhn(input))
|
|
664
|
+
return true;
|
|
665
|
+
if (siren === LA_POSTE_SIREN) {
|
|
666
|
+
let sum = 0;
|
|
667
|
+
for (let i = 0; i < input.length; i++) {
|
|
668
|
+
sum += input.charCodeAt(i) - 48;
|
|
669
|
+
}
|
|
670
|
+
return sum % 5 === 0;
|
|
671
|
+
}
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
|
|
629
675
|
// ../sdk/dist/core/country-rules.js
|
|
630
676
|
function warn(field, message, ruleId) {
|
|
631
677
|
return { field, message, ruleId };
|
|
@@ -660,15 +706,47 @@ function validateBelgiumSeller(input, _errors, warnings) {
|
|
|
660
706
|
warnings.push(warn("paymentReference", "Belgian sellers typically include a structured communication reference (+++NNN/NNNN/NNNNN+++ format).", "BE-02"));
|
|
661
707
|
}
|
|
662
708
|
}
|
|
709
|
+
var FR_SIREN_RE = /^\d{9}$/;
|
|
663
710
|
var FR_SIRET_RE = /^\d{14}$/;
|
|
664
|
-
var FR_VAT_RE = /^FR[
|
|
711
|
+
var FR_VAT_RE = /^FR[0-9A-HJ-NP-Z]{2}\d{9}$/;
|
|
712
|
+
var FR_VAT_NUMERIC_KEY_RE = /^FR(\d{2})(\d{9})$/;
|
|
713
|
+
var FR_SIREN_BASED_SCHEMES = ["0002", "0009", "0225"];
|
|
714
|
+
function frVatKey(siren) {
|
|
715
|
+
return (12 + 3 * (Number(siren) % 97)) % 97;
|
|
716
|
+
}
|
|
717
|
+
function isValidSirenOrSiret(id) {
|
|
718
|
+
if (FR_SIREN_RE.test(id))
|
|
719
|
+
return isValidLuhn(id);
|
|
720
|
+
if (FR_SIRET_RE.test(id))
|
|
721
|
+
return isValidLuhnSiret(id);
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
665
724
|
function validateFrance(input, _errors, warnings) {
|
|
666
|
-
const { companyId, vatNumber } = input.to ?? {};
|
|
667
|
-
|
|
668
|
-
|
|
725
|
+
const { companyId, companyIdScheme, vatNumber } = input.to ?? {};
|
|
726
|
+
const companyIdIsSiren = !companyIdScheme || FR_SIREN_BASED_SCHEMES.includes(companyIdScheme);
|
|
727
|
+
if (companyId != null && companyId !== "" && companyIdIsSiren) {
|
|
728
|
+
if (typeof companyId !== "string") {
|
|
729
|
+
warnings.push(warn("to.companyId", "French company ID should be a string of 9 (SIREN) or 14 (SIRET) digits.", "FR-01"));
|
|
730
|
+
} else if (!FR_SIREN_RE.test(companyId) && !FR_SIRET_RE.test(companyId)) {
|
|
731
|
+
warnings.push(warn("to.companyId", `French company ID should be a 9-digit SIREN or 14-digit SIRET, got "${companyId}".`, "FR-01"));
|
|
732
|
+
} else if (!isValidSirenOrSiret(companyId)) {
|
|
733
|
+
warnings.push(warn("to.companyId", `French company ID "${companyId}" has an invalid checksum. Verify the SIREN/SIRET.`, "FR-01"));
|
|
734
|
+
}
|
|
669
735
|
}
|
|
670
|
-
if (vatNumber &&
|
|
671
|
-
|
|
736
|
+
if (vatNumber != null && vatNumber !== "") {
|
|
737
|
+
if (typeof vatNumber !== "string") {
|
|
738
|
+
warnings.push(warn("to.vatNumber", "French VAT number should be a string matching FR + 2 characters + 9 digits (SIREN).", "FR-02"));
|
|
739
|
+
} else if (!FR_VAT_RE.test(vatNumber)) {
|
|
740
|
+
warnings.push(warn("to.vatNumber", `French VAT number should match format FR + 2 characters + 9 digits (SIREN), got "${vatNumber}".`, "FR-02"));
|
|
741
|
+
} else {
|
|
742
|
+
const numericKey = FR_VAT_NUMERIC_KEY_RE.exec(vatNumber);
|
|
743
|
+
if (numericKey) {
|
|
744
|
+
const [, key, siren] = numericKey;
|
|
745
|
+
if (Number(key) !== frVatKey(siren)) {
|
|
746
|
+
warnings.push(warn("to.vatNumber", `French VAT number "${vatNumber}" has an invalid verification key \u2014 expected FR${String(frVatKey(siren)).padStart(2, "0")}${siren}. Likely a typo.`, "FR-03"));
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
672
750
|
}
|
|
673
751
|
}
|
|
674
752
|
function validateItaly(input, _errors, warnings) {
|
|
@@ -1058,8 +1136,32 @@ function validateInvoice(input) {
|
|
|
1058
1136
|
};
|
|
1059
1137
|
}
|
|
1060
1138
|
|
|
1139
|
+
// ../sdk/dist/core/status-precedence.js
|
|
1140
|
+
var STATUS_PRECEDENCE = [
|
|
1141
|
+
{ status: "failed", family: "terminal-failure" },
|
|
1142
|
+
{ status: "rejected", family: "terminal-failure" },
|
|
1143
|
+
{ status: "paid", family: "terminal-success" },
|
|
1144
|
+
{ status: "partially_paid", family: "progress" },
|
|
1145
|
+
{ status: "accepted", family: "progress" },
|
|
1146
|
+
{ status: "conditionally_accepted", family: "progress" },
|
|
1147
|
+
{ status: "under_query", family: "progress" },
|
|
1148
|
+
{ status: "in_process", family: "progress" },
|
|
1149
|
+
{ status: "cleared", family: "progress" },
|
|
1150
|
+
{ status: "delivered", family: "progress" },
|
|
1151
|
+
{ status: "acknowledged", family: "progress" },
|
|
1152
|
+
// Terminal for developer wait semantics only — stays rank 40 (non-terminal)
|
|
1153
|
+
// in the projection guard (§3.12 two-level terminality).
|
|
1154
|
+
{ status: "no_action", family: "terminal-failure" },
|
|
1155
|
+
{ status: "submitted", family: "progress" },
|
|
1156
|
+
{ status: "unknown", family: "fallback" }
|
|
1157
|
+
];
|
|
1158
|
+
function statusFamily(status) {
|
|
1159
|
+
return STATUS_PRECEDENCE.find((e) => e.status === status)?.family ?? "fallback";
|
|
1160
|
+
}
|
|
1161
|
+
var TERMINAL_FAILURE_STATUSES = STATUS_PRECEDENCE.filter((e) => e.family === "terminal-failure").map((e) => e.status);
|
|
1162
|
+
|
|
1061
1163
|
// ../sdk/dist/version.js
|
|
1062
|
-
var SDK_VERSION = "2.
|
|
1164
|
+
var SDK_VERSION = "2.4.0";
|
|
1063
1165
|
|
|
1064
1166
|
// ../sdk/dist/core/client.js
|
|
1065
1167
|
function findHeaderCaseInsensitive(headers, name) {
|
|
@@ -1636,14 +1738,21 @@ function detectMimeType(filename) {
|
|
|
1636
1738
|
}
|
|
1637
1739
|
}
|
|
1638
1740
|
function parseSendResult(result) {
|
|
1639
|
-
|
|
1741
|
+
const rawStatus = result.status == null ? void 0 : String(result.status);
|
|
1742
|
+
const sendResult = {
|
|
1640
1743
|
id: String(result.id ?? ""),
|
|
1641
|
-
status: mapStatus(
|
|
1744
|
+
status: mapStatus(rawStatus ?? "submitted"),
|
|
1642
1745
|
peppolMessageId: result.peppolMessageId ?? result.peppol_message_id,
|
|
1643
1746
|
createdAt: String(result.createdAt ?? result.updatedAt ?? result.created_at ?? (/* @__PURE__ */ new Date()).toISOString()),
|
|
1644
1747
|
ublXml: result.ublXml,
|
|
1645
1748
|
warnings: Array.isArray(result.warnings) ? result.warnings : void 0
|
|
1646
1749
|
};
|
|
1750
|
+
if (rawStatus !== void 0)
|
|
1751
|
+
sendResult.rawStatus = rawStatus;
|
|
1752
|
+
const detail = parseStatusDetail(result.detail);
|
|
1753
|
+
if (detail)
|
|
1754
|
+
sendResult.detail = detail;
|
|
1755
|
+
return sendResult;
|
|
1647
1756
|
}
|
|
1648
1757
|
function parseDirectoryEntry(result) {
|
|
1649
1758
|
const participant = isRecord(result.participant) ? result.participant : result;
|
|
@@ -1679,6 +1788,51 @@ function formatPeppolId(scheme, id) {
|
|
|
1679
1788
|
function isRecord(value) {
|
|
1680
1789
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1681
1790
|
}
|
|
1791
|
+
var STATUS_DETAIL_AXES = ["platformFiscal", "delivery", "businessDisposition", "settlement"];
|
|
1792
|
+
function parseStatusDetailEntry(raw) {
|
|
1793
|
+
if (!isRecord(raw) || typeof raw.axis !== "string" || typeof raw.jurisdiction !== "string" || typeof raw.code !== "string" || typeof raw.label !== "string" || typeof raw.codeSystem !== "string" || typeof raw.codeVersion !== "string") {
|
|
1794
|
+
return void 0;
|
|
1795
|
+
}
|
|
1796
|
+
const entry = {
|
|
1797
|
+
axis: raw.axis,
|
|
1798
|
+
jurisdiction: raw.jurisdiction,
|
|
1799
|
+
code: raw.code,
|
|
1800
|
+
label: raw.label,
|
|
1801
|
+
codeSystem: raw.codeSystem,
|
|
1802
|
+
codeVersion: raw.codeVersion
|
|
1803
|
+
};
|
|
1804
|
+
if (isRecord(raw.standardCode) && typeof raw.standardCode.system === "string" && typeof raw.standardCode.code === "string") {
|
|
1805
|
+
entry.standardCode = { system: raw.standardCode.system, code: raw.standardCode.code };
|
|
1806
|
+
}
|
|
1807
|
+
if (typeof raw.reason === "string")
|
|
1808
|
+
entry.reason = raw.reason;
|
|
1809
|
+
if (Array.isArray(raw.warnings) && raw.warnings.every((w) => typeof w === "string")) {
|
|
1810
|
+
entry.warnings = [...raw.warnings];
|
|
1811
|
+
}
|
|
1812
|
+
if (typeof raw.failureCategory === "string") {
|
|
1813
|
+
entry.failureCategory = raw.failureCategory;
|
|
1814
|
+
}
|
|
1815
|
+
if (isRecord(raw.payment) && typeof raw.payment.amount === "number" && typeof raw.payment.currency === "string" && typeof raw.payment.date === "string") {
|
|
1816
|
+
entry.payment = { amount: raw.payment.amount, currency: raw.payment.currency, date: raw.payment.date };
|
|
1817
|
+
}
|
|
1818
|
+
if (typeof raw.paymentSemantics === "string") {
|
|
1819
|
+
entry.paymentSemantics = raw.paymentSemantics;
|
|
1820
|
+
}
|
|
1821
|
+
if (typeof raw.actor === "string")
|
|
1822
|
+
entry.actor = raw.actor;
|
|
1823
|
+
return entry;
|
|
1824
|
+
}
|
|
1825
|
+
function parseStatusDetail(raw) {
|
|
1826
|
+
if (!isRecord(raw))
|
|
1827
|
+
return void 0;
|
|
1828
|
+
const detail = {};
|
|
1829
|
+
for (const axis of STATUS_DETAIL_AXES) {
|
|
1830
|
+
const entry = parseStatusDetailEntry(raw[axis]);
|
|
1831
|
+
if (entry)
|
|
1832
|
+
detail[axis] = entry;
|
|
1833
|
+
}
|
|
1834
|
+
return Object.keys(detail).length > 0 ? detail : void 0;
|
|
1835
|
+
}
|
|
1682
1836
|
function parseContact(raw) {
|
|
1683
1837
|
const contact = {
|
|
1684
1838
|
id: String(raw.id ?? ""),
|
|
@@ -1734,11 +1888,17 @@ function parseLegalEntity(raw) {
|
|
|
1734
1888
|
return le;
|
|
1735
1889
|
}
|
|
1736
1890
|
function parseInvoiceSummary(raw) {
|
|
1891
|
+
const rawStatus = (raw.state ?? raw.status) == null ? void 0 : String(raw.state ?? raw.status);
|
|
1737
1892
|
const summary = {
|
|
1738
1893
|
id: String(raw.id ?? ""),
|
|
1739
1894
|
number: String(raw.invoiceNumber ?? raw.number ?? ""),
|
|
1740
|
-
status: mapStatus(
|
|
1895
|
+
status: mapStatus(rawStatus ?? "submitted")
|
|
1741
1896
|
};
|
|
1897
|
+
if (rawStatus !== void 0)
|
|
1898
|
+
summary.rawStatus = rawStatus;
|
|
1899
|
+
const detail = parseStatusDetail(raw.detail);
|
|
1900
|
+
if (detail)
|
|
1901
|
+
summary.detail = detail;
|
|
1742
1902
|
if (raw.createdAt != null)
|
|
1743
1903
|
summary.createdAt = String(raw.createdAt);
|
|
1744
1904
|
if (typeof raw.isCreditNote === "boolean")
|
|
@@ -2097,15 +2257,21 @@ ${validation.errors.map((e) => ` - ${e.field}: ${e.message}${e.suggestion ? ` (
|
|
|
2097
2257
|
* Transition an invoice to a new state.
|
|
2098
2258
|
* The gateway validates the state machine — invalid transitions return an error.
|
|
2099
2259
|
*
|
|
2260
|
+
* `"paid"` on a French CTC invoice reports the payment collection
|
|
2261
|
+
* (« signalement d'encaissement ») to the tax authority via the gateway —
|
|
2262
|
+
* a legal obligation of the French mandate for service invoices. The full
|
|
2263
|
+
* amount is reported from the invoice's stored tax breakdown (no amount to
|
|
2264
|
+
* pass), at most once per invoice: replays return the same report (200),
|
|
2265
|
+
* a concurrent report returns 409, a non-French invoice returns 422.
|
|
2266
|
+
* The invoice's own status becomes `paid` later, when the network confirms
|
|
2267
|
+
* (webhook / polling), not synchronously with this call.
|
|
2268
|
+
*
|
|
2100
2269
|
* @example
|
|
2101
2270
|
* ```ts
|
|
2102
|
-
*
|
|
2103
|
-
* await peppol.invoices.markAs("inv-123", "paid"
|
|
2104
|
-
* commit: "with_mail",
|
|
2105
|
-
* reason: "Payment received",
|
|
2106
|
-
* });
|
|
2271
|
+
* // France: report that the customer paid this invoice
|
|
2272
|
+
* await peppol.invoices.markAs("inv-123", "paid");
|
|
2107
2273
|
* ```
|
|
2108
|
-
* @throws {PeppolApiError} 501
|
|
2274
|
+
* @throws {PeppolApiError} 422 for "paid" on a non-French-CTC invoice; 501 for states the provider does not support
|
|
2109
2275
|
*/
|
|
2110
2276
|
async markAs(id, state, options) {
|
|
2111
2277
|
return this.adapter.markInvoiceAs(id, state, options);
|
|
@@ -2166,14 +2332,19 @@ ${validation.errors.map((e) => ` - ${e.field}: ${e.message}${e.suggestion ? ` (
|
|
|
2166
2332
|
const timeout = options?.timeout ?? 12e4;
|
|
2167
2333
|
const interval = options?.interval ?? 5e3;
|
|
2168
2334
|
const targets = Array.isArray(targetStatus) ? targetStatus : [targetStatus];
|
|
2169
|
-
const terminalFailures = ["failed", "rejected", "no_action"];
|
|
2170
2335
|
const startTime = Date.now();
|
|
2171
2336
|
while (true) {
|
|
2172
2337
|
const result = await this.getStatus(documentId);
|
|
2173
2338
|
if (targets.includes(result.status)) {
|
|
2174
2339
|
return result;
|
|
2175
2340
|
}
|
|
2176
|
-
if (
|
|
2341
|
+
if (TERMINAL_FAILURE_STATUSES.includes(result.status)) {
|
|
2342
|
+
throw new PeppolError(`Document ${documentId} reached terminal status "${result.status}" while waiting for "${targets.join('" or "')}"`);
|
|
2343
|
+
}
|
|
2344
|
+
if (statusFamily(result.status) === "terminal-success") {
|
|
2345
|
+
if (targets.every((t) => statusFamily(t) === "progress")) {
|
|
2346
|
+
return result;
|
|
2347
|
+
}
|
|
2177
2348
|
throw new PeppolError(`Document ${documentId} reached terminal status "${result.status}" while waiting for "${targets.join('" or "')}"`);
|
|
2178
2349
|
}
|
|
2179
2350
|
if (Date.now() - startTime >= timeout) {
|