626 lines
27 KiB
JavaScript
626 lines
27 KiB
JavaScript
jest.mock('@/api/doormile/endpoints', () => ({
|
|
__esModule: true,
|
|
getBookings: jest.fn(),
|
|
getBooking: jest.fn(),
|
|
getConsignments: jest.fn(),
|
|
getAdminCustomers: jest.fn(),
|
|
getAdminTenants: jest.fn(),
|
|
getMilers: jest.fn(),
|
|
getMiler: jest.fn(),
|
|
getMilerSummary: jest.fn(),
|
|
getMilerLogs: jest.fn(),
|
|
notifyMiler: jest.fn(),
|
|
getTenantLocations: jest.fn(),
|
|
getHubs: jest.fn(),
|
|
assignMilerToBooking: jest.fn(),
|
|
cancelBooking: jest.fn(),
|
|
updateConsignmentStatus: jest.fn(),
|
|
getAdminPricing: jest.fn(),
|
|
getReports: jest.fn(),
|
|
getLocationsSummary: jest.fn(),
|
|
getProfile: jest.fn()
|
|
}));
|
|
jest.mock('@/api/doormile/notify', () => ({
|
|
__esModule: true,
|
|
OpenToast: jest.fn(),
|
|
messageOf: jest.fn((err, fallback) => err?.message || fallback)
|
|
}));
|
|
|
|
import * as endpoints from '@/api/doormile/endpoints';
|
|
import { OpenToast } from '@/api/doormile/notify';
|
|
import {
|
|
fetchDeliveries,
|
|
fetchCountAPI,
|
|
cancelDeliveryAPI,
|
|
changeRiderAPI,
|
|
updateDeliveryAPI,
|
|
getorderdetails
|
|
} from '@/api/doormile/queries';
|
|
|
|
/**
|
|
* A booking as GET /admin/bookings returns it. Defaults make the row
|
|
* "dispatched" (it carries an assigned rider), because fetchDeliveries only
|
|
* surfaces dispatched rows.
|
|
*/
|
|
const booking = (over = {}) => ({
|
|
bookingid: 4021,
|
|
bookingno: 'DM-4021',
|
|
status: 'Miler_Assigned',
|
|
assignedmileruserid: 91,
|
|
tenantid: 4,
|
|
createdat: '2026-08-27T10:00:00',
|
|
updatedat: '2026-08-27T10:05:00',
|
|
pickupaddress: '12 MG Road, Bengaluru',
|
|
deliveryaddress: '44 Indiranagar, Bengaluru',
|
|
...over
|
|
});
|
|
|
|
/** The query key every caller of fetchDeliveries builds. */
|
|
const key = ({ status = 'all', startdate, enddate, rowsPerPage = 100 } = {}) => [
|
|
'deliveries',
|
|
1,
|
|
null,
|
|
status,
|
|
startdate,
|
|
enddate,
|
|
rowsPerPage
|
|
];
|
|
|
|
const withNoJoins = () => {
|
|
endpoints.getAdminCustomers.mockResolvedValue([]);
|
|
endpoints.getMilers.mockResolvedValue([]);
|
|
endpoints.getAdminTenants.mockResolvedValue([]);
|
|
endpoints.getConsignments.mockResolvedValue([]);
|
|
};
|
|
|
|
describe('deliveries data layer', () => {
|
|
beforeEach(withNoJoins);
|
|
|
|
describe('fetchDeliveries — dispatched filtering', () => {
|
|
it('should surface a booking that already has a rider assigned', async () => {
|
|
endpoints.getBookings.mockResolvedValue([booking()]);
|
|
const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() });
|
|
expect(rows).toHaveLength(1);
|
|
expect(rows[0]).toMatchObject({ bookingid: 4021, deliveryid: 4021, orderheaderid: 4021 });
|
|
});
|
|
|
|
it.each(['pending_pickup', 'created', 'new', 'booked', 'order_placed', 'unassigned'])(
|
|
'should exclude an undispatched %s booking with no rider and no consignment',
|
|
async (status) => {
|
|
endpoints.getBookings.mockResolvedValue([
|
|
booking({ status, assignedmileruserid: null, consignmentid: null })
|
|
]);
|
|
const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() });
|
|
expect(rows).toEqual([]);
|
|
}
|
|
);
|
|
|
|
it('should surface an undispatched-looking booking that nonetheless has a consignment', async () => {
|
|
endpoints.getBookings.mockResolvedValue([
|
|
booking({ status: 'pending_pickup', assignedmileruserid: null, consignmentid: 40 })
|
|
]);
|
|
const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() });
|
|
expect(rows).toHaveLength(1);
|
|
expect(rows[0].consignmentid).toBe(40);
|
|
});
|
|
|
|
it('should surface a booking whose status has moved past the pre-dispatch set', async () => {
|
|
endpoints.getBookings.mockResolvedValue([
|
|
booking({ status: 'Out_for_Delivery', assignedmileruserid: null, consignmentid: null })
|
|
]);
|
|
const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() });
|
|
expect(rows).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('fetchDeliveries — row mapping', () => {
|
|
it('should resolve the rider name from the joined miler list', async () => {
|
|
endpoints.getBookings.mockResolvedValue([booking()]);
|
|
endpoints.getMilers.mockResolvedValue([
|
|
{ userid: 91, milerprofileid: 11, firstname: 'Anita', lastname: 'Rao', phone: '9876543210' }
|
|
]);
|
|
|
|
const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() });
|
|
expect(rows[0]).toMatchObject({
|
|
ridername: 'Anita Rao',
|
|
userid: 91,
|
|
milerprofileid: 11,
|
|
ridercontact: '9876543210'
|
|
});
|
|
});
|
|
|
|
it('should fall back to a numbered rider label when the miler join fails', async () => {
|
|
// A failed join must degrade a display field, not blank the page.
|
|
endpoints.getBookings.mockResolvedValue([booking()]);
|
|
endpoints.getMilers.mockRejectedValue(new Error('milers 500'));
|
|
|
|
const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() });
|
|
expect(rows).toHaveLength(1);
|
|
expect(rows[0].ridername).toBe('Rider #91');
|
|
});
|
|
|
|
it('should resolve the delivery customer from the joined customer list', async () => {
|
|
endpoints.getBookings.mockResolvedValue([booking({ appcustomerid: 7 })]);
|
|
endpoints.getAdminCustomers.mockResolvedValue([
|
|
{ appcustomerid: 7, firstname: 'Vikram', phone: '9000000000' }
|
|
]);
|
|
|
|
const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() });
|
|
expect(rows[0]).toMatchObject({ deliverycustomer: 'Vikram', deliverycontactno: '9000000000', deliverytype: 'B' });
|
|
});
|
|
|
|
it('should mark a row with no matched customer as a C-type delivery', async () => {
|
|
endpoints.getBookings.mockResolvedValue([booking()]);
|
|
const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() });
|
|
expect(rows[0].deliverytype).toBe('C');
|
|
});
|
|
|
|
it('should bucket on the booking creation time, exposed as orderdate', async () => {
|
|
// Batch bucketing and date filtering both read orderdate; if this stops
|
|
// being createdat, rows are counted for a day but belong to no batch.
|
|
endpoints.getBookings.mockResolvedValue([booking({ createdat: '2026-08-27T07:00:00' })]);
|
|
const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() });
|
|
expect(rows[0].orderdate).toBe('2026-08-27T07:00:00');
|
|
});
|
|
|
|
it('should approximate distance as a straight line between the two coordinates', async () => {
|
|
endpoints.getBookings.mockResolvedValue([
|
|
booking({
|
|
pickuplatitude: 12.9716,
|
|
pickuplongitude: 77.5946,
|
|
deliverylatitude: 12.9352,
|
|
deliverylongitude: 77.6245
|
|
})
|
|
]);
|
|
const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() });
|
|
expect(rows[0].kms).toBeGreaterThan(4);
|
|
expect(rows[0].kms).toBeLessThan(7);
|
|
});
|
|
|
|
it('should leave distance undefined — not zero — when a coordinate is missing', async () => {
|
|
// A fabricated 0 km reads as a measured distance and feeds the rate card.
|
|
endpoints.getBookings.mockResolvedValue([booking({ pickuplatitude: null })]);
|
|
const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() });
|
|
expect(rows[0].kms).toBeUndefined();
|
|
});
|
|
|
|
it('should leave route-plan fields undefined rather than fabricating them', async () => {
|
|
endpoints.getBookings.mockResolvedValue([booking()]);
|
|
const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() });
|
|
expect(rows[0].transitminutes).toBeUndefined();
|
|
expect(rows[0].cumulativekms).toBeUndefined();
|
|
expect(rows[0].step).toBeUndefined();
|
|
});
|
|
|
|
it('should count parcels as the row quantity, defaulting to zero', async () => {
|
|
endpoints.getBookings.mockResolvedValue([booking({ parcels: [{}, {}] }), booking({ bookingid: 4022 })]);
|
|
const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() });
|
|
expect(rows[0].quantity).toBe(2);
|
|
expect(rows[1].quantity).toBe(0);
|
|
});
|
|
|
|
it('should carry the service charge through to every amount field the table reads', async () => {
|
|
endpoints.getBookings.mockResolvedValue([
|
|
booking({ serviceoptions: [{ estimatedprice: 240, estimateddeliveryat: '2026-08-27T12:00:00' }] })
|
|
]);
|
|
const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() });
|
|
expect(rows[0]).toMatchObject({ deliverycharges: 240, deliveryamt: 240, deliveryamount: 240 });
|
|
expect(rows[0].expecteddeliverytime).toBe('2026-08-27T12:00:00');
|
|
});
|
|
|
|
it('should let the consignment status drive the row status', async () => {
|
|
endpoints.getBookings.mockResolvedValue([
|
|
booking({ status: 'Converted_To_Consignment', consignmentid: 40 })
|
|
]);
|
|
endpoints.getConsignments.mockResolvedValue([{ consignmentid: 40, status: 'Out_for_Delivery' }]);
|
|
|
|
const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() });
|
|
expect(rows[0].orderstatus).toBe('active');
|
|
expect(rows[0].statusfromconsignment).toBe(true);
|
|
});
|
|
|
|
it('should fall back to the booking status when the consignment join fails', async () => {
|
|
endpoints.getBookings.mockResolvedValue([
|
|
booking({ status: 'Converted_To_Consignment', consignmentid: 40 })
|
|
]);
|
|
endpoints.getConsignments.mockRejectedValue(new Error('consignments 500'));
|
|
|
|
const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() });
|
|
expect(rows[0].orderstatus).toBe('picked');
|
|
});
|
|
});
|
|
|
|
describe('fetchDeliveries — date range', () => {
|
|
const threeDays = [
|
|
booking({ bookingid: 1, createdat: '2026-08-25T10:00:00' }),
|
|
booking({ bookingid: 2, createdat: '2026-08-26T10:00:00' }),
|
|
booking({ bookingid: 3, createdat: '2026-08-27T10:00:00' })
|
|
];
|
|
|
|
it('should return every row when no range is given', async () => {
|
|
endpoints.getBookings.mockResolvedValue(threeDays);
|
|
const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() });
|
|
expect(rows).toHaveLength(3);
|
|
});
|
|
|
|
it('should apply the requested range instead of ignoring it', async () => {
|
|
// The regression: startdate/enddate were accepted and then dropped, so
|
|
// today, yesterday and last week all produced a byte-identical row set.
|
|
endpoints.getBookings.mockResolvedValue(threeDays);
|
|
const { rows } = await fetchDeliveries({
|
|
pageParam: 1,
|
|
queryKey: key({ startdate: '2026-08-26', enddate: '2026-08-26' })
|
|
});
|
|
expect(rows.map((r) => r.bookingid)).toEqual([2]);
|
|
});
|
|
|
|
it('should include both range boundaries', async () => {
|
|
endpoints.getBookings.mockResolvedValue(threeDays);
|
|
const { rows } = await fetchDeliveries({
|
|
pageParam: 1,
|
|
queryKey: key({ startdate: '2026-08-25', enddate: '2026-08-27' })
|
|
});
|
|
expect(rows).toHaveLength(3);
|
|
});
|
|
|
|
it('should treat a missing bound as unbounded on that side', async () => {
|
|
endpoints.getBookings.mockResolvedValue(threeDays);
|
|
const from = await fetchDeliveries({ pageParam: 1, queryKey: key({ startdate: '2026-08-26' }) });
|
|
expect(from.rows.map((r) => r.bookingid)).toEqual([2, 3]);
|
|
|
|
const until = await fetchDeliveries({ pageParam: 1, queryKey: key({ enddate: '2026-08-26' }) });
|
|
expect(until.rows.map((r) => r.bookingid)).toEqual([1, 2]);
|
|
});
|
|
|
|
it('should keep a late-evening row on its own day despite a false trailing Z', async () => {
|
|
// Unstripped, 23:45Z shifts +5:30 into the next day and drops out of the
|
|
// selected date entirely.
|
|
endpoints.getBookings.mockResolvedValue([booking({ createdat: '2026-08-27T23:45:00Z' })]);
|
|
const { rows } = await fetchDeliveries({
|
|
pageParam: 1,
|
|
queryKey: key({ startdate: '2026-08-27', enddate: '2026-08-27' })
|
|
});
|
|
expect(rows).toHaveLength(1);
|
|
});
|
|
|
|
it('should drop a row with an unparseable creation date when a range is set', async () => {
|
|
endpoints.getBookings.mockResolvedValue([booking({ createdat: 'garbage' })]);
|
|
const { rows } = await fetchDeliveries({
|
|
pageParam: 1,
|
|
queryKey: key({ startdate: '2026-08-27', enddate: '2026-08-27' })
|
|
});
|
|
expect(rows).toEqual([]);
|
|
});
|
|
|
|
it('should return an empty page for a range with no rows', async () => {
|
|
endpoints.getBookings.mockResolvedValue(threeDays);
|
|
const { rows } = await fetchDeliveries({
|
|
pageParam: 1,
|
|
queryKey: key({ startdate: '2026-01-01', enddate: '2026-01-02' })
|
|
});
|
|
expect(rows).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('fetchDeliveries — pagination and failure', () => {
|
|
it('should advertise a next page when the server returned a full page', async () => {
|
|
endpoints.getBookings.mockResolvedValue(Array.from({ length: 25 }, (_, i) => booking({ bookingid: i + 1 })));
|
|
const result = await fetchDeliveries({ pageParam: 1, queryKey: key({ rowsPerPage: 25 }) });
|
|
expect(result.nextPage).toBe(2);
|
|
});
|
|
|
|
it('should stop paginating on a partial page', async () => {
|
|
endpoints.getBookings.mockResolvedValue(Array.from({ length: 9 }, (_, i) => booking({ bookingid: i + 1 })));
|
|
const result = await fetchDeliveries({ pageParam: 1, queryKey: key({ rowsPerPage: 25 }) });
|
|
expect(result.nextPage).toBeUndefined();
|
|
});
|
|
|
|
it('should request the page the caller asked for', async () => {
|
|
endpoints.getBookings.mockResolvedValue([]);
|
|
await fetchDeliveries({ pageParam: 3, queryKey: key({ rowsPerPage: 50 }) });
|
|
expect(endpoints.getBookings).toHaveBeenCalledWith(3, 50);
|
|
});
|
|
|
|
it('should default to the first page when pageParam is omitted', async () => {
|
|
endpoints.getBookings.mockResolvedValue([]);
|
|
await fetchDeliveries({ queryKey: key({ rowsPerPage: 50 }) });
|
|
expect(endpoints.getBookings).toHaveBeenCalledWith(1, 50);
|
|
});
|
|
|
|
it('should toast and return an empty page when the bookings call fails', async () => {
|
|
// The one join it cannot recover from — previously it rejected uncaught,
|
|
// with no toast anywhere across the four pages that call this.
|
|
endpoints.getBookings.mockRejectedValue({ message: 'bookings 503' });
|
|
|
|
const result = await fetchDeliveries({ pageParam: 1, queryKey: key() });
|
|
|
|
expect(result).toEqual({ rows: [], nextPage: undefined });
|
|
expect(OpenToast).toHaveBeenCalledWith('bookings 503', 'error', 2000);
|
|
});
|
|
|
|
it('should still render rows when every optional join fails', async () => {
|
|
endpoints.getBookings.mockResolvedValue([booking()]);
|
|
endpoints.getAdminCustomers.mockRejectedValue(new Error('x'));
|
|
endpoints.getMilers.mockRejectedValue(new Error('x'));
|
|
endpoints.getAdminTenants.mockRejectedValue(new Error('x'));
|
|
endpoints.getConsignments.mockRejectedValue(new Error('x'));
|
|
|
|
const { rows } = await fetchDeliveries({ pageParam: 1, queryKey: key() });
|
|
expect(rows).toHaveLength(1);
|
|
});
|
|
|
|
it('should return an empty page for an account with no bookings', async () => {
|
|
endpoints.getBookings.mockResolvedValue([]);
|
|
await expect(fetchDeliveries({ pageParam: 1, queryKey: key() })).resolves.toEqual({
|
|
rows: [],
|
|
nextPage: undefined
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('fetchCountAPI', () => {
|
|
it('should count dispatched rows into the page chip buckets', async () => {
|
|
endpoints.getBookings.mockResolvedValue([
|
|
booking({ bookingid: 1, status: 'Miler_Assigned' }),
|
|
booking({ bookingid: 2, status: 'Pickup_Scheduled' }),
|
|
booking({ bookingid: 3, status: 'Converted_To_Consignment' }),
|
|
booking({ bookingid: 4, status: 'Out_for_Delivery' }),
|
|
booking({ bookingid: 5, status: 'Delivered' }),
|
|
booking({ bookingid: 6, status: 'Cancelled' })
|
|
]);
|
|
|
|
await expect(fetchCountAPI()).resolves.toEqual({
|
|
total: 6,
|
|
uncoveredLength: 1,
|
|
assignedLength: 1,
|
|
arrivedLength: 0,
|
|
pickedLength: 1,
|
|
activeLength: 1,
|
|
coveredLength: 1,
|
|
cancelLength: 1,
|
|
skippedLength: 0
|
|
});
|
|
});
|
|
|
|
it('should count only dispatched bookings, matching what the table shows', async () => {
|
|
endpoints.getBookings.mockResolvedValue([
|
|
booking({ bookingid: 1, status: 'pending_pickup', assignedmileruserid: null }),
|
|
booking({ bookingid: 2, status: 'Delivered' })
|
|
]);
|
|
const counts = await fetchCountAPI();
|
|
expect(counts.total).toBe(1);
|
|
expect(counts.coveredLength).toBe(1);
|
|
});
|
|
|
|
it('should report zeroes — not undefined — for an empty account', async () => {
|
|
endpoints.getBookings.mockResolvedValue([]);
|
|
const counts = await fetchCountAPI();
|
|
Object.values(counts).forEach((value) => expect(value).toBe(0));
|
|
});
|
|
|
|
it('should report zeroes rather than throwing when the source call fails', async () => {
|
|
endpoints.getBookings.mockRejectedValue(new Error('boom'));
|
|
const counts = await fetchCountAPI();
|
|
expect(counts.total).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('cancelDeliveryAPI', () => {
|
|
it('should cancel through the booking-level cancel action', async () => {
|
|
endpoints.cancelBooking.mockResolvedValue({ success: true });
|
|
await cancelDeliveryAPI({ orderheaderid: 4021 }, 'customer unreachable');
|
|
expect(endpoints.cancelBooking).toHaveBeenCalledWith(4021, { reason: 'customer unreachable' });
|
|
});
|
|
|
|
it('should fall back to deliveryid when the row carries no orderheaderid', async () => {
|
|
endpoints.cancelBooking.mockResolvedValue({ success: true });
|
|
await cancelDeliveryAPI({ deliveryid: 4022 }, 'duplicate');
|
|
expect(endpoints.cancelBooking).toHaveBeenCalledWith(4022, { reason: 'duplicate' });
|
|
});
|
|
|
|
it('should send an undefined reason rather than inventing one', async () => {
|
|
endpoints.cancelBooking.mockResolvedValue({ success: true });
|
|
await cancelDeliveryAPI({ orderheaderid: 4021 });
|
|
expect(endpoints.cancelBooking).toHaveBeenCalledWith(4021, { reason: undefined });
|
|
});
|
|
|
|
it('should surface a rejected cancel to the caller', async () => {
|
|
endpoints.cancelBooking.mockRejectedValue({ message: 'already cancelled', httpStatus: 409 });
|
|
await expect(cancelDeliveryAPI({ orderheaderid: 4021 }, 'x')).rejects.toMatchObject({ httpStatus: 409 });
|
|
});
|
|
|
|
describe('a row that has already converted to a consignment', () => {
|
|
const converted = { bookingid: 41, deliveryid: 41, orderheaderid: 41, consignmentid: 61 };
|
|
|
|
it('should cancel on the consignment, where the lifecycle now lives', async () => {
|
|
// A booking freezes the moment the parcel is picked up; the booking-level
|
|
// cancel route is refused for it.
|
|
endpoints.updateConsignmentStatus.mockResolvedValue({ success: true });
|
|
|
|
await cancelDeliveryAPI(converted, 'customer unreachable');
|
|
|
|
expect(endpoints.updateConsignmentStatus).toHaveBeenCalledWith(61, { status: 'Cancelled' });
|
|
});
|
|
|
|
it('should NOT also call the booking cancel route', async () => {
|
|
endpoints.updateConsignmentStatus.mockResolvedValue({ success: true });
|
|
await cancelDeliveryAPI(converted, 'x');
|
|
expect(endpoints.cancelBooking).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should surface the consignment failure instead of retrying on the booking', async () => {
|
|
// The reported bug: PUT /admin/consignments/61/status returned 500, the
|
|
// code silently fell back to POST /admin/bookings/41/cancel, and the
|
|
// operator was shown that route's 400 — an error about the wrong
|
|
// endpoint entirely, which is why the real fault was invisible.
|
|
endpoints.updateConsignmentStatus.mockRejectedValue({
|
|
message: 'failed to update consignment status',
|
|
httpStatus: 500
|
|
});
|
|
endpoints.cancelBooking.mockRejectedValue({ message: 'booking cannot be cancelled', httpStatus: 400 });
|
|
|
|
await expect(cancelDeliveryAPI(converted, 'x')).rejects.toMatchObject({
|
|
message: 'failed to update consignment status',
|
|
httpStatus: 500
|
|
});
|
|
expect(endpoints.cancelBooking).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should surface a refusal envelope from the consignment route unchanged', async () => {
|
|
endpoints.updateConsignmentStatus.mockResolvedValue({ success: false, message: 'already delivered' });
|
|
|
|
await expect(cancelDeliveryAPI(converted, 'x')).resolves.toEqual({
|
|
success: false,
|
|
message: 'already delivered'
|
|
});
|
|
expect(endpoints.cancelBooking).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should never cancel a booking whose consignment is still live', async () => {
|
|
// The integrity hazard behind the removed fallback: a cancelled booking
|
|
// sitting against a consignment that is still out for delivery.
|
|
endpoints.updateConsignmentStatus.mockRejectedValue({ httpStatus: 500, message: 'boom' });
|
|
await cancelDeliveryAPI(converted, 'x').catch(() => { });
|
|
expect(endpoints.cancelBooking).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it('should still use the booking route for a row that has not converted', async () => {
|
|
endpoints.cancelBooking.mockResolvedValue({ success: true });
|
|
await cancelDeliveryAPI({ bookingid: 41, consignmentid: null }, 'duplicate');
|
|
expect(endpoints.cancelBooking).toHaveBeenCalledWith(41, { reason: 'duplicate' });
|
|
expect(endpoints.updateConsignmentStatus).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should refuse a row carrying neither id rather than calling an endpoint with undefined', async () => {
|
|
await expect(cancelDeliveryAPI({}, 'x')).rejects.toThrow(/nothing to cancel/i);
|
|
expect(endpoints.cancelBooking).not.toHaveBeenCalled();
|
|
expect(endpoints.updateConsignmentStatus).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('changeRiderAPI', () => {
|
|
it("should send the miler's USERID under the mileruserid key", async () => {
|
|
// assign-miler is the one admin route keyed on userid rather than
|
|
// milerprofileid. The previous guess was wrong on both key and value.
|
|
endpoints.assignMilerToBooking.mockResolvedValue({ success: true });
|
|
await changeRiderAPI({ userid: 91, milerprofileid: 11 }, { orderheaderid: 4021 });
|
|
expect(endpoints.assignMilerToBooking).toHaveBeenCalledWith(4021, { mileruserid: 91 });
|
|
});
|
|
|
|
it('should not send the milerprofileid, which this route does not accept', async () => {
|
|
endpoints.assignMilerToBooking.mockResolvedValue({ success: true });
|
|
await changeRiderAPI({ userid: 91, milerprofileid: 11 }, { orderheaderid: 4021 });
|
|
const [, body] = endpoints.assignMilerToBooking.mock.calls[0];
|
|
expect(body).not.toHaveProperty('milerid');
|
|
expect(body).not.toHaveProperty('milerprofileid');
|
|
});
|
|
|
|
it('should fall back to deliveryid for the booking id', async () => {
|
|
endpoints.assignMilerToBooking.mockResolvedValue({ success: true });
|
|
await changeRiderAPI({ userid: 91 }, { deliveryid: 4022 });
|
|
expect(endpoints.assignMilerToBooking).toHaveBeenCalledWith(4022, { mileruserid: 91 });
|
|
});
|
|
|
|
it('should surface a rejected assignment to the caller', async () => {
|
|
endpoints.assignMilerToBooking.mockRejectedValue({ message: 'miler unavailable', httpStatus: 409 });
|
|
await expect(changeRiderAPI({ userid: 91 }, { orderheaderid: 4021 })).rejects.toMatchObject({
|
|
httpStatus: 409
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('updateDeliveryAPI', () => {
|
|
beforeEach(() => endpoints.updateConsignmentStatus.mockResolvedValue({ success: true }));
|
|
|
|
it.each([
|
|
['started', 'Out_for_Delivery'],
|
|
['active', 'Out_for_Delivery'],
|
|
['delivered', 'Delivered'],
|
|
['cancelled', 'Cancelled'],
|
|
['canceled', 'Cancelled']
|
|
])('should translate the dialog choice %s to the consignment enum %s', async (chosen, sent) => {
|
|
await updateDeliveryAPI({ consignmentid: 40, orderstatus: chosen });
|
|
expect(endpoints.updateConsignmentStatus).toHaveBeenCalledWith(40, { status: sent });
|
|
});
|
|
|
|
it('should be case-insensitive about the dialog choice', async () => {
|
|
await updateDeliveryAPI({ consignmentid: 40, orderstatus: 'Delivered' });
|
|
expect(endpoints.updateConsignmentStatus).toHaveBeenCalledWith(40, { status: 'Delivered' });
|
|
});
|
|
|
|
it('should target the CONSIGNMENT id, not the booking id', async () => {
|
|
// deliveryid is always the bookingid, so `deliveryid ?? consignmentid`
|
|
// never fell through — every submit hit the endpoint with the wrong id.
|
|
await updateDeliveryAPI({ consignmentid: 40, deliveryid: 4021, orderstatus: 'delivered' });
|
|
expect(endpoints.updateConsignmentStatus).toHaveBeenCalledWith(40, expect.anything());
|
|
});
|
|
|
|
it('should send only `status` — the other dialog fields have no field here', async () => {
|
|
// Forwarding the whole dialog state produced 400 {"status is required"}.
|
|
await updateDeliveryAPI({
|
|
consignmentid: 40,
|
|
orderstatus: 'delivered',
|
|
deliveryamt: 240,
|
|
cumulativekms: 12,
|
|
userid: 91,
|
|
notes: 'left with security'
|
|
});
|
|
const [, body] = endpoints.updateConsignmentStatus.mock.calls[0];
|
|
expect(body).toEqual({ status: 'Delivered' });
|
|
});
|
|
|
|
it('should refuse with a prompt when no status was chosen', async () => {
|
|
await expect(updateDeliveryAPI({ consignmentid: 40, orderstatus: '' })).resolves.toEqual({
|
|
success: false,
|
|
message: 'Choose a status first.'
|
|
});
|
|
expect(endpoints.updateConsignmentStatus).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each([
|
|
['picked', 'already a consignment'],
|
|
['pending', 'only exists after pickup'],
|
|
['accepted', 'only exists after pickup'],
|
|
['arrived', 'nothing to set'],
|
|
['skipped', 'attempt count']
|
|
])('should refuse %s with a specific reason rather than guessing a status', async (chosen, fragment) => {
|
|
const result = await updateDeliveryAPI({ consignmentid: 40, orderstatus: chosen });
|
|
expect(result.success).toBe(false);
|
|
expect(result.message).toContain(fragment);
|
|
expect(endpoints.updateConsignmentStatus).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should refuse an unrecognised status naming the value that was rejected', async () => {
|
|
const result = await updateDeliveryAPI({ consignmentid: 40, orderstatus: 'teleported' });
|
|
expect(result.success).toBe(false);
|
|
expect(result.message).toContain('teleported');
|
|
expect(endpoints.updateConsignmentStatus).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should never write a pre-pickup booking state to the consignment endpoint', async () => {
|
|
await Promise.all(
|
|
['picked', 'pending', 'accepted', 'arrived', 'skipped'].map((orderstatus) =>
|
|
updateDeliveryAPI({ consignmentid: 40, orderstatus })
|
|
)
|
|
);
|
|
expect(endpoints.updateConsignmentStatus).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should surface a rejected status write to the caller', async () => {
|
|
endpoints.updateConsignmentStatus.mockRejectedValue({ message: 'status is required', httpStatus: 400 });
|
|
await expect(updateDeliveryAPI({ consignmentid: 40, orderstatus: 'delivered' })).rejects.toMatchObject({
|
|
httpStatus: 400
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getorderdetails', () => {
|
|
it('should read a single booking by id', async () => {
|
|
endpoints.getBooking.mockResolvedValue({ bookingid: 4021 });
|
|
await expect(getorderdetails(4021)).resolves.toEqual({ bookingid: 4021 });
|
|
expect(endpoints.getBooking).toHaveBeenCalledWith(4021);
|
|
});
|
|
});
|
|
});
|