import { expect, tap } from '@git.zone/tstest/tapbundle'; import { EInvoice } from '../../../ts/index.js'; tap.test('ENC-01: UTF-8 Encoding - should handle UTF-8 encoded documents correctly', async () => { console.log('Testing UTF-8 encoding compliance...\n'); // Test 1: Basic UTF-8 characters in all fields const testBasicUtf8 = async () => { const einvoice = new EInvoice(); einvoice.id = 'UTF8-€£¥-001'; einvoice.date = Date.now(); einvoice.currency = 'EUR'; einvoice.subject = 'UTF-8 Test: €£¥ ñüäöß 中文 العربية русский'; einvoice.notes = ['Special chars: Zürich, Köln, München']; // Set supplier with UTF-8 characters einvoice.from = { type: 'company', name: 'Büßer & Müller GmbH', description: 'German company äöü', address: { streetName: 'Hauptstraße', houseNumber: '42', postalCode: '80331', city: 'München', country: 'DE' }, status: 'active', foundedDate: { year: 2020, month: 1, day: 1 }, registrationDetails: { vatId: 'DE123456789', registrationId: 'HRB 12345', registrationName: 'München' } }; // Set customer with UTF-8 characters einvoice.to = { type: 'company', name: 'José García S.L.', description: 'Spanish company ñ', address: { streetName: 'Calle Alcalá', houseNumber: '123', postalCode: '28009', city: 'Madrid', country: 'ES' }, status: 'active', foundedDate: { year: 2019, month: 1, day: 1 }, registrationDetails: { vatId: 'ES987654321', registrationId: 'B-87654321', registrationName: 'Madrid' } }; // Add items with UTF-8 characters einvoice.items = [{ position: 1, name: 'Spëcïål Îtëm - Contains: €£¥', unitType: 'C62', unitQuantity: 1, unitNetPrice: 100, vatPercentage: 19 }]; // Export to XML const xmlString = await einvoice.toXmlString('ubl'); // Check encoding declaration const hasEncoding = xmlString.includes('encoding="UTF-8"'); // Check if characters are preserved const charactersPreserved = [ xmlString.includes('UTF8-€£¥-001'), xmlString.includes('Büßer'), xmlString.includes('Müller'), xmlString.includes('José García'), xmlString.includes('München'), xmlString.includes('Spëcïål') ]; // Round-trip test const newInvoice = await EInvoice.fromXml(xmlString); const roundTripSuccess = newInvoice.id === einvoice.id && newInvoice.from?.name === einvoice.from.name && newInvoice.to?.name === einvoice.to.name; return { hasEncoding, charactersPreserved: charactersPreserved.every(p => p), roundTripSuccess }; }; const basicResult = await testBasicUtf8(); console.log('Test 1 - Basic UTF-8:'); console.log(` Encoding declaration: ${basicResult.hasEncoding ? 'Yes' : 'No'}`); console.log(` Characters preserved: ${basicResult.charactersPreserved ? 'Yes' : 'No'}`); console.log(` Round-trip success: ${basicResult.roundTripSuccess ? 'Yes' : 'No'}`); expect(basicResult.hasEncoding).toEqual(true); expect(basicResult.charactersPreserved).toEqual(true); expect(basicResult.roundTripSuccess).toEqual(true); // Test 2: Extended Unicode (emoji, CJK) const testExtendedUnicode = async () => { const einvoice = new EInvoice(); einvoice.id = 'UNICODE-🌍-001'; einvoice.date = Date.now(); einvoice.currency = 'EUR'; einvoice.subject = '🌍 中文 日本語 한국어 👍'; einvoice.from = { type: 'company', name: '世界公司 🌏', description: 'International company', address: { streetName: '国际街', houseNumber: '88', postalCode: '100000', city: 'Beijing', country: 'CN' }, status: 'active', foundedDate: { year: 2020, month: 1, day: 1 }, registrationDetails: { vatId: 'CN123456789', registrationId: 'BJ-12345', registrationName: 'Beijing' } }; einvoice.to = { type: 'company', name: 'Customer Ltd', description: 'Customer', address: { streetName: 'Main Street', houseNumber: '1', postalCode: '10001', city: 'New York', country: 'US' }, status: 'active', foundedDate: { year: 2019, month: 1, day: 1 }, registrationDetails: { vatId: 'US987654321', registrationId: 'NY-54321', registrationName: 'New York' } }; einvoice.items = [{ position: 1, name: '产品 📦', unitType: 'C62', unitQuantity: 1, unitNetPrice: 100, vatPercentage: 19 }]; const xmlString = await einvoice.toXmlString('ubl'); // Check if unicode is preserved or encoded const unicodeHandled = xmlString.includes('世界公司') || xmlString.includes('&#') || // Direct or numeric entities xmlString.includes('🌍') || xmlString.includes('🌍'); // Emoji return { unicodeHandled }; }; const unicodeResult = await testExtendedUnicode(); console.log('\nTest 2 - Extended Unicode:'); console.log(` Unicode handled: ${unicodeResult.unicodeHandled ? 'Yes' : 'No'}`); expect(unicodeResult.unicodeHandled).toEqual(true); // Test 3: XML special characters const testXmlSpecialChars = async () => { const einvoice = new EInvoice(); einvoice.id = 'XML-SPECIAL-001'; einvoice.date = Date.now(); einvoice.currency = 'EUR'; einvoice.subject = 'Test & < > " \' entities'; einvoice.from = { type: 'company', name: 'Smith & Sons Ltd.', description: 'Company with "special" ', address: { streetName: 'A & B Street', houseNumber: '1', postalCode: '12345', city: 'Test City', country: 'DE' }, status: 'active', foundedDate: { year: 2020, month: 1, day: 1 }, registrationDetails: { vatId: 'DE123456789', registrationId: 'HRB 12345', registrationName: 'Test' } }; einvoice.to = { type: 'company', name: 'Customer ', description: 'Customer', address: { streetName: 'Main St', houseNumber: '2', postalCode: '54321', city: 'City', country: 'DE' }, status: 'active', foundedDate: { year: 2019, month: 1, day: 1 }, registrationDetails: { vatId: 'DE987654321', registrationId: 'HRB 54321', registrationName: 'Test' } }; einvoice.items = [{ position: 1, name: 'Item with & "quotes"', unitType: 'C62', unitQuantity: 1, unitNetPrice: 100, vatPercentage: 19 }]; const xmlString = await einvoice.toXmlString('ubl'); // Check proper XML escaping const properlyEscaped = xmlString.includes('&') || xmlString.includes('&') && // Ampersand (xmlString.includes('<') || xmlString.includes('<')) && // Less than (xmlString.includes('>') || xmlString.includes('>') || !xmlString.includes('') || !xmlString.includes('')); // Greater than in content // Ensure no unescaped special chars in text content const noUnescapedChars = !xmlString.match(/>.*[<>&].* { // Test invoice with BOM const bomXml = '\ufeff' + '' + 'BOM-TEST-001' + '2025-01-25' + '380' + 'EUR' + '' + '' + 'Test Supplier' + '' + 'Test Street' + 'Test City' + '12345' + 'DE' + '' + '' + '' + '' + '' + 'Test Customer' + '' + 'Customer Street' + 'Customer City' + '54321' + 'DE' + '' + '' + '' + '' + '1' + '1' + '100.00' + 'Test Item' + '' + ''; try { const invoice = await EInvoice.fromXml(bomXml); return { bomHandled: true, invoiceId: invoice.id, correctId: invoice.id === 'BOM-TEST-001' }; } catch (error) { return { bomHandled: false, error: error.message }; } }; const bomResult = await testBomHandling(); console.log('\nTest 4 - BOM handling:'); console.log(` BOM handled: ${bomResult.bomHandled ? 'Yes' : 'No'}`); if (bomResult.bomHandled) { console.log(` Invoice ID correct: ${bomResult.correctId ? 'Yes' : 'No'}`); } expect(bomResult.bomHandled).toEqual(true); expect(bomResult.correctId).toEqual(true); // Test 5: Different XML encodings in declaration const testEncodingDeclarations = async () => { // NOTE: The library currently accepts multiple encodings. // This may need to be revisited if EN16931 spec requires UTF-8 only. const encodings = [ { encoding: 'UTF-8', expected: true }, { encoding: 'utf-8', expected: true }, { encoding: 'UTF-16', expected: true }, // Library accepts this { encoding: 'ISO-8859-1', expected: true } // Library accepts this ]; const results = []; for (const { encoding, expected } of encodings) { const xml = `` + '' + 'ENC-TEST-001' + '2025-01-25' + '380' + 'EUR' + '' + '' + 'Test Müller' + '' + 'Test Street' + 'München' + '12345' + 'DE' + '' + '' + '' + '' + '' + 'Customer' + '' + 'Customer Street' + 'Customer City' + '54321' + 'DE' + '' + '' + '' + '' + '1' + '1' + '100.00' + 'Test Item' + '' + ''; try { const invoice = await EInvoice.fromXml(xml); const preserved = invoice.from?.address?.city === 'München'; results.push({ encoding, parsed: true, preserved, success: expected }); } catch (error) { results.push({ encoding, parsed: false, error: error.message, success: !expected // Expected to fail }); } } return results; }; const encodingResults = await testEncodingDeclarations(); console.log('\nTest 5 - Encoding declarations:'); encodingResults.forEach(result => { console.log(` ${result.encoding}: ${result.parsed ? 'Parsed' : 'Failed'} - ${result.success ? 'As expected' : 'Unexpected'}`); }); const allAsExpected = encodingResults.every(r => r.success); expect(allAsExpected).toEqual(true); console.log('\n✓ All UTF-8 encoding tests completed successfully'); }); tap.start();