@getpeppr/cli 0.7.0 → 0.7.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 +27 -0
- package/README.md +17 -0
- package/dist/index.js +87 -18
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@ 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.7.1] — 2026-08-18
|
|
8
|
+
|
|
9
|
+
Rebundle SDK 4.1.1. This release matters for `getpeppr validate`, which runs the
|
|
10
|
+
schematron validator locally from the bundled SDK.
|
|
11
|
+
|
|
12
|
+
**Four rule identifiers were wrong, and 0.7.0 still reports them.** They named
|
|
13
|
+
real rules from the Peppol rulebook — just not the ones being checked — which is
|
|
14
|
+
worse than a typo: you could look one up, read a genuine rule, and find it had
|
|
15
|
+
nothing to do with your document.
|
|
16
|
+
|
|
17
|
+
| `validate` reported | It now reports | What the check actually verifies |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| `BR-S-01` | `BR-S-05` | standard-rated line must carry a VAT rate above zero |
|
|
20
|
+
| `BR-S-05` | `BR-Z-05` | zero-rated line must carry a rate of exactly zero |
|
|
21
|
+
| `BR-S-06` | `BR-E-05` | VAT-exempt line must carry a rate of exactly zero |
|
|
22
|
+
| `BR-S-08` | `BR-AE-05` | reverse-charge line must carry a rate of exactly zero |
|
|
23
|
+
|
|
24
|
+
The checks themselves were always right — a zero-rated line carrying a non-zero
|
|
25
|
+
rate was always caught. Only the label was wrong. **If you branch on any of these
|
|
26
|
+
identifiers in a pipeline, they change with this version.**
|
|
27
|
+
|
|
28
|
+
Fixed in SDK 4.1.0 on 2026-08-18; CLI users only receive it now, because the CLI
|
|
29
|
+
bundles the SDK rather than depending on it at runtime.
|
|
30
|
+
|
|
31
|
+
Also carries SDK 4.1.1, which corrects the JSDoc on `importFile()` — raw import
|
|
32
|
+
does not preserve your document byte for byte, and the SDK said it did.
|
|
33
|
+
|
|
7
34
|
## [0.7.0] — 2026-08-16
|
|
8
35
|
|
|
9
36
|
Rebundle SDK 4.0.0 (GPR-1061). `getpeppr send --watch` could not confirm a
|
package/README.md
CHANGED
|
@@ -175,6 +175,23 @@ Removes `~/.config/getpeppr/credentials.json`. No-op if the file doesn't exist.
|
|
|
175
175
|
| `1` | Failure (invalid invoice, participant not found) |
|
|
176
176
|
| `2` | Input error (file not found, invalid JSON, bad arguments) |
|
|
177
177
|
|
|
178
|
+
### `send --watch` is two outcomes, not one
|
|
179
|
+
|
|
180
|
+
Since 0.7.0, a watch that fails because the API answered something the CLI
|
|
181
|
+
cannot honestly read exits **1**. Before 0.7.0 it printed a warning and exited
|
|
182
|
+
`0`, which meant an automation could read a broken API as a confirmed delivery.
|
|
183
|
+
|
|
184
|
+
A watch that runs out of time still exits **0**, and that is a different fact:
|
|
185
|
+
the document was accepted, and the watch stopped without having *seen* a
|
|
186
|
+
terminal status. It does not follow that the document is still in flight. The
|
|
187
|
+
deadline is checked between polls, so a status can turn terminal during the
|
|
188
|
+
final wait and never be read — the status printed on a timeout may already be
|
|
189
|
+
stale. Poll again or use a webhook rather than treating a timeout as a failure.
|
|
190
|
+
|
|
191
|
+
⚠️ The 60 seconds bound when the **last poll starts**, not when the command
|
|
192
|
+
ends. The deadline is checked before each request, so a slow or retried call
|
|
193
|
+
carries the run past it. If a pipeline needs a hard ceiling, impose your own.
|
|
194
|
+
|
|
178
195
|
## CI/CD integration
|
|
179
196
|
|
|
180
197
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -1161,7 +1161,7 @@ function statusFamily(status) {
|
|
|
1161
1161
|
var TERMINAL_FAILURE_STATUSES = STATUS_PRECEDENCE.filter((e) => e.family === "terminal-failure").map((e) => e.status);
|
|
1162
1162
|
|
|
1163
1163
|
// ../sdk/dist/version.js
|
|
1164
|
-
var SDK_VERSION = "4.
|
|
1164
|
+
var SDK_VERSION = "4.1.1";
|
|
1165
1165
|
|
|
1166
1166
|
// ../sdk/dist/core/client.js
|
|
1167
1167
|
function findHeaderCaseInsensitive(headers, name) {
|
|
@@ -1753,7 +1753,11 @@ var GetpepprAdapter = class {
|
|
|
1753
1753
|
const body = {
|
|
1754
1754
|
file: arrayBufferToBase64(options.file),
|
|
1755
1755
|
filename: options.filename,
|
|
1756
|
-
mimeType: options.mimeType ?? detectMimeType(options.filename)
|
|
1756
|
+
mimeType: options.mimeType ?? detectMimeType(options.filename),
|
|
1757
|
+
// Declared, never derived from the document: routing decides delivery, and
|
|
1758
|
+
// parsing a caller's XML for a destination would put a parse error on the
|
|
1759
|
+
// "whose invoice goes where" path.
|
|
1760
|
+
to: options.to
|
|
1757
1761
|
};
|
|
1758
1762
|
const result = await this.request("POST", "/invoices/import", body);
|
|
1759
1763
|
return parseSendResult(result);
|
|
@@ -1863,6 +1867,10 @@ function parseSendResult(body) {
|
|
|
1863
1867
|
const detail = parseStatusDetail(result.detail);
|
|
1864
1868
|
if (detail)
|
|
1865
1869
|
sendResult.detail = detail;
|
|
1870
|
+
const rulebook = result.rulebook;
|
|
1871
|
+
if (typeof rulebook === "object" && rulebook !== null && typeof rulebook.peppol === "string" && typeof rulebook.verifiedAt === "string") {
|
|
1872
|
+
sendResult.rulebook = rulebook;
|
|
1873
|
+
}
|
|
1866
1874
|
const submissionId = optionalWireId(result.submissionId);
|
|
1867
1875
|
if (submissionId)
|
|
1868
1876
|
sendResult.submissionId = submissionId;
|
|
@@ -2354,9 +2362,30 @@ ${validation.errors.map((e) => ` - ${e.field}: ${e.message}${e.suggestion ? ` (
|
|
|
2354
2362
|
return this.adapter.validateDocumentServer(input);
|
|
2355
2363
|
}
|
|
2356
2364
|
/**
|
|
2357
|
-
*
|
|
2358
|
-
*
|
|
2359
|
-
*
|
|
2365
|
+
* Send a UBL Invoice or CreditNote you built yourself.
|
|
2366
|
+
*
|
|
2367
|
+
* getpeppr does not regenerate, normalise, or repair the document — we
|
|
2368
|
+
* forward the bytes you supplied, unchanged, to the network. Only UBL Invoice
|
|
2369
|
+
* and CreditNote are accepted — a PDF, a CII document, or an XML that is
|
|
2370
|
+
* neither is refused. The file is base64-encoded into a JSON body; there is
|
|
2371
|
+
* no multipart upload.
|
|
2372
|
+
*
|
|
2373
|
+
* ⚠️ Byte-for-byte equality is NOT guaranteed, because the network
|
|
2374
|
+
* re-serialises the document in transit. Measured 2026-08-18 on a test
|
|
2375
|
+
* document: namespace declarations come back reordered, numeric character
|
|
2376
|
+
* references are resolved (`A` → `A`), whitespace inside tags is dropped,
|
|
2377
|
+
* and no element was added, removed or altered. That is one document and four
|
|
2378
|
+
* kinds of difference — indicative, not a warranty of what is preserved.
|
|
2379
|
+
* **If you seal your documents, hash a canonical form (C14N) rather than the
|
|
2380
|
+
* raw bytes.**
|
|
2381
|
+
*
|
|
2382
|
+
* `to` is required and is never read from the document. Routing decides
|
|
2383
|
+
* delivery, the document travels as payload, and getpeppr will not guess a
|
|
2384
|
+
* destination by parsing your XML.
|
|
2385
|
+
*
|
|
2386
|
+
* Before transmission the document is validated against the complete official
|
|
2387
|
+
* OpenPeppol rulebooks. A document violating a `fatal` rule is refused and is
|
|
2388
|
+
* NOT sent; the error names the rule.
|
|
2360
2389
|
*
|
|
2361
2390
|
* @example
|
|
2362
2391
|
* ```ts
|
|
@@ -2364,10 +2393,29 @@ ${validation.errors.map((e) => ` - ${e.field}: ${e.message}${e.suggestion ? ` (
|
|
|
2364
2393
|
* const result = await peppol.invoices.importFile({
|
|
2365
2394
|
* file: xmlBytes,
|
|
2366
2395
|
* filename: "invoice.xml",
|
|
2396
|
+
* to: { peppolId: "0208:0685660237" },
|
|
2367
2397
|
* });
|
|
2368
2398
|
* console.log(result.id, result.status);
|
|
2369
2399
|
* ```
|
|
2370
|
-
*
|
|
2400
|
+
*
|
|
2401
|
+
* @throws {PeppolApiError} 400 — `invalid_base64`, or a missing `file` /
|
|
2402
|
+
* `filename`. `missing_recipient` when `to.peppolId` is absent.
|
|
2403
|
+
* @throws {PeppolApiError} 422 — the document was refused and NOT sent. Two
|
|
2404
|
+
* families, and they do NOT retry the same way:
|
|
2405
|
+
*
|
|
2406
|
+
* - **The document was rejected** (`validation_failed`, `not_ubl_document`,
|
|
2407
|
+
* `document_too_complex`, `undecodable_document`, `unsupported_encoding`).
|
|
2408
|
+
* Terminal: the same bytes fail identically forever. Fix the document —
|
|
2409
|
+
* retrying is pure waste, and `validation_failed` names the rule.
|
|
2410
|
+
* - **The account may not send right now** (`peppol_identity_incomplete`,
|
|
2411
|
+
* `peppol_identity_not_verified`, `platform_billing_not_active`,
|
|
2412
|
+
* `production_access_expired`). ⛔ NOT terminal: these describe account
|
|
2413
|
+
* state, and account state changes — a verification completes, a contract
|
|
2414
|
+
* is activated. The identical document will go through once it does.
|
|
2415
|
+
*
|
|
2416
|
+
* Treating the second family as terminal costs a customer a real invoice;
|
|
2417
|
+
* treating the first as retryable costs an infinite loop. See the API
|
|
2418
|
+
* reference for the full list.
|
|
2371
2419
|
*/
|
|
2372
2420
|
async importFile(options) {
|
|
2373
2421
|
return this.adapter.importInvoice(options);
|
|
@@ -3174,7 +3222,7 @@ var brCo16 = (input) => {
|
|
|
3174
3222
|
}
|
|
3175
3223
|
return [];
|
|
3176
3224
|
};
|
|
3177
|
-
var
|
|
3225
|
+
var brS05 = (input) => {
|
|
3178
3226
|
const violations = [];
|
|
3179
3227
|
if (!input.lines)
|
|
3180
3228
|
return violations;
|
|
@@ -3182,43 +3230,43 @@ var brS01 = (input) => {
|
|
|
3182
3230
|
const line = input.lines[i];
|
|
3183
3231
|
const category = line.vatCategory ?? "S";
|
|
3184
3232
|
if (category === "S" && (line.vatRate === void 0 || line.vatRate <= 0)) {
|
|
3185
|
-
violations.push(violation("BR-S-
|
|
3233
|
+
violations.push(violation("BR-S-05", "error", `Line ${i}: standard rate (S) requires vatRate > 0, got ${line.vatRate ?? "undefined"}.`, `lines[${i}].vatRate`));
|
|
3186
3234
|
}
|
|
3187
3235
|
}
|
|
3188
3236
|
return violations;
|
|
3189
3237
|
};
|
|
3190
|
-
var
|
|
3238
|
+
var brZ05 = (input) => {
|
|
3191
3239
|
const violations = [];
|
|
3192
3240
|
if (!input.lines)
|
|
3193
3241
|
return violations;
|
|
3194
3242
|
for (let i = 0; i < input.lines.length; i++) {
|
|
3195
3243
|
const line = input.lines[i];
|
|
3196
3244
|
if (line.vatCategory === "Z" && line.vatRate !== 0) {
|
|
3197
|
-
violations.push(violation("BR-
|
|
3245
|
+
violations.push(violation("BR-Z-05", "error", `Line ${i}: zero-rated (Z) requires vatRate = 0, got ${line.vatRate}.`, `lines[${i}].vatRate`));
|
|
3198
3246
|
}
|
|
3199
3247
|
}
|
|
3200
3248
|
return violations;
|
|
3201
3249
|
};
|
|
3202
|
-
var
|
|
3250
|
+
var brE05 = (input) => {
|
|
3203
3251
|
const violations = [];
|
|
3204
3252
|
if (!input.lines)
|
|
3205
3253
|
return violations;
|
|
3206
3254
|
for (let i = 0; i < input.lines.length; i++) {
|
|
3207
3255
|
const line = input.lines[i];
|
|
3208
3256
|
if (line.vatCategory === "E" && line.vatRate !== 0) {
|
|
3209
|
-
violations.push(violation("BR-
|
|
3257
|
+
violations.push(violation("BR-E-05", "error", `Line ${i}: exempt (E) requires vatRate = 0, got ${line.vatRate}.`, `lines[${i}].vatRate`));
|
|
3210
3258
|
}
|
|
3211
3259
|
}
|
|
3212
3260
|
return violations;
|
|
3213
3261
|
};
|
|
3214
|
-
var
|
|
3262
|
+
var brAe05 = (input) => {
|
|
3215
3263
|
const violations = [];
|
|
3216
3264
|
if (!input.lines)
|
|
3217
3265
|
return violations;
|
|
3218
3266
|
for (let i = 0; i < input.lines.length; i++) {
|
|
3219
3267
|
const line = input.lines[i];
|
|
3220
3268
|
if (line.vatCategory === "AE" && line.vatRate !== 0) {
|
|
3221
|
-
violations.push(violation("BR-
|
|
3269
|
+
violations.push(violation("BR-AE-05", "error", `Line ${i}: reverse charge (AE) requires vatRate = 0, got ${line.vatRate}.`, `lines[${i}].vatRate`));
|
|
3222
3270
|
}
|
|
3223
3271
|
}
|
|
3224
3272
|
return violations;
|
|
@@ -3281,11 +3329,11 @@ var ALL_RULES = [
|
|
|
3281
3329
|
brCo13,
|
|
3282
3330
|
brCo15,
|
|
3283
3331
|
brCo16,
|
|
3284
|
-
// Tax categories (
|
|
3285
|
-
brS01,
|
|
3332
|
+
// Tax categories (one family per category)
|
|
3286
3333
|
brS05,
|
|
3287
|
-
|
|
3288
|
-
|
|
3334
|
+
brZ05,
|
|
3335
|
+
brE05,
|
|
3336
|
+
brAe05,
|
|
3289
3337
|
// Peppol-specific
|
|
3290
3338
|
peppolR004,
|
|
3291
3339
|
vatCategoryCodes,
|
|
@@ -3306,10 +3354,31 @@ function validateSchematron(input) {
|
|
|
3306
3354
|
}
|
|
3307
3355
|
return {
|
|
3308
3356
|
valid: errors.length === 0,
|
|
3357
|
+
coverage: { rulesChecked: SDK_SCHEMATRON_RULE_IDS.length, ofNetworkFatalRules: "partial" },
|
|
3309
3358
|
errors,
|
|
3310
3359
|
warnings
|
|
3311
3360
|
};
|
|
3312
3361
|
}
|
|
3362
|
+
var SDK_SCHEMATRON_RULE_IDS = [
|
|
3363
|
+
"BR-02",
|
|
3364
|
+
"BR-03",
|
|
3365
|
+
"BR-06",
|
|
3366
|
+
"BR-07",
|
|
3367
|
+
"BR-08",
|
|
3368
|
+
"BR-09",
|
|
3369
|
+
"BR-10",
|
|
3370
|
+
"BR-CL-17",
|
|
3371
|
+
"BR-CO-10",
|
|
3372
|
+
"BR-CO-13",
|
|
3373
|
+
"BR-CO-15",
|
|
3374
|
+
"BR-CO-16",
|
|
3375
|
+
"BR-S-05",
|
|
3376
|
+
"BR-Z-05",
|
|
3377
|
+
"BR-E-05",
|
|
3378
|
+
"BR-AE-05",
|
|
3379
|
+
"PEPPOL-EN16931-R004",
|
|
3380
|
+
"PEPPOL-EN16931-R080"
|
|
3381
|
+
];
|
|
3313
3382
|
|
|
3314
3383
|
// src/commands/validate.ts
|
|
3315
3384
|
function runValidation(input) {
|