@getpeppr/cli 0.10.0 → 0.11.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,6 +4,16 @@ 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.0] — 2026-09-04
8
+
9
+ Rebundles `@getpeppr/sdk` 5.1.0.
10
+
11
+ - **`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):
12
+ - **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.
13
+ - **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.
14
+ - The default is unchanged: omit `invoiceTypeCode` and the document kind decides (`380` invoice, `381` credit note).
15
+ - **The tarball now ships its `LICENSE` file** (the published 0.10.0 does not contain one) and the corrected README.
16
+
7
17
  ## [0.10.0] — 2026-08-28
8
18
 
9
19
  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
@@ -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
 
package/dist/index.js CHANGED
@@ -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;
@@ -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.1.0";
2460
2537
 
2461
2538
  // ../sdk/dist/core/api-result.js
2462
2539
  var API_RESULT_HEADER_NAMES = {
@@ -5880,7 +5957,7 @@ async function promptEnvironment() {
5880
5957
  });
5881
5958
  }
5882
5959
  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) => {
5960
+ 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
5961
  if (!flags.live && !flags.sandbox && process.stdin.isTTY !== true) {
5885
5962
  exitWithError("Error: --sandbox or --live required when stdin is not a TTY (CI mode).");
5886
5963
  }
@@ -6012,7 +6089,7 @@ function registerWhoamiCommand(program2) {
6012
6089
  // src/commands/logout.ts
6013
6090
  import pc9 from "picocolors";
6014
6091
  function registerLogoutCommand(program2) {
6015
- program2.command("logout").description("Remove ~/.config/getpeppr/credentials.json").action(() => {
6092
+ program2.command("logout").description("Remove the stored credentials file").action(() => {
6016
6093
  const path = getCredentialsPath();
6017
6094
  const removed = deleteCredentials();
6018
6095
  if (removed) {