einvoice/test/suite/einvoice_format-detection/test.fd-01.ubl-detection.ts

217 lines
8.7 KiB
TypeScript
Raw Normal View History

import { tap, expect } from '@git.zone/tstest/tapbundle';
2025-05-25 19:45:37 +00:00
import { EInvoice } from '../../../ts/index.js';
import { InvoiceFormat } from '../../../ts/interfaces/common.js';
import { FormatDetector } from '../../../ts/formats/utils/format.detector.js';
import { CorpusLoader, PerformanceTracker } from '../../helpers/test-utils.js';
/**
* Test ID: FD-01
* Test Description: UBL Format Detection
* Priority: High
*
* This test validates the accurate detection of UBL (Universal Business Language) format
* from XML invoice files across different UBL versions and implementations.
*/
tap.test('FD-01: UBL Format Detection - Corpus files', async (t) => {
// Load UBL test files from corpus
const ublFiles = await CorpusLoader.loadCategory('UBL_XMLRECHNUNG');
const peppolFiles = await CorpusLoader.loadCategory('PEPPOL');
const en16931UblFiles = await CorpusLoader.loadCategory('EN16931_UBL_EXAMPLES');
const allUblFiles = [...ublFiles, ...peppolFiles, ...en16931UblFiles];
console.log(`Testing ${allUblFiles.length} UBL files for format detection`);
let successCount = 0;
let failureCount = 0;
const detectionTimes: number[] = [];
for (const file of allUblFiles) {
try {
const xmlBuffer = await CorpusLoader.loadFile(file.path);
const xmlString = xmlBuffer.toString('utf-8');
// Track performance
const { result: detectedFormat, metric } = await PerformanceTracker.track(
'format-detection',
async () => FormatDetector.detectFormat(xmlString),
{ file: file.path, size: file.size }
);
detectionTimes.push(metric.duration);
// UBL files can be detected as UBL or XRechnung (which is UBL-based)
const validFormats = [InvoiceFormat.UBL, InvoiceFormat.XRECHNUNG];
if (validFormats.includes(detectedFormat)) {
successCount++;
t.pass(`${path.basename(file.path)}: Correctly detected as ${detectedFormat}`);
} else {
failureCount++;
t.fail(`${path.basename(file.path)}: Detected as ${detectedFormat}, expected UBL or XRechnung`);
}
} catch (error) {
failureCount++;
t.fail(`${path.basename(file.path)}: Detection failed - ${error.message}`);
}
}
// Calculate statistics
const avgTime = detectionTimes.length > 0
? detectionTimes.reduce((a, b) => a + b, 0) / detectionTimes.length
: 0;
console.log(`\nUBL Detection Summary:`);
console.log(`- Files tested: ${allUblFiles.length}`);
console.log(`- Successful detections: ${successCount} (${(successCount / allUblFiles.length * 100).toFixed(1)}%)`);
console.log(`- Failed detections: ${failureCount}`);
console.log(`- Average detection time: ${avgTime.toFixed(2)}ms`);
// Performance assertion
t.ok(avgTime < 10, 'Average detection time should be under 10ms');
// Success rate assertion (allow some flexibility for edge cases)
const successRate = successCount / allUblFiles.length;
t.ok(successRate > 0.9, 'Success rate should be above 90%');
});
tap.test('FD-01: UBL Format Detection - Specific UBL elements', async (t) => {
// Test specific UBL invoice
const ublInvoice = `<?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:ID>INV-001</cbc:ID>
<cbc:IssueDate>2024-01-01</cbc:IssueDate>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Test Supplier</cbc:Name>
</cac:PartyName>
</cac:Party>
</cac:AccountingSupplierParty>
</Invoice>`;
const format = FormatDetector.detectFormat(ublInvoice);
t.equal(format, InvoiceFormat.UBL, 'Should detect standard UBL invoice');
// Test UBL credit note
const ublCreditNote = `<?xml version="1.0" encoding="UTF-8"?>
<CreditNote xmlns="urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ID>CN-001</cbc:ID>
<cbc:IssueDate>2024-01-01</cbc:IssueDate>
</CreditNote>`;
const creditNoteFormat = FormatDetector.detectFormat(ublCreditNote);
t.equal(creditNoteFormat, InvoiceFormat.UBL, 'Should detect UBL credit note');
});
tap.test('FD-01: UBL Format Detection - PEPPOL BIS', async (t) => {
// Test PEPPOL BIS 3.0 (which is UBL-based)
const peppolInvoice = `<?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#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0</cbc:CustomizationID>
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
<cbc:ID>Peppol-001</cbc:ID>
</Invoice>`;
const format = FormatDetector.detectFormat(peppolInvoice);
t.ok(
[InvoiceFormat.UBL, InvoiceFormat.XRECHNUNG].includes(format),
'Should detect PEPPOL BIS as UBL or specialized format'
);
});
tap.test('FD-01: UBL Format Detection - Edge cases', async (t) => {
// Test with minimal UBL
const minimalUBL = '<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"></Invoice>';
const minimalFormat = FormatDetector.detectFormat(minimalUBL);
t.equal(minimalFormat, InvoiceFormat.UBL, 'Should detect minimal UBL invoice');
// Test with different namespace prefix
const differentPrefix = `<?xml version="1.0"?>
<ubl:Invoice xmlns:ubl="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ubl:ID>123</ubl:ID>
</ubl:Invoice>`;
const prefixFormat = FormatDetector.detectFormat(differentPrefix);
t.equal(prefixFormat, InvoiceFormat.UBL, 'Should detect UBL with different namespace prefix');
// Test without XML declaration
const noDeclaration = `<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<cbc:ID xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">456</cbc:ID>
</Invoice>`;
const noDecFormat = FormatDetector.detectFormat(noDeclaration);
t.equal(noDecFormat, InvoiceFormat.UBL, 'Should detect UBL without XML declaration');
});
tap.test('FD-01: UBL Format Detection - Performance benchmarks', async (t) => {
// Test detection speed with various file sizes
const testCases = [
{ name: 'Small UBL', size: 1000, content: generateUBLInvoice(5) },
{ name: 'Medium UBL', size: 10000, content: generateUBLInvoice(50) },
{ name: 'Large UBL', size: 100000, content: generateUBLInvoice(500) }
];
for (const testCase of testCases) {
const times: number[] = [];
// Run multiple iterations for accuracy
for (let i = 0; i < 100; i++) {
const start = performance.now();
FormatDetector.detectFormat(testCase.content);
times.push(performance.now() - start);
}
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
console.log(`${testCase.name} (${testCase.content.length} bytes): avg ${avgTime.toFixed(3)}ms`);
t.ok(avgTime < 5, `${testCase.name} detection should be under 5ms`);
}
});
// Helper function to generate UBL invoice with specified number of line items
function generateUBLInvoice(lineItems: number): string {
let invoice = `<?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:ID>TEST-${Date.now()}</cbc:ID>
<cbc:IssueDate>2024-01-01</cbc:IssueDate>`;
for (let i = 1; i <= lineItems; i++) {
invoice += `
<cac:InvoiceLine>
<cbc:ID>${i}</cbc:ID>
<cbc:InvoicedQuantity unitCode="EA">${i}</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">${i * 100}</cbc:LineExtensionAmount>
</cac:InvoiceLine>`;
}
invoice += '\n</Invoice>';
return invoice;
}
// Generate performance report at the end
tap.teardown(async () => {
const stats = PerformanceTracker.getStats('format-detection');
if (stats) {
console.log('\nPerformance Summary:');
console.log(`- Total detections: ${stats.count}`);
console.log(`- Average time: ${stats.avg.toFixed(2)}ms`);
console.log(`- Min/Max: ${stats.min.toFixed(2)}ms / ${stats.max.toFixed(2)}ms`);
console.log(`- P95: ${stats.p95.toFixed(2)}ms`);
}
});
// Import path for basename
import * as path from 'path';
tap.start();