update
This commit is contained in:
436
test/suite/einvoice_conversion/test.conv-01.format-conversion.ts
Normal file
436
test/suite/einvoice_conversion/test.conv-01.format-conversion.ts
Normal file
@ -0,0 +1,436 @@
|
||||
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('CONV-01: Format Conversion - should convert between invoice formats', async () => {
|
||||
// Test conversion between CII and UBL using paired files
|
||||
const ciiFiles = await CorpusLoader.getFiles('CII_XMLRECHNUNG');
|
||||
const ublFiles = await CorpusLoader.getFiles('UBL_XMLRECHNUNG');
|
||||
|
||||
// Find paired files (same base name)
|
||||
const pairs: Array<{cii: string, ubl: string, name: string}> = [];
|
||||
|
||||
for (const ciiFile of ciiFiles) {
|
||||
const baseName = path.basename(ciiFile).replace('.cii.xml', '');
|
||||
const matchingUbl = ublFiles.find(ubl =>
|
||||
path.basename(ubl).startsWith(baseName) && ubl.endsWith('.ubl.xml')
|
||||
);
|
||||
|
||||
if (matchingUbl) {
|
||||
pairs.push({ cii: ciiFile, ubl: matchingUbl, name: baseName });
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Found ${pairs.length} CII/UBL pairs for conversion testing`);
|
||||
|
||||
const { EInvoice } = await import('../../../ts/index.js');
|
||||
|
||||
let successCount = 0;
|
||||
const conversionIssues: string[] = [];
|
||||
|
||||
for (const pair of pairs.slice(0, 5)) { // Test first 5 pairs
|
||||
try {
|
||||
// Load CII invoice
|
||||
const ciiBuffer = await fs.readFile(pair.cii, 'utf-8');
|
||||
const ciiInvoice = await EInvoice.fromXml(ciiBuffer);
|
||||
|
||||
// Convert to UBL
|
||||
const { result: ublXml, metric } = await PerformanceTracker.track(
|
||||
'cii-to-ubl-conversion',
|
||||
async () => ciiInvoice.exportXml('ubl' as any),
|
||||
{ file: pair.name }
|
||||
);
|
||||
|
||||
expect(ublXml).toBeTruthy();
|
||||
expect(ublXml).toContain('xmlns:cbc=');
|
||||
expect(ublXml).toContain('xmlns:cac=');
|
||||
|
||||
// Load the converted UBL back
|
||||
const convertedInvoice = await EInvoice.fromXml(ublXml);
|
||||
|
||||
// Verify key fields are preserved
|
||||
verifyFieldMapping(ciiInvoice, convertedInvoice, pair.name);
|
||||
|
||||
successCount++;
|
||||
console.log(`✓ ${pair.name}: CII→UBL conversion successful (${metric.duration.toFixed(2)}ms)`);
|
||||
|
||||
} catch (error) {
|
||||
const issue = `${pair.name}: ${error.message}`;
|
||||
conversionIssues.push(issue);
|
||||
console.log(`✗ ${issue}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nCII→UBL Conversion Summary: ${successCount}/${Math.min(pairs.length, 5)} successful`);
|
||||
if (conversionIssues.length > 0) {
|
||||
console.log('Issues:', conversionIssues.slice(0, 3));
|
||||
}
|
||||
|
||||
// Performance summary
|
||||
const perfSummary = await PerformanceTracker.getSummary('cii-to-ubl-conversion');
|
||||
if (perfSummary) {
|
||||
console.log(`\nCII→UBL Conversion Performance:`);
|
||||
console.log(` Average: ${perfSummary.average.toFixed(2)}ms`);
|
||||
console.log(` P95: ${perfSummary.p95.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
expect(successCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('CONV-01: UBL to CII Conversion - should convert UBL invoices to CII format', async () => {
|
||||
const { EInvoice } = await import('../../../ts/index.js');
|
||||
|
||||
const ublFiles = await CorpusLoader.getFiles('UBL_XMLRECHNUNG');
|
||||
const testFiles = ublFiles.filter(f => f.endsWith('.xml')).slice(0, 3);
|
||||
|
||||
console.log(`Testing UBL to CII conversion with ${testFiles.length} files`);
|
||||
|
||||
let successCount = 0;
|
||||
let skipCount = 0;
|
||||
|
||||
for (const filePath of testFiles) {
|
||||
const fileName = path.basename(filePath);
|
||||
|
||||
try {
|
||||
const ublContent = await fs.readFile(filePath, 'utf-8');
|
||||
const ublInvoice = await EInvoice.fromXml(ublContent);
|
||||
|
||||
// Skip if detected as XRechnung (might have special requirements)
|
||||
const format = ublInvoice.getFormat ? ublInvoice.getFormat() : 'unknown';
|
||||
if (format.toString().toLowerCase().includes('xrechnung')) {
|
||||
console.log(`○ ${fileName}: Skipping XRechnung-specific file`);
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert to CII (Factur-X)
|
||||
const { result: ciiXml, metric } = await PerformanceTracker.track(
|
||||
'ubl-to-cii-conversion',
|
||||
async () => ublInvoice.exportXml('facturx' as any),
|
||||
{ file: fileName }
|
||||
);
|
||||
|
||||
expect(ciiXml).toBeTruthy();
|
||||
expect(ciiXml).toContain('CrossIndustryInvoice');
|
||||
expect(ciiXml).toContain('ExchangedDocument');
|
||||
|
||||
// Verify round-trip
|
||||
const ciiInvoice = await EInvoice.fromXml(ciiXml);
|
||||
expect(ciiInvoice.invoiceId).toEqual(ublInvoice.invoiceId);
|
||||
|
||||
successCount++;
|
||||
console.log(`✓ ${fileName}: UBL→CII conversion successful (${metric.duration.toFixed(2)}ms)`);
|
||||
|
||||
} catch (error) {
|
||||
console.log(`✗ ${fileName}: Conversion failed - ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nUBL→CII Conversion Summary: ${successCount} successful, ${skipCount} skipped`);
|
||||
|
||||
// Performance summary
|
||||
const perfSummary = await PerformanceTracker.getSummary('ubl-to-cii-conversion');
|
||||
if (perfSummary) {
|
||||
console.log(`\nUBL→CII Conversion Performance:`);
|
||||
console.log(` Average: ${perfSummary.average.toFixed(2)}ms`);
|
||||
console.log(` P95: ${perfSummary.p95.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
expect(successCount + skipCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('CONV-01: ZUGFeRD to XRechnung Conversion - should convert ZUGFeRD PDFs to XRechnung', async () => {
|
||||
const { EInvoice } = await import('../../../ts/index.js');
|
||||
|
||||
const zugferdPdfs = await CorpusLoader.getFiles('ZUGFERD_V2_CORRECT');
|
||||
const pdfFiles = zugferdPdfs.filter(f => f.endsWith('.pdf')).slice(0, 3);
|
||||
|
||||
console.log(`Testing ZUGFeRD to XRechnung conversion with ${pdfFiles.length} PDFs`);
|
||||
|
||||
let tested = 0;
|
||||
let successful = 0;
|
||||
|
||||
for (const filePath of pdfFiles) {
|
||||
const fileName = path.basename(filePath);
|
||||
|
||||
try {
|
||||
// Extract from PDF
|
||||
const pdfBuffer = await fs.readFile(filePath);
|
||||
const zugferdInvoice = await EInvoice.fromPdf(pdfBuffer);
|
||||
|
||||
// Convert to XRechnung
|
||||
const { result: xrechnungXml, metric } = await PerformanceTracker.track(
|
||||
'zugferd-to-xrechnung-conversion',
|
||||
async () => zugferdInvoice.exportXml('xrechnung' as any),
|
||||
{ file: fileName }
|
||||
);
|
||||
|
||||
expect(xrechnungXml).toBeTruthy();
|
||||
|
||||
// XRechnung should be UBL format with specific extensions
|
||||
if (xrechnungXml.includes('Invoice xmlns')) {
|
||||
expect(xrechnungXml).toContain('CustomizationID');
|
||||
expect(xrechnungXml).toContain('urn:cen.eu:en16931');
|
||||
}
|
||||
|
||||
tested++;
|
||||
successful++;
|
||||
console.log(`✓ ${fileName}: ZUGFeRD→XRechnung conversion successful (${metric.duration.toFixed(2)}ms)`);
|
||||
|
||||
} catch (error) {
|
||||
tested++;
|
||||
console.log(`○ ${fileName}: Conversion not available - ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nZUGFeRD→XRechnung Conversion Summary: ${successful}/${tested} successful`);
|
||||
|
||||
if (successful === 0 && tested > 0) {
|
||||
console.log('Note: ZUGFeRD to XRechnung conversion may need implementation');
|
||||
}
|
||||
|
||||
// Performance summary
|
||||
const perfSummary = await PerformanceTracker.getSummary('zugferd-to-xrechnung-conversion');
|
||||
if (perfSummary) {
|
||||
console.log(`\nZUGFeRD→XRechnung Conversion Performance:`);
|
||||
console.log(` Average: ${perfSummary.average.toFixed(2)}ms`);
|
||||
console.log(` P95: ${perfSummary.p95.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
expect(tested).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('CONV-01: Data Preservation During Conversion - should preserve invoice data across formats', async () => {
|
||||
const { EInvoice } = await import('../../../ts/index.js');
|
||||
|
||||
// Create a test invoice with comprehensive data
|
||||
const testInvoice = new EInvoice();
|
||||
testInvoice.id = 'DATA-PRESERVATION-TEST';
|
||||
testInvoice.invoiceId = 'INV-2024-001';
|
||||
testInvoice.date = Date.now();
|
||||
testInvoice.currency = 'EUR';
|
||||
|
||||
testInvoice.from = {
|
||||
name: 'Test Seller GmbH',
|
||||
type: 'company',
|
||||
description: 'Test seller company',
|
||||
address: {
|
||||
streetName: 'Musterstraße',
|
||||
houseNumber: '123',
|
||||
city: 'Berlin',
|
||||
country: 'Germany',
|
||||
postalCode: '10115'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE123456789',
|
||||
registrationId: 'HRB 12345',
|
||||
registrationName: 'Handelsregister Berlin'
|
||||
}
|
||||
};
|
||||
|
||||
testInvoice.to = {
|
||||
name: 'Test Buyer Ltd',
|
||||
type: 'company',
|
||||
description: 'Test buyer company',
|
||||
address: {
|
||||
streetName: 'Example Street',
|
||||
houseNumber: '456',
|
||||
city: 'London',
|
||||
country: 'United Kingdom',
|
||||
postalCode: 'SW1A 1AA'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2019, month: 6, day: 15 },
|
||||
registrationDetails: {
|
||||
vatId: 'GB987654321',
|
||||
registrationId: 'Companies House 87654321',
|
||||
registrationName: 'Companies House'
|
||||
}
|
||||
};
|
||||
|
||||
testInvoice.items = [
|
||||
{
|
||||
position: 1,
|
||||
name: 'Professional Service',
|
||||
articleNumber: 'SERV-001',
|
||||
unitType: 'HUR',
|
||||
unitQuantity: 8,
|
||||
unitNetPrice: 150,
|
||||
vatPercentage: 19
|
||||
},
|
||||
{
|
||||
position: 2,
|
||||
name: 'Software License',
|
||||
articleNumber: 'SOFT-001',
|
||||
unitType: 'EA',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 500,
|
||||
vatPercentage: 19
|
||||
}
|
||||
];
|
||||
|
||||
// Test conversions and check for data preservation
|
||||
const conversions: Array<{from: string, to: string}> = [
|
||||
{ from: 'facturx', to: 'ubl' },
|
||||
{ from: 'facturx', to: 'xrechnung' }
|
||||
];
|
||||
|
||||
for (const conversion of conversions) {
|
||||
console.log(`\nTesting ${conversion.from} → ${conversion.to} data preservation:`);
|
||||
|
||||
try {
|
||||
// Generate source XML
|
||||
const sourceXml = await testInvoice.exportXml(conversion.from as any);
|
||||
await testInvoice.loadXml(sourceXml);
|
||||
|
||||
// Convert to target format
|
||||
const { result: convertedXml, metric } = await PerformanceTracker.track(
|
||||
'data-preservation-conversion',
|
||||
async () => testInvoice.exportXml(conversion.to as any),
|
||||
{ conversion: `${conversion.from}-to-${conversion.to}` }
|
||||
);
|
||||
|
||||
const convertedInvoice = await EInvoice.fromXml(convertedXml);
|
||||
|
||||
// Check for data preservation
|
||||
const issues = checkDataPreservation(testInvoice, convertedInvoice);
|
||||
|
||||
if (issues.length === 0) {
|
||||
console.log(`✓ All critical data preserved (${metric.duration.toFixed(2)}ms)`);
|
||||
} else {
|
||||
console.log(`⚠ Data preservation issues found:`);
|
||||
issues.forEach(issue => console.log(` - ${issue}`));
|
||||
}
|
||||
|
||||
// Core fields should always be preserved
|
||||
expect(convertedInvoice.invoiceId).toEqual(testInvoice.invoiceId);
|
||||
expect(convertedInvoice.from.name).toEqual(testInvoice.from.name);
|
||||
expect(convertedInvoice.to.name).toEqual(testInvoice.to.name);
|
||||
|
||||
} catch (error) {
|
||||
console.log(`✗ Conversion failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CONV-01: Conversion Performance Benchmarks - should meet conversion performance targets', async () => {
|
||||
console.log('\nConversion Performance Benchmark Summary:');
|
||||
|
||||
const conversionOperations = [
|
||||
'cii-to-ubl-conversion',
|
||||
'ubl-to-cii-conversion',
|
||||
'zugferd-to-xrechnung-conversion'
|
||||
];
|
||||
|
||||
const benchmarkResults: { operation: string; metrics: any }[] = [];
|
||||
|
||||
for (const operation of conversionOperations) {
|
||||
const summary = await PerformanceTracker.getSummary(operation);
|
||||
if (summary) {
|
||||
benchmarkResults.push({ operation, metrics: summary });
|
||||
console.log(`\n${operation}:`);
|
||||
console.log(` Average: ${summary.average.toFixed(2)}ms`);
|
||||
console.log(` P95: ${summary.p95.toFixed(2)}ms`);
|
||||
console.log(` Count: ${summary.min !== undefined ? 'Available' : 'No data'}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (benchmarkResults.length > 0) {
|
||||
const overallAverage = benchmarkResults.reduce((sum, result) =>
|
||||
sum + result.metrics.average, 0) / benchmarkResults.length;
|
||||
|
||||
console.log(`\nOverall Conversion Performance:`);
|
||||
console.log(` Average across operations: ${overallAverage.toFixed(2)}ms`);
|
||||
|
||||
// Performance targets
|
||||
expect(overallAverage).toBeLessThan(1000); // Conversions should be under 1 second on average
|
||||
|
||||
benchmarkResults.forEach(result => {
|
||||
expect(result.metrics.p95).toBeLessThan(2000); // P95 should be under 2 seconds
|
||||
});
|
||||
|
||||
console.log(`✓ All conversion performance benchmarks met`);
|
||||
} else {
|
||||
console.log('No conversion performance data available');
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function to verify field mapping between invoices
|
||||
function verifyFieldMapping(source: EInvoice, converted: EInvoice, testName: string): void {
|
||||
const criticalFields = [
|
||||
{ field: 'invoiceId', name: 'Invoice ID' },
|
||||
{ field: 'currency', name: 'Currency' }
|
||||
];
|
||||
|
||||
for (const check of criticalFields) {
|
||||
const sourceVal = source[check.field as keyof EInvoice];
|
||||
const convertedVal = converted[check.field as keyof EInvoice];
|
||||
|
||||
if (sourceVal !== convertedVal) {
|
||||
console.log(` ⚠ ${check.name} mismatch: ${sourceVal} → ${convertedVal}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check seller/buyer names
|
||||
if (source.from?.name !== converted.from?.name) {
|
||||
console.log(` ⚠ Seller name mismatch: ${source.from?.name} → ${converted.from?.name}`);
|
||||
}
|
||||
|
||||
if (source.to?.name !== converted.to?.name) {
|
||||
console.log(` ⚠ Buyer name mismatch: ${source.to?.name} → ${converted.to?.name}`);
|
||||
}
|
||||
|
||||
// Check items count
|
||||
if (source.items?.length !== converted.items?.length) {
|
||||
console.log(` ⚠ Items count mismatch: ${source.items?.length} → ${converted.items?.length}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check data preservation
|
||||
function checkDataPreservation(source: EInvoice, converted: EInvoice): string[] {
|
||||
const issues: string[] = [];
|
||||
|
||||
// Check basic fields
|
||||
if (source.invoiceId !== converted.invoiceId) {
|
||||
issues.push(`Invoice ID changed: ${source.invoiceId} → ${converted.invoiceId}`);
|
||||
}
|
||||
|
||||
if (source.currency !== converted.currency) {
|
||||
issues.push(`Currency changed: ${source.currency} → ${converted.currency}`);
|
||||
}
|
||||
|
||||
// Check party information
|
||||
if (source.from?.name !== converted.from?.name) {
|
||||
issues.push(`Seller name changed: ${source.from?.name} → ${converted.from?.name}`);
|
||||
}
|
||||
|
||||
if (source.to?.name !== converted.to?.name) {
|
||||
issues.push(`Buyer name changed: ${source.to?.name} → ${converted.to?.name}`);
|
||||
}
|
||||
|
||||
// Check items
|
||||
if (source.items?.length !== converted.items?.length) {
|
||||
issues.push(`Items count changed: ${source.items?.length} → ${converted.items?.length}`);
|
||||
} else if (source.items && converted.items) {
|
||||
for (let i = 0; i < source.items.length; i++) {
|
||||
const sourceItem = source.items[i];
|
||||
const convertedItem = converted.items[i];
|
||||
|
||||
if (sourceItem.name !== convertedItem.name) {
|
||||
issues.push(`Item ${i+1} name changed: ${sourceItem.name} → ${convertedItem.name}`);
|
||||
}
|
||||
|
||||
if (sourceItem.unitNetPrice !== convertedItem.unitNetPrice) {
|
||||
issues.push(`Item ${i+1} price changed: ${sourceItem.unitNetPrice} → ${convertedItem.unitNetPrice}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
tap.start();
|
Reference in New Issue
Block a user