einvoice/test/suite/einvoice_edge-cases/test.edge-06.circular-references.ts

667 lines
19 KiB
TypeScript

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();