beliq-cli 0.2.2 → 0.4.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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # beliq-cli
2
2
 
3
- Validate, generate, parse, and convert EU e-invoices (XRechnung, ZUGFeRD, Factur-X, Peppol BIS) from the terminal or CI, against beliq's authority-pinned, drift-checked rules.
3
+ Validate, generate, parse, and convert EU e-invoices (XRechnung, ZUGFeRD, Factur-X, Peppol BIS) from the terminal or CI, through the beliq API. Every verdict states how deep its check went.
4
4
 
5
- The headline use is **validating e-invoices in CI**: point it at your invoice files, and a non-compliant document fails the build.
5
+ The headline use is **validating e-invoices in CI**: point it at your invoice files, and an invalid document fails the build.
6
6
 
7
7
  ## Install
8
8
 
@@ -17,16 +17,38 @@ 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 <p>] [--no-verify] [--output <file>] [--json]
22
- beliq parse <file|-> [--format auto|cii|ubl] [--json]
23
- beliq convert <file|-> --target-format cii|ubl|zugferd|facturx|xrechnung|peppol-bis [--source-format <f>] [--target-profile <p>] [--output <file>] [--json]
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|fatturapa|facturae|eslog|ksef [--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
+ `--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
+
27
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.
28
30
 
29
- `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. `--json` prints the raw API result (a report object in batch mode) as the only thing on stdout, so it pipes cleanly.
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.
32
+
33
+ `convert` returns XML for every target, `facturx` and `zugferd` included: the API does not wrap an XML source into a hybrid PDF. A PDF comes back only when a hybrid PDF source already is the target, and it needs `--output`. A successful convert means the API validated the converted XML before returning it. For a `facturx` or `zugferd` target at profile `en16931` that validation is the one for plain EN 16931 CII: no Factur-X profile schema is checked. It prints no verdict and no check depth. When the engine recorded source elements with no target equivalent, the summary gives their count; a loss the engine does not detect is not counted.
34
+
35
+ `parse` prints a one-line summary and, below it, each warning the API sent about what the document holds and the parsed invoice does not.
36
+
37
+ ## Reading a verdict
38
+
39
+ ```
40
+ $ beliq validate invoice.xml
41
+ VALID fatturapa (profile italy-fatturapa-ordinaria-fpr12) Schema-checked
42
+ 0 errors, 0 warnings
43
+ ```
44
+
45
+ - `VALID` and `INVALID` are the API's `valid`. `valid` means the document passed the rule packs and schemas beliq runs for its format, in the versions the result names. It does not mean every recipient, validation tool or tax authority accepts it.
46
+ - The third field is the check depth, printed as the API sends it in `verificationBadgeLabel`. [How verification works](https://docs.beliq.eu/compliance/how-verification-works/) explains each depth. Where the API sends no label, the CLI prints `check depth not stated` and names no depth.
47
+ - For a PDF, a line says which embedded XML the verdict is about. The PDF itself is not checked.
48
+ - When the result carries `franceCtcBlockingRuleIds`, the CLI prints the API's message for them, with the rule ids, and fails the file with exit code 1 under every `--fail-on`, also where the verdict is `VALID`. The API reports those ids beside `valid` and does not fold them into it.
49
+ - The batch table has the same depth in its `DEPTH` column. `--json` carries every field of the API result.
50
+
51
+ `generate` names the same depth in its summary, and `not validated` with `--no-verify`. The verdict is about the invoice XML, so for `--pdf` the summary says so and names the kind of PDF.
30
52
 
31
53
  ```bash
32
54
  # Validate a file, human-readable
@@ -41,8 +63,10 @@ beliq validate ./invoices
41
63
  # Validate from a pipe, machine-readable, fail the shell on any warning too
42
64
  cat invoice.xml | beliq validate - --json --fail-on warning
43
65
 
44
- # Generate an XRechnung from a JSON invoice, write the XML to stdout
45
- beliq generate examples/invoice.json --standard xrechnung > invoice.xml
66
+ # Generate an XRechnung from the example invoice, write the XML to stdout.
67
+ # The example ships in the npm package; in a clone of this repository it is
68
+ # examples/invoice.json.
69
+ beliq generate "$(npm root -g)/beliq-cli/examples/invoice.json" --standard xrechnung > invoice.xml
46
70
 
47
71
  # Convert a CII document to a Peppol BIS UBL document
48
72
  beliq convert invoice.xml --target-format peppol-bis --output peppol.xml
@@ -65,16 +89,18 @@ The exit code is the contract that makes it useful in scripts and CI:
65
89
  | Code | Meaning |
66
90
  |---|---|
67
91
  | 0 | success, or a valid document |
68
- | 1 | document invalid (`validate`, per `--fail-on`) |
92
+ | 1 | document invalid (`validate`, per `--fail-on`), or a result that carries `franceCtcBlockingRuleIds` |
69
93
  | 2 | usage error (bad flag, missing argument, missing API key, PDF without `--output`) |
70
- | 3 | beliq API error (bad key, quota, engine, a rejected document) |
94
+ | 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. |
71
95
  | 4 | I/O error (unreadable input, or an output path that already exists) |
72
96
 
73
- 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).
97
+ 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` or carries `franceCtcBlockingRuleIds`, `3` if any file could not be checked (unreadable, or the API errored on it).
98
+
99
+ On an API error the message goes to stderr, followed by the failed rules when the API names them (`generate` and `convert` refuse a document that fails the API's validation with a 422 that lists them). With `--json`, stdout also gets the error as `{ "error": { "status", "code", "message", "details" } }`.
74
100
 
75
101
  ## In CI
76
102
 
77
- Validate every invoice a build produces and fail on a non-compliant one:
103
+ Validate every invoice a build produces and fail on an invalid one:
78
104
 
79
105
  ```yaml
80
106
  - run: npx beliq-cli validate dist/invoices --fail-on error
@@ -82,13 +108,15 @@ Validate every invoice a build produces and fail on a non-compliant one:
82
108
  BELIQ_API_KEY: ${{ secrets.BELIQ_API_KEY }}
83
109
  ```
84
110
 
85
- `beliq validate` takes a directory, so this validates every `.xml`/`.pdf` invoice under `dist/invoices` in one run and fails the step if any is non-compliant. For a per-file step summary and job outputs on GitHub, use the Action `beliq-eu/beliq-validate-action`.
111
+ `beliq validate` takes a directory, so this validates every `.xml`/`.pdf` invoice under `dist/invoices` in one run and fails the step if any is invalid. For a per-file step summary and job outputs on GitHub, use the Action `beliq-eu/beliq-validate-action`.
86
112
 
87
113
  ## Development
88
114
 
89
115
  ```bash
90
116
  npm install
91
117
  npm run build
118
+ npm run typecheck # src and tests
119
+ npm run lint # ESLint with type information, whole repository
92
120
  npm test # unit tests, no network
93
121
  npm run scrub:check # no em-dash
94
122
  BELIQ_API_KEY=... npm run test:integration # live smoke against the real API
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,37 @@ export async function main(argv, io, env = process.env) {
63
64
  return EXIT.IO;
64
65
  }
65
66
  if (err instanceof BeliqApiError) {
66
- const code = err.code ? ` (${err.code})` : '';
67
- io.stderr(`beliq: API error ${err.status}${code}: ${err.message}`);
67
+ reportApiError(err, args, io);
68
68
  return EXIT.API;
69
69
  }
70
- io.stderr(`beliq: unexpected error: ${err.message}`);
71
- return 1;
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 or convert carries the validation
80
+ * result of the document it refused in `details`). With --json the error
81
+ * object goes to stdout as well, so a script reading stdout gets a payload on
82
+ * failure too.
83
+ */
84
+ function reportApiError(err, args, io) {
85
+ const code = err.code ? ` (${err.code})` : '';
86
+ io.stderr(`beliq: API error ${err.status}${code}: ${err.message}`);
87
+ const result = err.details?.validationResult;
88
+ if (result && typeof result === 'object')
89
+ io.stderr(renderValidationHuman(result));
90
+ if (args.flags.json === true) {
91
+ const error = { status: err.status, code: err.code, message: err.message, details: err.details };
92
+ io.stdout(`${JSON.stringify({ error }, null, 2)}\n`);
93
+ }
94
+ }
95
+ /** The message plus its cause: fetch reports only "fetch failed" and keeps the reason in `cause`. */
96
+ function describeError(err) {
97
+ const message = err instanceof Error ? err.message : String(err);
98
+ const cause = err instanceof Error && err.cause instanceof Error ? err.cause.message : undefined;
99
+ return cause ? `${message} (${cause})` : message;
100
+ }
@@ -1,14 +1,18 @@
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
+ import { sniffContentType } from '@beliq/sdk/helpers';
2
3
  import { flagStr, oneOf, requirePositional } from '../args.js';
3
4
  import { UsageError } from '../errors.js';
4
5
  import { emitDocument } from './emit.js';
5
- /** Convert targets that produce a hybrid PDF rather than an XML document. */
6
- const PDF_TARGETS = new Set(['facturx', 'zugferd']);
6
+ /** The targets for which the API can hand a hybrid PDF source back as a PDF. */
7
+ const HYBRID_TARGETS = new Set(['facturx', 'zugferd']);
7
8
  /**
8
- * Convert a document from one EN 16931 format to another. An XML target is
9
- * printed to stdout (or --output); a PDF target (facturx / zugferd) must go to
10
- * --output. Elements the conversion could not carry across are surfaced so a
11
- * lossy conversion is visible.
9
+ * Convert a document from one EN 16931 format to another. The API returns XML
10
+ * for every target, facturx and zugferd included: it does not wrap an XML
11
+ * source into a hybrid PDF. A PDF comes back only when a hybrid PDF source
12
+ * already is the target, and a PDF must go to --output. A 200 means the API
13
+ * validated the converted XML before returning it; it carries no verdict, so
14
+ * no check depth is printed. The count of lost elements is what the engine
15
+ * recorded, and a loss it does not detect is not in it.
12
16
  */
13
17
  export async function runConvert(args, deps, io) {
14
18
  const file = requirePositional(args, 'beliq convert <file|-> --target-format <format>');
@@ -17,19 +21,30 @@ export async function runConvert(args, deps, io) {
17
21
  throw new UsageError(`--target-format is required (one of: ${LIVE_CONVERT_TARGET_FORMATS.join(', ')})`);
18
22
  }
19
23
  const sourceFormat = oneOf(flagStr(args, 'source-format'), LIVE_CONVERT_SOURCE_FORMATS, 'source-format');
24
+ // The flat list, not the per-standard one generate checks: the engine's
25
+ // convert route resolves a profile the same way for both hybrid targets, and
26
+ // the API ignores it for the others.
27
+ const targetProfile = oneOf(flagStr(args, 'target-profile'), LIVE_PROFILES, 'target-profile');
20
28
  const bytes = await io.readInput(file);
29
+ if (sniffContentType(bytes) === 'application/pdf' && HYBRID_TARGETS.has(targetFormat) && !flagStr(args, 'output')) {
30
+ throw new UsageError(`a PDF source converted to ${targetFormat} can come back as a PDF and needs --output <file>`);
31
+ }
21
32
  const result = await deps.client.convert(bytes, {
22
33
  targetFormat: targetFormat,
23
34
  sourceFormat: sourceFormat,
24
- targetProfile: flagStr(args, 'target-profile'),
35
+ targetProfile: targetProfile,
25
36
  contentType: flagStr(args, 'content-type'),
26
37
  });
27
- const kind = PDF_TARGETS.has(targetFormat) ? 'pdf' : 'xml';
38
+ const kind = result.contentType.includes('pdf') ? 'pdf' : 'xml';
28
39
  const from = result.meta.sourceFormat ? `${result.meta.sourceFormat} ` : '';
29
40
  const to = result.meta.targetFormat ?? targetFormat;
30
41
  const lost = result.meta.lostElementsCount && result.meta.lostElementsCount > 0
31
- ? ` ${result.meta.lostElementsCount} element(s) could not be carried across.`
42
+ ? ` The engine recorded ${result.meta.lostElementsCount} source element(s) with no target equivalent.`
32
43
  : '';
44
+ const sandbox = result.meta.livemode === false ? ' (sandbox)' : '';
45
+ const checked = kind === 'pdf'
46
+ ? ' The API validated the embedded XML before returning it; the PDF around it was not checked.'
47
+ : ' The API validated the converted XML before returning it.';
33
48
  return emitDocument(io, args, {
34
49
  kind,
35
50
  bytes: result.bytes,
@@ -39,8 +54,10 @@ export async function runConvert(args, deps, io) {
39
54
  profileDetected: result.meta.profileDetected,
40
55
  lostElementsCount: result.meta.lostElementsCount,
41
56
  lostElements: result.meta.lostElements,
57
+ conversionTools: result.meta.conversionTools,
58
+ livemode: result.meta.livemode,
42
59
  contentType: result.contentType,
43
60
  },
44
- summary: `Converted ${from}to ${to}.${lost}`,
61
+ summary: `Converted ${from}to ${to}, returned as ${kind === 'pdf' ? 'a PDF' : 'XML'}${sandbox}.${checked}${lost}`,
45
62
  });
46
63
  }
@@ -2,10 +2,13 @@ import { LIVE_GENERATE_STANDARDS, isProfileAllowedForStandard, profilesForStanda
2
2
  import { flagBool, flagStr, oneOf, requirePositional } from '../args.js';
3
3
  import { UsageError } from '../errors.js';
4
4
  import { emitDocument } from './emit.js';
5
+ import { checkDepth, franceBlockLines, verdictWord } from '../verdict.js';
5
6
  /**
6
- * Generate a compliant document from an EN 16931 invoice given as JSON. XML is
7
- * printed to stdout (or --output); a PDF must go to --output. verify defaults on
8
- * so a non-compliant document fails closed rather than being handed back.
7
+ * Generate a document from an EN 16931 invoice given as JSON. XML is printed to
8
+ * stdout (or --output); a PDF must go to --output. verify defaults on, so the
9
+ * API refuses a document that fails its own validation instead of handing it
10
+ * back. The summary names how deep that validation went. The verdict is about
11
+ * the invoice XML, so for a PDF the summary says so and names the kind of PDF.
9
12
  */
10
13
  export async function runGenerate(args, deps, io) {
11
14
  const file = requirePositional(args, 'beliq generate <invoice.json|-> --standard <standard>');
@@ -43,11 +46,20 @@ export async function runGenerate(args, deps, io) {
43
46
  // render theirs either way, so this is inert for them.
44
47
  template: output === 'pdf' ? 'standard' : undefined,
45
48
  verify: !flagBool(args, 'no-verify'),
46
- seal,
49
+ // Always the JSON envelope: it is the only response that carries the
50
+ // verdict's check depth, and the API does the same work for either shape.
51
+ // --seal decides whether the hash and the verdict are printed.
52
+ seal: true,
47
53
  });
48
- const checked = result.meta.schematronVersion
49
- ? `, checked against Schematron ${result.meta.schematronVersion}`
50
- : '';
54
+ const verdict = result.validationResult;
55
+ // `verified: false` is the API's statement that --no-verify skipped validation.
56
+ const ran = verdict?.verified !== false;
57
+ const schematron = ran && result.meta.schematronVersion ? `checked against Schematron ${result.meta.schematronVersion}` : '';
58
+ const depth = verdict === undefined ? '' : ran ? checkDepth(verdict) : 'not validated';
59
+ const check = [schematron, depth].filter((part) => part !== '').join(', ');
60
+ const pdfKind = output === 'pdf' && result.meta.pdfKind ? ` (${result.meta.pdfKind})` : '';
61
+ const checked = check === '' ? '' : output === 'pdf' ? `; invoice XML ${check}` : `, ${check}`;
62
+ const france = verdict ? franceBlockLines(verdict).map((line) => ` ${line}`).join('') : '';
51
63
  const sandbox = result.meta.livemode === false ? ' (sandbox)' : '';
52
64
  const meta = {
53
65
  output,
@@ -56,16 +68,15 @@ export async function runGenerate(args, deps, io) {
56
68
  pdfKind: result.meta.pdfKind,
57
69
  outputEnvelope: result.meta.outputEnvelope,
58
70
  livemode: result.meta.livemode,
71
+ verificationBadge: verdict?.verificationBadge,
72
+ verificationBadgeLabel: verdict?.verificationBadgeLabel,
59
73
  };
60
- let summary = `Generated a ${standard} ${output} document${checked}${sandbox}.`;
74
+ let summary = `Generated a ${standard} ${output} document${pdfKind}${checked}${sandbox}.${france}`;
61
75
  if (seal) {
62
76
  meta.sha256 = result.sha256;
63
77
  meta.rulesetSha256 = result.meta.rulesetSha256;
64
- meta.validationResult = result.validationResult;
65
- const verdict = result.validationResult
66
- ? ` Validation ${result.validationResult.valid ? 'passed' : 'failed'}.`
67
- : '';
68
- summary += ` sha256 ${result.sha256}.${verdict}`;
78
+ meta.validationResult = verdict;
79
+ summary += ` sha256 ${result.sha256}.${verdict ? ` Verdict: ${verdictWord(verdict)}.` : ''}`;
69
80
  }
70
81
  return emitDocument(io, args, { kind: output, bytes: result.bytes, meta, summary });
71
82
  }
@@ -2,7 +2,7 @@ import { LIVE_PARSE_FORMATS } from '@beliq/sdk';
2
2
  import { flagBool, flagStr, oneOf, requirePositional } from '../args.js';
3
3
  import { EXIT } from '../exit.js';
4
4
  import { renderParseHuman } from '../format/parse.js';
5
- /** Parse one document into a structured EN 16931 invoice. */
5
+ /** Parse one document into a structured EN 16931 invoice, with the API's warnings about what it did not return. */
6
6
  export async function runParse(args, deps, io) {
7
7
  const file = requirePositional(args, 'beliq parse <file|->');
8
8
  const format = oneOf(flagStr(args, 'format'), LIVE_PARSE_FORMATS, 'format');
@@ -8,17 +8,22 @@ const FAIL_ON = ['error', 'warning'];
8
8
  // rate-limited), not one bad document. Fail fast rather than retry per file.
9
9
  const FATAL_API_STATUSES = new Set([401, 403, 429]);
10
10
  /**
11
- * Validate one or many documents against beliq's authority-pinned rules.
11
+ * Validate one or many documents. Each verdict names how deep its check went.
12
12
  *
13
- * One input (a file or `-`) keeps the classic per-document output. Multiple
14
- * files, or a directory (its `.xml`/`.pdf` files, recursively), switch to a
15
- * batch: a per-file verdict, an aggregate summary, and a batch exit code (0 all
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 or carries France CTC blocking rule
17
+ * ids, 3 some file could not be checked).
18
+ * A directory stays a batch even when it holds one invoice, so the --json shape
19
+ * follows what was asked for rather than how many files were found.
17
20
  */
18
21
  export async function runValidate(args, deps, io) {
19
22
  const format = oneOf(flagStr(args, 'format'), LIVE_VALIDATE_FORMATS, 'format');
20
23
  const failOn = (oneOf(flagStr(args, 'fail-on'), FAIL_ON, 'fail-on') ?? 'error');
21
24
  const contentType = flagStr(args, 'content-type');
25
+ // Sent only when set: the API's own default decides otherwise.
26
+ const franceCtc = flagBool(args, 'france-ctc') ? true : undefined;
22
27
  const json = flagBool(args, 'json');
23
28
  if (args.positionals.length === 0) {
24
29
  throw new UsageError('missing file argument. Usage: beliq validate <file|dir|-> [<file|dir> ...]');
@@ -30,19 +35,26 @@ export async function runValidate(args, deps, io) {
30
35
  if (files.length > 1 && files.includes('-')) {
31
36
  throw new UsageError('cannot mix "-" (stdin) with other inputs');
32
37
  }
33
- if (files.length === 1) {
34
- const result = await deps.client.validate(await io.readInput(files[0]), { format, contentType });
35
- io.stdout(json ? `${JSON.stringify(result, null, 2)}\n` : `${renderValidationHuman(result)}\n`);
38
+ // expandInputs returns a named file as given and a directory as the files
39
+ // inside it, so a single input that comes back unchanged was a file.
40
+ const single = args.positionals.length === 1 && files.length === 1 && files[0] === args.positionals[0];
41
+ if (single) {
42
+ const result = await deps.client.validate(await io.readInput(files[0]), { format, contentType, franceCtc });
43
+ io.stdout(json ? `${JSON.stringify(result, null, 2)}\n` : `${renderValidationHuman(result, { franceFailsFile: true })}\n`);
36
44
  return computeExitCode(result, failOn);
37
45
  }
38
- return runBatch(files, { format, contentType, failOn, json }, deps, io);
46
+ return runBatch(files, { format, contentType, franceCtc, failOn, json }, deps, io);
39
47
  }
40
48
  async function runBatch(files, opts, deps, io) {
41
49
  const rows = [];
42
50
  for (const file of files) {
43
51
  try {
44
52
  const bytes = await io.readInput(file);
45
- const result = await deps.client.validate(bytes, { format: opts.format, contentType: opts.contentType });
53
+ const result = await deps.client.validate(bytes, {
54
+ format: opts.format,
55
+ contentType: opts.contentType,
56
+ franceCtc: opts.franceCtc,
57
+ });
46
58
  rows.push({ file, result, fails: computeExitCode(result, opts.failOn) !== EXIT.OK });
47
59
  }
48
60
  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 === 'bearer' ? 'bearer' : 'header';
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
@@ -1,11 +1,18 @@
1
1
  export const EXIT = {
2
2
  /** Success, or a document that passed validation. */
3
3
  OK: 0,
4
- /** A document that failed validation per the chosen --fail-on threshold. */
4
+ /**
5
+ * A document that failed validation per the chosen --fail-on threshold, or one
6
+ * whose result carries France CTC blocking rule ids.
7
+ */
5
8
  INVALID: 1,
6
9
  /** A usage problem: bad flag, missing argument, missing API key, PDF without --output. */
7
10
  USAGE: 2,
8
- /** The beliq API returned an error (bad key, quota, engine, a rejected document). */
11
+ /**
12
+ * The beliq API returned an error (bad key, quota, engine, a rejected
13
+ * document), could not be reached, or something unexpected failed. Never a
14
+ * verdict on the document.
15
+ */
9
16
  API: 3,
10
17
  /** A local I/O error: an unreadable input or an output path that already exists. */
11
18
  IO: 4,
@@ -13,11 +20,15 @@ export const EXIT = {
13
20
  /**
14
21
  * The CI contract: EXIT.OK when the document passes the chosen threshold,
15
22
  * EXIT.INVALID otherwise. `error` (default) fails on any error; `warning` also
16
- * fails on any warning.
23
+ * fails on any warning. A result that carries France CTC blocking rule ids
24
+ * fails under both: the API reports those ids beside `valid`, also where `valid`
25
+ * is true, so the default threshold alone would let such a result pass.
17
26
  */
18
27
  export function computeExitCode(result, failOn) {
19
28
  const errorCount = result.errors?.length ?? 0;
20
29
  const warningCount = result.warnings?.length ?? 0;
30
+ if ((result.franceCtcBlockingRuleIds?.length ?? 0) > 0)
31
+ return EXIT.INVALID;
21
32
  const failsOnError = !result.valid || errorCount > 0;
22
33
  if (failOn === 'warning')
23
34
  return failsOnError || warningCount > 0 ? EXIT.INVALID : EXIT.OK;
@@ -2,9 +2,10 @@ function plural(n, word) {
2
2
  return `${n} ${word}${n === 1 ? '' : 's'}`;
3
3
  }
4
4
  /**
5
- * A one-line summary of a parse: the detected syntax and profile, then the
6
- * invoice number, line count, and gross total. `--json` emits the full parsed
7
- * invoice instead.
5
+ * A summary of a parse: the detected syntax and profile, then the invoice
6
+ * number, line count, and gross total, then each warning the API sent about
7
+ * what the document holds and the parsed invoice does not. `--json` emits the
8
+ * full result instead.
8
9
  */
9
10
  export function renderParseHuman(result) {
10
11
  const invoice = result.invoice;
@@ -14,5 +15,15 @@ export function renderParseHuman(result) {
14
15
  const gross = invoice?.totalGrossAmount != null
15
16
  ? `, gross ${invoice.totalGrossAmount} ${invoice.currencyCode ?? ''}`.trimEnd()
16
17
  : '';
17
- return `Parsed a ${result.format}${profile} document: ${number}, ${plural(lines, 'line')}${gross}`;
18
+ const head = `Parsed a ${result.format}${profile} document: ${number}, ${plural(lines, 'line')}${gross}`;
19
+ const warnings = result.warnings ?? [];
20
+ if (warnings.length === 0)
21
+ return head;
22
+ const listed = warnings.map((w) => {
23
+ const field = w.field ? ` (field ${w.field})` : '';
24
+ const terms = w.terms?.length ? ` Terms: ${w.terms.join(', ')}.` : '';
25
+ const elements = w.elements?.length ? ` ${plural(w.elements.length, 'element path')}; --json lists them.` : '';
26
+ return `- ${w.code}${field}: ${w.message}${terms}${elements}`;
27
+ });
28
+ return [head, `${plural(warnings.length, 'warning')}:`, ...listed].join('\n');
18
29
  }
@@ -1,3 +1,4 @@
1
+ import { checkDepth, franceBlockLines, franceBlockingRuleIds, pdfInputLine, rulesetChannelLine, verdictWord, } from '../verdict.js';
1
2
  function plural(n, word) {
2
3
  return `${n} ${word}${n === 1 ? '' : 's'}`;
3
4
  }
@@ -13,42 +14,66 @@ function renderTable(headers, rows) {
13
14
  return [line(headers), ...rows.map(line)].join('\n');
14
15
  }
15
16
  /**
16
- * A human-readable verdict: the pass/fail line with format, profile, and
17
- * ruleset version, the issue counts, and an aligned table of the errors then
18
- * warnings. `--json` emits the raw result instead.
17
+ * A human-readable verdict: the API's verdict word with the format, the
18
+ * profile, the check depth and the ruleset version, then the issue counts, what
19
+ * the API says about the ruleset channel, a PDF input and a France CTC block,
20
+ * and an aligned table of the errors then warnings. `--json` emits the raw
21
+ * result instead. `franceFailsFile` adds what a France CTC block does to the
22
+ * exit code of `validate`; a verdict printed for a refused generate or convert
23
+ * leaves it out, because that run exits 3.
19
24
  */
20
- export function renderValidationHuman(result) {
25
+ export function renderValidationHuman(result, opts = {}) {
21
26
  const errors = (result.errors ?? []);
22
27
  const warnings = (result.warnings ?? []);
23
- const verdict = result.valid ? 'VALID' : 'INVALID';
24
28
  const profile = result.profileDetected ? ` (profile ${result.profileDetected})` : '';
25
- const schematron = result.schematronVersion ? ` checked against Schematron ${result.schematronVersion}` : '';
26
- const head = `${verdict} ${result.format}${profile}${schematron}`;
29
+ // Nothing ran when `verified` is false, so there is no depth and no ruleset to name.
30
+ const ran = result.verified !== false;
31
+ const depth = ran ? ` ${checkDepth(result)}` : '';
32
+ const schematron = ran && result.schematronVersion ? ` checked against Schematron ${result.schematronVersion}` : '';
33
+ const head = `${verdictWord(result)} ${result.format}${profile}${depth}${schematron}`;
27
34
  const counts = `${plural(errors.length, 'error')}, ${plural(warnings.length, 'warning')}`;
35
+ const france = franceBlockLines(result);
36
+ const notes = [
37
+ rulesetChannelLine(result),
38
+ pdfInputLine(result),
39
+ ...france,
40
+ ...(france.length > 0 && opts.franceFailsFile
41
+ ? ['This file is reported as failed (exit 1) under every --fail-on.']
42
+ : []),
43
+ ].filter((line) => line !== undefined);
44
+ const top = [head, counts, ...notes].join('\n');
28
45
  const issues = [...errors, ...warnings];
29
46
  if (issues.length === 0)
30
- return `${head}\n${counts}`;
47
+ return top;
31
48
  const rows = issues.map((i) => [i.severity, i.ruleId, i.location || '-', i.message]);
32
- return `${head}\n${counts}\n\n${renderTable(['SEVERITY', 'RULE', 'LOCATION', 'MESSAGE'], rows)}`;
49
+ return `${top}\n\n${renderTable(['SEVERITY', 'RULE', 'LOCATION', 'MESSAGE'], rows)}`;
33
50
  }
34
51
  /**
35
52
  * A batch verdict: a per-file table (PASS / FAIL / ERROR with error and warning
36
- * counts) and a one-line summary. The exit code, not this text, is the CI
37
- * contract; this is the human view.
53
+ * counts and the check depth), a line for each file a France CTC block fails,
54
+ * and a one-line summary. The exit code, not this text, is the CI contract;
55
+ * this is the human view.
38
56
  */
39
57
  export function renderBatchHuman(rows) {
40
58
  const cells = rows.map((r) => 'error' in r
41
- ? ['ERROR', '-', '-', `${r.file} (${r.error})`]
59
+ ? ['ERROR', '-', '-', '-', `${r.file} (${r.error})`]
42
60
  : [
43
61
  r.fails ? 'FAIL' : 'PASS',
44
62
  String((r.result.errors ?? []).length),
45
63
  String((r.result.warnings ?? []).length),
64
+ checkDepth(r.result),
46
65
  r.file,
47
66
  ]);
48
- const table = renderTable(['STATUS', 'ERR', 'WARN', 'FILE'], cells);
67
+ const table = renderTable(['STATUS', 'ERR', 'WARN', 'DEPTH', 'FILE'], cells);
68
+ const blocked = rows.flatMap((r) => {
69
+ if ('error' in r)
70
+ return [];
71
+ const ids = franceBlockingRuleIds(r.result);
72
+ return ids.length > 0 ? [`${r.file}: France CTC blocking rule ids: ${ids.join(', ')}`] : [];
73
+ });
49
74
  const passed = rows.filter((r) => 'result' in r && !r.fails).length;
50
75
  const failed = rows.filter((r) => 'result' in r && r.fails).length;
51
76
  const errored = rows.filter((r) => 'error' in r).length;
52
77
  const summary = `${plural(rows.length, 'file')}: ${passed} passed, ${failed} failed, ${errored} errored`;
53
- return `${table}\n\n${summary}`;
78
+ return [table, ...(blocked.length > 0 ? [blocked.join('\n')] : []), summary].join('\n\n');
54
79
  }
package/dist/help.js CHANGED
@@ -7,19 +7,35 @@ 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 <p>] [--no-verify] [--seal] [--output <file>] [--json]
12
- beliq parse <file|-> [--format auto|cii|ubl] [--json]
13
- beliq convert <file|-> --target-format cii|ubl|zugferd|facturx|xrechnung|peppol-bis [--source-format <f>] [--target-profile <p>] [--output <file>] [--json]
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|fatturapa|facturae|eslog|ksef [--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. For a PDF the verdict is about
20
+ the embedded XML; the PDF itself is not checked.
21
+
22
+ Each verdict carries the check depth as the API states it, or "check depth not
23
+ stated" where the API states none. What the depths mean:
24
+ https://docs.beliq.eu/compliance/how-verification-works/
25
+
26
+ validate --france-ctc also applies the French CTC rules (BR-FR-CTC). A result
27
+ that carries France CTC blocking rule ids fails the file with exit 1 under
28
+ every --fail-on, also where the verdict is VALID.
29
+
30
+ generate --facturx-profile applies to zugferd and facturx; extended-ctc-fr is
31
+ Factur-X only.
19
32
 
20
33
  generate --seal also returns the document sha256 and validation verdict, so you
21
34
  can prove which rules the document passed; hashing the output reproduces it.
22
35
 
36
+ convert returns XML for every target, facturx and zugferd included. A PDF comes
37
+ back only when a hybrid PDF source already is the target, and needs --output.
38
+
23
39
  --pdf gives a hybrid PDF/A-3 with the XML embedded for zugferd and facturx.
24
40
  xrechnung and peppol-bis have no hybrid form, so --pdf returns a visualization
25
41
  with no XML inside it, and their legal document stays the XML.
@@ -29,18 +45,23 @@ Global options:
29
45
  --base-url <url> API base URL (default: BELIQ_BASE_URL, else https://api.beliq.eu)
30
46
  --auth <mode> header (default, X-API-Key) or bearer
31
47
  --json machine-readable JSON on stdout
48
+ --content-type <type>
49
+ the input's media type (default: application/pdf when the
50
+ bytes start with %PDF-, else application/xml)
32
51
  -h, --help show this help
33
52
  -v, --version show the version
34
53
 
35
54
  Exit codes:
36
55
  0 success, or a valid document
37
- 1 document invalid (validate, per --fail-on)
56
+ 1 document invalid (validate, per --fail-on), or France CTC blocking rule
57
+ ids in the result
38
58
  2 usage error
39
- 3 beliq API error
59
+ 3 beliq API error, the API could not be reached, or an unexpected error
40
60
  4 I/O error
41
61
 
42
62
  In batch mode the code covers the whole run: 0 if every file passes, 1 if a
43
- document fails --fail-on, 3 if any file could not be checked.
63
+ document fails --fail-on or carries France CTC blocking rule ids, 3 if any file
64
+ could not be checked.
44
65
 
45
66
  A file argument of "-" reads from stdin. Set BELIQ_API_KEY (or pass --api-key)
46
67
  with a key from the beliq dashboard; the free tier is enough to evaluate.
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 = 1;
9
+ process.exitCode = EXIT.API;
9
10
  });
@@ -0,0 +1,87 @@
1
+ export const VALIDATION_FIELDS = {
2
+ valid: 'text',
3
+ verified: 'text',
4
+ format: 'text',
5
+ profileDetected: 'text',
6
+ verificationBadgeLabel: 'text',
7
+ schematronVersion: 'text',
8
+ franceCtcBlockingRuleIds: 'text',
9
+ rulesetChannel: 'text',
10
+ rulesetFellBack: 'text',
11
+ rulesetSunsetsOn: 'text',
12
+ pdfInput: 'text',
13
+ errors: 'text',
14
+ warnings: 'text',
15
+ // The label above is the display form of these two ids.
16
+ verificationTier: 'json',
17
+ verificationBadge: 'json',
18
+ sha256: 'json',
19
+ rulesetSha256: 'json',
20
+ rulesetArtifacts: 'json',
21
+ driftCheckedAt: 'json',
22
+ ciusVersion: 'json',
23
+ peppolVersion: 'json',
24
+ facturxProfile: 'json',
25
+ facturxVersion: 'json',
26
+ franceCtcApplied: 'json',
27
+ franceCtcVersion: 'json',
28
+ franceExtendedCtcApplied: 'json',
29
+ italyFatturapaXsdBundle: 'json',
30
+ italyFatturapaRuntimeVersion: 'json',
31
+ italySdiMessaggiXsdBundle: 'json',
32
+ italySdiMessaggiXsdVersion: 'json',
33
+ spainFacturaeXsdBundle: 'json',
34
+ spainFacturaeRuntimeVersion: 'json',
35
+ sloveniaEslogXsdBundle: 'json',
36
+ sloveniaEslogRuntimeVersion: 'json',
37
+ polandKsefFa3XsdBundle: 'json',
38
+ polandKsefFa3RuntimeVersion: 'json',
39
+ romaniaRoCiusVersion: 'json',
40
+ netherlandsNlciusVersion: 'json',
41
+ };
42
+ export const PARSE_FIELDS = {
43
+ format: 'text',
44
+ profileDetected: 'text',
45
+ invoice: 'text',
46
+ warnings: 'text',
47
+ profileUrn: 'json',
48
+ businessProcessId: 'json',
49
+ franceCtcDetected: 'json',
50
+ };
51
+ /**
52
+ * Generate and convert build their --json object field by field, so a field
53
+ * can also be left out. `document` is the output itself, and `seal` fields are
54
+ * printed with --seal only.
55
+ */
56
+ export const GENERATE_FIELDS = {
57
+ bytes: 'document',
58
+ xml: 'document',
59
+ contentType: 'json',
60
+ sha256: 'seal',
61
+ // The summary reads the check depth from it; the whole object is printed with --seal.
62
+ validationResult: 'text',
63
+ meta: 'json',
64
+ };
65
+ export const GENERATE_META_FIELDS = {
66
+ schematronVersion: 'text',
67
+ livemode: 'text',
68
+ pdfKind: 'text',
69
+ outputEnvelope: 'json',
70
+ rulesetSha256: 'seal',
71
+ // With --seal the verdict object carries the same list.
72
+ rulesetArtifacts: 'seal',
73
+ };
74
+ export const CONVERT_FIELDS = {
75
+ bytes: 'document',
76
+ contentType: 'json',
77
+ meta: 'json',
78
+ };
79
+ export const CONVERT_META_FIELDS = {
80
+ sourceFormat: 'text',
81
+ targetFormat: 'text',
82
+ lostElementsCount: 'text',
83
+ livemode: 'text',
84
+ profileDetected: 'json',
85
+ lostElements: 'json',
86
+ conversionTools: 'json',
87
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Printed where the API sent no badge label for a verdict. It names no depth,
3
+ * so it claims none.
4
+ */
5
+ export const DEPTH_NOT_STATED = 'check depth not stated';
6
+ /** The API's own entry in `warnings` that explains `franceCtcBlockingRuleIds`. */
7
+ const FRANCE_BLOCK_ENTRY = 'FRANCE_CTC_FINDINGS_BLOCK_TRANSMISSION';
8
+ /**
9
+ * The verdict in the API's own words. `verified: false` means no ruleset ran
10
+ * (generate with verify off), so neither word applies.
11
+ */
12
+ export function verdictWord(result) {
13
+ if (result.verified === false)
14
+ return 'NOT VALIDATED';
15
+ return result.valid ? 'VALID' : 'INVALID';
16
+ }
17
+ /** The badge label the API sent, printed as it came, or the mark for none. */
18
+ export function checkDepth(result) {
19
+ return result.verificationBadgeLabel ?? DEPTH_NOT_STATED;
20
+ }
21
+ /** The France CTC rule ids the API reports as ending a transmission; empty when it sent none. */
22
+ export function franceBlockingRuleIds(result) {
23
+ return result.franceCtcBlockingRuleIds ?? [];
24
+ }
25
+ /**
26
+ * The lines that explain a France CTC block: the API's own message for it when
27
+ * the result carries one, and the rule ids unless that message already names
28
+ * every one of them.
29
+ */
30
+ export function franceBlockLines(result) {
31
+ const ids = franceBlockingRuleIds(result);
32
+ if (ids.length === 0)
33
+ return [];
34
+ const entry = (result.warnings ?? []).find((w) => w.ruleId === FRANCE_BLOCK_ENTRY);
35
+ const idLine = `France CTC blocking rule ids: ${ids.join(', ')}`;
36
+ if (!entry)
37
+ return [idLine];
38
+ const named = new Set(entry.message.split(/[^A-Za-z0-9_-]+/));
39
+ return ids.every((id) => named.has(id)) ? [entry.message] : [entry.message, idLine];
40
+ }
41
+ /**
42
+ * The embedded file's name comes from the uploaded PDF. Control and format
43
+ * characters, line separators and the double quote are replaced, so a crafted
44
+ * name can neither start a line of its own nor close the quotes around it.
45
+ */
46
+ function printable(name) {
47
+ return name.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}"]/gu, '?');
48
+ }
49
+ /** What the API says about a PDF input: which embedded file was read, and that the PDF was not checked. */
50
+ export function pdfInputLine(result) {
51
+ const pdf = result.pdfInput;
52
+ if (!pdf)
53
+ return undefined;
54
+ const name = pdf.attachmentName ? ` "${printable(pdf.attachmentName)}"` : '';
55
+ const others = pdf.attachmentCandidates > 1
56
+ ? ` ${pdf.attachmentCandidates} embedded files could be taken for the invoice, and nothing compared them.`
57
+ : '';
58
+ return (`PDF input: the verdict is about the embedded XML${name} (found by ${pdf.attachmentFoundBy}). ` +
59
+ `The PDF itself was not checked.${others}`);
60
+ }
61
+ /** Which ruleset channel the API says served the request, when the result names one. */
62
+ export function rulesetChannelLine(result) {
63
+ if (!result.rulesetChannel)
64
+ return undefined;
65
+ const fellBack = result.rulesetFellBack ? ', which had nothing to reach, so the latest ruleset ran' : '';
66
+ const sunset = result.rulesetSunsetsOn ? `; the retained ruleset is served until ${result.rulesetSunsetsOn}` : '';
67
+ return `Ruleset channel: ${result.rulesetChannel}${fellBack}${sunset}`;
68
+ }
@@ -1,8 +1,8 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
2
  <!-- A minimal CII (UN/CEFACT Cross Industry Invoice) carrying the XRechnung
3
3
  guideline id but missing the Buyer reference (BT-10), which XRechnung
4
- requires (rule BR-DE-15). Used as a known-invalid example for the gated
5
- live test and for the skill walkthrough. -->
4
+ requires (rule BR-DE-15). Used as the known-invalid example in the gated
5
+ live test. -->
6
6
  <rsm:CrossIndustryInvoice
7
7
  xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
8
8
  xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beliq-cli",
3
- "version": "0.2.2",
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.",
3
+ "version": "0.4.0",
4
+ "description": "beliq CLI: validate, generate, parse, and convert EU e-invoices (XRechnung, ZUGFeRD, Factur-X, Peppol BIS) from the terminal or CI. Every verdict states how deep its check went.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "author": {
@@ -48,23 +48,24 @@
48
48
  "build": "npm run clean && tsc",
49
49
  "dev": "tsc --watch",
50
50
  "start": "node dist/index.js",
51
- "typecheck": "tsc --noEmit",
52
- "lint": "eslint src",
53
- "lint:fix": "eslint src --fix",
51
+ "typecheck": "tsc -p tsconfig.test.json",
52
+ "lint": "eslint . --max-warnings 0",
53
+ "lint:fix": "eslint . --fix",
54
54
  "test": "vitest run --exclude test/integration.test.ts",
55
55
  "test:integration": "vitest run test/integration.test.ts",
56
56
  "scrub:check": "bash scripts/scrub-check.sh",
57
57
  "prepublishOnly": "npm run build"
58
58
  },
59
59
  "dependencies": {
60
- "@beliq/sdk": "^0.4.0"
60
+ "@beliq/sdk": "^0.5.1"
61
61
  },
62
62
  "devDependencies": {
63
+ "@eslint/js": "^10.0.1",
63
64
  "@types/node": "^20.14.0",
64
- "@typescript-eslint/eslint-plugin": "^7.18.0",
65
- "@typescript-eslint/parser": "^7.18.0",
66
- "eslint": "^8.57.0",
65
+ "eslint": "^10.11.0",
66
+ "globals": "^17.12.0",
67
67
  "typescript": "^5.5.4",
68
+ "typescript-eslint": "^8.70.1",
68
69
  "vitest": "^4.0.0"
69
70
  }
70
71
  }