import { BASE_REVENUE, BASE_KM_LIMIT, EXTRA_RATE_KM, FIXED_COST_PER_SLOT, VARIABLE_RATE_KM, rowDay, calcRiderMetrics, groupRowsByRider } from '@/lib/profitability'; /** * The unit economics of a delivery day. Every figure the Profitability report * shows comes from here, so the headline and the table underneath it cannot * disagree. * * The behaviours worth protecting, in order of how much money they move: * 1. Cancelled/skipped stops earn nothing AND cost nothing. * 2. Slot count (which drives fixed cost) is derived from `orderdate`, never * `assigntime` — the latter is re-stamped by any status change. * 3. A bare date has no slot, because midnight would otherwise be assumed. */ const order = (over = {}) => ({ orderdate: '2026-08-27T07:00:00', riderkms: 5, orderstatus: 'delivered', ...over }); describe('profitability', () => { describe('the published rate card', () => { it('should charge a 30 rupee base for the first 8 km, then 6/km', () => { expect(BASE_REVENUE).toBe(30); expect(BASE_KM_LIMIT).toBe(8); expect(EXTRA_RATE_KM).toBe(6); }); it('should slice a 500 rupee salary across three slots', () => { expect(FIXED_COST_PER_SLOT).toBeCloseTo(500 / 3, 10); expect(VARIABLE_RATE_KM).toBe(2.5); }); }); describe('rowDay', () => { it('should return the creation day of a booking', () => { expect(rowDay({ orderdate: '2026-08-27T07:00:00' })).toBe('2026-08-27'); }); it('should strip a false trailing Z so a late order stays on its own day', () => { expect(rowDay({ orderdate: '2026-08-27T23:30:00Z' })).toBe('2026-08-27'); }); it.each([{}, { orderdate: null }, { orderdate: '' }, { orderdate: 'garbage' }])( 'should return null for the unusable row %p', (row) => { expect(rowDay(row)).toBeNull(); } ); it('should not throw on a null row', () => { expect(rowDay(null)).toBeNull(); }); }); describe('calcRiderMetrics — revenue', () => { it('should charge only the base fare for a stop inside the base km limit', () => { const metrics = calcRiderMetrics({ orders: [order({ riderkms: 5 })] }, '2026-08-27'); expect(metrics.revenue).toBe(30); }); it('should charge only the base fare at exactly the base km limit', () => { // Boundary: 8 km is inclusive of the base fare, 8.01 is not. expect(calcRiderMetrics({ orders: [order({ riderkms: 8 })] }, '2026-08-27').revenue).toBe(30); }); it('should add the per-km rate beyond the base limit', () => { // 12 km => 30 + (12 - 8) * 6 = 54 expect(calcRiderMetrics({ orders: [order({ riderkms: 12 })] }, '2026-08-27').revenue).toBe(54); }); it('should accept riderkms as a numeric string, as the API returns it', () => { expect(calcRiderMetrics({ orders: [order({ riderkms: '12' })] }, '2026-08-27').revenue).toBe(54); }); it.each([null, undefined, '', 'abc', NaN])( 'should treat the unusable distance %p as zero km and still charge the base fare', (riderkms) => { const metrics = calcRiderMetrics({ orders: [order({ riderkms })] }, '2026-08-27'); expect(metrics.revenue).toBe(30); expect(metrics.kms).toBe(0); } ); it('should sum revenue across every billable stop', () => { const metrics = calcRiderMetrics( { orders: [order({ riderkms: 5 }), order({ riderkms: 12 })] }, '2026-08-27' ); expect(metrics.revenue).toBe(84); expect(metrics.kms).toBe(17); }); }); describe('calcRiderMetrics — exclusions', () => { it.each(['cancelled', 'canceled', 'skipped', 'CANCELLED', 'Skipped'])( 'should exclude a %s stop from revenue, km and slot cost', (orderstatus) => { const metrics = calcRiderMetrics( { orders: [order({ orderstatus, riderkms: 20 })] }, '2026-08-27' ); expect(metrics.revenue).toBe(0); expect(metrics.kms).toBe(0); expect(metrics.slotCount).toBe(0); expect(metrics.totalCost).toBe(0); } ); it('should still list an excluded stop in `orders` for the breakdown table', () => { // The table deliberately shows cancelled stops for context; only the money // excludes them. const metrics = calcRiderMetrics( { orders: [order({ orderstatus: 'cancelled' }), order()] }, '2026-08-27' ); expect(metrics.orders).toHaveLength(2); expect(metrics.billable).toHaveLength(1); }); it('should read `status` when `orderstatus` is absent', () => { const metrics = calcRiderMetrics( { orders: [{ orderdate: '2026-08-27T07:00:00', riderkms: 5, status: 'cancelled' }] }, '2026-08-27' ); expect(metrics.revenue).toBe(0); }); it('should treat a stop with no status at all as billable', () => { const metrics = calcRiderMetrics( { orders: [{ orderdate: '2026-08-27T07:00:00', riderkms: 5 }] }, '2026-08-27' ); expect(metrics.revenue).toBe(30); }); }); describe('calcRiderMetrics — slots and fixed cost', () => { it('should charge one slot when every stop falls in the same wave', () => { const metrics = calcRiderMetrics( { orders: [order({ orderdate: '2026-08-27T07:00:00' }), order({ orderdate: '2026-08-27T08:30:00' })] }, '2026-08-27' ); expect(metrics.slotCount).toBe(1); expect(metrics.fixedCost).toBeCloseTo(FIXED_COST_PER_SLOT, 6); }); it('should charge one slot per distinct wave worked', () => { const metrics = calcRiderMetrics( { orders: [ order({ orderdate: '2026-08-27T07:00:00' }), order({ orderdate: '2026-08-27T11:00:00' }), order({ orderdate: '2026-08-27T18:00:00' }) ] }, '2026-08-27' ); expect(metrics.slotCount).toBe(3); expect(metrics.fixedCost).toBeCloseTo(500, 6); }); it('should never charge more than three slots for a single day', () => { const orders = Array.from({ length: 12 }, (_, i) => order({ orderdate: `2026-08-27T${String(i * 2).padStart(2, '0')}:00:00` }) ); expect(calcRiderMetrics({ orders }, '2026-08-27').slotCount).toBe(3); }); it('should charge no slot for a bare date, which has no time component', () => { // Otherwise every date-only row would silently bill a Morning slot. const metrics = calcRiderMetrics({ orders: [order({ orderdate: '2026-08-27' })] }, '2026-08-27'); expect(metrics.slotCount).toBe(0); expect(metrics.fixedCost).toBe(0); expect(metrics.revenue).toBe(30); }); it('should charge no slot for an unparseable orderdate', () => { const metrics = calcRiderMetrics({ orders: [order({ orderdate: 'garbage' })] }, undefined); expect(metrics.slotCount).toBe(0); }); it('should ignore assigntime, which is re-stamped by any status change', () => { // Reading assigntime made a rider's slot count — and therefore their // fixed cost — drift through the day. const metrics = calcRiderMetrics( { orders: [order({ orderdate: '2026-08-27T07:00:00', assigntime: '2026-08-27T19:00:00' })] }, '2026-08-27' ); expect(metrics.slotCount).toBe(1); expect(metrics.fixedCost).toBeCloseTo(FIXED_COST_PER_SLOT, 6); }); it('should charge slots per day when no date filter is applied', () => { const metrics = calcRiderMetrics({ orders: [order({ orderdate: '2026-08-26T07:00:00' }), order({ orderdate: '2026-08-27T07:00:00' })] }); expect(metrics.slotCount).toBe(2); }); }); describe('calcRiderMetrics — cost, net and margin', () => { it('should compute the full P&L for a two-wave day', () => { const metrics = calcRiderMetrics( { orders: [ order({ orderdate: '2026-08-27T07:00:00', riderkms: 5 }), order({ orderdate: '2026-08-27T10:00:00', riderkms: 10 }) ] }, '2026-08-27' ); expect(metrics.revenue).toBe(72); // 30 + (30 + 2*6) expect(metrics.kms).toBe(15); expect(metrics.slotCount).toBe(2); expect(metrics.varCost).toBeCloseTo(37.5, 6); // 15 * 2.5 expect(metrics.fixedCost).toBeCloseTo((500 / 3) * 2, 6); expect(metrics.totalCost).toBeCloseTo(37.5 + (500 / 3) * 2, 6); expect(metrics.net).toBeCloseTo(72 - (37.5 + (500 / 3) * 2), 6); expect(metrics.margin).toBeCloseTo((metrics.net / 72) * 100, 6); }); it('should report a zero margin — not NaN or Infinity — when there is no revenue', () => { const metrics = calcRiderMetrics({ orders: [] }, '2026-08-27'); expect(metrics.revenue).toBe(0); expect(metrics.margin).toBe(0); expect(Number.isFinite(metrics.margin)).toBe(true); }); it('should report a negative net when cost exceeds revenue', () => { const metrics = calcRiderMetrics({ orders: [order({ riderkms: 1 })] }, '2026-08-27'); expect(metrics.net).toBeLessThan(0); expect(metrics.margin).toBeLessThan(0); }); it('should keep net equal to revenue minus total cost', () => { const metrics = calcRiderMetrics( { orders: [order({ riderkms: 30 }), order({ orderdate: '2026-08-27T17:00:00', riderkms: 4 })] }, '2026-08-27' ); expect(metrics.net).toBeCloseTo(metrics.revenue - metrics.totalCost, 9); expect(metrics.totalCost).toBeCloseTo(metrics.varCost + metrics.fixedCost, 9); }); }); describe('calcRiderMetrics — date filtering and empty input', () => { it('should count only the selected day when a date is given', () => { const metrics = calcRiderMetrics( { orders: [order({ orderdate: '2026-08-26T07:00:00' }), order({ orderdate: '2026-08-27T07:00:00' })] }, '2026-08-27' ); expect(metrics.orders).toHaveLength(1); expect(metrics.revenue).toBe(30); }); it('should count every day when no date is given', () => { const metrics = calcRiderMetrics({ orders: [order({ orderdate: '2026-08-26T07:00:00' }), order({ orderdate: '2026-08-27T07:00:00' })] }); expect(metrics.orders).toHaveLength(2); }); it('should return an all-zero result for a rider with no orders array', () => { const metrics = calcRiderMetrics({}, '2026-08-27'); expect(metrics).toMatchObject({ revenue: 0, kms: 0, slotCount: 0, totalCost: 0, net: 0, margin: 0 }); expect(metrics.orders).toEqual([]); }); it('should exclude a stop whose day cannot be determined when a date is selected', () => { const metrics = calcRiderMetrics({ orders: [order({ orderdate: 'garbage' })] }, '2026-08-27'); expect(metrics.orders).toHaveLength(0); expect(metrics.revenue).toBe(0); }); }); describe('groupRowsByRider', () => { const row = (over = {}) => ({ userid: 7, ridername: 'Anita', kms: 5, ...over }); it('should group every row belonging to the same rider', () => { const riders = groupRowsByRider([row(), row({ kms: 9 })]); expect(riders).toHaveLength(1); expect(riders[0]).toMatchObject({ id: '7', riderName: 'Anita' }); expect(riders[0].orders).toHaveLength(2); }); it('should map the row `kms` onto `riderkms`, which the money math reads', () => { // A booking carries no road distance, so the straight-line proxy feeds the // rate card. If this mapping breaks, every rider silently earns base fare. const [rider] = groupRowsByRider([row({ kms: 12 })]); expect(rider.orders[0].riderkms).toBe(12); expect(calcRiderMetrics(rider).revenue).toBe(54); }); it('should treat numeric and string user ids as the same rider', () => { expect(groupRowsByRider([row({ userid: 7 }), row({ userid: '7' })])).toHaveLength(1); }); it('should drop rows with no userid, which cannot be attributed to anyone', () => { expect(groupRowsByRider([row({ userid: null }), row({ userid: undefined }), row()])).toHaveLength(1); }); it('should fall back to a numbered placeholder when the rider has no name', () => { const [rider] = groupRowsByRider([row({ ridername: null })]); expect(rider.riderName).toBe('Rider #7'); }); it('should keep the first-seen name for a rider whose later rows are unnamed', () => { const [rider] = groupRowsByRider([row({ ridername: 'Anita' }), row({ ridername: null })]); expect(rider.riderName).toBe('Anita'); }); it.each([[[]], [null], [undefined]])('should return an empty list for %p', (rows) => { expect(groupRowsByRider(rows)).toEqual([]); }); it('should not mutate the caller rows', () => { const source = row(); groupRowsByRider([source]); expect(source).not.toHaveProperty('riderkms'); }); }); });