einvoice/test/suite/einvoice_validation/test.val-03.semantic-validation.ts

343 lines
12 KiB
TypeScript
Raw Normal View History

2025-05-25 19:45:37 +00:00
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { promises as fs } from 'fs';
import * as path from 'path';
import { CorpusLoader } from '../../helpers/corpus.loader.js';
import { PerformanceTracker } from '../../helpers/performance.tracker.js';
tap.test('VAL-03: Semantic Validation - should validate semantic correctness', async () => {
// Get various XML files from corpus to test semantic validation
const ciiFiles = await CorpusLoader.getFiles('CII_XMLRECHNUNG');
const ublFiles = await CorpusLoader.getFiles('UBL_XMLRECHNUNG');
const testFiles = [...ciiFiles.slice(0, 3), ...ublFiles.slice(0, 3)];
console.log(`Testing semantic validation on ${testFiles.length} files`);
let validCount = 0;
let invalidCount = 0;
let errorCount = 0;
const { EInvoice } = await import('../../../ts/index.js');
for (const filePath of testFiles) {
const fileName = path.basename(filePath);
try {
// Read and parse XML
const xmlContent = await fs.readFile(filePath, 'utf-8');
const { result: einvoice } = await PerformanceTracker.track(
'semantic-xml-loading',
async () => await EInvoice.fromXml(xmlContent)
);
// Perform semantic validation
const { result: validation } = await PerformanceTracker.track(
'semantic-validation',
async () => {
// Use semantic validation level if available
return await einvoice.validate(/* ValidationLevel.SEMANTIC */);
},
{ file: fileName }
);
if (validation.valid) {
validCount++;
console.log(`${fileName}: Semantically valid`);
} else {
invalidCount++;
console.log(`${fileName}: Semantic issues found`);
if (validation.errors && validation.errors.length > 0) {
const semanticErrors = validation.errors.filter(e =>
e.message && (
e.message.toLowerCase().includes('semantic') ||
e.message.toLowerCase().includes('codelist') ||
e.message.toLowerCase().includes('reference')
)
);
console.log(` Semantic errors: ${semanticErrors.length}`);
semanticErrors.slice(0, 2).forEach(err => {
console.log(` - ${err.code}: ${err.message}`);
});
}
}
} catch (error) {
errorCount++;
console.log(`${fileName}: Error - ${error.message}`);
}
}
console.log(`\nSemantic Validation Summary:`);
console.log(` Valid: ${validCount}`);
console.log(` Invalid: ${invalidCount}`);
console.log(` Errors: ${errorCount}`);
// Performance summary
const perfSummary = await PerformanceTracker.getSummary('semantic-validation');
if (perfSummary) {
console.log(`\nSemantic Validation Performance:`);
console.log(` Average: ${perfSummary.average.toFixed(2)}ms`);
console.log(` Min: ${perfSummary.min.toFixed(2)}ms`);
console.log(` Max: ${perfSummary.max.toFixed(2)}ms`);
console.log(` P95: ${perfSummary.p95.toFixed(2)}ms`);
}
// Expect most files to be processed (valid or invalid, but not errored)
expect(validCount + invalidCount).toBeGreaterThan(errorCount);
});
tap.test('VAL-03: Codelist Validation - should validate against codelists', async () => {
const { EInvoice } = await import('../../../ts/index.js');
const codelistTests = [
{
name: 'Valid currency code',
xml: `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ID>TEST-001</cbc:ID>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
</Invoice>`,
shouldBeValid: true
},
{
name: 'Invalid currency code',
xml: `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ID>TEST-002</cbc:ID>
<cbc:DocumentCurrencyCode>INVALID</cbc:DocumentCurrencyCode>
</Invoice>`,
shouldBeValid: false
},
{
name: 'Valid unit code',
xml: `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
<cbc:ID>TEST-003</cbc:ID>
<cac:InvoiceLine>
<cbc:InvoicedQuantity unitCode="EA">5</cbc:InvoicedQuantity>
</cac:InvoiceLine>
</Invoice>`,
shouldBeValid: true
},
{
name: 'Invalid unit code',
xml: `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
<cbc:ID>TEST-004</cbc:ID>
<cac:InvoiceLine>
<cbc:InvoicedQuantity unitCode="BADUNIT">5</cbc:InvoicedQuantity>
</cac:InvoiceLine>
</Invoice>`,
shouldBeValid: false
}
];
for (const test of codelistTests) {
try {
const { result: validation } = await PerformanceTracker.track(
'codelist-validation',
async () => {
const einvoice = await EInvoice.fromXml(test.xml);
return await einvoice.validate();
}
);
console.log(`${test.name}: ${validation.valid ? 'VALID' : 'INVALID'}`);
if (!test.shouldBeValid && !validation.valid) {
console.log(` ✓ Correctly identified invalid codelist value`);
if (validation.errors) {
const codelistErrors = validation.errors.filter(e =>
e.message && e.message.toLowerCase().includes('codelist')
);
console.log(` Codelist errors: ${codelistErrors.length}`);
}
} else if (test.shouldBeValid && validation.valid) {
console.log(` ✓ Correctly validated codelist value`);
} else {
console.log(` ○ Unexpected result (codelist validation may need implementation)`);
}
} catch (error) {
console.log(`${test.name}: Error - ${error.message}`);
}
}
});
tap.test('VAL-03: Reference Validation - should validate cross-references', async () => {
const { EInvoice } = await import('../../../ts/index.js');
const referenceTests = [
{
name: 'Valid party references',
xml: `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
<cbc:ID>REF-001</cbc:ID>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Seller Company</cbc:Name>
</cac:PartyName>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Buyer Company</cbc:Name>
</cac:PartyName>
</cac:Party>
</cac:AccountingCustomerParty>
</Invoice>`,
shouldBeValid: true
},
{
name: 'Missing required party information',
xml: `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
<cbc:ID>REF-002</cbc:ID>
<cac:AccountingSupplierParty>
<!-- Missing Party/PartyName -->
</cac:AccountingSupplierParty>
</Invoice>`,
shouldBeValid: false
}
];
for (const test of referenceTests) {
try {
const { result: validation } = await PerformanceTracker.track(
'reference-validation',
async () => {
const einvoice = await EInvoice.fromXml(test.xml);
return await einvoice.validate();
}
);
console.log(`${test.name}: ${validation.valid ? 'VALID' : 'INVALID'}`);
if (!test.shouldBeValid && !validation.valid) {
console.log(` ✓ Correctly identified missing references`);
if (validation.errors) {
const refErrors = validation.errors.filter(e =>
e.message && (
e.message.toLowerCase().includes('reference') ||
e.message.toLowerCase().includes('missing') ||
e.message.toLowerCase().includes('required')
)
);
console.log(` Reference errors: ${refErrors.length}`);
}
} else if (test.shouldBeValid && validation.valid) {
console.log(` ✓ Correctly validated references`);
} else {
console.log(` ○ Unexpected result (reference validation may need implementation)`);
}
} catch (error) {
console.log(`${test.name}: Error - ${error.message}`);
}
}
});
tap.test('VAL-03: Data Type Validation - should validate data types and formats', async () => {
const { EInvoice } = await import('../../../ts/index.js');
const dataTypeTests = [
{
name: 'Valid date format',
xml: `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ID>DT-001</cbc:ID>
<cbc:IssueDate>2024-01-15</cbc:IssueDate>
</Invoice>`,
shouldBeValid: true
},
{
name: 'Invalid date format',
xml: `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ID>DT-002</cbc:ID>
<cbc:IssueDate>not-a-date</cbc:IssueDate>
</Invoice>`,
shouldBeValid: false
},
{
name: 'Valid decimal amount',
xml: `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
<cbc:ID>DT-003</cbc:ID>
<cac:LegalMonetaryTotal>
<cbc:TaxExclusiveAmount currencyID="EUR">100.50</cbc:TaxExclusiveAmount>
</cac:LegalMonetaryTotal>
</Invoice>`,
shouldBeValid: true
},
{
name: 'Invalid decimal amount',
xml: `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
<cbc:ID>DT-004</cbc:ID>
<cac:LegalMonetaryTotal>
<cbc:TaxExclusiveAmount currencyID="EUR">not-a-number</cbc:TaxExclusiveAmount>
</cac:LegalMonetaryTotal>
</Invoice>`,
shouldBeValid: false
}
];
for (const test of dataTypeTests) {
try {
const { result: validation } = await PerformanceTracker.track(
'datatype-validation',
async () => {
const einvoice = await EInvoice.fromXml(test.xml);
return await einvoice.validate();
}
);
console.log(`${test.name}: ${validation.valid ? 'VALID' : 'INVALID'}`);
if (!test.shouldBeValid && !validation.valid) {
console.log(` ✓ Correctly identified data type violation`);
if (validation.errors) {
const typeErrors = validation.errors.filter(e =>
e.message && (
e.message.toLowerCase().includes('format') ||
e.message.toLowerCase().includes('type') ||
e.message.toLowerCase().includes('invalid')
)
);
console.log(` Data type errors: ${typeErrors.length}`);
}
} else if (test.shouldBeValid && validation.valid) {
console.log(` ✓ Correctly validated data type`);
} else {
console.log(` ○ Unexpected result (data type validation may need implementation)`);
}
} catch (error) {
console.log(`${test.name}: Error - ${error.message}`);
// For invalid data types, errors during parsing might be expected
if (!test.shouldBeValid) {
console.log(` ✓ Error expected for invalid data type`);
}
}
}
});
tap.start();