2025-05-26 04:04:51 +00:00
|
|
|
|
import { tap } from '@git.zone/tstest/tapbundle';
|
|
|
|
|
import { EInvoice } from '../../../ts/index.js';
|
|
|
|
|
import { PerformanceTracker } from '../performance.tracker.js';
|
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
tap.test('EDGE-04: Unusual Character Sets - should handle unusual and exotic character encodings', async () => {
|
|
|
|
|
// Test 1: Unicode edge cases with real invoice data
|
|
|
|
|
await PerformanceTracker.track('unicode-edge-cases', async () => {
|
|
|
|
|
const testCases = [
|
|
|
|
|
{
|
|
|
|
|
name: 'zero-width-characters',
|
|
|
|
|
text: 'Invoice\u200B\u200C\u200D\uFEFFNumber',
|
|
|
|
|
description: 'Zero-width spaces and joiners'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'right-to-left',
|
|
|
|
|
text: 'مرحبا INV-001 שלום',
|
|
|
|
|
description: 'RTL Arabic and Hebrew mixed with LTR'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'surrogate-pairs',
|
|
|
|
|
text: '𝐇𝐞𝐥𝐥𝐨 😀 🎉 Invoice',
|
|
|
|
|
description: 'Mathematical bold text and emojis'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'combining-characters',
|
|
|
|
|
text: 'Ińvȯíçë̃ Nüm̈bër̊',
|
|
|
|
|
description: 'Combining diacritical marks'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'control-characters',
|
|
|
|
|
text: 'Invoice Test', // Remove actual control chars as they break XML
|
|
|
|
|
description: 'Control characters (removed for XML safety)'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'bidi-override',
|
|
|
|
|
text: '\u202Eتسا Invoice 123\u202C',
|
|
|
|
|
description: 'Bidirectional override characters'
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const testCase of testCases) {
|
|
|
|
|
const einvoice = new EInvoice();
|
|
|
|
|
einvoice.issueDate = new Date(2024, 0, 1);
|
|
|
|
|
einvoice.invoiceId = testCase.text;
|
|
|
|
|
einvoice.subject = testCase.description;
|
|
|
|
|
|
|
|
|
|
// Set required fields
|
|
|
|
|
einvoice.from = {
|
|
|
|
|
type: 'company',
|
|
|
|
|
name: 'Test Unicode Company',
|
|
|
|
|
description: testCase.description,
|
|
|
|
|
address: {
|
|
|
|
|
streetName: 'Test Street',
|
|
|
|
|
houseNumber: '1',
|
|
|
|
|
postalCode: '12345',
|
|
|
|
|
city: 'Test City',
|
|
|
|
|
country: 'DE'
|
2025-05-26 04:04:51 +00:00
|
|
|
|
},
|
2025-05-27 15:26:22 +00:00
|
|
|
|
status: 'active',
|
|
|
|
|
foundedDate: { year: 2020, month: 1, day: 1 },
|
|
|
|
|
registrationDetails: {
|
|
|
|
|
vatId: 'DE123456789',
|
|
|
|
|
registrationId: 'HRB 12345',
|
|
|
|
|
registrationName: 'Commercial Register'
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
einvoice.to = {
|
|
|
|
|
type: 'person',
|
|
|
|
|
name: 'Test',
|
|
|
|
|
surname: 'Customer',
|
|
|
|
|
salutation: 'Mr' as const,
|
|
|
|
|
sex: 'male' as const,
|
|
|
|
|
title: 'Doctor' as const,
|
|
|
|
|
description: 'Test customer',
|
|
|
|
|
address: {
|
|
|
|
|
streetName: 'Customer Street',
|
|
|
|
|
houseNumber: '2',
|
|
|
|
|
postalCode: '54321',
|
|
|
|
|
city: 'Customer City',
|
|
|
|
|
country: 'DE'
|
2025-05-26 04:04:51 +00:00
|
|
|
|
}
|
2025-05-27 15:26:22 +00:00
|
|
|
|
};
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
// Add test item
|
|
|
|
|
einvoice.items = [{
|
|
|
|
|
position: 1,
|
|
|
|
|
name: `Item with ${testCase.name}`,
|
|
|
|
|
articleNumber: 'ART-001',
|
|
|
|
|
unitType: 'EA',
|
|
|
|
|
unitQuantity: 1,
|
|
|
|
|
unitNetPrice: 100,
|
|
|
|
|
vatPercentage: 19
|
|
|
|
|
}];
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
try {
|
|
|
|
|
// Export to UBL format
|
|
|
|
|
const ublString = await einvoice.toXmlString('ubl');
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
// Check if special characters are preserved
|
|
|
|
|
const preserved = ublString.includes(testCase.text);
|
|
|
|
|
console.log(`Unicode test ${testCase.name}: ${preserved ? 'preserved' : 'encoded'}`);
|
|
|
|
|
|
|
|
|
|
// Try to import it back
|
|
|
|
|
const newInvoice = new EInvoice();
|
|
|
|
|
await newInvoice.fromXmlString(ublString);
|
|
|
|
|
|
|
|
|
|
const roundTripPreserved = newInvoice.invoiceId === testCase.text;
|
|
|
|
|
console.log(`Unicode test ${testCase.name} round-trip: ${roundTripPreserved ? 'success' : 'modified'}`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log(`Unicode test ${testCase.name} failed: ${error.message}`);
|
2025-05-26 04:04:51 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
// Test 2: Various character encodings in invoice content
|
|
|
|
|
await PerformanceTracker.track('various-character-encodings', async () => {
|
|
|
|
|
const encodingTests = [
|
|
|
|
|
{
|
|
|
|
|
encoding: 'UTF-8',
|
|
|
|
|
text: 'Übung macht den Meister - äöüß'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
encoding: 'Latin',
|
|
|
|
|
text: 'Ñoño español - ¡Hola!'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
encoding: 'Cyrillic',
|
|
|
|
|
text: 'Счёт-фактура № 2024'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
encoding: 'Greek',
|
|
|
|
|
text: 'Τιμολόγιο: ΜΜΚΔ'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
encoding: 'Chinese',
|
|
|
|
|
text: '發票編號:貳零貳肆'
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const test of encodingTests) {
|
|
|
|
|
const einvoice = new EInvoice();
|
|
|
|
|
einvoice.issueDate = new Date(2024, 0, 1);
|
|
|
|
|
einvoice.invoiceId = `ENC-${test.encoding}`;
|
|
|
|
|
einvoice.subject = test.text;
|
|
|
|
|
|
|
|
|
|
einvoice.from = {
|
|
|
|
|
type: 'company',
|
|
|
|
|
name: test.text,
|
|
|
|
|
description: `Company using ${test.encoding}`,
|
|
|
|
|
address: {
|
|
|
|
|
streetName: 'Test Street',
|
|
|
|
|
houseNumber: '1',
|
|
|
|
|
postalCode: '12345',
|
|
|
|
|
city: test.text,
|
|
|
|
|
country: 'DE'
|
2025-05-26 04:04:51 +00:00
|
|
|
|
},
|
2025-05-27 15:26:22 +00:00
|
|
|
|
status: 'active',
|
|
|
|
|
foundedDate: { year: 2020, month: 1, day: 1 },
|
|
|
|
|
registrationDetails: {
|
|
|
|
|
vatId: 'DE123456789',
|
|
|
|
|
registrationId: 'HRB 12345',
|
|
|
|
|
registrationName: test.text
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
einvoice.to = {
|
|
|
|
|
type: 'company',
|
|
|
|
|
name: 'Customer Inc',
|
|
|
|
|
description: 'Test customer',
|
|
|
|
|
address: {
|
|
|
|
|
streetName: 'Customer Street',
|
|
|
|
|
houseNumber: '2',
|
|
|
|
|
postalCode: '54321',
|
|
|
|
|
city: 'Customer City',
|
|
|
|
|
country: 'DE'
|
2025-05-26 04:04:51 +00:00
|
|
|
|
},
|
2025-05-27 15:26:22 +00:00
|
|
|
|
status: 'active',
|
|
|
|
|
foundedDate: { year: 2019, month: 1, day: 1 },
|
|
|
|
|
registrationDetails: {
|
|
|
|
|
vatId: 'DE987654321',
|
|
|
|
|
registrationId: 'HRB 54321',
|
|
|
|
|
registrationName: 'Commercial Register'
|
2025-05-26 04:04:51 +00:00
|
|
|
|
}
|
2025-05-27 15:26:22 +00:00
|
|
|
|
};
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
einvoice.items = [{
|
|
|
|
|
position: 1,
|
|
|
|
|
name: test.text,
|
|
|
|
|
articleNumber: 'ART-001',
|
|
|
|
|
unitType: 'EA',
|
|
|
|
|
unitQuantity: 1,
|
|
|
|
|
unitNetPrice: 100,
|
|
|
|
|
vatPercentage: 19
|
|
|
|
|
}];
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
try {
|
|
|
|
|
// Test both UBL and CII formats
|
|
|
|
|
for (const format of ['ubl', 'cii'] as const) {
|
|
|
|
|
const xmlString = await einvoice.toXmlString(format);
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
// Check if text is preserved
|
|
|
|
|
const preserved = xmlString.includes(test.text);
|
|
|
|
|
console.log(`Encoding test ${test.encoding} in ${format}: ${preserved ? 'preserved' : 'modified'}`);
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
// Import back
|
|
|
|
|
const newInvoice = new EInvoice();
|
|
|
|
|
await newInvoice.fromXmlString(xmlString);
|
|
|
|
|
|
|
|
|
|
const descPreserved = newInvoice.subject === test.text;
|
|
|
|
|
console.log(`Encoding test ${test.encoding} round-trip in ${format}: ${descPreserved ? 'success' : 'failed'}`);
|
2025-05-26 04:04:51 +00:00
|
|
|
|
}
|
2025-05-27 15:26:22 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log(`Encoding test ${test.encoding} failed: ${error.message}`);
|
2025-05-26 04:04:51 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Test 3: Emoji and pictographic characters
|
2025-05-27 15:26:22 +00:00
|
|
|
|
await PerformanceTracker.track('emoji-and-pictographs', async () => {
|
|
|
|
|
const emojiTests = [
|
|
|
|
|
{
|
|
|
|
|
name: 'basic-emoji',
|
|
|
|
|
content: 'Invoice 📧 sent ✅'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'flag-emoji',
|
|
|
|
|
content: 'Country: 🇺🇸 🇬🇧 🇩🇪 🇫🇷'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'skin-tone-emoji',
|
|
|
|
|
content: 'Approved by 👍🏻👍🏼👍🏽👍🏾👍🏿'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'zwj-sequences',
|
|
|
|
|
content: 'Family: 👨👩👧👦'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'mixed-emoji-text',
|
|
|
|
|
content: '💰 Total: €1,234.56 💶'
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const test of emojiTests) {
|
|
|
|
|
const einvoice = new EInvoice();
|
|
|
|
|
einvoice.issueDate = new Date(2024, 0, 1);
|
|
|
|
|
einvoice.invoiceId = 'EMOJI-001';
|
|
|
|
|
einvoice.subject = test.content;
|
|
|
|
|
|
|
|
|
|
einvoice.from = {
|
|
|
|
|
type: 'company',
|
|
|
|
|
name: 'Emoji Company',
|
|
|
|
|
description: test.content,
|
|
|
|
|
address: {
|
|
|
|
|
streetName: 'Emoji Street',
|
|
|
|
|
houseNumber: '1',
|
|
|
|
|
postalCode: '12345',
|
|
|
|
|
city: 'Emoji City',
|
|
|
|
|
country: 'DE'
|
2025-05-26 04:04:51 +00:00
|
|
|
|
},
|
2025-05-27 15:26:22 +00:00
|
|
|
|
status: 'active',
|
|
|
|
|
foundedDate: { year: 2020, month: 1, day: 1 },
|
|
|
|
|
registrationDetails: {
|
|
|
|
|
vatId: 'DE123456789',
|
|
|
|
|
registrationId: 'HRB 12345',
|
|
|
|
|
registrationName: 'Commercial Register'
|
2025-05-26 04:04:51 +00:00
|
|
|
|
}
|
2025-05-27 15:26:22 +00:00
|
|
|
|
};
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
einvoice.to = {
|
|
|
|
|
type: 'person',
|
|
|
|
|
name: 'Emoji',
|
|
|
|
|
surname: 'Customer',
|
|
|
|
|
salutation: 'Mr' as const,
|
|
|
|
|
sex: 'male' as const,
|
|
|
|
|
title: 'Doctor' as const,
|
|
|
|
|
description: 'Customer who likes emojis',
|
|
|
|
|
address: {
|
|
|
|
|
streetName: 'Customer Street',
|
|
|
|
|
houseNumber: '2',
|
|
|
|
|
postalCode: '54321',
|
|
|
|
|
city: 'Customer City',
|
|
|
|
|
country: 'DE'
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
einvoice.items = [{
|
|
|
|
|
position: 1,
|
|
|
|
|
name: test.content,
|
|
|
|
|
articleNumber: 'EMOJI-001',
|
|
|
|
|
unitType: 'EA',
|
|
|
|
|
unitQuantity: 1,
|
|
|
|
|
unitNetPrice: 100,
|
|
|
|
|
vatPercentage: 19
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const ublString = await einvoice.toXmlString('ubl');
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
// Check if emoji content is preserved or encoded
|
|
|
|
|
const preserved = ublString.includes(test.content);
|
|
|
|
|
console.log(`Emoji test ${test.name}: ${preserved ? 'preserved' : 'encoded'}`);
|
|
|
|
|
|
|
|
|
|
// Count grapheme clusters (visual characters)
|
|
|
|
|
const graphemeCount = [...new Intl.Segmenter().segment(test.content)].length;
|
|
|
|
|
console.log(`Emoji test ${test.name} has ${graphemeCount} visual characters`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log(`Emoji test ${test.name} failed: ${error.message}`);
|
2025-05-26 04:04:51 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Test 4: Legacy and exotic scripts
|
2025-05-27 15:26:22 +00:00
|
|
|
|
await PerformanceTracker.track('exotic-scripts', async () => {
|
|
|
|
|
const scripts = [
|
|
|
|
|
{ name: 'chinese-traditional', text: '發票編號:貳零貳肆' },
|
|
|
|
|
{ name: 'japanese-mixed', text: '請求書番号:2024年' },
|
|
|
|
|
{ name: 'korean', text: '송장 번호: 2024' },
|
|
|
|
|
{ name: 'thai', text: 'ใบแจ้งหนี้: ๒๐๒๔' },
|
|
|
|
|
{ name: 'devanagari', text: 'चालान संख्या: २०२४' },
|
|
|
|
|
{ name: 'bengali', text: 'চালান নং: ২০২৪' },
|
|
|
|
|
{ name: 'tamil', text: 'விலைப்பட்டியல்: ௨௦௨௪' }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const script of scripts) {
|
|
|
|
|
const einvoice = new EInvoice();
|
|
|
|
|
einvoice.issueDate = new Date(2024, 0, 1);
|
|
|
|
|
einvoice.invoiceId = `SCRIPT-${script.name}`;
|
|
|
|
|
einvoice.subject = script.text;
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
einvoice.from = {
|
|
|
|
|
type: 'company',
|
|
|
|
|
name: 'International Company',
|
|
|
|
|
description: script.text,
|
|
|
|
|
address: {
|
|
|
|
|
streetName: 'International Street',
|
|
|
|
|
houseNumber: '1',
|
|
|
|
|
postalCode: '12345',
|
|
|
|
|
city: 'International City',
|
|
|
|
|
country: 'DE'
|
|
|
|
|
},
|
|
|
|
|
status: 'active',
|
|
|
|
|
foundedDate: { year: 2020, month: 1, day: 1 },
|
|
|
|
|
registrationDetails: {
|
|
|
|
|
vatId: 'DE123456789',
|
|
|
|
|
registrationId: 'HRB 12345',
|
|
|
|
|
registrationName: 'Commercial Register'
|
2025-05-26 04:04:51 +00:00
|
|
|
|
}
|
2025-05-27 15:26:22 +00:00
|
|
|
|
};
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
einvoice.to = {
|
|
|
|
|
type: 'company',
|
|
|
|
|
name: 'Local Company',
|
|
|
|
|
description: 'Customer',
|
|
|
|
|
address: {
|
|
|
|
|
streetName: 'Local Street',
|
|
|
|
|
houseNumber: '2',
|
|
|
|
|
postalCode: '54321',
|
|
|
|
|
city: 'Local City',
|
|
|
|
|
country: 'DE'
|
2025-05-26 04:04:51 +00:00
|
|
|
|
},
|
2025-05-27 15:26:22 +00:00
|
|
|
|
status: 'active',
|
|
|
|
|
foundedDate: { year: 2019, month: 1, day: 1 },
|
|
|
|
|
registrationDetails: {
|
|
|
|
|
vatId: 'DE987654321',
|
|
|
|
|
registrationId: 'HRB 54321',
|
|
|
|
|
registrationName: 'Commercial Register'
|
2025-05-26 04:04:51 +00:00
|
|
|
|
}
|
2025-05-27 15:26:22 +00:00
|
|
|
|
};
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
einvoice.items = [{
|
|
|
|
|
position: 1,
|
|
|
|
|
name: script.text,
|
|
|
|
|
articleNumber: 'INT-001',
|
|
|
|
|
unitType: 'EA',
|
|
|
|
|
unitQuantity: 1,
|
|
|
|
|
unitNetPrice: 100,
|
|
|
|
|
vatPercentage: 19
|
|
|
|
|
}];
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
try {
|
|
|
|
|
const ciiString = await einvoice.toXmlString('cii');
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
const preserved = ciiString.includes(script.text);
|
|
|
|
|
console.log(`Script ${script.name}: ${preserved ? 'preserved' : 'encoded'}`);
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
// Test round-trip
|
|
|
|
|
const newInvoice = new EInvoice();
|
|
|
|
|
await newInvoice.fromXmlString(ciiString);
|
|
|
|
|
|
|
|
|
|
const descPreserved = newInvoice.subject === script.text;
|
|
|
|
|
console.log(`Script ${script.name} round-trip: ${descPreserved ? 'success' : 'modified'}`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log(`Script ${script.name} failed: ${error.message}`);
|
2025-05-26 04:04:51 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
// Test 5: XML special characters in unusual positions
|
|
|
|
|
await PerformanceTracker.track('xml-special-characters', async () => {
|
|
|
|
|
const specialChars = [
|
|
|
|
|
{ char: '<', desc: 'less than' },
|
|
|
|
|
{ char: '>', desc: 'greater than' },
|
|
|
|
|
{ char: '&', desc: 'ampersand' },
|
|
|
|
|
{ char: '"', desc: 'quote' },
|
|
|
|
|
{ char: "'", desc: 'apostrophe' }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const special of specialChars) {
|
|
|
|
|
const einvoice = new EInvoice();
|
|
|
|
|
einvoice.issueDate = new Date(2024, 0, 1);
|
|
|
|
|
einvoice.invoiceId = `XML-${special.desc}`;
|
|
|
|
|
einvoice.subject = `Price ${special.char} 100`;
|
|
|
|
|
|
|
|
|
|
einvoice.from = {
|
|
|
|
|
type: 'company',
|
|
|
|
|
name: `Company ${special.char} Test`,
|
|
|
|
|
description: 'Special char test',
|
|
|
|
|
address: {
|
|
|
|
|
streetName: 'Test Street',
|
|
|
|
|
houseNumber: '1',
|
|
|
|
|
postalCode: '12345',
|
|
|
|
|
city: 'Test City',
|
|
|
|
|
country: 'DE'
|
2025-05-26 04:04:51 +00:00
|
|
|
|
},
|
2025-05-27 15:26:22 +00:00
|
|
|
|
status: 'active',
|
|
|
|
|
foundedDate: { year: 2020, month: 1, day: 1 },
|
|
|
|
|
registrationDetails: {
|
|
|
|
|
vatId: 'DE123456789',
|
|
|
|
|
registrationId: 'HRB 12345',
|
|
|
|
|
registrationName: 'Commercial Register'
|
2025-05-26 04:04:51 +00:00
|
|
|
|
}
|
2025-05-27 15:26:22 +00:00
|
|
|
|
};
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
einvoice.to = {
|
|
|
|
|
type: 'person',
|
|
|
|
|
name: 'Test',
|
|
|
|
|
surname: 'Customer',
|
|
|
|
|
salutation: 'Mr' as const,
|
|
|
|
|
sex: 'male' as const,
|
|
|
|
|
title: 'Doctor' as const,
|
|
|
|
|
description: 'Test customer',
|
|
|
|
|
address: {
|
|
|
|
|
streetName: 'Customer Street',
|
|
|
|
|
houseNumber: '2',
|
|
|
|
|
postalCode: '54321',
|
|
|
|
|
city: 'Customer City',
|
|
|
|
|
country: 'DE'
|
2025-05-26 04:04:51 +00:00
|
|
|
|
}
|
2025-05-27 15:26:22 +00:00
|
|
|
|
};
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
einvoice.items = [{
|
|
|
|
|
position: 1,
|
|
|
|
|
name: `Item ${special.char} Test`,
|
|
|
|
|
articleNumber: 'SPEC-001',
|
|
|
|
|
unitType: 'EA',
|
|
|
|
|
unitQuantity: 1,
|
|
|
|
|
unitNetPrice: 100,
|
|
|
|
|
vatPercentage: 19
|
|
|
|
|
}];
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
try {
|
|
|
|
|
const xmlString = await einvoice.toXmlString('ubl');
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
// Check if special chars are properly escaped
|
|
|
|
|
const escaped = xmlString.includes(`&${special.desc.replace(' ', '')};`) ||
|
|
|
|
|
xmlString.includes(`&#${special.char.charCodeAt(0)};`);
|
|
|
|
|
console.log(`XML special ${special.desc}: ${escaped ? 'properly escaped' : 'check encoding'}`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log(`XML special ${special.desc} failed: ${error.message}`);
|
2025-05-26 04:04:51 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
// Test 6: Character set conversion in format transformation
|
|
|
|
|
await PerformanceTracker.track('format-transform-charsets', async () => {
|
|
|
|
|
const testContents = [
|
|
|
|
|
{ name: 'multilingual', text: 'Hello مرحبا 你好 Здравствуйте' },
|
|
|
|
|
{ name: 'symbols', text: '€ £ ¥ $ ₹ ₽ ¢ ₩' },
|
|
|
|
|
{ name: 'accented', text: 'àáäâ èéëê ìíïî òóöô ùúüû ñç' },
|
|
|
|
|
{ name: 'mixed-emoji', text: 'Invoice 📄 Total: 💰 Status: ✅' }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const content of testContents) {
|
|
|
|
|
const einvoice = new EInvoice();
|
|
|
|
|
einvoice.issueDate = new Date(2024, 0, 1);
|
|
|
|
|
einvoice.invoiceId = 'CHARSET-001';
|
|
|
|
|
einvoice.subject = content.text;
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
einvoice.from = {
|
|
|
|
|
type: 'company',
|
|
|
|
|
name: 'Charset Test Company',
|
|
|
|
|
description: content.text,
|
|
|
|
|
address: {
|
|
|
|
|
streetName: 'Test 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: 'Commercial Register'
|
2025-05-26 04:04:51 +00:00
|
|
|
|
}
|
2025-05-27 15:26:22 +00:00
|
|
|
|
};
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
einvoice.to = {
|
|
|
|
|
type: 'company',
|
|
|
|
|
name: 'Customer Company',
|
|
|
|
|
description: 'Customer',
|
|
|
|
|
address: {
|
|
|
|
|
streetName: 'Customer Street',
|
|
|
|
|
houseNumber: '2',
|
|
|
|
|
postalCode: '54321',
|
|
|
|
|
city: 'Customer City',
|
|
|
|
|
country: 'DE'
|
|
|
|
|
},
|
|
|
|
|
status: 'active',
|
|
|
|
|
foundedDate: { year: 2019, month: 1, day: 1 },
|
|
|
|
|
registrationDetails: {
|
|
|
|
|
vatId: 'DE987654321',
|
|
|
|
|
registrationId: 'HRB 54321',
|
|
|
|
|
registrationName: 'Commercial Register'
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
einvoice.items = [{
|
|
|
|
|
position: 1,
|
|
|
|
|
name: content.text,
|
|
|
|
|
articleNumber: 'CHARSET-001',
|
|
|
|
|
unitType: 'EA',
|
|
|
|
|
unitQuantity: 1,
|
|
|
|
|
unitNetPrice: 100,
|
|
|
|
|
vatPercentage: 19
|
|
|
|
|
}];
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
try {
|
|
|
|
|
// Convert from UBL to CII
|
|
|
|
|
const ublString = await einvoice.toXmlString('ubl');
|
|
|
|
|
const newInvoice = new EInvoice();
|
|
|
|
|
await newInvoice.fromXmlString(ublString);
|
|
|
|
|
const ciiString = await newInvoice.toXmlString('cii');
|
2025-05-26 04:04:51 +00:00
|
|
|
|
|
2025-05-27 15:26:22 +00:00
|
|
|
|
// Check if content was preserved through transformation
|
|
|
|
|
const preserved = ciiString.includes(content.text);
|
|
|
|
|
console.log(`Format transform ${content.name}: ${preserved ? 'preserved' : 'modified'}`);
|
|
|
|
|
|
|
|
|
|
// Double check with round trip
|
|
|
|
|
const finalInvoice = new EInvoice();
|
|
|
|
|
await finalInvoice.fromXmlString(ciiString);
|
|
|
|
|
const roundTripPreserved = finalInvoice.subject === content.text;
|
|
|
|
|
console.log(`Format transform ${content.name} round-trip: ${roundTripPreserved ? 'success' : 'failed'}`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log(`Format transform ${content.name} failed: ${error.message}`);
|
2025-05-26 04:04:51 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Run the test
|
|
|
|
|
tap.start();
|