reconcile changes
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.db
|
||||||
|
*.sqlite3
|
||||||
|
*.pyc
|
||||||
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 app.services.routing.route_optimizer import RouteOptimizer
|
||||||
|
from datetime import timedelta
|
||||||
|
from dateutil.parser import parse as parse_date
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
@@ -331,15 +333,17 @@ async def reconcile_steps(body: Any = Body(default=None)):
|
|||||||
# ── Detect anomaly on the FULL order list ───────────────────────────
|
# ── Detect anomaly on the FULL order list ───────────────────────────
|
||||||
anomaly, reason = _detect_step_anomaly(all_orders)
|
anomaly, reason = _detect_step_anomaly(all_orders)
|
||||||
|
|
||||||
if not anomaly and not pending:
|
if not anomaly:
|
||||||
# Everything delivered, steps clean — nothing to do
|
# 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({
|
results.append({
|
||||||
"rider_id": rider_id,
|
"rider_id": rider_id,
|
||||||
"rider_name": rider_name,
|
"rider_name": rider_name,
|
||||||
"anomaly_detected": False,
|
"anomaly_detected": False,
|
||||||
"anomaly_type": "ok",
|
"anomaly_type": reason,
|
||||||
"delivered_count": len(delivered),
|
"delivered_count": len(delivered),
|
||||||
"pending_count": 0,
|
"pending_count": len(pending),
|
||||||
"resequenced": False,
|
"resequenced": False,
|
||||||
"orders": all_orders,
|
"orders": all_orders,
|
||||||
})
|
})
|
||||||
@@ -370,24 +374,53 @@ async def reconcile_steps(body: Any = Body(default=None)):
|
|||||||
)
|
)
|
||||||
return o
|
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 ──────────────────────────────────────
|
# ── Re-optimise pending orders ──────────────────────────────────────
|
||||||
# Run the same route optimizer used at assignment time so step order
|
# Run the same route optimizer used at assignment time so step order
|
||||||
# matches the greedy + 2-opt algorithm (now with crossings eliminated).
|
# matches the greedy + 2-opt algorithm (now with crossings eliminated).
|
||||||
# Fallback: sort by existing step value so at least the sequence is
|
# Fallback: sort by existing step value so at least the sequence is
|
||||||
# deterministic when the optimizer cannot run.
|
# deterministic when the optimizer cannot run.
|
||||||
resequenced_pending: list[dict] = sorted(pending, key=_step_val)
|
resequenced_pending: list[dict] = sorted(pending, key=_step_val)
|
||||||
|
used_optimizer = False
|
||||||
|
|
||||||
if pending:
|
if pending:
|
||||||
try:
|
try:
|
||||||
_opt = RouteOptimizer()
|
_opt = RouteOptimizer()
|
||||||
_optimized = await _opt.optimize_provider_payload(
|
_optimized = await _opt.optimize_provider_payload(
|
||||||
[_norm_coords(o) for o in pending],
|
[_norm_coords(o) for o in pending],
|
||||||
start_coords=None,
|
start_coords=start_coords,
|
||||||
)
|
)
|
||||||
if _optimized:
|
if _optimized:
|
||||||
# Optimizer returns list in route order (index 0 = first stop).
|
# Optimizer returns list in route order (index 0 = first stop).
|
||||||
# Use it; otherwise keep the sorted fallback.
|
# Use it; otherwise keep the sorted fallback.
|
||||||
resequenced_pending = _optimized
|
resequenced_pending = _optimized
|
||||||
|
used_optimizer = True
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"[Reconcile] Optimizer returned empty result for rider "
|
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}"
|
f"(non-fatal, keeping step-sorted fallback): {_oe}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── Renumber: delivered keep positions 1..N, pending continue after ─
|
# The optimizer computes cumulativekms/cumulative_eta relative to
|
||||||
# Sort delivered by their existing step so the sequence is stable.
|
# start_coords (the rider's last drop), i.e. starting back at 0 —
|
||||||
delivered_sorted = sorted(delivered, key=_step_val)
|
# 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)
|
# ── Recompute expected delivery time for pending orders ─────────────
|
||||||
for i, o in enumerate(delivered_sorted, start=1):
|
# A resequenced order's cumulative_eta just changed (offset above, or
|
||||||
o["step"] = i
|
# a fresh optimizer value) — any expectedDeliveryTime computed at the
|
||||||
max_delivered_step = len(delivered_sorted)
|
# 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.
|
# Pending steps start immediately after the last delivered step.
|
||||||
# resequenced_pending is already in route order (optimizer output or
|
# resequenced_pending is already in route order (optimizer output or
|
||||||
|
|||||||
Binary file not shown.
@@ -1044,13 +1044,17 @@ class RouteOptimizer:
|
|||||||
order["previouskms"] = int(round(step_dist)) # Bug fix: was hardcoded 0 for step 1
|
order["previouskms"] = int(round(step_dist)) # Bug fix: was hardcoded 0 for step 1
|
||||||
order["cumulativekms"] = int(round(cumulative_dist))
|
order["cumulativekms"] = int(round(cumulative_dist))
|
||||||
|
|
||||||
# actualkms = direct pickup-to-door distance (for billing)
|
# actualkms = direct pickup-to-door distance (for billing).
|
||||||
plat, plon = start_lat, start_lon
|
# 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:
|
if plat == 0:
|
||||||
plat, plon = (
|
plat, plon = start_lat, start_lon
|
||||||
_to_float(order.get("pickuplat")),
|
|
||||||
_to_float(order.get("pickuplon") or order.get("pickuplong")),
|
|
||||||
)
|
|
||||||
dlat, dlon = locations[solver_idx]
|
dlat, dlon = locations[solver_idx]
|
||||||
true_dist = self.haversine_distance(plat, plon, dlat, dlon) * 1.3
|
true_dist = self.haversine_distance(plat, plon, dlat, dlon) * 1.3
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user