reconcile changes
This commit is contained in:
Binary file not shown.
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user