einvoice/test/suite/einvoice_edge-cases/test.edge-10.timezone-edges.ts
2025-05-26 04:04:51 +00:00

695 lines
20 KiB
TypeScript

import { tap } from '@git.zone/tstest/tapbundle';
import * as plugins from '../plugins.js';
import { EInvoice } from '../../../ts/index.js';
import { PerformanceTracker } from '../performance.tracker.js';
const performanceTracker = new PerformanceTracker('EDGE-10: Time Zone Edge Cases');
tap.test('EDGE-10: Time Zone Edge Cases - should handle complex timezone scenarios', async (t) => {
const einvoice = new EInvoice();
// Test 1: Date/time across timezone boundaries
const timezoneBoundaries = await performanceTracker.measureAsync(
'timezone-boundary-crossing',
async () => {
const boundaryTests = [
{
name: 'midnight-utc',
dateTime: '2024-01-15T00:00:00Z',
timezone: 'UTC',
expectedLocal: '2024-01-15T00:00:00'
},
{
name: 'midnight-cross-positive',
dateTime: '2024-01-15T23:59:59+12:00',
timezone: 'Pacific/Auckland',
expectedUTC: '2024-01-15T11:59:59Z'
},
{
name: 'midnight-cross-negative',
dateTime: '2024-01-15T00:00:00-11:00',
timezone: 'Pacific/Midway',
expectedUTC: '2024-01-15T11:00:00Z'
},
{
name: 'date-line-crossing',
dateTime: '2024-01-15T12:00:00+14:00',
timezone: 'Pacific/Kiritimati',
expectedUTC: '2024-01-14T22:00:00Z'
}
];
const results = [];
for (const test of boundaryTests) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>TZ-TEST-001</ID>
<IssueDate>${test.dateTime}</IssueDate>
<DueDateTime>${test.dateTime}</DueDateTime>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const dates = await einvoice.normalizeDates(parsed, {
targetTimezone: test.timezone
});
results.push({
test: test.name,
parsed: true,
originalDateTime: test.dateTime,
normalizedDate: dates?.IssueDate,
isDatePreserved: dates?.dateIntegrity || false,
crossesDateBoundary: dates?.crossesDateLine || false
});
} catch (error) {
results.push({
test: test.name,
parsed: false,
error: error.message
});
}
}
return results;
}
);
timezoneBoundaries.forEach(result => {
t.ok(result.parsed, `Timezone boundary ${result.test} should be handled`);
});
// Test 2: DST (Daylight Saving Time) transitions
const dstTransitions = await performanceTracker.measureAsync(
'dst-transition-handling',
async () => {
const dstTests = [
{
name: 'spring-forward-gap',
dateTime: '2024-03-10T02:30:00',
timezone: 'America/New_York',
description: 'Time that does not exist due to DST'
},
{
name: 'fall-back-ambiguous',
dateTime: '2024-11-03T01:30:00',
timezone: 'America/New_York',
description: 'Time that occurs twice due to DST'
},
{
name: 'dst-boundary-exact',
dateTime: '2024-03-31T02:00:00',
timezone: 'Europe/London',
description: 'Exact moment of DST transition'
},
{
name: 'southern-hemisphere-dst',
dateTime: '2024-10-06T02:00:00',
timezone: 'Australia/Sydney',
description: 'Southern hemisphere DST transition'
}
];
const results = [];
for (const test of dstTests) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>DST-${test.name}</ID>
<IssueDateTime>${test.dateTime}</IssueDateTime>
<ProcessingTime timezone="${test.timezone}">${test.dateTime}</ProcessingTime>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const dstAnalysis = await einvoice.analyzeDSTIssues(parsed);
results.push({
scenario: test.name,
handled: true,
hasAmbiguity: dstAnalysis?.isAmbiguous || false,
isNonExistent: dstAnalysis?.isNonExistent || false,
suggestion: dstAnalysis?.suggestion,
adjustedTime: dstAnalysis?.adjusted
});
} catch (error) {
results.push({
scenario: test.name,
handled: false,
error: error.message
});
}
}
return results;
}
);
dstTransitions.forEach(result => {
t.ok(result.handled, `DST transition ${result.scenario} should be handled`);
if (result.hasAmbiguity || result.isNonExistent) {
t.ok(result.suggestion, 'DST issue should have suggestion');
}
});
// Test 3: Historic timezone changes
const historicTimezones = await performanceTracker.measureAsync(
'historic-timezone-changes',
async () => {
const historicTests = [
{
name: 'pre-timezone-standardization',
dateTime: '1850-01-01T12:00:00',
location: 'Europe/London',
description: 'Before standard time zones'
},
{
name: 'soviet-time-changes',
dateTime: '1991-03-31T02:00:00',
location: 'Europe/Moscow',
description: 'USSR timezone reorganization'
},
{
name: 'samoa-dateline-change',
dateTime: '2011-12-30T00:00:00',
location: 'Pacific/Apia',
description: 'Samoa skipped December 30, 2011'
},
{
name: 'crimea-timezone-change',
dateTime: '2014-03-30T02:00:00',
location: 'Europe/Simferopol',
description: 'Crimea timezone change'
}
];
const results = [];
for (const test of historicTests) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>HISTORIC-${test.name}</ID>
<HistoricDate>${test.dateTime}</HistoricDate>
<Location>${test.location}</Location>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const historicAnalysis = await einvoice.handleHistoricDate(parsed, {
validateHistoric: true
});
results.push({
test: test.name,
processed: true,
isHistoric: historicAnalysis?.isHistoric || false,
hasTimezoneChange: historicAnalysis?.timezoneChanged || false,
warnings: historicAnalysis?.warnings || []
});
} catch (error) {
results.push({
test: test.name,
processed: false,
error: error.message
});
}
}
return results;
}
);
historicTimezones.forEach(result => {
t.ok(result.processed, `Historic timezone ${result.test} should be processed`);
});
// Test 4: Fractional timezone offsets
const fractionalTimezones = await performanceTracker.measureAsync(
'fractional-timezone-offsets',
async () => {
const fractionalTests = [
{
name: 'newfoundland-half-hour',
offset: '-03:30',
timezone: 'America/St_Johns',
dateTime: '2024-01-15T12:00:00-03:30'
},
{
name: 'india-half-hour',
offset: '+05:30',
timezone: 'Asia/Kolkata',
dateTime: '2024-01-15T12:00:00+05:30'
},
{
name: 'nepal-quarter-hour',
offset: '+05:45',
timezone: 'Asia/Kathmandu',
dateTime: '2024-01-15T12:00:00+05:45'
},
{
name: 'chatham-islands',
offset: '+12:45',
timezone: 'Pacific/Chatham',
dateTime: '2024-01-15T12:00:00+12:45'
}
];
const results = [];
for (const test of fractionalTests) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>FRAC-${test.name}</ID>
<IssueDateTime>${test.dateTime}</IssueDateTime>
<PaymentDueTime>${test.dateTime}</PaymentDueTime>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const normalized = await einvoice.normalizeToUTC(parsed);
results.push({
test: test.name,
offset: test.offset,
parsed: true,
correctlyHandled: normalized?.timezoneHandled || false,
preservedPrecision: normalized?.precisionMaintained || false
});
} catch (error) {
results.push({
test: test.name,
offset: test.offset,
parsed: false,
error: error.message
});
}
}
return results;
}
);
fractionalTimezones.forEach(result => {
t.ok(result.parsed, `Fractional timezone ${result.test} should be parsed`);
if (result.parsed) {
t.ok(result.correctlyHandled, 'Fractional offset should be handled correctly');
}
});
// Test 5: Missing or ambiguous timezone info
const ambiguousTimezones = await performanceTracker.measureAsync(
'ambiguous-timezone-info',
async () => {
const ambiguousTests = [
{
name: 'no-timezone-info',
xml: `<Invoice>
<IssueDate>2024-01-15</IssueDate>
<IssueTime>14:30:00</IssueTime>
</Invoice>`
},
{
name: 'conflicting-timezones',
xml: `<Invoice>
<IssueDateTime>2024-01-15T14:30:00+02:00</IssueDateTime>
<Timezone>America/New_York</Timezone>
</Invoice>`
},
{
name: 'local-time-only',
xml: `<Invoice>
<Timestamp>2024-01-15T14:30:00</Timestamp>
</Invoice>`
},
{
name: 'invalid-offset',
xml: `<Invoice>
<DateTime>2024-01-15T14:30:00+25:00</DateTime>
</Invoice>`
}
];
const results = [];
for (const test of ambiguousTests) {
const fullXml = `<?xml version="1.0" encoding="UTF-8"?>${test.xml}`;
try {
const parsed = await einvoice.parseXML(fullXml);
const timezoneAnalysis = await einvoice.resolveTimezones(parsed, {
defaultTimezone: 'UTC',
strict: false
});
results.push({
test: test.name,
resolved: true,
hasAmbiguity: timezoneAnalysis?.ambiguous || false,
resolution: timezoneAnalysis?.resolution,
confidence: timezoneAnalysis?.confidence || 0
});
} catch (error) {
results.push({
test: test.name,
resolved: false,
error: error.message
});
}
}
return results;
}
);
ambiguousTimezones.forEach(result => {
t.ok(result.resolved || result.error,
`Ambiguous timezone ${result.test} should be handled`);
if (result.resolved && result.hasAmbiguity) {
t.ok(result.confidence < 100, 'Ambiguous timezone should have lower confidence');
}
});
// Test 6: Leap seconds handling
const leapSeconds = await performanceTracker.measureAsync(
'leap-seconds-handling',
async () => {
const leapSecondTests = [
{
name: 'leap-second-23-59-60',
dateTime: '2016-12-31T23:59:60Z',
description: 'Actual leap second'
},
{
name: 'near-leap-second',
dateTime: '2016-12-31T23:59:59.999Z',
description: 'Just before leap second'
},
{
name: 'after-leap-second',
dateTime: '2017-01-01T00:00:00.001Z',
description: 'Just after leap second'
}
];
const results = [];
for (const test of leapSecondTests) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>LEAP-${test.name}</ID>
<PreciseTimestamp>${test.dateTime}</PreciseTimestamp>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const timeHandling = await einvoice.handlePreciseTime(parsed);
results.push({
test: test.name,
handled: true,
isLeapSecond: timeHandling?.isLeapSecond || false,
adjusted: timeHandling?.adjusted || false,
precision: timeHandling?.precision
});
} catch (error) {
results.push({
test: test.name,
handled: false,
error: error.message
});
}
}
return results;
}
);
leapSeconds.forEach(result => {
t.ok(result.handled || result.error,
`Leap second ${result.test} should be processed`);
});
// Test 7: Format-specific timezone handling
const formatSpecificTimezones = await performanceTracker.measureAsync(
'format-specific-timezone-handling',
async () => {
const formats = [
{
format: 'ubl',
xml: `<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<IssueDate>2024-01-15</IssueDate>
<IssueTime>14:30:00+02:00</IssueTime>
</Invoice>`
},
{
format: 'cii',
xml: `<rsm:CrossIndustryInvoice>
<rsm:ExchangedDocument>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20240115143000</udt:DateTimeString>
</ram:IssueDateTime>
</rsm:ExchangedDocument>
</rsm:CrossIndustryInvoice>`
},
{
format: 'facturx',
xml: `<Invoice>
<IssueDateTime>2024-01-15T14:30:00</IssueDateTime>
<TimeZoneOffset>+0200</TimeZoneOffset>
</Invoice>`
}
];
const results = [];
for (const test of formats) {
try {
const parsed = await einvoice.parseDocument(test.xml);
const standardized = await einvoice.standardizeDateTime(parsed, {
sourceFormat: test.format
});
results.push({
format: test.format,
parsed: true,
hasDateTime: !!standardized?.dateTime,
hasTimezone: !!standardized?.timezone,
normalized: standardized?.normalized || false
});
} catch (error) {
results.push({
format: test.format,
parsed: false,
error: error.message
});
}
}
return results;
}
);
formatSpecificTimezones.forEach(result => {
t.ok(result.parsed, `Format ${result.format} timezone should be handled`);
});
// Test 8: Business day calculations across timezones
const businessDayCalculations = await performanceTracker.measureAsync(
'business-day-calculations',
async () => {
const businessTests = [
{
name: 'payment-terms-30-days',
issueDate: '2024-01-15T23:00:00+12:00',
terms: 30,
expectedDue: '2024-02-14'
},
{
name: 'cross-month-boundary',
issueDate: '2024-01-31T22:00:00-05:00',
terms: 1,
expectedDue: '2024-02-01'
},
{
name: 'weekend-adjustment',
issueDate: '2024-01-12T18:00:00Z', // Friday
terms: 3,
expectedDue: '2024-01-17' // Skip weekend
}
];
const results = [];
for (const test of businessTests) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>BUSINESS-${test.name}</ID>
<IssueDateTime>${test.issueDate}</IssueDateTime>
<PaymentTerms>
<NetDays>${test.terms}</NetDays>
</PaymentTerms>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const calculated = await einvoice.calculateDueDate(parsed, {
skipWeekends: true,
skipHolidays: true,
timezone: 'UTC'
});
results.push({
test: test.name,
calculated: true,
dueDate: calculated?.dueDate,
matchesExpected: calculated?.dueDate === test.expectedDue,
businessDaysUsed: calculated?.businessDays
});
} catch (error) {
results.push({
test: test.name,
calculated: false,
error: error.message
});
}
}
return results;
}
);
businessDayCalculations.forEach(result => {
t.ok(result.calculated, `Business day calculation ${result.test} should work`);
});
// Test 9: Timezone conversion errors
const timezoneConversionErrors = await performanceTracker.measureAsync(
'timezone-conversion-errors',
async () => {
const errorTests = [
{
name: 'invalid-timezone-name',
timezone: 'Invalid/Timezone',
dateTime: '2024-01-15T12:00:00'
},
{
name: 'deprecated-timezone',
timezone: 'US/Eastern', // Deprecated, use America/New_York
dateTime: '2024-01-15T12:00:00'
},
{
name: 'military-timezone',
timezone: 'Z', // Zulu time
dateTime: '2024-01-15T12:00:00'
},
{
name: 'three-letter-timezone',
timezone: 'EST', // Ambiguous
dateTime: '2024-01-15T12:00:00'
}
];
const results = [];
for (const test of errorTests) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>ERROR-${test.name}</ID>
<DateTime timezone="${test.timezone}">${test.dateTime}</DateTime>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const converted = await einvoice.convertTimezone(parsed, {
from: test.timezone,
to: 'UTC',
strict: true
});
results.push({
test: test.name,
handled: true,
converted: !!converted,
fallbackUsed: converted?.fallback || false,
warning: converted?.warning
});
} catch (error) {
results.push({
test: test.name,
handled: false,
error: error.message,
isTimezoneError: error.message.includes('timezone') ||
error.message.includes('time zone')
});
}
}
return results;
}
);
timezoneConversionErrors.forEach(result => {
t.ok(result.handled || result.isTimezoneError,
`Timezone error ${result.test} should be handled appropriately`);
});
// Test 10: Cross-format timezone preservation
const crossFormatTimezones = await performanceTracker.measureAsync(
'cross-format-timezone-preservation',
async () => {
const testData = {
dateTime: '2024-01-15T14:30:00+05:30',
timezone: 'Asia/Kolkata'
};
const sourceUBL = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>TZ-PRESERVE-001</ID>
<IssueDate>2024-01-15</IssueDate>
<IssueTime>${testData.dateTime}</IssueTime>
</Invoice>`;
const conversions = ['cii', 'xrechnung', 'facturx'];
const results = [];
for (const targetFormat of conversions) {
try {
const converted = await einvoice.convertFormat(sourceUBL, targetFormat);
const reparsed = await einvoice.parseDocument(converted);
const extractedDateTime = await einvoice.extractDateTime(reparsed);
results.push({
targetFormat,
converted: true,
timezonePreserved: extractedDateTime?.timezone === testData.timezone,
offsetPreserved: extractedDateTime?.offset === '+05:30',
dateTimeIntact: extractedDateTime?.iso === testData.dateTime
});
} catch (error) {
results.push({
targetFormat,
converted: false,
error: error.message
});
}
}
return results;
}
);
crossFormatTimezones.forEach(result => {
t.ok(result.converted, `Conversion to ${result.targetFormat} should succeed`);
if (result.converted) {
t.ok(result.timezonePreserved || result.offsetPreserved,
'Timezone information should be preserved');
}
});
// Print performance summary
performanceTracker.printSummary();
});
// Run the test
tap.start();