updates on the changes on warning and lat and long fixed to make it good
This commit is contained in:
@@ -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
116
src/lib/coords.js
Normal 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;
|
||||
};
|
||||
@@ -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) ||
|
||||
|
||||
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user