@getpeppr/cli 0.11.1 → 0.12.1

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,41 @@ 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.12.1] — 2026-09-06
8
+
9
+ Rebundles `@getpeppr/sdk` 5.3.0 (GPR-1280).
10
+
11
+ - 0.12.0 was published with a stale SDK 5.2.0 bundle: its `validate` did not
12
+ enforce the billing-profile rules and its `structure` rule IDs predated
13
+ GPR-1267. Version 0.12.0 is deprecated on the registry; 0.12.1 carries the
14
+ intended bundle and the changes below as originally described.
15
+
16
+ ## [0.12.0] — 2026-09-06 (deprecated — stale SDK bundle, superseded by 0.12.1)
17
+
18
+ Rebundles `@getpeppr/sdk` 5.3.0 (GPR-1280).
19
+
20
+ - `getpeppr validate` and `getpeppr convert --validate` now enforce the three
21
+ Peppol billing-profile rules on `invoiceTypeCode` (`P0100`, `P0101`,
22
+ `P0112`) because they validate or build UBL locally (GPR-884, GPR-1273).
23
+ Documents outside the profile now fail these commands where they previously
24
+ passed — pipelines gating on the exit code change verdict, hence the minor.
25
+ - The `structure` section rule IDs now designate the rules they actually
26
+ verify (GPR-1267): buyer name `BR-06` → `BR-07`, buyer street/city/postal
27
+ code → `GETPEPPR-BUYER-ADDRESS`, line VAT rate `BR-CO-17` →
28
+ `GETPEPPR-LINE-VAT-RATE`, `taxCurrencyRate` `BR-53` →
29
+ `GETPEPPR-TAX-CURRENCY-RATE`, the buyer-reference warning `BR-10` →
30
+ `PEPPOL-EN16931-R003`, and the due-date warning carries no id.
31
+ - The `structure` section no longer repeats the country advisory warnings
32
+ that the `countryRules` section already reports: every warning is
33
+ displayed and counted once, and `totalWarnings` drops accordingly
34
+ (GPR-1267). Scripts that relied on the duplicated entries in
35
+ `structure.warnings` must read them from `countryRules.warnings`.
36
+ - `getpeppr send` now uses an explicit JSON-send preflight aligned with
37
+ `POST /v1/invoices` (GPR-1285). Because that route currently ignores
38
+ `invoiceTypeCode`, profile rules `P0100`, `P0101`, and `P0112` no longer
39
+ block that inert field before sending. Structured `BR-CL-01` validation
40
+ and all other applicable offline and country checks remain active.
41
+
7
42
  ## [0.11.1] — 2026-09-04
8
43
 
9
44
  Rebundles `@getpeppr/sdk` 5.2.0 (GPR-1084).
package/README.md CHANGED
@@ -126,6 +126,14 @@ API key resolution order: `--key` flag → `GETPEPPR_API_KEY` env var → stored
126
126
 
127
127
  Send an invoice through the getpeppr API to the Peppol network. Targets sandbox by default; pass `--prod` for production.
128
128
 
