@getpeppr/cli 0.10.0 → 0.11.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,35 @@ 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.11.1] — 2026-09-04
8
+
9
+ Rebundles `@getpeppr/sdk` 5.2.0 (GPR-1084).
10
+
11
+ - The `schematron` section of `getpeppr validate` now reports `BR-16` for a
12
+ missing invoice line and uses explicit `GETPEPPR-*` IDs for ten SDK-local
13
+ diagnostics that previously cited different official rules. The separate
14
+ `structure` and `countryRules` sections are unchanged (GPR-1267 tracks their
15
+ reverse audit).
16
+ - Validation predicates, warning/error severities and process exit codes are
17
+ unchanged. The unit warning also stops calling every code outside the SDK's
18
+ small convenience list invalid under Rec20, and the payable warning no longer
19
+ invents `prepaidAmount` as the only possible cause. Scripts that filter the
20
+ `schematron` result by an old rule ID or warning text must update that filter.
21
+ - Human-readable output now labels the section `Offline Checks (partial)` and
22
+ reports `Pre-flight checks passed` or `Pre-flight checks found errors`; it no
23
+ longer claims that the partial pass proves validity or non-compliance. The
24
+ `--json` shape and process exit codes are unchanged.
25
+
26
+ ## [0.11.0] — 2026-09-04
27
+
28
+ Rebundles `@getpeppr/sdk` 5.1.0.
29
+
30
+ - **`getpeppr validate` follows BR-CL-01's two vocabularies** instead of a hand-written list of seven codes. Measured against the published 0.10.0 (which bundles SDK 5.0.0):
31
+ - **44 legal invoice codes stop being falsely refused** — `326` (partial invoice, named by `DE-R-017`), `382` and 42 others now pass. On 0.10.0 each exits 1.
32
+ - **Credit notes carrying `383`, `384`, `386`, `389` or `751` are now refused locally** with a `BR-CL-01` error suggesting legal credit-note codes. On 0.10.0 each passed local validation — every one is fatal on the network, so the document failed later and less clearly.
33
+ - The default is unchanged: omit `invoiceTypeCode` and the document kind decides (`380` invoice, `381` credit note).
34
+ - **The tarball now ships its `LICENSE` file** (the published 0.10.0 does not contain one) and the corrected README.
35
+
7
36
  ## [0.10.0] — 2026-08-28
8
37
 
9
38
  Bundles SDK **5.0.0**. Minor rather than patch because inputs that used to pass
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zero Loop Labs Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @getpeppr/cli
2
2
 
3
- Developer toolkit for Peppol e-invoicing. Scaffold invoices, validate against Peppol BIS 3.0, convert to UBL XML, look up participants in the Peppol Directory, and send invoices to the Peppol network — all from the command line.
3
+ Developer toolkit for Peppol e-invoicing. Scaffold invoices, run offline pre-flight checks, convert to UBL XML, look up participants in the Peppol Directory, and send invoices to the Peppol network — all from the command line.
4
4
 
5
5
  ## Installation
6
6
 
@@ -30,7 +30,7 @@ The generated template includes a Belgian seller, the sandbox test receiver as b
30
30
 
31
31
  ### `getpeppr validate` — Validate an invoice
32
32
 
33
- Run offline validation against Peppol BIS 3.0 business rules:
33
+ Run partial offline pre-flight checks before the network applies the complete Peppol BIS 3.0 rulebooks:
34
34
 
35
35
  ```bash
36
36
  getpeppr validate invoice.json
@@ -44,14 +44,14 @@ Validating: invoice.json
44
44
  ── Structure ──────────────────────────────
45
45
  ✓ No errors
46
46
 
47
- ── Business Rules (Peppol BIS 3.0) ───────
48
- All rules passed
47
+ ── Offline Checks (partial) ───────────────────
48
+ No findings
49
49
 
50
50
  ── Country Rules ────────────────────────────
51
51
  ⚠ No structured communication reference (BE-02)
52
52
 
53
53
  ── Summary ──────────────────────────────────
54
- Invoice is valid (1 warning)
54
+ Pre-flight checks passed (1 warning)
55
55
  ```
56
56
 
57
57
  | Flag | Description |
@@ -103,7 +103,7 @@ getpeppr lookup 0208:0685660237 --json
103
103
 
104
104
  ### `getpeppr login` — Save your API key
105
105
 
