@getpeppr/cli 0.8.4 → 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 CHANGED
@@ -4,12 +4,24 @@ 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
+
7
19
  ## [0.8.4] — 2026-08-22
8
20
 
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.
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.
13
25
 
14
26
  Every generated `O` line includes the builder-only exemption reason required by
15
27
  the SDK's local validator. The quick `getpeppr send --to …` payload remains on
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. The generated file is an integration fixture: a fresh sandbox sender has no VAT identifier, so every line uses category `O` at rate `0`.
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
  {
package/dist/index.js CHANGED
@@ -130,7 +130,7 @@ Validating: ${pc.bold(filename)}
130
130
  return lines.join("\n");
131
131
  }
132
132
 
133
- // ../../../getpeppr/packages/sdk/dist/core/canonical-schemes.js
133
+ // ../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
- // ../../../getpeppr/packages/sdk/dist/core/peppol-id.js
359
+ // ../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
- // ../../../getpeppr/packages/sdk/dist/core/iso6523-icd-codes.js
385
+ // ../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
- // ../../../getpeppr/packages/sdk/dist/core/ubl-builder.js
645
+ // ../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";
@@ -1242,7 +1242,7 @@ function buildCreditNoteXml(input) {
1242
1242
  </CreditNote>`;
1243
1243
  }
1244
1244
 
1245
- // ../../../getpeppr/packages/sdk/dist/core/checksums/luhn.js
1245
+ // ../sdk/dist/core/checksums/luhn.js
1246
1246
  function isValidLuhn(input) {
1247
1247
  if (typeof input !== "string")
1248
1248
  return false;
@@ -1283,7 +1283,7 @@ function isValidLuhnSiret(input) {
1283
1283
  return false;
1284
1284
  }
1285
1285
 
1286
- // ../../../getpeppr/packages/sdk/dist/core/country-rules.js
1286
+ // ../sdk/dist/core/country-rules.js
1287
1287
  function warn(field, message, ruleId) {
1288
1288
  return { field, message, ruleId };
1289
1289
  }
@@ -1424,7 +1424,7 @@ function validateCountryRules(input) {
1424
1424
  return { errors, warnings };
1425
1425
  }
1426
1426
 
1427
- // ../../../getpeppr/packages/sdk/dist/core/code-lists.js
1427
+ // ../sdk/dist/core/code-lists.js
1428
1428
  var CURRENCIES = /* @__PURE__ */ new Map([
1429
1429
  ["EUR", { code: "EUR", name: "Euro", minorUnits: 2 }],
1430
1430
  ["USD", { code: "USD", name: "US Dollar", minorUnits: 2 }],
@@ -1549,7 +1549,7 @@ function getAllUnits() {
1549
1549
  return Array.from(UNIT_CODES.entries()).map(([code, name]) => ({ code, name })).sort((a, b) => a.code.localeCompare(b.code));
1550
1550
  }
1551
1551
 
1552
- // ../../../getpeppr/packages/sdk/dist/core/validator.js
1552
+ // ../sdk/dist/core/validator.js
1553
1553
  function error(field, message, ruleId, suggestion) {
1554
1554
  return { field, message, ruleId, suggestion };
1555
1555
  }
@@ -1781,7 +1781,7 @@ function validateInvoice(input) {
1781
1781
  };
1782
1782
  }
1783
1783
 
1784
- // ../../../getpeppr/packages/sdk/dist/core/schematron.js
1784
+ // ../sdk/dist/core/schematron.js
1785
1785
  function violation(ruleId, severity, message, field) {
1786
1786
  return { ruleId, severity, message, field };
1787
1787
  }
@@ -2306,7 +2306,7 @@ var SDK_SCHEMATRON_RULE_IDS = [
2306
2306
  "PEPPOL-EN16931-R080"
2307
2307
  ];
2308
2308
 
2309
- // ../../../getpeppr/packages/sdk/dist/core/status-precedence.js
2309
+ // ../sdk/dist/core/status-precedence.js
2310
2310
  var STATUS_PRECEDENCE = [
2311
2311
  { status: "failed", family: "terminal-failure" },
2312
2312
  { status: "rejected", family: "terminal-failure" },
@@ -2330,10 +2330,10 @@ function statusFamily(status) {
2330
2330
  }
2331
2331
  var TERMINAL_FAILURE_STATUSES = STATUS_PRECEDENCE.filter((e) => e.family === "terminal-failure").map((e) => e.status);
2332
2332
 
2333
- // ../../../getpeppr/packages/sdk/dist/version.js
2334
- var SDK_VERSION = "4.7.0";
2333
+ // ../sdk/dist/version.js
2334
+ var SDK_VERSION = "4.8.0";
2335
2335
 
2336
- // ../../../getpeppr/packages/sdk/dist/core/client.js
2336
+ // ../sdk/dist/core/client.js
2337
2337
  function findHeaderCaseInsensitive(headers, name) {
2338
2338
  if (!headers)
2339
2339
  return void 0;
@@ -2604,6 +2604,8 @@ var GetpepprAdapter = class {
2604
2604
  params.set("limit", String(options.limit));
2605
2605
  if (options?.offset != null)
2606
2606
  params.set("offset", String(options.offset));
2607
+ if (options?.number != null)
2608
+ params.set("number", options.number);
2607
2609
  if (options?.includeLines)
2608
2610
  params.set("include", "lines");
2609
2611
  const query = params.toString() ? `?${params.toString()}` : "";
@@ -2737,7 +2739,9 @@ var GetpepprAdapter = class {
2737
2739
  params.set("limit", String(options.limit));
2738
2740
  if (options?.offset != null)
2739
2741
  params.set("offset", String(options.offset));
2740
- if (options?.invoiceId)
2742
+ if (options?.documentId != null)
2743
+ params.set("documentId", options.documentId);
2744
+ if (options?.invoiceId != null)
2741
2745
  params.set("invoiceId", options.invoiceId);
2742
2746
  if (options?.dateFrom)
2743
2747
  params.set("dateFrom", options.dateFrom);
@@ -3244,9 +3248,49 @@ function parseAccountIdentity(raw) {
3244
3248
  return {
3245
3249
  environment,
3246
3250
  legalEntity: parseAccountIdentityLegalEntity(readOwn(result, "legalEntity"), result),
3247
- identifiers
3251
+ identifiers,
3252
+ sandboxFirstSend: parseSandboxFirstSendProfile(readOwn(result, "sandboxFirstSend"), result)
3248
3253
  };
3249
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
+ }
3250
3294
  function parseAccountIdentityLegalEntity(raw, body) {
3251
3295
  if (raw === null)
3252
3296
  return null;
@@ -3976,8 +4020,8 @@ var EventOperations = class {
3976
4020
  * const result = await peppol.events.list({ limit: 10 });
3977
4021
  * console.log(result.data, result.meta);
3978
4022
  *
3979
- * // Filter by invoice
3980
- * const invoiceEvents = await peppol.events.list({ invoiceId: "inv-123" });
4023
+ * // Filter by provider document ID or getpeppr submission ID
4024
+ * const invoiceEvents = await peppol.events.list({ documentId: "inv-123" });
3981
4025
  * ```
3982
4026
  */
3983
4027
  async list(options) {
@@ -3988,7 +4032,7 @@ var EventOperations = class {
3988
4032
  *
3989
4033
  * @example
3990
4034
  * ```ts
3991
- * for await (const event of peppol.events.listAll({ invoiceId: "inv-123" })) {
4035
+ * for await (const event of peppol.events.listAll({ documentId: "inv-123" })) {
3992
4036
  * console.log(event.name, event.createdAt);
3993
4037
  * }
3994
4038
  * ```
@@ -4536,9 +4580,9 @@ function registerInitCommand(program2) {
4536
4580
  3. Convert to XML: getpeppr convert ${filename}
4537
4581
  4. Send: getpeppr send ${filename}
4538
4582
 
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.
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.
4542
4586
  `);
4543
4587
  process.exit(0);
4544
4588
  }
@@ -5102,11 +5146,10 @@ function buildDefaultSendPayload(overrides = {}) {
5102
5146
  unitPrice: amount,
5103
5147
  vatRate: 0,
5104
5148
  // 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.
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.
5110
5153
  vatCategory: "O",
5111
5154
  taxExemptReason: "Not subject to VAT"
5112
5155
  }
@@ -5282,6 +5325,42 @@ function registerSendCommand(program2) {
5282
5325
  }
5283
5326
  throw e;
5284
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
+ }
5285
5364
  if (flags.validate !== false) {
5286
5365
  const result2 = runValidation(payload);
5287
5366
  if (!result2.valid) {
@@ -5302,8 +5381,6 @@ function registerSendCommand(program2) {
5302
5381
  process.exit(0);
5303
5382
  }
5304
5383
  }
5305
- const baseUrl = flags.local ? LOCAL_BASE : API_BASE;
5306
- const client = new Peppol({ apiKey: auth.apiKey, baseUrl });
5307
5384
  let result;
5308
5385
  try {
5309
5386
  result = await client.invoices.send(payload);