updates on the changes on warning and lat and long fixed to make it good

This commit is contained in:
2026-09-18 11:59:43 +05:30
parent bc79ba4912
commit 05f08efc64
10 changed files with 669 additions and 28 deletions

View 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
View 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();
});
});

View File

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