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