159 lines
6.4 KiB
Markdown
159 lines
6.4 KiB
Markdown
# 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.
|