feat(validation): Implement EN16931 compliance validation types and VAT categories

- 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.
This commit is contained in:
2025-08-11 12:25:32 +00:00
parent 01c6e8daad
commit 10e14af85b
53 changed files with 11315 additions and 17 deletions

View File

@@ -0,0 +1,172 @@
import { expect, tap } from '@git.zone/tstest/tapbundle/index.js';
import * as path from 'path';
import * as fs from 'fs';
// Import conformance harness
import { ConformanceTestHarness, runConformanceTests } from '../ts/formats/validation/conformance.harness.js';
tap.test('Conformance Test Harness - initialization', async () => {
const harness = new ConformanceTestHarness();
expect(harness).toBeInstanceOf(ConformanceTestHarness);
});
tap.test('Conformance Test Harness - load test samples', async () => {
const harness = new ConformanceTestHarness();
// Check if test-samples directory exists
const samplesDir = path.join(process.cwd(), 'test-samples');
if (fs.existsSync(samplesDir)) {
await harness.loadTestSamples(samplesDir);
console.log('Test samples loaded successfully');
} else {
console.log('Test samples directory not found - skipping');
}
});
tap.test('Conformance Test Harness - run minimal test', async (tools) => {
const harness = new ConformanceTestHarness();
// Create a minimal test sample
const minimalUBL = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:CustomizationID>urn:cen.eu:en16931:2017</cbc:CustomizationID>
<cbc:ID>TEST-001</cbc:ID>
<cbc:IssueDate>2025-01-11</cbc:IssueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Test Seller</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Test Street 1</cbc:StreetName>
<cbc:CityName>Test City</cbc:CityName>
<cbc:PostalZone>12345</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Test Buyer</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Test Street 2</cbc:StreetName>
<cbc:CityName>Test City</cbc:CityName>
<cbc:PostalZone>54321</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">19.00</cbc:TaxAmount>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="EUR">100.00</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="EUR">19.00</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>19</cbc:Percent>
</cac:TaxCategory>
</cac:TaxSubtotal>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:TaxExclusiveAmount currencyID="EUR">100.00</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="EUR">119.00</cbc:TaxInclusiveAmount>
</cac:LegalMonetaryTotal>
<cac:InvoiceLine>
<cbc:ID>1</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">1</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">100.00</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Name>Test Product</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>19</cbc:Percent>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">100.00</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
</Invoice>`;
// Create temporary test directory
const tempDir = path.join(process.cwd(), '.nogit', 'test-conformance');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
// Write test file
const testFile = path.join(tempDir, 'minimal-test.xml');
fs.writeFileSync(testFile, minimalUBL);
// Create test sample metadata
const testSamples = [{
id: 'minimal-test',
name: 'minimal-test.xml',
path: testFile,
format: 'UBL' as const,
standard: 'EN16931',
expectedValid: false, // We expect some validation errors
description: 'Minimal test invoice'
}];
// Load test samples manually
(harness as any).testSamples = testSamples;
// Run conformance test
await harness.runConformanceTests();
// Generate coverage matrix
const coverage = harness.generateCoverageMatrix();
console.log(`Coverage: ${coverage.coveragePercentage.toFixed(1)}%`);
console.log(`Rules covered: ${coverage.coveredRules}/${coverage.totalRules}`);
// Clean up
fs.unlinkSync(testFile);
});
tap.test('Conformance Test Harness - coverage report generation', async () => {
const harness = new ConformanceTestHarness();
// Generate empty coverage report
const coverage = harness.generateCoverageMatrix();
expect(coverage.totalRules).toBeGreaterThan(100);
expect(coverage.coveredRules).toBeGreaterThanOrEqual(0);
expect(coverage.coveragePercentage).toBeGreaterThanOrEqual(0);
expect(coverage.byCategory.document.total).toBeGreaterThan(0);
expect(coverage.byCategory.calculation.total).toBeGreaterThan(0);
expect(coverage.byCategory.vat.total).toBeGreaterThan(0);
});
tap.test('Conformance Test Harness - full test suite', async (tools) => {
tools.timeout(60000); // 60 seconds timeout for full test
const samplesDir = path.join(process.cwd(), 'test-samples');
if (!fs.existsSync(samplesDir)) {
console.log('Test samples not found - skipping full conformance test');
console.log('Run: npm run download-test-samples');
return;
}
// Run full conformance test
console.log('\n=== Running Full Conformance Test Suite ===\n');
await runConformanceTests(samplesDir, true);
// Check if HTML report was generated
const reportPath = path.join(process.cwd(), 'coverage-report.html');
if (fs.existsSync(reportPath)) {
console.log(`\n✅ HTML report generated: ${reportPath}`);
}
});
export default tap;

128
test/test.currency-utils.ts Normal file
View File

@@ -0,0 +1,128 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import {
getCurrencyMinorUnits,
roundToCurrency,
getCurrencyTolerance,
areMonetaryValuesEqual,
CurrencyCalculator,
RoundingMode
} from '../ts/formats/utils/currency.utils.js';
tap.test('Currency Utils - should handle different currency decimal places', async () => {
// Standard 2 decimal currencies
expect(getCurrencyMinorUnits('EUR')).toEqual(2);
expect(getCurrencyMinorUnits('USD')).toEqual(2);
expect(getCurrencyMinorUnits('GBP')).toEqual(2);
// Zero decimal currencies
expect(getCurrencyMinorUnits('JPY')).toEqual(0);
expect(getCurrencyMinorUnits('KRW')).toEqual(0);
// Three decimal currencies
expect(getCurrencyMinorUnits('KWD')).toEqual(3);
expect(getCurrencyMinorUnits('TND')).toEqual(3);
// Unknown currency defaults to 2
expect(getCurrencyMinorUnits('XXX')).toEqual(2);
});
tap.test('Currency Utils - should round values correctly', async () => {
// EUR - 2 decimals
expect(roundToCurrency(10.234, 'EUR')).toEqual(10.23);
expect(roundToCurrency(10.235, 'EUR')).toEqual(10.24); // Half-up
expect(roundToCurrency(10.236, 'EUR')).toEqual(10.24);
// JPY - 0 decimals
expect(roundToCurrency(1234.56, 'JPY')).toEqual(1235);
expect(roundToCurrency(1234.49, 'JPY')).toEqual(1234);
// KWD - 3 decimals
expect(roundToCurrency(10.2345, 'KWD')).toEqual(10.235); // Half-up
expect(roundToCurrency(10.2344, 'KWD')).toEqual(10.234);
});
tap.test('Currency Utils - should use different rounding modes', async () => {
const value = 10.235;
// Half-up (default)
expect(roundToCurrency(value, 'EUR', RoundingMode.HALF_UP)).toEqual(10.24);
// Half-down
expect(roundToCurrency(value, 'EUR', RoundingMode.HALF_DOWN)).toEqual(10.23);
// Half-even (banker's rounding)
expect(roundToCurrency(10.235, 'EUR', RoundingMode.HALF_EVEN)).toEqual(10.24); // 23 is odd, round up
expect(roundToCurrency(10.245, 'EUR', RoundingMode.HALF_EVEN)).toEqual(10.24); // 24 is even, round down
// Always up
expect(roundToCurrency(10.231, 'EUR', RoundingMode.UP)).toEqual(10.24);
// Always down (truncate)
expect(roundToCurrency(10.239, 'EUR', RoundingMode.DOWN)).toEqual(10.23);
});
tap.test('Currency Utils - should calculate correct tolerance', async () => {
// EUR - tolerance is 0.005 (half of 0.01)
expect(getCurrencyTolerance('EUR')).toEqual(0.005);
// JPY - tolerance is 0.5 (half of 1)
expect(getCurrencyTolerance('JPY')).toEqual(0.5);
// KWD - tolerance is 0.0005 (half of 0.001)
expect(getCurrencyTolerance('KWD')).toEqual(0.0005);
});
tap.test('Currency Utils - should compare monetary values with tolerance', async () => {
// EUR comparisons
expect(areMonetaryValuesEqual(10.23, 10.234, 'EUR')).toEqual(true); // Within tolerance
expect(areMonetaryValuesEqual(10.23, 10.236, 'EUR')).toEqual(false); // Outside tolerance
// JPY comparisons
expect(areMonetaryValuesEqual(1234, 1234.4, 'JPY')).toEqual(true); // Within tolerance
expect(areMonetaryValuesEqual(1234, 1235, 'JPY')).toEqual(false); // Outside tolerance
// KWD comparisons
expect(areMonetaryValuesEqual(10.234, 10.2344, 'KWD')).toEqual(true); // Within tolerance
expect(areMonetaryValuesEqual(10.234, 10.235, 'KWD')).toEqual(false); // Outside tolerance
});
tap.test('CurrencyCalculator - should perform EN16931 calculations', async () => {
// EUR calculator
const eurCalc = new CurrencyCalculator('EUR');
// Line net calculation
const lineNet = eurCalc.calculateLineNet(5, 19.99, 2.50);
expect(lineNet).toEqual(97.45); // (5 * 19.99) - 2.50 = 97.45
// VAT calculation
const vat = eurCalc.calculateVAT(100, 19);
expect(vat).toEqual(19.00);
// JPY calculator (no decimals)
const jpyCalc = new CurrencyCalculator('JPY');
const jpyLineNet = jpyCalc.calculateLineNet(3, 1234.56);
expect(jpyLineNet).toEqual(3704); // Rounded to no decimals
const jpyVat = jpyCalc.calculateVAT(10000, 8);
expect(jpyVat).toEqual(800);
});
tap.test('CurrencyCalculator - should handle edge cases', async () => {
const calc = new CurrencyCalculator('EUR');
// Rounding at exact midpoint
expect(calc.round(10.235)).toEqual(10.24); // Half-up
expect(calc.round(10.245)).toEqual(10.25); // Half-up
// Very small values
expect(calc.round(0.001)).toEqual(0.00);
expect(calc.round(0.004)).toEqual(0.00);
expect(calc.round(0.005)).toEqual(0.01);
// Negative values
expect(calc.round(-10.234)).toEqual(-10.23);
expect(calc.round(-10.235)).toEqual(-10.24);
});
tap.start();

View File

@@ -0,0 +1,238 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { EInvoice } from '../ts/index.js';
import { ValidationLevel } from '../ts/interfaces/common.js';
// Test EN16931 business rules and code list validators
tap.test('EN16931 Validators - should validate business rules with feature flags', async () => {
// Create a minimal invoice that violates several EN16931 rules
const invoice = new EInvoice();
// Set some basic fields but leave mandatory ones missing
invoice.currency = 'EUR';
invoice.date = Date.now();
invoice.from = {
type: 'company',
name: 'Test Seller',
address: {
streetName: 'Test Street',
houseNumber: '1',
city: 'Berlin',
postalCode: '10115',
countryCode: 'DE'
}
} as any;
// Missing buyer details and invoice ID (violates BR-02, BR-07)
// Add an item with calculation issues
invoice.items = [{
position: 1,
name: 'Test Item',
unitType: 'C62', // Valid UNECE code
unitQuantity: 10,
unitNetPrice: 100,
vatPercentage: 19
}];
// Test without feature flags (should pass basic validation)
const basicResult = await invoice.validate(ValidationLevel.BUSINESS);
console.log('Basic validation errors:', basicResult.errors.length);
// Test with EN16931 business rules feature flag
const en16931Result = await invoice.validate(ValidationLevel.BUSINESS, {
featureFlags: ['EN16931_BUSINESS_RULES'],
checkCalculations: true,
checkVAT: true
});
console.log('EN16931 validation errors:', en16931Result.errors.length);
// Should find missing mandatory fields
const mandatoryErrors = en16931Result.errors.filter(e =>
e.code && ['BR-01', 'BR-02', 'BR-07'].includes(e.code)
);
expect(mandatoryErrors.length).toBeGreaterThan(0);
// Test code list validation
const codeListResult = await invoice.validate(ValidationLevel.BUSINESS, {
featureFlags: ['CODE_LIST_VALIDATION'],
checkCodeLists: true
});
console.log('Code list validation errors:', codeListResult.errors.length);
// Test invalid currency code
invoice.currency = 'XXX' as any; // Invalid currency
const currencyResult = await invoice.validate(ValidationLevel.BUSINESS, {
featureFlags: ['CODE_LIST_VALIDATION']
});
const currencyErrors = currencyResult.errors.filter(e =>
e.code && e.code.includes('BR-CL-03')
);
expect(currencyErrors.length).toEqual(1);
// Test with both validators enabled
const fullResult = await invoice.validate(ValidationLevel.BUSINESS, {
featureFlags: ['EN16931_BUSINESS_RULES', 'CODE_LIST_VALIDATION'],
checkCalculations: true,
checkVAT: true,
checkCodeLists: true,
reportOnly: true // Don't fail validation, just report
});
console.log('Full validation with both validators:');
console.log('- Total errors:', fullResult.errors.length);
console.log('- Valid (report-only mode):', fullResult.valid);
expect(fullResult.valid).toEqual(true); // Should be true in report-only mode
expect(fullResult.errors.length).toBeGreaterThan(0); // Should find issues
console.log('Error codes found:', fullResult.errors.map(e => e.code));
});
tap.test('EN16931 Validators - should validate calculations correctly', async () => {
const invoice = new EInvoice();
// Set up a complete invoice with correct mandatory fields
invoice.accountingDocId = 'INV-2024-001';
invoice.currency = 'EUR';
invoice.date = Date.now();
invoice.metadata = {
customizationId: 'urn:cen.eu:en16931:2017'
};
invoice.from = {
type: 'company',
name: 'Test Seller GmbH',
address: {
streetName: 'Hauptstraße',
houseNumber: '1',
city: 'Berlin',
postalCode: '10115',
countryCode: 'DE'
}
} as any;
invoice.to = {
type: 'company',
name: 'Test Buyer Ltd',
address: {
streetName: 'Main Street',
houseNumber: '10',
city: 'London',
postalCode: 'SW1A 1AA',
countryCode: 'GB'
}
} as any;
// Add items with specific amounts
invoice.items = [
{
position: 1,
name: 'Product A',
unitType: 'C62',
unitQuantity: 5,
unitNetPrice: 100.00,
vatPercentage: 19
},
{
position: 2,
name: 'Product B',
unitType: 'C62',
unitQuantity: 3,
unitNetPrice: 50.00,
vatPercentage: 19
}
];
// Expected calculations:
// Line 1: 5 * 100 = 500
// Line 2: 3 * 50 = 150
// Total net: 650
// VAT (19%): 123.50
// Total gross: 773.50
const result = await invoice.validate(ValidationLevel.BUSINESS, {
featureFlags: ['EN16931_BUSINESS_RULES'],
checkCalculations: true,
tolerance: 0.01
});
// Should not have calculation errors
const calcErrors = result.errors.filter(e =>
e.code && e.code.startsWith('BR-CO-')
);
console.log('Calculation validation errors:', calcErrors);
expect(calcErrors.length).toEqual(0);
// Verify computed totals
expect(invoice.totalNet).toEqual(650);
expect(invoice.totalVat).toEqual(123.50);
expect(invoice.totalGross).toEqual(773.50);
});
tap.test('EN16931 Validators - should validate VAT rules correctly', async () => {
const invoice = new EInvoice();
// Set up mandatory fields
invoice.accountingDocId = 'INV-2024-002';
invoice.currency = 'EUR';
invoice.date = Date.now();
invoice.metadata = {
customizationId: 'urn:cen.eu:en16931:2017'
};
invoice.from = {
type: 'company',
name: 'Seller',
address: { countryCode: 'DE' }
} as any;
invoice.to = {
type: 'company',
name: 'Buyer',
address: { countryCode: 'FR' }
} as any;
// Add mixed VAT rate items
invoice.items = [
{
position: 1,
name: 'Standard rated item',
unitType: 'C62',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19 // Standard rate
},
{
position: 2,
name: 'Zero rated item',
unitType: 'C62',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 0 // Zero rate
}
];
const result = await invoice.validate(ValidationLevel.BUSINESS, {
featureFlags: ['EN16931_BUSINESS_RULES'],
checkVAT: true
});
// Check for VAT breakdown requirements
const vatErrors = result.errors.filter(e =>
e.code && (e.code.startsWith('BR-S-') || e.code.startsWith('BR-Z-'))
);
console.log('VAT validation results:');
console.log('- VAT errors found:', vatErrors.length);
console.log('- Tax breakdown:', invoice.taxBreakdown);
// Should have proper tax breakdown
expect(invoice.taxBreakdown.length).toEqual(2);
expect(invoice.taxBreakdown.find(t => t.taxPercent === 19)).toBeTruthy();
expect(invoice.taxBreakdown.find(t => t.taxPercent === 0)).toBeTruthy();
});
export default tap.start();

View File

@@ -0,0 +1,163 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { SchematronValidator, HybridValidator } from '../ts/formats/validation/schematron.validator.js';
import { SchematronDownloader } from '../ts/formats/validation/schematron.downloader.js';
import { SchematronWorkerPool } from '../ts/formats/validation/schematron.worker.js';
tap.test('Schematron Infrastructure - should initialize correctly', async () => {
const validator = new SchematronValidator();
expect(validator).toBeInstanceOf(SchematronValidator);
expect(validator.hasRules()).toBeFalse();
});
tap.test('Schematron Infrastructure - should load Schematron rules', async () => {
const validator = new SchematronValidator();
// Load a simple test Schematron
const testSchematron = `<?xml version="1.0" encoding="UTF-8"?>
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron">
<sch:ns prefix="ubl" uri="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"/>
<sch:pattern id="test-pattern">
<sch:rule context="//ubl:Invoice">
<sch:assert test="ubl:ID" id="TEST-01">
Invoice must have an ID
</sch:assert>
</sch:rule>
</sch:pattern>
</sch:schema>`;
await validator.loadSchematron(testSchematron, false);
expect(validator.hasRules()).toBeTrue();
});
tap.test('Schematron Infrastructure - should detect phases', async () => {
const validator = new SchematronValidator();
const schematronWithPhases = `<?xml version="1.0" encoding="UTF-8"?>
<sch:schema xmlns:sch="http://purl.oclc.org/dsdl/schematron">
<sch:phase id="basic">
<sch:active pattern="basic-rules"/>
</sch:phase>
<sch:phase id="extended">
<sch:active pattern="basic-rules"/>
<sch:active pattern="extended-rules"/>
</sch:phase>
<sch:pattern id="basic-rules">
<sch:rule context="//Invoice">
<sch:assert test="ID">Invoice must have ID</sch:assert>
</sch:rule>
</sch:pattern>
</sch:schema>`;
await validator.loadSchematron(schematronWithPhases, false);
const phases = await validator.getPhases();
expect(phases).toContain('basic');
expect(phases).toContain('extended');
});
tap.test('Schematron Downloader - should initialize', async () => {
const downloader = new SchematronDownloader('.nogit/schematron-test');
await downloader.initialize();
// Check that sources are defined
expect(downloader).toBeInstanceOf(SchematronDownloader);
});
tap.test('Schematron Downloader - should list available sources', async () => {
const { SCHEMATRON_SOURCES } = await import('../ts/formats/validation/schematron.downloader.js');
// Check EN16931 sources
expect(SCHEMATRON_SOURCES.EN16931).toBeDefined();
expect(SCHEMATRON_SOURCES.EN16931.length).toBeGreaterThan(0);
const en16931Ubl = SCHEMATRON_SOURCES.EN16931.find(s => s.format === 'UBL');
expect(en16931Ubl).toBeDefined();
expect(en16931Ubl?.name).toEqual('EN16931-UBL');
// Check PEPPOL sources
expect(SCHEMATRON_SOURCES.PEPPOL).toBeDefined();
expect(SCHEMATRON_SOURCES.PEPPOL.length).toBeGreaterThan(0);
// Check XRechnung sources
expect(SCHEMATRON_SOURCES.XRECHNUNG).toBeDefined();
expect(SCHEMATRON_SOURCES.XRECHNUNG.length).toBeGreaterThan(0);
});
tap.test('Hybrid Validator - should combine validators', async () => {
const schematronValidator = new SchematronValidator();
const hybrid = new HybridValidator(schematronValidator);
// Add a mock TypeScript validator
const mockTSValidator = {
validate: (xml: string) => [{
ruleId: 'TS-TEST-01',
severity: 'error' as const,
message: 'Test error from TS validator',
btReference: undefined,
bgReference: undefined
}]
};
hybrid.addTSValidator(mockTSValidator);
// Test validation (will only run TS validator since no Schematron loaded)
const results = await hybrid.validate('<Invoice/>');
expect(results.length).toEqual(1);
expect(results[0].ruleId).toEqual('TS-TEST-01');
});
tap.test('Schematron Worker Pool - should initialize', async () => {
const pool = new SchematronWorkerPool(2);
// Test pool stats
const stats = pool.getStats();
expect(stats.totalWorkers).toEqual(0); // Not initialized yet
expect(stats.queuedTasks).toEqual(0);
// Note: Full worker pool test would require actual worker thread setup
// which may not work in all test environments
});
tap.test('Schematron Validator - SVRL parsing', async () => {
const validator = new SchematronValidator();
// Test SVRL output parsing
const testSVRL = `<?xml version="1.0" encoding="UTF-8"?>
<svrl:schematron-output xmlns:svrl="http://purl.oclc.org/dsdl/svrl">
<svrl:active-pattern document="test.xml"/>
<svrl:failed-assert test="count(ID) = 1"
location="/Invoice"
id="BR-01"
flag="fatal">
<svrl:text>[BR-01] Invoice must have exactly one ID</svrl:text>
</svrl:failed-assert>
<svrl:successful-report test="Currency = 'EUR'"
location="/Invoice"
id="INFO-01"
flag="information">
<svrl:text>Currency is EUR</svrl:text>
</svrl:successful-report>
</svrl:schematron-output>`;
// This would test the SVRL parsing logic
// The actual implementation would parse this and return ValidationResult[]
expect(testSVRL).toContain('failed-assert');
expect(testSVRL).toContain('BR-01');
});
tap.test('Schematron Integration - should handle missing files gracefully', async () => {
const validator = new SchematronValidator();
try {
await validator.loadSchematron('non-existent-file.sch', true);
expect(true).toBeFalse(); // Should not reach here
} catch (error) {
expect(error).toBeDefined();
}
});
tap.start();