@fin.cx/einvoice 6.2.0 → 7.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/einvoice.js +3 -2
- package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +7 -0
- package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +19 -3
- package/dist_ts/formats/validation/conformance.harness.js +1 -1
- package/dist_ts/formats/validation/schematron.downloader.d.ts +43 -15
- package/dist_ts/formats/validation/schematron.downloader.js +122 -77
- package/dist_ts/formats/validation/schematron.integration.d.ts +5 -1
- package/dist_ts/formats/validation/schematron.integration.js +16 -40
- package/dist_ts/formats/validation/schematron.validator.d.ts +35 -15
- package/dist_ts/formats/validation/schematron.validator.js +136 -138
- package/dist_ts/formats/validation/schematron.worker.d.ts +12 -9
- package/dist_ts/formats/validation/schematron.worker.js +148 -108
- package/dist_ts/plugins.d.ts +3 -1
- package/dist_ts/plugins.js +7 -2
- package/dist_ts_install/download-schematron.js +6 -3
- package/dist_ts_install/index.js +15 -11
- package/package.json +6 -8
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +2 -1
- package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +19 -2
- package/ts/formats/validation/conformance.harness.ts +1 -1
- package/ts/formats/validation/schematron.downloader.ts +146 -97
- package/ts/formats/validation/schematron.integration.ts +18 -44
- package/ts/formats/validation/schematron.validator.ts +181 -181
- package/ts/formats/validation/schematron.worker.ts +191 -135
- package/ts/plugins.ts +8 -0
- package/ts/vendor/modules.d.ts +0 -19
- package/dist_ts/vendor/saxonjs.d.ts +0 -23
- package/dist_ts/vendor/saxonjs.js +0 -9
- package/readme.hints.md +0 -1134
- package/readme.howtofixtests.md +0 -38
- package/readme.literature.md +0 -1
- package/readme.plan.md +0 -497
- package/ts/vendor/saxonjs.ts +0 -36
|
@@ -93,55 +93,140 @@ export class SchematronDownloader {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
|
-
* Download a Schematron file
|
|
96
|
+
* Download a Schematron file together with every file it includes.
|
|
97
|
+
*
|
|
98
|
+
* The published rule sets split their patterns over sch:include files, so a lone
|
|
99
|
+
* entry document cannot be validated with. The upstream directory layout is
|
|
100
|
+
* mirrored under the cache so those relative includes still resolve.
|
|
97
101
|
*/
|
|
98
102
|
public async download(source: SchematronSource): Promise<string> {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
const filePath = this.entryPathFor(source);
|
|
104
|
+
|
|
102
105
|
// Check if already cached
|
|
103
106
|
if (await this.isCached(filePath)) {
|
|
104
|
-
console.log(`Using cached Schematron: ${
|
|
107
|
+
console.log(`Using cached Schematron: ${source.name} v${source.version}`);
|
|
105
108
|
return filePath;
|
|
106
109
|
}
|
|
107
|
-
|
|
110
|
+
|
|
108
111
|
console.log(`Downloading Schematron: ${source.name} v${source.version}`);
|
|
109
|
-
|
|
112
|
+
|
|
110
113
|
try {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (!response.ok) {
|
|
114
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const content = await response.text();
|
|
118
|
-
|
|
119
|
-
// Validate it's actually Schematron
|
|
120
|
-
if (!content.includes('schematron') && !content.includes('sch:schema')) {
|
|
121
|
-
throw new Error('Downloaded file does not appear to be Schematron');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Save to cache
|
|
125
|
-
await fs.writeFile(filePath, content, 'utf-8');
|
|
126
|
-
|
|
114
|
+
const fileCount = await this.fetchWithIncludes(source.url, filePath, new Set<string>());
|
|
115
|
+
|
|
127
116
|
// Also save metadata
|
|
128
|
-
const metaPath = filePath.replace(
|
|
117
|
+
const metaPath = filePath.replace(/\.sch$/, '.meta.json');
|
|
129
118
|
await fs.writeFile(metaPath, JSON.stringify({
|
|
130
119
|
source: source.name,
|
|
131
120
|
version: source.version,
|
|
132
121
|
url: source.url,
|
|
133
122
|
format: source.format,
|
|
123
|
+
fileCount,
|
|
134
124
|
downloadDate: new Date().toISOString()
|
|
135
125
|
}, null, 2), 'utf-8');
|
|
136
|
-
|
|
137
|
-
|
|
126
|
+
|
|
127
|
+
await this.writeIndexEntry(source, filePath);
|
|
128
|
+
|
|
129
|
+
console.log(`Successfully downloaded: ${source.name} (${fileCount} file${fileCount === 1 ? '' : 's'})`);
|
|
138
130
|
return filePath;
|
|
139
131
|
} catch (error) {
|
|
140
132
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
141
133
|
throw new Error(`Failed to download ${source.name}: ${errorMessage}`);
|
|
142
134
|
}
|
|
143
135
|
}
|
|
144
|
-
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Where the entry document of a source lands: one directory per source and
|
|
139
|
+
* version, holding the upstream path so relative includes keep working.
|
|
140
|
+
*/
|
|
141
|
+
private entryPathFor(source: SchematronSource): string {
|
|
142
|
+
return path.join(this.cacheDir, `${source.name}-v${source.version}`, this.repositoryPath(source.url));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The part of a raw URL below the repository and its ref, e.g.
|
|
147
|
+
* `ubl/schematron/EN16931-UBL-validation.sch`.
|
|
148
|
+
*/
|
|
149
|
+
private repositoryPath(url: string): string {
|
|
150
|
+
const { pathname } = new URL(url);
|
|
151
|
+
const match = /\/raw\/[^/]+\/(.+)$/.exec(pathname);
|
|
152
|
+
return match ? match[1] : pathname.replace(/^\//, '');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Fetches one document and, recursively, every sch:include and sch:extends it
|
|
157
|
+
* names. Returns how many files were written.
|
|
158
|
+
*/
|
|
159
|
+
private async fetchWithIncludes(url: string, filePath: string, seen: Set<string>): Promise<number> {
|
|
160
|
+
if (seen.has(url)) {
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
seen.add(url);
|
|
164
|
+
|
|
165
|
+
const response = await fetch(url);
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText} for ${url}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const content = await response.text();
|
|
171
|
+
|
|
172
|
+
// Validate it's actually Schematron
|
|
173
|
+
if (!content.includes('schematron') && !content.includes('sch:schema')) {
|
|
174
|
+
throw new Error(`Downloaded file does not appear to be Schematron: ${url}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
178
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
179
|
+
|
|
180
|
+
let fileCount = 1;
|
|
181
|
+
for (const href of SchematronDownloader.extractIncludes(content)) {
|
|
182
|
+
const includePath = path.resolve(path.dirname(filePath), href);
|
|
183
|
+
if (!includePath.startsWith(path.resolve(this.cacheDir) + path.sep)) {
|
|
184
|
+
throw new Error(`Schematron include escapes the cache directory: ${href}`);
|
|
185
|
+
}
|
|
186
|
+
fileCount += await this.fetchWithIncludes(new URL(href, url).toString(), includePath, seen);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return fileCount;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The href of every sch:include and sch:extends in a schema, whatever prefix the
|
|
194
|
+
* document binds the Schematron namespace to.
|
|
195
|
+
*/
|
|
196
|
+
private static extractIncludes(content: string): string[] {
|
|
197
|
+
const hrefs: string[] = [];
|
|
198
|
+
const pattern = /<(?:[A-Za-z0-9_.-]+:)?(?:include|extends)\s+[^>]*href\s*=\s*"([^"]+)"/g;
|
|
199
|
+
let match: RegExpExecArray | null;
|
|
200
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
201
|
+
hrefs.push(match[1]);
|
|
202
|
+
}
|
|
203
|
+
return hrefs;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Records the entry document of each source, so a consumer finds it without
|
|
208
|
+
* guessing at file names.
|
|
209
|
+
*/
|
|
210
|
+
private async writeIndexEntry(source: SchematronSource, filePath: string): Promise<void> {
|
|
211
|
+
const indexPath = path.join(this.cacheDir, 'index.json');
|
|
212
|
+
let index: Record<string, unknown> = {};
|
|
213
|
+
try {
|
|
214
|
+
index = JSON.parse(await fs.readFile(indexPath, 'utf-8'));
|
|
215
|
+
} catch {
|
|
216
|
+
// no index yet
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
index[source.name] = {
|
|
220
|
+
entry: path.relative(this.cacheDir, filePath),
|
|
221
|
+
version: source.version,
|
|
222
|
+
format: source.format,
|
|
223
|
+
url: source.url,
|
|
224
|
+
downloadDate: new Date().toISOString(),
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
await fs.writeFile(indexPath, `${JSON.stringify(index, null, 2)}\n`, 'utf-8');
|
|
228
|
+
}
|
|
229
|
+
|
|
145
230
|
/**
|
|
146
231
|
* Download all Schematron files for a standard
|
|
147
232
|
*/
|
|
@@ -190,52 +275,56 @@ export class SchematronDownloader {
|
|
|
190
275
|
metadata: any;
|
|
191
276
|
}>> {
|
|
192
277
|
const files: Array<{ path: string; metadata: any }> = [];
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (entry.endsWith('.sch')) {
|
|
199
|
-
const filePath = path.join(this.cacheDir, entry);
|
|
200
|
-
const metaPath = filePath.replace('.sch', '.meta.json');
|
|
201
|
-
|
|
202
|
-
try {
|
|
203
|
-
const metadata = JSON.parse(await fs.readFile(metaPath, 'utf-8'));
|
|
204
|
-
files.push({ path: filePath, metadata });
|
|
205
|
-
} catch {
|
|
206
|
-
// No metadata file
|
|
207
|
-
files.push({ path: filePath, metadata: null });
|
|
208
|
-
}
|
|
209
|
-
}
|
|
278
|
+
|
|
279
|
+
for (const [name, entry] of Object.entries(await this.readIndex())) {
|
|
280
|
+
const filePath = path.join(this.cacheDir, (entry as { entry: string }).entry);
|
|
281
|
+
if (await this.isCached(filePath)) {
|
|
282
|
+
files.push({ path: filePath, metadata: { source: name, ...(entry as object) } });
|
|
210
283
|
}
|
|
211
|
-
} catch (error) {
|
|
212
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
213
|
-
console.warn(`Failed to list cached files: ${errorMessage}`);
|
|
214
284
|
}
|
|
215
|
-
|
|
285
|
+
|
|
216
286
|
return files;
|
|
217
287
|
}
|
|
218
|
-
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The index of downloaded entry documents; empty when nothing was downloaded yet.
|
|
291
|
+
*/
|
|
292
|
+
public async readIndex(): Promise<Record<string, { entry: string; version: string; format: string; url: string }>> {
|
|
293
|
+
try {
|
|
294
|
+
return JSON.parse(await fs.readFile(path.join(this.cacheDir, 'index.json'), 'utf-8'));
|
|
295
|
+
} catch {
|
|
296
|
+
return {};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* The entry document of one source, or null when it was not downloaded.
|
|
302
|
+
*/
|
|
303
|
+
public async getCachedEntry(sourceName: string): Promise<string | null> {
|
|
304
|
+
const entry = (await this.readIndex())[sourceName];
|
|
305
|
+
if (!entry) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
const filePath = path.join(this.cacheDir, entry.entry);
|
|
309
|
+
return (await this.isCached(filePath)) ? filePath : null;
|
|
310
|
+
}
|
|
311
|
+
|
|
219
312
|
/**
|
|
220
313
|
* Clear cache
|
|
221
314
|
*/
|
|
222
315
|
public async clearCache(): Promise<void> {
|
|
223
316
|
try {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
for (const entry of entries) {
|
|
227
|
-
if (entry.endsWith('.sch') || entry.endsWith('.meta.json')) {
|
|
228
|
-
await fs.unlink(path.join(this.cacheDir, entry));
|
|
229
|
-
}
|
|
317
|
+
for (const entry of await fs.readdir(this.cacheDir)) {
|
|
318
|
+
await fs.rm(path.join(this.cacheDir, entry), { recursive: true, force: true });
|
|
230
319
|
}
|
|
231
|
-
|
|
320
|
+
|
|
232
321
|
console.log('Schematron cache cleared');
|
|
233
322
|
} catch (error) {
|
|
234
323
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
235
324
|
console.warn(`Failed to clear cache: ${errorMessage}`);
|
|
236
325
|
}
|
|
237
326
|
}
|
|
238
|
-
|
|
327
|
+
|
|
239
328
|
/**
|
|
240
329
|
* Get the appropriate Schematron for a format
|
|
241
330
|
*/
|
|
@@ -265,43 +354,3 @@ export class SchematronDownloader {
|
|
|
265
354
|
console.log('All Schematron files updated');
|
|
266
355
|
}
|
|
267
356
|
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* ISO Schematron skeleton URLs
|
|
271
|
-
* These are needed to compile Schematron to XSLT
|
|
272
|
-
*/
|
|
273
|
-
export const ISO_SCHEMATRON_SKELETONS = {
|
|
274
|
-
'iso_dsdl_include.xsl': 'https://github.com/Schematron/schematron/raw/master/trunk/schematron/code/iso_dsdl_include.xsl',
|
|
275
|
-
'iso_abstract_expand.xsl': 'https://github.com/Schematron/schematron/raw/master/trunk/schematron/code/iso_abstract_expand.xsl',
|
|
276
|
-
'iso_svrl_for_xslt2.xsl': 'https://github.com/Schematron/schematron/raw/master/trunk/schematron/code/iso_svrl_for_xslt2.xsl',
|
|
277
|
-
'iso_schematron_skeleton_for_saxon.xsl': 'https://github.com/Schematron/schematron/raw/master/trunk/schematron/code/iso_schematron_skeleton_for_saxon.xsl'
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Download ISO Schematron skeleton files
|
|
282
|
-
*/
|
|
283
|
-
export async function downloadISOSkeletons(targetDir: string = 'assets_downloaded/schematron/iso'): Promise<void> {
|
|
284
|
-
await fs.mkdir(targetDir, { recursive: true });
|
|
285
|
-
|
|
286
|
-
console.log('Downloading ISO Schematron skeleton files...');
|
|
287
|
-
|
|
288
|
-
for (const [name, url] of Object.entries(ISO_SCHEMATRON_SKELETONS)) {
|
|
289
|
-
try {
|
|
290
|
-
const response = await fetch(url);
|
|
291
|
-
if (!response.ok) {
|
|
292
|
-
throw new Error(`HTTP ${response.status}`);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const content = await response.text();
|
|
296
|
-
const filePath = path.join(targetDir, name);
|
|
297
|
-
await fs.writeFile(filePath, content, 'utf-8');
|
|
298
|
-
|
|
299
|
-
console.log(`Downloaded: ${name}`);
|
|
300
|
-
} catch (error) {
|
|
301
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
302
|
-
console.warn(`Failed to download ${name}: ${errorMessage}`);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
console.log('ISO Schematron skeleton download complete');
|
|
307
|
-
}
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { SchematronValidator, HybridValidator } from './schematron.validator.js';
|
|
6
|
+
import { SchematronDownloader } from './schematron.downloader.js';
|
|
6
7
|
import { EN16931BusinessRulesValidator } from './en16931.business-rules.validator.js';
|
|
7
8
|
import { CodeListValidator } from './codelist.validator.js';
|
|
8
9
|
import type { ValidationResult, ValidationOptions, ValidationReport } from './validation.types.js';
|
|
9
10
|
import type { EInvoice } from '../../einvoice.js';
|
|
10
|
-
import * as path from 'path';
|
|
11
11
|
import { promises as fs } from 'fs';
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -66,57 +66,34 @@ export class IntegratedValidator {
|
|
|
66
66
|
|
|
67
67
|
await this.schematronValidator.loadSchematron(schematronPath, true);
|
|
68
68
|
if (!this.schematronValidator.canValidate()) {
|
|
69
|
-
throw new Error(`Schematron file
|
|
69
|
+
throw new Error(`Schematron file could not be compiled: ${schematronPath}`);
|
|
70
70
|
}
|
|
71
71
|
this.schematronLoaded = true;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
* Get the path to the appropriate Schematron file
|
|
75
|
+
* Get the path to the appropriate Schematron file.
|
|
76
|
+
*
|
|
77
|
+
* The downloader records the entry document of every rule set in its index, so the
|
|
78
|
+
* file is looked up by name rather than guessed from a pattern; an include of the
|
|
79
|
+
* rule set must never be mistaken for an entry document.
|
|
76
80
|
*/
|
|
77
81
|
private async getSchematronPath(
|
|
78
82
|
standard: 'EN16931' | 'PEPPOL' | 'XRECHNUNG',
|
|
79
83
|
format: 'UBL' | 'CII'
|
|
80
84
|
): Promise<string | null> {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
EN16931: {
|
|
86
|
-
UBL: 'EN16931-UBL-*.sch',
|
|
87
|
-
CII: 'EN16931-CII-*.sch'
|
|
88
|
-
},
|
|
89
|
-
PEPPOL: {
|
|
90
|
-
UBL: 'PEPPOL-EN16931-UBL-*.sch',
|
|
91
|
-
CII: 'PEPPOL-EN16931-CII-*.sch'
|
|
92
|
-
},
|
|
93
|
-
XRECHNUNG: {
|
|
94
|
-
UBL: 'XRechnung-UBL-*.sch',
|
|
95
|
-
CII: 'XRechnung-CII-*.sch'
|
|
96
|
-
}
|
|
85
|
+
const sourceNames: Record<string, Record<string, string>> = {
|
|
86
|
+
EN16931: { UBL: 'EN16931-UBL', CII: 'EN16931-CII' },
|
|
87
|
+
PEPPOL: { UBL: 'PEPPOL-EN16931-UBL', CII: 'PEPPOL-EN16931-CII' },
|
|
88
|
+
XRECHNUNG: { UBL: 'XRechnung-UBL', CII: 'XRechnung-CII' }
|
|
97
89
|
};
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
if (!
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
const files = await fs.readdir(basePath);
|
|
105
|
-
const regex = new RegExp(pattern.replace('*', '.*'));
|
|
106
|
-
const matches = files.filter(f => regex.test(f));
|
|
107
|
-
|
|
108
|
-
if (matches.length > 0) {
|
|
109
|
-
// Return the most recent version (lexicographically last)
|
|
110
|
-
matches.sort();
|
|
111
|
-
return path.join(basePath, matches[matches.length - 1]);
|
|
112
|
-
}
|
|
113
|
-
} catch {
|
|
114
|
-
// Directory doesn't exist
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return null;
|
|
90
|
+
|
|
91
|
+
const sourceName = sourceNames[standard]?.[format];
|
|
92
|
+
if (!sourceName) return null;
|
|
93
|
+
|
|
94
|
+
return new SchematronDownloader().getCachedEntry(sourceName);
|
|
118
95
|
}
|
|
119
|
-
|
|
96
|
+
|
|
120
97
|
/**
|
|
121
98
|
* Validate an invoice using all available validators
|
|
122
99
|
*/
|
|
@@ -142,10 +119,7 @@ export class IntegratedValidator {
|
|
|
142
119
|
if (xmlContent && this.schematronLoaded) {
|
|
143
120
|
try {
|
|
144
121
|
const schematronResults = await this.schematronValidator.validate(xmlContent, {
|
|
145
|
-
includeWarnings: !options.strictMode
|
|
146
|
-
parameters: {
|
|
147
|
-
profile: options.profile
|
|
148
|
-
}
|
|
122
|
+
includeWarnings: !options.strictMode
|
|
149
123
|
});
|
|
150
124
|
results.push(...schematronResults);
|
|
151
125
|
} catch (error) {
|