- Added validation types for EN16931 compliance in `validation.types.ts`, including interfaces for `ValidationResult`, `ValidationOptions`, and `ValidationReport`. - Introduced `VATCategoriesValidator` in `vat-categories.validator.ts` to validate VAT categories according to EN16931 rules, including detailed checks for standard, zero-rated, exempt, reverse charge, intra-community, export, and out-of-scope services. - Enhanced `IEInvoiceMetadata` interface in `en16931-metadata.ts` to include additional fields required for full standards compliance, such as delivery information, payment information, allowances, and charges. - Implemented helper methods for VAT calculations and validation logic to ensure accurate compliance with EN16931 standards.
591 lines
20 KiB
TypeScript
591 lines
20 KiB
TypeScript
/**
|
|
* Conformance Test Harness for EN16931 Validation
|
|
* Tests validators against official samples and generates coverage reports
|
|
*/
|
|
|
|
import * as plugins from '../../plugins.js';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { IntegratedValidator } from './schematron.integration.js';
|
|
import { EN16931BusinessRulesValidator } from './en16931.business-rules.validator.js';
|
|
import { CodeListValidator } from './codelist.validator.js';
|
|
import { VATCategoriesValidator } from './vat-categories.validator.js';
|
|
import type { ValidationResult, ValidationReport } from './validation.types.js';
|
|
import type { EInvoice } from '../../einvoice.js';
|
|
import { XMLToEInvoiceConverter } from '../converters/xml-to-einvoice.converter.js';
|
|
|
|
/**
|
|
* Test sample metadata
|
|
*/
|
|
interface TestSample {
|
|
id: string;
|
|
name: string;
|
|
path: string;
|
|
format: 'UBL' | 'CII';
|
|
standard: string;
|
|
expectedValid: boolean;
|
|
description?: string;
|
|
focusRules?: string[];
|
|
}
|
|
|
|
/**
|
|
* Test result for a single sample
|
|
*/
|
|
interface TestResult {
|
|
sampleId: string;
|
|
sampleName: string;
|
|
passed: boolean;
|
|
errors: ValidationResult[];
|
|
warnings: ValidationResult[];
|
|
rulesTriggered: string[];
|
|
executionTime: number;
|
|
validatorResults: {
|
|
typescript: ValidationResult[];
|
|
schematron: ValidationResult[];
|
|
vatCategories: ValidationResult[];
|
|
codeLists: ValidationResult[];
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Coverage report for all rules
|
|
*/
|
|
interface CoverageReport {
|
|
totalRules: number;
|
|
coveredRules: number;
|
|
coveragePercentage: number;
|
|
ruleDetails: Map<string, {
|
|
covered: boolean;
|
|
samplesCovering: string[];
|
|
errorCount: number;
|
|
warningCount: number;
|
|
}>;
|
|
uncoveredRules: string[];
|
|
byCategory: {
|
|
document: { total: number; covered: number };
|
|
calculation: { total: number; covered: number };
|
|
vat: { total: number; covered: number };
|
|
lineLevel: { total: number; covered: number };
|
|
codeLists: { total: number; covered: number };
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Conformance Test Harness
|
|
*/
|
|
export class ConformanceTestHarness {
|
|
private integratedValidator: IntegratedValidator;
|
|
private businessRulesValidator: EN16931BusinessRulesValidator;
|
|
private codeListValidator: CodeListValidator;
|
|
private vatCategoriesValidator: VATCategoriesValidator;
|
|
private xmlConverter: XMLToEInvoiceConverter;
|
|
private testSamples: TestSample[] = [];
|
|
private results: TestResult[] = [];
|
|
|
|
constructor() {
|
|
this.integratedValidator = new IntegratedValidator();
|
|
this.businessRulesValidator = new EN16931BusinessRulesValidator();
|
|
this.codeListValidator = new CodeListValidator();
|
|
this.vatCategoriesValidator = new VATCategoriesValidator();
|
|
this.xmlConverter = new XMLToEInvoiceConverter();
|
|
}
|
|
|
|
/**
|
|
* Load test samples from directory
|
|
*/
|
|
public async loadTestSamples(baseDir: string = 'test-samples'): Promise<void> {
|
|
this.testSamples = [];
|
|
|
|
// Load PEPPOL BIS 3.0 samples
|
|
const peppolDir = path.join(baseDir, 'peppol-bis3');
|
|
if (fs.existsSync(peppolDir)) {
|
|
const peppolFiles = fs.readdirSync(peppolDir);
|
|
for (const file of peppolFiles) {
|
|
if (file.endsWith('.xml')) {
|
|
this.testSamples.push({
|
|
id: `peppol-${path.basename(file, '.xml')}`,
|
|
name: file,
|
|
path: path.join(peppolDir, file),
|
|
format: 'UBL',
|
|
standard: 'PEPPOL-BIS-3.0',
|
|
expectedValid: true,
|
|
description: this.getDescriptionFromFilename(file),
|
|
focusRules: this.getFocusRulesFromFilename(file)
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load CEN TC434 samples
|
|
const cenDir = path.join(baseDir, 'cen-tc434');
|
|
if (fs.existsSync(cenDir)) {
|
|
const cenFiles = fs.readdirSync(cenDir);
|
|
for (const file of cenFiles) {
|
|
if (file.endsWith('.xml')) {
|
|
const format = file.includes('ubl') ? 'UBL' : 'CII';
|
|
this.testSamples.push({
|
|
id: `cen-${path.basename(file, '.xml')}`,
|
|
name: file,
|
|
path: path.join(cenDir, file),
|
|
format,
|
|
standard: 'EN16931',
|
|
expectedValid: true,
|
|
description: `CEN TC434 ${format} example`
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(`Loaded ${this.testSamples.length} test samples`);
|
|
}
|
|
|
|
/**
|
|
* Run all validators against a single test sample
|
|
*/
|
|
private async runTestSample(sample: TestSample): Promise<TestResult> {
|
|
const startTime = Date.now();
|
|
const result: TestResult = {
|
|
sampleId: sample.id,
|
|
sampleName: sample.name,
|
|
passed: false,
|
|
errors: [],
|
|
warnings: [],
|
|
rulesTriggered: [],
|
|
executionTime: 0,
|
|
validatorResults: {
|
|
typescript: [],
|
|
schematron: [],
|
|
vatCategories: [],
|
|
codeLists: []
|
|
}
|
|
};
|
|
|
|
try {
|
|
// Read XML content
|
|
const xmlContent = fs.readFileSync(sample.path, 'utf-8');
|
|
|
|
// Convert XML to EInvoice
|
|
const invoice = await this.xmlConverter.convert(xmlContent, sample.format);
|
|
|
|
// Run TypeScript validators
|
|
const businessRules = this.businessRulesValidator.validate(invoice);
|
|
result.validatorResults.typescript = businessRules;
|
|
|
|
const codeLists = this.codeListValidator.validate(invoice);
|
|
result.validatorResults.codeLists = codeLists;
|
|
|
|
const vatCategories = this.vatCategoriesValidator.validate(invoice);
|
|
result.validatorResults.vatCategories = vatCategories;
|
|
|
|
// Try to run Schematron if available
|
|
try {
|
|
await this.integratedValidator.loadSchematron('EN16931', sample.format);
|
|
const report = await this.integratedValidator.validate(invoice, xmlContent);
|
|
result.validatorResults.schematron = report.results.filter(r =>
|
|
r.source === 'Schematron'
|
|
);
|
|
} catch (error) {
|
|
console.warn(`Schematron not available for ${sample.format}: ${error.message}`);
|
|
}
|
|
|
|
// Aggregate results
|
|
const allResults = [
|
|
...businessRules,
|
|
...codeLists,
|
|
...vatCategories,
|
|
...result.validatorResults.schematron
|
|
];
|
|
|
|
result.errors = allResults.filter(r => r.severity === 'error');
|
|
result.warnings = allResults.filter(r => r.severity === 'warning');
|
|
result.rulesTriggered = [...new Set(allResults.map(r => r.ruleId))];
|
|
result.passed = result.errors.length === 0 === sample.expectedValid;
|
|
|
|
} catch (error) {
|
|
console.error(`Error testing ${sample.name}: ${error.message}`);
|
|
result.errors.push({
|
|
ruleId: 'TEST-ERROR',
|
|
source: 'TestHarness',
|
|
severity: 'error',
|
|
message: `Test execution failed: ${error.message}`
|
|
});
|
|
}
|
|
|
|
result.executionTime = Date.now() - startTime;
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Run conformance tests on all samples
|
|
*/
|
|
public async runConformanceTests(): Promise<void> {
|
|
console.log('\n🔬 Running conformance tests...\n');
|
|
this.results = [];
|
|
|
|
for (const sample of this.testSamples) {
|
|
process.stdout.write(`Testing ${sample.name}... `);
|
|
const result = await this.runTestSample(sample);
|
|
this.results.push(result);
|
|
|
|
if (result.passed) {
|
|
console.log('✅ PASSED');
|
|
} else {
|
|
console.log(`❌ FAILED (${result.errors.length} errors)`);
|
|
}
|
|
}
|
|
|
|
console.log('\n' + '='.repeat(60));
|
|
this.printSummary();
|
|
}
|
|
|
|
/**
|
|
* Generate BR coverage matrix
|
|
*/
|
|
public generateCoverageMatrix(): CoverageReport {
|
|
// Define all EN16931 business rules
|
|
const allRules = this.getAllEN16931Rules();
|
|
const ruleDetails = new Map<string, any>();
|
|
|
|
// Initialize rule details
|
|
for (const rule of allRules) {
|
|
ruleDetails.set(rule, {
|
|
covered: false,
|
|
samplesCovering: [],
|
|
errorCount: 0,
|
|
warningCount: 0
|
|
});
|
|
}
|
|
|
|
// Process test results
|
|
for (const result of this.results) {
|
|
for (const ruleId of result.rulesTriggered) {
|
|
if (ruleDetails.has(ruleId)) {
|
|
const detail = ruleDetails.get(ruleId);
|
|
detail.covered = true;
|
|
detail.samplesCovering.push(result.sampleId);
|
|
detail.errorCount += result.errors.filter(e => e.ruleId === ruleId).length;
|
|
detail.warningCount += result.warnings.filter(w => w.ruleId === ruleId).length;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculate coverage by category
|
|
const categories = {
|
|
document: { total: 0, covered: 0 },
|
|
calculation: { total: 0, covered: 0 },
|
|
vat: { total: 0, covered: 0 },
|
|
lineLevel: { total: 0, covered: 0 },
|
|
codeLists: { total: 0, covered: 0 }
|
|
};
|
|
|
|
for (const [rule, detail] of ruleDetails) {
|
|
const category = this.getRuleCategory(rule);
|
|
if (category && categories[category]) {
|
|
categories[category].total++;
|
|
if (detail.covered) {
|
|
categories[category].covered++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find uncovered rules
|
|
const uncoveredRules = Array.from(ruleDetails.entries())
|
|
.filter(([_, detail]) => !detail.covered)
|
|
.map(([rule, _]) => rule);
|
|
|
|
const coveredCount = Array.from(ruleDetails.values())
|
|
.filter(d => d.covered).length;
|
|
|
|
return {
|
|
totalRules: allRules.length,
|
|
coveredRules: coveredCount,
|
|
coveragePercentage: (coveredCount / allRules.length) * 100,
|
|
ruleDetails,
|
|
uncoveredRules,
|
|
byCategory: categories
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Print test summary
|
|
*/
|
|
private printSummary(): void {
|
|
const passed = this.results.filter(r => r.passed).length;
|
|
const failed = this.results.filter(r => !r.passed).length;
|
|
const totalErrors = this.results.reduce((sum, r) => sum + r.errors.length, 0);
|
|
const totalWarnings = this.results.reduce((sum, r) => sum + r.warnings.length, 0);
|
|
|
|
console.log('\n📊 Test Summary:');
|
|
console.log(` Total samples: ${this.testSamples.length}`);
|
|
console.log(` ✅ Passed: ${passed}`);
|
|
console.log(` ❌ Failed: ${failed}`);
|
|
console.log(` 🔴 Total errors: ${totalErrors}`);
|
|
console.log(` 🟡 Total warnings: ${totalWarnings}`);
|
|
|
|
// Show failed samples
|
|
if (failed > 0) {
|
|
console.log('\n❌ Failed samples:');
|
|
for (const result of this.results.filter(r => !r.passed)) {
|
|
console.log(` - ${result.sampleName} (${result.errors.length} errors)`);
|
|
for (const error of result.errors.slice(0, 3)) {
|
|
console.log(` • ${error.ruleId}: ${error.message}`);
|
|
}
|
|
if (result.errors.length > 3) {
|
|
console.log(` ... and ${result.errors.length - 3} more errors`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate HTML coverage report
|
|
*/
|
|
public async generateHTMLReport(outputPath: string = 'coverage-report.html'): Promise<void> {
|
|
const coverage = this.generateCoverageMatrix();
|
|
|
|
const html = `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>EN16931 Conformance Test Report</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; margin: 20px; }
|
|
h1 { color: #333; }
|
|
.summary { background: #f0f0f0; padding: 15px; border-radius: 5px; margin: 20px 0; }
|
|
.metric { display: inline-block; margin: 10px 20px 10px 0; }
|
|
.metric-value { font-size: 24px; font-weight: bold; color: #007bff; }
|
|
.coverage-bar { width: 100%; height: 30px; background: #e0e0e0; border-radius: 5px; overflow: hidden; }
|
|
.coverage-fill { height: 100%; background: linear-gradient(90deg, #28a745, #ffc107); }
|
|
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
|
|
th, td { padding: 10px; text-align: left; border: 1px solid #ddd; }
|
|
th { background: #f8f9fa; font-weight: bold; }
|
|
.covered { background: #d4edda; }
|
|
.uncovered { background: #f8d7da; }
|
|
.category-section { margin: 30px 0; }
|
|
.rule-tag { display: inline-block; padding: 2px 8px; margin: 2px; background: #007bff; color: white; border-radius: 3px; font-size: 12px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>EN16931 Conformance Test Report</h1>
|
|
<div class="summary">
|
|
<h2>Overall Coverage</h2>
|
|
<div class="metric">
|
|
<div class="metric-value">${coverage.coveragePercentage.toFixed(1)}%</div>
|
|
<div>Total Coverage</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-value">${coverage.coveredRules}</div>
|
|
<div>Rules Covered</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-value">${coverage.totalRules}</div>
|
|
<div>Total Rules</div>
|
|
</div>
|
|
<div class="coverage-bar">
|
|
<div class="coverage-fill" style="width: ${coverage.coveragePercentage}%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="category-section">
|
|
<h2>Coverage by Category</h2>
|
|
<table>
|
|
<tr>
|
|
<th>Category</th>
|
|
<th>Covered</th>
|
|
<th>Total</th>
|
|
<th>Percentage</th>
|
|
</tr>
|
|
${Object.entries(coverage.byCategory).map(([cat, data]) => `
|
|
<tr>
|
|
<td>${cat.charAt(0).toUpperCase() + cat.slice(1)}</td>
|
|
<td>${data.covered}</td>
|
|
<td>${data.total}</td>
|
|
<td>${data.total > 0 ? ((data.covered / data.total) * 100).toFixed(1) : 0}%</td>
|
|
</tr>
|
|
`).join('')}
|
|
</table>
|
|
</div>
|
|
|
|
<div class="category-section">
|
|
<h2>Test Samples</h2>
|
|
<table>
|
|
<tr>
|
|
<th>Sample</th>
|
|
<th>Status</th>
|
|
<th>Errors</th>
|
|
<th>Warnings</th>
|
|
<th>Rules Triggered</th>
|
|
</tr>
|
|
${this.results.map(r => `
|
|
<tr class="${r.passed ? 'covered' : 'uncovered'}">
|
|
<td>${r.sampleName}</td>
|
|
<td>${r.passed ? '✅ PASSED' : '❌ FAILED'}</td>
|
|
<td>${r.errors.length}</td>
|
|
<td>${r.warnings.length}</td>
|
|
<td>${r.rulesTriggered.length}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</table>
|
|
</div>
|
|
|
|
<div class="category-section">
|
|
<h2>Uncovered Rules</h2>
|
|
${coverage.uncoveredRules.length === 0 ? '<p>All rules covered! 🎉</p>' : `
|
|
<p>The following ${coverage.uncoveredRules.length} rules need test coverage:</p>
|
|
<div>
|
|
${coverage.uncoveredRules.map(rule =>
|
|
`<span class="rule-tag">${rule}</span>`
|
|
).join('')}
|
|
</div>
|
|
`}
|
|
</div>
|
|
|
|
<div class="category-section">
|
|
<p>Generated: ${new Date().toISOString()}</p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`;
|
|
|
|
fs.writeFileSync(outputPath, html);
|
|
console.log(`\n📄 HTML report generated: ${outputPath}`);
|
|
}
|
|
|
|
/**
|
|
* Get all EN16931 business rules
|
|
*/
|
|
private getAllEN16931Rules(): string[] {
|
|
return [
|
|
// Document level rules
|
|
'BR-01', 'BR-02', 'BR-03', 'BR-04', 'BR-05', 'BR-06', 'BR-07', 'BR-08', 'BR-09', 'BR-10',
|
|
'BR-11', 'BR-12', 'BR-13', 'BR-14', 'BR-15', 'BR-16', 'BR-17', 'BR-18', 'BR-19', 'BR-20',
|
|
|
|
// Line level rules
|
|
'BR-21', 'BR-22', 'BR-23', 'BR-24', 'BR-25', 'BR-26', 'BR-27', 'BR-28', 'BR-29', 'BR-30',
|
|
|
|
// Allowances and charges
|
|
'BR-31', 'BR-32', 'BR-33', 'BR-34', 'BR-35', 'BR-36', 'BR-37', 'BR-38', 'BR-39', 'BR-40',
|
|
'BR-41', 'BR-42', 'BR-43', 'BR-44', 'BR-45', 'BR-46', 'BR-47', 'BR-48', 'BR-49', 'BR-50',
|
|
'BR-51', 'BR-52', 'BR-53', 'BR-54', 'BR-55', 'BR-56', 'BR-57', 'BR-58', 'BR-59', 'BR-60',
|
|
'BR-61', 'BR-62', 'BR-63', 'BR-64', 'BR-65',
|
|
|
|
// Calculation rules
|
|
'BR-CO-01', 'BR-CO-02', 'BR-CO-03', 'BR-CO-04', 'BR-CO-05', 'BR-CO-06', 'BR-CO-07', 'BR-CO-08',
|
|
'BR-CO-09', 'BR-CO-10', 'BR-CO-11', 'BR-CO-12', 'BR-CO-13', 'BR-CO-14', 'BR-CO-15', 'BR-CO-16',
|
|
'BR-CO-17', 'BR-CO-18', 'BR-CO-19', 'BR-CO-20',
|
|
|
|
// VAT rules - Standard rate
|
|
'BR-S-01', 'BR-S-02', 'BR-S-03', 'BR-S-04', 'BR-S-05', 'BR-S-06', 'BR-S-07', 'BR-S-08',
|
|
|
|
// VAT rules - Zero rated
|
|
'BR-Z-01', 'BR-Z-02', 'BR-Z-03', 'BR-Z-04', 'BR-Z-05', 'BR-Z-06', 'BR-Z-07', 'BR-Z-08',
|
|
|
|
// VAT rules - Exempt
|
|
'BR-E-01', 'BR-E-02', 'BR-E-03', 'BR-E-04', 'BR-E-05', 'BR-E-06', 'BR-E-07', 'BR-E-08',
|
|
|
|
// VAT rules - Reverse charge
|
|
'BR-AE-01', 'BR-AE-02', 'BR-AE-03', 'BR-AE-04', 'BR-AE-05', 'BR-AE-06', 'BR-AE-07', 'BR-AE-08',
|
|
|
|
// VAT rules - Intra-community
|
|
'BR-K-01', 'BR-K-02', 'BR-K-03', 'BR-K-04', 'BR-K-05', 'BR-K-06', 'BR-K-07', 'BR-K-08',
|
|
'BR-K-09', 'BR-K-10',
|
|
|
|
// VAT rules - Export
|
|
'BR-G-01', 'BR-G-02', 'BR-G-03', 'BR-G-04', 'BR-G-05', 'BR-G-06', 'BR-G-07', 'BR-G-08',
|
|
|
|
// VAT rules - Out of scope
|
|
'BR-O-01', 'BR-O-02', 'BR-O-03', 'BR-O-04', 'BR-O-05', 'BR-O-06', 'BR-O-07', 'BR-O-08',
|
|
|
|
// Code list rules
|
|
'BR-CL-01', 'BR-CL-02', 'BR-CL-03', 'BR-CL-04', 'BR-CL-05', 'BR-CL-06', 'BR-CL-07', 'BR-CL-08',
|
|
'BR-CL-09', 'BR-CL-10', 'BR-CL-11', 'BR-CL-12', 'BR-CL-13', 'BR-CL-14', 'BR-CL-15', 'BR-CL-16',
|
|
'BR-CL-17', 'BR-CL-18', 'BR-CL-19', 'BR-CL-20', 'BR-CL-21', 'BR-CL-22', 'BR-CL-23', 'BR-CL-24',
|
|
'BR-CL-25', 'BR-CL-26'
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get category for a rule
|
|
*/
|
|
private getRuleCategory(ruleId: string): keyof CoverageReport['byCategory'] | null {
|
|
if (ruleId.startsWith('BR-CO-')) return 'calculation';
|
|
if (ruleId.match(/^BR-[SZAEKG0]-/)) return 'vat';
|
|
if (ruleId.startsWith('BR-CL-')) return 'codeLists';
|
|
if (ruleId.match(/^BR-2[0-9]/) || ruleId.match(/^BR-3[0-9]/)) return 'lineLevel';
|
|
if (ruleId.match(/^BR-[0-9]/) || ruleId.match(/^BR-1[0-9]/)) return 'document';
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get description from filename
|
|
*/
|
|
private getDescriptionFromFilename(filename: string): string {
|
|
const descriptions: Record<string, string> = {
|
|
'Allowance-example': 'Invoice with document level allowances',
|
|
'base-example': 'Basic EN16931 compliant invoice',
|
|
'base-negative-inv-correction': 'Negative invoice correction',
|
|
'vat-category-E': 'VAT Exempt invoice',
|
|
'vat-category-O': 'Out of scope services',
|
|
'vat-category-S': 'Standard rated VAT',
|
|
'vat-category-Z': 'Zero rated VAT',
|
|
'vat-category-AE': 'Reverse charge VAT',
|
|
'vat-category-K': 'Intra-community supply',
|
|
'vat-category-G': 'Export outside EU'
|
|
};
|
|
|
|
const key = filename.replace('.xml', '');
|
|
return descriptions[key] || filename;
|
|
}
|
|
|
|
/**
|
|
* Get focus rules from filename
|
|
*/
|
|
private getFocusRulesFromFilename(filename: string): string[] {
|
|
const focusMap: Record<string, string[]> = {
|
|
'vat-category-E': ['BR-E-01', 'BR-E-02', 'BR-E-03', 'BR-E-04', 'BR-E-05', 'BR-E-06'],
|
|
'vat-category-S': ['BR-S-01', 'BR-S-02', 'BR-S-03', 'BR-S-04', 'BR-S-05'],
|
|
'vat-category-Z': ['BR-Z-01', 'BR-Z-02', 'BR-Z-03', 'BR-Z-04', 'BR-Z-05'],
|
|
'vat-category-AE': ['BR-AE-01', 'BR-AE-02', 'BR-AE-03', 'BR-AE-04', 'BR-AE-05', 'BR-AE-06'],
|
|
'vat-category-K': ['BR-K-01', 'BR-K-02', 'BR-K-03', 'BR-K-04', 'BR-K-05', 'BR-K-06'],
|
|
'vat-category-G': ['BR-G-01', 'BR-G-02', 'BR-G-03', 'BR-G-04', 'BR-G-05', 'BR-G-06'],
|
|
'vat-category-O': ['BR-O-01', 'BR-O-02', 'BR-O-03', 'BR-O-04', 'BR-O-05', 'BR-O-06']
|
|
};
|
|
|
|
const key = filename.replace('.xml', '');
|
|
return focusMap[key] || [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Export convenience function to run conformance tests
|
|
*/
|
|
export async function runConformanceTests(
|
|
samplesDir: string = 'test-samples',
|
|
generateReport: boolean = true
|
|
): Promise<void> {
|
|
const harness = new ConformanceTestHarness();
|
|
|
|
// Load samples
|
|
await harness.loadTestSamples(samplesDir);
|
|
|
|
// Run tests
|
|
await harness.runConformanceTests();
|
|
|
|
// Generate reports
|
|
if (generateReport) {
|
|
const coverage = harness.generateCoverageMatrix();
|
|
console.log('\n📊 Coverage Report:');
|
|
console.log(` Overall: ${coverage.coveragePercentage.toFixed(1)}%`);
|
|
console.log(` Rules covered: ${coverage.coveredRules}/${coverage.totalRules}`);
|
|
|
|
// Show category breakdown
|
|
console.log('\n By Category:');
|
|
for (const [category, data] of Object.entries(coverage.byCategory)) {
|
|
const pct = data.total > 0 ? ((data.covered / data.total) * 100).toFixed(1) : '0';
|
|
console.log(` - ${category}: ${data.covered}/${data.total} (${pct}%)`);
|
|
}
|
|
|
|
// Generate HTML report
|
|
await harness.generateHTMLReport();
|
|
}
|
|
} |