einvoice/test/suite/einvoice_conversion/test.conv-09.round-trip.ts
2025-05-25 19:45:37 +00:00

429 lines
15 KiB
TypeScript

/**
* @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<string, number>()
};
// 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();