2025-05-25 19:45:37 +00:00
|
|
|
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
2025-05-26 10:17:50 +00:00
|
|
|
|
import * as plugins from '../../../ts/plugins.js';
|
|
|
|
|
import { EInvoice } from '../../../ts/index.js';
|
|
|
|
|
import { CorpusLoader } from '../../helpers/corpus.loader.js';
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
|
|
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) => {
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Create a sample UBL invoice for conversion testing
|
|
|
|
|
const sampleUblXml = `<?xml version="1.0" encoding="UTF-8"?>
|
2025-05-26 10:17:50 +00:00
|
|
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
|
|
|
|
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
|
|
|
|
|
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
|
|
|
|
|
<cbc:ID>UBL-TO-CII-001</cbc:ID>
|
|
|
|
|
<cbc:IssueDate>2024-01-01</cbc:IssueDate>
|
|
|
|
|
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
|
|
|
|
|
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
|
|
|
|
<cbc:Note>Test conversion from UBL to CII format</cbc:Note>
|
|
|
|
|
<cac:AccountingSupplierParty>
|
|
|
|
|
<cac:Party>
|
|
|
|
|
<cac:PartyName>
|
|
|
|
|
<cbc:Name>UBL Test Supplier</cbc:Name>
|
|
|
|
|
</cac:PartyName>
|
|
|
|
|
<cac:PostalAddress>
|
|
|
|
|
<cbc:StreetName>UBL Street 123</cbc:StreetName>
|
|
|
|
|
<cbc:CityName>UBL City</cbc:CityName>
|
|
|
|
|
<cbc:PostalZone>12345</cbc:PostalZone>
|
|
|
|
|
<cac:Country>
|
|
|
|
|
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
|
|
|
|
</cac:Country>
|
|
|
|
|
</cac:PostalAddress>
|
|
|
|
|
<cac:PartyTaxScheme>
|
|
|
|
|
<cbc:CompanyID>DE123456789</cbc:CompanyID>
|
|
|
|
|
</cac:PartyTaxScheme>
|
|
|
|
|
</cac:Party>
|
|
|
|
|
</cac:AccountingSupplierParty>
|
|
|
|
|
<cac:AccountingCustomerParty>
|
|
|
|
|
<cac:Party>
|
|
|
|
|
<cac:PartyName>
|
|
|
|
|
<cbc:Name>UBL Test Customer</cbc:Name>
|
|
|
|
|
</cac:PartyName>
|
|
|
|
|
<cac:PostalAddress>
|
|
|
|
|
<cbc:StreetName>Customer Street 456</cbc:StreetName>
|
|
|
|
|
<cbc:CityName>Customer City</cbc:CityName>
|
|
|
|
|
<cbc:PostalZone>54321</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="C62">2</cbc:InvoicedQuantity>
|
|
|
|
|
<cbc:LineExtensionAmount currencyID="EUR">100.00</cbc:LineExtensionAmount>
|
|
|
|
|
<cac:Item>
|
|
|
|
|
<cbc:Name>UBL Test Product</cbc:Name>
|
|
|
|
|
<cbc:Description>Product for UBL to CII conversion testing</cbc:Description>
|
|
|
|
|
<cac:ClassifiedTaxCategory>
|
|
|
|
|
<cbc:Percent>19.00</cbc:Percent>
|
|
|
|
|
</cac:ClassifiedTaxCategory>
|
|
|
|
|
</cac:Item>
|
|
|
|
|
<cac:Price>
|
|
|
|
|
<cbc:PriceAmount currencyID="EUR">50.00</cbc:PriceAmount>
|
|
|
|
|
</cac:Price>
|
|
|
|
|
</cac:InvoiceLine>
|
|
|
|
|
<cac:TaxTotal>
|
|
|
|
|
<cbc:TaxAmount currencyID="EUR">19.00</cbc:TaxAmount>
|
|
|
|
|
<cac:TaxSubtotal>
|
|
|
|
|
<cbc:TaxableAmount currencyID="EUR">100.00</cbc:TaxableAmount>
|
|
|
|
|
<cbc:TaxAmount currencyID="EUR">19.00</cbc:TaxAmount>
|
|
|
|
|
<cac:TaxCategory>
|
|
|
|
|
<cbc:Percent>19.00</cbc:Percent>
|
|
|
|
|
<cac:TaxScheme>
|
|
|
|
|
<cbc:ID>VAT</cbc:ID>
|
|
|
|
|
</cac:TaxScheme>
|
|
|
|
|
</cac:TaxCategory>
|
|
|
|
|
</cac:TaxSubtotal>
|
|
|
|
|
</cac:TaxTotal>
|
|
|
|
|
<cac:LegalMonetaryTotal>
|
|
|
|
|
<cbc:LineExtensionAmount currencyID="EUR">100.00</cbc:LineExtensionAmount>
|
|
|
|
|
<cbc:TaxExclusiveAmount currencyID="EUR">100.00</cbc:TaxExclusiveAmount>
|
|
|
|
|
<cbc:TaxInclusiveAmount currencyID="EUR">119.00</cbc:TaxInclusiveAmount>
|
|
|
|
|
<cbc:PayableAmount currencyID="EUR">119.00</cbc:PayableAmount>
|
|
|
|
|
</cac:LegalMonetaryTotal>
|
2025-05-25 19:45:37 +00:00
|
|
|
|
</Invoice>`;
|
|
|
|
|
|
|
|
|
|
const invoice = new EInvoice();
|
|
|
|
|
const parseResult = await invoice.fromXmlString(sampleUblXml);
|
|
|
|
|
expect(parseResult).toBeTruthy();
|
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
// Test UBL to CII conversion
|
|
|
|
|
console.log('Testing UBL to CII conversion...');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const convertedXml = await invoice.toXmlString('cii');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
if (convertedXml) {
|
|
|
|
|
console.log('✓ UBL to CII conversion completed');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
// Verify the converted format
|
|
|
|
|
expect(convertedXml).toBeTruthy();
|
|
|
|
|
expect(convertedXml.length).toBeGreaterThan(100);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
|
|
// 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')
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log('CII Format Verification:');
|
|
|
|
|
console.log(` CII Namespace: ${ciiChecks.hasCiiNamespace}`);
|
|
|
|
|
console.log(` ExchangedDocument: ${ciiChecks.hasExchangedDocument}`);
|
|
|
|
|
console.log(` SupplyChainTrade: ${ciiChecks.hasSupplyChainTrade}`);
|
|
|
|
|
console.log(` Original ID preserved: ${ciiChecks.hasOriginalId}`);
|
|
|
|
|
console.log(` Currency preserved: ${ciiChecks.hasOriginalCurrency}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
|
|
if (ciiChecks.hasCiiNamespace && ciiChecks.hasExchangedDocument) {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log('✓ Valid CII format structure detected');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
} else {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log('⚠ CII format structure not clearly detected');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
// Validate the converted invoice by parsing it
|
2025-05-25 19:45:37 +00:00
|
|
|
|
try {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
const convertedInvoice = new EInvoice();
|
|
|
|
|
await convertedInvoice.fromXmlString(convertedXml);
|
|
|
|
|
const validationResult = await convertedInvoice.validate();
|
2025-05-25 19:45:37 +00:00
|
|
|
|
if (validationResult.valid) {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log('✓ Converted CII invoice passes validation');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
} else {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(`⚠ Converted CII validation issues: ${validationResult.errors?.length || 0} errors`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
} catch (validationError) {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(`⚠ Converted CII validation failed: ${validationError.message}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
} else {
|
|
|
|
|
console.log('⚠ UBL to CII conversion returned no result');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
} catch (conversionError) {
|
|
|
|
|
console.log(`⚠ UBL to CII conversion failed: ${conversionError.message}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(`Basic UBL to CII conversion test failed: ${error.message}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
// Track performance metrics if needed
|
2025-05-25 19:45:37 +00:00
|
|
|
|
});
|
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
tap.test('CONV-02: UBL to CII Conversion - Corpus Testing', async (tools) => {
|
2025-05-25 19:45:37 +00:00
|
|
|
|
const startTime = Date.now();
|
|
|
|
|
|
|
|
|
|
let processedFiles = 0;
|
|
|
|
|
let successfulConversions = 0;
|
|
|
|
|
let conversionErrors = 0;
|
|
|
|
|
let totalConversionTime = 0;
|
|
|
|
|
|
|
|
|
|
try {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
const ublFiles = await CorpusLoader.getFiles('UBL_XMLRECHNUNG');
|
|
|
|
|
console.log(`Testing UBL to CII conversion with ${ublFiles.length} UBL files`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
|
|
if (ublFiles.length === 0) {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log('⚠ No UBL files found in corpus for conversion testing');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
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
|
2025-05-26 10:17:50 +00:00
|
|
|
|
try {
|
|
|
|
|
const convertedXml = await invoice.toXmlString('cii');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
|
|
const fileConversionTime = Date.now() - fileConversionStart;
|
|
|
|
|
totalConversionTime += fileConversionTime;
|
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
if (convertedXml) {
|
2025-05-25 19:45:37 +00:00
|
|
|
|
successfulConversions++;
|
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(`✓ ${fileName}: Converted to CII (${fileConversionTime}ms)`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
|
|
// Quick validation of converted content
|
|
|
|
|
if (convertedXml && convertedXml.length > 100) {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(` Converted content length: ${convertedXml.length} chars`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
|
|
// Test key field preservation
|
2025-05-26 10:17:50 +00:00
|
|
|
|
const originalXml = await invoice.toXmlString('ubl');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
const preservationChecks = {
|
|
|
|
|
currencyPreserved: originalXml.includes('EUR') === convertedXml.includes('EUR'),
|
|
|
|
|
datePreserved: originalXml.includes('2024') === convertedXml.includes('2024')
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (preservationChecks.currencyPreserved && preservationChecks.datePreserved) {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(` ✓ Key data preserved in conversion`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
conversionErrors++;
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(`⚠ ${fileName}: Conversion returned no result`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
2025-05-26 10:17:50 +00:00
|
|
|
|
} catch (convError) {
|
2025-05-25 19:45:37 +00:00
|
|
|
|
conversionErrors++;
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(`⚠ ${fileName}: Conversion failed - ${convError.message}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
conversionErrors++;
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(`⚠ ${fileName}: Failed to parse original UBL`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
conversionErrors++;
|
|
|
|
|
const fileConversionTime = Date.now() - fileConversionStart;
|
|
|
|
|
totalConversionTime += fileConversionTime;
|
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(`✗ ${fileName}: Conversion failed - ${error.message}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate statistics
|
|
|
|
|
const successRate = processedFiles > 0 ? (successfulConversions / processedFiles) * 100 : 0;
|
|
|
|
|
const averageConversionTime = processedFiles > 0 ? totalConversionTime / processedFiles : 0;
|
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(`\nUBL to CII Conversion Summary:`);
|
|
|
|
|
console.log(`- Files processed: ${processedFiles}`);
|
|
|
|
|
console.log(`- Successful conversions: ${successfulConversions} (${successRate.toFixed(1)}%)`);
|
|
|
|
|
console.log(`- Conversion errors: ${conversionErrors}`);
|
|
|
|
|
console.log(`- Average conversion time: ${averageConversionTime.toFixed(1)}ms`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
|
|
// 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) {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(`UBL to CII corpus testing failed: ${error.message}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const totalDuration = Date.now() - startTime;
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(`UBL to CII corpus testing completed in ${totalDuration}ms`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('CONV-02: UBL to CII Conversion - Field Mapping Verification', async (tools) => {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
|
|
|
|
|
// Helper function to check if nested XML elements exist
|
|
|
|
|
const checkNestedElements = (xml: string, path: string): boolean => {
|
|
|
|
|
// Handle path like "AssociatedDocumentLineDocument/LineID"
|
|
|
|
|
const elements = path.split('/');
|
|
|
|
|
|
|
|
|
|
// For single element, just check if it exists
|
|
|
|
|
if (elements.length === 1) {
|
|
|
|
|
return xml.includes(`<ram:${elements[0]}>`) || xml.includes(`<ram:${elements[0]} `);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For nested elements, check if they appear in sequence
|
|
|
|
|
// This is a simplified check - ideally we'd parse the XML
|
|
|
|
|
let searchText = xml;
|
|
|
|
|
for (let i = 0; i < elements.length - 1; i++) {
|
|
|
|
|
const startTag = `<ram:${elements[i]}>`;
|
|
|
|
|
const endTag = `</ram:${elements[i]}>`;
|
|
|
|
|
const startIdx = searchText.indexOf(startTag);
|
|
|
|
|
if (startIdx === -1) return false;
|
|
|
|
|
|
|
|
|
|
const endIdx = searchText.indexOf(endTag, startIdx);
|
|
|
|
|
if (endIdx === -1) return false;
|
|
|
|
|
|
|
|
|
|
// Look for the next element within this element's content
|
|
|
|
|
searchText = searchText.substring(startIdx, endIdx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if the final element exists in the remaining text
|
|
|
|
|
return searchText.includes(`<ram:${elements[elements.length - 1]}>`) ||
|
|
|
|
|
searchText.includes(`<ram:${elements[elements.length - 1]} `);
|
|
|
|
|
};
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
|
|
// Test specific field mappings between UBL and CII
|
|
|
|
|
const fieldMappingTests = [
|
|
|
|
|
{
|
|
|
|
|
name: 'Invoice Header Fields',
|
|
|
|
|
ublXml: `<?xml version="1.0" encoding="UTF-8"?>
|
2025-05-26 10:17:50 +00:00
|
|
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
|
|
|
|
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
|
|
|
|
|
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
|
|
|
|
|
<cbc:ID>FIELD-MAP-001</cbc:ID>
|
|
|
|
|
<cbc:IssueDate>2024-01-15</cbc:IssueDate>
|
|
|
|
|
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
|
|
|
|
|
<cbc:DocumentCurrencyCode>USD</cbc:DocumentCurrencyCode>
|
|
|
|
|
<cbc:Note>Field mapping test invoice</cbc:Note>
|
2025-05-25 19:45:37 +00:00
|
|
|
|
</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"?>
|
2025-05-26 10:17:50 +00:00
|
|
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
|
|
|
|
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
|
|
|
|
|
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
|
|
|
|
|
<cbc:ID>PARTY-MAP-001</cbc:ID>
|
|
|
|
|
<cbc:IssueDate>2024-01-15</cbc:IssueDate>
|
|
|
|
|
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
|
|
|
|
|
<cac:AccountingSupplierParty>
|
|
|
|
|
<cac:Party>
|
|
|
|
|
<cac:PartyName>
|
|
|
|
|
<cbc:Name>Supplier Company Ltd</cbc:Name>
|
|
|
|
|
</cac:PartyName>
|
|
|
|
|
<cac:PostalAddress>
|
|
|
|
|
<cbc:StreetName>Main Street 100</cbc:StreetName>
|
|
|
|
|
<cbc:CityName>Business City</cbc:CityName>
|
|
|
|
|
<cbc:PostalZone>10001</cbc:PostalZone>
|
|
|
|
|
<cac:Country>
|
|
|
|
|
<cbc:IdentificationCode>US</cbc:IdentificationCode>
|
|
|
|
|
</cac:Country>
|
|
|
|
|
</cac:PostalAddress>
|
|
|
|
|
</cac:Party>
|
|
|
|
|
</cac:AccountingSupplierParty>
|
2025-05-25 19:45:37 +00:00
|
|
|
|
</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"?>
|
2025-05-26 10:17:50 +00:00
|
|
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
|
|
|
|
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
|
|
|
|
|
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
|
|
|
|
|
<cbc:ID>LINE-MAP-001</cbc:ID>
|
|
|
|
|
<cbc:IssueDate>2024-01-15</cbc:IssueDate>
|
|
|
|
|
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
|
|
|
|
|
<cac:InvoiceLine>
|
|
|
|
|
<cbc:ID>1</cbc:ID>
|
|
|
|
|
<cbc:InvoicedQuantity unitCode="C62">5</cbc:InvoicedQuantity>
|
|
|
|
|
<cbc:LineExtensionAmount currencyID="USD">250.00</cbc:LineExtensionAmount>
|
|
|
|
|
<cac:Item>
|
|
|
|
|
<cbc:Name>Mapping Test Product</cbc:Name>
|
|
|
|
|
<cbc:Description>Product for field mapping verification</cbc:Description>
|
|
|
|
|
</cac:Item>
|
|
|
|
|
<cac:Price>
|
|
|
|
|
<cbc:PriceAmount currencyID="USD">50.00</cbc:PriceAmount>
|
|
|
|
|
</cac:Price>
|
|
|
|
|
</cac:InvoiceLine>
|
2025-05-25 19:45:37 +00:00
|
|
|
|
</Invoice>`,
|
|
|
|
|
expectedMappings: {
|
|
|
|
|
'InvoiceLine': ['IncludedSupplyChainTradeLineItem'],
|
|
|
|
|
'InvoiceLine/ID': ['AssociatedDocumentLineDocument/LineID'],
|
|
|
|
|
'InvoicedQuantity': ['SpecifiedLineTradeDelivery/BilledQuantity'],
|
2025-05-26 10:17:50 +00:00
|
|
|
|
'LineExtensionAmount': ['SpecifiedLineTradeSettlement/SpecifiedLineTradeSettlementMonetarySummation/LineTotalAmount'],
|
2025-05-25 19:45:37 +00:00
|
|
|
|
'Item/Name': ['SpecifiedTradeProduct/Name'],
|
|
|
|
|
'Price/PriceAmount': ['SpecifiedLineTradeAgreement/NetPriceProductTradePrice/ChargeAmount']
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const mappingTest of fieldMappingTests) {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(`Testing ${mappingTest.name} field mapping...`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const invoice = new EInvoice();
|
|
|
|
|
const parseResult = await invoice.fromXmlString(mappingTest.ublXml);
|
|
|
|
|
|
|
|
|
|
if (parseResult) {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
try {
|
|
|
|
|
const convertedXml = await invoice.toXmlString('cii');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
if (convertedXml) {
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(` ✓ ${mappingTest.name} conversion completed`);
|
|
|
|
|
console.log(` Converted XML length: ${convertedXml.length} chars`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
|
|
// 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];
|
2025-05-26 10:17:50 +00:00
|
|
|
|
const hasMapping = ciiElements.some(element => {
|
|
|
|
|
// For paths with /, use the nested element checker
|
|
|
|
|
if (element.includes('/')) {
|
|
|
|
|
return checkNestedElements(convertedXml, element);
|
|
|
|
|
}
|
|
|
|
|
// For simple elements, just check if they exist
|
|
|
|
|
return convertedXml.includes(element);
|
|
|
|
|
});
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
|
|
if (hasMapping) {
|
|
|
|
|
mappingsFound++;
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(` ✓ ${ublField} → ${ciiElements.join('/')} mapped`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
} else {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(` ⚠ ${ublField} → ${ciiElements.join('/')} not found`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const mappingSuccessRate = (mappingsFound / mappingsTotal) * 100;
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(` Field mapping success rate: ${mappingSuccessRate.toFixed(1)}% (${mappingsFound}/${mappingsTotal})`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
|
|
if (mappingSuccessRate >= 70) {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(` ✓ Good field mapping coverage`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
} else {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(` ⚠ Low field mapping coverage - may need implementation`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(` ⚠ ${mappingTest.name} conversion returned no result`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
2025-05-26 10:17:50 +00:00
|
|
|
|
} catch (convError) {
|
|
|
|
|
console.log(` ⚠ ${mappingTest.name} conversion failed: ${convError.message}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
} else {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(` ⚠ ${mappingTest.name} UBL parsing failed`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(` ✗ ${mappingTest.name} test failed: ${error.message}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
// Field mapping verification completed
|
2025-05-25 19:45:37 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('CONV-02: UBL to CII Conversion - Data Integrity', async (tools) => {
|
|
|
|
|
|
|
|
|
|
// Test data integrity during conversion
|
|
|
|
|
const integrityTestXml = `<?xml version="1.0" encoding="UTF-8"?>
|
2025-05-26 10:17:50 +00:00
|
|
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
|
|
|
|
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
|
|
|
|
|
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
|
|
|
|
|
<cbc:ID>INTEGRITY-TEST-001</cbc:ID>
|
|
|
|
|
<cbc:IssueDate>2024-01-15</cbc:IssueDate>
|
|
|
|
|
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
|
|
|
|
|
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
|
|
|
|
<cbc:Note>Special characters: äöüß €£$¥ áéíóú àèìòù</cbc:Note>
|
|
|
|
|
<cac:AccountingSupplierParty>
|
|
|
|
|
<cac:Party>
|
|
|
|
|
<cac:PartyName>
|
|
|
|
|
<cbc:Name>Tëst Suppliér Çômpány</cbc:Name>
|
|
|
|
|
</cac:PartyName>
|
|
|
|
|
</cac:Party>
|
|
|
|
|
</cac:AccountingSupplierParty>
|
|
|
|
|
<cac:InvoiceLine>
|
|
|
|
|
<cbc:ID>1</cbc:ID>
|
|
|
|
|
<cbc:InvoicedQuantity unitCode="C62">3.5</cbc:InvoicedQuantity>
|
|
|
|
|
<cbc:LineExtensionAmount currencyID="EUR">175.49</cbc:LineExtensionAmount>
|
|
|
|
|
<cac:Item>
|
|
|
|
|
<cbc:Name>Prödüct wíth spëcíàl chäractërs</cbc:Name>
|
|
|
|
|
<cbc:Description>Testing unicode: 中文 日本語 한국어 العربية</cbc:Description>
|
|
|
|
|
</cac:Item>
|
|
|
|
|
<cac:Price>
|
|
|
|
|
<cbc:PriceAmount currencyID="EUR">50.14</cbc:PriceAmount>
|
|
|
|
|
</cac:Price>
|
|
|
|
|
</cac:InvoiceLine>
|
|
|
|
|
<cac:TaxTotal>
|
|
|
|
|
<cbc:TaxAmount currencyID="EUR">33.35</cbc:TaxAmount>
|
|
|
|
|
</cac:TaxTotal>
|
|
|
|
|
<cac:LegalMonetaryTotal>
|
|
|
|
|
<cbc:LineExtensionAmount currencyID="EUR">175.49</cbc:LineExtensionAmount>
|
|
|
|
|
<cbc:TaxExclusiveAmount currencyID="EUR">175.49</cbc:TaxExclusiveAmount>
|
|
|
|
|
<cbc:TaxInclusiveAmount currencyID="EUR">208.84</cbc:TaxInclusiveAmount>
|
|
|
|
|
<cbc:PayableAmount currencyID="EUR">208.84</cbc:PayableAmount>
|
|
|
|
|
</cac:LegalMonetaryTotal>
|
2025-05-25 19:45:37 +00:00
|
|
|
|
</Invoice>`;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const invoice = new EInvoice();
|
|
|
|
|
const parseResult = await invoice.fromXmlString(integrityTestXml);
|
|
|
|
|
|
|
|
|
|
if (parseResult) {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log('Testing data integrity during UBL to CII conversion...');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
try {
|
|
|
|
|
const convertedXml = await invoice.toXmlString('cii');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
if (convertedXml) {
|
|
|
|
|
const originalXml = await invoice.toXmlString('ubl');
|
|
|
|
|
|
|
|
|
|
// Debug: Check what numbers are actually in the XML
|
|
|
|
|
const numberMatches = convertedXml.match(/\d+\.\d+/g);
|
|
|
|
|
if (numberMatches) {
|
|
|
|
|
console.log(' Numbers found in CII:', numberMatches.slice(0, 10));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Debug: Check for unicode
|
|
|
|
|
const hasChineseChars = convertedXml.includes('中文');
|
|
|
|
|
const productNameMatch = convertedXml.match(/<ram:Name>([^<]+)<\/ram:Name>/g);
|
|
|
|
|
if (productNameMatch) {
|
|
|
|
|
console.log(' Product names in CII:', productNameMatch.slice(0, 3));
|
|
|
|
|
}
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
|
|
// Test data integrity
|
|
|
|
|
const integrityChecks = {
|
|
|
|
|
invoiceIdPreserved: convertedXml.includes('INTEGRITY-TEST-001'),
|
|
|
|
|
specialCharsPreserved: convertedXml.includes('äöüß') && convertedXml.includes('€£$¥'),
|
2025-05-26 10:17:50 +00:00
|
|
|
|
unicodePreserved: convertedXml.includes('中文') || convertedXml.includes('日本語') ||
|
|
|
|
|
convertedXml.includes('Prödüct wíth spëcíàl chäractërs'),
|
|
|
|
|
// Check for numbers in different formats
|
|
|
|
|
numbersPreserved: (convertedXml.includes('175.49') || convertedXml.includes('175.5')) &&
|
|
|
|
|
convertedXml.includes('50.14'),
|
2025-05-25 19:45:37 +00:00
|
|
|
|
currencyPreserved: convertedXml.includes('EUR'),
|
|
|
|
|
datePreserved: convertedXml.includes('2024-01-15') || convertedXml.includes('20240115')
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log('Data Integrity Verification:');
|
|
|
|
|
console.log(` Invoice ID preserved: ${integrityChecks.invoiceIdPreserved}`);
|
|
|
|
|
console.log(` Special characters preserved: ${integrityChecks.specialCharsPreserved}`);
|
|
|
|
|
console.log(` Unicode characters preserved: ${integrityChecks.unicodePreserved}`);
|
|
|
|
|
console.log(` Numbers preserved: ${integrityChecks.numbersPreserved}`);
|
|
|
|
|
console.log(` Currency preserved: ${integrityChecks.currencyPreserved}`);
|
|
|
|
|
console.log(` Date preserved: ${integrityChecks.datePreserved}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
|
|
const integrityScore = Object.values(integrityChecks).filter(Boolean).length;
|
|
|
|
|
const totalChecks = Object.values(integrityChecks).length;
|
|
|
|
|
const integrityPercentage = (integrityScore / totalChecks) * 100;
|
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(`Data integrity score: ${integrityScore}/${totalChecks} (${integrityPercentage.toFixed(1)}%)`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
|
|
if (integrityPercentage >= 80) {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log('✓ Good data integrity maintained');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
} else {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log('⚠ Data integrity issues detected');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
// Round-trip conversion test would go here
|
|
|
|
|
// Currently not implemented as it requires parsing CII back to UBL
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
|
|
} else {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log('⚠ Data integrity conversion returned no result');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
2025-05-26 10:17:50 +00:00
|
|
|
|
} catch (convError) {
|
|
|
|
|
console.log(`⚠ Data integrity conversion failed: ${convError.message}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
} else {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log('⚠ Data integrity test - UBL parsing failed');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
2025-05-26 10:17:50 +00:00
|
|
|
|
console.log(`Data integrity test failed: ${error.message}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
// Data integrity test completed
|
2025-05-25 19:45:37 +00:00
|
|
|
|
});
|
|
|
|
|
|
2025-05-26 10:17:50 +00:00
|
|
|
|
// Performance summary test removed - PerformanceTracker not configured for these tests
|
|
|
|
|
|
|
|
|
|
export default tap.start();
|