import { tap } from '@git.zone/tstest/tapbundle'; import { EInvoice } from '../../../ts/index.js'; import { InvoiceFormat } from '../../../ts/interfaces/common.js'; import { PerformanceTracker } from '../performance.tracker.js'; tap.test('EDGE-06: Circular References - should handle circular reference scenarios', async () => { // Test 1: Self-referencing related documents await PerformanceTracker.track('self-referencing-documents', async () => { const einvoice = new EInvoice(); einvoice.issueDate = new Date(2024, 0, 1); einvoice.invoiceId = 'CIRC-001'; // Set up basic invoice data einvoice.from = { type: 'company', name: 'Circular Test Company', description: 'Testing circular references', address: { streetName: 'Test Street', houseNumber: '1', postalCode: '12345', city: 'Test City', country: 'DE' }, status: 'active', foundedDate: { year: 2020, month: 1, day: 1 }, registrationDetails: { vatId: 'DE123456789', registrationId: 'HRB 12345', registrationName: 'Commercial Register' } }; einvoice.to = { type: 'company', name: 'Customer Company', description: 'Customer', address: { streetName: 'Customer Street', houseNumber: '2', postalCode: '54321', city: 'Customer City', country: 'DE' }, status: 'active', foundedDate: { year: 2019, month: 1, day: 1 }, registrationDetails: { vatId: 'DE987654321', registrationId: 'HRB 54321', registrationName: 'Commercial Register' } }; // Add self-referencing related document einvoice.relatedDocuments = [{ relationType: 'references', documentId: 'CIRC-001', // Self-reference issueDate: Date.now() }]; einvoice.items = [{ position: 1, name: 'Test Service', articleNumber: 'SRV-001', unitType: 'EA', unitQuantity: 1, unitNetPrice: 100, vatPercentage: 19 }]; try { const xmlString = await einvoice.toXmlString('ubl'); console.log('Self-referencing document: XML generated successfully'); // Try to import it back const newInvoice = new EInvoice(); await newInvoice.fromXmlString(xmlString); console.log('Self-referencing document: Round-trip successful'); console.log(`Related documents preserved: ${newInvoice.relatedDocuments?.length || 0}`); } catch (error) { console.log(`Self-referencing document failed: ${error.message}`); } }); // Test 2: Circular issuer/recipient relationships await PerformanceTracker.track('circular-issuer-recipient', async () => { const invoices = []; // Create two companies that invoice each other const companyA = { type: 'company' as const, name: 'Company A', description: 'First company', address: { streetName: 'A Street', houseNumber: '1', postalCode: '12345', city: 'A City', country: 'DE' }, status: 'active' as const, foundedDate: { year: 2020, month: 1, day: 1 }, registrationDetails: { vatId: 'DE111111111', registrationId: 'HRB 11111', registrationName: 'Commercial Register' } }; const companyB = { type: 'company' as const, name: 'Company B', description: 'Second company', address: { streetName: 'B Street', houseNumber: '2', postalCode: '54321', city: 'B City', country: 'DE' }, status: 'active' as const, foundedDate: { year: 2019, month: 1, day: 1 }, registrationDetails: { vatId: 'DE222222222', registrationId: 'HRB 22222', registrationName: 'Commercial Register' } }; // Invoice 1: A invoices B const invoice1 = new EInvoice(); invoice1.issueDate = new Date(2024, 0, 1); invoice1.invoiceId = 'A-TO-B-001'; invoice1.from = companyA; invoice1.to = companyB; invoice1.items = [{ position: 1, name: 'Service from A', articleNumber: 'A-001', unitType: 'EA', unitQuantity: 1, unitNetPrice: 100, vatPercentage: 19 }]; // Invoice 2: B invoices A (circular) const invoice2 = new EInvoice(); invoice2.issueDate = new Date(2024, 0, 2); invoice2.invoiceId = 'B-TO-A-001'; invoice2.from = companyB; invoice2.to = companyA; invoice2.relatedDocuments = [{ relationType: 'references', documentId: 'A-TO-B-001', issueDate: invoice1.date }]; invoice2.items = [{ position: 1, name: 'Service from B', articleNumber: 'B-001', unitType: 'EA', unitQuantity: 1, unitNetPrice: 150, vatPercentage: 19 }]; invoices.push(invoice1, invoice2); try { for (const invoice of invoices) { const xmlString = await invoice.toXmlString('cii'); console.log(`Circular issuer/recipient ${invoice.invoiceId}: XML generated`); } console.log('Circular issuer/recipient relationships handled successfully'); } catch (error) { console.log(`Circular issuer/recipient failed: ${error.message}`); } }); // Test 3: Deep nesting with circular item descriptions await PerformanceTracker.track('deep-nesting-circular', async () => { const einvoice = new EInvoice(); einvoice.issueDate = new Date(2024, 0, 1); einvoice.invoiceId = 'DEEP-001'; einvoice.from = { type: 'company', name: 'Deep Nesting Company', description: 'Company that references itself in description: Deep Nesting Company', address: { streetName: 'Recursive Street', houseNumber: '∞', postalCode: '12345', city: 'Loop City', country: 'DE' }, status: 'active', foundedDate: { year: 2020, month: 1, day: 1 }, registrationDetails: { vatId: 'DE333333333', registrationId: 'HRB 33333', registrationName: 'Commercial Register' } }; einvoice.to = { type: 'person', name: 'Recursive', surname: 'Customer', salutation: 'Mr' as const, sex: 'male' as const, title: 'Doctor' as const, description: 'Customer who buys recursive items', address: { streetName: 'Customer Street', houseNumber: '2', postalCode: '54321', city: 'Customer City', country: 'DE' } }; // Create items with descriptions that reference each other const itemCount = 10; einvoice.items = []; for (let i = 0; i < itemCount; i++) { einvoice.items.push({ position: i + 1, name: `Item ${i} references Item ${(i + 1) % itemCount}`, articleNumber: `CIRC-${i}`, unitType: 'EA', unitQuantity: 1, unitNetPrice: 10 * (i + 1), vatPercentage: 19 }); } try { const ublString = await einvoice.toXmlString('ubl'); console.log(`Deep nesting: Generated ${einvoice.items.length} circularly referencing items`); console.log(`XML size: ${ublString.length} bytes`); // Test round-trip const newInvoice = new EInvoice(); await newInvoice.fromXmlString(ublString); console.log(`Deep nesting round-trip: ${newInvoice.items.length} items preserved`); } catch (error) { console.log(`Deep nesting failed: ${error.message}`); } }); // Test 4: Format conversion with patterns await PerformanceTracker.track('format-conversion-patterns', async () => { // Create invoice with repeating patterns const einvoice = new EInvoice(); einvoice.issueDate = new Date(2024, 0, 1); einvoice.invoiceId = 'PATTERN-001'; einvoice.from = { type: 'company', name: 'Pattern Company', description: 'Pattern Pattern Pattern', address: { streetName: 'Pattern Street', houseNumber: '123', postalCode: '12345', city: 'Pattern City', country: 'DE' }, status: 'active', foundedDate: { year: 2020, month: 1, day: 1 }, registrationDetails: { vatId: 'DE444444444', registrationId: 'HRB 44444', registrationName: 'Pattern Register' } }; einvoice.to = { type: 'company', name: 'Repeat Customer', description: 'Customer Customer Customer', address: { streetName: 'Repeat Street', houseNumber: '321', postalCode: '54321', city: 'Repeat City', country: 'DE' }, status: 'active', foundedDate: { year: 2019, month: 1, day: 1 }, registrationDetails: { vatId: 'DE555555555', registrationId: 'HRB 55555', registrationName: 'Repeat Register' } }; // Add items with repeating patterns einvoice.items = [ { position: 1, name: 'AAA AAA AAA Service', articleNumber: 'AAA-001', unitType: 'EA', unitQuantity: 3, unitNetPrice: 33.33, vatPercentage: 19 }, { position: 2, name: 'BBB BBB BBB Product', articleNumber: 'BBB-002', unitType: 'EA', unitQuantity: 2, unitNetPrice: 22.22, vatPercentage: 19 } ]; try { // Convert between formats const ublString = await einvoice.toXmlString('ubl'); console.log('Pattern invoice: UBL generated'); const ublInvoice = await EInvoice.fromXml(ublString); const ciiString = await ublInvoice.toXmlString('cii'); console.log('Pattern invoice: Converted to CII'); const ciiInvoice = await EInvoice.fromXml(ciiString); console.log(`Pattern preservation: ${ciiInvoice.from.description === 'Pattern Pattern Pattern'}`); } catch (error) { console.log(`Format conversion patterns failed: ${error.message}`); } }); // Test 5: Memory safety with large circular structures await PerformanceTracker.track('memory-safety-circular', async () => { const iterations = 50; const beforeMem = process.memoryUsage(); try { // Create many invoices that reference each other const invoices: EInvoice[] = []; for (let i = 0; i < iterations; i++) { const einvoice = new EInvoice(); einvoice.issueDate = new Date(2024, 0, 1); einvoice.invoiceId = `MEM-${i}`; // Reference the previous invoice if (i > 0) { einvoice.relatedDocuments = [{ relationType: 'references', documentId: `MEM-${i - 1}`, issueDate: Date.now() }]; } // And reference the next one (creating a cycle) if (i < iterations - 1) { if (!einvoice.relatedDocuments) einvoice.relatedDocuments = []; einvoice.relatedDocuments.push({ relationType: 'references', documentId: `MEM-${i + 1}`, issueDate: Date.now() }); } einvoice.from = { type: 'company', name: `Company ${i}`, description: `References Company ${(i + 1) % iterations}`, address: { streetName: 'Memory Street', houseNumber: String(i), postalCode: '12345', city: 'Memory City', country: 'DE' }, status: 'active', foundedDate: { year: 2020, month: 1, day: 1 }, registrationDetails: { vatId: `DE${String(i).padStart(9, '0')}`, registrationId: `HRB ${i}`, registrationName: 'Commercial Register' } }; einvoice.to = { type: 'person', name: 'Test', surname: 'Customer', salutation: 'Mr' as const, sex: 'male' as const, title: 'Doctor' as const, description: 'Test customer', address: { streetName: 'Customer Street', houseNumber: '2', postalCode: '54321', city: 'Customer City', country: 'DE' } }; einvoice.items = [{ position: 1, name: `Item referencing invoice ${(i + 1) % iterations}`, articleNumber: `MEM-${i}`, unitType: 'EA', unitQuantity: 1, unitNetPrice: 10, vatPercentage: 19 }]; invoices.push(einvoice); } // Try to export all let exportedCount = 0; for (const invoice of invoices) { try { const xml = await invoice.toXmlString('ubl'); if (xml) exportedCount++; } catch (e) { // Ignore individual failures } } const afterMem = process.memoryUsage(); const memIncrease = (afterMem.heapUsed - beforeMem.heapUsed) / 1024 / 1024; console.log(`Memory safety: Created ${iterations} circular invoices`); console.log(`Successfully exported: ${exportedCount}`); console.log(`Memory increase: ${memIncrease.toFixed(2)} MB`); console.log(`Memory stable: ${memIncrease < 50}`); } catch (error) { console.log(`Memory safety test failed: ${error.message}`); } }); // Test 6: Validation with circular references await PerformanceTracker.track('validation-circular', async () => { const einvoice = new EInvoice(); einvoice.issueDate = new Date(2024, 0, 1); einvoice.invoiceId = 'VAL-CIRC-001'; einvoice.from = { type: 'company', name: 'Validation Company', description: 'Company for validation testing', address: { streetName: 'Validation Street', houseNumber: '1', postalCode: '12345', city: 'Validation City', country: 'DE' }, status: 'active', foundedDate: { year: 2020, month: 1, day: 1 }, registrationDetails: { vatId: 'DE666666666', registrationId: 'HRB 66666', registrationName: 'Commercial Register' } }; einvoice.to = { type: 'company', name: 'Customer Company', description: 'Customer', address: { streetName: 'Customer Street', houseNumber: '2', postalCode: '54321', city: 'Customer City', country: 'DE' }, status: 'active', foundedDate: { year: 2019, month: 1, day: 1 }, registrationDetails: { vatId: 'DE777777777', registrationId: 'HRB 77777', registrationName: 'Commercial Register' } }; // Create items with interdependent values einvoice.items = [ { position: 1, name: 'Base Item', articleNumber: 'BASE-001', unitType: 'EA', unitQuantity: 10, unitNetPrice: 100, vatPercentage: 19 }, { position: 2, name: 'Dependent Item (10% of Base)', articleNumber: 'DEP-001', unitType: 'EA', unitQuantity: 1, unitNetPrice: 100, // Should be 10% of base total vatPercentage: 19 }, { position: 3, name: 'Circular Dependent (refers to position 2)', articleNumber: 'CIRC-001', unitType: 'EA', unitQuantity: 1, unitNetPrice: 10, // 10% of dependent vatPercentage: 19 } ]; try { const xmlString = await einvoice.toXmlString('xrechnung'); console.log('Validation with circular refs: XML generated'); // Validate const validationResult = await einvoice.validate(); console.log(`Validation result: ${validationResult.valid ? 'valid' : 'invalid'}`); console.log(`Validation errors: ${validationResult.errors.length}`); if (validationResult.errors.length > 0) { console.log(`First error: ${validationResult.errors[0].message}`); } } catch (error) { console.log(`Validation circular failed: ${error.message}`); } }); // Test 7: PDF operations with circular metadata await PerformanceTracker.track('pdf-circular-metadata', async () => { const einvoice = new EInvoice(); einvoice.issueDate = new Date(2024, 0, 1); einvoice.invoiceId = 'PDF-CIRC-001'; einvoice.from = { type: 'company', name: 'PDF Company', description: 'Company testing PDF with circular refs', address: { streetName: 'PDF Street', houseNumber: '1', postalCode: '12345', city: 'PDF City', country: 'DE' }, status: 'active', foundedDate: { year: 2020, month: 1, day: 1 }, registrationDetails: { vatId: 'DE888888888', registrationId: 'HRB 88888', registrationName: 'Commercial Register' } }; einvoice.to = { type: 'person', name: 'PDF', surname: 'Customer', salutation: 'Mr' as const, sex: 'male' as const, title: 'Doctor' as const, description: 'Customer for PDF testing', address: { streetName: 'Customer Street', houseNumber: '2', postalCode: '54321', city: 'Customer City', country: 'DE' } }; einvoice.items = [{ position: 1, name: 'PDF Service', articleNumber: 'PDF-001', unitType: 'EA', unitQuantity: 1, unitNetPrice: 100, vatPercentage: 19 }]; // Set circular metadata einvoice.metadata = { format: InvoiceFormat.FACTURX, version: '1.0', customizationId: 'urn:factur-x.eu:1p0:basicwl', extensions: { circularRef: 'PDF-CIRC-001' // Self-reference } }; try { const xmlString = await einvoice.toXmlString('facturx'); console.log('PDF circular metadata: XML generated'); console.log(`Metadata preserved: ${einvoice.metadata?.extensions?.circularRef === 'PDF-CIRC-001'}`); // Test with minimal PDF const minimalPDF = Buffer.from('%PDF-1.4\n%%EOF'); try { const pdfWithXml = await einvoice.embedInPdf(minimalPDF, 'facturx'); console.log('PDF circular metadata: Embedded in PDF'); } catch (e) { console.log('PDF circular metadata: Embedding failed (expected for minimal PDF)'); } } catch (error) { console.log(`PDF circular metadata failed: ${error.message}`); } }); // Test 8: Empty circular structures await PerformanceTracker.track('empty-circular-structures', async () => { const einvoice = new EInvoice(); einvoice.issueDate = new Date(2024, 0, 1); einvoice.invoiceId = ''; // Empty ID einvoice.from = { type: 'company', name: '', // Empty name description: '', address: { streetName: '', houseNumber: '', postalCode: '', city: '', country: '' }, status: 'active', foundedDate: { year: 2020, month: 1, day: 1 }, registrationDetails: { vatId: '', registrationId: '', registrationName: '' } }; einvoice.to = einvoice.from; // Circular reference to same empty object einvoice.items = []; // Empty items einvoice.relatedDocuments = [{ relationType: 'references', documentId: '', // Empty reference issueDate: Date.now() }]; try { const xmlString = await einvoice.toXmlString('ubl'); console.log('Empty circular: XML generated despite empty values'); console.log(`XML length: ${xmlString.length} bytes`); } catch (error) { console.log(`Empty circular failed: ${error.message}`); } }); }); // Run the test tap.start();