/** * @file test.conv-09.round-trip.ts * @description Tests for round-trip conversion integrity between formats */ import { tap } from '@git.zone/tstest/tapbundle'; import * as plugins from '../../plugins.js'; import { EInvoice } from '../../../ts/index.js'; import { CorpusLoader } from '../../suite/corpus.loader.js'; import { PerformanceTracker } from '../../suite/performance.tracker.js'; const corpusLoader = new CorpusLoader(); const performanceTracker = new PerformanceTracker('CONV-09: Round-Trip Conversion'); tap.test('CONV-09: Round-Trip Conversion - should maintain data integrity through round-trip conversions', async (t) => { // Test 1: UBL -> CII -> UBL round-trip const ublRoundTrip = await performanceTracker.measureAsync( 'ubl-cii-ubl-round-trip', async () => { const einvoice = new EInvoice(); // Create comprehensive UBL invoice const originalUBL = { format: 'ubl' as const, data: { documentType: 'INVOICE', invoiceNumber: 'UBL-RT-2024-001', issueDate: '2024-01-20', dueDate: '2024-02-20', currency: 'EUR', seller: { name: 'UBL Test Seller GmbH', address: 'Seller Street 123', city: 'Berlin', postalCode: '10115', country: 'DE', taxId: 'DE123456789', email: 'seller@example.com', phone: '+49 30 12345678' }, buyer: { name: 'UBL Test Buyer Ltd', address: 'Buyer Avenue 456', city: 'Munich', postalCode: '80331', country: 'DE', taxId: 'DE987654321', email: 'buyer@example.com' }, items: [ { description: 'Professional Services', quantity: 10, unitPrice: 150.00, vatRate: 19, lineTotal: 1500.00, itemId: 'SRV-001' }, { description: 'Software License', quantity: 5, unitPrice: 200.00, vatRate: 19, lineTotal: 1000.00, itemId: 'LIC-001' } ], totals: { netAmount: 2500.00, vatAmount: 475.00, grossAmount: 2975.00 }, paymentTerms: 'Net 30 days', notes: 'Thank you for your business!' } }; // Convert UBL -> CII const convertedToCII = await einvoice.convertFormat(originalUBL, 'cii'); // Convert CII -> UBL const backToUBL = await einvoice.convertFormat(convertedToCII, 'ubl'); // Compare key fields const comparison = { invoiceNumber: originalUBL.data.invoiceNumber === backToUBL.data.invoiceNumber, issueDate: originalUBL.data.issueDate === backToUBL.data.issueDate, sellerName: originalUBL.data.seller.name === backToUBL.data.seller.name, sellerTaxId: originalUBL.data.seller.taxId === backToUBL.data.seller.taxId, buyerName: originalUBL.data.buyer.name === backToUBL.data.buyer.name, itemCount: originalUBL.data.items.length === backToUBL.data.items.length, totalAmount: originalUBL.data.totals.grossAmount === backToUBL.data.totals.grossAmount, allFieldsMatch: JSON.stringify(originalUBL.data) === JSON.stringify(backToUBL.data) }; return { comparison, dataDifferences: !comparison.allFieldsMatch }; } ); // Test 2: CII -> UBL -> CII round-trip const ciiRoundTrip = await performanceTracker.measureAsync( 'cii-ubl-cii-round-trip', async () => { const einvoice = new EInvoice(); // Create CII invoice const originalCII = { format: 'cii' as const, data: { documentType: 'INVOICE', invoiceNumber: 'CII-RT-2024-001', issueDate: '2024-01-21', dueDate: '2024-02-21', currency: 'USD', seller: { name: 'CII Corporation', address: '100 Tech Park', city: 'San Francisco', postalCode: '94105', country: 'US', taxId: 'US12-3456789', registrationNumber: 'REG-12345' }, buyer: { name: 'CII Customer Inc', address: '200 Business Center', city: 'New York', postalCode: '10001', country: 'US', taxId: 'US98-7654321' }, items: [ { description: 'Cloud Storage Service', quantity: 100, unitPrice: 9.99, vatRate: 8.875, lineTotal: 999.00 } ], totals: { netAmount: 999.00, vatAmount: 88.67, grossAmount: 1087.67 }, paymentReference: 'PAY-2024-001' } }; // Convert CII -> UBL const convertedToUBL = await einvoice.convertFormat(originalCII, 'ubl'); // Convert UBL -> CII const backToCII = await einvoice.convertFormat(convertedToUBL, 'cii'); // Compare essential fields const fieldsMatch = { invoiceNumber: originalCII.data.invoiceNumber === backToCII.data.invoiceNumber, currency: originalCII.data.currency === backToCII.data.currency, sellerCountry: originalCII.data.seller.country === backToCII.data.seller.country, vatAmount: Math.abs(originalCII.data.totals.vatAmount - backToCII.data.totals.vatAmount) < 0.01, grossAmount: Math.abs(originalCII.data.totals.grossAmount - backToCII.data.totals.grossAmount) < 0.01 }; return { fieldsMatch, originalFormat: 'cii' }; } ); // Test 3: Complex multi-format round-trip with ZUGFeRD const zugferdRoundTrip = await performanceTracker.measureAsync( 'zugferd-multi-format-round-trip', async () => { const einvoice = new EInvoice(); // Create ZUGFeRD invoice const originalZugferd = { format: 'zugferd' as const, data: { documentType: 'INVOICE', invoiceNumber: 'ZF-RT-2024-001', issueDate: '2024-01-22', seller: { name: 'ZUGFeRD Handel GmbH', address: 'Handelsweg 10', city: 'Frankfurt', postalCode: '60311', country: 'DE', taxId: 'DE111222333', bankAccount: { iban: 'DE89370400440532013000', bic: 'COBADEFFXXX' } }, buyer: { name: 'ZUGFeRD Käufer AG', address: 'Käuferstraße 20', city: 'Hamburg', postalCode: '20095', country: 'DE', taxId: 'DE444555666' }, items: [ { description: 'Büromaterial Set', quantity: 50, unitPrice: 24.99, vatRate: 19, lineTotal: 1249.50, articleNumber: 'BM-2024' }, { description: 'Versandkosten', quantity: 1, unitPrice: 9.90, vatRate: 19, lineTotal: 9.90 } ], totals: { netAmount: 1259.40, vatAmount: 239.29, grossAmount: 1498.69 } } }; // Convert ZUGFeRD -> XRechnung -> UBL -> CII -> ZUGFeRD const toXRechnung = await einvoice.convertFormat(originalZugferd, 'xrechnung'); const toUBL = await einvoice.convertFormat(toXRechnung, 'ubl'); const toCII = await einvoice.convertFormat(toUBL, 'cii'); const backToZugferd = await einvoice.convertFormat(toCII, 'zugferd'); // Check critical business data preservation const dataIntegrity = { invoiceNumber: originalZugferd.data.invoiceNumber === backToZugferd.data.invoiceNumber, sellerTaxId: originalZugferd.data.seller.taxId === backToZugferd.data.seller.taxId, buyerTaxId: originalZugferd.data.buyer.taxId === backToZugferd.data.buyer.taxId, itemCount: originalZugferd.data.items.length === backToZugferd.data.items.length, totalPreserved: Math.abs(originalZugferd.data.totals.grossAmount - backToZugferd.data.totals.grossAmount) < 0.01, bankAccountPreserved: backToZugferd.data.seller.bankAccount && originalZugferd.data.seller.bankAccount.iban === backToZugferd.data.seller.bankAccount.iban }; return { dataIntegrity, conversionChain: 'ZUGFeRD -> XRechnung -> UBL -> CII -> ZUGFeRD', stepsCompleted: 4 }; } ); // Test 4: Round-trip with data validation at each step const validatedRoundTrip = await performanceTracker.measureAsync( 'validated-round-trip', async () => { const einvoice = new EInvoice(); const validationResults = []; // Start with UBL invoice const startInvoice = { format: 'ubl' as const, data: { documentType: 'INVOICE', invoiceNumber: 'VAL-RT-2024-001', issueDate: '2024-01-23', seller: { name: 'Validation Test Seller', address: 'Test Street 1', country: 'AT', taxId: 'ATU12345678' }, buyer: { name: 'Validation Test Buyer', address: 'Test Street 2', country: 'AT', taxId: 'ATU87654321' }, items: [{ description: 'Test Service', quantity: 1, unitPrice: 1000.00, vatRate: 20, lineTotal: 1000.00 }], totals: { netAmount: 1000.00, vatAmount: 200.00, grossAmount: 1200.00 } } }; // Validate original const originalValid = await einvoice.validateInvoice(startInvoice); validationResults.push({ step: 'original', valid: originalValid.isValid }); // Convert and validate at each step const formats = ['cii', 'xrechnung', 'zugferd', 'ubl']; let currentInvoice = startInvoice; for (const targetFormat of formats) { try { currentInvoice = await einvoice.convertFormat(currentInvoice, targetFormat); const validation = await einvoice.validateInvoice(currentInvoice); validationResults.push({ step: `converted-to-${targetFormat}`, valid: validation.isValid, errors: validation.errors?.length || 0 }); } catch (error) { validationResults.push({ step: `converted-to-${targetFormat}`, valid: false, error: error.message }); } } // Check if we made it back to original format with valid data const fullCircle = currentInvoice.format === startInvoice.format; const dataPreserved = currentInvoice.data.invoiceNumber === startInvoice.data.invoiceNumber && currentInvoice.data.totals.grossAmount === startInvoice.data.totals.grossAmount; return { validationResults, fullCircle, dataPreserved }; } ); // Test 5: Corpus round-trip testing const corpusRoundTrip = await performanceTracker.measureAsync( 'corpus-round-trip-analysis', async () => { const files = await corpusLoader.getFilesByPattern('**/*.xml'); const roundTripStats = { tested: 0, successful: 0, dataLoss: 0, conversionFailed: 0, formatCombinations: new Map() }; // Test a sample of files const sampleFiles = files.slice(0, 15); for (const file of sampleFiles) { try { const content = await plugins.fs.readFile(file, 'utf-8'); const einvoice = new EInvoice(); // Detect and parse original const format = await einvoice.detectFormat(content); if (!format || format === 'unknown') continue; const original = await einvoice.parseInvoice(content, format); roundTripStats.tested++; // Determine target format for round-trip const targetFormat = format === 'ubl' ? 'cii' : 'ubl'; const key = `${format}->${targetFormat}->${format}`; try { // Perform round-trip const converted = await einvoice.convertFormat(original, targetFormat); const backToOriginal = await einvoice.convertFormat(converted, format); // Check data preservation const criticalFieldsMatch = original.data.invoiceNumber === backToOriginal.data.invoiceNumber && original.data.seller?.taxId === backToOriginal.data.seller?.taxId && Math.abs((original.data.totals?.grossAmount || 0) - (backToOriginal.data.totals?.grossAmount || 0)) < 0.01; if (criticalFieldsMatch) { roundTripStats.successful++; } else { roundTripStats.dataLoss++; } // Track format combination roundTripStats.formatCombinations.set(key, (roundTripStats.formatCombinations.get(key) || 0) + 1 ); } catch (convError) { roundTripStats.conversionFailed++; } } catch (error) { // Skip files that can't be parsed } } return { ...roundTripStats, successRate: roundTripStats.tested > 0 ? (roundTripStats.successful / roundTripStats.tested * 100).toFixed(2) + '%' : 'N/A', formatCombinations: Array.from(roundTripStats.formatCombinations.entries()) }; } ); // Summary t.comment('\n=== CONV-09: Round-Trip Conversion Test Summary ==='); t.comment(`UBL -> CII -> UBL: ${ublRoundTrip.result.comparison.allFieldsMatch ? 'PERFECT MATCH' : 'DATA DIFFERENCES DETECTED'}`); t.comment(`CII -> UBL -> CII: ${Object.values(ciiRoundTrip.result.fieldsMatch).every(v => v) ? 'ALL FIELDS MATCH' : 'SOME FIELDS DIFFER'}`); t.comment(`Multi-format chain (${zugferdRoundTrip.result.conversionChain}): ${ Object.values(zugferdRoundTrip.result.dataIntegrity).filter(v => v).length }/${Object.keys(zugferdRoundTrip.result.dataIntegrity).length} checks passed`); t.comment(`\nValidated Round-trip Results:`); validatedRoundTrip.result.validationResults.forEach(r => { t.comment(` - ${r.step}: ${r.valid ? 'VALID' : 'INVALID'} ${r.errors ? `(${r.errors} errors)` : ''}`); }); t.comment(`\nCorpus Round-trip Analysis:`); t.comment(` - Files tested: ${corpusRoundTrip.result.tested}`); t.comment(` - Successful round-trips: ${corpusRoundTrip.result.successful}`); t.comment(` - Data loss detected: ${corpusRoundTrip.result.dataLoss}`); t.comment(` - Conversion failures: ${corpusRoundTrip.result.conversionFailed}`); t.comment(` - Success rate: ${corpusRoundTrip.result.successRate}`); t.comment(` - Format combinations tested:`); corpusRoundTrip.result.formatCombinations.forEach(([combo, count]) => { t.comment(` * ${combo}: ${count} files`); }); // Performance summary t.comment('\n=== Performance Summary ==='); performanceTracker.logSummary(); t.end(); }); tap.start();