/** * @file test.conv-08.extension-preservation.ts * @description Tests for preserving format-specific extensions during conversion */ 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-08: Extension Preservation'); tap.test('CONV-08: Extension Preservation - should preserve format-specific extensions', async (t) => { // Test 1: Preserve ZUGFeRD profile extensions const zugferdProfile = await performanceTracker.measureAsync( 'zugferd-profile-preservation', async () => { const einvoice = new EInvoice(); // Create invoice with ZUGFeRD-specific profile data const zugferdInvoice = { format: 'zugferd' as const, data: { documentType: 'INVOICE', invoiceNumber: 'ZF-2024-001', issueDate: '2024-01-15', seller: { name: 'Test GmbH', address: 'Test Street 1', country: 'DE', taxId: 'DE123456789' }, buyer: { name: 'Customer AG', address: 'Customer Street 2', country: 'DE', taxId: 'DE987654321' }, items: [{ description: 'Product with ZUGFeRD extensions', quantity: 1, unitPrice: 100.00, vatRate: 19 }], // ZUGFeRD-specific extensions extensions: { profile: 'EXTENDED', guidedInvoiceReference: 'GI-2024-001', contractReference: 'CONTRACT-2024', orderReference: 'ORDER-2024-001', additionalReferences: [ { type: 'DeliveryNote', number: 'DN-2024-001' }, { type: 'PurchaseOrder', number: 'PO-2024-001' } ] } } }; // Convert to UBL const converted = await einvoice.convertFormat(zugferdInvoice, 'ubl'); // Check if extensions are preserved const extensionPreserved = converted.data.extensions && converted.data.extensions.zugferd && converted.data.extensions.zugferd.profile === 'EXTENDED'; return { extensionPreserved, originalExtensions: zugferdInvoice.data.extensions }; } ); // Test 2: Preserve PEPPOL customization ID const peppolCustomization = await performanceTracker.measureAsync( 'peppol-customization-preservation', async () => { const einvoice = new EInvoice(); // Create PEPPOL invoice with customization const peppolInvoice = { format: 'ubl' as const, data: { documentType: 'INVOICE', invoiceNumber: 'PEPPOL-2024-001', issueDate: '2024-01-15', seller: { name: 'Nordic Supplier AS', address: 'Business Street 1', country: 'NO', taxId: 'NO999888777' }, buyer: { name: 'Swedish Buyer AB', address: 'Customer Street 2', country: 'SE', taxId: 'SE556677889901' }, items: [{ description: 'PEPPOL compliant service', quantity: 1, unitPrice: 1000.00, vatRate: 25 }], // PEPPOL-specific extensions extensions: { customizationID: 'urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0', profileID: 'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0', endpointID: { scheme: '0088', value: '7300010000001' } } } }; // Convert to CII const converted = await einvoice.convertFormat(peppolInvoice, 'cii'); // Check if PEPPOL extensions are preserved const peppolPreserved = converted.data.extensions && converted.data.extensions.peppol && converted.data.extensions.peppol.customizationID === peppolInvoice.data.extensions.customizationID; return { peppolPreserved, customizationID: peppolInvoice.data.extensions.customizationID }; } ); // Test 3: Preserve XRechnung routing information const xrechnungRouting = await performanceTracker.measureAsync( 'xrechnung-routing-preservation', async () => { const einvoice = new EInvoice(); // Create XRechnung with routing info const xrechnungInvoice = { format: 'xrechnung' as const, data: { documentType: 'INVOICE', invoiceNumber: 'XR-2024-001', issueDate: '2024-01-15', seller: { name: 'German Authority', address: 'Government Street 1', country: 'DE', taxId: 'DE123456789' }, buyer: { name: 'Public Institution', address: 'Public Street 2', country: 'DE', taxId: 'DE987654321' }, items: [{ description: 'Public service', quantity: 1, unitPrice: 500.00, vatRate: 19 }], // XRechnung-specific extensions extensions: { leitweg: '991-12345-67', buyerReference: 'BR-2024-001', processingCode: '01', specificationIdentifier: 'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.3' } } }; // Convert to another format const converted = await einvoice.convertFormat(xrechnungInvoice, 'ubl'); // Check if XRechnung routing is preserved const routingPreserved = converted.data.extensions && converted.data.extensions.xrechnung && converted.data.extensions.xrechnung.leitweg === '991-12345-67'; return { routingPreserved, leitweg: xrechnungInvoice.data.extensions.leitweg }; } ); // Test 4: Preserve multiple extensions in round-trip conversion const roundTripExtensions = await performanceTracker.measureAsync( 'round-trip-extension-preservation', async () => { const einvoice = new EInvoice(); // Create invoice with multiple extensions const originalInvoice = { format: 'ubl' as const, data: { documentType: 'INVOICE', invoiceNumber: 'MULTI-2024-001', issueDate: '2024-01-15', seller: { name: 'Multi-Extension Corp', address: 'Complex Street 1', country: 'FR', taxId: 'FR12345678901' }, buyer: { name: 'Extension Handler Ltd', address: 'Handler Street 2', country: 'IT', taxId: 'IT12345678901' }, items: [{ description: 'Complex product', quantity: 1, unitPrice: 250.00, vatRate: 22 }], // Multiple format extensions extensions: { // Business extensions orderReference: 'ORD-2024-001', contractReference: 'CTR-2024-001', projectReference: 'PRJ-2024-001', // Payment extensions paymentTerms: { dueDate: '2024-02-15', discountPercentage: 2, discountDays: 10 }, // Custom fields customFields: { department: 'IT', costCenter: 'CC-001', approver: 'John Doe', priority: 'HIGH' }, // Attachments metadata attachments: [ { name: 'terms.pdf', type: 'application/pdf', size: 102400 }, { name: 'delivery.jpg', type: 'image/jpeg', size: 204800 } ] } } }; // Convert UBL -> CII -> UBL const toCII = await einvoice.convertFormat(originalInvoice, 'cii'); const backToUBL = await einvoice.convertFormat(toCII, 'ubl'); // Check if all extensions survived round-trip const extensionsPreserved = backToUBL.data.extensions && backToUBL.data.extensions.orderReference === originalInvoice.data.extensions.orderReference && backToUBL.data.extensions.customFields && backToUBL.data.extensions.customFields.department === 'IT' && backToUBL.data.extensions.attachments && backToUBL.data.extensions.attachments.length === 2; return { extensionsPreserved, originalCount: Object.keys(originalInvoice.data.extensions).length, preservedCount: backToUBL.data.extensions ? Object.keys(backToUBL.data.extensions).length : 0 }; } ); // Test 5: Corpus validation - check extension preservation in real files const corpusExtensions = await performanceTracker.measureAsync( 'corpus-extension-analysis', async () => { const files = await corpusLoader.getFilesByPattern('**/*.xml'); const extensionStats = { totalFiles: 0, filesWithExtensions: 0, extensionTypes: new Set(), conversionTests: 0, preservationSuccess: 0 }; // Sample up to 20 files for conversion testing const sampleFiles = files.slice(0, 20); for (const file of sampleFiles) { try { const content = await plugins.fs.readFile(file, 'utf-8'); const einvoice = new EInvoice(); // Detect format const format = await einvoice.detectFormat(content); if (!format || format === 'unknown') continue; extensionStats.totalFiles++; // Parse to check for extensions const parsed = await einvoice.parseInvoice(content, format); if (parsed.data.extensions && Object.keys(parsed.data.extensions).length > 0) { extensionStats.filesWithExtensions++; Object.keys(parsed.data.extensions).forEach(ext => extensionStats.extensionTypes.add(ext)); // Try conversion to test preservation const targetFormat = format === 'ubl' ? 'cii' : 'ubl'; try { const converted = await einvoice.convertFormat(parsed, targetFormat); extensionStats.conversionTests++; if (converted.data.extensions && Object.keys(converted.data.extensions).length > 0) { extensionStats.preservationSuccess++; } } catch (convError) { // Conversion not supported, skip } } } catch (error) { // File parsing error, skip } } return extensionStats; } ); // Summary t.comment('\n=== CONV-08: Extension Preservation Test Summary ==='); t.comment(`ZUGFeRD Profile Extensions: ${zugferdProfile.result.extensionPreserved ? 'PRESERVED' : 'LOST'}`); t.comment(`PEPPOL Customization ID: ${peppolCustomization.result.peppolPreserved ? 'PRESERVED' : 'LOST'}`); t.comment(`XRechnung Routing Info: ${xrechnungRouting.result.routingPreserved ? 'PRESERVED' : 'LOST'}`); t.comment(`Round-trip Extensions: ${roundTripExtensions.result.originalCount} original, ${roundTripExtensions.result.preservedCount} preserved`); t.comment('\nCorpus Analysis:'); t.comment(`- Files analyzed: ${corpusExtensions.result.totalFiles}`); t.comment(`- Files with extensions: ${corpusExtensions.result.filesWithExtensions}`); t.comment(`- Extension types found: ${Array.from(corpusExtensions.result.extensionTypes).join(', ')}`); t.comment(`- Conversion tests: ${corpusExtensions.result.conversionTests}`); t.comment(`- Successful preservation: ${corpusExtensions.result.preservationSuccess}`); // Performance summary t.comment('\n=== Performance Summary ==='); performanceTracker.logSummary(); t.end(); }); tap.start();