@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 +7 -7
- package/dist/index.js +107 -25
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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,
|
|
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": "
|
|
222
|
-
"peppolId": "
|
|
223
|
-
"street": "
|
|
224
|
-
"city": "
|
|
225
|
-
"postalCode": "
|
|
226
|
-
"country": "
|
|
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[
|
|
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
|
-
|
|
673
|
-
|
|
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 &&
|
|
676
|
-
|
|
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.
|
|
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
|
-
*
|
|
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
|
|
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: "
|
|
3097
|
-
peppolId: "
|
|
3098
|
-
street: "
|
|
3099
|
-
city: "
|
|
3100
|
-
postalCode: "
|
|
3101
|
-
country: "
|
|
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: "
|
|
3139
|
-
peppolId: "
|
|
3140
|
-
street: "
|
|
3141
|
-
city: "
|
|
3142
|
-
postalCode: "
|
|
3143
|
-
country: "
|
|
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
|
{
|