@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 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: lineTax
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 - (prepaid ?? 0) + (rounding ?? 0);
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[A-Z0-9]{2}\d{9}$/;
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
- if (companyId && !FR_SIRET_RE.test(companyId)) {
668
- warnings.push(warn("to.companyId", `French company ID (SIRET) should be exactly 14 digits, got "${companyId}".`, "FR-01"));
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 && !FR_VAT_RE.test(vatNumber)) {
671
- warnings.push(warn("to.vatNumber", `French VAT number should match format FR + 2 characters + 9 digits (SIREN), got "${vatNumber}".`, "FR-02"));
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.0.0";
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
- return {
1741
+ const rawStatus = result.status == null ? void 0 : String(result.status);
1742
+ const sendResult = {
1640
1743
  id: String(result.id ?? ""),
1641
- status: mapStatus(String(result.status ?? "submitted")),
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(String(raw.state ?? raw.status ?? "submitted"))
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
- * await peppol.invoices.markAs("inv-123", "accepted");
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 if the gateway provider does not support state transitions
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 (terminalFailures.includes(result.status)) {
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) {