updates on bugs on the dispatch and ai button integrations

This commit is contained in:
2026-09-24 11:50:04 +05:30
parent 5eb4a6466b
commit cbce47f949
18 changed files with 1190 additions and 124 deletions

View File

@@ -224,7 +224,7 @@ describe('ZoneContext', () => {
return setup();
};
it.each(['hubid', 'sourcehubid', 'applocationid', 'tenantlocationid', 'locationid'])(
it.each(['hubid', 'sourcehubid', 'servicinghubid', 'pickuphubid', 'nearesthubid'])(
'should match on a direct %s',
(field) => {
const { result } = selectKoramangala();
@@ -232,6 +232,30 @@ describe('ZoneContext', () => {
}
);
// CHANGED. These three used to be asserted as matches, and that was the
// bug rather than the contract.
//
// A staff zone is a HUB. `applocationid` numbers cities, `tenantlocationid`
// and `locationid` number a client's own sites — so matching them against a
// hub id is a category error that lands orders in cities they never touched.
// A delivery row carries several at once (queries.js sets servicinghubid,
// applocationid and tenantlocationid together): DM-626241 is servicinghubid
// 5, Bangalore Earth Hub, alongside tenantlocationid 3, which read as a hub
// id is Hyderabad Mars Hub.
//
// The old `??` chain hid it by stopping at the first non-null field, so the
// collision only fired when the earlier fields happened to be missing.
//
// A tenant session is the mirror image and keeps its own assertions below:
// there the zone IS a tenantlocationid, and hub ids are the wrong space.
it.each(['applocationid', 'tenantlocationid', 'locationid'])(
'should NOT match a staff hub zone on a %s — different id space',
(field) => {
const { result } = selectKoramangala();
expect(result.current.matchesZone({ bookingid: 1, [field]: '2' })).toBe(false);
}
);
it('should match a numeric hub id against the string zone id', () => {
const { result } = selectKoramangala();
expect(result.current.matchesZone({ hubid: 2 })).toBe(true);

View File

@@ -0,0 +1,64 @@
import dayjs from 'dayjs';
import { filterByCreatedAt } from '@/lib/dateRange';
import { parseDoormileTimestamp } from '@/lib/doormileTimestamp';
/**
* The date filter the Orders and Bookings lists share.
*
* Both read the same rows from the same endpoint through the same picker, so
* the comparison has to be one piece of code. The shapes below are the three
* the picker actually emits — a plain day, a `{from, to}` preset, and the
* string 'all' — and getting any of them wrong empties the table while looking
* like a legitimate "no bookings today".
*/
const row = (createdat, bookingno) => ({ bookingno, createdat });
const ROWS = [
row('2026-09-22 15:48:00', 'DM-A'),
row('2026-09-23 10:10:00', 'DM-B'),
row('2026-09-24 10:58:00', 'DM-C'),
];
const nos = (rows) => rows.map((r) => r.bookingno);
describe('filterByCreatedAt', () => {
it('a single day keeps only that day', () => {
expect(nos(filterByCreatedAt(ROWS, '2026-09-23', parseDoormileTimestamp))).toEqual(['DM-B']);
});
it('a {from,to} preset keeps the inclusive span', () => {
const got = filterByCreatedAt(ROWS, { from: '2026-09-22', to: '2026-09-23' }, parseDoormileTimestamp);
expect(nos(got)).toEqual(['DM-A', 'DM-B']);
});
it("'all' keeps everything", () => {
expect(nos(filterByCreatedAt(ROWS, 'all', parseDoormileTimestamp))).toEqual(['DM-A', 'DM-B', 'DM-C']);
});
it('an open-ended from keeps that day onwards', () => {
const got = filterByCreatedAt(ROWS, { from: '2026-09-23', to: '' }, parseDoormileTimestamp);
expect(nos(got)).toEqual(['DM-B', 'DM-C']);
});
it('a row whose timestamp will not parse is dropped from a dated view', () => {
// and NOT from an undated one — the row exists, and "every booking" has to
// include it even when its timestamp is unusable.
const withJunk = [...ROWS, row('not a date', 'DM-JUNK')];
expect(nos(filterByCreatedAt(withJunk, '2026-09-24', parseDoormileTimestamp))).toEqual(['DM-C']);
expect(nos(filterByCreatedAt(withJunk, 'all', parseDoormileTimestamp))).toContain('DM-JUNK');
});
it('an empty or missing value is not a filter', () => {
expect(filterByCreatedAt(ROWS, '', parseDoormileTimestamp)).toHaveLength(3);
expect(filterByCreatedAt(ROWS, undefined, parseDoormileTimestamp)).toHaveLength(3);
});
it('survives a null row list', () => {
expect(filterByCreatedAt(null, '2026-09-24', parseDoormileTimestamp)).toEqual([]);
});
it("today's default shows today's bookings and nothing else", () => {
const today = dayjs().format('YYYY-MM-DD');
const rows = [row(`${today} 09:00:00`, 'TODAY'), row('2020-01-01 09:00:00', 'OLD')];
expect(nos(filterByCreatedAt(rows, today, parseDoormileTimestamp))).toEqual(['TODAY']);
});
});

View File

@@ -0,0 +1,134 @@
import { makeLocationFilter, ZONE_RADIUS_KM } from '@/lib/locationScope';
/**
* Control X's hub picker.
*
* Its `applocationid` was destructured out of the fetchDeliveries query key and
* then ignored — the same silent no-op the folder's CLAUDE.md records for
* startdate/enddate, and it survived the fix. Picking a hub rebuilt the key,
* refetched, and returned the same nationwide rows, so the header read
* "Coimbatore Neptune Hub" over a map drawing a Bangalore rider on Mysore Road.
*
* The hubs below are the real rows, including `Coimbatore Test Hub` at 0,0 —
* the Gulf of Guinea — which must never be used as a centre.
*/
const HUBS = [
{ hubid: 1, hubname: 'Coimbatore Jupiter Hub', applocationid: 1, latitude: 11.0168, longitude: 76.9558, pincode: '641012' },
{ hubid: 2, hubname: 'Coimbatore Neptune Hub', applocationid: 1, latitude: 11.0301, longitude: 77.0034, pincode: '641004' },
{ hubid: 17, hubname: 'Coimbatore Test Hub', applocationid: 1, latitude: 0, longitude: 0, pincode: '641099' },
{ hubid: 5, hubname: 'Bangalore Earth Hub', applocationid: 3, latitude: 12.9352, longitude: 77.6244, pincode: '560034' },
{ hubid: 11, hubname: 'Bangalore Whitefield Hub', applocationid: 3, latitude: 12.9698, longitude: 77.7499, pincode: '560066' },
];
const COIMBATORE = 1;
const BANGALORE = 3;
// DM-626241 as fetchDeliveries normalises it: Koramangala → Bangalore University.
const bangaloreRun = (over = {}) => ({
bookingid: 11, orderid: 'DM-626241', servicinghubid: 5,
pickuplatitude: 12.9352, pickuplongitude: 77.6244,
deliverylatitude: 12.94393, deliverylongitude: 77.50918,
...over,
});
const coimbatoreRun = (over = {}) => ({
bookingid: 9, orderid: 'DM-956478', servicinghubid: 1,
pickuplatitude: 11.0168, pickuplongitude: 76.9558,
deliverylatitude: 11.0183, deliverylongitude: 76.9680,
...over,
});
describe('the bug this closes', () => {
it('a Bangalore run does not appear under Coimbatore', () => {
const inCoimbatore = makeLocationFilter(HUBS, COIMBATORE);
expect(inCoimbatore(bangaloreRun())).toBe(false);
});
it('and still appears under Bangalore', () => {
const inBangalore = makeLocationFilter(HUBS, BANGALORE);
expect(inBangalore(bangaloreRun())).toBe(true);
});
it('a Coimbatore run appears under Coimbatore and not Bangalore', () => {
expect(makeLocationFilter(HUBS, COIMBATORE)(coimbatoreRun())).toBe(true);
expect(makeLocationFilter(HUBS, BANGALORE)(coimbatoreRun())).toBe(false);
});
});
describe('"All locations" is not a filter', () => {
it.each([0, '0', '', null, undefined, 'nonsense'])('%p returns null', (id) => {
expect(makeLocationFilter(HUBS, id)).toBeNull();
});
it('a city with no hubs fails open rather than emptying the board', () => {
// A blank board reads as "no work today". That is the more expensive
// mistake than showing too much, so an unresolvable picker shows all.
expect(makeLocationFilter(HUBS, 99)).toBeNull();
expect(makeLocationFilter([], COIMBATORE)).toBeNull();
expect(makeLocationFilter(null, COIMBATORE)).toBeNull();
});
});
describe('how a row is placed', () => {
it('by the hub it names, with no coordinates at all', () => {
const inBangalore = makeLocationFilter(HUBS, BANGALORE);
expect(inBangalore({ orderid: 'X', pickuphubid: 11 })).toBe(true);
expect(makeLocationFilter(HUBS, COIMBATORE)({ orderid: 'X', pickuphubid: 11 })).toBe(false);
});
it('by either end of the journey, not just the pickup', () => {
// A run that collects in Coimbatore and delivers there is Coimbatore's
// work whichever end you look at; one that only arrives is too.
const inCoimbatore = makeLocationFilter(HUBS, COIMBATORE);
const inbound = { orderid: 'IN', pickuplatitude: 12.9352, pickuplongitude: 77.6244,
deliverylatitude: 11.0168, deliverylongitude: 76.9558 };
expect(inCoimbatore(inbound)).toBe(true);
});
it('a stop just outside the radius does not belong', () => {
const inBangalore = makeLocationFilter(HUBS, BANGALORE);
// ~0.01 degree of latitude is ~1.1km, so 0.5 degrees is ~55km north.
const farAway = { orderid: 'FAR', pickuplatitude: 13.45, pickuplongitude: 77.6244,
deliverylatitude: 13.46, deliverylongitude: 77.6244 };
expect(inBangalore(farAway)).toBe(false);
});
it('the radius is the one ZoneContext uses', () => {
expect(ZONE_RADIUS_KM).toBe(35);
});
});
describe('a hub at 0,0 is not a centre', () => {
it('Coimbatore Test Hub does not pull the Gulf of Guinea into Coimbatore', () => {
// hub 17 is latitude 0, longitude 0 in the live data. Treated as a real
// point it would claim everything within 35km of the equator's origin.
const inCoimbatore = makeLocationFilter(HUBS, COIMBATORE);
expect(inCoimbatore({ orderid: 'NULL_ISLAND', pickuplatitude: 0.1, pickuplongitude: 0.1,
deliverylatitude: 0.1, deliverylongitude: 0.1 })).toBe(false);
});
it('but its hub id still places a row, since the id is not a guess', () => {
expect(makeLocationFilter(HUBS, COIMBATORE)({ orderid: 'T', servicinghubid: 17 })).toBe(true);
});
});
describe('a row nobody can place stays visible', () => {
it('no hub id and no usable coordinates — kept under every city', () => {
// Dropping it would remove it from ALL cities at once, with nothing on
// screen to say it had been hidden. That is the failure mode this whole
// area keeps producing, and it is worse than one stray row.
const orphan = { orderid: 'ORPHAN' };
expect(makeLocationFilter(HUBS, COIMBATORE)(orphan)).toBe(true);
expect(makeLocationFilter(HUBS, BANGALORE)(orphan)).toBe(true);
});
it('a row with 0,0 coordinates counts as unplaceable, not as an address', () => {
const unpinned = { orderid: 'ZERO', pickuplatitude: 0, pickuplongitude: 0,
deliverylatitude: 0, deliverylongitude: 0 };
expect(makeLocationFilter(HUBS, COIMBATORE)(unpinned)).toBe(true);
});
it('a null row is not a row', () => {
expect(makeLocationFilter(HUBS, COIMBATORE)(null)).toBe(false);
});
});

View File

@@ -0,0 +1,180 @@
import { renderHook, act } from '@testing-library/react';
jest.mock('@/lib/doormileHooks', () => ({
__esModule: true,
useHubs: jest.fn(),
useTenantLocations: jest.fn()
}));
jest.mock('@/lib/AuthContext', () => ({ __esModule: true, useAuth: jest.fn() }));
import { ZoneProvider, useZone } from '@/lib/ZoneContext';
import { useHubs, useTenantLocations } from '@/lib/doormileHooks';
import { useAuth } from '@/lib/AuthContext';
/**
* Which orders belong to the selected zone.
*
* These fixtures are the shapes the API really returns, not convenient ones.
* `GET /admin/hubs` serves models.Hub, and `GET /admin/bookings` serves
* models.PickupBooking — so a hub here carries `applocationid` and `city`, and
* a booking carries `pickuphubid` and `nearesthubid` and NOT `hubid`,
* `sourcehubid` or `servicinghubid`. The older suite in ZoneContext.test.jsx
* uses a hub fixture with a `city` the endpoint did not send, which is how a
* green suite sat on top of a city comparison that could never run.
*/
// Hub 5, Bengaluru. `city` is populated by attachHubCities in adminController.
const BLR_EARTH = {
hubid: 5, hubname: 'Bangalore Earth Hub', hubtype: 'sorting_center',
applocationid: 3, city: 'Bangalore', address: '', latitude: 12.9352,
longitude: 77.6244, pincode: '560034', status: 'Active'
};
const CBE_JUPITER = {
hubid: 1, hubname: 'Coimbatore Jupiter Hub', hubtype: 'sorting_center',
applocationid: 1, city: 'Coimbatore', address: '', latitude: 11.0168,
longitude: 76.9558, pincode: '641012', status: 'Active'
};
const wrapper = ({ children }) => <ZoneProvider>{children}</ZoneProvider>;
const setup = ({ user = { tenantid: null }, hubs = [CBE_JUPITER, BLR_EARTH], locations = [] } = {}) => {
useAuth.mockReturnValue({ user });
useHubs.mockReturnValue({ data: hubs, isLoading: false, refetch: jest.fn() });
useTenantLocations.mockReturnValue({ data: locations, isLoading: false, refetch: jest.fn() });
return renderHook(() => useZone(), { wrapper });
};
const inZone = (result, zoneId, item) => {
act(() => result.current.setSelectedZoneId(zoneId));
return result.current.matchesZone(item);
};
beforeEach(() => localStorage.clear());
describe('a booking is matched by the hub it actually records', () => {
it('pickuphubid alone puts the order in that zone', () => {
// The column exists, is populated by createExpressBooking, and was being
// ignored. Nothing else on this row can place it: no coordinates, and an
// address that names neither the hub nor its city.
const { result } = setup();
expect(inZone(result, '5', {
bookingid: 1, bookingno: 'DM-BLR001', pickuphubid: 5,
pickupaddress: 'Gate 3', deliveryaddress: 'Block C'
})).toBe(true);
});
it('nearesthubid alone puts the order in that zone', () => {
const { result } = setup();
expect(inZone(result, '5', {
bookingid: 2, bookingno: 'DM-BLR002', nearesthubid: 5,
pickupaddress: 'Gate 3', deliveryaddress: 'Block C'
})).toBe(true);
});
it('and does NOT put it in a different hub\'s zone', () => {
const { result } = setup();
expect(inZone(result, '1', {
bookingid: 1, bookingno: 'DM-BLR001', pickuphubid: 5,
pickupaddress: 'Gate 3', deliveryaddress: 'Block C'
})).toBe(false);
});
});
describe('a tenant session never matches on a hub id', () => {
const KITCHEN = { tenantlocationid: 5, locationname: 'Indiranagar Kitchen', city: 'Bengaluru',
latitude: 12.9784, longitude: 77.6408, pincode: '560038' };
it('a booking whose pickuphubid is 5 does not land in tenant location 5', () => {
// Two different id spaces. Matching them would file an order under a
// kitchen it has nothing to do with, on a bare numeric collision.
const { result } = setup({ user: { tenantid: '13' }, locations: [KITCHEN] });
expect(inZone(result, '5', {
bookingid: 3, bookingno: 'DM-X', tenantid: '13', pickuphubid: 5,
pickupaddress: 'Gate 3', deliveryaddress: 'Block C'
})).toBe(false);
});
it('but its own tenantlocationid still matches', () => {
const { result } = setup({ user: { tenantid: '13' }, locations: [KITCHEN] });
expect(inZone(result, '5', {
bookingid: 4, bookingno: 'DM-Y', tenantid: '13', tenantlocationid: 5,
pickupaddress: 'Gate 3', deliveryaddress: 'Block C'
})).toBe(true);
});
});
describe('the city comparison, which needs a city on the hub', () => {
it('matches a Bangalore address against the Bangalore hub', () => {
// `hub.city` comes from the applocations join. Before that field existed
// this branch read '' and was skipped on every staff session, leaving a
// 35km radius as the only thing still matching.
const { result } = setup();
expect(inZone(result, '5', {
bookingid: 5, bookingno: 'DM-BLR003',
pickupaddress: '80 Feet Road, Koramangala, Bangalore',
deliveryaddress: '100 Feet Road, Indiranagar, Bangalore'
})).toBe(true);
});
it('a hub with no city does not match everything', () => {
// Failing open here would put every order in every zone.
const { result } = setup({ hubs: [{ ...BLR_EARTH, city: undefined, latitude: undefined, longitude: undefined }] });
expect(inZone(result, '5', {
bookingid: 6, bookingno: 'DM-Z',
pickupaddress: 'Somewhere else entirely', deliveryaddress: 'Also elsewhere'
})).toBe(false);
});
});
describe('an order with no coordinates is not silently lost', () => {
it('still reaches its zone through pickuphubid', () => {
// The case that started this: coordinates were the only working matcher,
// so an order saved without them was invisible in every zone at once.
const { result } = setup();
const noCoords = {
bookingid: 7, bookingno: 'DM-NOCOORD', pickuphubid: 5,
pickuplatitude: 0, pickuplongitude: 0,
deliverylatitude: 0, deliverylongitude: 0,
pickupaddress: 'Gate 3', deliveryaddress: 'Block C'
};
expect(inZone(result, '5', noCoords)).toBe(true);
expect(inZone(result, '1', noCoords)).toBe(false);
});
});
describe('one id space at a time', () => {
// queries.js puts servicinghubid, applocationid and tenantlocationid on the
// SAME delivery row. They are three different numbering schemes, and testing
// them all against a hub id files orders under cities they never touched.
const DM626241 = {
bookingid: 11, bookingno: 'DM-626241',
servicinghubid: 5, // Bangalore Earth Hub — the real owner
tenantlocationid: 3, // a client site; as a HUB id this is Hyderabad
applocationid: 3, // a CITY id; as a hub id this is Hyderabad too
pickupaddress: 'Koramangala', deliveryaddress: 'Mysore Road',
};
const HYDERABAD_MARS = {
hubid: 3, hubname: 'Hyderabad Mars Hub', applocationid: 2, city: 'Hyderabad',
latitude: 17.4483, longitude: 78.3741, pincode: '500032', status: 'Active',
};
it('a Bangalore run does not land in Hyderabad on a numeric collision', () => {
const { result } = setup({ hubs: [CBE_JUPITER, BLR_EARTH, HYDERABAD_MARS] });
expect(inZone(result, '3', DM626241)).toBe(false);
});
it('and still lands in Bangalore, by the hub it actually names', () => {
const { result } = setup({ hubs: [CBE_JUPITER, BLR_EARTH, HYDERABAD_MARS] });
expect(inZone(result, '5', DM626241)).toBe(true);
});
it('a tenant session matches its own locations and no hub id', () => {
const KITCHEN = { tenantlocationid: 5, locationname: 'Indiranagar Kitchen',
city: 'Bengaluru', latitude: 12.9784, longitude: 77.6408, pincode: '560038' };
const { result } = setup({ user: { tenantid: '13' }, locations: [KITCHEN] });
expect(inZone(result, '5', { bookingid: 1, tenantid: '13', tenantlocationid: 5,
pickupaddress: 'a', deliveryaddress: 'b' })).toBe(true);
expect(inZone(result, '5', { bookingid: 2, tenantid: '13', servicinghubid: 5,
pickupaddress: 'a', deliveryaddress: 'b' })).toBe(false);
});
});