@getpeppr/cli 0.8.3 → 0.8.4

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 CHANGED
@@ -4,6 +4,17 @@ 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.8.4] — 2026-08-22
8
+
9
+ `getpeppr init` now scaffolds an invoice that a fresh sandbox account can send:
10
+ the SPF Economie test receiver plus out-of-scope `O/0` lines. The previous
11
+ template targeted the correct receiver but still carried VAT-bearing and
12
+ reverse-charge lines, so a new sender with no VAT identifier received 422.
13
+
14
+ Every generated `O` line includes the builder-only exemption reason required by
15
+ the SDK's local validator. The quick `getpeppr send --to …` payload remains on
16
+ the same contract. This release bundles SDK 4.7.0.
17
+
7
18
  ## [0.8.3] — 2026-08-21
8
19
 
9
20
  Rebundles `@getpeppr/sdk` **4.6.0** (was 4.5.0), so `getpeppr validate` and
package/README.md CHANGED
@@ -241,7 +241,7 @@ Scaffolding (`init`), validation, and conversion run fully offline — no API ke
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 is an integration fixture: a fresh sandbox sender has no VAT identifier, so every line uses category `O` at rate `0`.
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": 21
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": "AE"
283
+ "vatCategory": "O",
284
+ "taxExemptReason": "Integration test"
282
285
  }
283
286
  ],
284
287
  "paymentTerms": "Net 30 days",
package/dist/index.js CHANGED
@@ -130,7 +130,7 @@ Validating: ${pc.bold(filename)}
130
130
  return lines.join("\n");
131
131
  }
132
132
 
