Files
doormilxpress_astryx/tests/lib/customerAppBookings.test.js

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();
});
});
});