beliq-cli 0.2.1 → 0.3.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/README.md +16 -8
- package/dist/args.js +2 -2
- package/dist/cli.js +30 -4
- package/dist/commands/convert.js +6 -2
- package/dist/commands/generate.js +15 -2
- package/dist/commands/validate.js +19 -8
- package/dist/config.js +5 -2
- package/dist/exit.js +5 -1
- package/dist/help.js +18 -6
- package/dist/index.js +2 -1
- package/examples/invalid-xrechnung.xml +94 -0
- package/examples/invoice.json +64 -0
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -17,14 +17,18 @@ Requires Node.js >= 20.15.
|
|
|
17
17
|
## Usage
|
|
18
18
|
|
|
19
19
|
```
|
|
20
|
-
beliq validate <file|dir|-> [<file|dir> ...] [--format auto|cii|ubl] [--fail-on error|warning] [--json]
|
|
21
|
-
beliq generate <invoice.json|-> --standard xrechnung|zugferd|facturx|peppol-bis [--pdf] [--facturx-profile
|
|
22
|
-
beliq parse <file|-> [--format auto|cii|ubl] [--json]
|
|
23
|
-
beliq convert <file|-> --target-format cii|ubl|zugferd|facturx|xrechnung|peppol-bis [--source-format
|
|
20
|
+
beliq validate <file|dir|-> [<file|dir> ...] [--format auto|cii|ubl] [--fail-on error|warning] [--france-ctc] [--content-type <type>] [--json]
|
|
21
|
+
beliq generate <invoice.json|-> --standard xrechnung|zugferd|facturx|peppol-bis [--pdf] [--facturx-profile basicwl|en16931|extended|extended-ctc-fr] [--no-verify] [--seal] [--output <file>] [--json]
|
|
22
|
+
beliq parse <file|-> [--format auto|cii|ubl] [--content-type <type>] [--json]
|
|
23
|
+
beliq convert <file|-> --target-format cii|ubl|zugferd|facturx|xrechnung|peppol-bis [--source-format auto|cii|ubl|zugferd|facturx|xrechnung|peppol-bis] [--target-profile basicwl|en16931|extended|extended-ctc-fr] [--content-type <type>] [--output <file>] [--json]
|
|
24
24
|
beliq me [--json]
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
`
|
|
27
|
+
`--facturx-profile` applies to `zugferd` and `facturx`; `extended-ctc-fr` is Factur-X only. `generate --seal` also returns the document's sha256 and its validation verdict, so you can prove which rules the document passed. `--content-type` sets the input's media type for `validate`, `parse` and `convert`; without it, input starting with `%PDF-` is sent as `application/pdf` and anything else as `application/xml`.
|
|
28
|
+
|
|
29
|
+
`generate --pdf` returns a hybrid PDF/A-3 with the XML embedded for `zugferd` and `facturx`. `xrechnung` and `peppol-bis` have no hybrid form, so `--pdf` returns a visualization with no XML inside it, and their legal document stays the XML.
|
|
30
|
+
|
|
31
|
+
`validate` accepts an XML document or a ZUGFeRD/Factur-X PDF. A file argument of `-` reads from stdin. Pass several files, a shell glob, or a directory (its `.xml`/`.pdf` files, recursively) to validate a batch: you get a per-file verdict, a summary, and one exit code for the whole run. A directory is a batch even when it holds one file, so its `--json` shape does not depend on how many invoices it contains. `--france-ctc` also applies the French CTC rules (BR-FR-CTC) to each document. `--json` prints the raw API result (a report object in batch mode) as the only thing on stdout, so it pipes cleanly.
|
|
28
32
|
|
|
29
33
|
```bash
|
|
30
34
|
# Validate a file, human-readable
|
|
@@ -39,8 +43,10 @@ beliq validate ./invoices
|
|
|
39
43
|
# Validate from a pipe, machine-readable, fail the shell on any warning too
|
|
40
44
|
cat invoice.xml | beliq validate - --json --fail-on warning
|
|
41
45
|
|
|
42
|
-
# Generate an XRechnung from
|
|
43
|
-
|
|
46
|
+
# Generate an XRechnung from the example invoice, write the XML to stdout.
|
|
47
|
+
# The example ships in the npm package; in a clone of this repository it is
|
|
48
|
+
# examples/invoice.json.
|
|
49
|
+
beliq generate "$(npm root -g)/beliq-cli/examples/invoice.json" --standard xrechnung > invoice.xml
|
|
44
50
|
|
|
45
51
|
# Convert a CII document to a Peppol BIS UBL document
|
|
46
52
|
beliq convert invoice.xml --target-format peppol-bis --output peppol.xml
|
|
@@ -65,11 +71,13 @@ The exit code is the contract that makes it useful in scripts and CI:
|
|
|
65
71
|
| 0 | success, or a valid document |
|
|
66
72
|
| 1 | document invalid (`validate`, per `--fail-on`) |
|
|
67
73
|
| 2 | usage error (bad flag, missing argument, missing API key, PDF without `--output`) |
|
|
68
|
-
| 3 | beliq API error (bad key, quota, engine, a rejected document) |
|
|
74
|
+
| 3 | beliq API error (bad key, quota, engine, a rejected document), the API could not be reached (DNS, a refused connection, a timeout), or an unexpected error. Never a verdict on the document. |
|
|
69
75
|
| 4 | I/O error (unreadable input, or an output path that already exists) |
|
|
70
76
|
|
|
71
77
|
In batch mode (many files or a directory) the code covers the whole run: `0` if every file passes, `1` if a document fails `--fail-on`, `3` if any file could not be checked (unreadable, or the API errored on it).
|
|
72
78
|
|
|
79
|
+
On an API error the message goes to stderr, followed by the failed rules when the API names them (`generate` refuses a document that fails its own validation with a 422 that lists them). With `--json`, stdout also gets the error as `{ "error": { "status", "code", "message", "details" } }`.
|
|
80
|
+
|
|
73
81
|
## In CI
|
|
74
82
|
|
|
75
83
|
Validate every invoice a build produces and fail on a non-compliant one:
|
package/dist/args.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { UsageError } from './errors.js';
|
|
2
2
|
/** Flags that take no value. */
|
|
3
|
-
const BOOLEAN_FLAGS = new Set(['json', 'pdf', 'no-verify', 'france-ctc', 'seal']);
|
|
3
|
+
export const BOOLEAN_FLAGS = new Set(['json', 'pdf', 'no-verify', 'france-ctc', 'seal']);
|
|
4
4
|
/** Flags that take a value (`--flag value` or `--flag=value`). */
|
|
5
|
-
const VALUE_FLAGS = new Set([
|
|
5
|
+
export const VALUE_FLAGS = new Set([
|
|
6
6
|
'format',
|
|
7
7
|
'target-format',
|
|
8
8
|
'source-format',
|
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { resolveConfig } from './config.js';
|
|
|
4
4
|
import { EXIT } from './exit.js';
|
|
5
5
|
import { IoError, UsageError } from './errors.js';
|
|
6
6
|
import { HELP, version } from './help.js';
|
|
7
|
+
import { renderValidationHuman } from './format/validate.js';
|
|
7
8
|
import { runValidate } from './commands/validate.js';
|
|
8
9
|
import { runGenerate } from './commands/generate.js';
|
|
9
10
|
import { runParse } from './commands/parse.js';
|
|
@@ -63,11 +64,36 @@ export async function main(argv, io, env = process.env) {
|
|
|
63
64
|
return EXIT.IO;
|
|
64
65
|
}
|
|
65
66
|
if (err instanceof BeliqApiError) {
|
|
66
|
-
|
|
67
|
-
io.stderr(`beliq: API error ${err.status}${code}: ${err.message}`);
|
|
67
|
+
reportApiError(err, args, io);
|
|
68
68
|
return EXIT.API;
|
|
69
69
|
}
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
// Mostly a transport failure (DNS, a refused connection, TLS), which the SDK
|
|
71
|
+
// rethrows as fetch's own TypeError. None of these is a verdict on the
|
|
72
|
+
// document, and exit 1 is the "document invalid" code a CI gate reads.
|
|
73
|
+
io.stderr(`beliq: unexpected error: ${describeError(err)}`);
|
|
74
|
+
return EXIT.API;
|
|
72
75
|
}
|
|
73
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* An API error on stderr, followed by the rules a rejected document failed when
|
|
79
|
+
* the API names them (a 422 from generate carries its validation result in
|
|
80
|
+
* `details`). With --json the error object goes to stdout as well, so a script
|
|
81
|
+
* reading stdout gets a payload on failure too.
|
|
82
|
+
*/
|
|
83
|
+
function reportApiError(err, args, io) {
|
|
84
|
+
const code = err.code ? ` (${err.code})` : '';
|
|
85
|
+
io.stderr(`beliq: API error ${err.status}${code}: ${err.message}`);
|
|
86
|
+
const result = err.details?.validationResult;
|
|
87
|
+
if (result && typeof result === 'object')
|
|
88
|
+
io.stderr(renderValidationHuman(result));
|
|
89
|
+
if (args.flags.json === true) {
|
|
90
|
+
const error = { status: err.status, code: err.code, message: err.message, details: err.details };
|
|
91
|
+
io.stdout(`${JSON.stringify({ error }, null, 2)}\n`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** The message plus its cause: fetch reports only "fetch failed" and keeps the reason in `cause`. */
|
|
95
|
+
function describeError(err) {
|
|
96
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
97
|
+
const cause = err instanceof Error && err.cause instanceof Error ? err.cause.message : undefined;
|
|
98
|
+
return cause ? `${message} (${cause})` : message;
|
|
99
|
+
}
|
package/dist/commands/convert.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LIVE_CONVERT_SOURCE_FORMATS, LIVE_CONVERT_TARGET_FORMATS, } from '@beliq/sdk';
|
|
1
|
+
import { LIVE_CONVERT_SOURCE_FORMATS, LIVE_CONVERT_TARGET_FORMATS, LIVE_PROFILES, } from '@beliq/sdk';
|
|
2
2
|
import { flagStr, oneOf, requirePositional } from '../args.js';
|
|
3
3
|
import { UsageError } from '../errors.js';
|
|
4
4
|
import { emitDocument } from './emit.js';
|
|
@@ -17,11 +17,15 @@ export async function runConvert(args, deps, io) {
|
|
|
17
17
|
throw new UsageError(`--target-format is required (one of: ${LIVE_CONVERT_TARGET_FORMATS.join(', ')})`);
|
|
18
18
|
}
|
|
19
19
|
const sourceFormat = oneOf(flagStr(args, 'source-format'), LIVE_CONVERT_SOURCE_FORMATS, 'source-format');
|
|
20
|
+
// The flat list, not the per-standard one generate checks: the engine's
|
|
21
|
+
// convert route resolves a profile the same way for both hybrid targets, and
|
|
22
|
+
// the API ignores it for the others.
|
|
23
|
+
const targetProfile = oneOf(flagStr(args, 'target-profile'), LIVE_PROFILES, 'target-profile');
|
|
20
24
|
const bytes = await io.readInput(file);
|
|
21
25
|
const result = await deps.client.convert(bytes, {
|
|
22
26
|
targetFormat: targetFormat,
|
|
23
27
|
sourceFormat: sourceFormat,
|
|
24
|
-
targetProfile:
|
|
28
|
+
targetProfile: targetProfile,
|
|
25
29
|
contentType: flagStr(args, 'content-type'),
|
|
26
30
|
});
|
|
27
31
|
const kind = PDF_TARGETS.has(targetFormat) ? 'pdf' : 'xml';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LIVE_GENERATE_STANDARDS } from '@beliq/sdk';
|
|
1
|
+
import { LIVE_GENERATE_STANDARDS, isProfileAllowedForStandard, profilesForStandard, } from '@beliq/sdk';
|
|
2
2
|
import { flagBool, flagStr, oneOf, requirePositional } from '../args.js';
|
|
3
3
|
import { UsageError } from '../errors.js';
|
|
4
4
|
import { emitDocument } from './emit.js';
|
|
@@ -13,6 +13,15 @@ export async function runGenerate(args, deps, io) {
|
|
|
13
13
|
if (!standard) {
|
|
14
14
|
throw new UsageError(`--standard is required (one of: ${LIVE_GENERATE_STANDARDS.join(', ')})`);
|
|
15
15
|
}
|
|
16
|
+
// The SDK drops --facturx-profile outside the Factur-X / ZUGFeRD family.
|
|
17
|
+
// Inside it the pair is pinned (extended-ctc-fr is Factur-X only) and the API
|
|
18
|
+
// answers a wrong one with 422, so fail as a usage error before the call.
|
|
19
|
+
const facturxProfile = flagStr(args, 'facturx-profile');
|
|
20
|
+
if (facturxProfile !== undefined &&
|
|
21
|
+
(standard === 'facturx' || standard === 'zugferd') &&
|
|
22
|
+
!isProfileAllowedForStandard(standard, facturxProfile)) {
|
|
23
|
+
throw new UsageError(`--facturx-profile for ${standard} must be one of: ${profilesForStandard(standard).join(', ')}`);
|
|
24
|
+
}
|
|
16
25
|
const raw = await io.readInput(file);
|
|
17
26
|
let invoice;
|
|
18
27
|
try {
|
|
@@ -28,7 +37,11 @@ export async function runGenerate(args, deps, io) {
|
|
|
28
37
|
standard,
|
|
29
38
|
invoice,
|
|
30
39
|
output,
|
|
31
|
-
facturxProfile:
|
|
40
|
+
facturxProfile: facturxProfile,
|
|
41
|
+
// XRechnung and Peppol BIS have no hybrid PDF, and the API refuses PDF for
|
|
42
|
+
// them unless the request names a visual to render. Factur-X and ZUGFeRD
|
|
43
|
+
// render theirs either way, so this is inert for them.
|
|
44
|
+
template: output === 'pdf' ? 'standard' : undefined,
|
|
32
45
|
verify: !flagBool(args, 'no-verify'),
|
|
33
46
|
seal,
|
|
34
47
|
});
|
|
@@ -10,15 +10,19 @@ const FATAL_API_STATUSES = new Set([401, 403, 429]);
|
|
|
10
10
|
/**
|
|
11
11
|
* Validate one or many documents against beliq's authority-pinned rules.
|
|
12
12
|
*
|
|
13
|
-
* One
|
|
14
|
-
*
|
|
15
|
-
* batch: a per-file verdict, an aggregate summary, and a batch exit code (0
|
|
16
|
-
* pass, 1 some document fails --fail-on, 3 some file could not be checked).
|
|
13
|
+
* One file (or `-`) named on its own keeps the classic per-document output.
|
|
14
|
+
* Multiple inputs, or a directory (its `.xml`/`.pdf` files, recursively), switch
|
|
15
|
+
* to a batch: a per-file verdict, an aggregate summary, and a batch exit code (0
|
|
16
|
+
* all pass, 1 some document fails --fail-on, 3 some file could not be checked).
|
|
17
|
+
* A directory stays a batch even when it holds one invoice, so the --json shape
|
|
18
|
+
* follows what was asked for rather than how many files were found.
|
|
17
19
|
*/
|
|
18
20
|
export async function runValidate(args, deps, io) {
|
|
19
21
|
const format = oneOf(flagStr(args, 'format'), LIVE_VALIDATE_FORMATS, 'format');
|
|
20
22
|
const failOn = (oneOf(flagStr(args, 'fail-on'), FAIL_ON, 'fail-on') ?? 'error');
|
|
21
23
|
const contentType = flagStr(args, 'content-type');
|
|
24
|
+
// Sent only when set: the API's own default decides otherwise.
|
|
25
|
+
const franceCtc = flagBool(args, 'france-ctc') ? true : undefined;
|
|
22
26
|
const json = flagBool(args, 'json');
|
|
23
27
|
if (args.positionals.length === 0) {
|
|
24
28
|
throw new UsageError('missing file argument. Usage: beliq validate <file|dir|-> [<file|dir> ...]');
|
|
@@ -30,19 +34,26 @@ export async function runValidate(args, deps, io) {
|
|
|
30
34
|
if (files.length > 1 && files.includes('-')) {
|
|
31
35
|
throw new UsageError('cannot mix "-" (stdin) with other inputs');
|
|
32
36
|
}
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
// expandInputs returns a named file as given and a directory as the files
|
|
38
|
+
// inside it, so a single input that comes back unchanged was a file.
|
|
39
|
+
const single = args.positionals.length === 1 && files.length === 1 && files[0] === args.positionals[0];
|
|
40
|
+
if (single) {
|
|
41
|
+
const result = await deps.client.validate(await io.readInput(files[0]), { format, contentType, franceCtc });
|
|
35
42
|
io.stdout(json ? `${JSON.stringify(result, null, 2)}\n` : `${renderValidationHuman(result)}\n`);
|
|
36
43
|
return computeExitCode(result, failOn);
|
|
37
44
|
}
|
|
38
|
-
return runBatch(files, { format, contentType, failOn, json }, deps, io);
|
|
45
|
+
return runBatch(files, { format, contentType, franceCtc, failOn, json }, deps, io);
|
|
39
46
|
}
|
|
40
47
|
async function runBatch(files, opts, deps, io) {
|
|
41
48
|
const rows = [];
|
|
42
49
|
for (const file of files) {
|
|
43
50
|
try {
|
|
44
51
|
const bytes = await io.readInput(file);
|
|
45
|
-
const result = await deps.client.validate(bytes, {
|
|
52
|
+
const result = await deps.client.validate(bytes, {
|
|
53
|
+
format: opts.format,
|
|
54
|
+
contentType: opts.contentType,
|
|
55
|
+
franceCtc: opts.franceCtc,
|
|
56
|
+
});
|
|
46
57
|
rows.push({ file, result, fails: computeExitCode(result, opts.failOn) !== EXIT.OK });
|
|
47
58
|
}
|
|
48
59
|
catch (err) {
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DEFAULT_BASE_URL } from '@beliq/sdk';
|
|
2
|
-
import { flagStr } from './args.js';
|
|
2
|
+
import { flagStr, oneOf } from './args.js';
|
|
3
3
|
import { UsageError } from './errors.js';
|
|
4
|
+
const AUTH_MODES = ['header', 'bearer'];
|
|
4
5
|
/**
|
|
5
6
|
* Resolve the typed config with precedence flag > env > default. Throws a
|
|
6
7
|
* UsageError if no API key is available so the CLI reports a clean usage error
|
|
@@ -13,7 +14,9 @@ export function resolveConfig(args, env = process.env) {
|
|
|
13
14
|
throw new UsageError('no API key. Set BELIQ_API_KEY or pass --api-key. Create a key in the beliq dashboard under API Keys.');
|
|
14
15
|
}
|
|
15
16
|
const baseUrl = (flagStr(args, 'base-url') ?? env.BELIQ_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
17
|
+
// An unknown mode is refused rather than read as `header`: a typo in
|
|
18
|
+
// `bearer` would otherwise send the key a way the caller did not ask for.
|
|
16
19
|
const authRaw = (flagStr(args, 'auth') ?? env.BELIQ_AUTH ?? '').trim().toLowerCase();
|
|
17
|
-
const auth = authRaw
|
|
20
|
+
const auth = oneOf(authRaw || undefined, AUTH_MODES, 'auth (or BELIQ_AUTH)', 'header');
|
|
18
21
|
return { apiKey, baseUrl, auth };
|
|
19
22
|
}
|
package/dist/exit.js
CHANGED
|
@@ -5,7 +5,11 @@ export const EXIT = {
|
|
|
5
5
|
INVALID: 1,
|
|
6
6
|
/** A usage problem: bad flag, missing argument, missing API key, PDF without --output. */
|
|
7
7
|
USAGE: 2,
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* The beliq API returned an error (bad key, quota, engine, a rejected
|
|
10
|
+
* document), could not be reached, or something unexpected failed. Never a
|
|
11
|
+
* verdict on the document.
|
|
12
|
+
*/
|
|
9
13
|
API: 3,
|
|
10
14
|
/** A local I/O error: an unreadable input or an output path that already exists. */
|
|
11
15
|
IO: 4,
|
package/dist/help.js
CHANGED
|
@@ -7,24 +7,36 @@ export function version() {
|
|
|
7
7
|
export const HELP = `beliq: validate, generate, parse, and convert EU e-invoices
|
|
8
8
|
|
|
9
9
|
Usage:
|
|
10
|
-
beliq validate <file|dir|-> [<file|dir> ...] [--format auto|cii|ubl] [--fail-on error|warning] [--json]
|
|
11
|
-
beliq generate <invoice.json|-> --standard xrechnung|zugferd|facturx|peppol-bis [--pdf] [--facturx-profile
|
|
12
|
-
beliq parse <file|-> [--format auto|cii|ubl] [--json]
|
|
13
|
-
beliq convert <file|-> --target-format cii|ubl|zugferd|facturx|xrechnung|peppol-bis [--source-format
|
|
10
|
+
beliq validate <file|dir|-> [<file|dir> ...] [--format auto|cii|ubl] [--fail-on error|warning] [--france-ctc] [--content-type <type>] [--json]
|
|
11
|
+
beliq generate <invoice.json|-> --standard xrechnung|zugferd|facturx|peppol-bis [--pdf] [--facturx-profile basicwl|en16931|extended|extended-ctc-fr] [--no-verify] [--seal] [--output <file>] [--json]
|
|
12
|
+
beliq parse <file|-> [--format auto|cii|ubl] [--content-type <type>] [--json]
|
|
13
|
+
beliq convert <file|-> --target-format cii|ubl|zugferd|facturx|xrechnung|peppol-bis [--source-format auto|cii|ubl|zugferd|facturx|xrechnung|peppol-bis] [--target-profile basicwl|en16931|extended|extended-ctc-fr] [--content-type <type>] [--output <file>] [--json]
|
|
14
14
|
beliq me [--json]
|
|
15
15
|
|
|
16
16
|
validate accepts an XML document or a ZUGFeRD/Factur-X PDF. Pass several files,
|
|
17
17
|
a shell glob, or a directory (its .xml/.pdf files, recursively) to validate a
|
|
18
|
-
batch: a per-file verdict, a summary, and one exit code for the whole run.
|
|
18
|
+
batch: a per-file verdict, a summary, and one exit code for the whole run. A
|
|
19
|
+
directory is a batch even when it holds one file. validate --france-ctc also
|
|
20
|
+
applies the French CTC rules (BR-FR-CTC).
|
|
21
|
+
|
|
22
|
+
generate --facturx-profile applies to zugferd and facturx; extended-ctc-fr is
|
|
23
|
+
Factur-X only.
|
|
19
24
|
|
|
20
25
|
generate --seal also returns the document sha256 and validation verdict, so you
|
|
21
26
|
can prove which rules the document passed; hashing the output reproduces it.
|
|
22
27
|
|
|
28
|
+
--pdf gives a hybrid PDF/A-3 with the XML embedded for zugferd and facturx.
|
|
29
|
+
xrechnung and peppol-bis have no hybrid form, so --pdf returns a visualization
|
|
30
|
+
with no XML inside it, and their legal document stays the XML.
|
|
31
|
+
|
|
23
32
|
Global options:
|
|
24
33
|
--api-key <key> beliq API key (default: BELIQ_API_KEY)
|
|
25
34
|
--base-url <url> API base URL (default: BELIQ_BASE_URL, else https://api.beliq.eu)
|
|
26
35
|
--auth <mode> header (default, X-API-Key) or bearer
|
|
27
36
|
--json machine-readable JSON on stdout
|
|
37
|
+
--content-type <type>
|
|
38
|
+
the input's media type (default: application/pdf when the
|
|
39
|
+
bytes start with %PDF-, else application/xml)
|
|
28
40
|
-h, --help show this help
|
|
29
41
|
-v, --version show the version
|
|
30
42
|
|
|
@@ -32,7 +44,7 @@ Exit codes:
|
|
|
32
44
|
0 success, or a valid document
|
|
33
45
|
1 document invalid (validate, per --fail-on)
|
|
34
46
|
2 usage error
|
|
35
|
-
3 beliq API error
|
|
47
|
+
3 beliq API error, the API could not be reached, or an unexpected error
|
|
36
48
|
4 I/O error
|
|
37
49
|
|
|
38
50
|
In batch mode the code covers the whole run: 0 if every file passes, 1 if a
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { main } from './cli.js';
|
|
3
|
+
import { EXIT } from './exit.js';
|
|
3
4
|
import { nodeIO } from './io.js';
|
|
4
5
|
main(process.argv.slice(2), nodeIO()).then((code) => {
|
|
5
6
|
process.exitCode = code;
|
|
6
7
|
}, (err) => {
|
|
7
8
|
process.stderr.write(`beliq: fatal: ${err.message}\n`);
|
|
8
|
-
process.exitCode =
|
|
9
|
+
process.exitCode = EXIT.API;
|
|
9
10
|
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!-- A minimal CII (UN/CEFACT Cross Industry Invoice) carrying the XRechnung
|
|
3
|
+
guideline id but missing the Buyer reference (BT-10), which XRechnung
|
|
4
|
+
requires (rule BR-DE-15). Used as the known-invalid example in the gated
|
|
5
|
+
live test. -->
|
|
6
|
+
<rsm:CrossIndustryInvoice
|
|
7
|
+
xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
|
|
8
|
+
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
|
|
9
|
+
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
|
|
10
|
+
<rsm:ExchangedDocumentContext>
|
|
11
|
+
<ram:GuidelineSpecifiedDocumentContextParameter>
|
|
12
|
+
<ram:ID>urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0</ram:ID>
|
|
13
|
+
</ram:GuidelineSpecifiedDocumentContextParameter>
|
|
14
|
+
</rsm:ExchangedDocumentContext>
|
|
15
|
+
<rsm:ExchangedDocument>
|
|
16
|
+
<ram:ID>INV-2026-001</ram:ID>
|
|
17
|
+
<ram:TypeCode>380</ram:TypeCode>
|
|
18
|
+
<ram:IssueDateTime>
|
|
19
|
+
<udt:DateTimeString format="102">20260115</udt:DateTimeString>
|
|
20
|
+
</ram:IssueDateTime>
|
|
21
|
+
</rsm:ExchangedDocument>
|
|
22
|
+
<rsm:SupplyChainTradeTransaction>
|
|
23
|
+
<ram:IncludedSupplyChainTradeLineItem>
|
|
24
|
+
<ram:AssociatedDocumentLineDocument>
|
|
25
|
+
<ram:LineID>1</ram:LineID>
|
|
26
|
+
</ram:AssociatedDocumentLineDocument>
|
|
27
|
+
<ram:SpecifiedTradeProduct>
|
|
28
|
+
<ram:Name>Consulting</ram:Name>
|
|
29
|
+
</ram:SpecifiedTradeProduct>
|
|
30
|
+
<ram:SpecifiedLineTradeAgreement>
|
|
31
|
+
<ram:NetPriceProductTradePrice>
|
|
32
|
+
<ram:ChargeAmount>100.00</ram:ChargeAmount>
|
|
33
|
+
</ram:NetPriceProductTradePrice>
|
|
34
|
+
</ram:SpecifiedLineTradeAgreement>
|
|
35
|
+
<ram:SpecifiedLineTradeDelivery>
|
|
36
|
+
<ram:BilledQuantity unitCode="HUR">10</ram:BilledQuantity>
|
|
37
|
+
</ram:SpecifiedLineTradeDelivery>
|
|
38
|
+
<ram:SpecifiedLineTradeSettlement>
|
|
39
|
+
<ram:ApplicableTradeTax>
|
|
40
|
+
<ram:TypeCode>VAT</ram:TypeCode>
|
|
41
|
+
<ram:CategoryCode>S</ram:CategoryCode>
|
|
42
|
+
<ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
|
|
43
|
+
</ram:ApplicableTradeTax>
|
|
44
|
+
<ram:SpecifiedTradeSettlementLineMonetarySummation>
|
|
45
|
+
<ram:LineTotalAmount>1000.00</ram:LineTotalAmount>
|
|
46
|
+
</ram:SpecifiedTradeSettlementLineMonetarySummation>
|
|
47
|
+
</ram:SpecifiedLineTradeSettlement>
|
|
48
|
+
</ram:IncludedSupplyChainTradeLineItem>
|
|
49
|
+
<ram:ApplicableHeaderTradeAgreement>
|
|
50
|
+
<ram:SellerTradeParty>
|
|
51
|
+
<ram:Name>Seller GmbH</ram:Name>
|
|
52
|
+
<ram:PostalTradeAddress>
|
|
53
|
+
<ram:PostcodeCode>10115</ram:PostcodeCode>
|
|
54
|
+
<ram:CityName>Berlin</ram:CityName>
|
|
55
|
+
<ram:CountryID>DE</ram:CountryID>
|
|
56
|
+
</ram:PostalTradeAddress>
|
|
57
|
+
<ram:SpecifiedTaxRegistration>
|
|
58
|
+
<ram:ID schemeID="VA">DE123456789</ram:ID>
|
|
59
|
+
</ram:SpecifiedTaxRegistration>
|
|
60
|
+
</ram:SellerTradeParty>
|
|
61
|
+
<ram:BuyerTradeParty>
|
|
62
|
+
<ram:Name>Buyer GmbH</ram:Name>
|
|
63
|
+
<ram:PostalTradeAddress>
|
|
64
|
+
<ram:PostcodeCode>80331</ram:PostcodeCode>
|
|
65
|
+
<ram:CityName>Munich</ram:CityName>
|
|
66
|
+
<ram:CountryID>DE</ram:CountryID>
|
|
67
|
+
</ram:PostalTradeAddress>
|
|
68
|
+
</ram:BuyerTradeParty>
|
|
69
|
+
</ram:ApplicableHeaderTradeAgreement>
|
|
70
|
+
<ram:ApplicableHeaderTradeDelivery />
|
|
71
|
+
<ram:ApplicableHeaderTradeSettlement>
|
|
72
|
+
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
|
|
73
|
+
<ram:ApplicableTradeTax>
|
|
74
|
+
<ram:CalculatedAmount>190.00</ram:CalculatedAmount>
|
|
75
|
+
<ram:TypeCode>VAT</ram:TypeCode>
|
|
76
|
+
<ram:BasisAmount>1000.00</ram:BasisAmount>
|
|
77
|
+
<ram:CategoryCode>S</ram:CategoryCode>
|
|
78
|
+
<ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
|
|
79
|
+
</ram:ApplicableTradeTax>
|
|
80
|
+
<ram:SpecifiedTradePaymentTerms>
|
|
81
|
+
<ram:DueDateDateTime>
|
|
82
|
+
<udt:DateTimeString format="102">20260214</udt:DateTimeString>
|
|
83
|
+
</ram:DueDateDateTime>
|
|
84
|
+
</ram:SpecifiedTradePaymentTerms>
|
|
85
|
+
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
|
|
86
|
+
<ram:LineTotalAmount>1000.00</ram:LineTotalAmount>
|
|
87
|
+
<ram:TaxBasisTotalAmount>1000.00</ram:TaxBasisTotalAmount>
|
|
88
|
+
<ram:TaxTotalAmount currencyID="EUR">190.00</ram:TaxTotalAmount>
|
|
89
|
+
<ram:GrandTotalAmount>1190.00</ram:GrandTotalAmount>
|
|
90
|
+
<ram:DuePayableAmount>1190.00</ram:DuePayableAmount>
|
|
91
|
+
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
|
|
92
|
+
</ram:ApplicableHeaderTradeSettlement>
|
|
93
|
+
</rsm:SupplyChainTradeTransaction>
|
|
94
|
+
</rsm:CrossIndustryInvoice>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"number": "INV-2026-001",
|
|
3
|
+
"issueDate": "2026-01-15",
|
|
4
|
+
"currencyCode": "EUR",
|
|
5
|
+
"buyerReference": "04011000-12345-06",
|
|
6
|
+
"seller": {
|
|
7
|
+
"name": "Seller GmbH",
|
|
8
|
+
"vatId": "DE123456789",
|
|
9
|
+
"contactName": "Anna Muster",
|
|
10
|
+
"email": "billing@seller.example",
|
|
11
|
+
"phone": "+49 30 1234567",
|
|
12
|
+
"address": {
|
|
13
|
+
"street": "Hauptstr. 1",
|
|
14
|
+
"city": "Berlin",
|
|
15
|
+
"postalCode": "10115",
|
|
16
|
+
"countryCode": "DE"
|
|
17
|
+
},
|
|
18
|
+
"peppol": {
|
|
19
|
+
"schemeId": "9930",
|
|
20
|
+
"id": "DE123456789"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"buyer": {
|
|
24
|
+
"name": "Buyer GmbH",
|
|
25
|
+
"email": "ap@buyer.example",
|
|
26
|
+
"address": {
|
|
27
|
+
"street": "Marktweg 2",
|
|
28
|
+
"city": "Munich",
|
|
29
|
+
"postalCode": "80331",
|
|
30
|
+
"countryCode": "DE"
|
|
31
|
+
},
|
|
32
|
+
"vatId": "DE987654321",
|
|
33
|
+
"peppol": {
|
|
34
|
+
"schemeId": "9930",
|
|
35
|
+
"id": "DE987654321"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"lines": [
|
|
39
|
+
{
|
|
40
|
+
"description": "Consulting",
|
|
41
|
+
"quantity": 10,
|
|
42
|
+
"unitCode": "HUR",
|
|
43
|
+
"unitPrice": 100,
|
|
44
|
+
"lineTotal": 1000,
|
|
45
|
+
"vatRate": 19,
|
|
46
|
+
"vatCategoryCode": "S"
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
"taxSummary": [
|
|
50
|
+
{
|
|
51
|
+
"vatCategoryCode": "S",
|
|
52
|
+
"vatRate": 19,
|
|
53
|
+
"taxableAmount": 1000,
|
|
54
|
+
"taxAmount": 190
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"paymentMeans": {
|
|
58
|
+
"typeCode": "58",
|
|
59
|
+
"iban": "DE89370400440532013000"
|
|
60
|
+
},
|
|
61
|
+
"totalNetAmount": 1000,
|
|
62
|
+
"totalTaxAmount": 190,
|
|
63
|
+
"totalGrossAmount": 1190
|
|
64
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beliq-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "beliq CLI: validate, generate, parse, and convert EU e-invoices (XRechnung, ZUGFeRD, Factur-X, Peppol BIS) against authority-pinned, drift-checked rules, from the terminal or CI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"beliq": "dist/index.js"
|
|
38
38
|
},
|
|
39
39
|
"files": [
|
|
40
|
-
"dist"
|
|
40
|
+
"dist",
|
|
41
|
+
"examples"
|
|
41
42
|
],
|
|
42
43
|
"publishConfig": {
|
|
43
44
|
"provenance": true
|
|
@@ -56,12 +57,12 @@
|
|
|
56
57
|
"prepublishOnly": "npm run build"
|
|
57
58
|
},
|
|
58
59
|
"dependencies": {
|
|
59
|
-
"@beliq/sdk": "^0.
|
|
60
|
+
"@beliq/sdk": "^0.4.0"
|
|
60
61
|
},
|
|
61
62
|
"devDependencies": {
|
|
62
63
|
"@types/node": "^20.14.0",
|
|
63
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
64
|
-
"@typescript-eslint/parser": "^
|
|
64
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
65
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
65
66
|
"eslint": "^8.57.0",
|
|
66
67
|
"typescript": "^5.5.4",
|
|
67
68
|
"vitest": "^4.0.0"
|