einvoice/test/suite/einvoice_pdf-operations/test.pdf-04.xml-embedding.ts

643 lines
23 KiB
TypeScript
Raw Normal View History

2025-05-25 19:45:37 +00:00
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as plugins from '../../../ts/plugins.ts';
import { EInvoice } from '../../../ts/classes.xinvoice.ts';
import { CorpusLoader } from '../../helpers/corpus.loader.ts';
import { PerformanceTracker } from '../../helpers/performance.tracker.ts';
const testTimeout = 300000; // 5 minutes timeout for PDF processing
// PDF-04: XML Embedding into PDF
// Tests embedding XML invoice data into existing PDF files and creating
// new PDF/A-3 compliant files with embedded XML attachments
tap.test('PDF-04: XML Embedding - Basic Embedding Test', async (tools) => {
const startTime = Date.now();
// Test basic XML embedding functionality
try {
// Create a sample XML invoice for embedding
const sampleXml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>EMBED-TEST-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
<AccountingSupplierParty>
<Party>
<PartyName>
<Name>Test Supplier for Embedding</Name>
</PartyName>
</Party>
</AccountingSupplierParty>
<AccountingCustomerParty>
<Party>
<PartyName>
<Name>Test Customer for Embedding</Name>
</PartyName>
</Party>
</AccountingCustomerParty>
<LegalMonetaryTotal>
<PayableAmount currencyID="EUR">100.00</PayableAmount>
</LegalMonetaryTotal>
</Invoice>`;
const invoice = new EInvoice();
// Parse the XML first
const parseResult = await invoice.fromXmlString(sampleXml);
expect(parseResult).toBeTruthy();
// Test embedding if the API supports it
if (typeof invoice.embedIntoPdf === 'function') {
tools.log('Testing XML embedding into PDF...');
// Create a simple base PDF for testing (mock implementation)
const outputPath = plugins.path.join(process.cwd(), '.nogit', 'test-embedded.pdf');
await plugins.fs.ensureDir(plugins.path.dirname(outputPath));
try {
const embeddingResult = await invoice.embedIntoPdf({
outputPath: outputPath,
xmlContent: sampleXml,
attachmentName: 'ZUGFeRD-invoice.xml'
});
if (embeddingResult) {
tools.log('✓ XML embedding operation completed');
// Verify output file exists
const outputExists = await plugins.fs.pathExists(outputPath);
if (outputExists) {
const outputStats = await plugins.fs.stat(outputPath);
tools.log(`✓ Output PDF created: ${(outputStats.size / 1024).toFixed(1)}KB`);
// Clean up
await plugins.fs.remove(outputPath);
} else {
tools.log('⚠ Output PDF file not found');
}
} else {
tools.log('⚠ XML embedding returned no result');
}
} catch (embeddingError) {
tools.log(`⚠ XML embedding failed: ${embeddingError.message}`);
// This might be expected if embedding is not fully implemented
}
} else {
tools.log('⚠ XML embedding functionality not available (embedIntoPdf method not found)');
// Test alternative embedding approach if available
if (typeof invoice.toPdf === 'function') {
try {
const pdfResult = await invoice.toPdf();
if (pdfResult) {
tools.log('✓ Alternative PDF generation successful');
}
} catch (pdfError) {
tools.log(`⚠ Alternative PDF generation failed: ${pdfError.message}`);
}
} else {
tools.log('⚠ No PDF embedding/generation methods available');
}
}
} catch (error) {
tools.log(`Basic embedding test failed: ${error.message}`);
}
const duration = Date.now() - startTime;
PerformanceTracker.recordMetric('pdf-embedding-basic', duration);
});
tap.test('PDF-04: XML Embedding - Embedding into Existing PDF', async (tools) => {
const startTime = Date.now();
try {
// Look for existing PDF files in corpus to use as base
const existingPdfs = await CorpusLoader.getFiles('ZUGFERD_V1');
if (existingPdfs.length === 0) {
tools.log('⚠ No existing PDF files found for embedding test');
return;
}
const basePdf = existingPdfs[0];
const basePdfName = plugins.path.basename(basePdf);
tools.log(`Testing embedding into existing PDF: ${basePdfName}`);
// Create new XML content to embed
const newXmlContent = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>EMBED-EXISTING-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
<Note>This XML was embedded into an existing PDF</Note>
<LegalMonetaryTotal>
<PayableAmount currencyID="EUR">250.00</PayableAmount>
</LegalMonetaryTotal>
</Invoice>`;
const invoice = new EInvoice();
await invoice.fromXmlString(newXmlContent);
// Test embedding into existing PDF
const outputPath = plugins.path.join(process.cwd(), '.nogit', 'test-embed-existing.pdf');
await plugins.fs.ensureDir(plugins.path.dirname(outputPath));
try {
// Check if embedding into existing PDF is supported
if (typeof invoice.embedIntoPdf === 'function') {
const embeddingOptions = {
basePdfPath: basePdf,
outputPath: outputPath,
xmlContent: newXmlContent,
attachmentName: 'embedded-invoice.xml',
preserveExisting: true
};
const embeddingResult = await invoice.embedIntoPdf(embeddingOptions);
if (embeddingResult) {
tools.log('✓ Embedding into existing PDF completed');
// Verify the result
const outputExists = await plugins.fs.pathExists(outputPath);
if (outputExists) {
const outputStats = await plugins.fs.stat(outputPath);
const baseStats = await plugins.fs.stat(basePdf);
tools.log(`Base PDF size: ${(baseStats.size / 1024).toFixed(1)}KB`);
tools.log(`Output PDF size: ${(outputStats.size / 1024).toFixed(1)}KB`);
// Output should be larger than base (contains additional XML)
if (outputStats.size > baseStats.size) {
tools.log('✓ Output PDF is larger, suggesting successful embedding');
} else {
tools.log('⚠ Output PDF is not larger than base');
}
// Test extraction from embedded PDF
try {
const extractionInvoice = new EInvoice();
const extractionResult = await extractionInvoice.fromFile(outputPath);
if (extractionResult) {
const extractedXml = await extractionInvoice.toXmlString();
if (extractedXml.includes('EMBED-EXISTING-001')) {
tools.log('✓ Successfully extracted embedded XML');
} else {
tools.log('⚠ Extracted XML does not contain expected content');
}
} else {
tools.log('⚠ Could not extract XML from embedded PDF');
}
} catch (extractionError) {
tools.log(`⚠ Extraction test failed: ${extractionError.message}`);
}
// Clean up
await plugins.fs.remove(outputPath);
} else {
tools.log('⚠ Output PDF file not created');
}
} else {
tools.log('⚠ Embedding into existing PDF returned no result');
}
} else {
tools.log('⚠ Embedding into existing PDF not supported');
}
} catch (embeddingError) {
tools.log(`⚠ Embedding into existing PDF failed: ${embeddingError.message}`);
}
} catch (error) {
tools.log(`Embedding into existing PDF test failed: ${error.message}`);
}
const duration = Date.now() - startTime;
PerformanceTracker.recordMetric('pdf-embedding-existing', duration);
});
tap.test('PDF-04: XML Embedding - Multiple Format Embedding', async (tools) => {
const startTime = Date.now();
// Test embedding different XML formats (UBL, CII, etc.)
const xmlFormats = [
{
name: 'UBL Invoice',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>UBL-EMBED-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
<LegalMonetaryTotal>
<PayableAmount currencyID="EUR">100.00</PayableAmount>
</LegalMonetaryTotal>
</Invoice>`,
attachmentName: 'ubl-invoice.xml'
},
{
name: 'CII Invoice',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<CrossIndustryInvoice xmlns="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100">
<ExchangedDocumentContext>
<GuidelineSpecifiedDocumentContextParameter>
<ID>urn:cen.eu:en16931:2017</ID>
</GuidelineSpecifiedDocumentContextParameter>
</ExchangedDocumentContext>
<ExchangedDocument>
<ID>CII-EMBED-001</ID>
<TypeCode>380</TypeCode>
<IssueDateTime>
<DateTimeString format="102">20240101</DateTimeString>
</IssueDateTime>
</ExchangedDocument>
<SupplyChainTradeTransaction>
<ApplicableHeaderTradeSettlement>
<InvoiceCurrencyCode>EUR</InvoiceCurrencyCode>
<SpecifiedTradeSettlementHeaderMonetarySummation>
<DuePayableAmount>100.00</DuePayableAmount>
</SpecifiedTradeSettlementHeaderMonetarySummation>
</ApplicableHeaderTradeSettlement>
</SupplyChainTradeTransaction>
</CrossIndustryInvoice>`,
attachmentName: 'cii-invoice.xml'
}
];
for (const format of xmlFormats) {
tools.log(`Testing ${format.name} embedding...`);
try {
const invoice = new EInvoice();
const parseResult = await invoice.fromXmlString(format.xml);
if (parseResult) {
// Test embedding if available
if (typeof invoice.embedIntoPdf === 'function') {
const outputPath = plugins.path.join(process.cwd(), '.nogit', `test-${format.name.toLowerCase().replace(/\s+/g, '-')}.pdf`);
await plugins.fs.ensureDir(plugins.path.dirname(outputPath));
try {
const embeddingResult = await invoice.embedIntoPdf({
outputPath: outputPath,
xmlContent: format.xml,
attachmentName: format.attachmentName
});
if (embeddingResult) {
tools.log(`${format.name} embedding completed`);
// Verify file creation
const outputExists = await plugins.fs.pathExists(outputPath);
if (outputExists) {
const outputStats = await plugins.fs.stat(outputPath);
tools.log(` Output size: ${(outputStats.size / 1024).toFixed(1)}KB`);
// Clean up
await plugins.fs.remove(outputPath);
}
} else {
tools.log(`${format.name} embedding returned no result`);
}
} catch (embeddingError) {
tools.log(`${format.name} embedding failed: ${embeddingError.message}`);
}
} else {
tools.log(`${format.name} embedding not supported (no embedIntoPdf method)`);
}
} else {
tools.log(`${format.name} XML parsing failed`);
}
} catch (error) {
tools.log(`${format.name} embedding test failed: ${error.message}`);
}
}
const duration = Date.now() - startTime;
PerformanceTracker.recordMetric('pdf-embedding-multiple-formats', duration);
});
tap.test('PDF-04: XML Embedding - Metadata and Compliance', async (tools) => {
const startTime = Date.now();
// Test PDF/A-3 compliance and metadata handling
const testXml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>METADATA-TEST-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
<LegalMonetaryTotal>
<PayableAmount currencyID="EUR">100.00</PayableAmount>
</LegalMonetaryTotal>
</Invoice>`;
try {
const invoice = new EInvoice();
await invoice.fromXmlString(testXml);
// Test embedding with various metadata options
const metadataOptions = [
{
name: 'PDF/A-3 Compliance',
options: {
pdfACompliance: 'PDF/A-3',
title: 'Electronic Invoice METADATA-TEST-001',
author: 'EInvoice Test Suite',
subject: 'Invoice with embedded XML',
keywords: 'invoice, electronic, PDF/A-3, ZUGFeRD'
}
},
{
name: 'ZUGFeRD Metadata',
options: {
zugferdProfile: 'BASIC',
zugferdVersion: '2.1',
conformanceLevel: 'PDFA_3B'
}
},
{
name: 'Custom Metadata',
options: {
customMetadata: {
invoiceNumber: 'METADATA-TEST-001',
issueDate: '2024-01-01',
supplier: 'Test Supplier',
customer: 'Test Customer'
}
}
}
];
for (const metadataTest of metadataOptions) {
tools.log(`Testing ${metadataTest.name}...`);
try {
if (typeof invoice.embedIntoPdf === 'function') {
const outputPath = plugins.path.join(process.cwd(), '.nogit', `test-${metadataTest.name.toLowerCase().replace(/\s+/g, '-')}.pdf`);
await plugins.fs.ensureDir(plugins.path.dirname(outputPath));
const embeddingOptions = {
outputPath: outputPath,
xmlContent: testXml,
attachmentName: 'invoice.xml',
...metadataTest.options
};
const embeddingResult = await invoice.embedIntoPdf(embeddingOptions);
if (embeddingResult) {
tools.log(`${metadataTest.name} embedding completed`);
// Verify file and basic properties
const outputExists = await plugins.fs.pathExists(outputPath);
if (outputExists) {
const outputStats = await plugins.fs.stat(outputPath);
tools.log(` Output size: ${(outputStats.size / 1024).toFixed(1)}KB`);
// TODO: Add PDF metadata validation if PDF parsing library is available
// For now, just verify file creation
// Clean up
await plugins.fs.remove(outputPath);
}
} else {
tools.log(`${metadataTest.name} embedding returned no result`);
}
} else {
tools.log(`${metadataTest.name} embedding not supported`);
}
} catch (metadataError) {
tools.log(`${metadataTest.name} embedding failed: ${metadataError.message}`);
}
}
} catch (error) {
tools.log(`Metadata and compliance test failed: ${error.message}`);
}
const duration = Date.now() - startTime;
PerformanceTracker.recordMetric('pdf-embedding-metadata', duration);
});
tap.test('PDF-04: XML Embedding - Performance and Size Analysis', async (tools) => {
const startTime = Date.now();
// Test embedding performance with different XML sizes
const sizeTests = [
{
name: 'Small XML (1KB)',
xmlGenerator: () => `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>SMALL-XML-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<LegalMonetaryTotal>
<PayableAmount currencyID="EUR">100.00</PayableAmount>
</LegalMonetaryTotal>
</Invoice>`
},
{
name: 'Medium XML (10KB)',
xmlGenerator: () => {
let xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>MEDIUM-XML-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>`;
// Add multiple invoice lines to increase size
for (let i = 1; i <= 50; i++) {
xml += `
<InvoiceLine>
<ID>${i}</ID>
<InvoicedQuantity unitCode="C62">1</InvoicedQuantity>
<LineExtensionAmount currencyID="EUR">10.00</LineExtensionAmount>
<Item>
<Name>Test Item ${i} with description that makes this line longer</Name>
<Description>Detailed description of test item ${i} for size testing purposes</Description>
</Item>
<Price>
<PriceAmount currencyID="EUR">10.00</PriceAmount>
</Price>
</InvoiceLine>`;
}
xml += `
<LegalMonetaryTotal>
<PayableAmount currencyID="EUR">500.00</PayableAmount>
</LegalMonetaryTotal>
</Invoice>`;
return xml;
}
},
{
name: 'Large XML (50KB)',
xmlGenerator: () => {
let xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>LARGE-XML-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>`;
// Add many invoice lines to increase size significantly
for (let i = 1; i <= 200; i++) {
xml += `
<InvoiceLine>
<ID>${i}</ID>
<InvoicedQuantity unitCode="C62">1</InvoicedQuantity>
<LineExtensionAmount currencyID="EUR">25.00</LineExtensionAmount>
<Item>
<Name>Test Item ${i} with very long description that includes many details about the product or service being invoiced</Name>
<Description>This is a very detailed description of test item ${i} for size testing purposes. It includes information about specifications, features, benefits, and other relevant details that would typically be found in a real invoice line item description.</Description>
<AdditionalItemProperty>
<Name>Property${i}</Name>
<Value>Value for property ${i} with additional text to increase size</Value>
</AdditionalItemProperty>
</Item>
<Price>
<PriceAmount currencyID="EUR">25.00</PriceAmount>
</Price>
</InvoiceLine>`;
}
xml += `
<LegalMonetaryTotal>
<PayableAmount currencyID="EUR">5000.00</PayableAmount>
</LegalMonetaryTotal>
</Invoice>`;
return xml;
}
}
];
const performanceResults = [];
for (const sizeTest of sizeTests) {
tools.log(`Testing embedding performance: ${sizeTest.name}`);
try {
const xml = sizeTest.xmlGenerator();
const xmlSizeKB = Buffer.byteLength(xml, 'utf8') / 1024;
tools.log(` XML size: ${xmlSizeKB.toFixed(1)}KB`);
const invoice = new EInvoice();
await invoice.fromXmlString(xml);
const embeddingStartTime = Date.now();
if (typeof invoice.embedIntoPdf === 'function') {
const outputPath = plugins.path.join(process.cwd(), '.nogit', `test-${sizeTest.name.toLowerCase().replace(/\s+/g, '-')}.pdf`);
await plugins.fs.ensureDir(plugins.path.dirname(outputPath));
try {
const embeddingResult = await invoice.embedIntoPdf({
outputPath: outputPath,
xmlContent: xml,
attachmentName: 'invoice.xml'
});
const embeddingTime = Date.now() - embeddingStartTime;
if (embeddingResult) {
const outputExists = await plugins.fs.pathExists(outputPath);
if (outputExists) {
const outputStats = await plugins.fs.stat(outputPath);
const outputSizeKB = outputStats.size / 1024;
const result = {
name: sizeTest.name,
xmlSizeKB: xmlSizeKB,
outputSizeKB: outputSizeKB,
embeddingTimeMs: embeddingTime,
timePerKB: embeddingTime / xmlSizeKB
};
performanceResults.push(result);
tools.log(` Embedding time: ${embeddingTime}ms`);
tools.log(` Output PDF size: ${outputSizeKB.toFixed(1)}KB`);
tools.log(` Time per KB: ${(embeddingTime / xmlSizeKB).toFixed(2)}ms/KB`);
// Clean up
await plugins.fs.remove(outputPath);
}
} else {
tools.log(` ⚠ Embedding returned no result`);
}
} catch (embeddingError) {
tools.log(` ⚠ Embedding failed: ${embeddingError.message}`);
}
} else {
tools.log(` ⚠ Embedding not supported`);
}
} catch (error) {
tools.log(`${sizeTest.name} failed: ${error.message}`);
}
}
// Analyze performance results
if (performanceResults.length > 0) {
tools.log(`\nEmbedding Performance Analysis:`);
const avgTimePerKB = performanceResults.reduce((sum, r) => sum + r.timePerKB, 0) / performanceResults.length;
const maxTime = Math.max(...performanceResults.map(r => r.embeddingTimeMs));
const minTime = Math.min(...performanceResults.map(r => r.embeddingTimeMs));
tools.log(`- Average time per KB: ${avgTimePerKB.toFixed(2)}ms/KB`);
tools.log(`- Fastest embedding: ${minTime}ms`);
tools.log(`- Slowest embedding: ${maxTime}ms`);
// Performance expectations
expect(avgTimePerKB).toBeLessThan(100); // 100ms per KB max
expect(maxTime).toBeLessThan(10000); // 10 seconds max for any size
}
const duration = Date.now() - startTime;
PerformanceTracker.recordMetric('pdf-embedding-performance', duration);
});
tap.test('PDF-04: Performance Summary', async (tools) => {
const operations = [
'pdf-embedding-basic',
'pdf-embedding-existing',
'pdf-embedding-multiple-formats',
'pdf-embedding-metadata',
'pdf-embedding-performance'
];
tools.log(`\n=== XML Embedding Performance Summary ===`);
for (const operation of operations) {
const summary = await PerformanceTracker.getSummary(operation);
if (summary) {
tools.log(`${operation}:`);
tools.log(` avg=${summary.average}ms, min=${summary.min}ms, max=${summary.max}ms, p95=${summary.p95}ms`);
}
}
tools.log(`\nXML embedding testing completed.`);
});