production
This commit is contained in:
158
MILER_ROUTE_ORDER_REQUEST.md
Normal file
158
MILER_ROUTE_ORDER_REQUEST.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# P0 — The hub's route order never reaches the rider
|
||||
|
||||
**From:** Miler rider-app engineering · 24 Aug 2026
|
||||
**To:** Doormile backend
|
||||
**Endpoints:** `GET /miler/bookings`, `GET /miler/assignments`
|
||||
**Related:** *API requirements* §11b (request 13), open questions 6 and 7
|
||||
|
||||
---
|
||||
|
||||
## The rule we are building to
|
||||
|
||||
> **The console decides the order. The rider follows it.**
|
||||
|
||||
If operations has solved a route, the app must work the stops in that exact
|
||||
order — both legs, pickup and delivery — and must never re-sort by what is
|
||||
closest to the rider. Re-optimising a planned route is not a small difference:
|
||||
it changes the arrival windows customers were promised, and it makes the hub's
|
||||
own screen a fiction.
|
||||
|
||||
This is already how the app behaves. `lib/data/route_order.dart` is the single
|
||||
place that answers it, and its rule is:
|
||||
|
||||
| The payload says | The app does | The rider is told |
|
||||
|---|---|---|
|
||||
| any stop carries a sequence | works them in that order, exactly | **Hub route** |
|
||||
| no sequence, but booked times | orders by booked time | *No route assigned — ordered by booked time* |
|
||||
| no sequence, no times | keeps the backend's own array order | *No route assigned — shown in the order they came through* |
|
||||
| nothing at all, rider has GPS | nearest-first | *No route assigned — ordered by what is closest to you* |
|
||||
|
||||
Nearest-first is the **last** of four, it is labelled as a guess wherever it
|
||||
appears, and one sequenced stop is enough to switch the whole set into hub
|
||||
order.
|
||||
|
||||
---
|
||||
|
||||
## What is happening instead
|
||||
|
||||
**Every live row comes back `step: 0`.** Verified in production, rider Rajan A
|
||||
(`userid 38`), 21–24 Aug:
|
||||
|
||||
```
|
||||
GET /miler/bookings → 29 rows · step = 0 on all 29
|
||||
GET /miler/assignments → 12 rows · step = 0 on all 12, sequencedat = null
|
||||
```
|
||||
|
||||
`step` is on the contract and is present in the payload. It is simply never
|
||||
written. So the first branch of the table above has never once been taken on a
|
||||
real device, and every rider in production is working a route the app ordered,
|
||||
while the console believes he is working the route it planned.
|
||||
|
||||
Neither side is currently able to notice. That is the part we would most like
|
||||
to close.
|
||||
|
||||
### Why it bites hardest on the delivery leg
|
||||
|
||||
`GET /miler/assignments` is — correctly, and you have confirmed this — the
|
||||
**active** queue: `Assigned` / `Accepted` only. A booking leaves that queue the
|
||||
moment it is collected.
|
||||
|
||||
The delivery leg begins the moment it is collected.
|
||||
|
||||
So even if `step` were populated on assignments tomorrow, the sequence would
|
||||
vanish at exactly the point the rider starts delivering, and the delivery
|
||||
half of his day would still be ordered by the app. **The sequence has to be on
|
||||
`GET /miler/bookings`**, which is the only list that covers all of a rider's
|
||||
work.
|
||||
|
||||
---
|
||||
|
||||
## What we need
|
||||
|
||||
### 1. Populate `step` on every routed stop
|
||||
|
||||
On **both** `GET /miler/bookings` and `GET /miler/assignments`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"bookingid": 59,
|
||||
"stoptype": "delivery",
|
||||
"step": 4, // 1-based position in the solved route
|
||||
"sequencedat": "2026-08-24 07:12:00", // when the route was solved, IST
|
||||
"tripid": 2 // see §3
|
||||
}
|
||||
```
|
||||
|
||||
- **`step` is 1-based.** `0` keeps its current meaning — *this stop is not in a
|
||||
solved route* — and the app already sends those to the end of the list.
|
||||
- **Stable for the day.** A stop's step must not change between two polls
|
||||
unless the route was genuinely re-solved. The app re-sorts on every fetch;
|
||||
a step that drifts makes the list reshuffle under the rider's thumb.
|
||||
- **Unique within a rider's route**, so two stops cannot claim position 4.
|
||||
- **Survives the leg change.** The step a booking had as a pickup, or the step
|
||||
its delivery has, must still be on the row after `pickup-complete` — that is
|
||||
the case that is broken today.
|
||||
|
||||
### 2. `sequencedat`, preserved
|
||||
|
||||
Null means *never solved*. A timestamp means *solved then*. The app uses it for
|
||||
one thing only: telling a rider **"no route assigned"** honestly instead of
|
||||
implying the hub planned an order it did not. It is currently null on every row
|
||||
even where a step exists.
|
||||
|
||||
### 3. Confirm who owns Trip 1 / Trip 2 / Trip 3
|
||||
|
||||
A rider's day is split into up to three runs. Today the app derives that split
|
||||
**client-side, from the day part** — morning / afternoon / evening — because
|
||||
nothing in the payload names a run.
|
||||
|
||||
That is a guess, and it is the wrong kind: a rider's "Trip 2" and the hub's
|
||||
"Trip 2" are not necessarily the same set of stops, and neither screen can tell.
|
||||
|
||||
Two workable answers, and we need to know which:
|
||||
|
||||
- **The console owns runs** → return `tripid` (and `slotid` if they differ) on
|
||||
every row of `GET /miler/bookings`, stable for the day. We follow it and
|
||||
delete the day-part split entirely.
|
||||
- **Nobody owns runs** → say so, and the day-part split stays as documented
|
||||
client behaviour rather than an unlabelled invention.
|
||||
|
||||
---
|
||||
|
||||
## Two questions we cannot answer from this side
|
||||
|
||||
1. **Is sequencing automatic or manual?** Is a route solved when assignments
|
||||
are created, or does an operator have to trigger a route/optimizer action?
|
||||
If it is manual, then `step: 0` is often the *correct* answer, and the app
|
||||
should say **"no route assigned"** plainly rather than quietly ordering the
|
||||
stops itself — which is a different piece of work from populating the field.
|
||||
|
||||
2. **What happens on a mid-day re-solve?** If operations re-sequences a route
|
||||
while a rider is part-way through it, do completed stops keep their old
|
||||
steps? We will follow whatever you send; we need to know whether to expect
|
||||
the numbers to move.
|
||||
|
||||
---
|
||||
|
||||
## How to tell it is fixed
|
||||
|
||||
No app release is needed for any of this — the app reads these fields today.
|
||||
|
||||
1. `GET /miler/bookings` for a rider with a solved route returns `step ≥ 1` on
|
||||
the routed stops and a non-null `sequencedat`.
|
||||
2. The same booking still carries its `step` **after** `pickup-complete`.
|
||||
3. Two consecutive polls return the same steps.
|
||||
4. On the rider's phone, the queue heading reads **Hub route** rather than
|
||||
*Nearest first* — that string is driven directly by which branch of the
|
||||
table above was taken, so it is a one-glance check that the sequence
|
||||
actually arrived.
|
||||
|
||||
---
|
||||
|
||||
## What we are not asking for
|
||||
|
||||
- Not a routing engine. If the console already stores an order, exposing it is
|
||||
enough; we are not asking anyone to solve TSP.
|
||||
- Not a new endpoint. Two fields on two existing list responses.
|
||||
- Not a change to `/miler/assignments`' scoping. Active-queue-only is right —
|
||||
it is precisely why `/miler/bookings` has to carry the sequence.
|
||||
Reference in New Issue
Block a user