129
+ `getpeppr send` runs a JSON preflight aligned with `POST /v1/invoices`. That
130
+ endpoint currently ignores `invoiceTypeCode` and lets the provider generate the
131
+ UBL, so the send preflight does not apply `P0100`, `P0101`, or `P0112` to that
132
+ inert field. Structure checks including `BR-CL-01`, plus every other applicable
133
+ offline and country check, remain active. Use `getpeppr validate` or
134
+ `getpeppr convert --validate` for full local-UBL semantics; both retain those
135
+ profile checks because they validate or build the UBL locally.
136
+
129
137
  ```bash
130
138
  # Send a JSON file
131
139
  getpeppr send invoice.json
@@ -154,7 +162,7 @@ getpeppr send invoice.json --prod
154
162
  | `--attachment` | Attach the bundled test PDF |
155
163
  | `--watch` | Poll status until terminal (60s timeout) |
156
164
  | `-y, --yes` | Skip the `--prod` confirmation prompt |
157
- | `--no-validate` | Skip local pre-validation |
165
+ | `--no-validate` | Skip the JSON send preflight |
158
166
  | `--json` | Machine-readable JSON output |
159
167
  | `--quiet` | Exit code only, no output |
160
168
 
@@ -232,10 +240,11 @@ GETPEPPR_API_KEY=$SANDBOX_KEY getpeppr send invoice.json --json
232
240
 
233
241
  ## What it validates
234
242
 
235
- The CLI runs three validation engines from the [@getpeppr/sdk](https://www.npmjs.com/package/@getpeppr/sdk):
243
+ `getpeppr validate` and `getpeppr convert --validate` run three validation
244
+ engines from the [@getpeppr/sdk](https://www.npmjs.com/package/@getpeppr/sdk):
236
245
 
237
246
  1. **Structure** — Required fields, type checks, format validation
238
- 2. **Offline Checks** — 37 registered partial pre-flight checks: exact network-rule equivalents plus explicit `GETPEPPR-*` diagnostics. The legacy provider-capability diagnostic `unsupported_vat_category` can also appear, but is outside that count. This is not a Peppol conformance verdict.
247
+ 2. **Offline Checks** — 40 registered partial pre-flight checks: exact network-rule equivalents plus explicit `GETPEPPR-*` diagnostics. The legacy provider-capability diagnostic `unsupported_vat_category` can also appear, but is outside that count. This is not a Peppol conformance verdict.
239
248
  3. **Country Rules** — Belgium (BE), France (FR), Italy (IT), Netherlands (NL), Germany (DE)
240
249
 
241
250
  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.
@@ -294,7 +303,7 @@ This is the exact template generated by `getpeppr init`. See the [full type refe
294
303
 
295
304
  ## Ready to send?
296
305
 
297
- Once your invoice validates, send it to the Peppol network — straight from the CLI:
306
+ When your invoice JSON is ready, send it to the Peppol network — straight from the CLI:
298
307
 
299
308
  ```bash
300
309
  getpeppr login # store your sandbox key once
