# 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.