einvoice/test/suite/einvoice_conversion/test.conv-02.ubl-to-cii.ts

579 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 conversion processing
// CONV-02: UBL to CII Conversion
// Tests conversion from UBL Invoice format to CII (Cross-Industry Invoice) format
// including field mapping, data preservation, and semantic equivalence
tap.test('CONV-02: UBL to CII Conversion - Basic Conversion', async (tools) => {
const startTime = Date.now();
try {
// Create a sample UBL invoice for conversion testing
const sampleUblXml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>UBL-TO-CII-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
<Note>Test conversion from UBL to CII format</Note>
<AccountingSupplierParty>
<Party>
<PartyName>
<Name>UBL Test Supplier</Name>
</PartyName>
<PostalAddress>
<StreetName>UBL Street 123</StreetName>
<CityName>UBL City</CityName>
<PostalZone>12345</PostalZone>
<Country>
<IdentificationCode>DE</IdentificationCode>
</Country>
</PostalAddress>
<PartyTaxScheme>
<CompanyID>DE123456789</CompanyID>
</PartyTaxScheme>
</Party>
</AccountingSupplierParty>
<AccountingCustomerParty>
<Party>
<PartyName>
<Name>UBL Test Customer</Name>
</PartyName>
<PostalAddress>
<StreetName>Customer Street 456</StreetName>
<CityName>Customer City</CityName>
<PostalZone>54321</PostalZone>
<Country>
<IdentificationCode>DE</IdentificationCode>
</Country>
</PostalAddress>
</Party>
</AccountingCustomerParty>
<InvoiceLine>
<ID>1</ID>
<InvoicedQuantity unitCode="C62">2</InvoicedQuantity>
<LineExtensionAmount currencyID="EUR">100.00</LineExtensionAmount>
<Item>
<Name>UBL Test Product</Name>
<Description>Product for UBL to CII conversion testing</Description>
<ClassifiedTaxCategory>
<Percent>19.00</Percent>
</ClassifiedTaxCategory>
</Item>
<Price>
<PriceAmount currencyID="EUR">50.00</PriceAmount>
</Price>
</InvoiceLine>
<TaxTotal>
<TaxAmount currencyID="EUR">19.00</TaxAmount>
<TaxSubtotal>
<TaxableAmount currencyID="EUR">100.00</TaxableAmount>
<TaxAmount currencyID="EUR">19.00</TaxAmount>
<TaxCategory>
<Percent>19.00</Percent>
<TaxScheme>
<ID>VAT</ID>
</TaxScheme>
</TaxCategory>
</TaxSubtotal>
</TaxTotal>
<LegalMonetaryTotal>
<LineExtensionAmount currencyID="EUR">100.00</LineExtensionAmount>
<TaxExclusiveAmount currencyID="EUR">100.00</TaxExclusiveAmount>
<TaxInclusiveAmount currencyID="EUR">119.00</TaxInclusiveAmount>
<PayableAmount currencyID="EUR">119.00</PayableAmount>
</LegalMonetaryTotal>
</Invoice>`;
const invoice = new EInvoice();
const parseResult = await invoice.fromXmlString(sampleUblXml);
expect(parseResult).toBeTruthy();
// Test UBL to CII conversion if supported
if (typeof invoice.convertTo === 'function') {
tools.log('Testing UBL to CII conversion...');
try {
const conversionResult = await invoice.convertTo('CII');
if (conversionResult) {
tools.log('✓ UBL to CII conversion completed');
// Verify the converted format
const convertedXml = await conversionResult.toXmlString();
expect(convertedXml).toBeTruthy();
expect(convertedXml.length).toBeGreaterThan(100);
// Check for CII format characteristics
const ciiChecks = {
hasCiiNamespace: convertedXml.includes('CrossIndustryInvoice') ||
convertedXml.includes('urn:un:unece:uncefact:data:standard:CrossIndustryInvoice'),
hasExchangedDocument: convertedXml.includes('ExchangedDocument'),
hasSupplyChainTrade: convertedXml.includes('SupplyChainTradeTransaction'),
hasOriginalId: convertedXml.includes('UBL-TO-CII-001'),
hasOriginalCurrency: convertedXml.includes('EUR')
};
tools.log('CII Format Verification:');
tools.log(` CII Namespace: ${ciiChecks.hasCiiNamespace}`);
tools.log(` ExchangedDocument: ${ciiChecks.hasExchangedDocument}`);
tools.log(` SupplyChainTrade: ${ciiChecks.hasSupplyChainTrade}`);
tools.log(` Original ID preserved: ${ciiChecks.hasOriginalId}`);
tools.log(` Currency preserved: ${ciiChecks.hasOriginalCurrency}`);
if (ciiChecks.hasCiiNamespace && ciiChecks.hasExchangedDocument) {
tools.log('✓ Valid CII format structure detected');
} else {
tools.log('⚠ CII format structure not clearly detected');
}
// Validate the converted invoice
try {
const validationResult = await conversionResult.validate();
if (validationResult.valid) {
tools.log('✓ Converted CII invoice passes validation');
} else {
tools.log(`⚠ Converted CII validation issues: ${validationResult.errors?.length || 0} errors`);
}
} catch (validationError) {
tools.log(`⚠ Converted CII validation failed: ${validationError.message}`);
}
} else {
tools.log('⚠ UBL to CII conversion returned no result');
}
} catch (conversionError) {
tools.log(`⚠ UBL to CII conversion failed: ${conversionError.message}`);
}
} else {
tools.log('⚠ UBL to CII conversion not supported (convertTo method not available)');
// Test alternative conversion approach if available
if (typeof invoice.toCii === 'function') {
try {
const ciiResult = await invoice.toCii();
if (ciiResult) {
tools.log('✓ Alternative UBL to CII conversion successful');
}
} catch (alternativeError) {
tools.log(`⚠ Alternative conversion failed: ${alternativeError.message}`);
}
}
}
} catch (error) {
tools.log(`Basic UBL to CII conversion test failed: ${error.message}`);
}
const duration = Date.now() - startTime;
PerformanceTracker.recordMetric('conversion-ubl-to-cii-basic', duration);
});
tap.test('CONV-02: UBL to CII Conversion - Corpus Testing', { timeout: testTimeout }, async (tools) => {
const startTime = Date.now();
let processedFiles = 0;
let successfulConversions = 0;
let conversionErrors = 0;
let totalConversionTime = 0;
try {
const ublFiles = await CorpusLoader.getFiles('UBL_XML_RECHNUNG');
tools.log(`Testing UBL to CII conversion with ${ublFiles.length} UBL files`);
if (ublFiles.length === 0) {
tools.log('⚠ No UBL files found in corpus for conversion testing');
return;
}
// Process a subset of files for performance
const filesToProcess = ublFiles.slice(0, Math.min(8, ublFiles.length));
for (const filePath of filesToProcess) {
const fileName = plugins.path.basename(filePath);
const fileConversionStart = Date.now();
try {
processedFiles++;
const invoice = new EInvoice();
const parseResult = await invoice.fromFile(filePath);
if (parseResult) {
// Attempt conversion to CII
if (typeof invoice.convertTo === 'function') {
const conversionResult = await invoice.convertTo('CII');
const fileConversionTime = Date.now() - fileConversionStart;
totalConversionTime += fileConversionTime;
if (conversionResult) {
successfulConversions++;
tools.log(`${fileName}: Converted to CII (${fileConversionTime}ms)`);
// Quick validation of converted content
const convertedXml = await conversionResult.toXmlString();
if (convertedXml && convertedXml.length > 100) {
tools.log(` Converted content length: ${convertedXml.length} chars`);
// Test key field preservation
const originalXml = await invoice.toXmlString();
const preservationChecks = {
currencyPreserved: originalXml.includes('EUR') === convertedXml.includes('EUR'),
datePreserved: originalXml.includes('2024') === convertedXml.includes('2024')
};
if (preservationChecks.currencyPreserved && preservationChecks.datePreserved) {
tools.log(` ✓ Key data preserved in conversion`);
}
}
} else {
conversionErrors++;
tools.log(`${fileName}: Conversion returned no result`);
}
} else {
conversionErrors++;
tools.log(`${fileName}: Conversion method not available`);
}
} else {
conversionErrors++;
tools.log(`${fileName}: Failed to parse original UBL`);
}
} catch (error) {
conversionErrors++;
const fileConversionTime = Date.now() - fileConversionStart;
totalConversionTime += fileConversionTime;
tools.log(`${fileName}: Conversion failed - ${error.message}`);
}
}
// Calculate statistics
const successRate = processedFiles > 0 ? (successfulConversions / processedFiles) * 100 : 0;
const averageConversionTime = processedFiles > 0 ? totalConversionTime / processedFiles : 0;
tools.log(`\nUBL to CII Conversion Summary:`);
tools.log(`- Files processed: ${processedFiles}`);
tools.log(`- Successful conversions: ${successfulConversions} (${successRate.toFixed(1)}%)`);
tools.log(`- Conversion errors: ${conversionErrors}`);
tools.log(`- Average conversion time: ${averageConversionTime.toFixed(1)}ms`);
// Performance expectations
if (processedFiles > 0) {
expect(averageConversionTime).toBeLessThan(3000); // 3 seconds max per file
}
// We expect some conversions to work, but don't require 100% success
// as some files might have format-specific features that can't be converted
if (processedFiles > 0) {
expect(successRate).toBeGreaterThan(0); // At least one conversion should work
}
} catch (error) {
tools.log(`UBL to CII corpus testing failed: ${error.message}`);
throw error;
}
const totalDuration = Date.now() - startTime;
PerformanceTracker.recordMetric('conversion-ubl-to-cii-corpus', totalDuration);
tools.log(`UBL to CII corpus testing completed in ${totalDuration}ms`);
});
tap.test('CONV-02: UBL to CII Conversion - Field Mapping Verification', async (tools) => {
const startTime = Date.now();
// Test specific field mappings between UBL and CII
const fieldMappingTests = [
{
name: 'Invoice Header Fields',
ublXml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>FIELD-MAP-001</ID>
<IssueDate>2024-01-15</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<DocumentCurrencyCode>USD</DocumentCurrencyCode>
<Note>Field mapping test invoice</Note>
</Invoice>`,
expectedMappings: {
'ID': ['ExchangedDocument', 'ID'],
'IssueDate': ['ExchangedDocument', 'IssueDateTime'],
'InvoiceTypeCode': ['ExchangedDocument', 'TypeCode'],
'DocumentCurrencyCode': ['InvoiceCurrencyCode'],
'Note': ['IncludedNote']
}
},
{
name: 'Party Information',
ublXml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>PARTY-MAP-001</ID>
<IssueDate>2024-01-15</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<AccountingSupplierParty>
<Party>
<PartyName>
<Name>Supplier Company Ltd</Name>
</PartyName>
<PostalAddress>
<StreetName>Main Street 100</StreetName>
<CityName>Business City</CityName>
<PostalZone>10001</PostalZone>
<Country>
<IdentificationCode>US</IdentificationCode>
</Country>
</PostalAddress>
</Party>
</AccountingSupplierParty>
</Invoice>`,
expectedMappings: {
'AccountingSupplierParty': ['SellerTradeParty'],
'PartyName/Name': ['Name'],
'PostalAddress': ['PostalTradeAddress'],
'StreetName': ['LineOne'],
'CityName': ['CityName'],
'PostalZone': ['PostcodeCode'],
'Country/IdentificationCode': ['CountryID']
}
},
{
name: 'Line Items and Pricing',
ublXml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>LINE-MAP-001</ID>
<IssueDate>2024-01-15</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<InvoiceLine>
<ID>1</ID>
<InvoicedQuantity unitCode="C62">5</InvoicedQuantity>
<LineExtensionAmount currencyID="USD">250.00</LineExtensionAmount>
<Item>
<Name>Mapping Test Product</Name>
<Description>Product for field mapping verification</Description>
</Item>
<Price>
<PriceAmount currencyID="USD">50.00</PriceAmount>
</Price>
</InvoiceLine>
</Invoice>`,
expectedMappings: {
'InvoiceLine': ['IncludedSupplyChainTradeLineItem'],
'InvoiceLine/ID': ['AssociatedDocumentLineDocument/LineID'],
'InvoicedQuantity': ['SpecifiedLineTradeDelivery/BilledQuantity'],
'LineExtensionAmount': ['SpecifiedLineTradeSettlement/SpecifiedTradeSettlementLineMonetarySummation/LineTotalAmount'],
'Item/Name': ['SpecifiedTradeProduct/Name'],
'Price/PriceAmount': ['SpecifiedLineTradeAgreement/NetPriceProductTradePrice/ChargeAmount']
}
}
];
for (const mappingTest of fieldMappingTests) {
tools.log(`Testing ${mappingTest.name} field mapping...`);
try {
const invoice = new EInvoice();
const parseResult = await invoice.fromXmlString(mappingTest.ublXml);
if (parseResult) {
if (typeof invoice.convertTo === 'function') {
const conversionResult = await invoice.convertTo('CII');
if (conversionResult) {
const convertedXml = await conversionResult.toXmlString();
tools.log(`${mappingTest.name} conversion completed`);
tools.log(` Converted XML length: ${convertedXml.length} chars`);
// Check for expected CII structure elements
let mappingsFound = 0;
let mappingsTotal = Object.keys(mappingTest.expectedMappings).length;
for (const [ublField, ciiPath] of Object.entries(mappingTest.expectedMappings)) {
const ciiElements = Array.isArray(ciiPath) ? ciiPath : [ciiPath];
const hasMapping = ciiElements.some(element => convertedXml.includes(element));
if (hasMapping) {
mappingsFound++;
tools.log(`${ublField}${ciiElements.join('/')} mapped`);
} else {
tools.log(`${ublField}${ciiElements.join('/')} not found`);
}
}
const mappingSuccessRate = (mappingsFound / mappingsTotal) * 100;
tools.log(` Field mapping success rate: ${mappingSuccessRate.toFixed(1)}% (${mappingsFound}/${mappingsTotal})`);
if (mappingSuccessRate >= 70) {
tools.log(` ✓ Good field mapping coverage`);
} else {
tools.log(` ⚠ Low field mapping coverage - may need implementation`);
}
} else {
tools.log(`${mappingTest.name} conversion returned no result`);
}
} else {
tools.log(`${mappingTest.name} conversion not supported`);
}
} else {
tools.log(`${mappingTest.name} UBL parsing failed`);
}
} catch (error) {
tools.log(`${mappingTest.name} test failed: ${error.message}`);
}
}
const duration = Date.now() - startTime;
PerformanceTracker.recordMetric('conversion-ubl-to-cii-field-mapping', duration);
});
tap.test('CONV-02: UBL to CII Conversion - Data Integrity', async (tools) => {
const startTime = Date.now();
// Test data integrity during conversion
const integrityTestXml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>INTEGRITY-TEST-001</ID>
<IssueDate>2024-01-15</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
<Note>Special characters: äöüß £$¥ áéíóú àèìòù</Note>
<AccountingSupplierParty>
<Party>
<PartyName>
<Name>Tëst Suppliér Çômpány</Name>
</PartyName>
</Party>
</AccountingSupplierParty>
<InvoiceLine>
<ID>1</ID>
<InvoicedQuantity unitCode="C62">3.5</InvoicedQuantity>
<LineExtensionAmount currencyID="EUR">175.50</LineExtensionAmount>
<Item>
<Name>Prödüct wíth spëcíàl chäractërs</Name>
<Description>Testing unicode: 中文 العربية</Description>
</Item>
<Price>
<PriceAmount currencyID="EUR">50.14</PriceAmount>
</Price>
</InvoiceLine>
<TaxTotal>
<TaxAmount currencyID="EUR">33.35</TaxAmount>
</TaxTotal>
<LegalMonetaryTotal>
<LineExtensionAmount currencyID="EUR">175.50</LineExtensionAmount>
<TaxExclusiveAmount currencyID="EUR">175.50</TaxExclusiveAmount>
<TaxInclusiveAmount currencyID="EUR">208.85</TaxInclusiveAmount>
<PayableAmount currencyID="EUR">208.85</PayableAmount>
</LegalMonetaryTotal>
</Invoice>`;
try {
const invoice = new EInvoice();
const parseResult = await invoice.fromXmlString(integrityTestXml);
if (parseResult) {
tools.log('Testing data integrity during UBL to CII conversion...');
if (typeof invoice.convertTo === 'function') {
const conversionResult = await invoice.convertTo('CII');
if (conversionResult) {
const convertedXml = await conversionResult.toXmlString();
const originalXml = await invoice.toXmlString();
// Test data integrity
const integrityChecks = {
invoiceIdPreserved: convertedXml.includes('INTEGRITY-TEST-001'),
specialCharsPreserved: convertedXml.includes('äöüß') && convertedXml.includes('€£$¥'),
unicodePreserved: convertedXml.includes('中文') || convertedXml.includes('日本語'),
numbersPreserved: convertedXml.includes('175.50') && convertedXml.includes('50.14'),
currencyPreserved: convertedXml.includes('EUR'),
datePreserved: convertedXml.includes('2024-01-15') || convertedXml.includes('20240115')
};
tools.log('Data Integrity Verification:');
tools.log(` Invoice ID preserved: ${integrityChecks.invoiceIdPreserved}`);
tools.log(` Special characters preserved: ${integrityChecks.specialCharsPreserved}`);
tools.log(` Unicode characters preserved: ${integrityChecks.unicodePreserved}`);
tools.log(` Numbers preserved: ${integrityChecks.numbersPreserved}`);
tools.log(` Currency preserved: ${integrityChecks.currencyPreserved}`);
tools.log(` Date preserved: ${integrityChecks.datePreserved}`);
const integrityScore = Object.values(integrityChecks).filter(Boolean).length;
const totalChecks = Object.values(integrityChecks).length;
const integrityPercentage = (integrityScore / totalChecks) * 100;
tools.log(`Data integrity score: ${integrityScore}/${totalChecks} (${integrityPercentage.toFixed(1)}%)`);
if (integrityPercentage >= 80) {
tools.log('✓ Good data integrity maintained');
} else {
tools.log('⚠ Data integrity issues detected');
}
// Test round-trip if possible
if (typeof conversionResult.convertTo === 'function') {
try {
const roundTripResult = await conversionResult.convertTo('UBL');
if (roundTripResult) {
const roundTripXml = await roundTripResult.toXmlString();
if (roundTripXml.includes('INTEGRITY-TEST-001')) {
tools.log('✓ Round-trip conversion preserves ID');
}
}
} catch (roundTripError) {
tools.log(`⚠ Round-trip test failed: ${roundTripError.message}`);
}
}
} else {
tools.log('⚠ Data integrity conversion returned no result');
}
} else {
tools.log('⚠ Data integrity conversion not supported');
}
} else {
tools.log('⚠ Data integrity test - UBL parsing failed');
}
} catch (error) {
tools.log(`Data integrity test failed: ${error.message}`);
}
const duration = Date.now() - startTime;
PerformanceTracker.recordMetric('conversion-ubl-to-cii-data-integrity', duration);
});
tap.test('CONV-02: Performance Summary', async (tools) => {
const operations = [
'conversion-ubl-to-cii-basic',
'conversion-ubl-to-cii-corpus',
'conversion-ubl-to-cii-field-mapping',
'conversion-ubl-to-cii-data-integrity'
];
tools.log(`\n=== UBL to CII Conversion 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(`\nUBL to CII conversion testing completed.`);
});