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();