jest.mock('@/api/doormile/endpoints', () => ({ __esModule: true })); jest.mock('@/api/doormile/notify', () => ({ __esModule: true, OpenToast: jest.fn(), messageOf: jest.fn() })); import { deriveDeliveryStatus, mapBookingStatusToDeliveryStatus } from '@/api/doormile/queries'; /** * Raw booking/consignment enum -> the Deliveries page's lifecycle key. * * This is the single most consequential mapping in the console: it decides * which tab an order appears under, what the chip counts say, and what badge * the row renders. Three rules in it are explicit product direction rather than * convenience, and each has its own test below: * * 1. Miler_Assigned stays PENDING. Assigning is an OPERATOR action; Accepted * must reflect the RIDER's own action. * 2. Converted_To_Consignment is PICKED, not accepted — the parcel is in the * rider's hands the moment the booking converts. * 3. Once a consignment exists, ITS status wins: the booking's own status * stops moving after conversion, so reading only the booking left an * updated row showing a stale Converted_To_Consignment. */ describe('deriveDeliveryStatus', () => { describe('called with a bare status string', () => { it.each([ ['pending_pickup', 'pending'], ['pending_assignment', 'pending'], ['pending', 'pending'], ['miler_assigned', 'pending'], ['pickup_scheduled', 'accepted'], ['arrived', 'arrived'], ['converted_to_consignment', 'picked'], ['collected_by_miler', 'picked'], ['collected', 'picked'], ['picked', 'picked'], ['out_for_delivery', 'active'], ['in_transit', 'active'], ['intransit', 'active'], ['inwarded_at_hub', 'active'], ['inwarded', 'active'], ['at_hub', 'active'], ['outwarded_from_hub', 'active'], ['in_transit_to_hub', 'active'], ['active', 'active'], ['delivered', 'delivered'], ['completed', 'delivered'], ['cancelled', 'cancelled'], ['canceled', 'cancelled'], ['returned', 'cancelled'], ['failed', 'cancelled'], ['skipped', 'skipped'], ['rto', 'skipped'] ])('should map %s to %s', (raw, expected) => { expect(deriveDeliveryStatus(raw)).toBe(expected); }); it('should match the backend enum regardless of its casing', () => { expect(deriveDeliveryStatus('Out_for_Delivery')).toBe('active'); expect(deriveDeliveryStatus('CONVERTED_TO_CONSIGNMENT')).toBe('picked'); }); it('should tolerate surrounding whitespace', () => { expect(deriveDeliveryStatus(' Delivered ')).toBe('delivered'); }); it('should pass an unmapped enum through lowercased rather than dropping the row', () => { // The page surfaces these under an "Other" tab naming the enum; before // that, such a row was counted nowhere and shown nowhere. expect(deriveDeliveryStatus('Awaiting_Customs_Clearance')).toBe('awaiting_customs_clearance'); }); it.each([null, undefined, '', 0, false])( 'should default the blank status %p to pending', (raw) => { expect(deriveDeliveryStatus(raw)).toBe('pending'); } ); }); describe('the operator-vs-rider split (product rule)', () => { it('should keep Miler_Assigned on PENDING — assigning is an operator action', () => { // Bumping this to 'accepted' made a just-assigned, not-yet-acknowledged // order look already-accepted. Explicit product requirement. expect(deriveDeliveryStatus('miler_assigned')).toBe('pending'); expect(deriveDeliveryStatus({ status: 'Miler_Assigned' })).toBe('pending'); }); it('should treat Pickup_Scheduled as the rider having accepted', () => { expect(deriveDeliveryStatus('pickup_scheduled')).toBe('accepted'); }); it('should treat Converted_To_Consignment as PICKED, not accepted', () => { // It sat on 'accepted', which held a picked-up parcel in the Accepted tab // and left Picked permanently empty. expect(deriveDeliveryStatus('converted_to_consignment')).toBe('picked'); expect(deriveDeliveryStatus({ status: 'Converted_To_Consignment' })).toBe('picked'); }); }); describe('Pickup_Scheduled combined with a reached timestamp', () => { it.each([ ['2026-08-27T10:15:00', 'a real timestamp'], ['0', 'a zero-ish string that is still a value'], [1, 'an epoch-like number'] ])('should report ARRIVED when reachedAt is %p (%s)', (reachedAt) => { expect(deriveDeliveryStatus('pickup_scheduled', reachedAt)).toBe('arrived'); }); it.each([null, undefined, false, ''])( 'should report ACCEPTED when reachedAt is the blank value %p', (reachedAt) => { expect(deriveDeliveryStatus('pickup_scheduled', reachedAt)).toBe('accepted'); } ); it('should read the reached timestamp off a row under any of its field spellings', () => { ['reachedat', 'reached_at', 'reachedAt', 'reachedtime', 'reached_time'].forEach((field) => { expect(deriveDeliveryStatus({ status: 'Pickup_Scheduled', [field]: '2026-08-27T10:15:00' })).toBe('arrived'); }); }); it('should report ACCEPTED for a Pickup_Scheduled row with no reached field at all', () => { expect(deriveDeliveryStatus({ status: 'Pickup_Scheduled' })).toBe('accepted'); }); }); describe('called with a booking row', () => { it('should read the status from `status`, `bookingstatus` or `orderstatus`', () => { expect(deriveDeliveryStatus({ status: 'Delivered' })).toBe('delivered'); expect(deriveDeliveryStatus({ bookingstatus: 'Delivered' })).toBe('delivered'); expect(deriveDeliveryStatus({ orderstatus: 'Delivered' })).toBe('delivered'); }); it('should prefer `status` over the other two spellings', () => { expect(deriveDeliveryStatus({ status: 'Delivered', orderstatus: 'Cancelled' })).toBe('delivered'); }); it('should default a row with no status at all to pending', () => { expect(deriveDeliveryStatus({ bookingid: 4021 })).toBe('pending'); }); }); describe('consignment status precedence', () => { it('should let the consignment status attached to the row win over the booking status', () => { // The booking stops moving once it converts; PUT /consignments/:id/status // writes on the consignment, so reading only the booking showed a stale // Converted_To_Consignment after a successful Update Status. const row = { status: 'Converted_To_Consignment', consignmentstatus: 'Delivered' }; expect(deriveDeliveryStatus(row)).toBe('delivered'); }); it('should accept the snake_case spelling of the attached status', () => { expect(deriveDeliveryStatus({ status: 'Converted_To_Consignment', consignment_status: 'Out_for_Delivery' })).toBe('active'); }); it('should resolve the consignment through a joined map when the row carries only an id', () => { const map = new Map([['40', { status: 'Out_for_Delivery' }]]); expect(deriveDeliveryStatus({ status: 'Converted_To_Consignment', consignmentid: 40 }, map)).toBe('active'); }); it.each(['status', 'consignmentstatus', 'currentstatus', 'consignment_status'])( 'should read the joined record status from its `%s` field', (field) => { const map = new Map([['40', { [field]: 'Delivered' }]]); expect(deriveDeliveryStatus({ status: 'Converted_To_Consignment', consignmentid: 40 }, map)).toBe('delivered'); } ); it('should fall back to the booking status when the joined map has no such consignment', () => { const map = new Map([['99', { status: 'Delivered' }]]); expect(deriveDeliveryStatus({ status: 'Converted_To_Consignment', consignmentid: 40 }, map)).toBe('picked'); }); it('should fall back to the booking status when the consignment status is unrecognised', () => { // Degrades to exactly the behaviour from before the join existed. const row = { status: 'Converted_To_Consignment', consignmentstatus: 'Held_At_Customs' }; expect(deriveDeliveryStatus(row)).toBe('picked'); }); it.each(['', ' ', null, undefined])( 'should ignore a blank attached consignment status (%p)', (consignmentstatus) => { expect(deriveDeliveryStatus({ status: 'Converted_To_Consignment', consignmentstatus })).toBe('picked'); } ); it('should ignore a second argument that is not a Map', () => { // The signature is overloaded (reachedAt | consignmentMap); a plain object // must not be mistaken for a lookup table. const notAMap = { 40: { status: 'Delivered' } }; expect(deriveDeliveryStatus({ status: 'Converted_To_Consignment', consignmentid: 40 }, notAMap)).toBe('picked'); }); it('should still return picked for a Collected_By_Miler booking with no consignment', () => { expect(deriveDeliveryStatus({ status: 'Collected_By_Miler' })).toBe('picked'); }); it('should keep Converted_To_Consignment as picked when joined consignment only has generic status: Active', () => { const map = new Map([['40', { status: 'Active' }]]); expect(deriveDeliveryStatus({ status: 'Converted_To_Consignment', consignmentid: 40 }, map)).toBe('picked'); }); it('should bump Converted_To_Consignment to active when joined consignment has consignmentstatus: Out_for_Delivery', () => { const map = new Map([['40', { status: 'Active', consignmentstatus: 'Out_for_Delivery' }]]); expect(deriveDeliveryStatus({ status: 'Converted_To_Consignment', consignmentid: 40 }, map)).toBe('active'); }); }); describe('mapBookingStatusToDeliveryStatus', () => { it('should be a straight alias so no second copy of the mapping can drift', () => { const cases = [ ['pickup_scheduled', '2026-08-27T10:00:00'], ['miler_assigned', undefined], [{ status: 'Converted_To_Consignment', consignmentstatus: 'Delivered' }, undefined] ]; cases.forEach(([input, second]) => { expect(mapBookingStatusToDeliveryStatus(input, second)).toBe(deriveDeliveryStatus(input, second)); }); }); }); describe('classification invariants', () => { it('should be deterministic — the same input always yields the same tab', () => { const row = { status: 'Pickup_Scheduled', reachedat: '2026-08-27T10:00:00' }; const results = new Set(Array.from({ length: 5 }, () => deriveDeliveryStatus(row))); expect(results.size).toBe(1); }); it('should never return undefined or an empty string for any input shape', () => { const inputs = [null, undefined, '', 0, {}, { status: '' }, 'unknown_enum', { consignmentstatus: 'x' }]; inputs.forEach((input) => { const result = deriveDeliveryStatus(input); expect(typeof result).toBe('string'); expect(result).not.toBe(''); }); }); it('should never mutate the row it classifies', () => { const row = { status: 'Pickup_Scheduled', reachedat: '2026-08-27T10:00:00' }; const snapshot = JSON.parse(JSON.stringify(row)); deriveDeliveryStatus(row); expect(row).toEqual(snapshot); }); }); });