429 lines
15 KiB
TypeScript
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(); |