updates on the miler page and the create order calculations as well

This commit is contained in:
2026-10-07 17:18:42 +05:30
parent ff1b795292
commit 3d07ea8170
10 changed files with 489 additions and 99 deletions

View File

@@ -0,0 +1,156 @@
import React from 'react';
import { configure, render, screen, fireEvent, within } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
configure({ asyncUtilTimeout: 5000 });
/**
* Rider (miler) create and edit forms. The hooks are mocked; the server rules
* (duplicate phone, hub in another city, block holding) are covered by
* doormile_backend's routes_miler_account_pg_test.go.
*/
beforeAll(() => {
global.ResizeObserver = global.ResizeObserver || class { observe() {} unobserve() {} disconnect() {} };
// Radix Select needs these in jsdom.
window.HTMLElement.prototype.hasPointerCapture = window.HTMLElement.prototype.hasPointerCapture || (() => false);
window.HTMLElement.prototype.releasePointerCapture = window.HTMLElement.prototype.releasePointerCapture || (() => {});
window.HTMLElement.prototype.scrollIntoView = window.HTMLElement.prototype.scrollIntoView || (() => {});
});
jest.mock('lucide-react', () =>
new Proxy({}, { get: (_t, prop) => (prop === '__esModule' ? true : (props) => <span data-testid={`icon-${String(prop)}`} {...props} />) })
);
const mockCreate = jest.fn();
const mockUpdate = jest.fn();
const HUBS = [
{ hubid: 11, hubname: 'Coimbatore Neptune Hub', applocationid: 1 },
{ hubid: 21, hubname: 'Hyderabad Mars Hub', applocationid: 2 },
];
const CITIES = [
{ applocationid: 1, locationname: 'Coimbatore' },
{ applocationid: 2, locationname: 'Hyderabad' },
];
let mockRider = null;
jest.mock('@/lib/doormileHooks', () => ({
useTenants: () => ({ data: [{ tenantid: 5, tenantname: "Sai's Kitchen" }] }),
useHubs: () => ({ data: HUBS }),
useAppLocations: () => ({ data: CITIES }),
useCreateMiler: () => ({ mutate: mockCreate, isPending: false }),
useUpdateMiler: () => ({ mutate: mockUpdate, isPending: false }),
useMiler: () => ({ data: mockRider, isLoading: false }),
}));
import CreateRider from '@/pages/doormile/riders/CreateRider';
import EditRider from '@/pages/doormile/riders/EditRider';
/** Opens a Radix select by its trigger and returns the visible option labels. */
const openSelect = (trigger) => {
fireEvent.keyDown(trigger, { key: 'Enter' });
return screen.getAllByRole('option').map((o) => o.textContent);
};
const pick = (trigger, label) => {
openSelect(trigger);
fireEvent.click(screen.getByRole('option', { name: label }));
};
beforeEach(() => {
jest.clearAllMocks();
localStorage.clear();
mockRider = null;
});
describe('Create miler', () => {
const fill = () => {
fireEvent.change(screen.getByPlaceholderText('9876543210'), { target: { value: '9876500001' } });
fireEvent.change(screen.getByPlaceholderText('murali@example.com'), { target: { value: 'ravi@r.test' } });
const inputs = screen.getAllByRole('textbox');
// Display name and login name are the first two text inputs.
fireEvent.change(inputs[0], { target: { value: 'Ravi K' } });
fireEvent.change(inputs[1], { target: { value: 'ravi' } });
};
it('asks for no password: the rider sets their own MPIN in the app', () => {
render(<MemoryRouter><CreateRider /></MemoryRouter>);
expect(screen.queryByText('Initial password')).not.toBeInTheDocument();
expect(screen.getByText(/sets their own MPIN/)).toBeInTheDocument();
});
it('offers only the hubs of the chosen city', () => {
render(<MemoryRouter><CreateRider /></MemoryRouter>);
// Staff form: vehicle, client, city, hub.
const [, , city, hub] = screen.getAllByRole('combobox');
pick(city, 'Coimbatore');
const options = openSelect(hub);
expect(options).toContain('Coimbatore Neptune Hub');
expect(options).not.toContain('Hyderabad Mars Hub');
});
it("creates a rider in Doormile's own pool when no client is chosen", () => {
render(<MemoryRouter><CreateRider /></MemoryRouter>);
fill();
pick(screen.getAllByRole('combobox')[2], 'Coimbatore');
fireEvent.click(screen.getByRole('button', { name: /create miler/i }));
expect(mockCreate).toHaveBeenCalledTimes(1);
const payload = mockCreate.mock.calls[0][0];
expect(payload).toMatchObject({ tenantid: 0, applocationid: 1, contactno: '9876500001' });
expect(payload).not.toHaveProperty('password');
});
});
describe('Edit miler', () => {
const RIDER = {
milerprofileid: 7, displayname: 'Nagalakshmi', phone: '9876500004', email: 'naga@r.test',
tenantid: 5, applocationid: 1, hubid: 11, defaultvehicletype: 'Scooter',
};
const renderEdit = () =>
render(
<MemoryRouter initialEntries={[{ pathname: '/doormile/milers/edit', state: { rider: RIDER } }]}>
<EditRider />
</MemoryRouter>
);
it('saves the changed phone (and not the unchanged hub) — and shows email, client and city read-only', () => {
mockRider = RIDER;
renderEdit();
expect(screen.getByText('naga@r.test')).toBeInTheDocument();
expect(screen.getByText("Sai's Kitchen")).toBeInTheDocument();
expect(screen.getAllByText(/Can't be changed here/)).toHaveLength(3);
fireEvent.change(screen.getByDisplayValue('9876500004'), { target: { value: '9876500007' } });
fireEvent.click(screen.getByRole('button', { name: /save changes/i }));
expect(mockUpdate).toHaveBeenCalledTimes(1);
const { id, data } = mockUpdate.mock.calls[0][0];
expect(id).toBe(7);
// Only what changed: re-sending the old hub could fail a legacy rider's save.
expect(data).toEqual({ displayname: 'Nagalakshmi', contactno: '9876500007', defaultvehicletype: 'Scooter' });
});
it('a rename alone does not resend the login phone', () => {
mockRider = RIDER;
renderEdit();
fireEvent.change(screen.getByDisplayValue('Nagalakshmi'), { target: { value: 'Nagalakshmi R' } });
fireEvent.click(screen.getByRole('button', { name: /save changes/i }));
expect(mockUpdate.mock.calls[0][0].data).toEqual({ displayname: 'Nagalakshmi R', defaultvehicletype: 'Scooter' });
});
it("offers only hubs in the rider's city", () => {
mockRider = RIDER;
renderEdit();
const hubTrigger = screen.getAllByRole('combobox')[1];
const options = openSelect(hubTrigger);
expect(options).toContain('Coimbatore Neptune Hub');
expect(options).not.toContain('Hyderabad Mars Hub');
});
it('refuses a mobile number that is not 10 digits', () => {
mockRider = RIDER;
renderEdit();
fireEvent.change(screen.getByDisplayValue('9876500004'), { target: { value: '98765' } });
fireEvent.click(screen.getByRole('button', { name: /save changes/i }));
expect(mockUpdate).not.toHaveBeenCalled();
expect(within(document.body).getByText('Enter a 10-digit mobile number')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,68 @@
import { STANDARD_RATE, chargeForDistance, describeRate, pickRateCard } from '@/lib/orderPricing';
describe('order pricing', () => {
it('charges ₹15 per km on the standard rate', () => {
expect(chargeForDistance(1, STANDARD_RATE)).toBe(15);
expect(chargeForDistance(10, STANDARD_RATE)).toBe(150);
expect(chargeForDistance(3.4, STANDARD_RATE)).toBe(51);
});
it('bills at least 1 km on the standard rate, so an order is never ₹0', () => {
// A 0 finalprice makes the backend re-price from another client's card.
expect(chargeForDistance(0, STANDARD_RATE)).toBe(15);
expect(chargeForDistance(0.3, STANDARD_RATE)).toBe(15);
expect(chargeForDistance(1.2, STANDARD_RATE)).toBe(18);
});
it('ignores a card outside its effective dates, and keeps priority 0 as the top', () => {
const now = Date.parse('2026-10-07T12:00:00Z');
const list = [
{ pricingid: 1, tenantid: 5, priceperkm: 9, priority: 0, status: 'Active', effectiveto: '2026-09-30T00:00:00Z' },
{ pricingid: 2, tenantid: 5, priceperkm: 20, priority: 1, status: 'Active' },
{ pricingid: 3, tenantid: 5, priceperkm: 18, priority: 0, status: 'Active', effectivefrom: '2026-10-01T00:00:00Z' },
];
expect(pickRateCard(list, 5, now).pricingid).toBe(3);
});
it('keeps a rate card value of 0 as 0 — no made-up ₹40 base or 2 free km', () => {
const card = { tenantid: 5, baseprice: 0, basedistance: 0, priceperkm: 15 };
expect(chargeForDistance(4, card)).toBe(60); // was 40 + (4 - 2) × 15 = 70
});
it('applies a base fee that covers the first km, then the per-km rate', () => {
const card = { baseprice: 40, basedistance: 2, priceperkm: 15 };
expect(chargeForDistance(1.5, card)).toBe(40);
expect(chargeForDistance(5, card)).toBe(85);
});
it("uses the client's own active card, lowest priority first", () => {
const list = [
{ pricingid: 1, tenantid: 9, priceperkm: 99, status: 'Active' },
{ pricingid: 2, tenantid: 5, priceperkm: 20, priority: 2, status: 'Active' },
{ pricingid: 3, tenantid: 5, priceperkm: 18, priority: 1, status: 'Active' },
{ pricingid: 4, tenantid: 5, priceperkm: 5, priority: 0, status: 'Inactive' },
];
expect(pickRateCard(list, 5).pricingid).toBe(3);
});
it("never prices a client off another client's card", () => {
const list = [{ pricingid: 1, tenantid: 9, priceperkm: 99, status: 'Active' }];
expect(pickRateCard(list, 5)).toBe(STANDARD_RATE);
expect(pickRateCard([], 5)).toBe(STANDARD_RATE);
expect(pickRateCard(undefined, '')).toBe(STANDARD_RATE);
});
it('falls back to a Doormile-wide card before the standard rate', () => {
const list = [{ pricingid: 7, tenantid: 0, priceperkm: 12, status: 'Active' }];
expect(pickRateCard(list, 5).pricingid).toBe(7);
});
it('labels the rate it used', () => {
expect(describeRate(STANDARD_RATE)).toEqual({
base: 'No base fee',
perKm: '₹15 / km (min 1 km)',
source: 'Standard rate (no client rate card)',
});
expect(describeRate({ baseprice: 40, basedistance: 2, priceperkm: 15 }).base).toBe('₹40 (first 2 km)');
});
});