reconcile changes

This commit is contained in:
2026-07-06 15:48:56 +05:30
parent 871981035a
commit 7d60556131
6 changed files with 99 additions and 18 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
__pycache__/
*.db
*.sqlite3
*.pyc

View File

@@ -284,6 +284,8 @@ async def reconcile_steps(body: Any = Body(default=None)):
)
from app.services.routing.route_optimizer import RouteOptimizer
from datetime import timedelta
from dateutil.parser import parse as parse_date
results = []
@@ -331,15 +333,17 @@ async def reconcile_steps(body: Any = Body(default=None)):
# ── Detect anomaly on the FULL order list ───────────────────────────
anomaly, reason = _detect_step_anomaly(all_orders)
if not anomaly and not pending:
# Everything delivered, steps clean — nothing to do
if not anomaly:
# Steps are already clean and sequential — nothing to fix. Don't
# touch a healthy in-progress route just because it has pending
# stops; only an actual anomaly should trigger resequencing.
results.append({
"rider_id": rider_id,
"rider_name": rider_name,
"anomaly_detected": False,
"anomaly_type": "ok",
"anomaly_type": reason,
"delivered_count": len(delivered),
"pending_count": 0,
"pending_count": len(pending),
"resequenced": False,
"orders": all_orders,
})
@@ -370,24 +374,53 @@ async def reconcile_steps(body: Any = Body(default=None)):
)
return o
# ── Renumber: delivered keep positions 1..N, pending continue after ─
# Sort delivered by their existing step so the sequence is stable.
# Done BEFORE re-optimizing pending so the rider's last drop can be
# used as the start point for resequencing (see below).
delivered_sorted = sorted(delivered, key=_step_val)
# Reassign delivered steps cleanly (fills any gap in delivered portion)
for i, o in enumerate(delivered_sorted, start=1):
o["step"] = i
max_delivered_step = len(delivered_sorted)
# ── Rider's current position ────────────────────────────────────────
# A mid-route rider is at their last delivered drop, not back at the
# kitchen. Feed that in as the optimizer's start point so pending
# stops get resequenced from where the rider actually is; falls back
# to None (-> pickup/kitchen coords) only if nothing has been
# delivered yet, which is correct for a route that hasn't started.
start_coords = None
if delivered_sorted:
try:
slat = float(delivered_sorted[-1].get("deliverylat") or 0)
slon = float(delivered_sorted[-1].get("deliverylong") or 0)
if slat != 0 or slon != 0:
start_coords = (slat, slon)
except (ValueError, TypeError):
pass
# ── Re-optimise pending orders ──────────────────────────────────────
# Run the same route optimizer used at assignment time so step order
# matches the greedy + 2-opt algorithm (now with crossings eliminated).
# Fallback: sort by existing step value so at least the sequence is
# deterministic when the optimizer cannot run.
resequenced_pending: list[dict] = sorted(pending, key=_step_val)
used_optimizer = False
if pending:
try:
_opt = RouteOptimizer()
_optimized = await _opt.optimize_provider_payload(
[_norm_coords(o) for o in pending],
start_coords=None,
start_coords=start_coords,
)
if _optimized:
# Optimizer returns list in route order (index 0 = first stop).
# Use it; otherwise keep the sorted fallback.
resequenced_pending = _optimized
used_optimizer = True
else:
logger.warning(
f"[Reconcile] Optimizer returned empty result for rider "
@@ -399,14 +432,54 @@ async def reconcile_steps(body: Any = Body(default=None)):
f"(non-fatal, keeping step-sorted fallback): {_oe}"
)
# ── Renumber: delivered keep positions 1..N, pending continue after ─
# Sort delivered by their existing step so the sequence is stable.
delivered_sorted = sorted(delivered, key=_step_val)
# The optimizer computes cumulativekms/cumulative_eta relative to
# start_coords (the rider's last drop), i.e. starting back at 0 —
# but those fields mean "total since kitchen departure" and the
# delivered orders ahead of them keep their original kitchen-baseline
# values untouched. Offset the pending values so the full list stays
# continuous instead of jumping back down after the delivered stops.
if used_optimizer and delivered_sorted:
try:
base_km = float(delivered_sorted[-1].get("cumulativekms") or 0)
except (ValueError, TypeError):
base_km = 0.0
try:
base_eta = float(delivered_sorted[-1].get("cumulative_eta") or 0)
except (ValueError, TypeError):
base_eta = 0.0
if base_km or base_eta:
for o in resequenced_pending:
try:
o["cumulativekms"] = int(round(float(o.get("cumulativekms") or 0) + base_km))
except (ValueError, TypeError):
pass
try:
o["cumulative_eta"] = str(int(round(float(o.get("cumulative_eta") or 0) + base_eta)))
except (ValueError, TypeError):
pass
# Reassign delivered steps cleanly (fills any gap in delivered portion)
for i, o in enumerate(delivered_sorted, start=1):
o["step"] = i
max_delivered_step = len(delivered_sorted)
# ── Recompute expected delivery time for pending orders ─────────────
# A resequenced order's cumulative_eta just changed (offset above, or
# a fresh optimizer value) — any expectedDeliveryTime computed at the
# original assignment is now stale, same math as /riderassign uses.
for o in resequenced_pending:
pickup_slot_str = (
o.get("pickupSlot") or o.get("pickupslot")
or o.get("pickup_slot") or o.get("pickuptime")
)
if not pickup_slot_str or not o.get("cumulative_eta"):
continue
try:
pickup_time = parse_date(str(pickup_slot_str))
eta_mins = int(float(o["cumulative_eta"]))
expected_time = pickup_time + timedelta(minutes=eta_mins)
o["expectedDeliveryTime"] = expected_time.strftime("%Y-%m-%d %I:%M %p")
o["transitMinutes"] = eta_mins
except Exception as _te:
logger.warning(
f"[Reconcile] Could not recompute expectedDeliveryTime for "
f"rider {rider_id} order {o.get('orderid')}: {_te}"
)
# Pending steps start immediately after the last delivered step.
# resequenced_pending is already in route order (optimizer output or

View File

@@ -1044,13 +1044,17 @@ class RouteOptimizer:
order["previouskms"] = int(round(step_dist)) # Bug fix: was hardcoded 0 for step 1
order["cumulativekms"] = int(round(cumulative_dist))
# actualkms = direct pickup-to-door distance (for billing)
plat, plon = start_lat, start_lon
# actualkms = direct pickup-to-door distance (for billing).
# Always the order's own kitchen, never the routing depot: when
# start_coords is the rider's current position (e.g. reconcile
# mid-route) rather than the kitchen, start_lat/start_lon no
# longer equals the pickup location.
plat, plon = (
_to_float(order.get("pickuplat")),
_to_float(order.get("pickuplon") or order.get("pickuplong")),
)
if plat == 0:
plat, plon = (
_to_float(order.get("pickuplat")),
_to_float(order.get("pickuplon") or order.get("pickuplong")),
)
plat, plon = start_lat, start_lon
dlat, dlon = locations[solver_idx]
true_dist = self.haversine_distance(plat, plon, dlat, dlon) * 1.3

Binary file not shown.