@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.
Files changed (35) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/einvoice.js +3 -2
  3. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +7 -0
  4. package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +19 -3
  5. package/dist_ts/formats/validation/conformance.harness.js +1 -1
  6. package/dist_ts/formats/validation/schematron.downloader.d.ts +43 -15
  7. package/dist_ts/formats/validation/schematron.downloader.js +122 -77
  8. package/dist_ts/formats/validation/schematron.integration.d.ts +5 -1
  9. package/dist_ts/formats/validation/schematron.integration.js +16 -40
  10. package/dist_ts/formats/validation/schematron.validator.d.ts +35 -15
  11. package/dist_ts/formats/validation/schematron.validator.js +136 -138
  12. package/dist_ts/formats/validation/schematron.worker.d.ts +12 -9
  13. package/dist_ts/formats/validation/schematron.worker.js +148 -108
  14. package/dist_ts/plugins.d.ts +3 -1
  15. package/dist_ts/plugins.js +7 -2
  16. package/dist_ts_install/download-schematron.js +6 -3
  17. package/dist_ts_install/index.js +15 -11
  18. package/package.json +6 -8
  19. package/ts/00_commitinfo_data.ts +1 -1
  20. package/ts/einvoice.ts +2 -1
  21. package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +19 -2
  22. package/ts/formats/validation/conformance.harness.ts +1 -1
  23. package/ts/formats/validation/schematron.downloader.ts +146 -97
  24. package/ts/formats/validation/schematron.integration.ts +18 -44
  25. package/ts/formats/validation/schematron.validator.ts +181 -181
  26. package/ts/formats/validation/schematron.worker.ts +191 -135
  27. package/ts/plugins.ts +8 -0
  28. package/ts/vendor/modules.d.ts +0 -19
  29. package/dist_ts/vendor/saxonjs.d.ts +0 -23
  30. package/dist_ts/vendor/saxonjs.js +0 -9
  31. package/readme.hints.md +0 -1134
  32. package/readme.howtofixtests.md +0 -38
  33. package/readme.literature.md +0 -1
  34. package/readme.plan.md +0 -497
  35. package/ts/vendor/saxonjs.ts +0 -36
@@ -1,190 +1,207 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import type { ValidationResult } from './validation.types.js';
3
- import { loadSaxonJS, type ISaxonJSTransformOptions, type ISaxonJSTransformResult } from '../../vendor/saxonjs.js';
4
3
 
5
4
  /**
6
5
  * Schematron validation options
7
6
  */
8
7
  export interface SchematronOptions {
9
8
  phase?: string; // Schematron phase to activate
10
- parameters?: Record<string, any>; // Parameters to pass to Schematron
11
9
  includeWarnings?: boolean; // Include warning-level messages
12
10
  maxErrors?: number; // Maximum errors before stopping
13
11
  }
14
12
 
