einvoice/test/suite/einvoice_edge-cases/test.edge-10.timezone-edges.ts
Philipp Kunz 784a50bc7f fix(tests): Fixed ENC-01, ENC-02, and ENC-03 encoding tests
- Fixed UTF-8 encoding test (ENC-01) to accept multiple encoding declarations
- Fixed UTF-16 encoding test (ENC-02) by rewriting with correct API usage
- Fixed ISO-8859-1 encoding test (ENC-03) with proper address fields and methods
- All three encoding tests now pass successfully
- Updated edge-cases tests (EDGE-02 through EDGE-07) with new test structure
2025-05-28 13:05:59 +00:00

566 lines
19 KiB
TypeScript

import { tap, expect } from '@git.zone/tstest/tapbundle';
import { EInvoice } from '../../../ts/index.js';
tap.test('EDGE-10: Time Zone Edge Cases - should handle complex timezone scenarios', async () => {
console.log('Testing timezone edge cases...\n');
// Test 1: Various date/time formats in UBL
const testUblDateFormats = async () => {
const dateFormats = [
{
name: 'UTC with Z',
xml: `<?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>TZ-001</cbc:ID>
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
<cbc:IssueTime>14:30:00Z</cbc:IssueTime>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Timezone 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>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>`,
expectedTime: '14:30:00Z'
},
{
name: 'UTC with +00:00',
xml: `<?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>TZ-002</cbc:ID>
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
<cbc:IssueTime>14:30:00+00:00</cbc:IssueTime>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Timezone 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>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>`,
expectedTime: '14:30:00+00:00'
},
{
name: 'Positive timezone offset',
xml: `<?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>TZ-003</cbc:ID>
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
<cbc:IssueTime>20:30:00+08:00</cbc:IssueTime>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Timezone 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>SG</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>`,
expectedTime: '20:30:00+08:00'
}
];
const results = [];
for (const test of dateFormats) {
try {
const einvoice = await EInvoice.fromXml(test.xml);
results.push({
name: test.name,
parsed: true,
hasDate: !!einvoice.date,
invoiceId: einvoice.id,
format: einvoice.getFormat()
});
} catch (error) {
results.push({
name: test.name,
parsed: false,
error: error.message
});
}
}
return results;
};
const ublDateResults = await testUblDateFormats();
console.log('Test 1 - UBL date/time formats:');
ublDateResults.forEach(result => {
console.log(` ${result.name}: ${result.parsed ? `Parsed (${result.invoiceId})` : 'Failed'}`);
});
expect(ublDateResults.every(r => r.parsed)).toEqual(true);
// Test 2: Date edge cases
const testDateEdgeCases = async () => {
const edgeCases = [
{
name: 'Leap year date',
date: '2024-02-29',
description: 'February 29th in leap year'
},
{
name: 'Year boundary',
date: '2024-12-31',
description: 'Last day of year'
},
{
name: 'DST transition',
date: '2025-03-30',
description: 'Daylight saving time transition date'
},
{
name: 'Far future date',
date: '2099-12-31',
description: 'Date far in the future'
}
];
const results = [];
for (const testCase of edgeCases) {
const xml = `<?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>DATE-EDGE-${testCase.name.toUpperCase().replace(/\s+/g, '-')}</cbc:ID>
<cbc:IssueDate>${testCase.date}</cbc:IssueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Date 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>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 einvoice = await EInvoice.fromXml(xml);
const dateValid = einvoice.date && !isNaN(einvoice.date);
results.push({
name: testCase.name,
parsed: true,
dateValid,
date: new Date(einvoice.date).toISOString()
});
} catch (error) {
results.push({
name: testCase.name,
parsed: false,
error: error.message
});
}
}
return results;
};
const dateEdgeResults = await testDateEdgeCases();
console.log('\nTest 2 - Date edge cases:');
dateEdgeResults.forEach(result => {
console.log(` ${result.name}: ${result.parsed ? `Valid (${result.date})` : 'Failed'}`);
});
expect(dateEdgeResults.every(r => r.parsed && r.dateValid)).toEqual(true);
// Test 3: Invalid date formats
const testInvalidDateFormats = async () => {
const invalidFormats = [
{
name: 'Invalid date',
date: '2025-02-30',
description: 'February 30th does not exist'
},
{
name: 'Wrong format',
date: '25/01/2025',
description: 'DD/MM/YYYY instead of YYYY-MM-DD'
},
{
name: 'Incomplete date',
date: '2025-01',
description: 'Missing day'
},
{
name: 'Text date',
date: 'January 25, 2025',
description: 'Text format instead of ISO'
}
];
const results = [];
for (const testCase of invalidFormats) {
const xml = `<?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>INVALID-DATE-${testCase.name.toUpperCase().replace(/\s+/g, '-')}</cbc:ID>
<cbc:IssueDate>${testCase.date}</cbc:IssueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Date 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>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 einvoice = await EInvoice.fromXml(xml);
// Check if date was parsed or set to current date as fallback
const dateSet = einvoice.date > 0;
results.push({
name: testCase.name,
handled: true,
dateSet,
invoiceId: einvoice.id
});
} catch (error) {
results.push({
name: testCase.name,
handled: false,
errorInformative: error.message.includes('date') ||
error.message.includes('Date') ||
error.message.includes('validation')
});
}
}
return results;
};
const invalidDateResults = await testInvalidDateFormats();
console.log('\nTest 3 - Invalid date formats:');
invalidDateResults.forEach(result => {
console.log(` ${result.name}: ${result.handled ? 'Handled gracefully' : 'Failed'} ${result.errorInformative ? '[Informative error]' : ''}`);
});
expect(invalidDateResults.every(r => r.handled || r.errorInformative)).toEqual(true);
// Test 4: CII date formats
const testCiiDateFormats = async () => {
const ciiXml = `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>CII-TZ-001</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20250125</udt:DateTimeString>
</ram:IssueDateTime>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
<ram:Name>CII Timezone Supplier</ram:Name>
<ram:PostalTradeAddress>
<ram:LineOne>Test Street 1</ram:LineOne>
<ram:CityName>Test City</ram:CityName>
<ram:PostcodeCode>12345</ram:PostcodeCode>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>CII Customer</ram:Name>
<ram:PostalTradeAddress>
<ram:LineOne>Customer Street 2</ram:LineOne>
<ram:CityName>Customer City</ram:CityName>
<ram:PostcodeCode>54321</ram:PostcodeCode>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
</ram:BuyerTradeParty>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeSettlement>
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>`;
try {
const einvoice = await EInvoice.fromXml(ciiXml);
return {
parsed: true,
format: einvoice.getFormat(),
hasDate: !!einvoice.date,
invoiceId: einvoice.id
};
} catch (error) {
return {
parsed: false,
error: error.message
};
}
};
const ciiResult = await testCiiDateFormats();
console.log('\nTest 4 - CII date format:');
console.log(` CII with format 102: ${ciiResult.parsed ? `Parsed (${ciiResult.invoiceId})` : 'Failed'}`);
expect(ciiResult.parsed).toEqual(true);
// Test 5: Different timezone representations
const testTimezoneRepresentations = async () => {
const timezones = [
{ tz: '-11:00', name: 'Extreme negative offset' },
{ tz: '-05:00', name: 'EST' },
{ tz: '+01:00', name: 'CET' },
{ tz: '+05:30', name: 'IST (half hour offset)' },
{ tz: '+13:00', name: 'Extreme positive offset' },
{ tz: '+14:00', name: 'Maximum positive offset' }
];
const results = [];
for (const { tz, name } of timezones) {
const xml = `<?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>TZ-${tz.replace(/[+:-]/g, '')}</cbc:ID>
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
<cbc:IssueTime>12:00:00${tz}</cbc:IssueTime>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>TZ 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>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 einvoice = await EInvoice.fromXml(xml);
results.push({
timezone: tz,
name,
parsed: true,
hasDate: !!einvoice.date
});
} catch (error) {
results.push({
timezone: tz,
name,
parsed: false,
error: error.message
});
}
}
return results;
};
const timezoneResults = await testTimezoneRepresentations();
console.log('\nTest 5 - Timezone representations:');
timezoneResults.forEach(result => {
console.log(` ${result.name} (${result.timezone}): ${result.parsed ? 'Parsed' : 'Failed'}`);
});
expect(timezoneResults.every(r => r.parsed)).toEqual(true);
console.log('\n✓ All timezone edge cases handled appropriately');
});
tap.start();