@getpeppr/cli 0.8.3 → 0.9.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 +23 -0
- package/README.md +7 -4
- package/dist/index.js +156 -67
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ All notable changes to `@getpeppr/cli` are documented here.
|
|
|
4
4
|
|
|
5
5
|
The CLI bundles `@getpeppr/sdk` into its published artifact (tsup `noExternal`), so `validate`, `convert` and `init` run the SDK's validators, country rules and UBL builder locally — an SDK release only reaches CLI users once the CLI is rebundled. Rebundle-only releases are listed with the SDK version they ship.
|
|
6
6
|
|
|
7
|
+
## [0.9.0] — 2026-08-23
|
|
8
|
+
|
|
9
|
+
Before every sandbox send, the CLI now reads the exact sender profile from
|
|
10
|
+
`GET /v1/identity`. Its own generated fixture is adapted to O/0 for a
|
|
11
|
+
routing-only sender or AE/0 for a sender carrying a Tax Identifier. A
|
|
12
|
+
user-authored file is never rewritten silently: an incompatible tax mode is
|
|
13
|
+
refused locally before the invoice API is called.
|
|
14
|
+
|
|
15
|
+
This replaces the incomplete static O/0 assumption in 0.8.4. It also bundles
|
|
16
|
+
SDK 4.8.0 and fails closed when the provider inventory or scheme role cannot be
|
|
17
|
+
verified (GPR-1149).
|
|
18
|
+
|
|
19
|
+
## [0.8.4] — 2026-08-22
|
|
20
|
+
|
|
21
|
+
`getpeppr init` scaffolded the SPF Economie test receiver plus out-of-scope
|
|
22
|
+
`O/0` lines. That fixed routing-only senders but incorrectly assumed every
|
|
23
|
+
sandbox sender lacked a Tax Identifier; 0.9.0 supersedes the static choice with
|
|
24
|
+
the exact sender profile.
|
|
25
|
+
|
|
26
|
+
Every generated `O` line includes the builder-only exemption reason required by
|
|
27
|
+
the SDK's local validator. The quick `getpeppr send --to …` payload remains on
|
|
28
|
+
the same contract. This release bundles SDK 4.7.0.
|
|
29
|
+
|
|
7
30
|
## [0.8.3] — 2026-08-21
|
|
8
31
|
|
|
9
32
|
Rebundles `@getpeppr/sdk` **4.6.0** (was 4.5.0), so `getpeppr validate` and
|
package/README.md
CHANGED
|
@@ -237,11 +237,11 @@ The CLI runs three validation engines from the [@getpeppr/sdk](https://www.npmjs
|
|
|
237
237
|
2. **Business Rules** — Peppol BIS 3.0 / EN 16931 compliance (BR-xx, BR-CO-xx, PEPPOL-xx rules)
|
|
238
238
|
3. **Country Rules** — Belgium (BE), France (FR), Italy (IT), Netherlands (NL), Germany (DE)
|
|
239
239
|
|
|
240
|
-
Scaffolding (`init`), validation, and conversion run fully offline — no API key or network connection required. Only `lookup`, `send`, and `whoami` need a network connection.
|
|
240
|
+
Scaffolding (`init`), validation, and conversion run fully offline — no API key or network connection required. Only `lookup`, `send`, and `whoami` need a network connection. Before a sandbox `send`, the CLI reads `GET /v1/identity`: its own generated fixture is adapted to the returned O/0 or AE/0 profile, while a user-edited file is never rewritten silently and is refused locally if its tax mode conflicts with the sender.
|
|
241
241
|
|
|
242
242
|
## Invoice format
|
|
243
243
|
|
|
244
|
-
The input file must be a JSON object matching the getpeppr `InvoiceInput` type. `number`, `to`, and `lines` are required for API sends. The `from` party is included in the scaffold because offline validation and UBL conversion need seller metadata; API sends use the legal entity linked to your key.
|
|
244
|
+
The input file must be a JSON object matching the getpeppr `InvoiceInput` type. `number`, `to`, and `lines` are required for API sends. The `from` party is included in the scaffold because offline validation and UBL conversion need seller metadata; API sends use the legal entity linked to your key. The generated file starts as an O/0 integration fixture. At send time the CLI verifies it against the actual sender profile and never forwards an incompatible tax mode.
|
|
245
245
|
|
|
246
246
|
```json
|
|
247
247
|
{
|
|
@@ -271,14 +271,17 @@ The input file must be a JSON object matching the getpeppr `InvoiceInput` type.
|
|
|
271
271
|
"description": "Conseil en transformation numérique",
|
|
272
272
|
"quantity": 10,
|
|
273
273
|
"unitPrice": 950,
|
|
274
|
-
"vatRate":
|
|
274
|
+
"vatRate": 0,
|
|
275
|
+
"vatCategory": "O",
|
|
276
|
+
"taxExemptReason": "Integration test"
|
|
275
277
|
},
|
|
276
278
|
{
|
|
277
279
|
"description": "Software license — annual subscription",
|
|
278
280
|
"quantity": 1,
|
|
279
281
|
"unitPrice": 2400,
|
|
280
282
|
"vatRate": 0,
|
|
281
|
-
"vatCategory": "
|
|
283
|
+
"vatCategory": "O",
|
|
284
|
+
"taxExemptReason": "Integration test"
|
|
282
285
|
}
|
|
283
286
|
],
|
|
284
287
|
"paymentTerms": "Net 30 days",
|
package/dist/index.js
CHANGED
|
@@ -717,6 +717,28 @@ function formatVatRate(vatRate, field) {
|
|
|
717
717
|
}
|
|
718
718
|
return String(vatRate);
|
|
719
719
|
}
|
|
720
|
+
function formatBaseQuantity(baseQuantity, field) {
|
|
721
|
+
if (typeof baseQuantity !== "number" || !Number.isFinite(baseQuantity) || baseQuantity <= 0) {
|
|
722
|
+
throw new UblBuilderInputError("baseQuantity must be a finite number greater than zero.", field, "PEPPOL-EN16931-R121");
|
|
723
|
+
}
|
|
724
|
+
const numeric = String(baseQuantity);
|
|
725
|
+
const exponentMarker = numeric.search(/[eE]/);
|
|
726
|
+
if (exponentMarker === -1)
|
|
727
|
+
return numeric;
|
|
728
|
+
const coefficient = numeric.slice(0, exponentMarker);
|
|
729
|
+
const exponent = Number(numeric.slice(exponentMarker + 1));
|
|
730
|
+
const decimalPoint = coefficient.indexOf(".");
|
|
731
|
+
const digits = coefficient.replace(".", "");
|
|
732
|
+
const integerDigits = decimalPoint === -1 ? coefficient.length : decimalPoint;
|
|
733
|
+
const outputPoint = integerDigits + exponent;
|
|
734
|
+
if (outputPoint <= 0) {
|
|
735
|
+
return `0.${"0".repeat(-outputPoint)}${digits}`;
|
|
736
|
+
}
|
|
737
|
+
if (outputPoint >= digits.length) {
|
|
738
|
+
return `${digits}${"0".repeat(outputPoint - digits.length)}`;
|
|
739
|
+
}
|
|
740
|
+
return `${digits.slice(0, outputPoint)}.${digits.slice(outputPoint)}`;
|
|
741
|
+
}
|
|
720
742
|
function round2(n) {
|
|
721
743
|
return Math.round(n * 100) / 100;
|
|
722
744
|
}
|
|
@@ -974,7 +996,7 @@ function buildDocumentLineXml(line, index, currency, lineTag, qtyTag) {
|
|
|
974
996
|
</cac:Item>
|
|
975
997
|
<cac:Price>
|
|
976
998
|
<cbc:PriceAmount currencyID="${escapeXml(currency)}">${formatAmount(line.unitPrice)}</cbc:PriceAmount>
|
|
977
|
-
${line.baseQuantity
|
|
999
|
+
${line.baseQuantity !== void 0 ? `<cbc:BaseQuantity unitCode="${escapeXml(resolveUnitCode(line.baseQuantityUnit ?? line.unit ?? DEFAULT_UNIT))}">${formatBaseQuantity(line.baseQuantity, `lines[${index}].baseQuantity`)}</cbc:BaseQuantity>` : ""}
|
|
978
1000
|
</cac:Price>
|
|
979
1001
|
</cac:${lineTag}>`;
|
|
980
1002
|
}
|
|
@@ -1605,6 +1627,11 @@ function validateLine(line, index, isCreditNote = false) {
|
|
|
1605
1627
|
} else if (line.unitPrice < 0) {
|
|
1606
1628
|
errors.push(error(`${path}.unitPrice`, `Unit price cannot be negative, got ${line.unitPrice}`, void 0, "For discounts, use a negative quantity or a separate discount line"));
|
|
1607
1629
|
}
|
|
1630
|
+
if (line.baseQuantity !== void 0) {
|
|
1631
|
+
if (typeof line.baseQuantity !== "number" || !Number.isFinite(line.baseQuantity) || line.baseQuantity <= 0) {
|
|
1632
|
+
errors.push(error(`${path}.baseQuantity`, "Base quantity must be a finite number greater than zero", "PEPPOL-EN16931-R121", "Use a positive number for the item price base quantity"));
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1608
1635
|
if (line.vatRate === void 0 || line.vatRate === null) {
|
|
1609
1636
|
errors.push(error(`${path}.vatRate`, "VAT rate is required", "BR-CO-17"));
|
|
1610
1637
|
} else if (line.vatRate < 0 || line.vatRate > 100) {
|
|
@@ -2304,7 +2331,7 @@ function statusFamily(status) {
|
|
|
2304
2331
|
var TERMINAL_FAILURE_STATUSES = STATUS_PRECEDENCE.filter((e) => e.family === "terminal-failure").map((e) => e.status);
|
|
2305
2332
|
|
|
2306
2333
|
// ../sdk/dist/version.js
|
|
2307
|
-
var SDK_VERSION = "4.
|
|
2334
|
+
var SDK_VERSION = "4.8.0";
|
|
2308
2335
|
|
|
2309
2336
|
// ../sdk/dist/core/client.js
|
|
2310
2337
|
function findHeaderCaseInsensitive(headers, name) {
|
|
@@ -2537,9 +2564,8 @@ var GetpepprAdapter = class {
|
|
|
2537
2564
|
clearTimeout(timeoutId);
|
|
2538
2565
|
}
|
|
2539
2566
|
}
|
|
2540
|
-
//
|
|
2541
|
-
//
|
|
2542
|
-
// { invoice: {...}, send_after_import: true }, createInvoice omits the flag.
|
|
2567
|
+
// sendInvoice and createInvoice hit the same endpoint. The latter adds the
|
|
2568
|
+
// legacy `_draft` marker, which the current gateway rejects explicitly.
|
|
2543
2569
|
async sendInvoice(input, options) {
|
|
2544
2570
|
const headers = {};
|
|
2545
2571
|
if (options?.idempotencyKey) {
|
|
@@ -2578,6 +2604,8 @@ var GetpepprAdapter = class {
|
|
|
2578
2604
|
params.set("limit", String(options.limit));
|
|
2579
2605
|
if (options?.offset != null)
|
|
2580
2606
|
params.set("offset", String(options.offset));
|
|
2607
|
+
if (options?.number != null)
|
|
2608
|
+
params.set("number", options.number);
|
|
2581
2609
|
if (options?.includeLines)
|
|
2582
2610
|
params.set("include", "lines");
|
|
2583
2611
|
const query = params.toString() ? `?${params.toString()}` : "";
|
|
@@ -2711,7 +2739,9 @@ var GetpepprAdapter = class {
|
|
|
2711
2739
|
params.set("limit", String(options.limit));
|
|
2712
2740
|
if (options?.offset != null)
|
|
2713
2741
|
params.set("offset", String(options.offset));
|
|
2714
|
-
if (options?.
|
|
2742
|
+
if (options?.documentId != null)
|
|
2743
|
+
params.set("documentId", options.documentId);
|
|
2744
|
+
if (options?.invoiceId != null)
|
|
2715
2745
|
params.set("invoiceId", options.invoiceId);
|
|
2716
2746
|
if (options?.dateFrom)
|
|
2717
2747
|
params.set("dateFrom", options.dateFrom);
|
|
@@ -3218,9 +3248,49 @@ function parseAccountIdentity(raw) {
|
|
|
3218
3248
|
return {
|
|
3219
3249
|
environment,
|
|
3220
3250
|
legalEntity: parseAccountIdentityLegalEntity(readOwn(result, "legalEntity"), result),
|
|
3221
|
-
identifiers
|
|
3251
|
+
identifiers,
|
|
3252
|
+
sandboxFirstSend: parseSandboxFirstSendProfile(readOwn(result, "sandboxFirstSend"), result)
|
|
3222
3253
|
};
|
|
3223
3254
|
}
|
|
3255
|
+
function parseSandboxFirstSendProfile(raw, body) {
|
|
3256
|
+
if (raw === null)
|
|
3257
|
+
return null;
|
|
3258
|
+
if (!isRecord(raw)) {
|
|
3259
|
+
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));
|
|
3260
|
+
}
|
|
3261
|
+
const status = readOwn(raw, "status");
|
|
3262
|
+
if (status === "blocked") {
|
|
3263
|
+
const code = readOwn(raw, "code");
|
|
3264
|
+
const message = readOwn(raw, "message");
|
|
3265
|
+
if (typeof code === "string" && code.trim() !== "" && typeof message === "string" && message.trim() !== "") {
|
|
3266
|
+
return { status, code, message };
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
if (status === "ready") {
|
|
3270
|
+
const taxMode = readOwn(raw, "taxMode");
|
|
3271
|
+
const line = readOwn(raw, "line");
|
|
3272
|
+
if ((taxMode === "outside_scope" || taxMode === "reverse_charge") && isRecord(line)) {
|
|
3273
|
+
const vatRate = readOwn(line, "vatRate");
|
|
3274
|
+
const vatCategory = readOwn(line, "vatCategory");
|
|
3275
|
+
const taxExemptReason = readOwn(line, "taxExemptReason");
|
|
3276
|
+
if (vatRate === 0 && typeof taxExemptReason === "string" && taxExemptReason.trim() !== "" && taxMode === "outside_scope" && vatCategory === "O") {
|
|
3277
|
+
return {
|
|
3278
|
+
status,
|
|
3279
|
+
taxMode,
|
|
3280
|
+
line: { vatRate, vatCategory, taxExemptReason }
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
3283
|
+
if (vatRate === 0 && typeof taxExemptReason === "string" && taxExemptReason.trim() !== "" && taxMode === "reverse_charge" && vatCategory === "AE") {
|
|
3284
|
+
return {
|
|
3285
|
+
status,
|
|
3286
|
+
taxMode,
|
|
3287
|
+
line: { vatRate, vatCategory, taxExemptReason }
|
|
3288
|
+
};
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
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));
|
|
3293
|
+
}
|
|
3224
3294
|
function parseAccountIdentityLegalEntity(raw, body) {
|
|
3225
3295
|
if (raw === null)
|
|
3226
3296
|
return null;
|
|
@@ -3521,16 +3591,12 @@ var InvoiceOperations = class {
|
|
|
3521
3591
|
this.adapter = adapter;
|
|
3522
3592
|
}
|
|
3523
3593
|
/**
|
|
3524
|
-
*
|
|
3525
|
-
* Validates input client-side, then creates the invoice via the gateway.
|
|
3526
|
-
* Use `sendById()` to send the draft when ready.
|
|
3594
|
+
* Request draft creation from the gateway.
|
|
3527
3595
|
*
|
|
3528
|
-
* @
|
|
3529
|
-
*
|
|
3530
|
-
*
|
|
3531
|
-
*
|
|
3532
|
-
* await peppol.invoices.sendById(draft.id);
|
|
3533
|
-
* ```
|
|
3596
|
+
* @deprecated The current Storecove-backed gateway does not support drafts
|
|
3597
|
+
* and returns 422 `drafts_not_supported`. Submit the final document with
|
|
3598
|
+
* `invoices.send()` instead.
|
|
3599
|
+
* @throws {PeppolApiError} 422 with code `drafts_not_supported`
|
|
3534
3600
|
*/
|
|
3535
3601
|
async create(input, options) {
|
|
3536
3602
|
const validation = validateInvoice(input);
|
|
@@ -3545,14 +3611,11 @@ ${validation.errors.map((e) => ` - ${e.field}: ${e.message}${e.suggestion ? ` (
|
|
|
3545
3611
|
return result;
|
|
3546
3612
|
}
|
|
3547
3613
|
/**
|
|
3548
|
-
*
|
|
3549
|
-
* The invoice must have been previously created with `create()`.
|
|
3614
|
+
* Request sending of an existing draft invoice by ID.
|
|
3550
3615
|
*
|
|
3551
|
-
* @
|
|
3552
|
-
*
|
|
3553
|
-
*
|
|
3554
|
-
* ```
|
|
3555
|
-
* @throws {PeppolApiError} 501 if the gateway provider does not support draft sending
|
|
3616
|
+
* @deprecated The current Storecove-backed gateway has no draft lifecycle and
|
|
3617
|
+
* always returns 501. Submit the final document with `invoices.send()`.
|
|
3618
|
+
* @throws {PeppolApiError} 501 with the current gateway provider
|
|
3556
3619
|
*/
|
|
3557
3620
|
async sendById(id) {
|
|
3558
3621
|
return this.adapter.sendInvoiceById(id);
|
|
@@ -3723,52 +3786,39 @@ ${validation.errors.map((e) => ` - ${e.field}: ${e.message}${e.suggestion ? ` (
|
|
|
3723
3786
|
return this.adapter.importInvoice(options);
|
|
3724
3787
|
}
|
|
3725
3788
|
/**
|
|
3726
|
-
*
|
|
3789
|
+
* Request acknowledgement of a received invoice.
|
|
3727
3790
|
*
|
|
3728
|
-
* @
|
|
3729
|
-
*
|
|
3730
|
-
*
|
|
3731
|
-
* console.log(result.status); // "accepted"
|
|
3732
|
-
* ```
|
|
3733
|
-
* @throws {PeppolApiError} 501 if the gateway provider does not support acknowledgement
|
|
3791
|
+
* @deprecated The current Storecove-backed gateway does not support
|
|
3792
|
+
* acknowledgement and always returns 501.
|
|
3793
|
+
* @throws {PeppolApiError} 501 with the current gateway provider
|
|
3734
3794
|
*/
|
|
3735
3795
|
async acknowledge(id) {
|
|
3736
3796
|
return this.adapter.acknowledgeInvoice(id);
|
|
3737
3797
|
}
|
|
3738
3798
|
/**
|
|
3739
|
-
*
|
|
3740
|
-
* Only include the fields you want to change — partial updates are supported.
|
|
3799
|
+
* Request an update to an existing invoice.
|
|
3741
3800
|
*
|
|
3742
|
-
* @
|
|
3743
|
-
*
|
|
3744
|
-
*
|
|
3745
|
-
* dueDate: "2026-04-01",
|
|
3746
|
-
* lines: [
|
|
3747
|
-
* { id: "line-1", quantity: 5 },
|
|
3748
|
-
* { id: "line-2", _destroy: true },
|
|
3749
|
-
* ],
|
|
3750
|
-
* });
|
|
3751
|
-
* ```
|
|
3752
|
-
* @throws {PeppolApiError} 501 if the gateway provider does not support invoice updates
|
|
3801
|
+
* @deprecated Storecove documents are immutable after submission. The
|
|
3802
|
+
* current gateway always returns 501; issue a credit note instead.
|
|
3803
|
+
* @throws {PeppolApiError} 501 with the current gateway provider
|
|
3753
3804
|
*/
|
|
3754
3805
|
async update(id, input) {
|
|
3755
3806
|
return this.adapter.updateInvoice(id, input);
|
|
3756
3807
|
}
|
|
3757
3808
|
/**
|
|
3758
|
-
*
|
|
3809
|
+
* Request deletion of an invoice.
|
|
3759
3810
|
*
|
|
3760
|
-
* @
|
|
3761
|
-
*
|
|
3762
|
-
*
|
|
3763
|
-
* ```
|
|
3764
|
-
* @throws {PeppolApiError} 501 if the gateway provider does not support invoice deletion
|
|
3811
|
+
* @deprecated The current Storecove-backed gateway does not support invoice
|
|
3812
|
+
* deletion and always returns 501.
|
|
3813
|
+
* @throws {PeppolApiError} 501 with the current gateway provider
|
|
3765
3814
|
*/
|
|
3766
3815
|
async delete(id) {
|
|
3767
3816
|
return this.adapter.deleteInvoice(id);
|
|
3768
3817
|
}
|
|
3769
3818
|
/**
|
|
3770
|
-
*
|
|
3771
|
-
*
|
|
3819
|
+
* Report a French CTC invoice as paid.
|
|
3820
|
+
* Other state transitions are retained for API compatibility but the current
|
|
3821
|
+
* Storecove-backed gateway returns 501 for them.
|
|
3772
3822
|
*
|
|
3773
3823
|
* `"paid"` on a French CTC invoice reports the payment collection
|
|
3774
3824
|
* (« signalement d'encaissement ») to the tax authority via the gateway —
|
|
@@ -3970,8 +4020,8 @@ var EventOperations = class {
|
|
|
3970
4020
|
* const result = await peppol.events.list({ limit: 10 });
|
|
3971
4021
|
* console.log(result.data, result.meta);
|
|
3972
4022
|
*
|
|
3973
|
-
* // Filter by
|
|
3974
|
-
* const invoiceEvents = await peppol.events.list({
|
|
4023
|
+
* // Filter by provider document ID or getpeppr submission ID
|
|
4024
|
+
* const invoiceEvents = await peppol.events.list({ documentId: "inv-123" });
|
|
3975
4025
|
* ```
|
|
3976
4026
|
*/
|
|
3977
4027
|
async list(options) {
|
|
@@ -3982,7 +4032,7 @@ var EventOperations = class {
|
|
|
3982
4032
|
*
|
|
3983
4033
|
* @example
|
|
3984
4034
|
* ```ts
|
|
3985
|
-
* for await (const event of peppol.events.listAll({
|
|
4035
|
+
* for await (const event of peppol.events.listAll({ documentId: "inv-123" })) {
|
|
3986
4036
|
* console.log(event.name, event.createdAt);
|
|
3987
4037
|
* }
|
|
3988
4038
|
* ```
|
|
@@ -4448,14 +4498,17 @@ var INVOICE_TEMPLATE = {
|
|
|
4448
4498
|
description: "Conseil en transformation num\xE9rique",
|
|
4449
4499
|
quantity: 10,
|
|
4450
4500
|
unitPrice: 950,
|
|
4451
|
-
vatRate:
|
|
4501
|
+
vatRate: 0,
|
|
4502
|
+
vatCategory: "O",
|
|
4503
|
+
taxExemptReason: "Integration test"
|
|
4452
4504
|
},
|
|
4453
4505
|
{
|
|
4454
4506
|
description: "Software license \u2014 annual subscription",
|
|
4455
4507
|
quantity: 1,
|
|
4456
4508
|
unitPrice: 2400,
|
|
4457
4509
|
vatRate: 0,
|
|
4458
|
-
vatCategory: "
|
|
4510
|
+
vatCategory: "O",
|
|
4511
|
+
taxExemptReason: "Integration test"
|
|
4459
4512
|
}
|
|
4460
4513
|
],
|
|
4461
4514
|
paymentTerms: "Net 30 days",
|
|
@@ -4491,7 +4544,9 @@ var CREDIT_NOTE_TEMPLATE = {
|
|
|
4491
4544
|
description: "Avoir partiel \u2014 Conseil en transformation num\xE9rique",
|
|
4492
4545
|
quantity: 2,
|
|
4493
4546
|
unitPrice: 950,
|
|
4494
|
-
vatRate:
|
|
4547
|
+
vatRate: 0,
|
|
4548
|
+
vatCategory: "O",
|
|
4549
|
+
taxExemptReason: "Integration test"
|
|
4495
4550
|
}
|
|
4496
4551
|
],
|
|
4497
4552
|
note: "Avoir pour prestations non r\xE9alis\xE9es \u2014 r\xE9f. INV-2026-001"
|
|
@@ -4525,9 +4580,9 @@ function registerInitCommand(program2) {
|
|
|
4525
4580
|
3. Convert to XML: getpeppr convert ${filename}
|
|
4526
4581
|
4. Send: getpeppr send ${filename}
|
|
4527
4582
|
|
|
4528
|
-
${pc2.dim("Sandbox note:")} this template
|
|
4529
|
-
|
|
4530
|
-
|
|
4583
|
+
${pc2.dim("Sandbox note:")} this offline template starts with O/0 tax lines.
|
|
4584
|
+
On send, the CLI checks GET /v1/identity and refuses a conflicting sender
|
|
4585
|
+
profile before anything reaches the provider.
|
|
4531
4586
|
`);
|
|
4532
4587
|
process.exit(0);
|
|
4533
4588
|
}
|
|
@@ -5090,11 +5145,11 @@ function buildDefaultSendPayload(overrides = {}) {
|
|
|
5090
5145
|
quantity: 1,
|
|
5091
5146
|
unitPrice: amount,
|
|
5092
5147
|
vatRate: 0,
|
|
5093
|
-
// vatCategory "O" =
|
|
5094
|
-
//
|
|
5095
|
-
//
|
|
5096
|
-
//
|
|
5097
|
-
//
|
|
5148
|
+
// vatCategory "O" = outside the scope of VAT (UBL 2.1 / EN 16931).
|
|
5149
|
+
// This offline fixture starts at O/0. Before sending, the CLI reads
|
|
5150
|
+
// GET /identity and replaces these tax fields with the sender-specific
|
|
5151
|
+
// O/0 or AE/0 first-send profile. The SDK transport strips the
|
|
5152
|
+
// builder-only reason so Storecove can derive its own provider text.
|
|
5098
5153
|
vatCategory: "O",
|
|
5099
5154
|
taxExemptReason: "Not subject to VAT"
|
|
5100
5155
|
}
|
|
@@ -5270,6 +5325,42 @@ function registerSendCommand(program2) {
|
|
|
5270
5325
|
}
|
|
5271
5326
|
throw e;
|
|
5272
5327
|
}
|
|
5328
|
+
const baseUrl = flags.local ? LOCAL_BASE : API_BASE;
|
|
5329
|
+
const client = new Peppol({ apiKey: auth.apiKey, baseUrl });
|
|
5330
|
+
if (auth.environment === "sandbox") {
|
|
5331
|
+
let profile;
|
|
5332
|
+
try {
|
|
5333
|
+
profile = (await client.identity.get()).sandboxFirstSend;
|
|
5334
|
+
} catch (e) {
|
|
5335
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
5336
|
+
process.stderr.write(`${pc6.red("\u2722")} Could not verify the sandbox sender profile: ${msg}
|
|
5337
|
+
`);
|
|
5338
|
+
process.exit(1);
|
|
5339
|
+
return;
|
|
5340
|
+
}
|
|
5341
|
+
if (!profile || profile.status !== "ready") {
|
|
5342
|
+
const message = profile?.status === "blocked" ? profile.message : "The sandbox first-send profile is unavailable.";
|
|
5343
|
+
process.stderr.write(`${pc6.red("\u2722")} ${message}
|
|
5344
|
+
`);
|
|
5345
|
+
process.exit(1);
|
|
5346
|
+
return;
|
|
5347
|
+
}
|
|
5348
|
+
const taxEntries = [payload.lines, payload.allowances, payload.charges].filter(Array.isArray).flat();
|
|
5349
|
+
const payloadOutsideScope = taxEntries.length > 0 && taxEntries.every((entry) => entry.vatCategory === "O");
|
|
5350
|
+
const profileOutsideScope = profile.taxMode === "outside_scope";
|
|
5351
|
+
if (file) {
|
|
5352
|
+
if (payloadOutsideScope !== profileOutsideScope) {
|
|
5353
|
+
process.stderr.write(
|
|
5354
|
+
`${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.
|
|
5355
|
+
`
|
|
5356
|
+
);
|
|
5357
|
+
process.exit(1);
|
|
5358
|
+
return;
|
|
5359
|
+
}
|
|
5360
|
+
} else {
|
|
5361
|
+
payload.lines = payload.lines.map((line) => ({ ...line, ...profile.line }));
|
|
5362
|
+
}
|
|
5363
|
+
}
|
|
5273
5364
|
if (flags.validate !== false) {
|
|
5274
5365
|
const result2 = runValidation(payload);
|
|
5275
5366
|
if (!result2.valid) {
|
|
@@ -5290,8 +5381,6 @@ function registerSendCommand(program2) {
|
|
|
5290
5381
|
process.exit(0);
|
|
5291
5382
|
}
|
|
5292
5383
|
}
|
|
5293
|
-
const baseUrl = flags.local ? LOCAL_BASE : API_BASE;
|
|
5294
|
-
const client = new Peppol({ apiKey: auth.apiKey, baseUrl });
|
|
5295
5384
|
let result;
|
|
5296
5385
|
try {
|
|
5297
5386
|
result = await client.invoices.send(payload);
|