@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/einvoice.d.ts +4 -8
- package/dist_ts/einvoice.js +46 -7
- package/dist_ts/formats/converters/xml-to-einvoice.converter.d.ts +21 -0
- package/dist_ts/formats/converters/xml-to-einvoice.converter.js +127 -0
- package/dist_ts/formats/semantic/bt-bg.model.d.ts +445 -0
- package/dist_ts/formats/semantic/bt-bg.model.js +12 -0
- package/dist_ts/formats/semantic/semantic.adapter.d.ts +87 -0
- package/dist_ts/formats/semantic/semantic.adapter.js +538 -0
- package/dist_ts/formats/semantic/semantic.validator.d.ts +48 -0
- package/dist_ts/formats/semantic/semantic.validator.js +596 -0
- package/dist_ts/formats/utils/currency.calculator.decimal.d.ts +98 -0
- package/dist_ts/formats/utils/currency.calculator.decimal.js +212 -0
- package/dist_ts/formats/utils/currency.utils.d.ts +99 -0
- package/dist_ts/formats/utils/currency.utils.js +250 -0
- package/dist_ts/formats/utils/decimal.d.ts +151 -0
- package/dist_ts/formats/utils/decimal.js +435 -0
- package/dist_ts/formats/validation/codelist.validator.d.ts +49 -0
- package/dist_ts/formats/validation/codelist.validator.js +188 -0
- package/dist_ts/formats/validation/conformance.harness.d.ts +99 -0
- package/dist_ts/formats/validation/conformance.harness.js +471 -0
- package/dist_ts/formats/validation/en16931.business-rules.validator.d.ts +58 -0
- package/dist_ts/formats/validation/en16931.business-rules.validator.js +459 -0
- package/dist_ts/formats/validation/facturx.validator.d.ts +75 -0
- package/dist_ts/formats/validation/facturx.validator.js +522 -0
- package/dist_ts/formats/validation/integrated.validator.d.ts +82 -0
- package/dist_ts/formats/validation/integrated.validator.js +311 -0
- package/dist_ts/formats/validation/peppol.validator.d.ts +77 -0
- package/dist_ts/formats/validation/peppol.validator.js +529 -0
- package/dist_ts/formats/validation/schematron.downloader.d.ts +71 -0
- package/dist_ts/formats/validation/schematron.downloader.js +254 -0
- package/dist_ts/formats/validation/schematron.integration.d.ts +60 -0
- package/dist_ts/formats/validation/schematron.integration.js +230 -0
- package/dist_ts/formats/validation/schematron.validator.d.ts +79 -0
- package/dist_ts/formats/validation/schematron.validator.js +288 -0
- package/dist_ts/formats/validation/schematron.worker.d.ts +50 -0
- package/dist_ts/formats/validation/schematron.worker.js +185 -0
- package/dist_ts/formats/validation/validation.types.d.ts +163 -0
- package/dist_ts/formats/validation/validation.types.js +181 -0
- package/dist_ts/formats/validation/vat-categories.validator.d.ts +87 -0
- package/dist_ts/formats/validation/vat-categories.validator.js +497 -0
- package/dist_ts/formats/validation/xrechnung.validator.d.ts +92 -0
- package/dist_ts/formats/validation/xrechnung.validator.js +417 -0
- package/dist_ts/interfaces/common.d.ts +1 -0
- package/dist_ts/interfaces/en16931-metadata.d.ts +83 -0
- package/dist_ts/interfaces/en16931-metadata.js +6 -0
- package/dist_ts/plugins.d.ts +3 -1
- package/dist_ts/plugins.js +7 -2
- package/dist_ts_install/download-schematron.d.ts +6 -0
- package/dist_ts_install/download-schematron.js +62 -0
- package/dist_ts_install/download-test-samples.d.ts +19 -0
- package/dist_ts_install/download-test-samples.js +169 -0
- package/dist_ts_install/download-xrechnung-rules.d.ts +7 -0
- package/dist_ts_install/download-xrechnung-rules.js +146 -0
- package/dist_ts_install/index.d.ts +8 -0
- package/dist_ts_install/index.js +246 -0
- package/package.json +13 -8
- package/readme.md +255 -810
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +63 -14
- package/ts/formats/converters/xml-to-einvoice.converter.ts +142 -0
- package/ts/formats/semantic/bt-bg.model.ts +524 -0
- package/ts/formats/semantic/semantic.adapter.ts +600 -0
- package/ts/formats/semantic/semantic.validator.ts +654 -0
- package/ts/formats/utils/currency.calculator.decimal.ts +323 -0
- package/ts/formats/utils/currency.utils.ts +299 -0
- package/ts/formats/utils/decimal.ts +509 -0
- package/ts/formats/validation/codelist.validator.ts +317 -0
- package/ts/formats/validation/conformance.harness.ts +591 -0
- package/ts/formats/validation/en16931.business-rules.validator.ts +694 -0
- package/ts/formats/validation/facturx.validator.ts +579 -0
- package/ts/formats/validation/integrated.validator.ts +405 -0
- package/ts/formats/validation/peppol.validator.ts +589 -0
- package/ts/formats/validation/schematron.downloader.ts +304 -0
- package/ts/formats/validation/schematron.integration.ts +285 -0
- package/ts/formats/validation/schematron.validator.ts +348 -0
- package/ts/formats/validation/schematron.worker.ts +221 -0
- package/ts/formats/validation/validation.types.ts +274 -0
- package/ts/formats/validation/vat-categories.validator.ts +845 -0
- package/ts/formats/validation/xrechnung.validator.ts +494 -0
- package/ts/interfaces/common.ts +1 -0
- package/ts/interfaces/en16931-metadata.ts +104 -0
- package/ts/plugins.ts +8 -0
- package/ts/tspublish.json +3 -0
- package/dist_ts/classes.decoder.d.ts +0 -40
- package/dist_ts/classes.decoder.js +0 -320
- package/dist_ts/classes.encoder.d.ts +0 -51
- package/dist_ts/classes.encoder.js +0 -293
- package/dist_ts/classes.xinvoice.d.ts +0 -136
- package/dist_ts/classes.xinvoice.js +0 -352
- package/dist_ts/formats/base.decoder.d.ts +0 -19
- package/dist_ts/formats/base.decoder.js +0 -130
- package/dist_ts/formats/base.validator.d.ts +0 -44
- package/dist_ts/formats/base.validator.js +0 -39
- package/dist_ts/formats/decoder.factory.d.ts +0 -15
- package/dist_ts/formats/decoder.factory.js +0 -46
- package/dist_ts/formats/factorx.decoder.d.ts +0 -18
- package/dist_ts/formats/factorx.decoder.js +0 -178
- package/dist_ts/formats/factorx.encoder.d.ts +0 -51
- package/dist_ts/formats/factorx.encoder.js +0 -293
- package/dist_ts/formats/facturx.decoder.d.ts +0 -18
- package/dist_ts/formats/facturx.decoder.js +0 -210
- package/dist_ts/formats/facturx.encoder.d.ts +0 -51
- package/dist_ts/formats/facturx.encoder.js +0 -293
- package/dist_ts/formats/facturx.validator.d.ts +0 -67
- package/dist_ts/formats/facturx.validator.js +0 -266
- package/dist_ts/formats/pdf/robust-pdf.extractor.d.ts +0 -40
- package/dist_ts/formats/pdf/robust-pdf.extractor.js +0 -324
- package/dist_ts/formats/ubl.validator.d.ts +0 -82
- package/dist_ts/formats/ubl.validator.js +0 -306
- package/dist_ts/formats/validator.factory.d.ts +0 -18
- package/dist_ts/formats/validator.factory.js +0 -78
- package/dist_ts/formats/xinvoice.decoder.d.ts +0 -28
- package/dist_ts/formats/xinvoice.decoder.js +0 -332
- package/dist_ts/formats/xinvoice.encoder.d.ts +0 -19
- 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
|
+
}
|