@getpeppr/cli 0.5.1 → 0.5.3

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/README.md CHANGED
@@ -26,7 +26,7 @@ getpeppr init --credit-note # credit note template
26
26
  getpeppr init --force # overwrite existing file
27
27
  ```
28
28
 
29
- The generated template includes a Belgian seller, a German buyer, and two invoice lines — ready to edit, validate, and convert locally.
29
+ The generated template includes a Belgian seller, the sandbox test receiver as buyer, and two invoice lines — ready to edit, validate, convert, and send from a sandbox account.
30
30
 
31
31
  ### `getpeppr validate` — Validate an invoice
32
32
 
@@ -218,12 +218,12 @@ The input file must be a JSON object matching the getpeppr `InvoiceInput` type.
218
218
  "country": "BE"
219
219
  },
220
220
  "to": {
221
- "name": "Müller & Partner GmbH",
222
- "peppolId": "0204:DE987654321",
223
- "street": "Friedrichstraße 123",
224
- "city": "Berlin",
225
- "postalCode": "10117",
226
- "country": "DE"
221
+ "name": "SPF Economie (test receiver)",
222
+ "peppolId": "9925:BE0314595348",
223
+ "street": "Rue du Progrès 50",
224
+ "city": "Brussels",
225
+ "postalCode": "1210",
226
+ "country": "BE"
227
227
  },
228
228
  "lines": [
229
229
  {
package/dist/index.js CHANGED
@@ -631,6 +631,47 @@ function buildCreditNoteXml(input) {
631
631
  </CreditNote>`;
632
632
  }
633
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
+
634
675
  // ../sdk/dist/core/country-rules.js
635
676
  function warn(field, message, ruleId) {
636
677
  return { field, message, ruleId };
@@ -665,15 +706,47 @@ function validateBelgiumSeller(input, _errors, warnings) {
665
706
  warnings.push(warn("paymentReference", "Belgian sellers typically include a structured communication reference (+++NNN/NNNN/NNNNN+++ format).", "BE-02"));
666
707
  }
667
708
  }
709
+ var FR_SIREN_RE = /^\d{9}$/;
668
710
  var FR_SIRET_RE = /^\d{14}$/;
669
- 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
+ }
670
724
  function validateFrance(input, _errors, warnings) {
671
- const { companyId, vatNumber } = input.to ?? {};
672
- if (companyId && !FR_SIRET_RE.test(companyId)) {
673
- 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
+ }
674
735
  }
675
- if (vatNumber && !FR_VAT_RE.test(vatNumber)) {
676
- 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
+ }
677
750
  }
678
751
  }
679
752
  function validateItaly(input, _errors, warnings) {
@@ -1088,7 +1161,7 @@ function statusFamily(status) {
1088
1161
  var TERMINAL_FAILURE_STATUSES = STATUS_PRECEDENCE.filter((e) => e.family === "terminal-failure").map((e) => e.status);
1089
1162
 
1090
1163
  // ../sdk/dist/version.js
1091
- var SDK_VERSION = "2.3.0";
1164
+ var SDK_VERSION = "2.4.0";
1092
1165
 
1093
1166
  // ../sdk/dist/core/client.js
1094
1167
  function findHeaderCaseInsensitive(headers, name) {
@@ -2184,15 +2257,21 @@ ${validation.errors.map((e) => ` - ${e.field}: ${e.message}${e.suggestion ? ` (
2184
2257
  * Transition an invoice to a new state.
2185
2258
  * The gateway validates the state machine — invalid transitions return an error.
2186
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
+ *
2187
2269
  * @example
2188
2270
  * ```ts
2189
- * await peppol.invoices.markAs("inv-123", "accepted");
2190
- * await peppol.invoices.markAs("inv-123", "paid", {
2191
- * commit: "with_mail",
2192
- * reason: "Payment received",
2193
- * });
2271
+ * // France: report that the customer paid this invoice
2272
+ * await peppol.invoices.markAs("inv-123", "paid");
2194
2273
  * ```
2195
- * @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
2196
2275
  */
2197
2276
  async markAs(id, state, options) {
2198
2277
  return this.adapter.markInvoiceAs(id, state, options);
@@ -3092,13 +3171,15 @@ var INVOICE_TEMPLATE = {
3092
3171
  postalCode: "1050",
3093
3172
  country: "BE"
3094
3173
  },
3174
+ // Sandbox test receiver (GPR-828): the only recipient guaranteed reachable on
3175
+ // the Storecove test network -- real directory companies make sandbox sends fail.
3095
3176
  to: {
3096
- name: "M\xFCller & Partner GmbH",
3097
- peppolId: "0204:DE987654321",
3098
- street: "Friedrichstra\xDFe 123",
3099
- city: "Berlin",
3100
- postalCode: "10117",
3101
- country: "DE"
3177
+ name: "SPF Economie (test receiver)",
3178
+ peppolId: "9925:BE0314595348",
3179
+ street: "Rue du Progr\xE8s 50",
3180
+ city: "Brussels",
3181
+ postalCode: "1210",
3182
+ country: "BE"
3102
3183
  },
3103
3184
  lines: [
3104
3185
  {
@@ -3134,13 +3215,14 @@ var CREDIT_NOTE_TEMPLATE = {
3134
3215
  postalCode: "1050",
3135
3216
  country: "BE"
3136
3217
  },
3218
+ // Sandbox test receiver (GPR-828) \u2014 keep in sync with templates/invoice.ts.
3137
3219
  to: {
3138
- name: "M\xFCller & Partner GmbH",
3139
- peppolId: "0204:DE987654321",
3140
- street: "Friedrichstra\xDFe 123",
3141
- city: "Berlin",
3142
- postalCode: "10117",
3143
- country: "DE"
3220
+ name: "SPF Economie (test receiver)",
3221
+ peppolId: "9925:BE0314595348",
3222
+ street: "Rue du Progr\xE8s 50",
3223
+ city: "Brussels",
3224
+ postalCode: "1210",
3225
+ country: "BE"
3144
3226
  },
3145
3227
  lines: [
3146
3228
  {