Files
edr-platform/apps/edr-freight-api/src/modules/train-scheduling/batch-window.util.spec.ts
2026-06-13 07:59:48 +00:00

137 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
getBatchWindowForTimestamp,
listBatchWindowsForDate,
listBatchWindowsForBookings,
BATCH_WINDOW_START_HOURS,
boardWindowForTimestamp,
listBoardWindowsForRange,
groupBookingsIntoBoardWindows,
} from './batch-window.util';
describe('batch-window.util', () => {
it('maps 20:15 EAT to the 19:0022:00 window', () => {
// 20:15 EAT = 17:15 UTC on 11 Jun 2026
const ts = new Date('2026-06-11T17:15:00.000Z');
const window = getBatchWindowForTimestamp(ts);
expect(window.label).toContain('19:00');
expect(window.label).toContain('22:00');
expect(window.label).toContain('11 Jun 2026');
});
it('maps 08:30 EAT to the 07:0010:00 window', () => {
const ts = new Date('2026-06-11T05:30:00.000Z'); // 08:30 EAT
const window = getBatchWindowForTimestamp(ts);
expect(window.label).toContain('07:00');
expect(window.label).toContain('10:00');
});
it('maps 02:00 EAT to the previous day 22:0007:00 window', () => {
const ts = new Date('2026-06-11T23:00:00.000Z'); // 02:00 EAT on 12 Jun
const window = getBatchWindowForTimestamp(ts);
expect(window.label).toContain('22:00');
expect(window.label).toContain('07:00');
expect(window.label).toContain('11 Jun 2026');
});
it('lists six windows for a calendar day', () => {
const ref = new Date('2026-06-11T12:00:00.000Z');
const windows = listBatchWindowsForDate(ref);
expect(windows).toHaveLength(BATCH_WINDOW_START_HOURS.length);
expect(windows[0].label).toContain('07:00');
expect(windows[windows.length - 1].label).toContain('22:00');
});
it('includes cross-day overnight window when booking signed at 00:02 EAT', () => {
// 21:02 UTC = 00:02 EAT on 12 Jun → belongs to 11 Jun 22:0007:00 window
const fullyExecutedAt = new Date('2026-06-11T21:02:05.153Z');
const scheduleDate = new Date('2026-06-12T06:00:00.000Z');
const windows = listBatchWindowsForBookings([fullyExecutedAt], scheduleDate);
const overnight = windows.find((w) => w.label.includes('22:00') && w.label.includes('07:00'));
expect(overnight).toBeDefined();
expect(overnight!.label).toContain('11 Jun 2026');
expect(getBatchWindowForTimestamp(fullyExecutedAt).key).toBe(overnight!.key);
});
});
describe('batch-window board windows (midnight-based 3h slots)', () => {
it('maps 04:00 EAT to the 03:0006:00 slot', () => {
// 01:00 UTC = 04:00 EAT on 11 Jun
const w = boardWindowForTimestamp(new Date('2026-06-11T01:00:00.000Z'));
expect(w.label).toContain('03:00');
expect(w.label).toContain('06:00');
expect(w.date).toBe('2026-06-11');
expect(w.dateLabel).toContain('11 Jun');
});
it('maps 00:30 EAT to the 00:0003:00 slot of that EAT day', () => {
// 21:30 UTC on 10 Jun = 00:30 EAT on 11 Jun
const w = boardWindowForTimestamp(new Date('2026-06-10T21:30:00.000Z'));
expect(w.label).toContain('00:00');
expect(w.label).toContain('03:00');
expect(w.date).toBe('2026-06-11');
});
it('maps 23:00 EAT to the final 21:0024:00 slot', () => {
// 20:00 UTC = 23:00 EAT on 11 Jun
const w = boardWindowForTimestamp(new Date('2026-06-11T20:00:00.000Z'));
expect(w.label).toContain('21:00');
expect(w.label).toContain('24:00');
expect(w.date).toBe('2026-06-11');
});
it('lists a continuous range open→departure clamped at both ends', () => {
// open 05 Jun 08:00 EAT (05:00 UTC) → departs 08 Jun 14:00 EAT (11:00 UTC)
const open = new Date('2026-06-05T05:00:00.000Z');
const departure = new Date('2026-06-08T11:00:00.000Z');
const windows = listBoardWindowsForRange(open, departure);
// Day 5: 06,09,12,15,18,21 = 6 ; Days 6,7: 8 each ; Day 8: 00,03,06,09,12 = 5
expect(windows).toHaveLength(6 + 8 + 8 + 5);
expect(windows[0].date).toBe('2026-06-05');
expect(windows[0].label).toContain('06:00');
expect(windows[0].label).toContain('09:00');
const last = windows[windows.length - 1];
expect(last.date).toBe('2026-06-08');
expect(last.label).toContain('12:00');
expect(last.label).toContain('15:00');
// chronological + unique keys
const keys = windows.map((w) => w.key);
expect(new Set(keys).size).toBe(keys.length);
});
it('handles a same-day open→departure range', () => {
const open = new Date('2026-06-05T05:00:00.000Z'); // 08:00 EAT (0609 slot)
const departure = new Date('2026-06-05T11:00:00.000Z'); // 14:00 EAT (1215 slot)
const windows = listBoardWindowsForRange(open, departure);
// 06,09,12 = 3 slots
expect(windows).toHaveLength(3);
expect(windows.every((w) => w.date === '2026-06-05')).toBe(true);
});
it('buckets bookings by fullyExecutedAt and keeps empty + pending windows', () => {
const open = new Date('2026-06-05T05:00:00.000Z');
const departure = new Date('2026-06-06T11:00:00.000Z');
const items = [
{ id: 'a', ts: new Date('2026-06-05T05:30:00.000Z') }, // 08:30 EAT → 0609 on 5th
{ id: 'b', ts: null }, // pending
];
const map = groupBookingsIntoBoardWindows(
items,
(i) => i.ts,
open,
departure,
'pending-contract',
);
const pending = map.get('pending-contract');
expect(pending?.items.map((i) => i.id)).toEqual(['b']);
const withA = [...map.values()].find((b) => b.items.some((i) => i.id === 'a'));
expect(withA?.window?.date).toBe('2026-06-05');
// empty slots are retained for the UI
const emptyCount = [...map.values()].filter(
(b) => b.window && b.items.length === 0,
).length;
expect(emptyCount).toBeGreaterThan(0);
});
});