// Package legs answers one question, in one place: does a parcel go straight to // its receiver, or does it go via a base first? // // It exists because two packages need that answer and neither can import the // other. controllers decides it at pickup-complete (which state the consignment // lands in, and which base the rider is sent to); internal/routing needs it when // it sequences a rider's stops, because a hub-routed parcel's next stop is the // BASE, not the address on the booking. controllers already imports // internal/routing, so routing cannot import back — and a second copy of the // rule in routing is exactly the kind of duplication this codebase has been // bitten by before (the rival status table in dispatchShared, the batch windows // that drifted between two pages). // // So the rule lives here and both call it. Neither owns it. package legs import "math" // HaversineKM is the straight-line distance between two points, in kilometres. // One definition for the whole codebase — controllers.haversineKM delegates to // it rather than keeping a second copy. func HaversineKM(lat1, lon1, lat2, lon2 float64) float64 { const earthRadiusKM = 6371.0 toRad := func(deg float64) float64 { return deg * math.Pi / 180 } dLat := toRad(lat2 - lat1) dLon := toRad(lon2 - lon1) a := math.Sin(dLat/2)*math.Sin(dLat/2) + math.Cos(toRad(lat1))*math.Cos(toRad(lat2))*math.Sin(dLon/2)*math.Sin(dLon/2) return earthRadiusKM * 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a)) } // MaxHyperlocalKM bounds the straight-line pickup→delivery distance under which // a booking with a missing or unusable pincode is still treated as hyperlocal. // It is ONLY consulted when the pincode rule cannot decide: console-created // kitchen→customer bookings frequently carry accurate coordinates and no // delivery pincode, and must not be routed through a base because of it. const MaxHyperlocalKM = 30.0 // SamePostalArea reports whether two pincodes fall in the same 3-digit postal // area. Pincodes shorter than 3 characters are treated as unknown rather than // matching, so bad data falls back to the safe hub route instead of quietly // claiming two parcels belong together. func SamePostalArea(pickupPincode, deliveryPincode string) bool { if len(pickupPincode) < 3 || len(deliveryPincode) < 3 { return false } return pickupPincode[:3] == deliveryPincode[:3] } // IsHyperlocal decides whether a parcel can skip the base and be carried // straight to the receiver by the collecting rider. // // The pincode-prefix rule decides outright when both pincodes are present — a // matching prefix is hyperlocal, a differing one is genuinely inter-area, and // distance does not get a vote. Pollachi is 40km from Coimbatore, closer than // plenty of runs treated as local, and it is still a different postal area. // // Only when a pincode is missing or too short does the straight-line distance // decide instead. With neither pincodes nor coordinates the answer is false, // which routes via a base — the safe direction to be wrong in, because a parcel // that reaches a base can still be forwarded, while one handed to a rider who // cannot reach the receiver is stuck. func IsHyperlocal(pickupPincode, deliveryPincode string, pLat, pLng, dLat, dLng float64) bool { if len(pickupPincode) >= 3 && len(deliveryPincode) >= 3 { return SamePostalArea(pickupPincode, deliveryPincode) } if pLat != 0 && pLng != 0 && dLat != 0 && dLng != 0 { return HaversineKM(pLat, pLng, dLat, dLng) <= MaxHyperlocalKM } return false }