15
- type TSchematronSourceKind = 'none' | 'schematron' | 'sef';
13
+ /**
14
+ * Turns one failed assertion or fired report into a ValidationResult.
15
+ */
16
+ export function schematronFailureToValidationResult(
17
+ failure: plugins.smartxmlSchematron.ISchematronFailure,
18
+ ): ValidationResult {
19
+ const flag = (failure.flag || failure.role || '').toLowerCase();
20
+ let severity: 'error' | 'warning' | 'info' = failure.kind === 'assert' ? 'error' : 'warning';
21
+ if (flag.includes('fatal') || flag.includes('error')) {
22
+ severity = 'error';
23
+ } else if (flag.includes('warning')) {
24
+ severity = 'warning';
25
+ } else if (flag.includes('info')) {
26
+ severity = 'info';
27
+ }
28
+
29
+ const btMatch = failure.message.match(/\[BT-(\d+)\]/);
30
+ const bgMatch = failure.message.match(/\[BG-(\d+)\]/);
31
+
32
+ return {
33
+ ruleId: failure.id || failure.patternId || 'UNKNOWN',
34
+ source: 'SCHEMATRON',
35
+ severity,
36
+ message: failure.message,
37
+ syntaxPath: failure.location,
38
+ expected: failure.test,
39
+ btReference: btMatch ? `BT-${btMatch[1]}` : undefined,
40
+ bgReference: bgMatch ? `BG-${bgMatch[1]}` : undefined,
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Turns a whole report into results. An expression the engine could not evaluate has
46
+ * proven nothing, so it is reported as an error rather than passing silently.
47
+ */
48
+ export function schematronReportToValidationResults(
49
+ report: plugins.smartxmlSchematron.ISchematronReport,
50
+ ): ValidationResult[] {
51
+ return [
52
+ ...report.failures.map(schematronFailureToValidationResult),
53
+ ...report.engineErrors.map((engineError) => ({
54
+ ruleId: 'SCHEMATRON-ENGINE-ERROR',
55
+ source: 'SCHEMATRON',
56
+ severity: 'error' as const,
57
+ message: `${engineError.message} (at ${engineError.origin})`,
58
+ expected: engineError.expression,
59
+ })),
60
+ ];
61
+ }
16
62
 
17
63
  /**
18
- * Schematron validation engine using Saxon-JS
19
- * Provides official standards validation through Schematron rules
64
+ * Applies the reporting options of a validation call to a result list.
65
+ */
66
+ export function applySchematronOptions(
67
+ results: ValidationResult[],
68
+ options: SchematronOptions,
69
+ ): ValidationResult[] {
70
+ const filtered = options.includeWarnings
71
+ ? results
72
+ : results.filter((result) => result.severity !== 'warning');
73
+
74
+ if (
75
+ options.maxErrors &&
76
+ filtered.filter((result) => result.severity === 'error').length > options.maxErrors
77
+ ) {
78
+ return filtered.slice(0, options.maxErrors);
79
+ }
80
+
81
+ return filtered;
82
+ }
83
+
84
+ /**
85
+ * Schematron validation engine.
86
+ *
87
+ * The rules are interpreted on the XPath engine of `@push.rocks/smartxml`, so the
88
+ * published `.sch` files of CEN, KoSIT and PEPPOL run as they are downloaded; no
89
+ * XSLT processor and no precompiled stylesheet is involved.
20
90
  */
21
91
  export class SchematronValidator {
22
92
  private schematronRules: string;
23
- private sourceKind: TSchematronSourceKind = 'none';
24
- private stylesheetFileName?: string;
25
- private stylesheetInternal?: unknown;
26
-
93
+ private schema?: plugins.smartxmlSchematron.Schematron;
94
+ private schemaError?: string;
95
+ private baseUri?: string;
96
+ /** one compiled schema per activated phase; a phase selects patterns at compile time */
97
+ private schemasByPhase = new Map<string, plugins.smartxmlSchematron.Schematron>();
98
+
27
99
  constructor(schematronRules?: string) {
28
100
  this.schematronRules = schematronRules || '';
101
+ if (this.schematronRules) {
102
+ this.compile();
103
+ }
29
104
  }
30
-
105
+
31
106
  /**
32
107
  * Load Schematron rules from file or string
33
108
  */
34
109
  public async loadSchematron(source: string, isFilePath: boolean = true): Promise<void> {
35
- let sourceContent: string;
110
+ this.schematronRules = isFilePath ? await plugins.fs.readFile(source, 'utf-8') : source;
111
+ this.baseUri = isFilePath ? source : undefined;
112
+ this.compile();
113
+ }
114
+
115
+ /**
116
+ * Compiles the loaded rules. A schema that does not compile keeps its reason, so
117
+ * validate() can report it instead of load failing far from the cause.
118
+ */
119
+ private compile(): void {
120
+ this.schema = undefined;
121
+ this.schemaError = undefined;
122
+ this.schemasByPhase.clear();
36
123
 
37
- if (isFilePath) {
38
- sourceContent = await plugins.fs.readFile(source, 'utf-8');
39
- } else {
40
- sourceContent = source;
124
+ const trimmed = this.schematronRules.trim();
125
+ if (!trimmed) {
126
+ return;
127
+ }
128
+ if (trimmed.startsWith('{')) {
129
+ this.schemaError =
130
+ 'the source is a compiled XSLT stylesheet (SEF); load the Schematron .sch file instead, it is executed directly';
131
+ return;
41
132
  }
42
133
 
43
- this.schematronRules = sourceContent;
44
- this.sourceKind = this.detectSourceKind(source, sourceContent, isFilePath);
45
- this.stylesheetFileName = undefined;
46
- this.stylesheetInternal = undefined;
134
+ try {
135
+ this.schema = this.compileForPhase(undefined);
136
+ this.schemasByPhase.set('', this.schema);
137
+ } catch (error) {
138
+ this.schemaError = error instanceof Error ? error.message : String(error);
139
+ }
140
+ }
47
141
 
48
- if (this.sourceKind === 'sef') {
49
- if (isFilePath) {
50
- this.stylesheetFileName = source;
51
- } else {
52
- this.stylesheetInternal = JSON.parse(sourceContent);
53
- }
142
+ private compileForPhase(phase: string | undefined): plugins.smartxmlSchematron.Schematron {
143
+ return plugins.smartxmlSchematron.Schematron.fromString(this.schematronRules, {
144
+ phase,
145
+ baseUri: this.baseUri,
146
+ // the published rule sets split their patterns over sch:include files
147
+ resolveInclude: (href, base) =>
148
+ plugins.readFileSync(
149
+ plugins.path.resolve(plugins.path.dirname(base ?? this.baseUri ?? '.'), href),
150
+ 'utf-8',
151
+ ),
152
+ });
153
+ }
154
+
155
+ /**
156
+ * The schema for one phase. A phase activates a subset of the patterns, which the
157
+ * engine resolves while compiling, so each phase gets its own compiled schema.
158
+ */
159
+ private schemaForPhase(phase?: string): plugins.smartxmlSchematron.Schematron {
160
+ const key = phase ?? '';
161
+ const cached = this.schemasByPhase.get(key);
162
+ if (cached) {
163
+ return cached;
54
164
  }
165
+ const compiled = this.compileForPhase(phase);
166
+ this.schemasByPhase.set(key, compiled);
167
+ return compiled;
55
168
  }
56
-
169
+
57
170
  /**
58
171
  * Validate an XML document against loaded Schematron rules
59
172
  */
60
173
  public async validate(
61
174
  xmlContent: string,
62
- options: SchematronOptions = {}
175
+ options: SchematronOptions = {},
63
176
  ): Promise<ValidationResult[]> {
64
177
  if (!this.schematronRules) {
65
178
  throw new Error('No Schematron rules loaded');
66
179
  }
67
-
68
- const results: ValidationResult[] = [];
69
180
 
70
- if (!this.canValidate()) {
71
- return [this.createUnavailableResult()];
181
+ if (!this.schema) {
182
+ return [this.createUnusableResult()];
72
183
  }
73
-
74
- try {
75
- const transformOptions: ISaxonJSTransformOptions = {
76
- sourceText: xmlContent,
77
- destination: 'serialized',
78
- stylesheetParams: options.parameters || {}
79
- };
80
-
81
- if (this.stylesheetFileName) {
82
- transformOptions.stylesheetFileName = this.stylesheetFileName;
83
- } else {
84
- transformOptions.stylesheetInternal = this.stylesheetInternal;
85
- }
86
184
 
87
- // Transform the XML with a precompiled Saxon-JS SEF stylesheet.
88
- const SaxonJS = await loadSaxonJS();
89
- const transformResult = await SaxonJS.transform(transformOptions, 'async') as ISaxonJSTransformResult;
90
-
91
- // Parse the SVRL (Schematron Validation Report Language) output
92
- results.push(...this.parseSVRL(transformResult.principalResult));
93
-
94
- // Apply options filters
95
- if (!options.includeWarnings) {
96
- return results.filter(r => r.severity !== 'warning');
97
- }
98
-
99
- if (options.maxErrors && results.filter(r => r.severity === 'error').length > options.maxErrors) {
100
- return results.slice(0, options.maxErrors);
101
- }
102
-
103
- return results;
185
+ let results: ValidationResult[] = [];
186
+ try {
187
+ results = schematronReportToValidationResults(
188
+ this.schemaForPhase(options.phase).validate(xmlContent),
189
+ );
104
190
  } catch (error) {
105
191
  const errorMessage = error instanceof Error ? error.message : String(error);
106
- results.push({
107
- ruleId: 'SCHEMATRON-ERROR',
108
- source: 'SCHEMATRON',
109
- severity: 'error',
110
- message: `Schematron validation failed: ${errorMessage}`,
111
- btReference: undefined,
112
- bgReference: undefined
113
- });
114
- return results;
192
+ return [
193
+ {
194
+ ruleId: 'SCHEMATRON-ERROR',
195
+ source: 'SCHEMATRON',
196
+ severity: 'error',
197
+ message: `Schematron validation failed: ${errorMessage}`,
198
+ },
199
+ ];
115
200
  }
201
+
202
+ return applySchematronOptions(results, options);
116
203
  }
117
-
118
- /**
119
- * Parse SVRL output to ValidationResult array
120
- */
121
- private parseSVRL(svrlXml: string): ValidationResult[] {
122
- const results: ValidationResult[] = [];
123
-
124
- // Parse SVRL XML
125
- const parser = new plugins.xmldom.DOMParser();
126
- const doc = parser.parseFromString(svrlXml, 'text/xml');
127
-
128
- // Get all failed assertions and successful reports
129
- const failedAsserts = doc.getElementsByTagName('svrl:failed-assert');
130
- const successfulReports = doc.getElementsByTagName('svrl:successful-report');
131
-
132
- // Process failed assertions (these are errors)
133
- for (let i = 0; i < failedAsserts.length; i++) {
134
- const assert = failedAsserts[i];
135
- const result = this.extractValidationResult(assert, 'error');
136
- if (result) results.push(result);
137
- }
138
-
139
- // Process successful reports (these can be warnings or info)
140
- for (let i = 0; i < successfulReports.length; i++) {
141
- const report = successfulReports[i];
142
- const result = this.extractValidationResult(report, 'warning');
143
- if (result) results.push(result);
144
- }
145
-
146
- return results;
147
- }
148
-
149
- /**
150
- * Extract ValidationResult from SVRL element
151
- */
152
- private extractValidationResult(
153
- element: Element,
154
- defaultSeverity: 'error' | 'warning'
155
- ): ValidationResult | null {
156
- const text = element.getElementsByTagName('svrl:text')[0]?.textContent || '';
157
- const location = element.getAttribute('location') || undefined;
158
- const test = element.getAttribute('test') || '';
159
- const id = element.getAttribute('id') || element.getAttribute('role') || 'UNKNOWN';
160
- const flag = element.getAttribute('flag') || defaultSeverity;
161
-
162
- // Determine severity from flag attribute
163
- let severity: 'error' | 'warning' | 'info' = defaultSeverity;
164
- if (flag.toLowerCase().includes('fatal') || flag.toLowerCase().includes('error')) {
165
- severity = 'error';
166
- } else if (flag.toLowerCase().includes('warning')) {
167
- severity = 'warning';
168
- } else if (flag.toLowerCase().includes('info')) {
169
- severity = 'info';
170
- }
171
-
172
- // Extract BT/BG references if present
173
- const btMatch = text.match(/\[BT-(\d+)\]/);
174
- const bgMatch = text.match(/\[BG-(\d+)\]/);
175
-
176
- return {
177
- ruleId: id,
178
- source: 'EN16931',
179
- severity,
180
- message: text,
181
- syntaxPath: location,
182
- btReference: btMatch ? `BT-${btMatch[1]}` : undefined,
183
- bgReference: bgMatch ? `BG-${bgMatch[1]}` : undefined,
184
- profile: 'EN16931'
185
- };
186
- }
187
-
204
+
188
205
  /**
189
206
  * Check if validator has rules loaded
190
207
  */
@@ -193,65 +210,48 @@ export class SchematronValidator {
193
210
  }
194
211
 
195
212
  /**
196
- * Check if loaded rules can be executed by Saxon-JS.
213
+ * Check whether the loaded rules compiled and can be executed.
197
214
  */
198
215
  public canValidate(): boolean {
199
- return this.sourceKind === 'sef' && Boolean(this.stylesheetFileName || this.stylesheetInternal);
216
+ return Boolean(this.schema);
200
217
  }
201
218
 
202
- private detectSourceKind(source: string, sourceContent: string, isFilePath: boolean): TSchematronSourceKind {
203
- const trimmed = sourceContent.trim();
204
- if (!trimmed) {
205
- return 'none';
206
- }
207
-
208
- if (isFilePath && /\.sef\.json$/i.test(source)) {
209
- return 'sef';
210
- }
211
-
212
- if (trimmed.startsWith('{')) {
213
- return 'sef';
214
- }
215
-
216
- return 'schematron';
217
- }
218
-
219
- private createUnavailableResult(): ValidationResult {
219
+ private createUnusableResult(): ValidationResult {
220
220
  return {
221
- ruleId: 'SCHEMATRON-UNCOMPILED',
221
+ ruleId: 'SCHEMATRON-UNUSABLE',
222
222
  source: 'SCHEMATRON',
223
223
  severity: 'error',
224
- message: 'Schematron validation requires a precompiled Saxon-JS SEF JSON stylesheet. Raw .sch files are loaded for inspection only and are not executed.',
225
- remediation: 'Compile Schematron rules to .sef.json with Saxon tooling and load the compiled SEF resource.'
224
+ message: `Schematron rules could not be compiled: ${this.schemaError ?? 'the source is empty'}`,
225
+ remediation: 'Load a Schematron schema whose sch:include files sit next to it.',
226
226
  };
227
227
  }
228
-
228
+
229
229
  /**
230
230
  * Get list of available phases from Schematron
231
231
  */
232
232
  public async getPhases(): Promise<string[]> {
233
233
  if (!this.schematronRules) return [];
234
-
234
+
235
235
  const parser = new plugins.xmldom.DOMParser();
236
236
  const doc = parser.parseFromString(this.schematronRules, 'text/xml');
237
237
  const phases = doc.getElementsByTagName('sch:phase');
238
-
238
+
239
239
  const phaseNames: string[] = [];
240
240
  for (let i = 0; i < phases.length; i++) {
241
241
  const id = phases[i].getAttribute('id');
242
242
  if (id) phaseNames.push(id);
243
243
  }
244
-
244
+
245
245
  return phaseNames;
246
246
  }
247
-
247
+
248
248
  /**
249
249
  * Validate with specific phase activated
250
250
  */
251
251
  public async validateWithPhase(
252
252
  xmlContent: string,
253
253
  phase: string,
254
- options: SchematronOptions = {}
254
+ options: SchematronOptions = {},
255
255
  ): Promise<ValidationResult[]> {
256
256
  return this.validate(xmlContent, { ...options, phase });
257
257
  }
@@ -261,10 +261,10 @@ export class SchematronValidator {
261
261
  * Factory function to create validator with standard Schematron packs
262
262
  */
263
263
  export async function createStandardValidator(
264
- standard: 'EN16931' | 'XRECHNUNG' | 'PEPPOL' | 'FACTURX'
264
+ standard: 'EN16931' | 'XRECHNUNG' | 'PEPPOL' | 'FACTURX',
265
265
  ): Promise<SchematronValidator> {
266
266
  const validator = new SchematronValidator();
267
-
267
+
268
268
  // Load appropriate Schematron based on standard
269
269
  // These paths would point to actual Schematron files in production
270
270
  switch (standard) {
@@ -285,7 +285,7 @@ export async function createStandardValidator(
285
285
  await validator.loadSchematron('assets_downloaded/schematron/facturx/Factur-X-EN16931-validation.sch');
286
286
  break;
287
287
  }
288
-
288
+
289
289
  return validator;
290
290
  }
291
291
 
@@ -295,27 +295,27 @@ export async function createStandardValidator(
295
295
  export class HybridValidator {
296
296
  private schematronValidator: SchematronValidator;
297
297
  private tsValidators: Array<{ validate: (xml: string) => ValidationResult[] }> = [];
298
-
298
+
299
299
  constructor(schematronValidator?: SchematronValidator) {
300
300
  this.schematronValidator = schematronValidator || new SchematronValidator();
301
301
  }
302
-
302
+
303
303
  /**
304
304
  * Add a TypeScript validator to the pipeline
305
305
  */
306
306
  public addTSValidator(validator: { validate: (xml: string) => ValidationResult[] }): void {
307
307
  this.tsValidators.push(validator);
308
308
  }
309
-
309
+
310
310
  /**
311
311
  * Run all validators and merge results
312
312
  */
313
313
  public async validate(
314
314
  xmlContent: string,
315
- options: SchematronOptions = {}
315
+ options: SchematronOptions = {},
316
316
  ): Promise<ValidationResult[]> {
317
317
  const results: ValidationResult[] = [];
318
-
318
+
319
319
  // Run TypeScript validators first (faster, better UX)
320
320
  for (const validator of this.tsValidators) {
321
321
  try {
@@ -325,7 +325,7 @@ export class HybridValidator {
325
325
  console.warn(`TS validator failed: ${errorMessage}`);
326
326
  }
327
327
  }
328
-
328
+
329
329
  // Run Schematron validation if available
330
330
  if (this.schematronValidator.hasRules()) {
331
331
  try {
@@ -336,10 +336,10 @@ export class HybridValidator {
336
336
  console.warn(`Schematron validation failed: ${errorMessage}`);
337
337
  }
338
338
  }
339
-
339
+
340
340
  // Deduplicate results by ruleId
341
341
  const seen = new Set<string>();
342
- return results.filter(r => {
342
+ return results.filter((r) => {
343
343
  if (seen.has(r.ruleId)) return false;
344
344
  seen.add(r.ruleId);
345
345
  return true;