181 lines
6.5 KiB
JavaScript
181 lines
6.5 KiB
JavaScript
import {
|
|
baseLine,
|
|
nextActionLabel,
|
|
routingReason,
|
|
routingRoute,
|
|
summariseRouting
|
|
} from '@/lib/routingSummary';
|
|
|
|
/**
|
|
* The routing block, turned into sentences.
|
|
*
|
|
* This is what a support agent reads to a rider who has rung asking why his
|
|
* screen says hand over at a base instead of deliver to the customer. Two
|
|
* things carry the weight here:
|
|
*
|
|
* 1. A PROJECTION MUST NOT READ AS A DECISION. Before pickup nothing has been
|
|
* routed; `decided: false` means the answer is what WILL happen. Stating a
|
|
* forecast as a fact is a wrong answer delivered confidently.
|
|
* 2. THE WIRE SAYS HUB, PEOPLE SAY BASE. The translation happens at this edge
|
|
* and nowhere else, and it never travels back up the wire.
|
|
*/
|
|
|
|
const HUB_ROUTED = {
|
|
pickup_source_type: 'customer',
|
|
pickup_source_name: 'Anitha R',
|
|
from_address: '12 Race Course Road, Coimbatore',
|
|
from_pincode: '641018',
|
|
to_address: '44 Mount Road, Chennai',
|
|
destination_pincode: '600002',
|
|
is_hyperlocal: false,
|
|
consignment_state: 'Created',
|
|
next_action: 'inward_at_hub',
|
|
next_hub: {
|
|
id: 1,
|
|
name: 'Coimbatore Hub',
|
|
address: '14 Avinashi Road, Peelamedu, Coimbatore',
|
|
pincode: '641004',
|
|
latitude: 11.0272,
|
|
longitude: 76.9905
|
|
},
|
|
inwardedat: null,
|
|
decided: true
|
|
};
|
|
|
|
const HYPERLOCAL = {
|
|
...HUB_ROUTED,
|
|
destination_pincode: '641004',
|
|
is_hyperlocal: true,
|
|
consignment_state: 'Out_for_Delivery',
|
|
next_action: 'deliver',
|
|
next_hub: null
|
|
};
|
|
|
|
describe('routingReason — why it is going that way', () => {
|
|
it('should say a base is involved when the areas differ', () => {
|
|
expect(routingReason(HUB_ROUTED)).toMatch(/through a base/i);
|
|
});
|
|
|
|
it('should say the rider carries it straight there when the areas match', () => {
|
|
expect(routingReason(HYPERLOCAL)).toMatch(/straight to the receiver/i);
|
|
});
|
|
|
|
it('should never say "hub" to an operator', () => {
|
|
// Internal vocabulary. Screens say base.
|
|
expect(routingReason(HUB_ROUTED).toLowerCase()).not.toContain('hub');
|
|
});
|
|
|
|
it('should return an empty string rather than throw when there is no routing block', () => {
|
|
// An older booking, or a backend that has not shipped this yet.
|
|
expect(routingReason(null)).toBe('');
|
|
expect(routingReason(undefined)).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('routingRoute — the from → to line', () => {
|
|
it('should show both pincodes', () => {
|
|
expect(routingRoute(HUB_ROUTED)).toContain('641018');
|
|
expect(routingRoute(HUB_ROUTED)).toContain('600002');
|
|
});
|
|
|
|
it('should not caveat a decision that has actually been taken', () => {
|
|
expect(routingRoute(HUB_ROUTED)).not.toMatch(/not yet collected/i);
|
|
});
|
|
|
|
it('should mark an undecided booking as a forecast, not a fact', () => {
|
|
const undecided = { ...HUB_ROUTED, decided: false };
|
|
expect(routingRoute(undecided)).toMatch(/not yet collected/i);
|
|
expect(routingRoute(undecided)).toMatch(/will happen, not what has/i);
|
|
});
|
|
|
|
it('should show a dash for a pincode the booking never carried', () => {
|
|
// The delivery-pincode column is genuinely empty on plenty of rows; a blank
|
|
// gap in the line reads as a rendering bug rather than as missing data.
|
|
expect(routingRoute({ ...HUB_ROUTED, destination_pincode: '' })).toContain('—');
|
|
});
|
|
});
|
|
|
|
describe('nextActionLabel — what the rider does next', () => {
|
|
it('should translate every action the backend can send', () => {
|
|
expect(nextActionLabel('pickup')).toMatch(/collect/i);
|
|
expect(nextActionLabel('inward_at_hub')).toMatch(/base/i);
|
|
expect(nextActionLabel('start_delivery')).toMatch(/delivery run/i);
|
|
expect(nextActionLabel('deliver')).toMatch(/receiver/i);
|
|
expect(nextActionLabel('handed_to_hub')).toMatch(/handed over/i);
|
|
expect(nextActionLabel('none')).toMatch(/nothing further/i);
|
|
});
|
|
|
|
it('should render inward_at_hub as a BASE handover, never as a hub one', () => {
|
|
expect(nextActionLabel('inward_at_hub').toLowerCase()).not.toContain('hub');
|
|
});
|
|
|
|
it('should show an unknown action as itself rather than swallow it', () => {
|
|
// The backend may add actions. A blank row tells the operator nothing; the
|
|
// raw word at least tells them something new exists.
|
|
expect(nextActionLabel('return_to_sender')).toBe('return_to_sender');
|
|
});
|
|
|
|
it('should show a dash when there is no action at all', () => {
|
|
expect(nextActionLabel(undefined)).toBe('—');
|
|
expect(nextActionLabel('')).toBe('—');
|
|
});
|
|
});
|
|
|
|
describe('baseLine — naming the base', () => {
|
|
it('should read the base with its pincode', () => {
|
|
expect(baseLine(HUB_ROUTED.next_hub)).toBe('Coimbatore Hub · 641004');
|
|
});
|
|
|
|
it('should fall back to the name alone when a base has no pincode', () => {
|
|
expect(baseLine({ name: 'Coimbatore Hub' })).toBe('Coimbatore Hub');
|
|
});
|
|
|
|
it('should be null when no base is involved', () => {
|
|
// A hyperlocal parcel has no base. That is the correct answer, not a gap.
|
|
expect(baseLine(null)).toBeNull();
|
|
expect(baseLine({})).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('summariseRouting — everything the drawer renders', () => {
|
|
it('should carry the base, its address and the next step for a hub-routed parcel', () => {
|
|
const s = summariseRouting(HUB_ROUTED);
|
|
expect(s.base).toBe('Coimbatore Hub · 641004');
|
|
expect(s.baseAddress).toBe('14 Avinashi Road, Peelamedu, Coimbatore');
|
|
expect(s.nextAction).toMatch(/base/i);
|
|
expect(s.consignmentState).toBe('Created');
|
|
expect(s.decided).toBe(true);
|
|
});
|
|
|
|
it('should carry no base for a hyperlocal parcel', () => {
|
|
const s = summariseRouting(HYPERLOCAL);
|
|
expect(s.base).toBeNull();
|
|
expect(s.baseAddress).toBeNull();
|
|
expect(s.nextAction).toMatch(/receiver/i);
|
|
});
|
|
|
|
it('should surface the received-at time once the parcel is actually inwarded', () => {
|
|
const s = summariseRouting({
|
|
...HUB_ROUTED,
|
|
consignment_state: 'Inwarded_at_Hub',
|
|
next_action: 'handed_to_hub',
|
|
inwardedat: '2026-09-02T14:22:10Z'
|
|
});
|
|
expect(s.inwardedAt).toBe('2026-09-02T14:22:10Z');
|
|
expect(s.nextAction).toMatch(/handed over/i);
|
|
});
|
|
|
|
it('should show a dash for a parcel that has no consignment yet', () => {
|
|
const s = summariseRouting({ ...HUB_ROUTED, consignment_state: '', decided: false });
|
|
expect(s.consignmentState).toBe('—');
|
|
expect(s.decided).toBe(false);
|
|
});
|
|
|
|
it('should be null when the booking carries no routing block, so the panel hides', () => {
|
|
// An older backend, or a booking fetched before this shipped. The drawer
|
|
// renders nothing rather than a panel of dashes.
|
|
expect(summariseRouting(null)).toBeNull();
|
|
expect(summariseRouting(undefined)).toBeNull();
|
|
});
|
|
});
|