Files
doormilxpress_astryx/tests/lib/batchBucket.test.js
2026-08-27 13:40:48 +05:30

139 lines
5.5 KiB
JavaScript
Raw Permalink 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 { 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');
});
});
});