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

425 lines
15 KiB
TypeScript

import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as plugins from '../../../ts/plugins.ts';
import { EInvoice } from '../../../ts/classes.xinvoice.ts';
import { CorpusLoader } from '../../helpers/corpus.loader.ts';
import { PerformanceTracker } from '../../helpers/performance.tracker.ts';
const testTimeout = 300000; // 5 minutes timeout for corpus processing
// VAL-09: Semantic Level Validation
// Tests semantic-level validation including data types, value ranges,
// and cross-field dependencies according to EN16931 semantic model
tap.test('VAL-09: Semantic Level Validation - Data Type Validation', async (tools) => {
const startTime = Date.now();
// Test numeric field validation
const numericValidationTests = [
{ value: '123.45', field: 'InvoiceTotal', valid: true },
{ value: '0.00', field: 'InvoiceTotal', valid: true },
{ value: 'abc', field: 'InvoiceTotal', valid: false },
{ value: '', field: 'InvoiceTotal', valid: false },
{ value: '123.456', field: 'InvoiceTotal', valid: true }, // Should handle rounding
{ value: '-123.45', field: 'InvoiceTotal', valid: false }, // Negative not allowed
];
for (const test of numericValidationTests) {
try {
// Create a minimal test invoice with the value to test
const testXml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>TEST-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<LegalMonetaryTotal>
<TaxExclusiveAmount currencyID="EUR">${test.value}</TaxExclusiveAmount>
</LegalMonetaryTotal>
</Invoice>`;
const invoice = new EInvoice();
const parseResult = await invoice.fromXmlString(testXml);
if (test.valid) {
expect(parseResult).toBeTruthy();
tools.log(`✓ Valid numeric value '${test.value}' accepted for ${test.field}`);
} else {
// Should either fail parsing or validation
const validationResult = await invoice.validate();
expect(validationResult.valid).toBe(false);
tools.log(`✓ Invalid numeric value '${test.value}' rejected for ${test.field}`);
}
} catch (error) {
if (!test.valid) {
tools.log(`✓ Invalid numeric value '${test.value}' properly rejected with error: ${error.message}`);
} else {
throw error;
}
}
}
const duration = Date.now() - startTime;
PerformanceTracker.recordMetric('semantic-validation-datatypes', duration);
});
tap.test('VAL-09: Semantic Level Validation - Date Format Validation', async (tools) => {
const startTime = Date.now();
// Test date format validation according to ISO 8601
const dateValidationTests = [
{ value: '2024-01-01', valid: true },
{ value: '2024-12-31', valid: true },
{ value: '2024-02-29', valid: true }, // Leap year
{ value: '2023-02-29', valid: false }, // Not a leap year
{ value: '2024-13-01', valid: false }, // Invalid month
{ value: '2024-01-32', valid: false }, // Invalid day
{ value: '24-01-01', valid: false }, // Wrong format
{ value: '2024/01/01', valid: false }, // Wrong separator
{ value: '', valid: false }, // Empty
{ value: 'invalid-date', valid: false }, // Non-date string
];
for (const test of dateValidationTests) {
try {
const testXml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>TEST-001</ID>
<IssueDate>${test.value}</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
</Invoice>`;
const invoice = new EInvoice();
const parseResult = await invoice.fromXmlString(testXml);
if (test.valid) {
expect(parseResult).toBeTruthy();
const validationResult = await invoice.validate();
expect(validationResult.valid).toBeTrue();
tools.log(`✓ Valid date '${test.value}' accepted`);
} else {
// Should either fail parsing or validation
if (parseResult) {
const validationResult = await invoice.validate();
expect(validationResult.valid).toBe(false);
}
tools.log(`✓ Invalid date '${test.value}' rejected`);
}
} catch (error) {
if (!test.valid) {
tools.log(`✓ Invalid date '${test.value}' properly rejected with error: ${error.message}`);
} else {
throw error;
}
}
}
const duration = Date.now() - startTime;
PerformanceTracker.recordMetric('semantic-validation-dates', duration);
});
tap.test('VAL-09: Semantic Level Validation - Currency Code Validation', async (tools) => {
const startTime = Date.now();
// Test currency code validation according to ISO 4217
const currencyValidationTests = [
{ code: 'EUR', valid: true },
{ code: 'USD', valid: true },
{ code: 'GBP', valid: true },
{ code: 'JPY', valid: true },
{ code: 'CHF', valid: true },
{ code: 'SEK', valid: true },
{ code: 'XXX', valid: false }, // Invalid currency
{ code: 'ABC', valid: false }, // Non-existent currency
{ code: 'eur', valid: false }, // Lowercase
{ code: 'EURO', valid: false }, // Too long
{ code: 'EU', valid: false }, // Too short
{ code: '', valid: false }, // Empty
{ code: '123', valid: false }, // Numeric
];
for (const test of currencyValidationTests) {
try {
const testXml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>TEST-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<DocumentCurrencyCode>${test.code}</DocumentCurrencyCode>
<LegalMonetaryTotal>
<TaxExclusiveAmount currencyID="${test.code}">100.00</TaxExclusiveAmount>
</LegalMonetaryTotal>
</Invoice>`;
const invoice = new EInvoice();
const parseResult = await invoice.fromXmlString(testXml);
if (test.valid) {
expect(parseResult).toBeTruthy();
tools.log(`✓ Valid currency code '${test.code}' accepted`);
} else {
// Should either fail parsing or validation
if (parseResult) {
const validationResult = await invoice.validate();
expect(validationResult.valid).toBe(false);
}
tools.log(`✓ Invalid currency code '${test.code}' rejected`);
}
} catch (error) {
if (!test.valid) {
tools.log(`✓ Invalid currency code '${test.code}' properly rejected with error: ${error.message}`);
} else {
throw error;
}
}
}
const duration = Date.now() - startTime;
PerformanceTracker.recordMetric('semantic-validation-currency', duration);
});
tap.test('VAL-09: Semantic Level Validation - Cross-Field Dependencies', async (tools) => {
const startTime = Date.now();
// Test semantic dependencies between fields
const dependencyTests = [
{
name: 'Tax Amount vs Tax Rate',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>TEST-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<TaxTotal>
<TaxAmount currencyID="EUR">19.00</TaxAmount>
<TaxSubtotal>
<TaxableAmount currencyID="EUR">100.00</TaxableAmount>
<TaxAmount currencyID="EUR">19.00</TaxAmount>
<TaxCategory>
<Percent>19.00</Percent>
</TaxCategory>
</TaxSubtotal>
</TaxTotal>
</Invoice>`,
valid: true
},
{
name: 'Inconsistent Tax Calculation',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>TEST-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<TaxTotal>
<TaxAmount currencyID="EUR">20.00</TaxAmount>
<TaxSubtotal>
<TaxableAmount currencyID="EUR">100.00</TaxableAmount>
<TaxAmount currencyID="EUR">19.00</TaxAmount>
<TaxCategory>
<Percent>19.00</Percent>
</TaxCategory>
</TaxSubtotal>
</TaxTotal>
</Invoice>`,
valid: false
}
];
for (const test of dependencyTests) {
try {
const invoice = new EInvoice();
const parseResult = await invoice.fromXmlString(test.xml);
if (parseResult) {
const validationResult = await invoice.validate();
if (test.valid) {
expect(validationResult.valid).toBeTrue();
tools.log(`${test.name}: Valid cross-field dependency accepted`);
} else {
expect(validationResult.valid).toBe(false);
tools.log(`${test.name}: Invalid cross-field dependency rejected`);
}
} else if (!test.valid) {
tools.log(`${test.name}: Invalid dependency rejected at parse time`);
} else {
throw new Error(`Expected valid parse for ${test.name}`);
}
} catch (error) {
if (!test.valid) {
tools.log(`${test.name}: Invalid dependency properly rejected with error: ${error.message}`);
} else {
throw error;
}
}
}
const duration = Date.now() - startTime;
PerformanceTracker.recordMetric('semantic-validation-dependencies', duration);
});
tap.test('VAL-09: Semantic Level Validation - Value Range Validation', async (tools) => {
const startTime = Date.now();
// Test value range constraints
const rangeTests = [
{
field: 'Tax Percentage',
value: '19.00',
valid: true,
description: 'Normal tax rate'
},
{
field: 'Tax Percentage',
value: '0.00',
valid: true,
description: 'Zero tax rate'
},
{
field: 'Tax Percentage',
value: '100.00',
valid: true,
description: 'Maximum tax rate'
},
{
field: 'Tax Percentage',
value: '-5.00',
valid: false,
description: 'Negative tax rate'
},
{
field: 'Tax Percentage',
value: '150.00',
valid: false,
description: 'Unrealistic high tax rate'
}
];
for (const test of rangeTests) {
try {
const testXml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>TEST-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<TaxTotal>
<TaxSubtotal>
<TaxCategory>
<Percent>${test.value}</Percent>
</TaxCategory>
</TaxSubtotal>
</TaxTotal>
</Invoice>`;
const invoice = new EInvoice();
const parseResult = await invoice.fromXmlString(testXml);
if (test.valid) {
expect(parseResult).toBeTruthy();
tools.log(`${test.description}: Valid value '${test.value}' accepted for ${test.field}`);
} else {
// Should either fail parsing or validation
if (parseResult) {
const validationResult = await invoice.validate();
expect(validationResult.valid).toBe(false);
}
tools.log(`${test.description}: Invalid value '${test.value}' rejected for ${test.field}`);
}
} catch (error) {
if (!test.valid) {
tools.log(`${test.description}: Invalid value properly rejected with error: ${error.message}`);
} else {
throw error;
}
}
}
const duration = Date.now() - startTime;
PerformanceTracker.recordMetric('semantic-validation-ranges', duration);
});
tap.test('VAL-09: Semantic Level Validation - Corpus Semantic Validation', { timeout: testTimeout }, async (tools) => {
const startTime = Date.now();
let processedFiles = 0;
let validFiles = 0;
let semanticErrors = 0;
// Test semantic validation against UBL corpus files
try {
const ublFiles = await CorpusLoader.getFiles('UBL_XML_RECHNUNG');
for (const filePath of ublFiles.slice(0, 10)) { // Process first 10 files for performance
try {
const invoice = new EInvoice();
const parseResult = await invoice.fromFile(filePath);
processedFiles++;
if (parseResult) {
const validationResult = await invoice.validate();
if (validationResult.valid) {
validFiles++;
} else {
// Check if errors are semantic-level
const semanticErrorTypes = ['data-type', 'range', 'dependency', 'format'];
const hasSemanticErrors = validationResult.errors?.some(error =>
semanticErrorTypes.some(type => error.message.toLowerCase().includes(type))
);
if (hasSemanticErrors) {
semanticErrors++;
tools.log(`Semantic validation errors in ${plugins.path.basename(filePath)}`);
}
}
}
// Performance check
if (processedFiles % 5 === 0) {
const currentDuration = Date.now() - startTime;
const avgPerFile = currentDuration / processedFiles;
tools.log(`Processed ${processedFiles} files, avg ${avgPerFile.toFixed(0)}ms per file`);
}
} catch (error) {
tools.log(`Failed to process ${plugins.path.basename(filePath)}: ${error.message}`);
}
}
const successRate = processedFiles > 0 ? (validFiles / processedFiles) * 100 : 0;
const semanticErrorRate = processedFiles > 0 ? (semanticErrors / processedFiles) * 100 : 0;
tools.log(`Semantic validation completed:`);
tools.log(`- Processed: ${processedFiles} files`);
tools.log(`- Valid: ${validFiles} files (${successRate.toFixed(1)}%)`);
tools.log(`- Semantic errors: ${semanticErrors} files (${semanticErrorRate.toFixed(1)}%)`);
// Semantic validation should have high success rate for well-formed corpus
expect(successRate).toBeGreaterThan(70);
} catch (error) {
tools.log(`Corpus semantic validation failed: ${error.message}`);
throw error;
}
const totalDuration = Date.now() - startTime;
PerformanceTracker.recordMetric('semantic-validation-corpus', totalDuration);
// Performance expectation: should complete within reasonable time
expect(totalDuration).toBeLessThan(60000); // 60 seconds max
tools.log(`Semantic validation performance: ${totalDuration}ms total`);
});
tap.test('VAL-09: Performance Summary', async (tools) => {
const operations = [
'semantic-validation-datatypes',
'semantic-validation-dates',
'semantic-validation-currency',
'semantic-validation-dependencies',
'semantic-validation-ranges',
'semantic-validation-corpus'
];
for (const operation of operations) {
const summary = await PerformanceTracker.getSummary(operation);
if (summary) {
tools.log(`${operation}: avg=${summary.average}ms, min=${summary.min}ms, max=${summary.max}ms, p95=${summary.p95}ms`);
}
}
});