@fin.cx/einvoice 5.0.2 → 5.1.2

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 (116) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/einvoice.d.ts +4 -8
  3. package/dist_ts/einvoice.js +46 -7
  4. package/dist_ts/formats/converters/xml-to-einvoice.converter.d.ts +21 -0
  5. package/dist_ts/formats/converters/xml-to-einvoice.converter.js +127 -0
  6. package/dist_ts/formats/semantic/bt-bg.model.d.ts +445 -0
  7. package/dist_ts/formats/semantic/bt-bg.model.js +12 -0
  8. package/dist_ts/formats/semantic/semantic.adapter.d.ts +87 -0
  9. package/dist_ts/formats/semantic/semantic.adapter.js +538 -0
  10. package/dist_ts/formats/semantic/semantic.validator.d.ts +48 -0
  11. package/dist_ts/formats/semantic/semantic.validator.js +596 -0
  12. package/dist_ts/formats/utils/currency.calculator.decimal.d.ts +98 -0
  13. package/dist_ts/formats/utils/currency.calculator.decimal.js +212 -0
  14. package/dist_ts/formats/utils/currency.utils.d.ts +99 -0
  15. package/dist_ts/formats/utils/currency.utils.js +250 -0
  16. package/dist_ts/formats/utils/decimal.d.ts +151 -0
  17. package/dist_ts/formats/utils/decimal.js +435 -0
  18. package/dist_ts/formats/validation/codelist.validator.d.ts +49 -0
  19. package/dist_ts/formats/validation/codelist.validator.js +188 -0
  20. package/dist_ts/formats/validation/conformance.harness.d.ts +99 -0
  21. package/dist_ts/formats/validation/conformance.harness.js +471 -0
  22. package/dist_ts/formats/validation/en16931.business-rules.validator.d.ts +58 -0
  23. package/dist_ts/formats/validation/en16931.business-rules.validator.js +459 -0
  24. package/dist_ts/formats/validation/facturx.validator.d.ts +75 -0
  25. package/dist_ts/formats/validation/facturx.validator.js +522 -0
  26. package/dist_ts/formats/validation/integrated.validator.d.ts +82 -0
  27. package/dist_ts/formats/validation/integrated.validator.js +311 -0
  28. package/dist_ts/formats/validation/peppol.validator.d.ts +77 -0
  29. package/dist_ts/formats/validation/peppol.validator.js +529 -0
  30. package/dist_ts/formats/validation/schematron.downloader.d.ts +71 -0
  31. package/dist_ts/formats/validation/schematron.downloader.js +254 -0
  32. package/dist_ts/formats/validation/schematron.integration.d.ts +60 -0
  33. package/dist_ts/formats/validation/schematron.integration.js +230 -0
  34. package/dist_ts/formats/validation/schematron.validator.d.ts +79 -0
  35. package/dist_ts/formats/validation/schematron.validator.js +288 -0
  36. package/dist_ts/formats/validation/schematron.worker.d.ts +50 -0
  37. package/dist_ts/formats/validation/schematron.worker.js +185 -0
  38. package/dist_ts/formats/validation/validation.types.d.ts +163 -0
  39. package/dist_ts/formats/validation/validation.types.js +181 -0
  40. package/dist_ts/formats/validation/vat-categories.validator.d.ts +87 -0
  41. package/dist_ts/formats/validation/vat-categories.validator.js +497 -0
  42. package/dist_ts/formats/validation/xrechnung.validator.d.ts +92 -0
  43. package/dist_ts/formats/validation/xrechnung.validator.js +417 -0
  44. package/dist_ts/interfaces/common.d.ts +1 -0
  45. package/dist_ts/interfaces/en16931-metadata.d.ts +83 -0
  46. package/dist_ts/interfaces/en16931-metadata.js +6 -0
  47. package/dist_ts/plugins.d.ts +3 -1
  48. package/dist_ts/plugins.js +7 -2
  49. package/dist_ts_install/download-schematron.d.ts +6 -0
  50. package/dist_ts_install/download-schematron.js +62 -0
  51. package/dist_ts_install/download-test-samples.d.ts +19 -0
  52. package/dist_ts_install/download-test-samples.js +169 -0
  53. package/dist_ts_install/download-xrechnung-rules.d.ts +7 -0
  54. package/dist_ts_install/download-xrechnung-rules.js +146 -0
  55. package/dist_ts_install/index.d.ts +8 -0
  56. package/dist_ts_install/index.js +246 -0
  57. package/package.json +13 -8
  58. package/readme.md +255 -810
  59. package/ts/00_commitinfo_data.ts +1 -1
  60. package/ts/einvoice.ts +63 -14
  61. package/ts/formats/converters/xml-to-einvoice.converter.ts +142 -0
  62. package/ts/formats/semantic/bt-bg.model.ts +524 -0
  63. package/ts/formats/semantic/semantic.adapter.ts +600 -0
  64. package/ts/formats/semantic/semantic.validator.ts +654 -0
  65. package/ts/formats/utils/currency.calculator.decimal.ts +323 -0
  66. package/ts/formats/utils/currency.utils.ts +299 -0
  67. package/ts/formats/utils/decimal.ts +509 -0
  68. package/ts/formats/validation/codelist.validator.ts +317 -0
  69. package/ts/formats/validation/conformance.harness.ts +591 -0
  70. package/ts/formats/validation/en16931.business-rules.validator.ts +694 -0
  71. package/ts/formats/validation/facturx.validator.ts +579 -0
  72. package/ts/formats/validation/integrated.validator.ts +405 -0
  73. package/ts/formats/validation/peppol.validator.ts +589 -0
  74. package/ts/formats/validation/schematron.downloader.ts +304 -0
  75. package/ts/formats/validation/schematron.integration.ts +285 -0
  76. package/ts/formats/validation/schematron.validator.ts +348 -0
  77. package/ts/formats/validation/schematron.worker.ts +221 -0
  78. package/ts/formats/validation/validation.types.ts +274 -0
  79. package/ts/formats/validation/vat-categories.validator.ts +845 -0
  80. package/ts/formats/validation/xrechnung.validator.ts +494 -0
  81. package/ts/interfaces/common.ts +1 -0
  82. package/ts/interfaces/en16931-metadata.ts +104 -0
  83. package/ts/plugins.ts +8 -0
  84. package/ts/tspublish.json +3 -0
  85. package/dist_ts/classes.decoder.d.ts +0 -40
  86. package/dist_ts/classes.decoder.js +0 -320
  87. package/dist_ts/classes.encoder.d.ts +0 -51
  88. package/dist_ts/classes.encoder.js +0 -293
  89. package/dist_ts/classes.xinvoice.d.ts +0 -136
  90. package/dist_ts/classes.xinvoice.js +0 -352
  91. package/dist_ts/formats/base.decoder.d.ts +0 -19
  92. package/dist_ts/formats/base.decoder.js +0 -130
  93. package/dist_ts/formats/base.validator.d.ts +0 -44
  94. package/dist_ts/formats/base.validator.js +0 -39
  95. package/dist_ts/formats/decoder.factory.d.ts +0 -15
  96. package/dist_ts/formats/decoder.factory.js +0 -46
  97. package/dist_ts/formats/factorx.decoder.d.ts +0 -18
  98. package/dist_ts/formats/factorx.decoder.js +0 -178
  99. package/dist_ts/formats/factorx.encoder.d.ts +0 -51
  100. package/dist_ts/formats/factorx.encoder.js +0 -293
  101. package/dist_ts/formats/facturx.decoder.d.ts +0 -18
  102. package/dist_ts/formats/facturx.decoder.js +0 -210
  103. package/dist_ts/formats/facturx.encoder.d.ts +0 -51
  104. package/dist_ts/formats/facturx.encoder.js +0 -293
  105. package/dist_ts/formats/facturx.validator.d.ts +0 -67
  106. package/dist_ts/formats/facturx.validator.js +0 -266
  107. package/dist_ts/formats/pdf/robust-pdf.extractor.d.ts +0 -40
  108. package/dist_ts/formats/pdf/robust-pdf.extractor.js +0 -324
  109. package/dist_ts/formats/ubl.validator.d.ts +0 -82
  110. package/dist_ts/formats/ubl.validator.js +0 -306
  111. package/dist_ts/formats/validator.factory.d.ts +0 -18
  112. package/dist_ts/formats/validator.factory.js +0 -78
  113. package/dist_ts/formats/xinvoice.decoder.d.ts +0 -28
  114. package/dist_ts/formats/xinvoice.decoder.js +0 -332
  115. package/dist_ts/formats/xinvoice.encoder.d.ts +0 -19
  116. package/dist_ts/formats/xinvoice.encoder.js +0 -282
