667 lines
19 KiB
TypeScript
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(); |