updates on the changes on warning and lat and long fixed to make it good
This commit is contained in:
136
tests/lib/bulkOrderPayload.test.js
Normal file
136
tests/lib/bulkOrderPayload.test.js
Normal file
@@ -0,0 +1,136 @@
|
||||
import { buildBulkBookingPayload, buildBulkBookingPayloads } from '@/lib/bulkOrderPayload';
|
||||
|
||||
/**
|
||||
* Coordinates on the bulk booking payload.
|
||||
*
|
||||
* This is the regression guard for the billing defect. The builder used
|
||||
* `Number(value) || 0` on all four coordinates, so a row with no usable pin
|
||||
* was submitted as (0, 0) — and the backend cannot tell that apart from a
|
||||
* deliberate pin in the Gulf of Guinea.
|
||||
*
|
||||
* Both failure modes came from that one substitution:
|
||||
*
|
||||
* • With a charge quoted, the console had measured the distance from those
|
||||
* same 0,0 coordinates. OSRM refuses to route to open ocean, so the
|
||||
* Haversine fallback returned roughly 11,000 km, and adminController
|
||||
* honours a supplied `finalprice` verbatim. Massive overcharge.
|
||||
* • With no charge quoted, the backend measured for itself — and skipped the
|
||||
* calculation entirely, because its guard is
|
||||
* `if booking.Deliverylatitude != 0 && ...`. Base rate, no distance
|
||||
* component. Undercharge.
|
||||
*
|
||||
* A coordinate we do not have is now null, which the server can refuse.
|
||||
*/
|
||||
|
||||
const PICKUP = {
|
||||
address: 'Doormile Base, Peelamedu',
|
||||
pincode: '641004',
|
||||
city: 'Coimbatore',
|
||||
latitude: 11.0168,
|
||||
longitude: 76.9558,
|
||||
};
|
||||
|
||||
const ROW = {
|
||||
firstname: 'Priya',
|
||||
contactno: '9876543210',
|
||||
address: '45 Cross Cut Rd, Gandhipuram',
|
||||
postcode: '641012',
|
||||
city: 'Coimbatore',
|
||||
latitude: 11.0272,
|
||||
longitude: 76.9722,
|
||||
totalcharge: 180,
|
||||
};
|
||||
|
||||
const build = (row = {}, pickup = PICKUP) =>
|
||||
buildBulkBookingPayload({
|
||||
row: { ...ROW, ...row },
|
||||
sharedPickup: pickup,
|
||||
tenantId: 3,
|
||||
tenantLocationId: 7,
|
||||
deliverytime: '2026-09-18 10:00:00',
|
||||
notes: '',
|
||||
anchors: [],
|
||||
});
|
||||
|
||||
describe('buildBulkBookingPayload — coordinates', () => {
|
||||
it('passes real coordinates through untouched', () => {
|
||||
const p = build();
|
||||
expect(p.pickuplatitude).toBe(11.0168);
|
||||
expect(p.pickuplongitude).toBe(76.9558);
|
||||
expect(p.deliverylatitude).toBe(11.0272);
|
||||
expect(p.deliverylongitude).toBe(76.9722);
|
||||
});
|
||||
|
||||
it('sends null, NOT 0, for a delivery pin that is missing', () => {
|
||||
const p = build({ latitude: null, longitude: null });
|
||||
expect(p.deliverylatitude).toBeNull();
|
||||
expect(p.deliverylongitude).toBeNull();
|
||||
// The specific regression: 0 would have been priced as an 11,000 km run.
|
||||
expect(p.deliverylatitude).not.toBe(0);
|
||||
});
|
||||
|
||||
it('sends null for an empty sheet cell', () => {
|
||||
// An untouched Excel cell arrives as '' rather than null, and
|
||||
// `Number('') || 0` is 0.
|
||||
const p = build({ latitude: '', longitude: '' });
|
||||
expect(p.deliverylatitude).toBeNull();
|
||||
expect(p.deliverylongitude).toBeNull();
|
||||
});
|
||||
|
||||
it('sends null for an unparseable sheet cell', () => {
|
||||
const p = build({ latitude: 'N/A', longitude: '--' });
|
||||
expect(p.deliverylatitude).toBeNull();
|
||||
expect(p.deliverylongitude).toBeNull();
|
||||
});
|
||||
|
||||
it('refuses to forward a literal zero it was handed', () => {
|
||||
// A row that already carried 0 — which is what the geocoding shim used to
|
||||
// return for a suggestion with no coordinates — must not be laundered
|
||||
// into a valid-looking payload.
|
||||
const p = build({ latitude: 0, longitude: 0 });
|
||||
expect(p.deliverylatitude).toBeNull();
|
||||
expect(p.deliverylongitude).toBeNull();
|
||||
});
|
||||
|
||||
it('sends null for a coordinate outside its axis range', () => {
|
||||
const p = build({ latitude: 94.912, longitude: 200 });
|
||||
expect(p.deliverylatitude).toBeNull();
|
||||
expect(p.deliverylongitude).toBeNull();
|
||||
});
|
||||
|
||||
it('nulls the pickup end too, rather than relying on the backend rescue', () => {
|
||||
// adminController falls back to the hub or tenant location when pickup is
|
||||
// 0,0. That rescue exists, but sending data that needs rescuing is not the
|
||||
// same as sending correct data.
|
||||
const p = build({}, { ...PICKUP, latitude: null, longitude: undefined });
|
||||
expect(p.pickuplatitude).toBeNull();
|
||||
expect(p.pickuplongitude).toBeNull();
|
||||
});
|
||||
|
||||
it('treats the two ends independently', () => {
|
||||
const p = build({ latitude: null, longitude: null });
|
||||
expect(p.pickuplatitude).toBe(11.0168);
|
||||
expect(p.deliverylatitude).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildBulkBookingPayloads — batch', () => {
|
||||
it('nulls only the rows that are missing a pin', () => {
|
||||
const payloads = buildBulkBookingPayloads({
|
||||
rows: [ROW, { ...ROW, latitude: 0, longitude: 0 }, { ...ROW, latitude: '' }],
|
||||
sharedPickup: PICKUP,
|
||||
tenantId: 3,
|
||||
tenantLocationId: 7,
|
||||
deliverytime: '2026-09-18 10:00:00',
|
||||
notes: '',
|
||||
anchors: [],
|
||||
});
|
||||
|
||||
expect(payloads).toHaveLength(3);
|
||||
expect(payloads[0].deliverylatitude).toBe(11.0272);
|
||||
expect(payloads[1].deliverylatitude).toBeNull();
|
||||
expect(payloads[2].deliverylatitude).toBeNull();
|
||||
// The good row keeps its longitude even though a sibling lost its latitude.
|
||||
expect(payloads[2].deliverylongitude).toBe(76.9722);
|
||||
});
|
||||
});
|
||||
202
tests/lib/coords.test.js
Normal file
202
tests/lib/coords.test.js
Normal file
@@ -0,0 +1,202 @@
|
||||
import {
|
||||
coord,
|
||||
coordError,
|
||||
coordsOf,
|
||||
hasCoords,
|
||||
latitude,
|
||||
longitude,
|
||||
} from '@/lib/coords';
|
||||
|
||||
/**
|
||||
* Coordinate validation.
|
||||
*
|
||||
* The first block is the whole reason this module exists: the guard it replaces
|
||||
* was `Number.isFinite(Number(value))`, and every case below passed it, because
|
||||
* null, '' and false all coerce to 0 and 0 is finite. A missing pin became
|
||||
* latitude 0 / longitude 0 and was priced as an 11,000 km run.
|
||||
*/
|
||||
|
||||
const COIMBATORE = { latitude: 11.0168, longitude: 76.9558 };
|
||||
|
||||
describe('coord', () => {
|
||||
describe('the empty values that used to pass as zero', () => {
|
||||
it.each([
|
||||
['null', null],
|
||||
['undefined', undefined],
|
||||
['empty string', ''],
|
||||
['false', false],
|
||||
['true', true],
|
||||
['an empty array', []],
|
||||
])('rejects %s', (_label, value) => {
|
||||
// Number(value) is 0 for every one of these, and 0 is finite — which is
|
||||
// exactly how they got through before.
|
||||
expect(coord(value, 'lat')).toBeNull();
|
||||
expect(coord(value, 'lng')).toBeNull();
|
||||
});
|
||||
|
||||
it('confirms the coercion that caused the bug', () => {
|
||||
expect(Number(null)).toBe(0);
|
||||
expect(Number.isFinite(Number(null))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('zero', () => {
|
||||
it('rejects a literal zero on both axes', () => {
|
||||
// (0, 0) is a real point in the Gulf of Guinea. In this product it is
|
||||
// only ever the residue of a failed geocode.
|
||||
expect(coord(0, 'lat')).toBeNull();
|
||||
expect(coord(0, 'lng')).toBeNull();
|
||||
expect(coord('0', 'lat')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('unparseable values', () => {
|
||||
it.each(['abc', 'N/A', '--', 'NaN', {}])('rejects %p', (value) => {
|
||||
expect(coord(value, 'lat')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('range', () => {
|
||||
it('accepts the poles and the antimeridian exactly', () => {
|
||||
expect(coord(90, 'lat')).toBe(90);
|
||||
expect(coord(-90, 'lat')).toBe(-90);
|
||||
expect(coord(180, 'lng')).toBe(180);
|
||||
expect(coord(-180, 'lng')).toBe(-180);
|
||||
});
|
||||
|
||||
it('rejects a latitude past the poles', () => {
|
||||
expect(coord(90.1, 'lat')).toBeNull();
|
||||
expect(coord(-91, 'lat')).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects a longitude past the antimeridian', () => {
|
||||
expect(coord(180.5, 'lng')).toBeNull();
|
||||
expect(coord(-181, 'lng')).toBeNull();
|
||||
});
|
||||
|
||||
it('catches a transposition only when the longitude exceeds 90', () => {
|
||||
// The honest limit of a range check. Kolkata is (22.5726, 88.3639) and
|
||||
// Dibrugarh is (27.4728, 94.9120) — transpose the second and the
|
||||
// "latitude" becomes 94.9, past the pole, so it is caught.
|
||||
expect(coord(94.912, 'lat')).toBeNull();
|
||||
expect(hasCoords(94.912, 27.4728)).toBe(false);
|
||||
});
|
||||
|
||||
it('CANNOT catch a transposed Coimbatore pin — documented limitation', () => {
|
||||
// 76.9558 is a perfectly legal latitude (northern Siberia), so a
|
||||
// transposed Coimbatore pin passes every check in this module. Range
|
||||
// validation only catches a swap when one axis leaves the other's
|
||||
// domain, i.e. |longitude| > 90. Anything west of 90°E transposes
|
||||
// silently.
|
||||
//
|
||||
// Pinned as a test so nobody reads `coord` as swap-proof. Catching this
|
||||
// needs a plausibility check against the operating region (see
|
||||
// geocodingService.isWithinIndia), not a bounds check.
|
||||
expect(coord(COIMBATORE.longitude, 'lat')).toBe(76.9558);
|
||||
expect(hasCoords(COIMBATORE.longitude, COIMBATORE.latitude)).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects an unknown axis rather than guessing a bound', () => {
|
||||
expect(coord(11.0168, 'elevation')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('real coordinates', () => {
|
||||
it('passes a Coimbatore pin through unchanged', () => {
|
||||
expect(coord(COIMBATORE.latitude, 'lat')).toBe(11.0168);
|
||||
expect(coord(COIMBATORE.longitude, 'lng')).toBe(76.9558);
|
||||
});
|
||||
|
||||
it('parses a numeric string, which is how a sheet cell arrives', () => {
|
||||
expect(coord('11.0168', 'lat')).toBe(11.0168);
|
||||
expect(coord(' 76.9558 ', 'lng')).toBe(76.9558);
|
||||
});
|
||||
|
||||
it('accepts southern and western hemispheres', () => {
|
||||
expect(coord(-33.8688, 'lat')).toBe(-33.8688);
|
||||
expect(coord(-74.006, 'lng')).toBe(-74.006);
|
||||
});
|
||||
});
|
||||
|
||||
describe('axis helpers', () => {
|
||||
it('applies the latitude bound', () => {
|
||||
expect(latitude(91)).toBeNull();
|
||||
expect(latitude(11.0168)).toBe(11.0168);
|
||||
});
|
||||
|
||||
it('applies the longitude bound', () => {
|
||||
expect(longitude(76.9558)).toBe(76.9558);
|
||||
expect(longitude(181)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasCoords', () => {
|
||||
it('needs both halves', () => {
|
||||
expect(hasCoords(11.0168, 76.9558)).toBe(true);
|
||||
expect(hasCoords(11.0168, null)).toBe(false);
|
||||
expect(hasCoords(null, 76.9558)).toBe(false);
|
||||
expect(hasCoords(11.0168, 0)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects the pair the geocoding shim used to produce', () => {
|
||||
// geocodingService returned 0 from geometry.location.lat() when a place
|
||||
// had no coordinates. This is that pair.
|
||||
expect(hasCoords(0, 0)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('coordsOf', () => {
|
||||
it('reads the API/form shape', () => {
|
||||
expect(coordsOf(COIMBATORE)).toEqual({ latitude: 11.0168, longitude: 76.9558 });
|
||||
});
|
||||
|
||||
it('reads the Leaflet/map-picker shape', () => {
|
||||
expect(coordsOf({ lat: 11.0168, lng: 76.9558 })).toEqual({
|
||||
latitude: 11.0168,
|
||||
longitude: 76.9558,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null when either half is unusable', () => {
|
||||
expect(coordsOf({ latitude: 11.0168, longitude: null })).toBeNull();
|
||||
expect(coordsOf({ latitude: 0, longitude: 0 })).toBeNull();
|
||||
expect(coordsOf({ latitude: '', longitude: '' })).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for a non-object', () => {
|
||||
expect(coordsOf(null)).toBeNull();
|
||||
expect(coordsOf(undefined)).toBeNull();
|
||||
expect(coordsOf('11.0168,76.9558')).toBeNull();
|
||||
});
|
||||
|
||||
it('prefers the explicit spelling when both are present', () => {
|
||||
expect(coordsOf({ latitude: 11.0168, lat: 99, longitude: 76.9558, lng: 99 })).toEqual({
|
||||
latitude: 11.0168,
|
||||
longitude: 76.9558,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('coordError', () => {
|
||||
it('names a missing pin rather than a number problem', () => {
|
||||
expect(coordError('', 'lat')).toBe('Navigate cannot work without a pin');
|
||||
expect(coordError(null, 'lng')).toBe('Navigate cannot work without a pin');
|
||||
expect(coordError(0, 'lat')).toBe('Navigate cannot work without a pin');
|
||||
});
|
||||
|
||||
it('names a number problem when it is one', () => {
|
||||
expect(coordError('abc', 'lat')).toBe('Enter a number');
|
||||
expect(coordError(true, 'lat')).toBe('Enter a number');
|
||||
});
|
||||
|
||||
it('names the axis and its bounds when out of range', () => {
|
||||
expect(coordError(91, 'lat')).toBe('Latitude must be between -90 and 90');
|
||||
expect(coordError(181, 'lng')).toBe('Longitude must be between -180 and 180');
|
||||
});
|
||||
|
||||
it('is null for a good coordinate', () => {
|
||||
expect(coordError(11.0168, 'lat')).toBeNull();
|
||||
expect(coordError(76.9558, 'lng')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -35,11 +35,40 @@ describe('calculateDrivingDistance', () => {
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should accept zero coordinates, which are a real position', async () => {
|
||||
it('should reject zero coordinates, which are only ever a failed geocode', async () => {
|
||||
// This assertion is INVERTED from what it used to say, and the reason is
|
||||
// that the old pair of expectations could not both hold.
|
||||
//
|
||||
// The case above requires a null latitude to be rejected. `Number(null)`
|
||||
// is 0, so to any guard built on Number() a null latitude and a zero
|
||||
// latitude are the same value — there is no implementation that rejects
|
||||
// one and accepts the other. The old code accepted both, which is why
|
||||
// "should reject a null latitude value" was failing on main.
|
||||
//
|
||||
// So it is a choice, and zero loses. Geographically (0, 0) is a real
|
||||
// point in the Gulf of Guinea; in this product it is not reachable —
|
||||
// Doormile operates inside the India bounding box (latitude 6.5 to 37.5,
|
||||
// longitude 68 to 97.5, see geocodingService.isWithinIndia) and every
|
||||
// zero coordinate ever seen in production was the residue of a geocode
|
||||
// that failed. Accepting them cost real money: OSRM has no route to open
|
||||
// ocean, so the Haversine fallback measured a Coimbatore pickup at
|
||||
// roughly 11,000 km, and the backend honours that as the order's price.
|
||||
global.fetch.mockResolvedValue(osrmOk(1000, 120));
|
||||
await expect(
|
||||
calculateDrivingDistance({ latitude: 0, longitude: 0 }, { latitude: 0, longitude: 1 })
|
||||
).resolves.toEqual(expect.any(Number));
|
||||
).rejects.toThrow('Invalid coordinates');
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject a coordinate outside its own axis range', async () => {
|
||||
// Nothing in the console checked this before. Note the limit: this
|
||||
// catches a longitude read as a latitude only when it exceeds 90, so a
|
||||
// transposed Coimbatore pin (11.0168, 76.9558) still passes. See
|
||||
// lib/coords for why, and what would actually catch it.
|
||||
await expect(
|
||||
calculateDrivingDistance({ latitude: 94.912, longitude: 27.4728 }, IND)
|
||||
).rejects.toThrow('Invalid coordinates');
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,7 +90,11 @@ describe('calculateDrivingDistance', () => {
|
||||
await calculateDrivingDistance(BLR, IND);
|
||||
const url = global.fetch.mock.calls[0][0];
|
||||
expect(url).toContain('/route/v1/driving/77.5946,12.9716;77.6245,12.9352');
|
||||
expect(url).toContain('overview=false');
|
||||
// The route geometry IS requested: `calculateDrivingRoute` returns a
|
||||
// polyline for the live map preview, so this asks for overview=full with
|
||||
// geojson geometry. It read `overview=false` until the polyline landed.
|
||||
expect(url).toContain('overview=full');
|
||||
expect(url).toContain('geometries=geojson');
|
||||
});
|
||||
|
||||
it('should use the public OSRM host by default', async () => {
|
||||
@@ -113,10 +146,13 @@ describe('calculateDrivingDistance', () => {
|
||||
|
||||
it('should apply the 1.3 road multiplier to the straight-line distance', async () => {
|
||||
// One degree of latitude is 111.19 km great-circle; x1.3 = 144.5 -> 145.
|
||||
// Measured along a meridian over India rather than from (0, 0): the
|
||||
// arithmetic is identical at any longitude, and zero coordinates are now
|
||||
// refused before the fetch.
|
||||
global.fetch.mockResolvedValue({ ok: false, json: async () => ({}) });
|
||||
const distance = await calculateDrivingDistance(
|
||||
{ latitude: 0, longitude: 0 },
|
||||
{ latitude: 1, longitude: 0 }
|
||||
{ latitude: 10, longitude: 77 },
|
||||
{ latitude: 11, longitude: 77 }
|
||||
);
|
||||
expect(distance).toBe(145);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user