@@ -0,0 +1,591 @@
1
+ /**
2
+ * Conformance Test Harness for EN16931 Validation
3
+ * Tests validators against official samples and generates coverage reports
4
+ */
5
+
6
+ import * as plugins from '../../plugins.js';
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import { IntegratedValidator } from './schematron.integration.js';
10
+ import { EN16931BusinessRulesValidator } from './en16931.business-rules.validator.js';
11
+ import { CodeListValidator } from './codelist.validator.js';
12
+ import { VATCategoriesValidator } from './vat-categories.validator.js';
13
+ import type { ValidationResult, ValidationReport } from './validation.types.js';
14
+ import type { EInvoice } from '../../einvoice.js';
15
+ import { XMLToEInvoiceConverter } from '../converters/xml-to-einvoice.converter.js';
16
+
17
+ /**
18
+ * Test sample metadata
19
+ */
20
+ interface TestSample {
21
+ id: string;
22
+ name: string;
23
+ path: string;
24
+ format: 'UBL' | 'CII';
25
+ standard: string;
26
+ expectedValid: boolean;
27
+ description?: string;
28
+ focusRules?: string[];
29
+ }
30
+
31
+ /**
32
+ * Test result for a single sample
33
+ */
34
+ interface TestResult {
35
+ sampleId: string;
36
+ sampleName: string;
37
+ passed: boolean;
38
+ errors: ValidationResult[];
39
+ warnings: ValidationResult[];
40
+ rulesTriggered: string[];
41
+ executionTime: number;
42
+ validatorResults: {
43
+ typescript: ValidationResult[];
44
+ schematron: ValidationResult[];
45
+ vatCategories: ValidationResult[];
46
+ codeLists: ValidationResult[];
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Coverage report for all rules
52
+ */
53
+ interface CoverageReport {
54
+ totalRules: number;
55
+ coveredRules: number;
56
+ coveragePercentage: number;
57
+ ruleDetails: Map<string, {
58
+ covered: boolean;
59
+ samplesCovering: string[];
60
+ errorCount: number;
61
+ warningCount: number;
62
+ }>;
63
+ uncoveredRules: string[];
64
+ byCategory: {
65
+ document: { total: number; covered: number };
66
+ calculation: { total: number; covered: number };
67
+ vat: { total: number; covered: number };
68
+ lineLevel: { total: number; covered: number };
69
+ codeLists: { total: number; covered: number };
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Conformance Test Harness
75
+ */
76
+ export class ConformanceTestHarness {
77
+ private integratedValidator: IntegratedValidator;
78
+ private businessRulesValidator: EN16931BusinessRulesValidator;
79
+ private codeListValidator: CodeListValidator;
80
+ private vatCategoriesValidator: VATCategoriesValidator;
81
+ private xmlConverter: XMLToEInvoiceConverter;
82
+ private testSamples: TestSample[] = [];
83
+ private results: TestResult[] = [];
84
+
85
+ constructor() {
86
+ this.integratedValidator = new IntegratedValidator();
87
+ this.businessRulesValidator = new EN16931BusinessRulesValidator();
88
+ this.codeListValidator = new CodeListValidator();
89
+ this.vatCategoriesValidator = new VATCategoriesValidator();
90
+ this.xmlConverter = new XMLToEInvoiceConverter();
91
+ }
92
+
93
+ /**
94
+ * Load test samples from directory
95
+ */
96
+ public async loadTestSamples(baseDir: string = 'test-samples'): Promise<void> {
97
+ this.testSamples = [];
98
+
99
+ // Load PEPPOL BIS 3.0 samples
100
+ const peppolDir = path.join(baseDir, 'peppol-bis3');
101
+ if (fs.existsSync(peppolDir)) {
102
+ const peppolFiles = fs.readdirSync(peppolDir);
103
+ for (const file of peppolFiles) {
104
+ if (file.endsWith('.xml')) {
105
+ this.testSamples.push({
106
+ id: `peppol-${path.basename(file, '.xml')}`,
107
+ name: file,
108
+ path: path.join(peppolDir, file),
109
+ format: 'UBL',
110
+ standard: 'PEPPOL-BIS-3.0',
111
+ expectedValid: true,
112
+ description: this.getDescriptionFromFilename(file),
113
+ focusRules: this.getFocusRulesFromFilename(file)
114
+ });
115
+ }
116
+ }
117
+ }
118
+
119
+ // Load CEN TC434 samples
120
+ const cenDir = path.join(baseDir, 'cen-tc434');
121
+ if (fs.existsSync(cenDir)) {
122
+ const cenFiles = fs.readdirSync(cenDir);
123
+ for (const file of cenFiles) {
124
+ if (file.endsWith('.xml')) {
125
+ const format = file.includes('ubl') ? 'UBL' : 'CII';
126
+ this.testSamples.push({
127
+ id: `cen-${path.basename(file, '.xml')}`,
128
+ name: file,
129
+ path: path.join(cenDir, file),
130
+ format,
131
+ standard: 'EN16931',
132
+ expectedValid: true,
133
+ description: `CEN TC434 ${format} example`
134
+ });
135
+ }
136
+ }
137
+ }
138
+
139
+ console.log(`Loaded ${this.testSamples.length} test samples`);
140
+ }
141
+
142
+ /**
143
+ * Run all validators against a single test sample
144
+ */
145
+ private async runTestSample(sample: TestSample): Promise<TestResult> {
146
+ const startTime = Date.now();
147
+ const result: TestResult = {
148
+ sampleId: sample.id,
149
+ sampleName: sample.name,
150
+ passed: false,
151
+ errors: [],
152
+ warnings: [],
153
+ rulesTriggered: [],
154
+ executionTime: 0,
155
+ validatorResults: {
156
+ typescript: [],
157
+ schematron: [],
158
+ vatCategories: [],
159
+ codeLists: []
160
+ }
161
+ };
162
+
163
+ try {
164
+ // Read XML content
165
+ const xmlContent = fs.readFileSync(sample.path, 'utf-8');
166
+
167
+ // Convert XML to EInvoice
168
+ const invoice = await this.xmlConverter.convert(xmlContent, sample.format);
169
+
170
+ // Run TypeScript validators
171
+ const businessRules = this.businessRulesValidator.validate(invoice);
172
+ result.validatorResults.typescript = businessRules;
173
+
174
+ const codeLists = this.codeListValidator.validate(invoice);
175
+ result.validatorResults.codeLists = codeLists;
176
+
177
+ const vatCategories = this.vatCategoriesValidator.validate(invoice);
178
+ result.validatorResults.vatCategories = vatCategories;
179
+
180
+ // Try to run Schematron if available
181
+ try {
182
+ await this.integratedValidator.loadSchematron('EN16931', sample.format);
183
+ const report = await this.integratedValidator.validate(invoice, xmlContent);
184
+ result.validatorResults.schematron = report.results.filter(r =>
185
+ r.source === 'Schematron'
186
+ );
187
+ } catch (error) {
188
+ console.warn(`Schematron not available for ${sample.format}: ${error.message}`);
189
+ }
190
+
191
+ // Aggregate results
192
+ const allResults = [
193
+ ...businessRules,
194
+ ...codeLists,
195
+ ...vatCategories,
196
+ ...result.validatorResults.schematron
197
+ ];
198
+
199
+ result.errors = allResults.filter(r => r.severity === 'error');
200
+ result.warnings = allResults.filter(r => r.severity === 'warning');
201
+ result.rulesTriggered = [...new Set(allResults.map(r => r.ruleId))];
202
+ result.passed = result.errors.length === 0 === sample.expectedValid;
203
+
204
+ } catch (error) {
205
+ console.error(`Error testing ${sample.name}: ${error.message}`);
206
+ result.errors.push({
207
+ ruleId: 'TEST-ERROR',
208
+ source: 'TestHarness',
209
+ severity: 'error',
210
+ message: `Test execution failed: ${error.message}`
211
+ });
212
+ }
213
+
214
+ result.executionTime = Date.now() - startTime;
215
+ return result;
216
+ }
217
+
218
+ /**
219
+ * Run conformance tests on all samples
220
+ */
221
+ public async runConformanceTests(): Promise<void> {
222
+ console.log('\nšŸ”¬ Running conformance tests...\n');
223
+ this.results = [];
224
+
225
+ for (const sample of this.testSamples) {
226
+ process.stdout.write(`Testing ${sample.name}... `);
227
+ const result = await this.runTestSample(sample);
228
+ this.results.push(result);
229
+
230
+ if (result.passed) {
231
+ console.log('āœ… PASSED');
232
+ } else {
233
+ console.log(`āŒ FAILED (${result.errors.length} errors)`);
234
+ }
235
+ }
236
+
237
+ console.log('\n' + '='.repeat(60));
238
+ this.printSummary();
239
+ }
240
+
241
+ /**
242
+ * Generate BR coverage matrix
243
+ */
244
+ public generateCoverageMatrix(): CoverageReport {
245
+ // Define all EN16931 business rules
246
+ const allRules = this.getAllEN16931Rules();
247
+ const ruleDetails = new Map<string, any>();
248
+
249
+ // Initialize rule details
250
+ for (const rule of allRules) {
251
+ ruleDetails.set(rule, {
252
+ covered: false,
253
+ samplesCovering: [],
254
+ errorCount: 0,
255
+ warningCount: 0
256
+ });
257
+ }
258
+
259
+ // Process test results
260
+ for (const result of this.results) {
261
+ for (const ruleId of result.rulesTriggered) {
262
+ if (ruleDetails.has(ruleId)) {
263
+ const detail = ruleDetails.get(ruleId);
264
+ detail.covered = true;
265
+ detail.samplesCovering.push(result.sampleId);
266
+ detail.errorCount += result.errors.filter(e => e.ruleId === ruleId).length;
267
+ detail.warningCount += result.warnings.filter(w => w.ruleId === ruleId).length;
268
+ }
269
+ }
270
+ }
271
+
272
+ // Calculate coverage by category
273
+ const categories = {
274
+ document: { total: 0, covered: 0 },
275
+ calculation: { total: 0, covered: 0 },
276
+ vat: { total: 0, covered: 0 },
277
+ lineLevel: { total: 0, covered: 0 },
278
+ codeLists: { total: 0, covered: 0 }
279
+ };
280
+
281
+ for (const [rule, detail] of ruleDetails) {
282
+ const category = this.getRuleCategory(rule);
283
+ if (category && categories[category]) {
284
+ categories[category].total++;
285
+ if (detail.covered) {
286
+ categories[category].covered++;
287
+ }
288
+ }
289
+ }
290
+
291
+ // Find uncovered rules
292
+ const uncoveredRules = Array.from(ruleDetails.entries())
293
+ .filter(([_, detail]) => !detail.covered)
294
+ .map(([rule, _]) => rule);
295
+
296
+ const coveredCount = Array.from(ruleDetails.values())
297
+ .filter(d => d.covered).length;
298
+
299
+ return {
300
+ totalRules: allRules.length,
301
+ coveredRules: coveredCount,
302
+ coveragePercentage: (coveredCount / allRules.length) * 100,
303
+ ruleDetails,
304
+ uncoveredRules,
305
+ byCategory: categories
306
+ };
307
+ }
308
+
309
+ /**
310
+ * Print test summary
311
+ */
312
+ private printSummary(): void {
313
+ const passed = this.results.filter(r => r.passed).length;
314
+ const failed = this.results.filter(r => !r.passed).length;
315
+ const totalErrors = this.results.reduce((sum, r) => sum + r.errors.length, 0);
316
+ const totalWarnings = this.results.reduce((sum, r) => sum + r.warnings.length, 0);
317
+
318
+ console.log('\nšŸ“Š Test Summary:');
319
+ console.log(` Total samples: ${this.testSamples.length}`);
320
+ console.log(` āœ… Passed: ${passed}`);
321
+ console.log(` āŒ Failed: ${failed}`);
322
+ console.log(` šŸ”“ Total errors: ${totalErrors}`);
323
+ console.log(` 🟔 Total warnings: ${totalWarnings}`);
324
+
325
+ // Show failed samples
326
+ if (failed > 0) {
327
+ console.log('\nāŒ Failed samples:');
328
+ for (const result of this.results.filter(r => !r.passed)) {
329
+ console.log(` - ${result.sampleName} (${result.errors.length} errors)`);
330
+ for (const error of result.errors.slice(0, 3)) {
331
+ console.log(` • ${error.ruleId}: ${error.message}`);
332
+ }
333
+ if (result.errors.length > 3) {
334
+ console.log(` ... and ${result.errors.length - 3} more errors`);
335
+ }
336
+ }
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Generate HTML coverage report
342
+ */
343
+ public async generateHTMLReport(outputPath: string = 'coverage-report.html'): Promise<void> {
344
+ const coverage = this.generateCoverageMatrix();
345
+
346
+ const html = `
347
+ <!DOCTYPE html>
348
+ <html>
349
+ <head>
350
+ <title>EN16931 Conformance Test Report</title>
351
+ <style>
352
+ body { font-family: Arial, sans-serif; margin: 20px; }
353
+ h1 { color: #333; }
354
+ .summary { background: #f0f0f0; padding: 15px; border-radius: 5px; margin: 20px 0; }
355
+ .metric { display: inline-block; margin: 10px 20px 10px 0; }
356
+ .metric-value { font-size: 24px; font-weight: bold; color: #007bff; }
357
+ .coverage-bar { width: 100%; height: 30px; background: #e0e0e0; border-radius: 5px; overflow: hidden; }
358
+ .coverage-fill { height: 100%; background: linear-gradient(90deg, #28a745, #ffc107); }
359
+ table { width: 100%; border-collapse: collapse; margin: 20px 0; }
360
+ th, td { padding: 10px; text-align: left; border: 1px solid #ddd; }
361
+ th { background: #f8f9fa; font-weight: bold; }
362
+ .covered { background: #d4edda; }
363
+ .uncovered { background: #f8d7da; }
364
+ .category-section { margin: 30px 0; }
365
+ .rule-tag { display: inline-block; padding: 2px 8px; margin: 2px; background: #007bff; color: white; border-radius: 3px; font-size: 12px; }
366
+ </style>
367
+ </head>
368
+ <body>
369
+ <h1>EN16931 Conformance Test Report</h1>
370
+ <div class="summary">
371
+ <h2>Overall Coverage</h2>
372
+ <div class="metric">
373
+ <div class="metric-value">${coverage.coveragePercentage.toFixed(1)}%</div>
374
+ <div>Total Coverage</div>
375
+ </div>
376
+ <div class="metric">
377
+ <div class="metric-value">${coverage.coveredRules}</div>
378
+ <div>Rules Covered</div>
379
+ </div>
380
+ <div class="metric">
381
+ <div class="metric-value">${coverage.totalRules}</div>
382
+ <div>Total Rules</div>
383
+ </div>
384
+ <div class="coverage-bar">
385
+ <div class="coverage-fill" style="width: ${coverage.coveragePercentage}%"></div>
386
+ </div>
387
+ </div>
388
+
389
+ <div class="category-section">
390
+ <h2>Coverage by Category</h2>
391
+ <table>
392
+ <tr>
393
+ <th>Category</th>
394
+ <th>Covered</th>
395
+ <th>Total</th>
396
+ <th>Percentage</th>
397
+ </tr>
398
+ ${Object.entries(coverage.byCategory).map(([cat, data]) => `
399
+ <tr>
400
+ <td>${cat.charAt(0).toUpperCase() + cat.slice(1)}</td>
401
+ <td>${data.covered}</td>
402
+ <td>${data.total}</td>
403
+ <td>${data.total > 0 ? ((data.covered / data.total) * 100).toFixed(1) : 0}%</td>
404
+ </tr>
405
+ `).join('')}
406
+ </table>
407
+ </div>
408
+
409
+ <div class="category-section">
410
+ <h2>Test Samples</h2>
411
+ <table>
412
+ <tr>
413
+ <th>Sample</th>
414
+ <th>Status</th>
415
+ <th>Errors</th>
416
+ <th>Warnings</th>
417
+ <th>Rules Triggered</th>
418
+ </tr>
419
+ ${this.results.map(r => `
420
+ <tr class="${r.passed ? 'covered' : 'uncovered'}">
421
+ <td>${r.sampleName}</td>
422
+ <td>${r.passed ? 'āœ… PASSED' : 'āŒ FAILED'}</td>
423
+ <td>${r.errors.length}</td>
424
+ <td>${r.warnings.length}</td>
425
+ <td>${r.rulesTriggered.length}</td>
426
+ </tr>
427
+ `).join('')}
428
+ </table>
429
+ </div>
430
+
431
+ <div class="category-section">
432
+ <h2>Uncovered Rules</h2>
433
+ ${coverage.uncoveredRules.length === 0 ? '<p>All rules covered! šŸŽ‰</p>' : `
434
+ <p>The following ${coverage.uncoveredRules.length} rules need test coverage:</p>
435
+ <div>
436
+ ${coverage.uncoveredRules.map(rule =>
437
+ `<span class="rule-tag">${rule}</span>`
438
+ ).join('')}
439
+ </div>
440
+ `}
441
+ </div>
442
+
443
+ <div class="category-section">
444
+ <p>Generated: ${new Date().toISOString()}</p>
445
+ </div>
446
+ </body>
447
+ </html>
448
+ `;
449
+
450
+ fs.writeFileSync(outputPath, html);
451
+ console.log(`\nšŸ“„ HTML report generated: ${outputPath}`);
452
+ }
453
+
454
+ /**
455
+ * Get all EN16931 business rules
456
+ */
457
+ private getAllEN16931Rules(): string[] {
458
+ return [
459
+ // Document level rules
460
+ 'BR-01', 'BR-02', 'BR-03', 'BR-04', 'BR-05', 'BR-06', 'BR-07', 'BR-08', 'BR-09', 'BR-10',
461
+ 'BR-11', 'BR-12', 'BR-13', 'BR-14', 'BR-15', 'BR-16', 'BR-17', 'BR-18', 'BR-19', 'BR-20',
462
+
463
+ // Line level rules
464
+ 'BR-21', 'BR-22', 'BR-23', 'BR-24', 'BR-25', 'BR-26', 'BR-27', 'BR-28', 'BR-29', 'BR-30',
465
+
466
+ // Allowances and charges
467
+ 'BR-31', 'BR-32', 'BR-33', 'BR-34', 'BR-35', 'BR-36', 'BR-37', 'BR-38', 'BR-39', 'BR-40',
468
+ 'BR-41', 'BR-42', 'BR-43', 'BR-44', 'BR-45', 'BR-46', 'BR-47', 'BR-48', 'BR-49', 'BR-50',
469
+ 'BR-51', 'BR-52', 'BR-53', 'BR-54', 'BR-55', 'BR-56', 'BR-57', 'BR-58', 'BR-59', 'BR-60',
470
+ 'BR-61', 'BR-62', 'BR-63', 'BR-64', 'BR-65',
471
+
472
+ // Calculation rules
473
+ 'BR-CO-01', 'BR-CO-02', 'BR-CO-03', 'BR-CO-04', 'BR-CO-05', 'BR-CO-06', 'BR-CO-07', 'BR-CO-08',
474
+ 'BR-CO-09', 'BR-CO-10', 'BR-CO-11', 'BR-CO-12', 'BR-CO-13', 'BR-CO-14', 'BR-CO-15', 'BR-CO-16',
475
+ 'BR-CO-17', 'BR-CO-18', 'BR-CO-19', 'BR-CO-20',
476
+
477
+ // VAT rules - Standard rate
478
+ 'BR-S-01', 'BR-S-02', 'BR-S-03', 'BR-S-04', 'BR-S-05', 'BR-S-06', 'BR-S-07', 'BR-S-08',
479
+
480
+ // VAT rules - Zero rated
481
+ 'BR-Z-01', 'BR-Z-02', 'BR-Z-03', 'BR-Z-04', 'BR-Z-05', 'BR-Z-06', 'BR-Z-07', 'BR-Z-08',
482
+
483
+ // VAT rules - Exempt
484
+ 'BR-E-01', 'BR-E-02', 'BR-E-03', 'BR-E-04', 'BR-E-05', 'BR-E-06', 'BR-E-07', 'BR-E-08',
485
+
486
+ // VAT rules - Reverse charge
487
+ 'BR-AE-01', 'BR-AE-02', 'BR-AE-03', 'BR-AE-04', 'BR-AE-05', 'BR-AE-06', 'BR-AE-07', 'BR-AE-08',
488
+
489
+ // VAT rules - Intra-community
490
+ 'BR-K-01', 'BR-K-02', 'BR-K-03', 'BR-K-04', 'BR-K-05', 'BR-K-06', 'BR-K-07', 'BR-K-08',
491
+ 'BR-K-09', 'BR-K-10',
492
+
493
+ // VAT rules - Export
494
+ 'BR-G-01', 'BR-G-02', 'BR-G-03', 'BR-G-04', 'BR-G-05', 'BR-G-06', 'BR-G-07', 'BR-G-08',
495
+
496
+ // VAT rules - Out of scope
497
+ 'BR-O-01', 'BR-O-02', 'BR-O-03', 'BR-O-04', 'BR-O-05', 'BR-O-06', 'BR-O-07', 'BR-O-08',
498
+
499
+ // Code list rules
500
+ 'BR-CL-01', 'BR-CL-02', 'BR-CL-03', 'BR-CL-04', 'BR-CL-05', 'BR-CL-06', 'BR-CL-07', 'BR-CL-08',
501
+ 'BR-CL-09', 'BR-CL-10', 'BR-CL-11', 'BR-CL-12', 'BR-CL-13', 'BR-CL-14', 'BR-CL-15', 'BR-CL-16',
502
+ 'BR-CL-17', 'BR-CL-18', 'BR-CL-19', 'BR-CL-20', 'BR-CL-21', 'BR-CL-22', 'BR-CL-23', 'BR-CL-24',
503
+ 'BR-CL-25', 'BR-CL-26'
504
+ ];
505
+ }
506
+
507
+ /**
508
+ * Get category for a rule
509
+ */
510
+ private getRuleCategory(ruleId: string): keyof CoverageReport['byCategory'] | null {
511
+ if (ruleId.startsWith('BR-CO-')) return 'calculation';
512
+ if (ruleId.match(/^BR-[SZAEKG0]-/)) return 'vat';
513
+ if (ruleId.startsWith('BR-CL-')) return 'codeLists';
514
+ if (ruleId.match(/^BR-2[0-9]/) || ruleId.match(/^BR-3[0-9]/)) return 'lineLevel';
515
+ if (ruleId.match(/^BR-[0-9]/) || ruleId.match(/^BR-1[0-9]/)) return 'document';
516
+ return null;
517
+ }
518
+
519
+ /**
520
+ * Get description from filename
521
+ */
522
+ private getDescriptionFromFilename(filename: string): string {
523
+ const descriptions: Record<string, string> = {
524
+ 'Allowance-example': 'Invoice with document level allowances',
525
+ 'base-example': 'Basic EN16931 compliant invoice',
526
+ 'base-negative-inv-correction': 'Negative invoice correction',
527
+ 'vat-category-E': 'VAT Exempt invoice',
528
+ 'vat-category-O': 'Out of scope services',
529
+ 'vat-category-S': 'Standard rated VAT',
530
+ 'vat-category-Z': 'Zero rated VAT',
531
+ 'vat-category-AE': 'Reverse charge VAT',
532
+ 'vat-category-K': 'Intra-community supply',
533
+ 'vat-category-G': 'Export outside EU'
534
+ };
535
+
536
+ const key = filename.replace('.xml', '');
537
+ return descriptions[key] || filename;
538
+ }
539
+
540
+ /**
541
+ * Get focus rules from filename
542
+ */
543
+ private getFocusRulesFromFilename(filename: string): string[] {
544
+ const focusMap: Record<string, string[]> = {
545
+ 'vat-category-E': ['BR-E-01', 'BR-E-02', 'BR-E-03', 'BR-E-04', 'BR-E-05', 'BR-E-06'],
546
+ 'vat-category-S': ['BR-S-01', 'BR-S-02', 'BR-S-03', 'BR-S-04', 'BR-S-05'],
547
+ 'vat-category-Z': ['BR-Z-01', 'BR-Z-02', 'BR-Z-03', 'BR-Z-04', 'BR-Z-05'],
548
+ 'vat-category-AE': ['BR-AE-01', 'BR-AE-02', 'BR-AE-03', 'BR-AE-04', 'BR-AE-05', 'BR-AE-06'],
549
+ 'vat-category-K': ['BR-K-01', 'BR-K-02', 'BR-K-03', 'BR-K-04', 'BR-K-05', 'BR-K-06'],
550
+ 'vat-category-G': ['BR-G-01', 'BR-G-02', 'BR-G-03', 'BR-G-04', 'BR-G-05', 'BR-G-06'],
551
+ 'vat-category-O': ['BR-O-01', 'BR-O-02', 'BR-O-03', 'BR-O-04', 'BR-O-05', 'BR-O-06']
552
+ };
553
+
554
+ const key = filename.replace('.xml', '');
555
+ return focusMap[key] || [];
556
+ }
557
+ }
558
+
559
+ /**
560
+ * Export convenience function to run conformance tests
561
+ */
562
+ export async function runConformanceTests(
563
+ samplesDir: string = 'test-samples',
564
+ generateReport: boolean = true
565
+ ): Promise<void> {
566
+ const harness = new ConformanceTestHarness();
567
+
568
+ // Load samples
569
+ await harness.loadTestSamples(samplesDir);
570
+
571
+ // Run tests
572
+ await harness.runConformanceTests();
573
+
574
+ // Generate reports
575
+ if (generateReport) {
576
+ const coverage = harness.generateCoverageMatrix();
577
+ console.log('\nšŸ“Š Coverage Report:');
578
+ console.log(` Overall: ${coverage.coveragePercentage.toFixed(1)}%`);
579
+ console.log(` Rules covered: ${coverage.coveredRules}/${coverage.totalRules}`);
580
+
581
+ // Show category breakdown
582
+ console.log('\n By Category:');
583
+ for (const [category, data] of Object.entries(coverage.byCategory)) {
584
+ const pct = data.total > 0 ? ((data.covered / data.total) * 100).toFixed(1) : '0';
585
+ console.log(` - ${category}: ${data.covered}/${data.total} (${pct}%)`);
586
+ }
587
+
588
+ // Generate HTML report
589
+ await harness.generateHTMLReport();
590
+ }
591
+ }