133
- // ../sdk/dist/core/canonical-schemes.js
133
+ // ../../../getpeppr/packages/sdk/dist/core/canonical-schemes.js
134
134
  var ALIAS_TO_EAS = /* @__PURE__ */ new Map([
135
135
  ["AD:VAT", "9922"],
136
136
  ["AE:TIN", "0235"],
@@ -356,7 +356,7 @@ function countryForScheme(scheme) {
356
356
  var CANONICAL_SCHEME_COUNT = ALIAS_TO_EAS.size;
357
357
  var SCHEME_COUNTRY_COUNT = EAS_TO_COUNTRY.size;
358
358
 
359
- // ../sdk/dist/core/peppol-id.js
359
+ // ../../../getpeppr/packages/sdk/dist/core/peppol-id.js
360
360
  function isWellFormedPeppolId(peppolId) {
361
361
  if (!peppolId.includes(":"))
362
362
  return false;
@@ -382,7 +382,7 @@ function parsePeppolId(peppolId) {
382
382
  };
383
383
  }
384
384
 
385
- // ../sdk/dist/core/iso6523-icd-codes.js
385
+ // ../../../getpeppr/packages/sdk/dist/core/iso6523-icd-codes.js
386
386
  var ICD_CODES = /* @__PURE__ */ new Set([
387
387
  "0002",
388
388
  "0003",
@@ -642,7 +642,7 @@ function canCarryPartyIdentification(scheme, context) {
642
642
  return scheme === "SEPA" && context !== "AccountingCustomerParty";
643
643
  }
644
644
 
645
- // ../sdk/dist/core/ubl-builder.js
645
+ // ../../../getpeppr/packages/sdk/dist/core/ubl-builder.js
646
646
  var UBL_NS = "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2";
647
647
  var CAC_NS = "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2";
648
648
  var CBC_NS = "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2";
@@ -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 != null ? `<cbc:BaseQuantity unitCode="${escapeXml(resolveUnitCode(line.baseQuantityUnit ?? line.unit ?? DEFAULT_UNIT))}">${line.baseQuantity}</cbc: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
  }
@@ -1220,7 +1242,7 @@ function buildCreditNoteXml(input) {
1220
1242
  </CreditNote>`;
1221
1243
  }
1222
1244
 
1223
- // ../sdk/dist/core/checksums/luhn.js
1245
+ // ../../../getpeppr/packages/sdk/dist/core/checksums/luhn.js
1224
1246
  function isValidLuhn(input) {
1225
1247
  if (typeof input !== "string")
1226
1248
  return false;
@@ -1261,7 +1283,7 @@ function isValidLuhnSiret(input) {
1261
1283
  return false;
1262
1284
  }
1263
1285
 
1264
- // ../sdk/dist/core/country-rules.js
1286
+ // ../../../getpeppr/packages/sdk/dist/core/country-rules.js
1265
1287
  function warn(field, message, ruleId) {
1266
1288
  return { field, message, ruleId };
1267
1289
  }
@@ -1402,7 +1424,7 @@ function validateCountryRules(input) {
1402
1424
  return { errors, warnings };
1403
1425
  }
1404
1426
 
1405
- // ../sdk/dist/core/code-lists.js
1427
+ // ../../../getpeppr/packages/sdk/dist/core/code-lists.js
1406
1428
  var CURRENCIES = /* @__PURE__ */ new Map([
1407
1429
  ["EUR", { code: "EUR", name: "Euro", minorUnits: 2 }],
1408
1430
  ["USD", { code: "USD", name: "US Dollar", minorUnits: 2 }],
@@ -1527,7 +1549,7 @@ function getAllUnits() {
1527
1549
  return Array.from(UNIT_CODES.entries()).map(([code, name]) => ({ code, name })).sort((a, b) => a.code.localeCompare(b.code));
1528
1550
  }
1529
1551
 
1530
- // ../sdk/dist/core/validator.js
1552
+ // ../../../getpeppr/packages/sdk/dist/core/validator.js
1531
1553
  function error(field, message, ruleId, suggestion) {
1532
1554
  return { field, message, ruleId, suggestion };
1533
1555
  }
@@ -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) {
@@ -1754,7 +1781,7 @@ function validateInvoice(input) {
1754
1781
  };
1755
1782
  }
1756
1783
 
1757
- // ../sdk/dist/core/schematron.js
1784
+ // ../../../getpeppr/packages/sdk/dist/core/schematron.js
1758
1785
  function violation(ruleId, severity, message, field) {
1759
1786
  return { ruleId, severity, message, field };
1760
1787
  }
@@ -2279,7 +2306,7 @@ var SDK_SCHEMATRON_RULE_IDS = [
2279
2306
  "PEPPOL-EN16931-R080"
2280
2307
  ];
2281
2308
 
2282
- // ../sdk/dist/core/status-precedence.js
2309
+ // ../../../getpeppr/packages/sdk/dist/core/status-precedence.js
2283
2310
  var STATUS_PRECEDENCE = [
2284
2311
  { status: "failed", family: "terminal-failure" },
2285
2312
  { status: "rejected", family: "terminal-failure" },
@@ -2303,10 +2330,10 @@ function statusFamily(status) {
2303
2330
  }
2304
2331
  var TERMINAL_FAILURE_STATUSES = STATUS_PRECEDENCE.filter((e) => e.family === "terminal-failure").map((e) => e.status);
2305
2332
 
2306
- // ../sdk/dist/version.js
2307
- var SDK_VERSION = "4.6.0";
2333
+ // ../../../getpeppr/packages/sdk/dist/version.js
2334
+ var SDK_VERSION = "4.7.0";
2308
2335
 
2309
- // ../sdk/dist/core/client.js
2336
+ // ../../../getpeppr/packages/sdk/dist/core/client.js
2310
2337
  function findHeaderCaseInsensitive(headers, name) {
2311
2338
  if (!headers)
2312
2339
  return void 0;
@@ -2537,9 +2564,8 @@ var GetpepprAdapter = class {
2537
2564
  clearTimeout(timeoutId);
2538
2565
  }
2539
2566
  }
2540
- // NOTE: sendInvoice and createInvoice hit the same SDK endpoint (POST /invoices).
2541
- // The gateway (Tasks 9-10) differentiates them: sendInvoice wraps with
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) {
@@ -3521,16 +3547,12 @@ var InvoiceOperations = class {
3521
3547
  this.adapter = adapter;
3522
3548
  }
3523
3549
  /**
3524
- * Create a draft invoice without sending it.
3525
- * Validates input client-side, then creates the invoice via the gateway.
3526
- * Use `sendById()` to send the draft when ready.
3550
+ * Request draft creation from the gateway.
3527
3551
  *
3528
- * @example
3529
- * ```ts
3530
- * const draft = await peppol.invoices.create({ number: "INV-001", to, lines });
3531
- * // Later, when ready:
3532
- * await peppol.invoices.sendById(draft.id);
3533
- * ```
3552
+ * @deprecated The current Storecove-backed gateway does not support drafts
3553
+ * and returns 422 `drafts_not_supported`. Submit the final document with
3554
+ * `invoices.send()` instead.
3555
+ * @throws {PeppolApiError} 422 with code `drafts_not_supported`
3534
3556
  */
3535
3557
  async create(input, options) {
3536
3558
  const validation = validateInvoice(input);
@@ -3545,14 +3567,11 @@ ${validation.errors.map((e) => ` - ${e.field}: ${e.message}${e.suggestion ? ` (
3545
3567
  return result;
3546
3568
  }
3547
3569
  /**
3548
- * Send an existing draft invoice by ID.
3549
- * The invoice must have been previously created with `create()`.
3570
+ * Request sending of an existing draft invoice by ID.
3550
3571
  *
3551
- * @example
3552
- * ```ts
3553
- * await peppol.invoices.sendById("inv-1");
3554
- * ```
3555
- * @throws {PeppolApiError} 501 if the gateway provider does not support draft sending
3572
+ * @deprecated The current Storecove-backed gateway has no draft lifecycle and
3573
+ * always returns 501. Submit the final document with `invoices.send()`.
3574
+ * @throws {PeppolApiError} 501 with the current gateway provider
3556
3575
  */
3557
3576
  async sendById(id) {
3558
3577
  return this.adapter.sendInvoiceById(id);
@@ -3723,52 +3742,39 @@ ${validation.errors.map((e) => ` - ${e.field}: ${e.message}${e.suggestion ? ` (
3723
3742
  return this.adapter.importInvoice(options);
3724
3743
  }
3725
3744
  /**
3726
- * Acknowledge a received invoice.
3745
+ * Request acknowledgement of a received invoice.
3727
3746
  *
3728
- * @example
3729
- * ```ts
3730
- * const result = await peppol.invoices.acknowledge("inv-123");
3731
- * console.log(result.status); // "accepted"
3732
- * ```
3733
- * @throws {PeppolApiError} 501 if the gateway provider does not support acknowledgement
3747
+ * @deprecated The current Storecove-backed gateway does not support
3748
+ * acknowledgement and always returns 501.
3749
+ * @throws {PeppolApiError} 501 with the current gateway provider
3734
3750
  */
3735
3751
  async acknowledge(id) {
3736
3752
  return this.adapter.acknowledgeInvoice(id);
3737
3753
  }
3738
3754
  /**
3739
- * Update an existing invoice (draft only).
3740
- * Only include the fields you want to change — partial updates are supported.
3755
+ * Request an update to an existing invoice.
3741
3756
  *
3742
- * @example
3743
- * ```ts
3744
- * const updated = await peppol.invoices.update("inv-123", {
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
3757
+ * @deprecated Storecove documents are immutable after submission. The
3758
+ * current gateway always returns 501; issue a credit note instead.
3759
+ * @throws {PeppolApiError} 501 with the current gateway provider
3753
3760
  */
3754
3761
  async update(id, input) {
3755
3762
  return this.adapter.updateInvoice(id, input);
3756
3763
  }
3757
3764
  /**
3758
- * Delete an invoice.
3765
+ * Request deletion of an invoice.
3759
3766
  *
3760
- * @example
3761
- * ```ts
3762
- * const deleted = await peppol.invoices.delete("inv-123");
3763
- * ```
3764
- * @throws {PeppolApiError} 501 if the gateway provider does not support invoice deletion
3767
+ * @deprecated The current Storecove-backed gateway does not support invoice
3768
+ * deletion and always returns 501.
3769
+ * @throws {PeppolApiError} 501 with the current gateway provider
3765
3770
  */
3766
3771
  async delete(id) {
3767
3772
  return this.adapter.deleteInvoice(id);
3768
3773
  }
3769
3774
  /**
3770
- * Transition an invoice to a new state.
3771
- * The gateway validates the state machine invalid transitions return an error.
3775
+ * Report a French CTC invoice as paid.
3776
+ * Other state transitions are retained for API compatibility but the current
3777
+ * Storecove-backed gateway returns 501 for them.
3772
3778
  *
3773
3779
  * `"paid"` on a French CTC invoice reports the payment collection
3774
3780
  * (« signalement d'encaissement ») to the tax authority via the gateway —
@@ -4448,14 +4454,17 @@ var INVOICE_TEMPLATE = {
4448
4454
  description: "Conseil en transformation num\xE9rique",
4449
4455
  quantity: 10,
4450
4456
  unitPrice: 950,
4451
- vatRate: 21
4457
+ vatRate: 0,
4458
+ vatCategory: "O",
4459
+ taxExemptReason: "Integration test"
4452
4460
  },
4453
4461
  {
4454
4462
  description: "Software license \u2014 annual subscription",
4455
4463
  quantity: 1,
4456
4464
  unitPrice: 2400,
4457
4465
  vatRate: 0,
4458
- vatCategory: "AE"
4466
+ vatCategory: "O",
4467
+ taxExemptReason: "Integration test"
4459
4468
  }
4460
4469
  ],
4461
4470
  paymentTerms: "Net 30 days",
@@ -4491,7 +4500,9 @@ var CREDIT_NOTE_TEMPLATE = {
4491
4500
  description: "Avoir partiel \u2014 Conseil en transformation num\xE9rique",
4492
4501
  quantity: 2,
4493
4502
  unitPrice: 950,
4494
- vatRate: 21
4503
+ vatRate: 0,
4504
+ vatCategory: "O",
4505
+ taxExemptReason: "Integration test"
4495
4506
  }
4496
4507
  ],
4497
4508
  note: "Avoir pour prestations non r\xE9alis\xE9es \u2014 r\xE9f. INV-2026-001"
@@ -4525,9 +4536,9 @@ function registerInitCommand(program2) {
4525
4536
  3. Convert to XML: getpeppr convert ${filename}
4526
4537
  4. Send: getpeppr send ${filename}
4527
4538
 
4528
- ${pc2.dim("Sandbox note:")} this template includes VAT. To send it on a sandbox
4529
- account, first register your VAT on the Peppol identity page, or set each
4530
- line's "vatCategory" to "O" (outside the scope of VAT) for a no-VAT test send.
4539
+ ${pc2.dim("Sandbox note:")} ready for a fresh sandbox account. This template
4540
+ targets the test receiver and uses O/0 tax lines. Replace the fixture tax
4541
+ treatment with the real category after registering the sender's VAT details.
4531
4542
  `);
4532
4543
  process.exit(0);
4533
4544
  }
@@ -5090,11 +5101,12 @@ function buildDefaultSendPayload(overrides = {}) {
5090
5101
  quantity: 1,
5091
5102
  unitPrice: amount,
5092
5103
  vatRate: 0,
5093
- // vatCategory "O" = "Services outside scope of tax" (UBL 2.1 / EN 16931).
5094
- // Public entities (SPF Economie) are VAT-exempt. The SDK pre-validator
5095
- // requires the builder-only reason; the send transport strips it so
5096
- // Storecove can still derive its own provider-specific text.
5097
- // If sandbox returns 422 on this combination, fallback is vatRate: 21 + vatCategory: "S".
5104
+ // vatCategory "O" = outside the scope of VAT (UBL 2.1 / EN 16931).
5105
+ // A fresh sandbox sender has no VAT identifier, so this integration
5106
+ // fixture deliberately uses O/0. The SDK pre-validator requires the
5107
+ // builder-only reason; the send transport strips it so Storecove can
5108
+ // derive its own provider-specific text.
5109
+ // Do not fall back to VAT-bearing category "S": a fresh sandbox sender has no VAT number.
5098
5110
  vatCategory: "O",
5099
5111
  taxExemptReason: "Not subject to VAT"
5100
5112
  }