import { tap, expect } from '@git.zone/tstest/tapbundle'; import { EInvoice, EInvoiceFormatError } from '../ts/index.js'; import { InvoiceFormat } from '../ts/interfaces/common.js'; import { TestFileHelpers, TestFileCategories, PerformanceUtils, TestInvoiceFactory } from './helpers/utils.js'; import * as path from 'path'; /** * Cross-format conversion test suite */ // Test conversion between CII and UBL using paired files tap.test('Conversion - CII to UBL using XML-Rechnung pairs', async () => { // Get matching CII and UBL files const ciiFiles = await TestFileHelpers.getTestFiles(TestFileCategories.CII_XMLRECHNUNG, '*.xml'); const ublFiles = await TestFileHelpers.getTestFiles(TestFileCategories.UBL_XMLRECHNUNG, '*.xml'); // 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`); 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 TestFileHelpers.loadTestFile(pair.cii); const ciiInvoice = await EInvoice.fromXml(ciiBuffer.toString('utf-8')); // Convert to UBL const { result: ublXml, duration } = await PerformanceUtils.measure( 'cii-to-ubl', async () => ciiInvoice.exportXml('ubl') ); expect(ublXml).toBeTruthy(); expect(ublXml).toInclude('xmlns:cbc='); expect(ublXml).toInclude('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 (${duration.toFixed(2)}ms)`); } catch (error) { const issue = `${pair.name}: ${error.message}`; conversionIssues.push(issue); console.log(`✗ ${issue}`); } } console.log(`\nConversion Summary: ${successCount}/${pairs.length} successful`); if (conversionIssues.length > 0) { console.log('Issues:', conversionIssues); } }); // Test conversion from UBL to CII tap.test('Conversion - UBL to CII reverse conversion', async () => { const ublFiles = await TestFileHelpers.getTestFiles(TestFileCategories.UBL_XMLRECHNUNG, '*.xml'); console.log(`Testing UBL to CII conversion with ${ublFiles.length} files`); for (const file of ublFiles.slice(0, 3)) { const fileName = path.basename(file); try { const ublBuffer = await TestFileHelpers.loadTestFile(file); const ublInvoice = await EInvoice.fromXml(ublBuffer.toString('utf-8')); // Skip if detected as XRechnung (might have special requirements) if (ublInvoice.getFormat() === InvoiceFormat.XRECHNUNG) { console.log(`○ ${fileName}: Skipping XRechnung-specific file`); continue; } // Convert to CII (Factur-X) const ciiXml = await ublInvoice.exportXml('facturx'); expect(ciiXml).toBeTruthy(); expect(ciiXml).toInclude('CrossIndustryInvoice'); expect(ciiXml).toInclude('ExchangedDocument'); // Verify round-trip const ciiInvoice = await EInvoice.fromXml(ciiXml); expect(ciiInvoice.invoiceId).toEqual(ublInvoice.invoiceId); console.log(`✓ ${fileName}: UBL→CII conversion successful`); } catch (error) { if (error instanceof EInvoiceFormatError) { console.log(`✗ ${fileName}: Format error - ${error.message}`); if (error.unsupportedFeatures) { console.log(` Unsupported features: ${error.unsupportedFeatures.join(', ')}`); } } else { console.log(`✗ ${fileName}: ${error.message}`); } } } }); // Test ZUGFeRD to XRechnung conversion tap.test('Conversion - ZUGFeRD to XRechnung format', async () => { const zugferdPdfs = await TestFileHelpers.getTestFiles(TestFileCategories.ZUGFERD_V2_CORRECT, '*.pdf'); let tested = 0; for (const file of zugferdPdfs.slice(0, 3)) { const fileName = path.basename(file); try { // Extract from PDF const pdfBuffer = await TestFileHelpers.loadTestFile(file); const zugferdInvoice = await EInvoice.fromPdf(pdfBuffer); // Convert to XRechnung const xrechnungXml = await zugferdInvoice.exportXml('xrechnung'); expect(xrechnungXml).toBeTruthy(); // XRechnung should be UBL format with specific extensions if (xrechnungXml.includes('Invoice xmlns')) { expect(xrechnungXml).toInclude('CustomizationID'); expect(xrechnungXml).toInclude('urn:cen.eu:en16931'); } tested++; console.log(`✓ ${fileName}: ZUGFeRD→XRechnung conversion successful`); } catch (error) { console.log(`○ ${fileName}: Conversion not available - ${error.message}`); } } if (tested === 0) { console.log('Note: ZUGFeRD to XRechnung conversion may need implementation'); } }); // Test data loss detection during conversion tap.test('Conversion - Data loss detection and reporting', async () => { // Create a complex invoice with all possible fields const complexInvoice = new EInvoice(); Object.assign(complexInvoice, TestInvoiceFactory.createComplexInvoice()); // Add format-specific fields complexInvoice.buyerReference = 'PO-2024-12345'; complexInvoice.electronicAddress = { scheme: '0088', value: '1234567890123' }; complexInvoice.notes = [ 'Special handling required', 'Express delivery requested', 'Contact buyer before delivery' ]; // Generate source XML const sourceXml = await complexInvoice.exportXml('facturx'); await complexInvoice.loadXml(sourceXml); // Test conversions and check for data loss const formats: Array<{from: string, to: string}> = [ { from: 'facturx', to: 'ubl' }, { from: 'facturx', to: 'xrechnung' }, { from: 'facturx', to: 'zugferd' } ]; for (const conversion of formats) { console.log(`\nTesting ${conversion.from} → ${conversion.to} conversion:`); try { const convertedXml = await complexInvoice.exportXml(conversion.to as any); const convertedInvoice = await EInvoice.fromXml(convertedXml); // Check for data preservation const issues = checkDataPreservation(complexInvoice, convertedInvoice); if (issues.length === 0) { console.log(`✓ All data preserved in ${conversion.to} format`); } else { console.log(`⚠ Data loss detected in ${conversion.to} format:`); issues.forEach(issue => console.log(` - ${issue}`)); } } catch (error) { console.log(`✗ Conversion failed: ${error.message}`); } } }); // Test edge cases in conversion tap.test('Conversion - Edge cases and special characters', async () => { const edgeCaseInvoice = new EInvoice(); Object.assign(edgeCaseInvoice, TestInvoiceFactory.createMinimalInvoice()); // Add edge case data edgeCaseInvoice.from.name = 'Müller & Söhne GmbH & Co. KG'; edgeCaseInvoice.to.name = 'L\'Entreprise Française S.à.r.l.'; edgeCaseInvoice.items[0].name = 'Product with "quotes" and '; edgeCaseInvoice.notes = ['Note with € symbol', 'Japanese: こんにちは']; // Test conversion with special characters const formats = ['facturx', 'ubl', 'xrechnung'] as const; for (const format of formats) { try { const xml = await edgeCaseInvoice.exportXml(format); // Verify special characters are properly encoded expect(xml).toInclude('Müller'); expect(xml).toInclude('Française'); expect(xml).toContain('"'); // Encoded quotes expect(xml).toContain('<'); // Encoded less-than // Verify it can be parsed back const parsed = await EInvoice.fromXml(xml); expect(parsed.from.name).toEqual('Müller & Söhne GmbH & Co. KG'); console.log(`✓ ${format}: Special characters handled correctly`); } catch (error) { console.log(`✗ ${format}: Failed with special characters - ${error.message}`); } } }); // Test batch conversion performance tap.test('Conversion - Batch conversion performance', async () => { const files = await TestFileHelpers.getTestFiles(TestFileCategories.UBL_XMLRECHNUNG, '*.xml'); const batchSize = Math.min(10, files.length); console.log(`Testing batch conversion of ${batchSize} files`); const startTime = performance.now(); const results = await Promise.all( files.slice(0, batchSize).map(async (file) => { try { const buffer = await TestFileHelpers.loadTestFile(file); const invoice = await EInvoice.fromXml(buffer.toString('utf-8')); const converted = await invoice.exportXml('facturx'); return { success: true, size: converted.length }; } catch (error) { return { success: false, error: error.message }; } }) ); const duration = performance.now() - startTime; const successCount = results.filter(r => r.success).length; console.log(`✓ Batch conversion completed in ${duration.toFixed(2)}ms`); console.log(` Success rate: ${successCount}/${batchSize}`); console.log(` Average time per conversion: ${(duration / batchSize).toFixed(2)}ms`); expect(duration / batchSize).toBeLessThan(500); // Should be under 500ms per conversion }); // Test format-specific extensions preservation tap.test('Conversion - Format-specific extensions', async () => { // This tests that format-specific extensions don't break conversion const extensionTests = [ { name: 'XRechnung BuyerReference', xml: ` urn:cen.eu:en16931:2017#compliant#urn:xrechnung:cius:2.0 123 04011000-12345-03 ` } ]; for (const test of extensionTests) { try { const invoice = await EInvoice.fromXml(test.xml); // Convert to CII const ciiXml = await invoice.exportXml('facturx'); expect(ciiXml).toBeTruthy(); // Convert back to UBL const ciiInvoice = await EInvoice.fromXml(ciiXml); const ublXml = await ciiInvoice.exportXml('ubl'); // Check if buyer reference survived if (invoice.buyerReference) { expect(ublXml).toInclude(invoice.buyerReference); } console.log(`✓ ${test.name}: Extension preserved through conversion`); } catch (error) { console.log(`✗ ${test.name}: ${error.message}`); } } }); // Test conversion error handling tap.test('Conversion - Error handling and recovery', async () => { // Test with minimal invalid invoice const invalidInvoice = new EInvoice(); invalidInvoice.id = 'TEST-INVALID'; // Missing required fields like from, to, items try { await invalidInvoice.exportXml('facturx'); throw new Error('Should have thrown an error for invalid invoice'); } catch (error) { console.log(`✓ Invalid invoice error caught: ${error.message}`); if (error instanceof EInvoiceFormatError) { console.log(` Compatibility report:\n${error.getCompatibilityReport()}`); } } }); // Performance summary for conversions tap.test('Conversion - Performance Summary', async () => { const conversionStats = PerformanceUtils.getStats('cii-to-ubl'); if (conversionStats) { console.log('\nConversion Performance:'); console.log(`CII to UBL conversions: ${conversionStats.count}`); console.log(`Average time: ${conversionStats.avg.toFixed(2)}ms`); console.log(`Min/Max: ${conversionStats.min.toFixed(2)}ms / ${conversionStats.max.toFixed(2)}ms`); // Conversions should be reasonably fast expect(conversionStats.avg).toBeLessThan(100); } }); // Helper function to verify field mapping function verifyFieldMapping(source: EInvoice, converted: EInvoice, testName: string): void { const criticalFields = [ { field: 'invoiceId', name: 'Invoice ID' }, { field: 'date', name: 'Invoice Date' }, { 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 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.buyerReference && source.buyerReference !== converted.buyerReference) { issues.push(`Buyer reference lost or changed`); } if (source.notes && source.notes.length !== converted.notes?.length) { issues.push(`Notes count changed: ${source.notes.length} → ${converted.notes?.length || 0}`); } if (source.electronicAddress && !converted.electronicAddress) { issues.push(`Electronic address lost`); } // Check payment details if (source.paymentOptions?.sepaConnection?.iban !== converted.paymentOptions?.sepaConnection?.iban) { issues.push(`IBAN changed or lost`); } return issues; } tap.start();