174 lines
6.6 KiB
JavaScript
174 lines
6.6 KiB
JavaScript
import {
|
|
BOOKING_SOURCE,
|
|
BOOKING_TABS,
|
|
bookingsForTab,
|
|
customerAppBookings,
|
|
isCustomerAppBooking,
|
|
quotedPrice,
|
|
serviceOptionOf,
|
|
serviceType,
|
|
} from '@/lib/customerAppBookings';
|
|
|
|
/**
|
|
* Which bookings belong on the Bookings page.
|
|
*
|
|
* The page shows what customers booked in the app; Orders shows what this
|
|
* console created. `bookingsource` is the only field that separates them, and
|
|
* the trap is its second value: the API and routes were renamed from "CRM" to
|
|
* "express", but the STORED column value was deliberately left as
|
|
* `"CRM_Console"` because it is existing data rather than a label. Matching on
|
|
* "express" would silently show every console order on the customer page.
|
|
*/
|
|
|
|
const appBooking = (over = {}) => ({
|
|
bookingid: 1,
|
|
bookingno: 'BK-1',
|
|
bookingsource: 'Customer_App',
|
|
status: 'Pending_Pickup',
|
|
...over,
|
|
});
|
|
|
|
const consoleBooking = (over = {}) => ({
|
|
bookingid: 2,
|
|
bookingno: 'BK-2',
|
|
bookingsource: 'CRM_Console',
|
|
status: 'Pending_Pickup',
|
|
...over,
|
|
});
|
|
|
|
describe('isCustomerAppBooking', () => {
|
|
it('should accept a booking made in the app', () => {
|
|
expect(isCustomerAppBooking(appBooking())).toBe(true);
|
|
});
|
|
|
|
it('should reject a console-created booking', () => {
|
|
expect(isCustomerAppBooking(consoleBooking())).toBe(false);
|
|
});
|
|
|
|
it('should still reject the console value under its post-rename name', () => {
|
|
// The outward naming became "express"; the stored value never did. Anything
|
|
// that starts matching on "express" here has misunderstood that.
|
|
expect(BOOKING_SOURCE.CONSOLE).toBe('CRM_Console');
|
|
expect(isCustomerAppBooking({ bookingsource: 'express' })).toBe(false);
|
|
});
|
|
|
|
it('should tolerate casing and stray whitespace', () => {
|
|
// Compared against a raw database column, not a validated enum.
|
|
expect(isCustomerAppBooking({ bookingsource: ' customer_app ' })).toBe(true);
|
|
expect(isCustomerAppBooking({ bookingsource: 'CUSTOMER_APP' })).toBe(true);
|
|
});
|
|
|
|
it('should not claim a booking with no source', () => {
|
|
// Filing unknown rows under "customer app" would overstate B2C volume, and
|
|
// that is a number people make decisions on.
|
|
expect(isCustomerAppBooking({})).toBe(false);
|
|
expect(isCustomerAppBooking({ bookingsource: '' })).toBe(false);
|
|
expect(isCustomerAppBooking(null)).toBe(false);
|
|
expect(isCustomerAppBooking(undefined)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('customerAppBookings', () => {
|
|
it('should keep only app bookings and drop console ones', () => {
|
|
const list = [appBooking({ bookingid: 1 }), consoleBooking({ bookingid: 2 }), appBooking({ bookingid: 3 })];
|
|
expect(customerAppBookings(list).map((b) => b.bookingid)).toEqual([3, 1]);
|
|
});
|
|
|
|
it('should order newest first', () => {
|
|
const list = [appBooking({ bookingid: 5 }), appBooking({ bookingid: 42 }), appBooking({ bookingid: 12 })];
|
|
expect(customerAppBookings(list).map((b) => b.bookingid)).toEqual([42, 12, 5]);
|
|
});
|
|
|
|
it('should not mutate the list it was given', () => {
|
|
// The same array is the Orders page's cache entry — sorting it in place
|
|
// would reorder another page's rows underneath it.
|
|
const list = [appBooking({ bookingid: 1 }), appBooking({ bookingid: 9 })];
|
|
customerAppBookings(list);
|
|
expect(list.map((b) => b.bookingid)).toEqual([1, 9]);
|
|
});
|
|
|
|
it('should return an empty list rather than throw on nothing', () => {
|
|
expect(customerAppBookings(undefined)).toEqual([]);
|
|
expect(customerAppBookings(null)).toEqual([]);
|
|
expect(customerAppBookings([])).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('the price quote', () => {
|
|
const withOptions = (options) => appBooking({ serviceoptions: options });
|
|
|
|
it('should read the service option the booking carries', () => {
|
|
const b = withOptions([{ servicetype: 'Fast', estimatedprice: 210, createdat: '2026-09-01T10:00:00Z' }]);
|
|
expect(serviceType(b)).toBe('Fast');
|
|
expect(quotedPrice(b)).toBe(210);
|
|
});
|
|
|
|
it('should take the newest option when a booking has several', () => {
|
|
// Matches the backend's own `ORDER BY createdat DESC LIMIT 1`, so the page
|
|
// and GET /customer/bookings/{id}/price cannot disagree.
|
|
const b = withOptions([
|
|
{ servicetype: 'Normal', estimatedprice: 100, createdat: '2026-09-01T10:00:00Z' },
|
|
{ servicetype: 'Superfast', estimatedprice: 300, createdat: '2026-09-02T10:00:00Z' },
|
|
]);
|
|
expect(serviceOptionOf(b).servicetype).toBe('Superfast');
|
|
expect(quotedPrice(b)).toBe(300);
|
|
});
|
|
|
|
it('should report no quote rather than a zero price', () => {
|
|
// A booking with no service option has no price. Rendering 0 would read as
|
|
// "free", which is a different claim.
|
|
expect(serviceOptionOf(appBooking())).toBeNull();
|
|
expect(serviceOptionOf(withOptions([]))).toBeNull();
|
|
expect(serviceType(appBooking())).toBe('');
|
|
});
|
|
|
|
it('should not throw on a missing booking', () => {
|
|
expect(() => quotedPrice(undefined)).not.toThrow();
|
|
expect(quotedPrice(undefined)).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('the status tabs', () => {
|
|
it('should let the All tab through untouched', () => {
|
|
const list = [appBooking({ status: 'Pending_Pickup' }), appBooking({ status: 'Cancelled' })];
|
|
expect(bookingsForTab(list, 'all')).toHaveLength(2);
|
|
});
|
|
|
|
it('should put an uncollected unassigned booking under Created tab', () => {
|
|
const list = [
|
|
appBooking({ bookingid: 1, status: 'Pending_Pickup' }),
|
|
appBooking({ bookingid: 2, status: 'Created' }),
|
|
appBooking({ bookingid: 3, status: 'Miler_Assigned' }),
|
|
];
|
|
expect(bookingsForTab(list, 'created').map((b) => b.bookingid)).toEqual([1, 2]);
|
|
});
|
|
|
|
it('should put an assigned / in-progress booking under Rider Assigned tab', () => {
|
|
const list = [
|
|
appBooking({ bookingid: 1, status: 'Miler_Assigned' }),
|
|
appBooking({ bookingid: 2, status: 'Converted_To_Consignment' }),
|
|
appBooking({ bookingid: 3, status: 'Pending_Pickup' }),
|
|
];
|
|
expect(bookingsForTab(list, 'assigned').map((b) => b.bookingid)).toEqual([1, 2]);
|
|
});
|
|
|
|
it('should match status case-insensitively', () => {
|
|
expect(bookingsForTab([appBooking({ status: 'CANCELLED' })], 'cancelled')).toHaveLength(1);
|
|
});
|
|
|
|
it('should fall back to showing everything for an unknown tab', () => {
|
|
// An unknown tab value can only come from a bug in the page. Falling back to
|
|
// the full list keeps the data visible; silently returning nothing would
|
|
// look identical to "this customer has no bookings", which is a lie the
|
|
// operator cannot see through.
|
|
expect(bookingsForTab([appBooking()], 'nonsense')).toHaveLength(1);
|
|
});
|
|
|
|
it('should give every tab a value and a label', () => {
|
|
BOOKING_TABS.forEach((t) => {
|
|
expect(t.value).toBeTruthy();
|
|
expect(t.label).toBeTruthy();
|
|
});
|
|
});
|
|
});
|