updates on the miler page and the create order calculations as well
This commit is contained in:
156
tests/integration/milerAccount.test.jsx
Normal file
156
tests/integration/milerAccount.test.jsx
Normal 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();
|
||||
});
|
||||
});
|
||||
68
tests/lib/orderPricing.test.js
Normal file
68
tests/lib/orderPricing.test.js
Normal 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)');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user