@jasy/cli 1.0.0-alpha.3 → 1.0.0-alpha.4
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/LICENSE +21 -0
- package/README.md +2 -2
- package/dist/commands/export.js +2 -3
- package/dist/commands/read.js +2 -3
- package/dist/commands/validate.js +5 -8
- package/dist/commands/verapdf.js +22 -37
- package/dist/core/detect.js +3 -2
- package/dist/core/export.js +1 -1
- package/dist/core/extract.js +3 -2
- package/dist/core/parse.js +42 -55
- package/dist/core/pdfa.js +8 -8
- package/dist/core/read.js +1 -1
- package/dist/core/validate.js +5 -8
- package/dist/core/verapdf.js +37 -51
- package/dist/index.js +29 -41
- package/dist/tui/app.js +31 -43
- package/dist/tui/file-open.js +2 -2
- package/package.json +2 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Florian Heuberger
|
|
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
|
@@ -64,8 +64,8 @@ structural checks carry the everyday case.
|
|
|
64
64
|
pure JS). No upload, DSGVO-safe.
|
|
65
65
|
- **Excel by hand** - the `.xlsx` is a ZIP we build ourselves, deflated with our own writer and CRC32.
|
|
66
66
|
- **Reads multi-file PDFs** - pulls the right e-invoice XML out even when a tool embedded its own JSON too.
|
|
67
|
-
- Generates with [`@jasy/zugferd`](https://
|
|
68
|
-
[`@jasy/pdf`](https://
|
|
67
|
+
- Generates with [`@jasy/zugferd`](https://npmx.dev/@jasy/zugferd) on the hand-rolled
|
|
68
|
+
[`@jasy/pdf`](https://npmx.dev/@jasy/pdf) engine.
|
|
69
69
|
|
|
70
70
|
---
|
|
71
71
|
|
package/dist/commands/export.js
CHANGED
|
@@ -12,7 +12,6 @@ const FORMATS = {
|
|
|
12
12
|
excel: "xlsx",
|
|
13
13
|
};
|
|
14
14
|
export function exportCommand(args) {
|
|
15
|
-
var _a, _b, _c;
|
|
16
15
|
let file;
|
|
17
16
|
let out;
|
|
18
17
|
let fmtArg;
|
|
@@ -30,13 +29,13 @@ export function exportCommand(args) {
|
|
|
30
29
|
process.exitCode = 1;
|
|
31
30
|
return;
|
|
32
31
|
}
|
|
33
|
-
const format =
|
|
32
|
+
const format = FORMATS[fmtArg ?? ""] ?? FORMATS[out ? extname(out).slice(1) : ""] ?? "json";
|
|
34
33
|
if (format === "xlsx" && !out) {
|
|
35
34
|
console.error("✗ xlsx is binary - give an output path with -o <file.xlsx>");
|
|
36
35
|
process.exitCode = 1;
|
|
37
36
|
return;
|
|
38
37
|
}
|
|
39
|
-
const base =
|
|
38
|
+
const base = process.env.INIT_CWD ?? process.cwd();
|
|
40
39
|
let bytes;
|
|
41
40
|
try {
|
|
42
41
|
bytes = readFileSync(resolve(base, file));
|
package/dist/commands/read.js
CHANGED
|
@@ -10,7 +10,6 @@ const bold = (s) => paint("1", s);
|
|
|
10
10
|
const dim = (s) => paint("2", s);
|
|
11
11
|
const money = (n) => n.toFixed(2);
|
|
12
12
|
export function readCommand(args) {
|
|
13
|
-
var _a, _b;
|
|
14
13
|
let file;
|
|
15
14
|
let out;
|
|
16
15
|
let dumpXml = false;
|
|
@@ -29,7 +28,7 @@ export function readCommand(args) {
|
|
|
29
28
|
return;
|
|
30
29
|
}
|
|
31
30
|
// resolve against where the user actually stood - pnpm scripts cd into the package dir
|
|
32
|
-
const base =
|
|
31
|
+
const base = process.env.INIT_CWD ?? process.cwd();
|
|
33
32
|
let r;
|
|
34
33
|
try {
|
|
35
34
|
r = readInvoiceFile(resolve(base, file));
|
|
@@ -66,7 +65,7 @@ export function readCommand(args) {
|
|
|
66
65
|
}
|
|
67
66
|
else {
|
|
68
67
|
console.log(` source ${r.isPdf ? "PDF - embedded XML extracted" : "raw XML"}`);
|
|
69
|
-
console.log(` guideline ${
|
|
68
|
+
console.log(` guideline ${r.meta.guideline ?? "-"}`);
|
|
70
69
|
console.log(` XML ${r.xml.length} bytes`);
|
|
71
70
|
console.log(`\n ${dim("→ --xml print the XML · -o <file> save it")}`);
|
|
72
71
|
}
|
|
@@ -15,7 +15,6 @@ const red = (s) => paint("31", s);
|
|
|
15
15
|
const dim = (s) => paint("2", s);
|
|
16
16
|
const bold = (s) => paint("1", s);
|
|
17
17
|
export function validateCommand(args) {
|
|
18
|
-
var _a;
|
|
19
18
|
const file = args.find((a) => !a.startsWith("-"));
|
|
20
19
|
const verbose = args.includes("-v") || args.includes("--verbose");
|
|
21
20
|
const json = args.includes("--json");
|
|
@@ -30,7 +29,7 @@ export function validateCommand(args) {
|
|
|
30
29
|
fail("usage: jasy validate <file.pdf|file.xml> [--json] [-v]");
|
|
31
30
|
return;
|
|
32
31
|
}
|
|
33
|
-
const base =
|
|
32
|
+
const base = process.env.INIT_CWD ?? process.cwd();
|
|
34
33
|
let bytes;
|
|
35
34
|
try {
|
|
36
35
|
bytes = readFileSync(resolve(base, file));
|
|
@@ -47,7 +46,7 @@ export function validateCommand(args) {
|
|
|
47
46
|
try {
|
|
48
47
|
rules = validateInvoiceXml(read.xml, profileFor(read.meta));
|
|
49
48
|
}
|
|
50
|
-
catch
|
|
49
|
+
catch {
|
|
51
50
|
rules = null;
|
|
52
51
|
}
|
|
53
52
|
}
|
|
@@ -58,7 +57,7 @@ export function validateCommand(args) {
|
|
|
58
57
|
try {
|
|
59
58
|
vera = runVeraPdf(resolve(base, file));
|
|
60
59
|
}
|
|
61
|
-
catch
|
|
60
|
+
catch {
|
|
62
61
|
vera = null;
|
|
63
62
|
}
|
|
64
63
|
}
|
|
@@ -76,7 +75,6 @@ export function validateCommand(args) {
|
|
|
76
75
|
}
|
|
77
76
|
/** The machine-readable report (`--json`): the exact same data the printed report shows. */
|
|
78
77
|
function toJson(file, read, rules, pdfa, vera, recognized, ok) {
|
|
79
|
-
var _a;
|
|
80
78
|
return {
|
|
81
79
|
file: basename(file),
|
|
82
80
|
summary: describeInvoice(read.meta),
|
|
@@ -102,7 +100,7 @@ function toJson(file, read, rules, pdfa, vera, recognized, ok) {
|
|
|
102
100
|
available: true,
|
|
103
101
|
valid: vera.ok,
|
|
104
102
|
profile: vera.profile,
|
|
105
|
-
failedRules:
|
|
103
|
+
failedRules: vera.failedRules ?? vera.failures.length,
|
|
106
104
|
failures: vera.failures,
|
|
107
105
|
}
|
|
108
106
|
: { available: false }
|
|
@@ -111,7 +109,6 @@ function toJson(file, read, rules, pdfa, vera, recognized, ok) {
|
|
|
111
109
|
}
|
|
112
110
|
/** The human-readable report (default): a coloured summary line + per-check detail. */
|
|
113
111
|
function printReport(file, read, rules, pdfa, vera, recognized, ok, verbose) {
|
|
114
|
-
var _a;
|
|
115
112
|
const label = (s) => s.padEnd(20);
|
|
116
113
|
console.log(`\n ${bold(basename(file))} ${dim("·")} ${describeInvoice(read.meta)}\n`);
|
|
117
114
|
// business rules
|
|
@@ -143,7 +140,7 @@ function printReport(file, read, rules, pdfa, vera, recognized, ok, verbose) {
|
|
|
143
140
|
}
|
|
144
141
|
// full ISO 19005 (PDF/A) via veraPDF
|
|
145
142
|
if (vera) {
|
|
146
|
-
const n =
|
|
143
|
+
const n = vera.failedRules ?? vera.failures.length;
|
|
147
144
|
console.log(` ${label("PDF/A (veraPDF)")}${vera.ok ? green("✓ compliant") : red(`✗ ${n} failed`)}`);
|
|
148
145
|
if (!vera.ok)
|
|
149
146
|
for (const f of vera.failures)
|
package/dist/commands/verapdf.js
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
-
});
|
|
9
|
-
};
|
|
10
1
|
import { resolve } from "node:path";
|
|
11
2
|
import { detectTools, installVeraPdf, runVeraPdf, findVerapdf } from "../core/verapdf.js";
|
|
12
3
|
// `jasy verapdf` - a guided doctor for the optional full-ISO PDF/A validator:
|
|
@@ -19,18 +10,15 @@ const green = (s) => paint("32", s);
|
|
|
19
10
|
const red = (s) => paint("31", s);
|
|
20
11
|
const dim = (s) => paint("2", s);
|
|
21
12
|
const bold = (s) => paint("1", s);
|
|
22
|
-
export function verapdfCommand(args) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
doctor();
|
|
30
|
-
});
|
|
13
|
+
export async function verapdfCommand(args) {
|
|
14
|
+
if (args.includes("--install"))
|
|
15
|
+
return install();
|
|
16
|
+
const file = args.find((a) => !a.startsWith("-"));
|
|
17
|
+
if (file)
|
|
18
|
+
return validateFile(file);
|
|
19
|
+
doctor();
|
|
31
20
|
}
|
|
32
21
|
function doctor() {
|
|
33
|
-
var _a, _b;
|
|
34
22
|
const t = detectTools();
|
|
35
23
|
console.log(`
|
|
36
24
|
${bold("veraPDF")} - the official open-source PDF/A validator (PDF Association).
|
|
@@ -39,8 +27,8 @@ function doctor() {
|
|
|
39
27
|
your invoice never leaves the machine. Free, no account.
|
|
40
28
|
`);
|
|
41
29
|
const row = (label, ok, detail) => console.log(` ${label.padEnd(11)}${ok ? green("✓ " + detail) : red("✗ " + detail)}`);
|
|
42
|
-
row("Java", !!t.java,
|
|
43
|
-
row("veraPDF", !!t.verapdf, t.verapdf ? `${t.verapdf} ${dim(
|
|
30
|
+
row("Java", !!t.java, t.java ?? "not found");
|
|
31
|
+
row("veraPDF", !!t.verapdf, t.verapdf ? `${t.verapdf} ${dim(t.verapdfPath ?? "")}` : "not found");
|
|
44
32
|
console.log("");
|
|
45
33
|
if (!t.java) {
|
|
46
34
|
console.log(` ${dim("Java is required - veraPDF is a Java app. Install a JRE 11+:")}`);
|
|
@@ -55,33 +43,30 @@ function doctor() {
|
|
|
55
43
|
console.log(` ${green("Ready.")} ${dim("`jasy validate <file>` now adds the full ISO PDF/A check automatically.")}`);
|
|
56
44
|
}
|
|
57
45
|
}
|
|
58
|
-
function install() {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
});
|
|
46
|
+
async function install() {
|
|
47
|
+
try {
|
|
48
|
+
const bin = await installVeraPdf((s) => console.log(` ${dim(s)}`));
|
|
49
|
+
console.log(` ${green("✓ veraPDF installed")} ${dim(bin)}`);
|
|
50
|
+
console.log(` ${dim("`jasy validate <file>` now runs the full ISO PDF/A check.")}`);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
console.error(` ${red("✗ " + e.message)}`);
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
}
|
|
70
56
|
}
|
|
71
57
|
function validateFile(file) {
|
|
72
|
-
var _a, _b, _c;
|
|
73
58
|
if (!findVerapdf()) {
|
|
74
59
|
console.error(` ${red("✗ veraPDF is not installed")} - run ${bold("jasy verapdf --install")}`);
|
|
75
60
|
process.exitCode = 1;
|
|
76
61
|
return;
|
|
77
62
|
}
|
|
78
|
-
const base =
|
|
63
|
+
const base = process.env.INIT_CWD ?? process.cwd();
|
|
79
64
|
try {
|
|
80
65
|
const r = runVeraPdf(resolve(base, file));
|
|
81
66
|
const head = r.ok
|
|
82
67
|
? green("✓ compliant")
|
|
83
|
-
: red(`✗ ${
|
|
84
|
-
console.log(`\n ${
|
|
68
|
+
: red(`✗ ${r.failedRules ?? r.failures.length} rule(s) failed`);
|
|
69
|
+
console.log(`\n ${r.profile ?? "PDF/A"} ${head}`);
|
|
85
70
|
for (const f of r.failures) {
|
|
86
71
|
const n = f.failedChecks;
|
|
87
72
|
console.log(` ${red("✗")} ISO clause ${bold(f.clause)} ${dim(`(${n} check${n === 1 ? "" : "s"})`)}`);
|
package/dist/core/detect.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// profile (plain EN16931 vs the German XRechnung CIUS). Pure tag/regex inspection - no full parse
|
|
3
3
|
// needed (that comes later). Lets the CLI say "ZUGFeRD EN16931 (CII)" before doing anything heavy.
|
|
4
4
|
export function detectInvoice(xml) {
|
|
5
|
-
var _a, _b, _c, _d;
|
|
6
5
|
const syntax = /<(?:rsm:)?CrossIndustryInvoice/.test(xml)
|
|
7
6
|
? "CII"
|
|
8
7
|
: /<Invoice\b[^>]*urn:oasis:names:specification:ubl/.test(xml) ||
|
|
@@ -10,7 +9,9 @@ export function detectInvoice(xml) {
|
|
|
10
9
|
? "UBL"
|
|
11
10
|
: "unknown";
|
|
12
11
|
// BT-24: CII carries it in GuidelineSpecified…/ram:ID, UBL in cbc:CustomizationID.
|
|
13
|
-
const guideline =
|
|
12
|
+
const guideline = xml.match(/GuidelineSpecifiedDocumentContextParameter>\s*<(?:ram:)?ID>([^<]+)</)?.[1] ??
|
|
13
|
+
xml.match(/<(?:cbc:)?CustomizationID>([^<]+)</)?.[1] ??
|
|
14
|
+
null;
|
|
14
15
|
const profile = !guideline
|
|
15
16
|
? "unknown"
|
|
16
17
|
: /xrechnung/i.test(guideline)
|
package/dist/core/export.js
CHANGED
|
@@ -13,7 +13,7 @@ function summary(inv, t) {
|
|
|
13
13
|
}
|
|
14
14
|
/** Full invoice model + a computed totals block, pretty-printed. */
|
|
15
15
|
export function exportJson(inv, t) {
|
|
16
|
-
return JSON.stringify(
|
|
16
|
+
return JSON.stringify({ ...inv, totals: summary(inv, t) }, null, 2);
|
|
17
17
|
}
|
|
18
18
|
/** A plain-text receipt - no ANSI, safe to pipe or save. */
|
|
19
19
|
export function exportText(inv, t) {
|
package/dist/core/extract.js
CHANGED
|
@@ -22,7 +22,6 @@ export function extractEmbeddedXml(pdf) {
|
|
|
22
22
|
// attaches its own gobl.json / source) - so we collect every Filespec and pick the invoice XML by name
|
|
23
23
|
// (factur-x.xml / zugferd-invoice.xml / xrechnung.xml / order-x.xml), then any .xml, then the first.
|
|
24
24
|
function findEmbeddedFileObject(s) {
|
|
25
|
-
var _a, _b;
|
|
26
25
|
const specs = [];
|
|
27
26
|
const efRe = /\/EF\s*<<[^>]*?\/(?:UF|F)\s+(\d+)\s+0\s+R/g;
|
|
28
27
|
for (let m = efRe.exec(s); m; m = efRe.exec(s)) {
|
|
@@ -30,7 +29,9 @@ function findEmbeddedFileObject(s) {
|
|
|
30
29
|
}
|
|
31
30
|
if (specs.length) {
|
|
32
31
|
const invoiceXml = /(factur-x|zugferd-invoice|xrechnung|order-x|cii)\.xml$/i;
|
|
33
|
-
const pick =
|
|
32
|
+
const pick = specs.find((sp) => invoiceXml.test(sp.name)) ??
|
|
33
|
+
specs.find((sp) => /\.xml$/i.test(sp.name)) ??
|
|
34
|
+
specs[0];
|
|
34
35
|
return pick.obj;
|
|
35
36
|
}
|
|
36
37
|
const t = s.search(/\/Type\s*\/EmbeddedFile/);
|
package/dist/core/parse.js
CHANGED
|
@@ -5,10 +5,9 @@ import { detectInvoice } from "./detect.js";
|
|
|
5
5
|
const unesc = (s) => s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
6
6
|
/** Inner content of the first `<tag …>…</tag>` (CII tags don't self-nest, so non-greedy is exact). */
|
|
7
7
|
function inner(xml, tag) {
|
|
8
|
-
var _a;
|
|
9
8
|
if (xml === undefined)
|
|
10
9
|
return undefined;
|
|
11
|
-
return
|
|
10
|
+
return new RegExp(`<${tag}(?:\\s[^>]*)?>([\\s\\S]*?)</${tag}>`).exec(xml)?.[1];
|
|
12
11
|
}
|
|
13
12
|
/** Inner content of every `<tag …>…</tag>`. */
|
|
14
13
|
function innerAll(xml, tag) {
|
|
@@ -27,10 +26,9 @@ function val(xml, tag) {
|
|
|
27
26
|
}
|
|
28
27
|
/** An attribute on the first `<tag … name="X" …>`. */
|
|
29
28
|
function attr(xml, tag, name) {
|
|
30
|
-
var _a;
|
|
31
29
|
if (xml === undefined)
|
|
32
30
|
return undefined;
|
|
33
|
-
return
|
|
31
|
+
return new RegExp(`<${tag}\\s[^>]*\\b${name}="([^"]*)"`).exec(xml)?.[1];
|
|
34
32
|
}
|
|
35
33
|
const num = (s) => (s === undefined ? 0 : parseFloat(s));
|
|
36
34
|
/** `format="102"` date `20260620` → `2026-06-20`. */
|
|
@@ -39,7 +37,6 @@ function date(scope) {
|
|
|
39
37
|
return d && d.length === 8 ? `${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}` : undefined;
|
|
40
38
|
}
|
|
41
39
|
function parseAddress(s) {
|
|
42
|
-
var _a;
|
|
43
40
|
return {
|
|
44
41
|
postCode: val(s, "ram:PostcodeCode"),
|
|
45
42
|
line1: val(s, "ram:LineOne"),
|
|
@@ -47,7 +44,7 @@ function parseAddress(s) {
|
|
|
47
44
|
line3: val(s, "ram:LineThree"),
|
|
48
45
|
city: val(s, "ram:CityName"),
|
|
49
46
|
subdivision: val(s, "ram:CountrySubDivisionName"),
|
|
50
|
-
country:
|
|
47
|
+
country: val(s, "ram:CountryID") ?? "",
|
|
51
48
|
};
|
|
52
49
|
}
|
|
53
50
|
function parseContact(s) {
|
|
@@ -66,55 +63,52 @@ function taxReg(s, scheme) {
|
|
|
66
63
|
return m ? unesc(m[1]) : undefined;
|
|
67
64
|
}
|
|
68
65
|
function parseSeller(s) {
|
|
69
|
-
var _a, _b;
|
|
70
66
|
const org = inner(s, "ram:SpecifiedLegalOrganization");
|
|
71
67
|
return {
|
|
72
|
-
name:
|
|
68
|
+
name: val(s, "ram:Name") ?? "",
|
|
73
69
|
tradingName: val(org, "ram:TradingBusinessName"),
|
|
74
70
|
legalRegistrationId: val(org, "ram:ID"),
|
|
75
71
|
vatId: taxReg(s, "VA"),
|
|
76
72
|
taxNumber: taxReg(s, "FC"),
|
|
77
73
|
electronicAddress: val(inner(s, "ram:URIUniversalCommunication"), "ram:URIID"),
|
|
78
|
-
address: parseAddress(
|
|
74
|
+
address: parseAddress(inner(s, "ram:PostalTradeAddress") ?? ""),
|
|
79
75
|
contact: parseContact(inner(s, "ram:DefinedTradeContact")),
|
|
80
76
|
};
|
|
81
77
|
}
|
|
82
78
|
function parseBuyer(s) {
|
|
83
|
-
var _a, _b;
|
|
84
79
|
const org = inner(s, "ram:SpecifiedLegalOrganization");
|
|
85
80
|
return {
|
|
86
|
-
name:
|
|
81
|
+
name: val(s, "ram:Name") ?? "",
|
|
87
82
|
tradingName: val(org, "ram:TradingBusinessName"),
|
|
88
83
|
legalRegistrationId: val(org, "ram:ID"),
|
|
89
84
|
vatId: taxReg(s, "VA"),
|
|
90
85
|
electronicAddress: val(inner(s, "ram:URIUniversalCommunication"), "ram:URIID"),
|
|
91
|
-
address: parseAddress(
|
|
86
|
+
address: parseAddress(inner(s, "ram:PostalTradeAddress") ?? ""),
|
|
92
87
|
contact: parseContact(inner(s, "ram:DefinedTradeContact")),
|
|
93
88
|
};
|
|
94
89
|
}
|
|
95
90
|
function parseLine(s, index) {
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
const tax = (_e = inner(inner(s, "ram:SpecifiedLineTradeSettlement"), "ram:ApplicableTradeTax")) !== null && _e !== void 0 ? _e : "";
|
|
91
|
+
const doc = inner(s, "ram:AssociatedDocumentLineDocument") ?? "";
|
|
92
|
+
const product = inner(s, "ram:SpecifiedTradeProduct") ?? "";
|
|
93
|
+
const price = inner(s, "ram:NetPriceProductTradePrice") ?? "";
|
|
94
|
+
const del = inner(s, "ram:SpecifiedLineTradeDelivery") ?? "";
|
|
95
|
+
const tax = inner(inner(s, "ram:SpecifiedLineTradeSettlement"), "ram:ApplicableTradeTax") ?? "";
|
|
102
96
|
const id = val(doc, "ram:LineID");
|
|
103
97
|
const note = inner(doc, "ram:IncludedNote");
|
|
104
98
|
const basis = inner(price, "ram:BasisQuantity");
|
|
105
99
|
return {
|
|
106
100
|
id: id !== undefined && id !== String(index + 1) ? id : undefined, // omit the auto-number
|
|
107
|
-
name:
|
|
101
|
+
name: val(product, "ram:Name") ?? "",
|
|
108
102
|
description: val(product, "ram:Description"),
|
|
109
103
|
sellerItemId: val(product, "ram:SellerAssignedID"),
|
|
110
104
|
buyerItemId: val(product, "ram:BuyerAssignedID"),
|
|
111
105
|
standardItemId: val(product, "ram:GlobalID"),
|
|
112
106
|
quantity: num(val(del, "ram:BilledQuantity")),
|
|
113
|
-
unit:
|
|
107
|
+
unit: attr(del, "ram:BilledQuantity", "unitCode") ?? "",
|
|
114
108
|
netUnitPrice: num(val(price, "ram:ChargeAmount")),
|
|
115
109
|
priceBaseQuantity: basis ? num(val(price, "ram:BasisQuantity")) : undefined,
|
|
116
110
|
vat: {
|
|
117
|
-
category: (
|
|
111
|
+
category: (val(tax, "ram:CategoryCode") ?? "S"),
|
|
118
112
|
ratePercent: num(val(tax, "ram:RateApplicablePercent")),
|
|
119
113
|
},
|
|
120
114
|
note: note ? val(note, "ram:Content") : undefined,
|
|
@@ -122,7 +116,6 @@ function parseLine(s, index) {
|
|
|
122
116
|
}
|
|
123
117
|
/** A document-level allowance (discount) or charge (surcharge), BG-20 / BG-21. */
|
|
124
118
|
function parseAllowanceCii(ac) {
|
|
125
|
-
var _a;
|
|
126
119
|
const cat = inner(ac, "ram:CategoryTradeTax");
|
|
127
120
|
return {
|
|
128
121
|
isCharge: val(inner(ac, "ram:ChargeIndicator"), "udt:Indicator") === "true",
|
|
@@ -130,7 +123,7 @@ function parseAllowanceCii(ac) {
|
|
|
130
123
|
reason: val(ac, "ram:Reason"),
|
|
131
124
|
reasonCode: val(ac, "ram:ReasonCode"),
|
|
132
125
|
vat: {
|
|
133
|
-
category: (
|
|
126
|
+
category: (val(cat, "ram:CategoryCode") ?? "S"),
|
|
134
127
|
ratePercent: num(val(cat, "ram:RateApplicablePercent")),
|
|
135
128
|
},
|
|
136
129
|
};
|
|
@@ -149,12 +142,11 @@ function exemptionsCii(set) {
|
|
|
149
142
|
}
|
|
150
143
|
/** Parse a UN/CEFACT CII invoice (EN16931 / ZUGFeRD / XRechnung-CII) into the Invoice model. */
|
|
151
144
|
export function parseCII(xml) {
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
const set = (_e = inner(tx, "ram:ApplicableHeaderTradeSettlement")) !== null && _e !== void 0 ? _e : "";
|
|
145
|
+
const header = inner(xml, "rsm:ExchangedDocument") ?? "";
|
|
146
|
+
const tx = inner(xml, "rsm:SupplyChainTradeTransaction") ?? "";
|
|
147
|
+
const agr = inner(tx, "ram:ApplicableHeaderTradeAgreement") ?? "";
|
|
148
|
+
const del = inner(tx, "ram:ApplicableHeaderTradeDelivery") ?? "";
|
|
149
|
+
const set = inner(tx, "ram:ApplicableHeaderTradeSettlement") ?? "";
|
|
158
150
|
const type = num(val(header, "ram:TypeCode"));
|
|
159
151
|
const notes = innerAll(header, "ram:IncludedNote")
|
|
160
152
|
.map((n) => val(n, "ram:Content"))
|
|
@@ -188,17 +180,17 @@ export function parseCII(xml) {
|
|
|
188
180
|
const paid = val(totals, "ram:TotalPrepaidAmount");
|
|
189
181
|
const allowancesCharges = innerAll(set, "ram:SpecifiedTradeAllowanceCharge").map(parseAllowanceCii);
|
|
190
182
|
return {
|
|
191
|
-
number:
|
|
192
|
-
issueDate:
|
|
183
|
+
number: val(header, "ram:ID") ?? "",
|
|
184
|
+
issueDate: date(inner(header, "ram:IssueDateTime")) ?? "",
|
|
193
185
|
type: type && type !== 380 ? type : undefined,
|
|
194
|
-
currency:
|
|
186
|
+
currency: val(set, "ram:InvoiceCurrencyCode") ?? "",
|
|
195
187
|
dueDate: date(inner(terms, "ram:DueDateDateTime")),
|
|
196
188
|
buyerReference: val(agr, "ram:BuyerReference"),
|
|
197
189
|
purchaseOrderRef: val(inner(agr, "ram:BuyerOrderReferencedDocument"), "ram:IssuerAssignedID"),
|
|
198
190
|
contractRef: val(inner(agr, "ram:ContractReferencedDocument"), "ram:IssuerAssignedID"),
|
|
199
191
|
notes: notes.length ? notes : undefined,
|
|
200
|
-
seller: parseSeller(
|
|
201
|
-
buyer: parseBuyer(
|
|
192
|
+
seller: parseSeller(inner(agr, "ram:SellerTradeParty") ?? ""),
|
|
193
|
+
buyer: parseBuyer(inner(agr, "ram:BuyerTradeParty") ?? ""),
|
|
202
194
|
delivery,
|
|
203
195
|
payeeName: val(inner(set, "ram:PayeeTradeParty"), "ram:Name"),
|
|
204
196
|
lines: innerAll(tx, "ram:IncludedSupplyChainTradeLineItem").map(parseLine),
|
|
@@ -210,7 +202,6 @@ export function parseCII(xml) {
|
|
|
210
202
|
}
|
|
211
203
|
// ── UBL (OASIS Invoice-2) - the second EN16931 syntax (PEPPOL; XRechnung accepts it too) ────────────
|
|
212
204
|
function parseAddressUbl(s) {
|
|
213
|
-
var _a;
|
|
214
205
|
return {
|
|
215
206
|
line1: val(s, "cbc:StreetName"),
|
|
216
207
|
line2: val(s, "cbc:AdditionalStreetName"),
|
|
@@ -218,7 +209,7 @@ function parseAddressUbl(s) {
|
|
|
218
209
|
city: val(s, "cbc:CityName"),
|
|
219
210
|
postCode: val(s, "cbc:PostalZone"),
|
|
220
211
|
subdivision: val(s, "cbc:CountrySubentity"),
|
|
221
|
-
country:
|
|
212
|
+
country: val(inner(s, "cac:Country"), "cbc:IdentificationCode") ?? "",
|
|
222
213
|
};
|
|
223
214
|
}
|
|
224
215
|
function parseContactUbl(s) {
|
|
@@ -240,47 +231,44 @@ function ublTaxId(party, scheme) {
|
|
|
240
231
|
return undefined;
|
|
241
232
|
}
|
|
242
233
|
function parsePartyUbl(scope) {
|
|
243
|
-
|
|
244
|
-
const party = (_a = inner(scope, "cac:Party")) !== null && _a !== void 0 ? _a : "";
|
|
234
|
+
const party = inner(scope, "cac:Party") ?? "";
|
|
245
235
|
const legal = inner(party, "cac:PartyLegalEntity");
|
|
246
236
|
return {
|
|
247
237
|
party,
|
|
248
|
-
name:
|
|
238
|
+
name: val(legal, "cbc:RegistrationName") ?? "",
|
|
249
239
|
tradingName: val(inner(party, "cac:PartyName"), "cbc:Name"),
|
|
250
240
|
legalRegistrationId: val(legal, "cbc:CompanyID"),
|
|
251
241
|
vatId: ublTaxId(party, "VAT"),
|
|
252
242
|
electronicAddress: val(party, "cbc:EndpointID"),
|
|
253
|
-
address: parseAddressUbl(
|
|
243
|
+
address: parseAddressUbl(inner(party, "cac:PostalAddress") ?? ""),
|
|
254
244
|
contact: parseContactUbl(inner(party, "cac:Contact")),
|
|
255
245
|
};
|
|
256
246
|
}
|
|
257
247
|
function parseLineUbl(s, index) {
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
const tax = (_c = inner(item, "cac:ClassifiedTaxCategory")) !== null && _c !== void 0 ? _c : "";
|
|
248
|
+
const item = inner(s, "cac:Item") ?? "";
|
|
249
|
+
const price = inner(s, "cac:Price") ?? "";
|
|
250
|
+
const tax = inner(item, "cac:ClassifiedTaxCategory") ?? "";
|
|
262
251
|
const id = val(s, "cbc:ID");
|
|
263
252
|
const base = val(price, "cbc:BaseQuantity");
|
|
264
253
|
return {
|
|
265
254
|
id: id !== undefined && id !== String(index + 1) ? id : undefined, // omit the auto-number
|
|
266
|
-
name:
|
|
255
|
+
name: val(item, "cbc:Name") ?? "",
|
|
267
256
|
description: val(item, "cbc:Description"),
|
|
268
257
|
sellerItemId: val(inner(item, "cac:SellersItemIdentification"), "cbc:ID"),
|
|
269
258
|
buyerItemId: val(inner(item, "cac:BuyersItemIdentification"), "cbc:ID"),
|
|
270
259
|
standardItemId: val(inner(item, "cac:StandardItemIdentification"), "cbc:ID"),
|
|
271
260
|
quantity: num(val(s, "cbc:InvoicedQuantity")),
|
|
272
|
-
unit:
|
|
261
|
+
unit: attr(s, "cbc:InvoicedQuantity", "unitCode") ?? "",
|
|
273
262
|
netUnitPrice: num(val(price, "cbc:PriceAmount")),
|
|
274
263
|
priceBaseQuantity: base !== undefined ? num(base) : undefined,
|
|
275
264
|
vat: {
|
|
276
|
-
category: (
|
|
265
|
+
category: (val(tax, "cbc:ID") ?? "S"),
|
|
277
266
|
ratePercent: num(val(tax, "cbc:Percent")),
|
|
278
267
|
},
|
|
279
268
|
note: val(s, "cbc:Note"),
|
|
280
269
|
};
|
|
281
270
|
}
|
|
282
271
|
function parseAllowanceUbl(ac) {
|
|
283
|
-
var _a;
|
|
284
272
|
const cat = inner(ac, "cac:TaxCategory");
|
|
285
273
|
return {
|
|
286
274
|
isCharge: val(ac, "cbc:ChargeIndicator") === "true",
|
|
@@ -288,7 +276,7 @@ function parseAllowanceUbl(ac) {
|
|
|
288
276
|
reason: val(ac, "cbc:AllowanceChargeReason"),
|
|
289
277
|
reasonCode: val(ac, "cbc:AllowanceChargeReasonCode"),
|
|
290
278
|
vat: {
|
|
291
|
-
category: (
|
|
279
|
+
category: (val(cat, "cbc:ID") ?? "S"),
|
|
292
280
|
ratePercent: num(val(cat, "cbc:Percent")),
|
|
293
281
|
},
|
|
294
282
|
};
|
|
@@ -307,12 +295,11 @@ function exemptionsUbl(xml) {
|
|
|
307
295
|
}
|
|
308
296
|
/** Parse an OASIS UBL invoice (EN16931 / PEPPOL / XRechnung-UBL) into the Invoice model. */
|
|
309
297
|
export function parseUBL(xml) {
|
|
310
|
-
var _a, _b, _c, _d, _e;
|
|
311
298
|
// UBL has no head wrapper; the header fields are direct children before the supplier party
|
|
312
299
|
const cut = xml.indexOf("<cac:AccountingSupplierParty");
|
|
313
300
|
const head = cut >= 0 ? xml.slice(0, cut) : xml;
|
|
314
|
-
const seller = parsePartyUbl(
|
|
315
|
-
const buyer = parsePartyUbl(
|
|
301
|
+
const seller = parsePartyUbl(inner(xml, "cac:AccountingSupplierParty") ?? "");
|
|
302
|
+
const buyer = parsePartyUbl(inner(xml, "cac:AccountingCustomerParty") ?? "");
|
|
316
303
|
const type = num(val(head, "cbc:InvoiceTypeCode"));
|
|
317
304
|
const notes = innerAll(head, "cbc:Note")
|
|
318
305
|
.map(unesc)
|
|
@@ -348,10 +335,10 @@ export function parseUBL(xml) {
|
|
|
348
335
|
const li = xml.indexOf("<cac:InvoiceLine");
|
|
349
336
|
const allowancesCharges = innerAll(li >= 0 ? xml.slice(0, li) : xml, "cac:AllowanceCharge").map(parseAllowanceUbl);
|
|
350
337
|
return {
|
|
351
|
-
number:
|
|
352
|
-
issueDate:
|
|
338
|
+
number: val(head, "cbc:ID") ?? "",
|
|
339
|
+
issueDate: val(head, "cbc:IssueDate") ?? "",
|
|
353
340
|
type: type && type !== 380 ? type : undefined,
|
|
354
|
-
currency:
|
|
341
|
+
currency: val(head, "cbc:DocumentCurrencyCode") ?? "",
|
|
355
342
|
dueDate: val(head, "cbc:DueDate"),
|
|
356
343
|
buyerReference: val(head, "cbc:BuyerReference"),
|
|
357
344
|
purchaseOrderRef: val(inner(head, "cac:OrderReference"), "cbc:ID"),
|
package/dist/core/pdfa.js
CHANGED
|
@@ -3,15 +3,14 @@
|
|
|
3
3
|
// layer that actually trips e-invoice PDFs in practice: the rules our own writer satisfies, read
|
|
4
4
|
// backwards. Great as a regression guard for us and a fast local signal for foreign PDFs.
|
|
5
5
|
export function checkPdfA3(pdf) {
|
|
6
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
7
6
|
const s = Buffer.from(pdf).toString("latin1");
|
|
8
|
-
const xmp =
|
|
7
|
+
const xmp = s.match(/<x:xmpmeta[\s\S]*?<\/x:xmpmeta>/)?.[0] ?? "";
|
|
9
8
|
const checks = [];
|
|
10
9
|
const add = (id, label, ok, detail) => {
|
|
11
10
|
checks.push({ id, label, ok, detail });
|
|
12
11
|
};
|
|
13
12
|
// header + binary marker (≥4 bytes > 127 on the comment line) - required for PDF/A
|
|
14
|
-
const version =
|
|
13
|
+
const version = s.match(/^%PDF-(\d\.\d)/)?.[1];
|
|
15
14
|
add("header", "PDF header & version", !!version, version ? `%PDF-${version}` : "missing");
|
|
16
15
|
const afterHeader = s.slice(s.indexOf("\n") + 1, s.indexOf("\n") + 9);
|
|
17
16
|
const binaryMarker = afterHeader.startsWith("%") &&
|
|
@@ -25,12 +24,13 @@ export function checkPdfA3(pdf) {
|
|
|
25
24
|
add("file-id", "document /ID present", /\/ID\s*\[\s*<[0-9A-Fa-f]/.test(s));
|
|
26
25
|
// XMP metadata + PDF/A identification
|
|
27
26
|
add("xmp", "XMP metadata present", xmp.length > 0);
|
|
28
|
-
const part =
|
|
27
|
+
const part = xmp.match(/pdfaid:part[^0-9]{0,4}(\d)/)?.[1];
|
|
29
28
|
add("pdfa-part", "declares PDF/A part 3", part === "3", part ? `part ${part}` : "missing");
|
|
30
|
-
const conformance =
|
|
29
|
+
const conformance = xmp.match(/pdfaid:conformance[^ABU]{0,4}([ABU])/)?.[1];
|
|
31
30
|
add("pdfa-conformance", "PDF/A conformance level", !!conformance, conformance ? `level ${conformance}` : "missing");
|
|
32
31
|
// Factur-X / ZUGFeRD XMP extension schema (the e-invoice fingerprint)
|
|
33
|
-
const fxFile =
|
|
32
|
+
const fxFile = xmp.match(/DocumentFileName[^>]*>\s*([^<\s]+\.xml)/i)?.[1] ??
|
|
33
|
+
(/DocumentFileName/i.test(xmp) ? "?" : undefined);
|
|
34
34
|
add("facturx-xmp", "Factur-X/ZUGFeRD XMP extension", /ConformanceLevel/i.test(xmp) && /DocumentType/i.test(xmp), fxFile);
|
|
35
35
|
// PDF/A OutputIntent with an embedded ICC profile (needed once colour/transparency is used)
|
|
36
36
|
add("output-intent", "PDF/A OutputIntent (sRGB ICC)", /\/OutputIntent/.test(s) && /\/DestOutputProfile/.test(s) && /\/S\s*\/GTS_PDFA1/.test(s));
|
|
@@ -40,8 +40,8 @@ export function checkPdfA3(pdf) {
|
|
|
40
40
|
/\/EF\s*<</.test(s));
|
|
41
41
|
// every font must be embedded (no font descriptor without a font program)
|
|
42
42
|
// count the descriptor objects (/Type /FontDescriptor), not the /FontDescriptor ref entries
|
|
43
|
-
const descriptors = (
|
|
44
|
-
const programs = (
|
|
43
|
+
const descriptors = (s.match(/\/Type\s*\/FontDescriptor\b/g) ?? []).length;
|
|
44
|
+
const programs = (s.match(/\/FontFile[23]?\b/g) ?? []).length;
|
|
45
45
|
add("fonts-embedded", "all fonts embedded", descriptors === 0 || programs >= descriptors, `${programs}/${descriptors} programs`);
|
|
46
46
|
return { ok: checks.every((c) => c.ok), checks };
|
|
47
47
|
}
|
package/dist/core/read.js
CHANGED
|
@@ -15,7 +15,7 @@ export function readInvoice(data) {
|
|
|
15
15
|
invoice = parseInvoice(xml); // throws for not-yet-supported syntaxes
|
|
16
16
|
totals = computeInvoice(invoice);
|
|
17
17
|
}
|
|
18
|
-
catch
|
|
18
|
+
catch {
|
|
19
19
|
/* leave the parsed view empty - the raw XML + detection still work */
|
|
20
20
|
}
|
|
21
21
|
return { isPdf, xml, meta, invoice, totals };
|
package/dist/core/validate.js
CHANGED
|
@@ -20,10 +20,9 @@ export function profileFor(meta) {
|
|
|
20
20
|
return ubl ? "en16931-ubl" : "en16931-cii";
|
|
21
21
|
}
|
|
22
22
|
function runRuleSet(set, xml) {
|
|
23
|
-
var _a;
|
|
24
23
|
const sef = gunzipSync(readFileSync(join(RULES_DIR, `${set}.sef.json.gz`))).toString("utf-8");
|
|
25
24
|
const out = SaxonJS.transform({ stylesheetText: sef, sourceText: xml, destination: "serialized" }, "sync");
|
|
26
|
-
return parseSvrl(
|
|
25
|
+
return parseSvrl(out.principalResult ?? "");
|
|
27
26
|
}
|
|
28
27
|
/** Validate invoice XML against a profile's Schematron rules → a structured pass/fail report. */
|
|
29
28
|
export function validateInvoiceXml(xml, profile) {
|
|
@@ -39,17 +38,16 @@ export function validateInvoiceXml(xml, profile) {
|
|
|
39
38
|
// The rules emit SVRL (Schematron Validation Report Language). Each <svrl:failed-assert> is a violation;
|
|
40
39
|
// flag="warning" downgrades it. Regex is enough for this well-formed machine output.
|
|
41
40
|
function parseSvrl(svrl) {
|
|
42
|
-
var _a, _b, _c, _d, _e;
|
|
43
41
|
const errors = [];
|
|
44
42
|
const warnings = [];
|
|
45
43
|
const re = /<svrl:failed-assert\b([^>]*)>([\s\S]*?)<\/svrl:failed-assert>/g;
|
|
46
44
|
for (let m = re.exec(svrl); m; m = re.exec(svrl)) {
|
|
47
|
-
const flag =
|
|
48
|
-
const text = (
|
|
45
|
+
const flag = attr(m[1], "flag") ?? attr(m[1], "role") ?? "fatal";
|
|
46
|
+
const text = (m[2].match(/<svrl:text>([\s\S]*?)<\/svrl:text>/)?.[1] ?? "")
|
|
49
47
|
.replace(/\s+/g, " ")
|
|
50
48
|
.trim();
|
|
51
49
|
const violation = {
|
|
52
|
-
id:
|
|
50
|
+
id: text.match(/\[((?:BR|BG)-?[A-Z0-9-]+)\]/)?.[1],
|
|
53
51
|
test: attr(m[1], "test"),
|
|
54
52
|
location: attr(m[1], "location"),
|
|
55
53
|
text,
|
|
@@ -60,6 +58,5 @@ function parseSvrl(svrl) {
|
|
|
60
58
|
return { errors, warnings };
|
|
61
59
|
}
|
|
62
60
|
function attr(s, name) {
|
|
63
|
-
|
|
64
|
-
return (_a = s.match(new RegExp(`\\b${name}="([^"]*)"`))) === null || _a === void 0 ? void 0 : _a[1];
|
|
61
|
+
return s.match(new RegExp(`\\b${name}="([^"]*)"`))?.[1];
|
|
65
62
|
}
|
package/dist/core/verapdf.js
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
-
});
|
|
9
|
-
};
|
|
10
1
|
import { spawnSync } from "node:child_process";
|
|
11
2
|
import { existsSync, mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
12
3
|
import { join } from "node:path";
|
|
@@ -31,22 +22,20 @@ export function findVerapdf() {
|
|
|
31
22
|
}
|
|
32
23
|
/** What's available right now - drives the `jasy verapdf` doctor. */
|
|
33
24
|
export function detectTools() {
|
|
34
|
-
var _a, _b, _c, _d;
|
|
35
25
|
const jv = spawnSync("java", ["-version"], { encoding: "utf-8" }); // java prints the version to stderr
|
|
36
26
|
const java = jv.error
|
|
37
27
|
? undefined
|
|
38
|
-
: ((
|
|
28
|
+
: ((jv.stderr || jv.stdout).match(/version "([^"]+)"/)?.[1] ?? "found");
|
|
39
29
|
const verapdfPath = findVerapdf();
|
|
40
30
|
let verapdf;
|
|
41
31
|
if (verapdfPath) {
|
|
42
32
|
const vv = spawnSync(verapdfPath, ["--version"], { encoding: "utf-8" });
|
|
43
|
-
verapdf = vv.error ? undefined : (
|
|
33
|
+
verapdf = vv.error ? undefined : (vv.stdout.match(/veraPDF (\S+)/)?.[1] ?? "found");
|
|
44
34
|
}
|
|
45
35
|
return { java, verapdf, verapdfPath };
|
|
46
36
|
}
|
|
47
37
|
/** Parse veraPDF's `--format xml` report (the real 1.30 shape - verified against live output). */
|
|
48
38
|
export function parseVeraReport(xml) {
|
|
49
|
-
var _a;
|
|
50
39
|
const ok = /isCompliant="true"/.test(xml);
|
|
51
40
|
const det = xml.match(/passedRules="(\d+)"\s+failedRules="(\d+)"/);
|
|
52
41
|
const failures = [
|
|
@@ -54,7 +43,7 @@ export function parseVeraReport(xml) {
|
|
|
54
43
|
].map((m) => ({ clause: m[1], failedChecks: Number(m[2]) }));
|
|
55
44
|
return {
|
|
56
45
|
ok,
|
|
57
|
-
profile:
|
|
46
|
+
profile: xml.match(/profileName="([^"]*)"/)?.[1],
|
|
58
47
|
passedRules: det ? Number(det[1]) : undefined,
|
|
59
48
|
failedRules: det ? Number(det[2]) : undefined,
|
|
60
49
|
failures,
|
|
@@ -62,7 +51,6 @@ export function parseVeraReport(xml) {
|
|
|
62
51
|
}
|
|
63
52
|
/** Run veraPDF on a PDF (auto-detecting the PDF/A flavour) and return the parsed report. */
|
|
64
53
|
export function runVeraPdf(file, bin = findVerapdf()) {
|
|
65
|
-
var _a;
|
|
66
54
|
if (!bin)
|
|
67
55
|
throw new Error("veraPDF is not installed - run `jasy verapdf --install`");
|
|
68
56
|
const r = spawnSync(bin, ["--format", "xml", file], {
|
|
@@ -71,7 +59,7 @@ export function runVeraPdf(file, bin = findVerapdf()) {
|
|
|
71
59
|
});
|
|
72
60
|
if (r.error)
|
|
73
61
|
throw r.error; // e.g. Java missing - the verapdf launcher couldn't start
|
|
74
|
-
const xml =
|
|
62
|
+
const xml = r.stdout ?? "";
|
|
75
63
|
if (!xml.includes("<validationReport")) {
|
|
76
64
|
throw new Error("veraPDF returned no report" + (r.stderr ? `: ${r.stderr.split("\n")[0]}` : ""));
|
|
77
65
|
}
|
|
@@ -122,40 +110,38 @@ function autoInstallXml(home) {
|
|
|
122
110
|
</AutomatedInstallation>`;
|
|
123
111
|
}
|
|
124
112
|
/** Download + headless-install veraPDF into ~/.jasy/verapdf. `log` streams progress to the UI. */
|
|
125
|
-
export function installVeraPdf(log) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
throw new Error("install did not complete: " + (r.stderr || r.stdout || "").split("\n").slice(-3).join(" "));
|
|
154
|
-
}
|
|
155
|
-
return MANAGED_BIN;
|
|
156
|
-
}
|
|
157
|
-
finally {
|
|
158
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
113
|
+
export async function installVeraPdf(log) {
|
|
114
|
+
const tools = detectTools();
|
|
115
|
+
if (!tools.java) {
|
|
116
|
+
throw new Error("Java is required (veraPDF is a Java app). Install a JRE 11+ first, then re-run `jasy verapdf --install`.");
|
|
117
|
+
}
|
|
118
|
+
log(`Java ${tools.java} found.`);
|
|
119
|
+
const tmp = mkdtempSync(join(tmpdir(), "jasy-vera-"));
|
|
120
|
+
try {
|
|
121
|
+
log("Downloading veraPDF (~33 MB) ...");
|
|
122
|
+
const res = await fetch(INSTALLER_URL);
|
|
123
|
+
if (!res.ok)
|
|
124
|
+
throw new Error(`download failed: HTTP ${res.status}`);
|
|
125
|
+
const zip = Buffer.from(await res.arrayBuffer());
|
|
126
|
+
log("Extracting the installer ...");
|
|
127
|
+
const jar = extractFromZip(zip, /izpack-installer-[^/]*\.jar$/);
|
|
128
|
+
if (!jar)
|
|
129
|
+
throw new Error("could not find the veraPDF installer inside the download");
|
|
130
|
+
const jarPath = join(tmp, "installer.jar");
|
|
131
|
+
const xmlPath = join(tmp, "auto-install.xml");
|
|
132
|
+
writeFileSync(jarPath, jar);
|
|
133
|
+
writeFileSync(xmlPath, autoInstallXml(VERAPDF_HOME));
|
|
134
|
+
rmSync(VERAPDF_HOME, { recursive: true, force: true }); // clean (re)install
|
|
135
|
+
log(`Installing into ${VERAPDF_HOME} ...`);
|
|
136
|
+
const r = spawnSync("java", ["-Djava.awt.headless=true", "-jar", jarPath, xmlPath], {
|
|
137
|
+
encoding: "utf-8",
|
|
138
|
+
});
|
|
139
|
+
if (!existsSync(MANAGED_BIN)) {
|
|
140
|
+
throw new Error("install did not complete: " + (r.stderr || r.stdout || "").split("\n").slice(-3).join(" "));
|
|
159
141
|
}
|
|
160
|
-
|
|
142
|
+
return MANAGED_BIN;
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
146
|
+
}
|
|
161
147
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
2
|
import { readCommand } from "./commands/read.js";
|
|
12
3
|
import { validateCommand } from "./commands/validate.js";
|
|
13
4
|
import { exportCommand } from "./commands/export.js";
|
|
@@ -19,41 +10,38 @@ const argv = process.argv.slice(2);
|
|
|
19
10
|
const cmd = argv[0];
|
|
20
11
|
// run a command and return true (then we exit), or open the interactive TUI and return false (it owns
|
|
21
12
|
// the process). Wrapped in an async fn so `verapdf` can await without needing top-level await.
|
|
22
|
-
function dispatch() {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return false;
|
|
51
|
-
});
|
|
13
|
+
async function dispatch() {
|
|
14
|
+
if (cmd === "read") {
|
|
15
|
+
readCommand(argv.slice(1));
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
if (cmd === "validate") {
|
|
19
|
+
validateCommand(argv.slice(1));
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
if (cmd === "export") {
|
|
23
|
+
exportCommand(argv.slice(1));
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
if (cmd === "verapdf") {
|
|
27
|
+
await verapdfCommand(argv.slice(1));
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
// shorthand: `jasy some-invoice.pdf` == `jasy read some-invoice.pdf`
|
|
31
|
+
if (cmd && /\.(pdf|xml)$/i.test(cmd)) {
|
|
32
|
+
readCommand(argv);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (cmd === "-h" || cmd === "--help") {
|
|
36
|
+
printHelp();
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
launchTui();
|
|
40
|
+
return false;
|
|
52
41
|
}
|
|
53
42
|
void dispatch().then((done) => {
|
|
54
|
-
var _a;
|
|
55
43
|
if (done)
|
|
56
|
-
process.exit(
|
|
44
|
+
process.exit(process.exitCode ?? 0);
|
|
57
45
|
});
|
|
58
46
|
function printHelp() {
|
|
59
47
|
console.log(`jasy - ZUGFeRD / XRechnung terminal
|
package/dist/tui/app.js
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
-
});
|
|
9
|
-
};
|
|
10
1
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
11
2
|
import { basename, resolve, dirname, join } from "node:path";
|
|
12
3
|
import { fileURLToPath } from "node:url";
|
|
@@ -36,7 +27,7 @@ const CRANE = (() => {
|
|
|
36
27
|
.split("\n")
|
|
37
28
|
.map((l) => l.replace(/\s+$/, "")); // keep leading spaces (alignment), drop trailing
|
|
38
29
|
}
|
|
39
|
-
catch
|
|
30
|
+
catch {
|
|
40
31
|
return []; // no logo file - the start screen just shows the box
|
|
41
32
|
}
|
|
42
33
|
})();
|
|
@@ -124,7 +115,7 @@ export function launchTui() {
|
|
|
124
115
|
}
|
|
125
116
|
// XML business rules (Schematron) - EN 16931, + XRechnung BR-DE when applicable
|
|
126
117
|
rows.push({
|
|
127
|
-
text:
|
|
118
|
+
text: rules?.profile.startsWith("xrechnung") ? "XRechnung rules" : "EN 16931 rules",
|
|
128
119
|
fg: INK,
|
|
129
120
|
status: rules ? (rules.valid ? "OK" : `${rules.errors.length} errors`) : "n/a",
|
|
130
121
|
statusFg: rules ? (rules.valid ? OK : ERR) : FAINT,
|
|
@@ -181,7 +172,7 @@ export function launchTui() {
|
|
|
181
172
|
const statusEnd = x + w - 3;
|
|
182
173
|
const header = buildHeader(w); // pinned
|
|
183
174
|
const body = buildBody(w); // scrollable
|
|
184
|
-
const canExport = !!(
|
|
175
|
+
const canExport = !!(loaded?.read.invoice && loaded.read.totals);
|
|
185
176
|
// the crane sits above the box - only on the true start screen (nothing loaded) and only if it fits
|
|
186
177
|
const showCrane = !loaded && !error && CRANE.length > 0 && cols >= CRANE_W && screenH >= CRANE.length + 9;
|
|
187
178
|
const boxTop = TOP + (showCrane ? CRANE.length + 1 : 0);
|
|
@@ -196,10 +187,9 @@ export function launchTui() {
|
|
|
196
187
|
const visible = body.slice(scroll, scroll + viewH);
|
|
197
188
|
const boxH = 5 + headBlock + viewH + (notice ? 1 : 0);
|
|
198
189
|
const drawRow = (r, y) => {
|
|
199
|
-
var _a;
|
|
200
190
|
draw.text(x + 2, y, r.text, { fg: r.fg });
|
|
201
191
|
if (r.status)
|
|
202
|
-
draw.text(statusEnd - r.status.length, y, r.status, { fg:
|
|
192
|
+
draw.text(statusEnd - r.status.length, y, r.status, { fg: r.statusFg ?? r.fg });
|
|
203
193
|
};
|
|
204
194
|
draw.clear();
|
|
205
195
|
if (showCrane)
|
|
@@ -255,8 +245,8 @@ export function launchTui() {
|
|
|
255
245
|
process.exit(0);
|
|
256
246
|
}
|
|
257
247
|
function exportTo(format) {
|
|
258
|
-
const inv = loaded
|
|
259
|
-
if (!inv || !
|
|
248
|
+
const inv = loaded?.read.invoice;
|
|
249
|
+
if (!inv || !loaded?.read.totals)
|
|
260
250
|
return;
|
|
261
251
|
const name = `${inv.number.replace(/[^\w.-]+/g, "_")}.${format === "txt" ? "txt" : format}`;
|
|
262
252
|
try {
|
|
@@ -268,35 +258,33 @@ export function launchTui() {
|
|
|
268
258
|
}
|
|
269
259
|
render();
|
|
270
260
|
}
|
|
271
|
-
function openFlow() {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
rules = null;
|
|
288
|
-
}
|
|
261
|
+
async function openFlow() {
|
|
262
|
+
const chosen = await openFileDialog({ screen, draw, input, startDir: process.cwd(), quit });
|
|
263
|
+
if (chosen) {
|
|
264
|
+
notice = null;
|
|
265
|
+
scroll = 0;
|
|
266
|
+
try {
|
|
267
|
+
const bytes = readFileSync(chosen);
|
|
268
|
+
const read = readInvoice(bytes);
|
|
269
|
+
const pdfa = read.isPdf ? checkPdfA3(bytes) : null;
|
|
270
|
+
let rules = null;
|
|
271
|
+
if (read.meta.syntax !== "unknown") {
|
|
272
|
+
try {
|
|
273
|
+
rules = validateInvoiceXml(read.xml, profileFor(read.meta));
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
rules = null;
|
|
289
277
|
}
|
|
290
|
-
loaded = { path: chosen, read, pdfa, rules };
|
|
291
|
-
error = null;
|
|
292
|
-
}
|
|
293
|
-
catch (e) {
|
|
294
|
-
error = e.message;
|
|
295
|
-
loaded = null;
|
|
296
278
|
}
|
|
279
|
+
loaded = { path: chosen, read, pdfa, rules };
|
|
280
|
+
error = null;
|
|
297
281
|
}
|
|
298
|
-
|
|
299
|
-
|
|
282
|
+
catch (e) {
|
|
283
|
+
error = e.message;
|
|
284
|
+
loaded = null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
render();
|
|
300
288
|
}
|
|
301
289
|
screen.enter();
|
|
302
290
|
screen.hideCursor();
|
|
@@ -312,7 +300,7 @@ export function launchTui() {
|
|
|
312
300
|
void openFlow();
|
|
313
301
|
return true;
|
|
314
302
|
}
|
|
315
|
-
if ((key.name === "j" || key.name === "t" || key.name === "x") &&
|
|
303
|
+
if ((key.name === "j" || key.name === "t" || key.name === "x") && loaded?.read.invoice) {
|
|
316
304
|
exportTo(key.name === "j" ? "json" : key.name === "t" ? "txt" : "xlsx");
|
|
317
305
|
return true;
|
|
318
306
|
}
|
package/dist/tui/file-open.js
CHANGED
|
@@ -34,7 +34,7 @@ export function openFileDialog({ screen, draw, input, startDir, quit, }) {
|
|
|
34
34
|
});
|
|
35
35
|
items = [...up, ...entries];
|
|
36
36
|
}
|
|
37
|
-
catch
|
|
37
|
+
catch {
|
|
38
38
|
items = up;
|
|
39
39
|
}
|
|
40
40
|
state = { selectedIndex: 0, scrollOffset: 0 };
|
|
@@ -115,7 +115,7 @@ export function openFileDialog({ screen, draw, input, startDir, quit, }) {
|
|
|
115
115
|
if (statSync(path).isFile())
|
|
116
116
|
close(path);
|
|
117
117
|
}
|
|
118
|
-
catch
|
|
118
|
+
catch {
|
|
119
119
|
/* nothing to open */
|
|
120
120
|
}
|
|
121
121
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jasy/cli",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.4",
|
|
4
4
|
"description": "Interactive terminal to validate, read and export ZUGFeRD / XRechnung e-invoices.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@jano-editor/ui": "1.0.0-alpha.8",
|
|
26
26
|
"saxon-js": "^2.6.0",
|
|
27
|
-
"@jasy/zugferd": "1.0.0-alpha.
|
|
27
|
+
"@jasy/zugferd": "1.0.0-alpha.2"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/node": "^25.9.3",
|