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

@@ -10,6 +10,7 @@
// so a test that passes is a statement about what the page does, not about a
// copy of it that happens to agree today.
import { latitude, longitude } from '@/lib/coords';
import { PICKUP_SOURCE, buildFlowFields, flowForDraft } from '@/lib/orderFlow';
export const BULK_SERVICE_OPTION = 'Normal';
@@ -83,15 +84,40 @@ export const buildBulkBookingPayload = ({
pickupaddress: origin.address || '',
pickuppincode: String(origin.pincode || origin.postcode || ''),
pickupcity: origin.city || '',
pickuplatitude: Number(origin.latitude) || 0,
pickuplongitude: Number(origin.longitude) || 0,
// A coordinate we do not have is sent as null, never as 0.
//
// `Number(x) || 0` turned every missing pin into (0, 0), and the backend
// cannot tell that apart from a deliberate pin in the Gulf of Guinea. For
// the PICKUP end it currently recovers — adminController falls back to the
// hub or tenant location when pickup is 0,0 — but relying on that means
// relying on a rescue for data we should not have sent.
pickuplatitude: latitude(origin.latitude),
pickuplongitude: longitude(origin.longitude),
customer_phone: row?.contactno != null ? String(row.contactno) : '',
customer_name: row?.firstname || '',
deliveryaddress: row?.address || '',
deliverypincode: row?.postcode != null ? String(row.postcode) : '',
deliverycity: row?.city || '',
deliverylatitude: Number(row?.latitude) || 0,
deliverylongitude: Number(row?.longitude) || 0,
// The DELIVERY end has no rescue, which is where the money went. With
// delivery at 0,0 the backend skips its distance calculation entirely
// (`if booking.Deliverylatitude != 0 && ...`) and prices the order at base
// rate with no distance component — or, when the console did manage to
// quote a charge from those same 0,0 coordinates, bills an 11,000 km run.
// One defect, opposite errors.
//
// Be clear about what null buys and what it does not. It does NOT make the
// server refuse: adminController declares these as `float64`, not
// `*float64`, and Go unmarshals JSON null into a float64 as 0 with no
// error — verified, not assumed. So by the time a null reaches the handler
// it is indistinguishable from the 0 this used to send.
//
// The submission guard in MultipleOrders is therefore the actual fix; this
// is defence in depth and an honest local shape. It stops the console
// itself from treating a missing pin as a position — the distance
// calculation and the zone filters read these values too — and it means a
// future `*float64` on the server side starts working with no change here.
deliverylatitude: latitude(row?.latitude),
deliverylongitude: longitude(row?.longitude),
deliverytime,
service_option: BULK_SERVICE_OPTION,
finalprice: Number(row?.totalcharge) || 0,

116
src/lib/coords.js Normal file
View File

@@ -0,0 +1,116 @@
// ==============================|| Coordinates ||============================== //
//
// One place that decides whether a latitude/longitude is real.
//
// ── Why this file exists ──
//
// The console validated coordinates with `Number.isFinite(Number(value))` in
// about twenty places, and that guard does not do what it looks like it does:
//
// Number(null) === 0 → finite → passes
// Number('') === 0 → finite → passes
// Number(false) === 0 → finite → passes
//
// A missing coordinate therefore became latitude 0, longitude 0 — a point in
// the Gulf of Guinea — and every downstream consumer treated it as a real
// place. The cost was not a blank field, it was a wrong price: OSRM refuses to
// route to open ocean, `calculateDrivingRoute` falls back to Haversine, and a
// Coimbatore pickup measured against 0,0 comes out at roughly 11,000 km. That
// distance reaches `calculateTotalCharge`, becomes the row's `finalprice`, and
// the backend honours it verbatim (`adminController.go`: `if req.Finalprice > 0`).
// The same defect bills the opposite way when the console fails to quote at
// all: the backend then measures for itself, skips the calculation because
// delivery is 0,0, and charges base price with no distance component.
//
// So a zero coordinate is rejected here rather than merely reported. On Earth
// (0, 0) is a valid point; in this product it is only ever the residue of a
// failed geocode, and treating it as data has never once been correct.
//
// Range is checked too, because nothing in the console checked it before. It
// is worth being precise about what that does and does not buy, since the
// axes genuinely do swap inside this codebase — OSRM takes lon,lat while
// Leaflet takes lat,lng — so a transposed call site is a live risk:
//
// Caught: a longitude past 90 read as a latitude. Dibrugarh (27.4728,
// 94.9120) transposed gives latitude 94.9, past the pole.
// NOT caught: anything west of 90°E. Coimbatore is (11.0168, 76.9558), and
// 76.9558 is a legal latitude — northern Siberia — so a
// transposed Coimbatore pin satisfies every rule here.
//
// Detecting that second case needs a plausibility check against the operating
// region rather than a bounds check; `geocodingService.isWithinIndia` is the
// right tool and is deliberately not folded in here, because this module is
// about whether a number is a coordinate at all, not about where we operate.
//
// The logic is lifted from `hubForm.js`, which has always got this right. It
// was correct in one form and nowhere else; now it is correct in one place.
/** Latitude is bounded at the poles, longitude at the antimeridian. */
const BOUNDS = Object.freeze({ lat: 90, lng: 180 });
/**
* One coordinate, or null when there isn't one.
*
* @param {unknown} value raw cell, form field, or API field
* @param {'lat'|'lng'} axis which bound to apply
* @returns {number|null} a usable coordinate, or null
*/
export const coord = (value, axis) => {
if (value === null || value === undefined || value === '') return null;
// A boolean coerces to 0/1 and an empty array to 0. Neither is a coordinate,
// and both reach here from spreadsheet cells.
if (typeof value === 'boolean') return null;
const n = Number(value);
if (!Number.isFinite(n)) return null;
if (n === 0) return null;
const max = BOUNDS[axis];
if (max === undefined) return null;
return n < -max || n > max ? null : n;
};
export const latitude = (value) => coord(value, 'lat');
export const longitude = (value) => coord(value, 'lng');
/**
* True when both halves of a pair are usable.
*
* Both or neither: a point with one axis is not half a location, it is no
* location, and passing it on lets the missing half default to 0 somewhere
* further downstream — which is the bug this module exists to end.
*/
export const hasCoords = (lat, lng) =>
coord(lat, 'lat') !== null && coord(lng, 'lng') !== null;
/**
* A `{ latitude, longitude }` pair, or null.
*
* Reads the shapes this codebase actually passes around — `{latitude,longitude}`
* from the API and the forms, `{lat,lng}` from Leaflet and the map pickers —
* so callers stop re-deriving which spelling they were handed.
*/
export const coordsOf = (source) => {
if (!source || typeof source !== 'object') return null;
const lat = coord(source.latitude ?? source.lat, 'lat');
const lng = coord(source.longitude ?? source.lng, 'lng');
if (lat === null || lng === null) return null;
return { latitude: lat, longitude: lng };
};
/**
* The message shown when a coordinate is missing or wrong, or null when it is
* fine. Kept next to the rule so the form text and the validation cannot drift.
*/
export const coordError = (value, axis) => {
const axisName = axis === 'lat' ? 'Latitude' : 'Longitude';
if (value === null || value === undefined || value === '') {
return 'Navigate cannot work without a pin';
}
const n = Number(value);
if (typeof value === 'boolean' || !Number.isFinite(n)) return 'Enter a number';
if (n === 0) return 'Navigate cannot work without a pin';
const max = BOUNDS[axis];
if (n < -max || n > max) return `${axisName} must be between ${-max} and ${max}`;
return null;
};

View File

@@ -4,6 +4,8 @@
* and falls back seamlessly to Haversine with a 1.3 road winding multiplier.
*/
import { coordsOf } from '@/lib/coords';
let lastRouteDurationMin = null;
export const getLastRouteDurationMin = () => lastRouteDurationMin;
@@ -15,15 +17,27 @@ export const getLastRouteDurationMin = () => lastRouteDurationMin;
* @returns {Promise<{ distance: number, minutes: number, polyline: Array<[number, number]>, resolved: boolean }>}
*/
export const calculateDrivingRoute = async (origin, destination) => {
const lat1 = Number(origin?.latitude);
const lon1 = Number(origin?.longitude);
const lat2 = Number(destination?.latitude);
const lon2 = Number(destination?.longitude);
if (!Number.isFinite(lat1) || !Number.isFinite(lon1) || !Number.isFinite(lat2) || !Number.isFinite(lon2)) {
// Validated through lib/coords rather than with Number.isFinite.
//
// The previous guard was `Number.isFinite(Number(value))`, which accepts
// null, '' and false — all three coerce to 0, and 0 is finite. A missing pin
// therefore reached OSRM as (0, 0). OSRM answers HTTP 200 with no `routes`
// for a point in open ocean, so control fell through to the Haversine
// fallback below and a Coimbatore pickup measured against 0,0 returned
// roughly 11,000 km. That number becomes the row's charge and the backend
// honours it verbatim, so the guard being wrong here was a billing defect,
// not a display one.
const from = coordsOf(origin);
const to = coordsOf(destination);
if (!from || !to) {
throw new Error("Invalid coordinates");
}
const lat1 = from.latitude;
const lon1 = from.longitude;
const lat2 = to.latitude;
const lon2 = to.longitude;
// 1. Try OSRM API with full GeoJSON geometry for live polyline map preview
const osrmBaseUrl =
(typeof import.meta !== 'undefined' && import.meta?.env?.VITE_OSRM_URL) ||

View File

@@ -145,10 +145,25 @@ export const standardizePlace = ({
longitude: Number.isFinite(lngNum) ? lngNum : null,
provider,
raw,
// Google-Maps-shaped accessors, for the call sites that still read a place
// the way the Places API returned one.
//
// These return null — not 0 — when there is no coordinate, and that is a
// fix rather than a style choice. Returning 0 made this object contradict
// itself: `.latitude` above says null while `.geometry.location.lat()`
// said 0, so which answer a caller got depended on which spelling it
// happened to read. MultipleOrders read the accessor, so a suggestion with
// no coordinates became a drop pinned at (0, 0); the pre-submit guard
// tested `== null` and waved it through; the distance calculation then
// routed Coimbatore to the Gulf of Guinea, fell back to Haversine, and
// billed roughly 11,000 km — a number the backend honours verbatim
// because the console sent it as `finalprice`.
//
// null propagates as "no pin", which every guard already handles.
geometry: {
location: {
lat: () => (Number.isFinite(latNum) ? latNum : 0),
lng: () => (Number.isFinite(lngNum) ? lngNum : 0)
lat: () => (Number.isFinite(latNum) && latNum !== 0 ? latNum : null),
lng: () => (Number.isFinite(lngNum) && lngNum !== 0 ? lngNum : null)
}
},
address_components: [