106
- Store a getpeppr API key in `~/.config/getpeppr/credentials.json` (mode `0600`). Sandbox and live keys can coexist.
106
+ Store a getpeppr API key in `$XDG_CONFIG_HOME/getpeppr/credentials.json` — `~/.config/getpeppr/credentials.json` by default, `%APPDATA%\getpeppr\credentials.json` on Windows. On Unix-like systems the file is created and checked with mode `0600`; Windows has no POSIX mode. Sandbox and live keys can coexist.
107
107
 
108
108
  ```bash
109
109
  # Interactive (recommended) — prompts for the key with masked input
@@ -149,6 +149,7 @@ getpeppr send invoice.json --prod
149
149
  | `--to <peppol-id>` | Recipient Peppol ID (when no file) |
150
150
  | `--amount <number>` | Line amount in major currency units |
151
151
  | `--currency <iso>` | ISO 4217 code (default `EUR`) |
152
+ | `--country <iso>` | Recipient country (ISO 3166-1 alpha-2) when no file |
152
153
  | `--desc <text>` | Line description |
153
154
  | `--attachment` | Attach the bundled test PDF |
154
155
  | `--watch` | Poll status until terminal (60s timeout) |
@@ -189,7 +190,7 @@ If no legal entity exists in the environment yet, `whoami` says so and points to
189
190
  getpeppr logout
190
191
  ```
191
192
 
192
- Removes `~/.config/getpeppr/credentials.json`. No-op if the file doesn't exist.
193
+ Removes the credentials file. No-op if the file doesn't exist.
193
194
 
194
195
  ## Exit codes
195
196
 
