einvoice/test/suite/einvoice_conversion/test.conv-10.batch-conversion.ts

537 lines
20 KiB
TypeScript

/**
* @file test.conv-10.batch-conversion.ts
* @description Tests for batch conversion operations and performance
*/
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as plugins from '../../plugins.js';
import { EInvoice } from '../../../ts/index.js';
tap.test('CONV-10: Batch Conversion - should handle sequential batch loading', async (t) => {
const einvoice = new EInvoice();
const batchSize = 10;
const results = {
processed: 0,
successful: 0,
failed: 0,
totalTime: 0,
averageTime: 0
};
// Create test UBL invoices
const ublInvoices = Array.from({ length: batchSize }, (_, i) => {
const invoiceNumber = `BATCH-SEQ-2024-${String(i + 1).padStart(3, '0')}`;
return `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ID>${invoiceNumber}</cbc:ID>
<cbc:IssueDate>2024-01-25</cbc:IssueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Seller Company ${i + 1}</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Address ${i + 1}</cbc:StreetName>
<cbc:CityName>Berlin</cbc:CityName>
<cbc:PostalZone>10115</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>DE${String(123456789 + i).padStart(9, '0')}</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Buyer Company ${i + 1}</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Buyer Address ${i + 1}</cbc:StreetName>
<cbc:CityName>Munich</cbc:CityName>
<cbc:PostalZone>80331</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:InvoiceLine>
<cbc:ID>1</cbc:ID>
<cbc:InvoicedQuantity unitCode="EA">${i + 1}</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">${(i + 1) * (100.00 + (i * 10))}</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Name>Product ${i + 1}</cbc:Name>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">${100.00 + (i * 10)}</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="EUR">${(i + 1) * (100.00 + (i * 10))}</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="EUR">${(i + 1) * (100.00 + (i * 10))}</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="EUR">${((i + 1) * (100.00 + (i * 10)) * 1.19).toFixed(2)}</cbc:TaxInclusiveAmount>
<cbc:PayableAmount currencyID="EUR">${((i + 1) * (100.00 + (i * 10)) * 1.19).toFixed(2)}</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
</Invoice>`;
});
// Process sequentially
const startTime = Date.now();
for (const xmlContent of ublInvoices) {
results.processed++;
try {
const loaded = await einvoice.loadXml(xmlContent);
if (loaded && loaded.id) {
results.successful++;
} else {
console.log('Loaded but no id:', loaded?.id);
}
} catch (error) {
console.log('Error loading invoice:', error);
results.failed++;
}
}
results.totalTime = Date.now() - startTime;
results.averageTime = results.totalTime / results.processed;
console.log(`Sequential Batch (${results.processed} invoices):`);
console.log(` - Successful: ${results.successful}`);
console.log(` - Failed: ${results.failed}`);
console.log(` - Total time: ${results.totalTime}ms`);
console.log(` - Average time per invoice: ${results.averageTime.toFixed(2)}ms`);
expect(results.successful).toEqual(batchSize);
expect(results.failed).toEqual(0);
});
tap.test('CONV-10: Batch Conversion - should handle parallel batch loading', async (t) => {
const einvoice = new EInvoice();
const batchSize = 10;
const results = {
processed: 0,
successful: 0,
failed: 0,
totalTime: 0,
averageTime: 0
};
// Create test CII invoices
const ciiInvoices = Array.from({ length: batchSize }, (_, i) => {
const invoiceNumber = `BATCH-PAR-2024-${String(i + 1).padStart(3, '0')}`;
return `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice
xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocumentContext>
<ram:BusinessProcessSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
</ram:BusinessProcessSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>${invoiceNumber}</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20240125</udt:DateTimeString>
</ram:IssueDateTime>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
<ram:Name>Parallel Seller ${i + 1}</ram:Name>
<ram:PostalTradeAddress>
<ram:LineOne>Parallel Address ${i + 1}</ram:LineOne>
<ram:CityName>Paris</ram:CityName>
<ram:PostcodeCode>75001</ram:PostcodeCode>
<ram:CountryID>FR</ram:CountryID>
</ram:PostalTradeAddress>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="VA">FR${String(12345678901 + i).padStart(11, '0')}</ram:ID>
</ram:SpecifiedTaxRegistration>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>Parallel Buyer ${i + 1}</ram:Name>
<ram:PostalTradeAddress>
<ram:LineOne>Parallel Buyer Address ${i + 1}</ram:LineOne>
<ram:CityName>Lyon</ram:CityName>
<ram:PostcodeCode>69001</ram:PostcodeCode>
<ram:CountryID>FR</ram:CountryID>
</ram:PostalTradeAddress>
</ram:BuyerTradeParty>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeSettlement>
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:LineTotalAmount>500.00</ram:LineTotalAmount>
<ram:TaxBasisTotalAmount>500.00</ram:TaxBasisTotalAmount>
<ram:TaxTotalAmount currencyID="EUR">100.00</ram:TaxTotalAmount>
<ram:GrandTotalAmount>600.00</ram:GrandTotalAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>`;
});
// Process in parallel
const startTime = Date.now();
const loadingPromises = ciiInvoices.map(async (xmlContent) => {
try {
const loaded = await einvoice.loadXml(xmlContent);
return { success: true, loaded };
} catch (error) {
return { success: false, error };
}
});
const loadingResults = await Promise.all(loadingPromises);
results.processed = loadingResults.length;
results.successful = loadingResults.filter(r => r.success && r.loaded?.id).length;
results.failed = loadingResults.filter(r => !r.success).length;
results.totalTime = Date.now() - startTime;
results.averageTime = results.totalTime / results.processed;
console.log(`\nParallel Batch (${results.processed} invoices):`);
console.log(` - Successful: ${results.successful}`);
console.log(` - Failed: ${results.failed}`);
console.log(` - Total time: ${results.totalTime}ms`);
console.log(` - Average time per invoice: ${results.averageTime.toFixed(2)}ms`);
expect(results.successful).toEqual(batchSize);
expect(results.failed).toEqual(0);
});
tap.test('CONV-10: Batch Conversion - should handle mixed format batch loading', async (t) => {
const einvoice = new EInvoice();
const results = {
byFormat: new Map<string, { processed: number; successful: number; failed: number }>(),
totalProcessed: 0,
totalSuccessful: 0
};
// Create mixed format invoices (3 of each)
const mixedInvoices = [
// UBL invoices
...Array.from({ length: 3 }, (_, i) => ({
format: 'ubl',
content: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ID>MIXED-UBL-${i + 1}</cbc:ID>
<cbc:IssueDate>2024-01-26</cbc:IssueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>UBL Seller ${i + 1}</cbc:Name>
</cac:PartyName>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>UBL Buyer ${i + 1}</cbc:Name>
</cac:PartyName>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:LegalMonetaryTotal>
<cbc:PayableAmount currencyID="EUR">297.50</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
</Invoice>`
})),
// CII invoices
...Array.from({ length: 3 }, (_, i) => ({
format: 'cii',
content: `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice
xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocument>
<ram:ID>MIXED-CII-${i + 1}</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20240126</udt:DateTimeString>
</ram:IssueDateTime>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
<ram:Name>CII Seller ${i + 1}</ram:Name>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>CII Buyer ${i + 1}</ram:Name>
</ram:BuyerTradeParty>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeSettlement>
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>`
}))
];
// Process mixed batch
for (const invoice of mixedInvoices) {
const format = invoice.format;
if (!results.byFormat.has(format)) {
results.byFormat.set(format, { processed: 0, successful: 0, failed: 0 });
}
const formatStats = results.byFormat.get(format)!;
formatStats.processed++;
results.totalProcessed++;
try {
const loaded = await einvoice.loadXml(invoice.content);
if (loaded && loaded.id) {
formatStats.successful++;
results.totalSuccessful++;
}
} catch (error) {
formatStats.failed++;
}
}
const successRate = (results.totalSuccessful / results.totalProcessed * 100).toFixed(2) + '%';
console.log(`\nMixed Format Batch:`);
console.log(` - Total processed: ${results.totalProcessed}`);
console.log(` - Success rate: ${successRate}`);
console.log(` - Format statistics:`);
results.byFormat.forEach((stats, format) => {
console.log(` * ${format}: ${stats.successful}/${stats.processed} successful`);
});
expect(results.totalSuccessful).toEqual(results.totalProcessed);
});
tap.test('CONV-10: Batch Conversion - should handle large batch with memory monitoring', async (t) => {
const einvoice = new EInvoice();
const batchSize = 50;
const memorySnapshots = [];
// Capture initial memory
if (global.gc) global.gc();
const initialMemory = process.memoryUsage();
// Create large batch of simple UBL invoices
const largeBatch = Array.from({ length: batchSize }, (_, i) => {
const invoiceNumber = `LARGE-BATCH-${String(i + 1).padStart(4, '0')}`;
return `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ID>${invoiceNumber}</cbc:ID>
<cbc:IssueDate>2024-01-27</cbc:IssueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Large Batch Seller ${i + 1}</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Street ${i + 1}, Building ${i % 10 + 1}</cbc:StreetName>
<cbc:CityName>Berlin</cbc:CityName>
<cbc:PostalZone>${10000 + i}</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>DE${String(100000000 + i).padStart(9, '0')}</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Large Batch Buyer ${i + 1}</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Avenue ${i + 1}, Suite ${i % 20 + 1}</cbc:StreetName>
<cbc:CityName>Munich</cbc:CityName>
<cbc:PostalZone>${80000 + i}</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
</cac:Party>
</cac:AccountingCustomerParty>
${Array.from({ length: 5 }, (_, j) => `
<cac:InvoiceLine>
<cbc:ID>${j + 1}</cbc:ID>
<cbc:InvoicedQuantity unitCode="EA">${j + 1}</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">${(j + 1) * (50.00 + j * 10)}</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Name>Product ${i + 1}-${j + 1} with detailed description</cbc:Name>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">${50.00 + j * 10}</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>`).join('')}
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="EUR">${Array.from({ length: 5 }, (_, j) => (j + 1) * (50.00 + j * 10)).reduce((a, b) => a + b, 0)}</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="EUR">${Array.from({ length: 5 }, (_, j) => (j + 1) * (50.00 + j * 10)).reduce((a, b) => a + b, 0)}</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="EUR">${(Array.from({ length: 5 }, (_, j) => (j + 1) * (50.00 + j * 10)).reduce((a, b) => a + b, 0) * 1.19).toFixed(2)}</cbc:TaxInclusiveAmount>
<cbc:PayableAmount currencyID="EUR">${(Array.from({ length: 5 }, (_, j) => (j + 1) * (50.00 + j * 10)).reduce((a, b) => a + b, 0) * 1.19).toFixed(2)}</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
</Invoice>`;
});
// Process in chunks and monitor memory
const chunkSize = 10;
let processed = 0;
let successful = 0;
for (let i = 0; i < largeBatch.length; i += chunkSize) {
const chunk = largeBatch.slice(i, i + chunkSize);
// Process chunk
const chunkResults = await Promise.all(
chunk.map(async (xmlContent) => {
try {
const loaded = await einvoice.loadXml(xmlContent);
return loaded && loaded.id;
} catch {
return false;
}
})
);
processed += chunk.length;
successful += chunkResults.filter(r => r).length;
// Capture memory snapshot
const currentMemory = process.memoryUsage();
memorySnapshots.push({
processed,
heapUsed: Math.round((currentMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024 * 100) / 100,
external: Math.round((currentMemory.external - initialMemory.external) / 1024 / 1024 * 100) / 100
});
}
// Force garbage collection if available
if (global.gc) global.gc();
const finalMemory = process.memoryUsage();
const results = {
processed,
successful,
successRate: (successful / processed * 100).toFixed(2) + '%',
memoryIncrease: {
heapUsed: Math.round((finalMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024 * 100) / 100,
external: Math.round((finalMemory.external - initialMemory.external) / 1024 / 1024 * 100) / 100
},
averageMemoryPerInvoice: Math.round((finalMemory.heapUsed - initialMemory.heapUsed) / processed / 1024 * 100) / 100
};
console.log(`\nLarge Batch Memory Analysis (${results.processed} invoices):`);
console.log(` - Success rate: ${results.successRate}`);
console.log(` - Memory increase: ${results.memoryIncrease.heapUsed}MB heap`);
console.log(` - Average memory per invoice: ${results.averageMemoryPerInvoice}KB`);
expect(results.successful).toEqual(batchSize);
expect(results.memoryIncrease.heapUsed).toBeLessThan(100); // Should use less than 100MB for 50 invoices
});
tap.test('CONV-10: Batch Conversion - should handle corpus batch loading', async (t) => {
const einvoice = new EInvoice();
const batchStats = {
totalFiles: 0,
processed: 0,
successful: 0,
failedParsing: 0,
formats: new Set<string>(),
processingTimes: [] as number[]
};
// Get a few corpus files for testing
const corpusDir = plugins.path.join(process.cwd(), 'test/assets/corpus');
const xmlFiles: string[] = [];
// Manually check a few known corpus files
const testFiles = [
'XML-Rechnung/UBL/EN16931_Einfach.ubl.xml',
'XML-Rechnung/CII/EN16931_Einfach.cii.xml',
'PEPPOL/Valid/billing-3.0-invoice-full-sample.xml'
];
for (const file of testFiles) {
const fullPath = plugins.path.join(corpusDir, file);
try {
await plugins.fs.access(fullPath);
xmlFiles.push(fullPath);
} catch {
// File doesn't exist, skip
}
}
batchStats.totalFiles = xmlFiles.length;
if (xmlFiles.length > 0) {
// Process files
for (const file of xmlFiles) {
const startTime = Date.now();
try {
const content = await plugins.fs.readFile(file, 'utf-8');
const loaded = await einvoice.loadXml(content);
if (loaded && loaded.id) {
batchStats.processed++;
batchStats.successful++;
// Track format from filename
if (file.includes('.ubl.')) batchStats.formats.add('ubl');
else if (file.includes('.cii.')) batchStats.formats.add('cii');
else if (file.includes('PEPPOL')) batchStats.formats.add('ubl');
} else {
batchStats.failedParsing++;
}
batchStats.processingTimes.push(Date.now() - startTime);
} catch (error) {
batchStats.failedParsing++;
}
}
// Calculate statistics
const avgProcessingTime = batchStats.processingTimes.length > 0 ?
batchStats.processingTimes.reduce((a, b) => a + b, 0) / batchStats.processingTimes.length : 0;
console.log(`\nCorpus Batch Loading (${batchStats.totalFiles} files):`);
console.log(` - Successfully parsed: ${batchStats.processed}`);
console.log(` - Failed parsing: ${batchStats.failedParsing}`);
console.log(` - Average processing time: ${Math.round(avgProcessingTime)}ms`);
console.log(` - Formats found: ${Array.from(batchStats.formats).join(', ')}`);
expect(batchStats.successful).toBeGreaterThan(0);
} else {
console.log('\nCorpus Batch Loading: No test files found, skipping test');
expect(true).toEqual(true); // Pass the test if no files found
}
});
tap.start();