183 lines
6.6 KiB
Python
183 lines
6.6 KiB
Python
"""
|
|
Customer coordinate store.
|
|
|
|
Purpose:
|
|
For each incoming order, look up the customer's phone number.
|
|
If a previous verified delivery exists (rider coords on file), use those
|
|
coords instead of the (possibly noisy) input delivery coords.
|
|
If no record exists, create one so future calls benefit from it.
|
|
|
|
Storage:
|
|
Pure dict + JSON — no FAISS dependency.
|
|
phone_meta.json maps phone -> {name, rider_lat, rider_lon}.
|
|
Coord lookup key is phone number, not coordinates, so vector search
|
|
was never needed; FAISS has been removed.
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import logging
|
|
from math import radians, cos, sin, asin, sqrt
|
|
from threading import Lock
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
STORE_DIR = os.getenv("CUSTOMER_STORE_DIR", "ml_data/faiss_customer")
|
|
META_PATH = os.path.join(STORE_DIR, "phone_meta.json")
|
|
|
|
# Within this distance the stored rider coords are considered the real delivery point
|
|
SIMILARITY_THRESHOLD_KM = float(os.getenv("FAISS_SIMILARITY_KM", "0.5"))
|
|
|
|
|
|
def _haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
|
"""Great-circle distance in km."""
|
|
try:
|
|
lon1, lat1, lon2, lat2 = map(radians, [float(lon1), float(lat1), float(lon2), float(lat2)])
|
|
dlon = lon2 - lon1
|
|
dlat = lat2 - lat1
|
|
a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
|
|
return 2 * asin(min(1.0, sqrt(a))) * 6371
|
|
except Exception:
|
|
return float("inf")
|
|
|
|
|
|
class CustomerCoordStore:
|
|
"""
|
|
Thread-safe customer delivery coordinate store (dict + JSON backend).
|
|
|
|
Primary flow (called per order before assignment):
|
|
verified_lat, verified_lon, corrected = store.get_verified_coords(
|
|
phone, customer_name, input_lat, input_lon
|
|
)
|
|
"""
|
|
|
|
def __init__(self):
|
|
self._lock = Lock()
|
|
os.makedirs(STORE_DIR, exist_ok=True)
|
|
self._meta: dict = {} # phone -> {name, rider_lat, rider_lon}
|
|
self._load()
|
|
|
|
# ------------------------------------------------------------------
|
|
# Persistence
|
|
# ------------------------------------------------------------------
|
|
|
|
def _load(self):
|
|
if os.path.exists(META_PATH):
|
|
try:
|
|
with open(META_PATH, "r", encoding="utf-8") as f:
|
|
self._meta = json.load(f)
|
|
logger.info(f"[CustomerStore] Loaded {len(self._meta)} customer records.")
|
|
except Exception as e:
|
|
logger.warning(f"[CustomerStore] Could not load {META_PATH}: {e} — starting fresh.")
|
|
self._meta = {}
|
|
else:
|
|
self._meta = {}
|
|
logger.info("[CustomerStore] No existing store found — starting fresh.")
|
|
|
|
def _save(self):
|
|
try:
|
|
with open(META_PATH, "w", encoding="utf-8") as f:
|
|
json.dump(self._meta, f, indent=2)
|
|
except Exception as e:
|
|
logger.warning(f"[CustomerStore] Save failed: {e}")
|
|
|
|
# ------------------------------------------------------------------
|
|
# Public API
|
|
# ------------------------------------------------------------------
|
|
|
|
def get_verified_coords(
|
|
self,
|
|
phone: str,
|
|
customer_name: str,
|
|
input_lat: float,
|
|
input_lon: float,
|
|
) -> tuple:
|
|
"""
|
|
Verify / correct delivery coordinates using stored history.
|
|
|
|
Returns:
|
|
(lat, lon, was_corrected)
|
|
was_corrected=True → coords replaced with verified stored coords
|
|
was_corrected=False → input coords used (new customer or new location)
|
|
"""
|
|
if not phone or not input_lat or not input_lon:
|
|
return input_lat, input_lon, False
|
|
|
|
phone = str(phone).strip()
|
|
|
|
with self._lock:
|
|
record = self._meta.get(phone)
|
|
|
|
if record:
|
|
stored_lat = record["rider_lat"]
|
|
stored_lon = record["rider_lon"]
|
|
dist_km = _haversine(stored_lat, stored_lon, input_lat, input_lon)
|
|
|
|
if dist_km <= SIMILARITY_THRESHOLD_KM:
|
|
# Stored rider coords match input → they represent the real door
|
|
logger.info(
|
|
f"[CustomerStore] {phone}: verified coords used "
|
|
f"(stored↔input dist={dist_km:.3f}km, within {SIMILARITY_THRESHOLD_KM}km)"
|
|
)
|
|
return stored_lat, stored_lon, True
|
|
else:
|
|
# Customer location changed significantly, trust new input
|
|
logger.info(
|
|
f"[CustomerStore] {phone}: location changed "
|
|
f"(dist={dist_km:.3f}km > {SIMILARITY_THRESHOLD_KM}km), using input coords"
|
|
)
|
|
return input_lat, input_lon, False
|
|
else:
|
|
# New customer — create record with input coords
|
|
self._create_record(phone, customer_name, input_lat, input_lon)
|
|
logger.info(f"[CustomerStore] {phone}: new record created ({input_lat}, {input_lon})")
|
|
return input_lat, input_lon, False
|
|
|
|
def update_rider_coords(self, phone: str, rider_lat: float, rider_lon: float):
|
|
"""
|
|
Called when a delivery is confirmed to update with actual rider GPS.
|
|
This is what makes stored coords progressively more accurate.
|
|
"""
|
|
phone = str(phone).strip()
|
|
with self._lock:
|
|
if phone in self._meta:
|
|
record = self._meta[phone]
|
|
record["rider_lat"] = rider_lat
|
|
record["rider_lon"] = rider_lon
|
|
self._save()
|
|
logger.info(f"[CustomerStore] {phone}: rider coords updated ({rider_lat}, {rider_lon})")
|
|
|
|
def record_count(self) -> int:
|
|
return len(self._meta)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Internal helpers
|
|
# ------------------------------------------------------------------
|
|
|
|
def _create_record(self, phone: str, name: str, lat: float, lon: float):
|
|
self._meta[phone] = {
|
|
"name": name,
|
|
"rider_lat": lat,
|
|
"rider_lon": lon,
|
|
}
|
|
self._save()
|
|
|
|
|
|
# Singleton — keep old name for backward compatibility with any imports
|
|
_store_instance: "CustomerCoordStore | None" = None
|
|
_store_lock = Lock()
|
|
|
|
|
|
def get_faiss_store() -> CustomerCoordStore:
|
|
"""Returns the singleton CustomerCoordStore (backward-compat name)."""
|
|
global _store_instance
|
|
if _store_instance is None:
|
|
with _store_lock:
|
|
if _store_instance is None:
|
|
_store_instance = CustomerCoordStore()
|
|
return _store_instance
|
|
|
|
|
|
# Also expose under new name
|
|
get_customer_store = get_faiss_store
|