230 lines
8.5 KiB
TypeScript
230 lines
8.5 KiB
TypeScript
|
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-02: EN16931 Business Rules - should validate Business Rules (BR-*)', async () => {
|
||
|
// Get EN16931 UBL test files for business rules
|
||
|
const brFiles = await CorpusLoader.getFiles('EN16931_UBL_INVOICE');
|
||
|
const businessRuleFiles = brFiles.filter(f => path.basename(f).startsWith('BR-') && path.basename(f).endsWith('.xml'));
|
||
|
|
||
|
console.log(`Testing ${businessRuleFiles.length} Business Rule validation files`);
|
||
|
|
||
|
const results = {
|
||
|
passed: 0,
|
||
|
failed: 0,
|
||
|
errors: [] as string[]
|
||
|
};
|
||
|
|
||
|
// Import required classes
|
||
|
const { EInvoice } = await import('../../../ts/index.js');
|
||
|
|
||
|
for (const filePath of businessRuleFiles.slice(0, 15)) { // Test first 15 for performance
|
||
|
const fileName = path.basename(filePath);
|
||
|
const shouldFail = fileName.startsWith('BR-'); // These files test specific BR violations
|
||
|
|
||
|
try {
|
||
|
// Read XML content
|
||
|
const xmlContent = await fs.readFile(filePath, 'utf-8');
|
||
|
|
||
|
// Track performance of business rule validation
|
||
|
const { result: einvoice } = await PerformanceTracker.track(
|
||
|
'br-xml-loading',
|
||
|
async () => {
|
||
|
return await EInvoice.fromXml(xmlContent);
|
||
|
},
|
||
|
{ file: fileName }
|
||
|
);
|
||
|
|
||
|
const { result: validation } = await PerformanceTracker.track(
|
||
|
'br-validation',
|
||
|
async () => {
|
||
|
// Use business validation level if available
|
||
|
return await einvoice.validate(/* ValidationLevel.BUSINESS */);
|
||
|
},
|
||
|
{ file: fileName }
|
||
|
);
|
||
|
|
||
|
// Most BR-*.xml files are designed to fail specific business rules
|
||
|
if (shouldFail && !validation.valid) {
|
||
|
results.passed++;
|
||
|
console.log(`✓ ${fileName}: Correctly failed validation`);
|
||
|
|
||
|
// Check that the correct BR code is in the errors
|
||
|
const brCode = fileName.match(/BR-\d+/)?.[0];
|
||
|
if (brCode && validation.errors) {
|
||
|
const hasCorrectError = validation.errors.some(e => e.code && e.code.includes(brCode));
|
||
|
if (!hasCorrectError) {
|
||
|
console.log(` ⚠ Expected error code ${brCode} not found`);
|
||
|
}
|
||
|
}
|
||
|
} else if (!shouldFail && validation.valid) {
|
||
|
results.passed++;
|
||
|
console.log(`✓ ${fileName}: Correctly passed validation`);
|
||
|
} else {
|
||
|
results.failed++;
|
||
|
results.errors.push(`${fileName}: Unexpected result - valid: ${validation.valid}`);
|
||
|
console.log(`✗ ${fileName}: Unexpected validation result`);
|
||
|
if (validation.errors && validation.errors.length > 0) {
|
||
|
console.log(` Errors: ${validation.errors.map(e => `${e.code}: ${e.message}`).join('; ')}`);
|
||
|
}
|
||
|
}
|
||
|
} catch (error) {
|
||
|
results.failed++;
|
||
|
results.errors.push(`${fileName}: ${error.message}`);
|
||
|
console.log(`✗ ${fileName}: Error - ${error.message}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
console.log(`\nBusiness Rules Summary: ${results.passed} passed, ${results.failed} failed`);
|
||
|
if (results.errors.length > 0) {
|
||
|
console.log('Sample failures:', results.errors.slice(0, 3));
|
||
|
}
|
||
|
|
||
|
// Performance summary
|
||
|
const perfSummary = await PerformanceTracker.getSummary('br-validation');
|
||
|
if (perfSummary) {
|
||
|
console.log(`\nBusiness Rule 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`);
|
||
|
}
|
||
|
|
||
|
// Allow some failures as not all validators may be implemented
|
||
|
expect(results.passed).toBeGreaterThan(0);
|
||
|
});
|
||
|
|
||
|
tap.test('VAL-02: Specific Business Rule Tests - should test common BR violations', async () => {
|
||
|
const { EInvoice } = await import('../../../ts/index.js');
|
||
|
|
||
|
const brTestCases = [
|
||
|
{
|
||
|
name: 'BR-02: Invoice ID must be present',
|
||
|
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">
|
||
|
<!-- Missing ID element -->
|
||
|
<cbc:IssueDate>2024-01-01</cbc:IssueDate>
|
||
|
</Invoice>`,
|
||
|
shouldFail: true,
|
||
|
expectedCode: 'BR-02'
|
||
|
},
|
||
|
{
|
||
|
name: 'BR-04: Invoice currency must be present',
|
||
|
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:IssueDate>2024-01-01</cbc:IssueDate>
|
||
|
<!-- Missing DocumentCurrencyCode -->
|
||
|
</Invoice>`,
|
||
|
shouldFail: true,
|
||
|
expectedCode: 'BR-04'
|
||
|
},
|
||
|
{
|
||
|
name: 'Valid minimal invoice',
|
||
|
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:IssueDate>2024-01-01</cbc:IssueDate>
|
||
|
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
||
|
</Invoice>`,
|
||
|
shouldFail: false,
|
||
|
expectedCode: null
|
||
|
}
|
||
|
];
|
||
|
|
||
|
for (const testCase of brTestCases) {
|
||
|
try {
|
||
|
const { result: validation } = await PerformanceTracker.track(
|
||
|
'br-test-case-validation',
|
||
|
async () => {
|
||
|
const einvoice = await EInvoice.fromXml(testCase.xml);
|
||
|
return await einvoice.validate();
|
||
|
}
|
||
|
);
|
||
|
|
||
|
console.log(`${testCase.name}: ${validation.valid ? 'VALID' : 'INVALID'}`);
|
||
|
|
||
|
if (testCase.shouldFail) {
|
||
|
expect(validation.valid).toEqual(false);
|
||
|
if (testCase.expectedCode && validation.errors) {
|
||
|
const hasExpectedError = validation.errors.some(e =>
|
||
|
e.code && e.code.includes(testCase.expectedCode)
|
||
|
);
|
||
|
// Note: This may not pass until business rule validation is fully implemented
|
||
|
if (!hasExpectedError) {
|
||
|
console.log(` Note: Expected error code ${testCase.expectedCode} not found (may not be implemented)`);
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
// Note: This may fail until validation is fully implemented
|
||
|
console.log(` Valid invoice: ${validation.valid ? 'correctly passed' : 'failed validation'}`);
|
||
|
}
|
||
|
|
||
|
} catch (error) {
|
||
|
console.log(`${testCase.name}: Error - ${error.message}`);
|
||
|
if (testCase.shouldFail) {
|
||
|
// Error is expected for invalid invoices
|
||
|
console.log(` ✓ Error expected for invalid invoice`);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
tap.test('VAL-02: Business Rule Categories - should test different BR categories', async () => {
|
||
|
const { EInvoice } = await import('../../../ts/index.js');
|
||
|
|
||
|
// Get files for different BR categories
|
||
|
const brFiles = await CorpusLoader.getFiles('EN16931_UBL_INVOICE');
|
||
|
|
||
|
const categories = {
|
||
|
'BR-CO': brFiles.filter(f => path.basename(f).startsWith('BR-CO')), // Calculation rules
|
||
|
'BR-CL': brFiles.filter(f => path.basename(f).startsWith('BR-CL')), // Codelist rules
|
||
|
'BR-E': brFiles.filter(f => path.basename(f).startsWith('BR-E')), // Extension rules
|
||
|
'BR-S': brFiles.filter(f => path.basename(f).startsWith('BR-S')), // Seller rules
|
||
|
'BR-G': brFiles.filter(f => path.basename(f).startsWith('BR-G')) // Group rules
|
||
|
};
|
||
|
|
||
|
for (const [category, files] of Object.entries(categories)) {
|
||
|
if (files.length === 0) continue;
|
||
|
|
||
|
console.log(`\nTesting ${category} rules (${files.length} files)`);
|
||
|
|
||
|
let categoryPassed = 0;
|
||
|
let categoryFailed = 0;
|
||
|
|
||
|
for (const filePath of files.slice(0, 3)) { // Test first 3 per category
|
||
|
const fileName = path.basename(filePath);
|
||
|
|
||
|
try {
|
||
|
const xmlContent = await fs.readFile(filePath, 'utf-8');
|
||
|
const einvoice = await EInvoice.fromXml(xmlContent);
|
||
|
|
||
|
const { result: validation } = await PerformanceTracker.track(
|
||
|
`${category.toLowerCase()}-validation`,
|
||
|
async () => await einvoice.validate()
|
||
|
);
|
||
|
|
||
|
if (!validation.valid) {
|
||
|
categoryPassed++; // Expected for BR test files
|
||
|
console.log(` ✓ ${fileName}: Correctly identified violation`);
|
||
|
} else {
|
||
|
categoryFailed++;
|
||
|
console.log(` ○ ${fileName}: No violation detected (may need implementation)`);
|
||
|
}
|
||
|
|
||
|
} catch (error) {
|
||
|
console.log(` ✗ ${fileName}: Error - ${error.message}`);
|
||
|
categoryFailed++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
console.log(` Summary: ${categoryPassed} correctly identified, ${categoryFailed} missed/errored`);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
tap.start();
|