Files
doormilxpress_astryx/tests/lib/profitability.test.js
2026-08-27 13:40:48 +05:30

328 lines
13 KiB
JavaScript

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