package/dist/index.js CHANGED
@@ -1698,16 +1698,19 @@ function assertString(value, fieldPath, errors) {
1698
1698
  var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
1699
1699
  var ALLOWANCE_CHARGE_CONTRACT_RULE = "GETPEPPR-ALLOWANCE-CHARGE-AMOUNT";
1700
1700
  var DERIVED_AMOUNT_CONTRACT_RULE2 = "GETPEPPR-DERIVED-AMOUNT";
1701
+ var BUYER_ADDRESS_DELIVERY_RULE = "GETPEPPR-BUYER-ADDRESS";
1702
+ var LINE_VAT_RATE_RULE = "GETPEPPR-LINE-VAT-RATE";
1703
+ var TAX_CURRENCY_RATE_RULE = "GETPEPPR-TAX-CURRENCY-RATE";
1701
1704
  function survivesCents2(value) {
1702
1705
  return Number.isFinite(value) && Number.isFinite(value * 100);
1703
1706
  }
1704
1707
  function validateParty(party, path) {
1705
1708
  const errors = [];
1706
1709
  if (party.name === void 0 || party.name === null || party.name === "") {
1707
- errors.push(error(`${path}.name`, "Business name is required", "BR-06"));
1710
+ errors.push(error(`${path}.name`, "Business name is required", "BR-07"));
1708
1711
  } else if (!assertString(party.name, `${path}.name`, errors)) {
1709
1712
  } else if (!party.name.trim()) {
1710
- errors.push(error(`${path}.name`, "Business name is required", "BR-06"));
1713
+ errors.push(error(`${path}.name`, "Business name is required", "BR-07"));
1711
1714
  }
1712
1715
  if (party.peppolId === void 0 || party.peppolId === null || party.peppolId === "") {
1713
1716
  errors.push(error(`${path}.peppolId`, "Peppol participant ID is required", void 0, 'Format: "scheme:id", e.g. "0208:0685660237" for Belgian companies'));
@@ -1726,22 +1729,22 @@ function validateParty(party, path) {
1726
1729
  function validateBuyerAddress(party, path) {
1727
1730
  const errors = [];
1728
1731
  if (party.street === void 0 || party.street === null || party.street === "") {
1729
- errors.push(error(`${path}.street`, "Street address is required for the buyer", "BR-50", 'e.g. "123 Business Street"'));
1732
+ errors.push(error(`${path}.street`, "Street address is required for the buyer", BUYER_ADDRESS_DELIVERY_RULE, 'e.g. "123 Business Street"'));
1730
1733
  } else if (!assertString(party.street, `${path}.street`, errors)) {
1731
1734
  } else if (!party.street.trim()) {
1732
- errors.push(error(`${path}.street`, "Street address is required for the buyer", "BR-50", 'e.g. "123 Business Street"'));
1735
+ errors.push(error(`${path}.street`, "Street address is required for the buyer", BUYER_ADDRESS_DELIVERY_RULE, 'e.g. "123 Business Street"'));
1733
1736
  }
1734
1737
  if (party.city === void 0 || party.city === null || party.city === "") {
1735
- errors.push(error(`${path}.city`, "City is required for the buyer", "BR-51", 'e.g. "Brussels"'));
1738
+ errors.push(error(`${path}.city`, "City is required for the buyer", BUYER_ADDRESS_DELIVERY_RULE, 'e.g. "Brussels"'));
1736
1739
  } else if (!assertString(party.city, `${path}.city`, errors)) {
1737
1740
  } else if (!party.city.trim()) {
1738
- errors.push(error(`${path}.city`, "City is required for the buyer", "BR-51", 'e.g. "Brussels"'));
1741
+ errors.push(error(`${path}.city`, "City is required for the buyer", BUYER_ADDRESS_DELIVERY_RULE, 'e.g. "Brussels"'));
1739
1742
  }
1740
1743
  if (party.postalCode === void 0 || party.postalCode === null || party.postalCode === "") {
1741
- errors.push(error(`${path}.postalCode`, "Postal code is required for the buyer", "BR-53", 'e.g. "1000"'));
1744
+ errors.push(error(`${path}.postalCode`, "Postal code is required for the buyer", BUYER_ADDRESS_DELIVERY_RULE, 'e.g. "1000"'));
1742
1745
  } else if (!assertString(party.postalCode, `${path}.postalCode`, errors)) {
1743
1746
  } else if (!party.postalCode.trim()) {
1744
- errors.push(error(`${path}.postalCode`, "Postal code is required for the buyer", "BR-53", 'e.g. "1000"'));
1747
+ errors.push(error(`${path}.postalCode`, "Postal code is required for the buyer", BUYER_ADDRESS_DELIVERY_RULE, 'e.g. "1000"'));
1745
1748
  }
1746
1749
  return errors;
1747
1750
  }
@@ -1770,7 +1773,7 @@ function validateLine(line, index, isCreditNote = false) {
1770
1773
  }
1771
1774
  }
1772
1775
  if (line.vatRate === void 0 || line.vatRate === null) {
1773
- errors.push(error(`${path}.vatRate`, "VAT rate is required", "BR-CO-17"));
1776
+ errors.push(error(`${path}.vatRate`, "VAT rate is required", LINE_VAT_RATE_RULE));
1774
1777
  } else if (line.vatRate < 0 || line.vatRate > 100) {
1775
1778
  errors.push(error(`${path}.vatRate`, `VAT rate must be between 0 and 100, got ${line.vatRate}`, void 0, "Use 0 for zero-rated, 21 for standard Belgian VAT, etc."));
1776
1779
  }
@@ -1945,10 +1948,25 @@ function validateInvoice(input) {
1945
1948
  }
1946
1949
  }
1947
1950
  if (!input.buyerReference && !input.orderReference) {
1948
- warnings.push(warning("buyerReference", "Either buyerReference or orderReference is required by Peppol BIS 3.0 (BT-10).", "BR-10"));
1951
+ warnings.push(warning(
1952
+ "buyerReference",
1953
+ "Either buyerReference or orderReference is required by Peppol BIS 3.0 (BT-10).",
1954
+ // GPR-1267 — PEPPOL-EN16931-R003 IS this rule: "A buyer reference or
1955
+ // purchase order reference MUST be provided." BR-10 is the buyer
1956
+ // postal address.
1957
+ "PEPPOL-EN16931-R003"
1958
+ ));
1949
1959
  }
1950
1960
  if (input.taxCurrency && input.taxCurrency !== (input.currency ?? "EUR") && !input.taxCurrencyRate) {
1951
- errors.push(error("taxCurrencyRate", "Tax currency rate is required when taxCurrency differs from document currency", "BR-53", "Set taxCurrencyRate to the exchange rate from document currency to tax currency"));
1961
+ errors.push(error(
1962
+ "taxCurrencyRate",
1963
+ "Tax currency rate is required when taxCurrency differs from document currency",
1964
+ // GPR-1267 — BR-53 mandates the BT-111 total in accounting currency,
1965
+ // which the builder derives itself; the client-supplied rate is an
1966
+ // input-contract requirement, not that rule.
1967
+ TAX_CURRENCY_RATE_RULE,
1968
+ "Set taxCurrencyRate to the exchange rate from document currency to tax currency"
1969
+ ));
1952
1970
  }
1953
1971
  if (input.taxCurrency && input.taxCurrency === (input.currency ?? "EUR")) {
1954
1972
  warnings.push(warning("taxCurrency", "Tax currency is the same as document currency \u2014 TaxCurrencyCode will be omitted"));
@@ -1963,7 +1981,7 @@ function validateInvoice(input) {
1963
1981
  errors.push(error("taxCurrency", `Invalid tax currency code: "${input.taxCurrency}"`, void 0, 'Use ISO 4217 (e.g., "EUR", "USD")'));
1964
1982
  }
1965
1983
  if (!input.dueDate) {
1966
- warnings.push(warning("dueDate", "No due date specified. Recommended for payment terms.", "BR-09"));
1984
+ warnings.push(warning("dueDate", "No due date specified. Recommended for payment terms."));
1967
1985
  }
1968
1986
  if (!input.to?.vatNumber) {
1969
1987
  warnings.push(warning("to.vatNumber", "Buyer VAT number not provided. May be required for B2B."));
@@ -2380,6 +2398,90 @@ var buyerPeppolIdRequired = (input) => {
2380
2398
  }
2381
2399
  return [];
2382
2400
  };
2401
+ var PROFILE_INVOICE_TYPE_CODES = /* @__PURE__ */ new Set([
2402
+ "71",
2403
+ "80",
2404
+ "82",
2405
+ "84",
2406
+ "102",
2407
+ "218",
2408
+ "219",
2409
+ "326",
2410
+ "331",
2411
+ "380",
2412
+ "382",
2413
+ "383",
2414
+ "384",
2415
+ "386",
2416
+ "388",
2417
+ "393",
2418
+ "395",
2419
+ "553",
2420
+ "575",
2421
+ "623",
2422
+ "780",
2423
+ "817",
2424
+ "870",
2425
+ "875",
2426
+ "876",
2427
+ "877"
2428
+ ]);
2429
+ var PROFILE_CREDIT_NOTE_TYPE_CODES = /* @__PURE__ */ new Set([
2430
+ "381",
2431
+ "396",
2432
+ "81",
2433
+ "83",
2434
+ "532"
2435
+ ]);
2436
+ function normalizeXmlSpace(value) {
2437
+ return value.replace(/[\u0009\u000A\u000D\u0020]+/g, " ").replace(/^ /, "").replace(/ $/, "");
2438
+ }
2439
+ function normalizedTypeCode(value, fallback) {
2440
+ if (value === void 0 || value === null)
2441
+ return String(fallback);
2442
+ if (typeof value === "number" && Number.isFinite(value))
2443
+ return String(value);
2444
+ if (typeof value === "string" && value.length <= 16)
2445
+ return normalizeXmlSpace(value);
2446
+ return void 0;
2447
+ }
2448
+ function normalizedCountry(value) {
2449
+ return typeof value === "string" ? normalizeXmlSpace(value).toUpperCase() : void 0;
2450
+ }
2451
+ var invoiceTypeCodeForProfile = (input) => {
2452
+ if (input.isCreditNote)
2453
+ return [];
2454
+ const invoiceTypeCode = normalizedTypeCode(input.invoiceTypeCode, 380);
2455
+ if (invoiceTypeCode !== void 0 && PROFILE_INVOICE_TYPE_CODES.has(invoiceTypeCode))
2456
+ return [];
2457
+ return [
2458
+ violation("PEPPOL-EN16931-P0100", "error", "Invoice type code is not allowed by the Peppol billing profile used by the local UBL builder.", "invoiceTypeCode")
2459
+ ];
2460
+ };
2461
+ var creditNoteTypeCodeForProfile = (input) => {
2462
+ if (!input.isCreditNote)
2463
+ return [];
2464
+ const invoiceTypeCode = normalizedTypeCode(input.invoiceTypeCode, 381);
2465
+ if (invoiceTypeCode !== void 0 && PROFILE_CREDIT_NOTE_TYPE_CODES.has(invoiceTypeCode))
2466
+ return [];
2467
+ return [
2468
+ violation("PEPPOL-EN16931-P0101", "error", "Credit note type code is not allowed by the Peppol billing profile used by the local UBL builder.", "invoiceTypeCode")
2469
+ ];
2470
+ };
2471
+ var germanInvoiceTypeCode = (input) => {
2472
+ if (input.isCreditNote)
2473
+ return [];
2474
+ const invoiceTypeCode = normalizedTypeCode(input.invoiceTypeCode, 380);
2475
+ if (invoiceTypeCode !== "326" && invoiceTypeCode !== "384")
2476
+ return [];
2477
+ const sellerCountry = normalizedCountry(input.from?.country);
2478
+ const buyerCountry = normalizedCountry(input.to?.country);
2479
+ if (sellerCountry === "DE" && buyerCountry === "DE")
2480
+ return [];
2481
+ return [
2482
+ violation("PEPPOL-EN16931-P0112", "error", "Invoice type code 326 or 384 is only allowed when both buyer and seller are German organizations.", "invoiceTypeCode")
2483
+ ];
2484
+ };
2383
2485
  var vatCategoryCodes = (input) => {
2384
2486
  const violations = [];
2385
2487
  const sendable = SENDABLE_VAT_CATEGORIES.join(", ");
@@ -2445,6 +2547,9 @@ var ALL_RULES = [
2445
2547
  brIc10,
2446
2548
  // Peppol-specific
2447
2549
  buyerPeppolIdRequired,
2550
+ invoiceTypeCodeForProfile,
2551
+ creditNoteTypeCodeForProfile,
2552
+ germanInvoiceTypeCode,
2448
2553
  vatCategoryCodes,
2449
2554
  unitCodeRecognised
2450
2555
  ];
@@ -2505,6 +2610,9 @@ var SDK_SCHEMATRON_RULE_IDS = [
2505
2610
  "BR-O-10",
2506
2611
  "BR-IC-10",
2507
2612
  "GETPEPPR-BUYER-PEPPOL-ID-REQUIRED",
2613
+ "PEPPOL-EN16931-P0100",
2614
+ "PEPPOL-EN16931-P0101",
2615
+ "PEPPOL-EN16931-P0112",
2508
2616
  "GETPEPPR-UNIT-CODE-RECOGNISED"
2509
2617
  ];
2510
2618
 
@@ -2533,7 +2641,7 @@ function statusFamily(status) {
2533
2641
  var TERMINAL_FAILURE_STATUSES = STATUS_PRECEDENCE.filter((e) => e.family === "terminal-failure").map((e) => e.status);
2534
2642
 
2535
2643
  // ../sdk/dist/version.js
2536
- var SDK_VERSION = "5.2.0";
2644
+ var SDK_VERSION = "5.3.0";
2537
2645
 
2538
2646
  // ../sdk/dist/core/api-result.js
2539
2647
  var API_RESULT_HEADER_NAMES = {
@@ -4424,6 +4532,8 @@ var DirectoryOperations = class {
4424
4532
  }
4425
4533
  /**
4426
4534
  * Search the Peppol Directory for participants.
4535
+ * Pagination is exact and participant-based. Queries wider than the public
4536
+ * Directory's accessible result window are rejected; add narrower criteria.
4427
4537
  *
4428
4538
  * @example
4429
4539
  * ```ts
@@ -4889,14 +4999,32 @@ var TransportOperations = class {
4889
4999
  };
4890
5000
 
4891
5001
  // src/commands/validate.ts
4892
- function runValidation(input) {
5002
+ var SEND_INERT_INVOICE_TYPE_PROFILE_RULES = /* @__PURE__ */ new Set([
5003
+ "PEPPOL-EN16931-P0100",
5004
+ "PEPPOL-EN16931-P0101",
5005
+ "PEPPOL-EN16931-P0112"
5006
+ ]);
5007
+ function isSendRelevantSchematronViolation(violation2) {
5008
+ return !(violation2.field === "invoiceTypeCode" && SEND_INERT_INVOICE_TYPE_PROFILE_RULES.has(violation2.ruleId));
5009
+ }
5010
+ function runMergedValidation(input, schematronFilter) {
4893
5011
  const structure = validateInvoice(input);
4894
- const schematron = validateSchematron(input);
5012
+ const completeSchematron = validateSchematron(input);
5013
+ const schematron = {
5014
+ errors: completeSchematron.errors.filter(schematronFilter),
5015
+ warnings: completeSchematron.warnings
5016
+ };
4895
5017
  const countryRules = validateCountryRules(input);
5018
+ const countryKeys = new Set(
5019
+ countryRules.warnings.map((w) => JSON.stringify([w.field, w.message, w.ruleId]))
5020
+ );
5021
+ const structureWarnings = structure.warnings.filter(
5022
+ (w) => !countryKeys.has(JSON.stringify([w.field, w.message, w.ruleId]))
5023
+ );
4896
5024
  const totalErrors = structure.errors.length + schematron.errors.length + countryRules.errors.length;
4897
- const totalWarnings = structure.warnings.length + schematron.warnings.length + countryRules.warnings.length;
5025
+ const totalWarnings = structureWarnings.length + schematron.warnings.length + countryRules.warnings.length;
4898
5026
  return {
4899
- structure: { errors: structure.errors, warnings: structure.warnings },
5027
+ structure: { errors: structure.errors, warnings: structureWarnings },
4900
5028
  schematron: { errors: schematron.errors, warnings: schematron.warnings },
4901
5029
  countryRules: {
4902
5030
  errors: countryRules.errors,
@@ -4907,6 +5035,12 @@ function runValidation(input) {
4907
5035
  valid: totalErrors === 0
4908
5036
  };
4909
5037
  }
5038
+ function runValidation(input) {
5039
+ return runMergedValidation(input, () => true);
5040
+ }
5041
+ function runSendPreflight(input) {
5042
+ return runMergedValidation(input, isSendRelevantSchematronViolation);
5043
+ }
4910
5044
  function registerValidateCommand(program2) {
4911
5045
  program2.command("validate").description("Validate a Peppol invoice JSON file").argument("<file>", "path to invoice JSON file").option("--json", "output results as JSON").option("--quiet", "exit code only, no output").action(async (file, options) => {
4912
5046
  const input = readAndValidateInvoiceJson(file);
@@ -5754,7 +5888,7 @@ function formatSendResult(result, mode) {
5754
5888
  var API_BASE = "https://api.getpeppr.dev/v1";
5755
5889
  var LOCAL_BASE = "http://localhost:3001/api/v1";
5756
5890
  function registerSendCommand(program2) {
5757
- program2.command("send").description("Send an invoice to the Peppol network via getpeppr API").argument("[file]", "optional path to invoice JSON (mutex with --to/--amount/...)").option("--prod", "target production (live keys + confirmation)").option("--local", "target localhost:3001 dev server").option("--key <key>", "override API key \u2014 for CI/scripted use only; visible in `ps` and shell history. Prefer GETPEPPR_API_KEY env var.").option("--to <peppol-id>", "recipient peppol id (e.g., 9925:BE0314595348)").option("--country <iso>", "recipient ISO 3166-1 alpha-2 country override (e.g., BE)").option("--amount <number>", "line amount in major currency units (decimal allowed)").option("--currency <iso>", "ISO 4217 currency (default EUR)").option("--desc <text>", "line description").option("--attachment", "attach the test PDF").option("--watch", "poll status until a terminal state (60s timeout)").option("-y, --yes", "skip --prod confirmation prompt").option("--no-validate", "skip pre-validation locally").option("--json", "output JSON").option("--quiet", "exit code only, no output").action(async (file, flags) => {
5891
+ program2.command("send").description("Send an invoice to the Peppol network via getpeppr API").argument("[file]", "optional path to invoice JSON (mutex with --to/--amount/...)").option("--prod", "target production (live keys + confirmation)").option("--local", "target localhost:3001 dev server").option("--key <key>", "override API key \u2014 for CI/scripted use only; visible in `ps` and shell history. Prefer GETPEPPR_API_KEY env var.").option("--to <peppol-id>", "recipient peppol id (e.g., 9925:BE0314595348)").option("--country <iso>", "recipient ISO 3166-1 alpha-2 country override (e.g., BE)").option("--amount <number>", "line amount in major currency units (decimal allowed)").option("--currency <iso>", "ISO 4217 currency (default EUR)").option("--desc <text>", "line description").option("--attachment", "attach the test PDF").option("--watch", "poll status until a terminal state (60s timeout)").option("-y, --yes", "skip --prod confirmation prompt").option("--no-validate", "skip the JSON send preflight").option("--json", "output JSON").option("--quiet", "exit code only, no output").action(async (file, flags) => {
5758
5892
  let auth;
5759
5893
  try {
5760
5894
  auth = resolveApiKey({
@@ -5828,7 +5962,7 @@ function registerSendCommand(program2) {
5828
5962
  }
5829
5963
  }
5830
5964
  if (flags.validate !== false) {
5831
- const result2 = runValidation(payload);
5965
+ const result2 = runSendPreflight(payload);
5832
5966
  if (!result2.valid) {
5833
5967
  process.stderr.write(
5834
5968
  `${pc6.red("\u2717")} Pre-validation failed (${result2.totalErrors} errors). Use --no-validate to skip.