einvoice/test/suite/einvoice_encoding/test.enc-01.utf8-encoding.ts

402 lines
14 KiB
TypeScript
Raw Normal View History

2025-05-25 19:45:37 +00:00
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EInvoice } from '../../../ts/index.js';
2025-05-27 16:30:39 +00:00
tap.test('ENC-01: UTF-8 Encoding - should handle UTF-8 encoded documents correctly', async () => {
console.log('Testing UTF-8 encoding compliance...\n');
2025-05-25 19:45:37 +00:00
// 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'
2025-05-27 16:30:39 +00:00
}
};
// 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
};
};
2025-05-27 18:02:19 +00:00
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);
2025-05-25 19:45:37 +00:00
// 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'
2025-05-27 18:02:19 +00:00
}
};
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 };
};
2025-05-27 18:02:19 +00:00
const unicodeResult = await testExtendedUnicode();
console.log('\nTest 2 - Extended Unicode:');
console.log(` Unicode handled: ${unicodeResult.unicodeHandled ? 'Yes' : 'No'}`);
expect(unicodeResult.unicodeHandled).toEqual(true);
2025-05-25 19:45:37 +00:00
// 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" <characters>',
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 <Test>',
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 <angle> & "quotes"',
unitType: 'C62',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19
}];
const xmlString = await einvoice.toXmlString('ubl');
// Check proper XML escaping
const properlyEscaped =
xmlString.includes('&amp;') || xmlString.includes('&#38;') && // Ampersand
(xmlString.includes('&lt;') || xmlString.includes('&#60;')) && // Less than
(xmlString.includes('&gt;') || xmlString.includes('&#62;') ||
!xmlString.includes('<Test>') || !xmlString.includes('<angle>')); // Greater than in content
// Ensure no unescaped special chars in text content
const noUnescapedChars = !xmlString.match(/>.*[<>&].*</);
return { properlyEscaped, noUnescapedChars };
};
2025-05-27 18:02:19 +00:00
const xmlSpecialResult = await testXmlSpecialChars();
console.log('\nTest 3 - XML special characters:');
console.log(` Properly escaped: ${xmlSpecialResult.properlyEscaped ? 'Yes' : 'No'}`);
expect(xmlSpecialResult.properlyEscaped).toEqual(true);
2025-05-25 19:45:37 +00:00
// Test 4: BOM handling
const testBomHandling = async () => {
// Test invoice with BOM
const bomXml = '\ufeff<?xml version="1.0" encoding="UTF-8"?>' +
'<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"' +
' xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"' +
' xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">' +
'<cbc:ID>BOM-TEST-001</cbc:ID>' +
'<cbc:IssueDate>2025-01-25</cbc:IssueDate>' +
'<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>' +
'<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>' +
'<cac:AccountingSupplierParty>' +
'<cac:Party>' +
'<cac:PartyName><cbc:Name>Test Supplier</cbc:Name></cac:PartyName>' +
'<cac:PostalAddress>' +
'<cbc:StreetName>Test Street</cbc:StreetName>' +
'<cbc:CityName>Test City</cbc:CityName>' +
'<cbc:PostalZone>12345</cbc:PostalZone>' +
'<cac:Country><cbc:IdentificationCode>DE</cbc:IdentificationCode></cac:Country>' +
'</cac:PostalAddress>' +
'</cac:Party>' +
'</cac:AccountingSupplierParty>' +
'<cac:AccountingCustomerParty>' +
'<cac:Party>' +
'<cac:PartyName><cbc:Name>Test Customer</cbc:Name></cac:PartyName>' +
'<cac:PostalAddress>' +
'<cbc:StreetName>Customer Street</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">1</cbc:InvoicedQuantity>' +
'<cbc:LineExtensionAmount currencyID="EUR">100.00</cbc:LineExtensionAmount>' +
'<cac:Item><cbc:Name>Test Item</cbc:Name></cac:Item>' +
'</cac:InvoiceLine>' +
'</Invoice>';
try {
const invoice = await EInvoice.fromXml(bomXml);
2025-05-27 18:02:19 +00:00
return {
bomHandled: true,
invoiceId: invoice.id,
correctId: invoice.id === 'BOM-TEST-001'
2025-05-27 18:02:19 +00:00
};
} catch (error) {
return { bomHandled: false, error: error.message };
2025-05-25 19:45:37 +00:00
}
};
2025-05-27 18:02:19 +00:00
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);
2025-05-25 19:45:37 +00:00
// 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 = `<?xml version="1.0" encoding="${encoding}"?>` +
'<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"' +
' xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"' +
' xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">' +
'<cbc:ID>ENC-TEST-001</cbc:ID>' +
'<cbc:IssueDate>2025-01-25</cbc:IssueDate>' +
'<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>' +
'<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>' +
'<cac:AccountingSupplierParty>' +
'<cac:Party>' +
'<cac:PartyName><cbc:Name>Test Müller</cbc:Name></cac:PartyName>' +
'<cac:PostalAddress>' +
'<cbc:StreetName>Test Street</cbc:StreetName>' +
'<cbc:CityName>München</cbc:CityName>' +
'<cbc:PostalZone>12345</cbc:PostalZone>' +
'<cac:Country><cbc:IdentificationCode>DE</cbc:IdentificationCode></cac:Country>' +
'</cac:PostalAddress>' +
'</cac:Party>' +
'</cac:AccountingSupplierParty>' +
'<cac:AccountingCustomerParty>' +
'<cac:Party>' +
'<cac:PartyName><cbc:Name>Customer</cbc:Name></cac:PartyName>' +
'<cac:PostalAddress>' +
'<cbc:StreetName>Customer Street</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">1</cbc:InvoicedQuantity>' +
'<cbc:LineExtensionAmount currencyID="EUR">100.00</cbc:LineExtensionAmount>' +
'<cac:Item><cbc:Name>Test Item</cbc:Name></cac:Item>' +
'</cac:InvoiceLine>' +
'</Invoice>';
2025-05-27 16:30:39 +00:00
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
});
2025-05-27 18:02:19 +00:00
}
2025-05-27 16:30:39 +00:00
}
return results;
};
2025-05-27 18:02:19 +00:00
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);
2025-05-27 18:02:19 +00:00
console.log('\n✓ All UTF-8 encoding tests completed successfully');
2025-05-25 19:45:37 +00:00
});
tap.start();