@@ -234,7 +235,7 @@ GETPEPPR_API_KEY=$SANDBOX_KEY getpeppr send invoice.json --json
234
235
  The CLI runs three validation engines from the [@getpeppr/sdk](https://www.npmjs.com/package/@getpeppr/sdk):
235
236
 
236
237
  1. **Structure** — Required fields, type checks, format validation
237
- 2. **Business Rules** — Peppol BIS 3.0 / EN 16931 compliance (BR-xx, BR-CO-xx, PEPPOL-xx rules)
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.
238
239
  3. **Country Rules** — Belgium (BE), France (FR), Italy (IT), Netherlands (NL), Germany (DE)
239
240
 
240
241
  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.
package/dist/index.js CHANGED
@@ -65,7 +65,7 @@ function formatWarning(item) {
65
65
  function formatSection(title, errors, warnings) {
66
66
  const lines = [sectionHeader(title)];
67
67
  if (errors.length === 0 && warnings.length === 0) {
68
- lines.push(` ${pc.green("\u2713")} All rules passed`);
68
+ lines.push(` ${pc.green("\u2713")} No findings`);
69
69
  return lines.join("\n");
70
70
  }
71
71
  if (errors.length === 0) {
@@ -94,7 +94,7 @@ Validating: ${pc.bold(filename)}
94
94
  lines.push("");
95
95
  lines.push(
96
96
  formatSection(
97
- "Business Rules (Peppol BIS 3.0)",
97
+ "Offline Checks (partial)",
98
98
  result.schematron.errors,
99
99
  result.schematron.warnings
100
100
  )
@@ -111,10 +111,10 @@ Validating: ${pc.bold(filename)}
111
111
  lines.push(sectionHeader("Summary"));
112
112
  const { totalErrors, totalWarnings, valid } = result;
113
113
  if (valid && totalWarnings === 0) {
114
- lines.push(` ${pc.green(pc.bold("\u2713 Invoice is valid"))}`);
114
+ lines.push(` ${pc.green(pc.bold("\u2713 Pre-flight checks passed"))}`);
115
115
  } else if (valid) {
116
116
  lines.push(
117
- ` ${pc.green(pc.bold("\u2713 Invoice is valid"))} ${pc.dim(`(${totalWarnings} warning${totalWarnings === 1 ? "" : "s"})`)}`
117
+ ` ${pc.green(pc.bold("\u2713 Pre-flight checks passed"))} ${pc.dim(`(${totalWarnings} warning${totalWarnings === 1 ? "" : "s"})`)}`
118
118
  );
119
119
  } else {
120
120
  const parts = [];
@@ -123,7 +123,7 @@ Validating: ${pc.bold(filename)}
123
123
  parts.push(`${totalWarnings} warning${totalWarnings === 1 ? "" : "s"}`);
124
124
  }
125
125
  lines.push(
126
- ` ${pc.red(pc.bold(`\u2717 ${parts.join(", ")}`))} \u2014 invoice non-compliant`
126
+ ` ${pc.red(pc.bold("\u2717 Pre-flight checks found errors"))} ${pc.dim(`(${parts.join(", ")})`)}`
127
127
  );
128
128
  }
129
129
  lines.push("");
@@ -773,8 +773,9 @@ function formatBaseQuantity(baseQuantity, field) {
773
773
  }
774
774
  return `${digits.slice(0, outputPoint)}.${digits.slice(outputPoint)}`;
775
775
  }
776
- function round2(n) {
777
- return Math.round(n * 100) / 100;
776
+ function roundUblCurrencyAmount(value) {
777
+ const rounded = Math.round((value + Math.sign(value) * Number.EPSILON) * 100) / 100;
778
+ return Object.is(rounded, -0) ? 0 : rounded;
778
779
  }
779
780
  function normalizedTaxExemptReason(vatCategory, reason) {
780
781
  if (!EXEMPTION_REASON_CATEGORIES.has(vatCategory) || typeof reason !== "string") {
@@ -1063,7 +1064,7 @@ function calculateTaxSubtotals(lines, allowances, charges, options = {}) {
1063
1064
  throw new UblBuilderInputError(`Conflicting taxExemptReason values for VAT group ${vatCategory}/${effectiveVatRate}.`, "taxExemptReason");
1064
1065
  }
1065
1066
  existing.taxExemptReason ??= reason;
1066
- existing.taxableAmount = round2(existing.taxableAmount + amount);
1067
+ existing.taxableAmount += amount;
1067
1068
  } else {
1068
1069
  groups.set(key, {
1069
1070
  vatRate: effectiveVatRate,
@@ -1092,8 +1093,8 @@ function calculateTaxSubtotals(lines, allowances, charges, options = {}) {
1092
1093
  }
1093
1094
  }
1094
1095
  return Array.from(groups.values()).map((subtotal) => {
1095
- const taxableAmount = round2(subtotal.taxableAmount);
1096
- const taxAmount = round2(taxableAmount * (subtotal.vatRate / 100));
1096
+ const taxableAmount = roundUblCurrencyAmount(subtotal.taxableAmount);
1097
+ const taxAmount = roundUblCurrencyAmount(taxableAmount * (subtotal.vatRate / 100));
1097
1098
  if (!survivesCents(taxableAmount) || !survivesCents(taxAmount)) {
1098
1099
  throw new UblBuilderInputError(NON_FINITE_DERIVED_AMOUNT_MESSAGE, "totals", DERIVED_AMOUNT_CONTRACT_RULE);
1099
1100
  }
@@ -1105,12 +1106,12 @@ function calculateDocumentTotals(lines, allowances, charges, options = {}) {
1105
1106
  const lineExtensionAmount = lines.reduce((sum, line, index) => sum + calculateLineExtensionAmount(line, index), 0);
1106
1107
  const allowanceTotalAmount = (allowances ?? []).reduce((sum, a) => sum + a.amount, 0);
1107
1108
  const chargeTotalAmount = (charges ?? []).reduce((sum, c) => sum + c.amount, 0);
1108
- const taxExclusiveAmount = round2(lineExtensionAmount - allowanceTotalAmount + chargeTotalAmount);
1109
+ const taxExclusiveAmount = roundUblCurrencyAmount(lineExtensionAmount - allowanceTotalAmount + chargeTotalAmount);
1109
1110
  if (!survivesCents(allowanceTotalAmount) || !survivesCents(chargeTotalAmount) || !survivesCents(taxExclusiveAmount)) {
1110
1111
  throw new UblBuilderInputError(NON_FINITE_DERIVED_AMOUNT_MESSAGE, "totals", DERIVED_AMOUNT_CONTRACT_RULE);
1111
1112
  }
1112
- const totalTax = round2(taxSubtotals.reduce((sum, st) => sum + st.taxAmount, 0));
1113
- const taxInclusiveAmount = round2(taxExclusiveAmount + totalTax);
1113
+ const totalTax = roundUblCurrencyAmount(taxSubtotals.reduce((sum, st) => sum + st.taxAmount, 0));
1114
+ const taxInclusiveAmount = roundUblCurrencyAmount(taxExclusiveAmount + totalTax);
1114
1115
  if (!survivesCents(totalTax) || !survivesCents(taxInclusiveAmount)) {
1115
1116
  throw new UblBuilderInputError(NON_FINITE_DERIVED_AMOUNT_MESSAGE, "totals", DERIVED_AMOUNT_CONTRACT_RULE);
1116
1117
  }
@@ -1145,7 +1146,7 @@ function buildTaxTotalXml(taxSubtotals, totalTax, currency) {
1145
1146
  </cac:TaxTotal>`;
1146
1147
  }
1147
1148
  function buildTaxCurrencyTotalXml(totalTax, taxCurrency, rate) {
1148
- const convertedAmount = Math.round(totalTax * rate * 100) / 100;
1149
+ const convertedAmount = roundUblCurrencyAmount(totalTax * rate);
1149
1150
  return `<cac:TaxTotal>
1150
1151
  <cbc:TaxAmount currencyID="${escapeXml(taxCurrency)}">${formatAmount(convertedAmount)}</cbc:TaxAmount>
1151
1152
  </cac:TaxTotal>`;
@@ -1606,6 +1607,79 @@ function resolveUnit(input) {
1606
1607
  function getAllUnits() {
1607
1608
  return Array.from(UNIT_CODES.entries()).map(([code, name]) => ({ code, name })).sort((a, b) => a.code.localeCompare(b.code));
1608
1609
  }
1610
+ var INVOICE_TYPE_CODES = [
1611
+ 71,
1612
+ 80,
1613
+ 81,
1614
+ 82,
1615
+ 84,
1616
+ 102,
1617
+ 130,
1618
+ 202,
1619
+ 203,
1620
+ 204,
1621
+ 211,
1622
+ 218,
1623
+ 219,
1624
+ 295,
1625
+ 325,
1626
+ 326,
1627
+ 331,
1628
+ 380,
1629
+ 382,
1630
+ 383,
1631
+ 384,
1632
+ 385,
1633
+ 386,
1634
+ 387,
1635
+ 388,
1636
+ 389,
1637
+ 390,
1638
+ 393,
1639
+ 394,
1640
+ 395,
1641
+ 456,
1642
+ 457,
1643
+ 471,
1644
+ 472,
1645
+ 473,
1646
+ 500,
1647
+ 501,
1648
+ 527,
1649
+ 553,
1650
+ 575,
1651
+ 623,
1652
+ 633,
1653
+ 751,
1654
+ 780,
1655
+ 817,
1656
+ 870,
1657
+ 875,
1658
+ 876,
1659
+ 877,
1660
+ 935
1661
+ ];
1662
+ var CREDIT_NOTE_TYPE_CODES = [
1663
+ 81,
1664
+ 83,
1665
+ 261,
1666
+ 262,
1667
+ 296,
1668
+ 308,
1669
+ 381,
1670
+ 396,
1671
+ 420,
1672
+ 458,
1673
+ 502,
1674
+ 503,
1675
+ 532
1676
+ ];
1677
+ function getInvoiceTypeCodes() {
1678
+ return [...INVOICE_TYPE_CODES];
1679
+ }
1680
+ function getCreditNoteTypeCodes() {
1681
+ return [...CREDIT_NOTE_TYPE_CODES];
1682
+ }
1609
1683
 
1610
1684
  // ../sdk/dist/core/validator.js
1611
1685
  function error(field, message, ruleId, suggestion) {
@@ -1736,9 +1810,12 @@ function validateInvoice(input) {
1736
1810
  } else if (!input.number.trim()) {
1737
1811
  errors.push(error("number", "Invoice number is required", "BR-02", "Must be unique per supplier"));
1738
1812
  }
1739
- const VALID_TYPE_CODES = [380, 381, 383, 384, 386, 389, 751];
1740
- if (input.invoiceTypeCode != null && !VALID_TYPE_CODES.includes(input.invoiceTypeCode)) {
1741
- errors.push(error("invoiceTypeCode", `Invalid invoice type code: ${input.invoiceTypeCode}`, void 0, "Valid codes: 380, 381, 383, 384, 386, 389, 751"));
1813
+ if (input.invoiceTypeCode != null) {
1814
+ const isCreditNote = input.isCreditNote === true;
1815
+ const legalCodes = isCreditNote ? getCreditNoteTypeCodes() : getInvoiceTypeCodes();
1816
+ if (!legalCodes.includes(input.invoiceTypeCode)) {
1817
+ errors.push(error("invoiceTypeCode", `Invalid ${isCreditNote ? "credit note" : "invoice"} type code: ${input.invoiceTypeCode}`, "BR-CL-01", `Valid ${isCreditNote ? "credit note" : "invoice"} type codes: ${legalCodes.join(", ")}`));
1818
+ }
1742
1819
  }
1743
1820
  if (input.isCreditNote) {
1744
1821
  const ref = input.invoiceReference;
@@ -1910,12 +1987,12 @@ function validateInvoice(input) {
1910
1987
  function violation(ruleId, severity, message, field) {
1911
1988
  return { ruleId, severity, message, field };
1912
1989
  }
1913
- var _knownUnitCodes;
1914
- function getKnownUnitCodes() {
1915
- if (!_knownUnitCodes) {
1916
- _knownUnitCodes = new Set(getAllUnits().map((u) => u.code));
1990
+ var _sdkUnitCodes;
1991
+ function getSdkUnitCodes() {
1992
+ if (!_sdkUnitCodes) {
1993
+ _sdkUnitCodes = new Set(getAllUnits().map((u) => u.code));
1917
1994
  }
1918
- return _knownUnitCodes;
1995
+ return _sdkUnitCodes;
1919
1996
  }
1920
1997
  var VALID_VAT_CATEGORIES = /* @__PURE__ */ new Set(["S", "Z", "E", "AE", "K", "G", "O", "L", "M", "B"]);
1921
1998
  var SENDABLE_VAT_CATEGORIES = ["S", "Z", "E", "AE", "K", "G", "O"];
@@ -1935,18 +2012,18 @@ var br02 = (input) => {
1935
2012
  }
1936
2013
  return [];
1937
2014
  };
1938
- var br03 = (input) => {
2015
+ var issueDateDefaulted = (input) => {
1939
2016
  if (!input.date) {
1940
2017
  return [
1941
- violation("BR-03", "warning", "Invoice issue date is not set. The SDK will default to today's date.", "date")
2018
+ violation("GETPEPPR-ISSUE-DATE-DEFAULTED", "warning", "Invoice issue date is not set. The SDK will default to today's date.", "date")
1942
2019
  ];
1943
2020
  }
1944
2021
  return [];
1945
2022
  };
1946
- var br06 = (input) => {
2023
+ var sellerVatRecommendation = (input) => {
1947
2024
  if (input.from && !input.from.vatNumber) {
1948
2025
  return [
1949
- violation("BR-06", "warning", "Seller party has no VAT number. The gateway will use the account's VAT registration.", "from.vatNumber")
2026
+ violation("GETPEPPR-FROM-VAT-RECOMMENDED", "warning", "Seller party has no VAT number. The gateway will use the account's VAT registration.", "from.vatNumber")
1950
2027
  ];
1951
2028
  }
1952
2029
  return [];
@@ -1957,29 +2034,29 @@ var br07 = (input) => {
1957
2034
  }
1958
2035
  return [];
1959
2036
  };
1960
- var br08 = (input) => {
2037
+ var br16 = (input) => {
1961
2038
  if (!input.lines || input.lines.length === 0) {
1962
- return [violation("BR-08", "error", "Invoice must have at least one line item.", "lines")];
2039
+ return [violation("BR-16", "error", "Invoice must have at least one line item.", "lines")];
1963
2040
  }
1964
2041
  return [];
1965
2042
  };
1966
- var br09 = (input) => {
2043
+ var paymentTimingRecommendation = (input) => {
1967
2044
  if (!input.dueDate && !input.paymentTerms) {
1968
2045
  return [
1969
- violation("BR-09", "warning", "Neither due date nor payment terms specified. At least one is recommended.", "dueDate")
2046
+ violation("GETPEPPR-PAYMENT-TIMING-RECOMMENDED", "warning", "Neither due date nor payment terms specified. At least one is recommended.", "dueDate")
1970
2047
  ];
1971
2048
  }
1972
2049
  return [];
1973
2050
  };
1974
- var br10 = (input) => {
2051
+ var buyerOrOrderReferenceRecommendation = (input) => {
1975
2052
  if (!input.buyerReference && !input.orderReference) {
1976
2053
  return [
1977
- violation("BR-10", "warning", "Neither buyerReference nor orderReference specified. Peppol BIS 3.0 requires at least one.", "buyerReference")
2054
+ violation("GETPEPPR-BUYER-OR-ORDER-REFERENCE-RECOMMENDED", "warning", "Neither buyerReference nor orderReference specified. Peppol BIS 3.0 requires at least one.", "buyerReference")
1978
2055
  ];
1979
2056
  }
1980
2057
  return [];
1981
2058
  };
1982
- var brCo10 = (input) => {
2059
+ var lineNetSanity = (input) => {
1983
2060
  const violations = [];
1984
2061
  if (!input.lines)
1985
2062
  return violations;
@@ -1989,12 +2066,12 @@ var brCo10 = (input) => {
1989
2066
  if (!Number.isFinite(net)) {
1990
2067
  const baseQty = line.baseQuantity ?? 1;
1991
2068
  const detail = baseQty === 0 ? "baseQuantity is 0, causing division by zero." : "Computed line amount is not a finite number.";
1992
- violations.push(violation("BR-CO-10", "error", `Line ${i}: invalid net amount. ${detail}`, `lines[${i}]`));
2069
+ violations.push(violation("GETPEPPR-LINE-NET-SANITY", "error", `Line ${i}: invalid net amount. ${detail}`, `lines[${i}]`));
1993
2070
  }
1994
2071
  }
1995
2072
  return violations;
1996
2073
  };
1997
- var brCo13 = (input) => {
2074
+ var computedVatSanity = (input) => {
1998
2075
  if (!input.lines || input.lines.length === 0)
1999
2076
  return [];
2000
2077
  let totalVat = 0;
@@ -2013,17 +2090,17 @@ var brCo13 = (input) => {
2013
2090
  }
2014
2091
  if (!Number.isFinite(totalVat)) {
2015
2092
  return [
2016
- violation("BR-CO-13", "error", "Computed total VAT amount is not a finite number. Check line amounts and VAT rates.")
2093
+ violation("GETPEPPR-COMPUTED-VAT-SANITY", "error", "Computed total VAT amount is not a finite number. Check line amounts and VAT rates.")
2017
2094
  ];
2018
2095
  }
2019
2096
  if (totalVat < -0.01) {
2020
2097
  return [
2021
- violation("BR-CO-13", "warning", `Computed total VAT is negative (${totalVat.toFixed(2)}). This is unusual for an invoice.`)
2098
+ violation("GETPEPPR-COMPUTED-VAT-SANITY", "warning", `Computed total VAT is negative (${totalVat.toFixed(2)}). This is unusual for an invoice.`)
2022
2099
  ];
2023
2100
  }
2024
2101
  return [];
2025
2102
  };
2026
- var brCo15 = (input) => {
2103
+ var taxInclusiveSanity = (input) => {
2027
2104
  if (!input.lines || input.lines.length === 0)
2028
2105
  return [];
2029
2106
  let lineTotal = 0;
@@ -2046,17 +2123,17 @@ var brCo15 = (input) => {
2046
2123
  const taxInclusive = lineTotal + vatTotal;
2047
2124
  if (!Number.isFinite(taxInclusive)) {
2048
2125
  return [
2049
- violation("BR-CO-15", "error", "Computed tax-inclusive amount is not a finite number.")
2126
+ violation("GETPEPPR-TAX-INCLUSIVE-SANITY", "error", "Computed tax-inclusive amount is not a finite number.")
2050
2127
  ];
2051
2128
  }
2052
2129
  if (taxInclusive < -0.01) {
2053
2130
  return [
2054
- violation("BR-CO-15", "warning", `Computed tax-inclusive amount is negative (${taxInclusive.toFixed(2)}). Consider using a credit note instead.`)
2131
+ violation("GETPEPPR-TAX-INCLUSIVE-SANITY", "warning", `Computed tax-inclusive amount is negative (${taxInclusive.toFixed(2)}). Consider using a credit note instead.`)
2055
2132
  ];
2056
2133
  }
2057
2134
  return [];
2058
2135
  };
2059
- var brCo16 = (input) => {
2136
+ var payableSanity = (input) => {
2060
2137
  if (!input.lines || input.lines.length === 0)
2061
2138
  return [];
2062
2139
  let lineTotal = 0;
@@ -2082,12 +2159,12 @@ var brCo16 = (input) => {
2082
2159
  const payable = taxInclusive - prepaid + rounding;
2083
2160
  if (!Number.isFinite(payable)) {
2084
2161
  return [
2085
- violation("BR-CO-16", "error", "Computed payable amount is not a finite number.")
2162
+ violation("GETPEPPR-PAYABLE-SANITY", "error", "Computed payable amount is not a finite number.")
2086
2163
  ];
2087
2164
  }
2088
2165
  if (payable < -0.01) {
2089
2166
  return [
2090
- violation("BR-CO-16", "warning", `Computed payable amount is negative (${payable.toFixed(2)}). Prepaid amount (${prepaid}) exceeds the invoice total.`)
2167
+ violation("GETPEPPR-PAYABLE-SANITY", "warning", `Computed payable amount is negative (${payable.toFixed(2)}). Check invoice totals, prepaid amount (${prepaid}), and rounding amount (${rounding}).`)
2091
2168
  ];
2092
2169
  }
2093
2170
  return [];
@@ -2295,10 +2372,10 @@ function validateUblBuilderVat(input) {
2295
2372
  ...UBL_BUILDER_VAT_RULES.flatMap((rule) => rule(input))
2296
2373
  ];
2297
2374
  }
2298
- var peppolR004 = (input) => {
2375
+ var buyerPeppolIdRequired = (input) => {
2299
2376
  if (!input.to?.peppolId) {
2300
2377
  return [
2301
- violation("PEPPOL-EN16931-R004", "error", "Buyer electronic address (peppolId) is required for Peppol delivery.", "to.peppolId")
2378
+ violation("GETPEPPR-BUYER-PEPPOL-ID-REQUIRED", "error", "Buyer electronic address (peppolId) is required for Peppol delivery.", "to.peppolId")
2302
2379
  ];
2303
2380
  }
2304
2381
  return [];
@@ -2323,17 +2400,17 @@ var vatCategoryCodes = (input) => {
2323
2400
  (input.charges ?? []).forEach((c, i) => check(c.vatCategory, `charges[${i}].vatCategory`, `Charge ${i}`));
2324
2401
  return violations;
2325
2402
  };
2326
- var peppolR080 = (input) => {
2403
+ var unitCodeRecognised = (input) => {
2327
2404
  const violations = [];
2328
2405
  if (!input.lines)
2329
2406
  return violations;
2330
- const knownCodes = getKnownUnitCodes();
2407
+ const knownCodes = getSdkUnitCodes();
2331
2408
  for (let i = 0; i < input.lines.length; i++) {
2332
2409
  const line = input.lines[i];
2333
2410
  if (line.unit) {
2334
2411
  const resolved = resolveUnit(line.unit);
2335
2412
  if (!knownCodes.has(resolved)) {
2336
- violations.push(violation("PEPPOL-EN16931-R080", "warning", `Line ${i}: unit "${line.unit}" (resolved: "${resolved}") is not a known UN/ECE Rec20 code. Common codes: EA, HUR, DAY, KGM.`, `lines[${i}].unit`));
2413
+ violations.push(violation("GETPEPPR-UNIT-CODE-RECOGNISED", "warning", `Line ${i}: unit "${line.unit}" (resolved: "${resolved}") is not in the SDK's common unit list. It may still be a valid UN/ECE Rec20/21 code; network validation decides. Common codes: EA, HUR, DAY, KGM.`, `lines[${i}].unit`));
2337
2414
  }
2338
2415
  }
2339
2416
  }
@@ -2342,17 +2419,17 @@ var peppolR080 = (input) => {
2342
2419
  var ALL_RULES = [
2343
2420
  // Required fields (BR)
2344
2421
  br02,
2345
- br03,
2346
- br06,
2422
+ issueDateDefaulted,
2423
+ sellerVatRecommendation,
2347
2424
  br07,
2348
- br08,
2349
- br09,
2350
- br10,
2425
+ br16,
2426
+ paymentTimingRecommendation,
2427
+ buyerOrOrderReferenceRecommendation,
2351
2428
  // Calculations (BR-CO)
2352
- brCo10,
2353
- brCo13,
2354
- brCo15,
2355
- brCo16,
2429
+ lineNetSanity,
2430
+ computedVatSanity,
2431
+ taxInclusiveSanity,
2432
+ payableSanity,
2356
2433
  // Tax categories (one family per category)
2357
2434
  brS05,
2358
2435
  brZ05,
@@ -2367,9 +2444,9 @@ var ALL_RULES = [
2367
2444
  brO10,
2368
2445
  brIc10,
2369
2446
  // Peppol-specific
2370
- peppolR004,
2447
+ buyerPeppolIdRequired,
2371
2448
  vatCategoryCodes,
2372
- peppolR080
2449
+ unitCodeRecognised
2373
2450
  ];
2374
2451
  function validateSchematron(input) {
2375
2452
  const errors = [];
@@ -2393,17 +2470,17 @@ function validateSchematron(input) {
2393
2470
  }
2394
2471
  var SDK_SCHEMATRON_RULE_IDS = [
2395
2472
  "BR-02",
2396
- "BR-03",
2397
- "BR-06",
2473
+ "GETPEPPR-ISSUE-DATE-DEFAULTED",
2474
+ "GETPEPPR-FROM-VAT-RECOMMENDED",
2398
2475
  "BR-07",
2399
- "BR-08",
2400
- "BR-09",
2401
- "BR-10",
2476
+ "BR-16",
2477
+ "GETPEPPR-PAYMENT-TIMING-RECOMMENDED",
2478
+ "GETPEPPR-BUYER-OR-ORDER-REFERENCE-RECOMMENDED",
2402
2479
  "BR-CL-17",
2403
- "BR-CO-10",
2404
- "BR-CO-13",
2405
- "BR-CO-15",
2406
- "BR-CO-16",
2480
+ "GETPEPPR-LINE-NET-SANITY",
2481
+ "GETPEPPR-COMPUTED-VAT-SANITY",
2482
+ "GETPEPPR-TAX-INCLUSIVE-SANITY",
2483
+ "GETPEPPR-PAYABLE-SANITY",
2407
2484
  "BR-S-05",
2408
2485
  "BR-Z-05",
2409
2486
  "BR-E-05",
@@ -2427,8 +2504,8 @@ var SDK_SCHEMATRON_RULE_IDS = [
2427
2504
  "BR-G-10",
2428
2505
  "BR-O-10",
2429
2506
  "BR-IC-10",
2430
- "PEPPOL-EN16931-R004",
2431
- "PEPPOL-EN16931-R080"
2507
+ "GETPEPPR-BUYER-PEPPOL-ID-REQUIRED",
2508
+ "GETPEPPR-UNIT-CODE-RECOGNISED"
2432
2509
  ];
2433
2510
 
2434
2511
  // ../sdk/dist/core/status-precedence.js
@@ -2456,7 +2533,7 @@ function statusFamily(status) {
2456
2533
  var TERMINAL_FAILURE_STATUSES = STATUS_PRECEDENCE.filter((e) => e.family === "terminal-failure").map((e) => e.status);
2457
2534
 
2458
2535
  // ../sdk/dist/version.js
2459
- var SDK_VERSION = "5.0.0";
2536
+ var SDK_VERSION = "5.2.0";
2460
2537
 
2461
2538
  // ../sdk/dist/core/api-result.js
2462
2539
  var API_RESULT_HEADER_NAMES = {
@@ -4081,7 +4158,8 @@ ${validation.errors.map((e) => ` - ${e.field}: ${e.message}${e.suggestion ? ` (
4081
4158
  /**
4082
4159
  * Validate an invoice server-side using the getpeppr gateway's offline SDK-backed checks.
4083
4160
  * The gateway runs SDK validation, verifies UBL XML generation, and evaluates offline
4084
- * Peppol business rules without sending the invoice to Storecove.
4161
+ * pre-flight checks without sending the invoice to Storecove. This is not a Peppol
4162
+ * conformance verdict.
4085
4163
  *
4086
4164
  * @example
4087
4165
  * ```ts
@@ -5880,7 +5958,7 @@ async function promptEnvironment() {
5880
5958
  });
5881
5959
  }
5882
5960
  function registerLoginCommand(program2) {
5883
- program2.command("login").description("Save a getpeppr API key to ~/.config/getpeppr/credentials.json").option("--key <key>", "API key \u2014 for CI/scripted use only; visible in `ps` and shell history. Prefer the interactive prompt or GETPEPPR_API_KEY env var.").option("--sandbox", "store as sandbox key (default)").option("--live", "store as live (production) key").action(async (flags) => {
5961
+ program2.command("login").description("Save a getpeppr API key to the credentials file ($XDG_CONFIG_HOME/getpeppr, %APPDATA%\\getpeppr on Windows)").option("--key <key>", "API key \u2014 for CI/scripted use only; visible in `ps` and shell history. Prefer the interactive prompt or GETPEPPR_API_KEY env var.").option("--sandbox", "store as sandbox key (default)").option("--live", "store as live (production) key").action(async (flags) => {
5884
5962
  if (!flags.live && !flags.sandbox && process.stdin.isTTY !== true) {
5885
5963
  exitWithError("Error: --sandbox or --live required when stdin is not a TTY (CI mode).");
5886
5964
  }
@@ -6012,7 +6090,7 @@ function registerWhoamiCommand(program2) {
6012
6090
  // src/commands/logout.ts
6013
6091
  import pc9 from "picocolors";
6014
6092
  function registerLogoutCommand(program2) {
6015
- program2.command("logout").description("Remove ~/.config/getpeppr/credentials.json").action(() => {
6093
+ program2.command("logout").description("Remove the stored credentials file").action(() => {
6016
6094
  const path = getCredentialsPath();
6017
6095
  const removed = deleteCredentials();
6018
6096
  if (removed) {