import { tap, expect } from '@git.zone/tstest/tapbundle'; import { EInvoice } from '../../../ts/index.js'; import { InvoiceFormat, ValidationLevel } from '../../../ts/interfaces/common.js'; import { CorpusLoader, PerformanceTracker } from '../../helpers/test-utils.js'; import * as path from 'path'; /** * Test ID: STD-07 * Test Description: UBL 2.1 Compliance * Priority: High * * This test validates compliance with the OASIS UBL 2.1 standard, * ensuring proper namespace handling, element ordering, and schema validation. */ tap.test('STD-07: UBL 2.1 Compliance - should validate UBL 2.1 standard compliance', async (t) => { // Test data for UBL 2.1 compliance checks const ublNamespaces = { invoice: 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2', creditNote: 'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2', cac: 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2', cbc: 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2' }; // Test 1: Namespace Declaration Compliance t.test('UBL 2.1 namespace declarations', async (st) => { const ublFiles = await CorpusLoader.getFiles('UBL_XMLRECHNUNG'); const testFiles = ublFiles.slice(0, 5); // Test first 5 files for (const file of testFiles) { const xmlBuffer = await CorpusLoader.loadFile(file); const xmlString = xmlBuffer.toString('utf-8'); // Check for proper namespace declarations const hasInvoiceNS = xmlString.includes(ublNamespaces.invoice) || xmlString.includes(ublNamespaces.creditNote); const hasCACNS = xmlString.includes(ublNamespaces.cac); const hasCBCNS = xmlString.includes(ublNamespaces.cbc); expect(hasInvoiceNS).toBeTrue(); expect(hasCACNS).toBeTrue(); expect(hasCBCNS).toBeTrue(); st.pass(`✓ ${path.basename(file)}: Correct UBL 2.1 namespaces`); } }); // Test 2: Required Elements Structure t.test('UBL 2.1 required elements structure', async (st) => { const requiredElements = [ 'UBLVersionID', 'ID', 'IssueDate', 'InvoiceTypeCode', 'DocumentCurrencyCode', 'AccountingSupplierParty', 'AccountingCustomerParty', 'LegalMonetaryTotal', 'InvoiceLine' ]; const testInvoice = new EInvoice(); testInvoice.id = 'UBL-TEST-001'; testInvoice.issueDate = new Date(); testInvoice.currency = 'EUR'; testInvoice.from = { name: 'Test Supplier', address: { country: 'DE' }, vatNumber: 'DE123456789' }; testInvoice.to = { name: 'Test Customer', address: { country: 'DE' } }; testInvoice.items = [{ name: 'Test Item', quantity: 1, unitPrice: 100, taxPercent: 19 }]; const ublXml = await testInvoice.toXmlString('ubl'); // Check for required elements for (const element of requiredElements) { const hasElement = ublXml.includes(` { const invoice = new EInvoice(); invoice.id = 'ORDER-TEST-001'; invoice.issueDate = new Date(); invoice.from = { name: 'Seller', address: { country: 'DE' } }; invoice.to = { name: 'Buyer', address: { country: 'DE' } }; invoice.items = [{ name: 'Item', quantity: 1, unitPrice: 100 }]; const xml = await invoice.toXmlString('ubl'); // Check element order (simplified check) const ublVersionPos = xml.indexOf('UBLVersionID'); const idPos = xml.indexOf(''); const issueDatePos = xml.indexOf('IssueDate'); const supplierPos = xml.indexOf('AccountingSupplierParty'); const customerPos = xml.indexOf('AccountingCustomerParty'); // UBL requires specific ordering expect(ublVersionPos).toBeLessThan(idPos); expect(idPos).toBeLessThan(issueDatePos); expect(supplierPos).toBeLessThan(customerPos); st.pass('✓ UBL 2.1 element ordering is correct'); }); // Test 4: Data Type Compliance t.test('UBL 2.1 data type compliance', async (st) => { const testCases = [ { field: 'IssueDate', value: '2024-01-15', pattern: /\d{4}-\d{2}-\d{2}/ }, { field: 'DocumentCurrencyCode', value: 'EUR', pattern: /^[A-Z]{3}$/ }, { field: 'InvoiceTypeCode', value: '380', pattern: /^\d{3}$/ }, { field: 'Quantity', value: '10.00', pattern: /^\d+\.\d{2}$/ } ]; const invoice = new EInvoice(); invoice.id = 'DATATYPE-TEST'; invoice.issueDate = new Date('2024-01-15'); invoice.currency = 'EUR'; invoice.from = { name: 'Test', address: { country: 'DE' } }; invoice.to = { name: 'Test', address: { country: 'DE' } }; invoice.items = [{ name: 'Item', quantity: 10, unitPrice: 100 }]; const xml = await invoice.toXmlString('ubl'); for (const test of testCases) { const fieldMatch = xml.match(new RegExp(`]*>([^<]+)`)); if (fieldMatch) { expect(test.pattern.test(fieldMatch[1])).toBeTrue(); st.pass(`✓ ${test.field}: Correct data type format`); } } }); // Test 5: Extension Point Compliance t.test('UBL 2.1 extension point handling', async (st) => { const invoice = new EInvoice(); invoice.id = 'EXT-TEST-001'; invoice.issueDate = new Date(); invoice.from = { name: 'Test', address: { country: 'DE' } }; invoice.to = { name: 'Test', address: { country: 'DE' } }; invoice.items = [{ name: 'Item', quantity: 1, unitPrice: 100 }]; // Add custom extension data invoice.metadata = { format: InvoiceFormat.UBL, extensions: { 'CustomField': 'CustomValue' } }; const xml = await invoice.toXmlString('ubl'); // UBL allows extensions through UBLExtensions element const hasExtensionCapability = xml.includes('UBLExtensions') || xml.includes('') || !xml.includes('CustomField'); // Should not appear in main body expect(hasExtensionCapability).toBeTrue(); st.pass('✓ UBL 2.1 extension handling is compliant'); }); // Test 6: Codelist Compliance t.test('UBL 2.1 codelist compliance', async (st) => { const validCodes = { currencyCode: ['EUR', 'USD', 'GBP', 'CHF'], countryCode: ['DE', 'FR', 'IT', 'ES', 'NL'], taxCategoryCode: ['S', 'Z', 'E', 'AE', 'K'], invoiceTypeCode: ['380', '381', '384', '389'] }; // Test valid codes for (const [codeType, codes] of Object.entries(validCodes)) { for (const code of codes) { // Simple validation - in real implementation would check against full codelist expect(code.length).toBeGreaterThan(0); st.pass(`✓ Valid ${codeType}: ${code}`); } } }); // Performance tracking const perfSummary = await PerformanceTracker.getSummary('ubl-compliance'); if (perfSummary) { console.log('\nUBL 2.1 Compliance Test Performance:'); console.log(` Average: ${perfSummary.average.toFixed(2)}ms`); console.log(` Min: ${perfSummary.min.toFixed(2)}ms`); console.log(` Max: ${perfSummary.max.toFixed(2)}ms`); } }); tap.start();