import { BATCHES, getBatchForHour, batchRangeLabel, getRowBatchId, getBatchLabel } from '@/lib/batchBucket'; /** * Morning/Afternoon/Evening wave bucketing. * * The bug these tests exist to prevent: the windows used to cover only 14.5 of * 24 hours, so an order placed at 2:43pm belonged to no batch and vanished from * every batch filter. The invariant is TOTAL COVERAGE — every hour of the day * lands in exactly one batch — plus the field the bucketing reads (`orderdate`, * never `assigntime` or `expecteddeliverytime`). */ describe('batchBucket', () => { describe('BATCHES definition', () => { it('should define exactly the three operator-facing waves', () => { expect(BATCHES.map((b) => b.id)).toEqual(['morning', 'afternoon', 'evening']); }); it('should cover the whole day with no gap and no overlap', () => { const sorted = [...BATCHES].sort((a, b) => a.startHour - b.startHour); expect(sorted[0].startHour).toBe(0); expect(sorted[sorted.length - 1].endHour).toBe(24); sorted.slice(1).forEach((batch, index) => { expect(batch.startHour).toBe(sorted[index].endHour); }); }); it('should assign every quarter-hour of the day to exactly one batch', () => { for (let hour = 0; hour < 24; hour += 0.25) { const matches = BATCHES.filter((b) => hour >= b.startHour && hour < b.endHour); expect(matches).toHaveLength(1); } }); }); describe('getBatchForHour', () => { it.each([ [0, 'morning'], [8.99, 'morning'], [9, 'afternoon'], [14.716, 'afternoon'], [15.99, 'afternoon'], [16, 'evening'], [23.99, 'evening'] ])('should bucket hour %p into %s', (hour, expected) => { expect(getBatchForHour(hour)).toBe(expected); }); it('should treat every window as half-open [start, end)', () => { expect(getBatchForHour(9)).toBe('afternoon'); expect(getBatchForHour(8.999999)).toBe('morning'); expect(getBatchForHour(16)).toBe('evening'); expect(getBatchForHour(15.999999)).toBe('afternoon'); }); it.each([-0.1, 24, 25, NaN])('should return null for the out-of-range hour %p', (hour) => { expect(getBatchForHour(hour)).toBeNull(); }); }); describe('getRowBatchId', () => { it('should bucket on orderdate — the booking creation time', () => { expect(getRowBatchId({ orderdate: '2026-08-27T07:15:00' })).toBe('morning'); expect(getRowBatchId({ orderdate: '2026-08-27T10:00:00' })).toBe('afternoon'); expect(getRowBatchId({ orderdate: '2026-08-27T18:30:00' })).toBe('evening'); }); it('should bucket the 2:43pm order that used to fall into the old 12:30-4pm black hole', () => { expect(getRowBatchId({ orderdate: '2026-08-27T14:43:00' })).toBe('afternoon'); }); it('should use fractional hours so the back half of an hour is not mis-bucketed', () => { expect(getRowBatchId({ orderdate: '2026-08-27T15:45:00' })).toBe('afternoon'); expect(getRowBatchId({ orderdate: '2026-08-27T16:01:00' })).toBe('evening'); }); it('should strip a false trailing Z rather than shifting the row into another batch', () => { // 20:00 with a false Z read as a real UTC instant becomes 01:30 the next // day in IST, which would move an evening order into Morning. expect(getRowBatchId({ orderdate: '2026-08-27T20:00:00Z' })).toBe('evening'); }); it('should refuse to bucket a bare date, which has no time component', () => { // Midnight is a real morning-batch hour, so guessing would silently pile // every date-only row into Morning. expect(getRowBatchId({ orderdate: '2026-08-27' })).toBeNull(); expect(getRowBatchId({ orderdate: ' 2026-08-27 ' })).toBeNull(); }); it.each([ ['a row with no orderdate', {}], ['a null orderdate', { orderdate: null }], ['an empty orderdate', { orderdate: '' }], ['a null row', null], ['an undefined row', undefined] ])('should return null for %s', (_label, row) => { expect(getRowBatchId(row)).toBeNull(); }); it('should return null for an unparseable orderdate rather than throwing', () => { expect(getRowBatchId({ orderdate: 'yesterday-ish' })).toBeNull(); }); it('should ignore assigntime and expecteddeliverytime entirely', () => { // Both were tried and rejected: assigntime is re-stamped by any status // change, expecteddeliverytime describes the promised slot. const row = { orderdate: '2026-08-27T07:00:00', assigntime: '2026-08-27T18:00:00', expecteddeliverytime: '2026-08-27T16:30:00' }; expect(getRowBatchId(row)).toBe('morning'); }); }); describe('labels', () => { it('should describe the morning and afternoon windows as ranges', () => { expect(batchRangeLabel(BATCHES[0])).toBe('12 AM–9 AM'); expect(batchRangeLabel(BATCHES[1])).toBe('9 AM–4 PM'); }); it('should describe a window that runs to end of day as "After X"', () => { expect(batchRangeLabel(BATCHES[2])).toBe('After 4 PM'); }); it('should render a half-hour boundary with minutes', () => { expect(batchRangeLabel({ startHour: 12.5, endHour: 16 })).toBe('12:30 PM–4 PM'); }); it('should return the human label for a known batch id', () => { expect(getBatchLabel('morning')).toBe('Morning Batch'); expect(getBatchLabel('evening')).toBe('Evening Batch'); }); it('should fall back to the id itself for an unknown batch', () => { expect(getBatchLabel('midnight')).toBe('midnight'); }); }); });