1054 lines
49 KiB
Markdown
1054 lines
49 KiB
Markdown
# Miler App — API requirements for the backend team
|
||
|
||
**From:** Miler rider-app engineering
|
||
**Date:** 21 Aug 2026 · **revised 24 Aug 2026**
|
||
**Against:** *Doormile Miler App — API reference* (38 `/miler/*` routes)
|
||
**Base URL:** `https://api.doormile.com/api/v1`
|
||
|
||
Everything below was traced in the shipped app and, where it says *verified*,
|
||
confirmed with live calls against production using rider **Rajan A**
|
||
(`userid 38`, `tenantid 13`, configid 1001) on 20–21 Aug 2026.
|
||
|
||
Requests are ordered by **what is blocking riders today**. Each one states the
|
||
symptom, the evidence, and the smallest change that fixes it.
|
||
|
||
---
|
||
|
||
## 0. Status — updated 21 Aug 2026
|
||
|
||
The backend team shipped requests **1, 2, 3, 5, 6** and cross-cutting items
|
||
**10.1–10.4** the same day. The app has been updated against the new contract
|
||
and the whole suite is green; each section below carries its own status line.
|
||
|
||
**What the app now does with it**
|
||
|
||
| Shipped | What the app changed |
|
||
|---|---|
|
||
| `Collected_By_Miler` | A real rung in `ConsignmentState`, excluded from the hub guard — a rider is never told the hub is holding a parcel that is in his own box. |
|
||
| `POST /consignments/:id/start-delivery` | **Start round** is a server write again. The local released-set is written **only** for what the server released; a stop it refused keeps its PICKED word rather than showing a rung the hub does not agree with. |
|
||
| `GET /consignments/:consignmentid` | The delivery gate reads state in one call instead of pulling and sorting the whole history. The logs walk stays as a fallback so a rider on this build meeting an older deployment is not stranded. |
|
||
| `consignmentid` + `consignmentstatus` on lists | The 17/29 blocker is closed, and the row itself now says where the delivery half has got to — so a delivered stop drops off the Deliveries tab without a per-stop round trip. |
|
||
| `Arrived_At_Pickup` | Parsed and mapped to the *arrived* rung; **I've arrived** now shows in the console. The undocumented `At_Customer` spelling still parses. |
|
||
| Skip from `Collected_By_Miler` | A failed attempt is reportable the moment the parcel is collected, not only once the round has started. |
|
||
| Error codes on 4xx | Every branch reads `ApiResult.code` — `INVALID_STATE`, `IDEMPOTENCY_IN_PROGRESS`. No delivery decision is made from message prose any more. |
|
||
| `Idempotency-Key` | Sent on `pickup-complete`, `deliver`, `skip`, `payment` and `start-delivery` as a stable `verb:resource:day` key. `IDEMPOTENCY_IN_PROGRESS` is waited out and re-asked twice rather than surfaced — a rider is no longer told his delivery failed while it is in the act of succeeding. |
|
||
|
||
**The flag split — confirmed and handled.** `Collected_By_Miler` +
|
||
`start-delivery` ship behind `MILER_COLLECTED_STATE_ENABLED`, **default off**,
|
||
so hyperlocal pickup still goes straight to `Out_for_Delivery` today. This
|
||
build is correct in both worlds and does not need the flag flipped:
|
||
|
||
- **Flag off** — the row's `consignmentstatus` already reads `Out_for_Delivery`
|
||
when the load reaches the Deliveries tab, so **Start round** spends no
|
||
request at all and behaves exactly as it does now. The app never fires a
|
||
`start-delivery` that can only be refused.
|
||
- **Flag on** — the row reads `Collected_By_Miler`, the press calls
|
||
`start-delivery`, and the local released-set is written only for what the
|
||
server released.
|
||
|
||
Nothing here needs coordinating: flip the flag whenever this build is out.
|
||
|
||
**Also shipped, unannounced and now consumed:** `step`, `stoptype`,
|
||
`etaminutes`, `cumulativekms` and `cumulativeeta` on the booking row. The
|
||
adapter builds a fixed map, so it had been dropping all five; the app now reads
|
||
them. `step` in particular is what lets the delivery leg follow the hub's route
|
||
instead of re-sorting nearest-first — **but see request 13: it is `0` on every
|
||
row.**
|
||
|
||
**Still open:** request 4 (upload route for proof of delivery — the photo
|
||
cannot leave the phone until this exists), **13** (populate the sequence),
|
||
**14** (an outcome for a failed attempt), 7, 8, 9.
|
||
|
||
---
|
||
|
||
## 0y. Backend reply — 25 Aug, and what the app did with it
|
||
|
||
Point by point, with what changed on this side. Where an item needed no code,
|
||
that is said rather than left implied.
|
||
|
||
| Their answer | App-side |
|
||
|---|---|
|
||
| **1 · `reached` persists** `Arrived_At_Pickup` + lat/lng + `reachedat` | Consumed. The local ARRIVED store is **kept for now** — see below. |
|
||
| **2 · `step: 0` is not a bug; gate on `sequencedat`** | **Fixed, and it was a real bug on our side.** |
|
||
| **3 · pickup-flow endpoints live**, `PATCH /addresses` built | Already wired; §3 of `MILER_LOGISTICS_API.md` describes it. |
|
||
| **4 · `pickup_proof` accepts a `bookingid`** | **Wired.** The parcel photo now leaves the phone. |
|
||
| **5 · Spaces rotation held** (jupiter still ships the key) | Understood. See the ask below. |
|
||
| **6 · Ordering not server-enforced** — do we want it? | **Yes, please.** Reasoning below. |
|
||
| **7 · Error codes present** | Consumed; nothing to do. |
|
||
|
||
### 2 · `sequencedat` — the bug was ours
|
||
|
||
The contract was read correctly and implemented correctly: `RouteOrder` gates on
|
||
`sequencedat` and treats `step: 0` with a null stamp as *no route, fall back*,
|
||
exactly as specified.
|
||
|
||
It never ran, because **`ApiConfig.pickupFromBooking` was dropping the field.**
|
||
The adapter mapped `step`, `stoptype`, `etaminutes`, `cumulativekms` and
|
||
`cumulativeeta` — and not the one that says whether any of them mean anything.
|
||
So every adapted row looked unsequenced and the app fell back to nearest-first
|
||
on routes the hub had actually solved.
|
||
|
||
Carried through now, and pinned by `route_order_test` so it cannot be dropped
|
||
again. **No backend action needed, and no `bookingno` to send you** — the rows
|
||
you saw with `step: 0` + `sequencedat: null` were correct and the app was
|
||
mishandling the correct answer.
|
||
|
||
### 1 · Why the local ARRIVED store is still there
|
||
|
||
It is redundant now and it is not yet removable, for one reason: your own
|
||
verification note says the `Miler_Assigned → Arrived_At_Pickup` flip is the one
|
||
branch that could not be exercised live — booking 179 never got assigned.
|
||
|
||
The store costs nothing while it waits. It is the weakest local record in the
|
||
app: any server answer outranks it, and it is dropped the moment the stop is
|
||
collected. So during the rollout it is a superset — it covers a build meeting an
|
||
older deployment, and it goes quiet the instant `reached` starts persisting.
|
||
|
||
**Removing it is one constant and one call site.** Confirm the flip on a real
|
||
assigned booking and it goes in the same sitting.
|
||
|
||
### 6 · Yes, enforce the ordering
|
||
|
||
`addresses` → `parcel` → `payment` → `pickup-complete` is a client convention
|
||
today, and the app follows it deliberately — the note in
|
||
`MILER_LOGISTICS_API.md` §4 states each dependency. Please make it a server
|
||
rule with `INVALID_STATE`.
|
||
|
||
Not because we expect to violate it. Because a convention only one client knows
|
||
is a convention that breaks the first time a second client — or a retry, or a
|
||
background replay — gets the order wrong, and the failure would be silent: a
|
||
consignment routed and priced from an address that arrived too late, with
|
||
nothing on either side saying so.
|
||
|
||
---
|
||
|
||
## 0x. What we still need from you
|
||
|
||
Two asks, one small and one that is costing requests today.
|
||
|
||
### A · A per-day series on `GET /miler/earnings` — P2
|
||
|
||
The Earnings chart draws seven bars. The response carries six **totals for the
|
||
period asked for** and no series, so the app was reading a `breakdown` array
|
||
that is not on the contract — it was always null, and the chart drew an empty
|
||
week while the rider had ridden all of it.
|
||
|
||
It is fixed on our side by asking **seven `period=daily&date=…` calls
|
||
concurrently** and building the week from them. That works, and it is bounded,
|
||
and it is seven requests for one chart.
|
||
|
||
**Ask:** `breakdown: [{ day, kms }]` on `period=weekly`. The app already has the
|
||
fast path for it — the seven calls are skipped the moment that field appears,
|
||
with no further change here.
|
||
|
||
### B · Per-stop `riderkms` on `GET /miler/bookings` — P2
|
||
|
||
Activity now shows the rider his distance for the day. The only source is
|
||
per-stop distance, which the app reads from `compliance.actualkm` /
|
||
`riderkms` — present on some rows and absent on most, so the figure reads as a
|
||
dash for riders who have certainly ridden.
|
||
|
||
**Ask:** return the distance the server already computes per stop on the booking
|
||
row, under whichever of those two names you prefer. The app reads both, plus its
|
||
own `actualkms` spelling, so no coordination is needed on naming.
|
||
|
||
### C · Nothing else
|
||
|
||
The ARRIVED → PICKED → ACTIVE ladder, the pickup flow, the pricing call, the
|
||
upload route and the error codes are all contracted and consumed. If item 1's
|
||
flip verifies, this list is A and B.
|
||
|
||
---
|
||
|
||
## 0z. Superseded — the two asks that were open before the 25 Aug reply
|
||
|
||
**Both are answered.** Kept because the reasoning under each is still the record
|
||
of why the app is shaped the way it is, and because §1's workaround is still in
|
||
the build. Read §0y first; this is the request that prompted it.
|
||
|
||
## 0z. Open, as of 25 Aug — the two that still shape the rider's day
|
||
|
||
Everything else on the ARRIVED → PICKED → ACTIVE flow is settled. These two are
|
||
not, and both are already worked around on the app side rather than waiting on
|
||
you — so nothing is blocked, but both workarounds are costs we would rather not
|
||
keep paying.
|
||
|
||
### 1 · `POST /miler/bookings/:id/reached` does not persist the arrival
|
||
|
||
**Today.** It answers `200 {"success": true}` and the booking stays on
|
||
`Miler_Assigned`. The arrival is never recorded, so the hub cannot see that a
|
||
rider is standing at a pickup.
|
||
|
||
**Request body the app already sends:**
|
||
|
||
```jsonc
|
||
{ "latitude": 11.0168, "longitude": 76.9558 }
|
||
```
|
||
|
||
**Please make it write:** the ARRIVED state, the latitude/longitude received,
|
||
and an arrival timestamp.
|
||
|
||
**What the app does meanwhile.** ARRIVED is treated as a **rider-owned rung**
|
||
and kept in a local store (`arrived_order_ids`), because the rung existed only
|
||
as a field on an in-memory row and the very next queue poll — which the arrival
|
||
sheet itself triggers — rebuilt that row from a server still saying
|
||
`Miler_Assigned` and put it back on ACCEPTED. From the rider's side, marking
|
||
arrived did nothing.
|
||
|
||
That store is deliberately the **weakest** local record in the app: anything the
|
||
server does know about a stop outranks it, and it is dropped the moment the stop
|
||
is collected. **Delete it the day this endpoint persists** — a local record that
|
||
outranks the server is a liability once the server has the answer. It is one
|
||
constant and one call site; say the word and it goes.
|
||
|
||
### 2 · `GET /miler/bookings` returns `step: 0` on every live row
|
||
|
||
The transport is there and the app consumes all five fields. The data is not:
|
||
every live booking arrives with `step: 0`, so the hub-planned route order cannot
|
||
be followed and the app falls back to sorting nearest-first. A rider works an
|
||
optimised route by guessing at it.
|
||
|
||
**Expected on each row:**
|
||
|
||
```jsonc
|
||
{ "step": 1, "stoptype": "...", "etaminutes": 10,
|
||
"cumulativekms": 2.5, "cumulativeeta": 15 }
|
||
```
|
||
|
||
This is request 13 below, restated because it is the largest remaining gap on
|
||
the logistics line and the one with no app-side workaround worth having.
|
||
|
||
### 3 · Nothing else is needed for this flow
|
||
|
||
The ARRIVED → PICKED → ACTIVE ladder needs no other backend change, unless
|
||
something has moved in the `pickup-complete` / consignment-state contract that
|
||
§1–§3 of `MILER_LOGISTICS_API.md` does not already describe.
|
||
|
||
**For the record, what shipped on the app side alongside this:** ARRIVED kept
|
||
locally; PICKED held after `pickup-complete` instead of mirroring the
|
||
compatibility-mode release; ACTIVE only once the rider presses **Start
|
||
delivery**; geofence enforcement restored at 10 m with GPS-accuracy and
|
||
stale-fix handling and a no-fix refusal; and tests pinning the rung ladder.
|
||
|
||
---
|
||
|
||
## 0a. Second delivery — what the app consumed on 24 Aug
|
||
|
||
The backend's follow-up batch (stop typing, COD, pre-pickup skip, earnings
|
||
counts, profile fields, reject-by-body) is **in and wired**. What changed on
|
||
this side:
|
||
|
||
| Shipped | Where it lands in the app |
|
||
|---|---|
|
||
| `stoptype` + `step` on `GET /miler/bookings` | Stop typing is authoritative now — the app no longer infers a stop's leg from its shape. The mixed pickup/delivery route is reachable. |
|
||
| `codamount` + `paymentmode` | Cash-to-collect is read from the booking instead of a field the payload never carried, so a COD stop states the real figure at the door. |
|
||
| `POST /bookings/:id/skip` | The pre-pickup skip. The app was posting a **booking** id to `/consignments/:id/skip` — the only skip route that existed — which either 404'd or skipped whichever consignment happened to hold that number. That is now the right route with the right id, and the stop stays resumable. |
|
||
| `cancelled_stops` + `total_stops` | The success rate has a denominator. It was `completed_stops` alone, which read 100% on a day with three cancellations. |
|
||
| `email` + `address` on `PUT /profile` | Both fields the edit screen has always collected now leave the device. `EMAIL_IN_USE` (409) is branched on by code. |
|
||
| `reason` in the reject body | Sent in the body; the query-string duplicate is dropped. |
|
||
|
||
**Still not shipped, and still shaping the app:** `step` is `0` on every live
|
||
row (request 13), so the admin route order the app is built to follow does not
|
||
exist in the payload and it falls back to its own ordering. That one is the
|
||
difference between the app following the hub's plan and inventing a plan, and
|
||
it is now written up on its own — **`MILER_ROUTE_ORDER_REQUEST.md`** — because
|
||
it is the single largest gap left between what the console believes a rider is
|
||
doing and what he is actually doing.
|
||
|
||
Short version: the console decides the order, the rider follows it, and the app
|
||
already works that way. But `step` is never written, and it disappears from
|
||
`/miler/assignments` the moment a stop is collected — which is the moment the
|
||
delivery leg starts. So every delivery in production today is ordered by the
|
||
app, not by the hub, and neither side can currently tell.
|
||
|
||
---
|
||
|
||
## 0c. Third delivery — answered 24 Aug, consumed the same day
|
||
|
||
Every open question came back. What the app did with each:
|
||
|
||
| Answer | What changed here |
|
||
|---|---|
|
||
| **Sequencing is automatic**, `sequencedat` is the authority signal, `step` survives the pickup leg | `RouteOrder` reads the stamp as the authority and accepts a positive `step` alongside it. `step: 0` + null stamp is now a *stated* three-case condition rather than an unexplained fallback. |
|
||
| **POD upload — `POST /miler/uploads/sign`** | Wired end to end: sign, `PUT` the bytes with the returned headers verbatim (the `x-amz-acl` is part of the signature, and the bearer token must not travel to the object store), send the public URL as `photourl`. A failed upload never blocks a hand-over — the parcel is in the customer's hands whatever the network did, and the local copy is kept. |
|
||
| **Skip: `attemptcount` is truth, ceiling of 3, no auto-RTO** | The app already parked rather than closed a skipped stop; it now reads the count off the response and, on the third, tells the rider the hub has it — the difference between a fourth attempt and a phone call. |
|
||
| **No `tripid`/`slotid`, ever** | The day-part split is documented client behaviour now rather than a guess standing in for a field. Recorded on `TripSlots`, which is the class that would be deleted if that changed. |
|
||
| **Delivery OTP is fully server-side** | Nothing to build: the app never claimed to verify a code, and it already branches on `OTP_INVALID` / `OTP_REQUIRED` by code. "OTP verified" is only ever drawn off a 200. |
|
||
| **`ridercharges` is the client price, not rider pay; `bonuspoints` unused** | The reward-points tile is **withheld** rather than showing a permanent zero — that reads as *you have earned nothing*, not as *not built yet*. It returns automatically the moment the figure is non-zero. No money figure anywhere in the app is presented as what a rider earned. |
|
||
| **`reached` persists behind the flag** | Already handled: the app reads the transition rather than the 200, so it is correct with the flag off and with it on. |
|
||
|
||
Left on our side: the console status mappings (`arrived_at_pickup → arrived`,
|
||
`collected_by_miler → picked`) before the flag is flipped. That is console work,
|
||
not rider-app work.
|
||
|
||
---
|
||
|
||
## 0b. Summary table
|
||
|
||
| # | Request | Priority | Status |
|
||
|---|---|---|---|
|
||
| 1 | A consignment state that means *collected, not yet riding* | **P0** | ✅ Shipped 21 Aug — `Collected_By_Miler` + `start-delivery` |
|
||
| 2 | `GET /miler/consignments/:id` (current state, one call) | **P0** | ✅ Shipped 21 Aug |
|
||
| 3 | Return `consignmentid` on `GET /miler/bookings` | **P0** | ✅ Shipped 21 Aug — with `consignmentstatus` |
|
||
| 4 | A file-upload route for proof of delivery | **P1** | ⏳ **Open** — POD cannot leave the phone |
|
||
| 5 | A booking state for *arrived at pickup* | **P1** | ✅ Shipped 21 Aug — `Arrived_At_Pickup` |
|
||
| 6 | Allow cancel/skip after pickup, or document the refusal | **P1** | ✅ Shipped 21 Aug — skip from collected |
|
||
| 7 | Real route distance + duration on the assignment | **P2** | ⏳ Open — figures are estimates |
|
||
| 8 | Persist `bonuspoints`, and a per-stop payout figure | **P2** | ⏳ Open — Earnings shows placeholders |
|
||
| 9 | Notification read-state table | **P3** | ⏳ Open — known stub |
|
||
| 13 | Route sequence — field shipped, **never populated** | **P0** | ⚠️ **Half** — `step` is on both endpoints but is `0` everywhere, `sequencedat` null. **Written up on its own: `MILER_ROUTE_ORDER_REQUEST.md`** |
|
||
| 14 | An outcome for a failed delivery attempt | **P1** | ⏳ **Open** — skip leaves the consignment open with no way to say why |
|
||
| 15 | `reached` must actually persist `Arrived_At_Pickup` | **P0** | 🔴 **BLOCKER** — reproduced: returns 200, writes nothing |
|
||
| 16 | Admin mapping for `Arrived_At_Pickup` + `Collected_By_Miler` | **P0** | 🔴 **Console change** — both absent; flag must not be flipped first |
|
||
| 17 | What `/admin/bookings` returns in `status` | **P1** | ✅ Answered 22 Aug — frozen booking status, plus `consignmentstatus` for the live one |
|
||
| 18 | Per-stop type + step ordering on `/miler/bookings` | **P0** | ✅ Shipped 24 Aug — consumed |
|
||
| 19 | `codamount` on the booking | **P0** | ✅ Shipped 24 Aug — consumed |
|
||
| 20 | A pre-pickup skip route | **P1** | ✅ Shipped 24 Aug — consumed; fixed a wrong-id post on our side |
|
||
| 21 | `cancelled_stops` / `total_stops` on earnings | **P2** | ✅ Shipped 24 Aug — consumed |
|
||
| 22 | `email` / `address` on `PUT /profile` | **P2** | ✅ Shipped 24 Aug — consumed |
|
||
| 23 | A trip/slot id, if the console owns the split into runs | **P2** | ✅ Answered 24 Aug — there is none; the day-part split is ours by agreement |
|
||
| 4 | A file-upload route for proof of delivery | **P1** | ✅ Shipped 24 Aug — signed URL, wired end to end |
|
||
| 13 | Route sequence — populated, with `sequencedat` as the authority | **P0** | ✅ Answered 24 Aug — automatic on every assignment; consumed |
|
||
| 24 | A rider payout rate card | **P2** | ⏳ **Open** — `ridercharges` is the client price; no rider figure is shown until there is one |
|
||
|
||
---
|
||
|
||
## 1. P0 — There is no consignment state meaning "collected, not yet riding"
|
||
|
||
> **✅ Shipped 21 Aug 2026 — Option A.** `pickup-complete` now stops at
|
||
> `Collected_By_Miler`; `POST /miler/consignments/:id/start-delivery` makes the
|
||
> release. The app's **Start round** is a server write again.
|
||
|
||
### Symptom (reported)
|
||
> *"If I try to update as Picked it is directly updating as **active** in the
|
||
> console."*
|
||
|
||
### Evidence — verified
|
||
`GET /miler/consignments/logs/34` returns the consignment's real history:
|
||
|
||
```
|
||
Out_for_Delivery "Package collected by miler and converted to consignment"
|
||
Delivered "Delivered to SEQTEST Ukkadam at (11.00506, 76.95087)"
|
||
```
|
||
|
||
The first row is written by **`pickup-complete`** — it carries that handler's
|
||
own remark. So on the hyperlocal path, `pickup-complete` moves the consignment
|
||
straight to `Out_for_Delivery`.
|
||
|
||
The console maps (`src/utils/bookingStatus.js`):
|
||
|
||
```js
|
||
picked_up: 'picked',
|
||
converted_to_consignment: 'picked',
|
||
out_for_delivery: 'active', // ← what the operator sees
|
||
```
|
||
|
||
So the moment the rider taps **Picked** at the kitchen counter, the consignment
|
||
is `Out_for_Delivery` and the operator's board says **active** — while the food
|
||
is still on the counter and the rider has not moved.
|
||
|
||
### Why this is a backend request, not a UI one
|
||
The app already models the distinction locally (it knows the rider has not set
|
||
off), but local state cannot reach the hub, and the operator's board is fed
|
||
from server state. There is no server state that expresses *collected but not
|
||
yet out for delivery*, so the information does not exist to display.
|
||
|
||
### Requested change — pick **one**
|
||
|
||
**Option A (preferred).** Add a consignment status between conversion and the
|
||
road:
|
||
|
||
```
|
||
Created → Inwarded_at_Hub → … → Out_for_Delivery → Delivered
|
||
▲
|
||
Collected_By_Miler ← NEW
|
||
```
|
||
|
||
`pickup-complete` writes `Collected_By_Miler` for hyperlocal work.
|
||
A new call moves it on:
|
||
|
||
```http
|
||
POST /miler/consignments/:id/start-delivery
|
||
→ 200 { success, data: { consignmentid, status: "Out_for_Delivery" } }
|
||
400 if the consignment is not Collected_By_Miler
|
||
```
|
||
|
||
`deliver` then accepts `Out_for_Delivery` exactly as it does now.
|
||
|
||
**Option B (smaller).** Leave the consignment alone and keep the **booking** at
|
||
`Picked_Up` until the rider starts the round, moving it to
|
||
`Converted_To_Consignment` at that point. Cheaper, but it leaves the two
|
||
vocabularies coupled, which is the root of several problems in this document.
|
||
|
||
> **Note on `POST /miler/deliveries/start`:** the app used to call this. It is
|
||
> **not** in the 38-route contract and returns `404 Cannot POST` — verified. We
|
||
> have removed the call. Request 1 Option A is the properly-specced replacement.
|
||
|
||
---
|
||
|
||
## 2. P0 — No way to read a consignment's current state in one call
|
||
|
||
> **✅ Shipped 21 Aug 2026.** `GET /miler/consignments/:consignmentid` returns
|
||
> the status plus `collected` / `out_for_delivery` / `delivered` /
|
||
> `can_start_delivery` / `can_deliver` / `can_skip`. The gate reads it; the
|
||
> logs walk remains only as a fallback for older deployments.
|
||
|
||
### Symptom (reported)
|
||
> *"I can't update it as delivered/skipped/cancelled — it's showing a lot of
|
||
> errors."*
|
||
|
||
### Evidence — verified
|
||
`POST /miler/consignments/34/deliver` →
|
||
|
||
```json
|
||
400 { "success": false, "message": "consignment is not out for delivery" }
|
||
```
|
||
|
||
That message is returned for **two opposite situations**:
|
||
|
||
| Real state | Meaning | What the rider should be told |
|
||
|---|---|---|
|
||
| `Inwarded_at_Hub` | not released yet | "the hub still has it" |
|
||
| `Delivered` | **already delivered** | "already done — nothing to do" |
|
||
|
||
In the reported case it was the second: consignment 34 had been delivered at
|
||
10:59 the previous day. The rider was shown an error for work he had completed.
|
||
|
||
### Why the app cannot fix this alone
|
||
`GET /miler/bookings` reports only the **booking** status, which is terminal at
|
||
`Converted_To_Consignment` and never learns about the delivery half. The only
|
||
route that exposes consignment state is
|
||
`GET /miler/consignments/logs/:consignmentid`, which returns the whole event
|
||
history — the app now sorts it by `historyid` and takes the last row. That
|
||
works, but it is a log-walk standing in for a state read, on the rider's mobile
|
||
data, before every delivery.
|
||
|
||
### Requested change
|
||
|
||
```http
|
||
GET /miler/consignments/:consignmentid
|
||
→ 200 {
|
||
success: true,
|
||
data: {
|
||
consignmentid: 34,
|
||
trackingno: "DM-TRK-…",
|
||
status: "Out_for_Delivery",
|
||
bookingid: 60,
|
||
attemptcount: 0,
|
||
updatedat: "2026-08-20T10:59:35Z"
|
||
}
|
||
}
|
||
404 if it is not this rider's consignment
|
||
```
|
||
|
||
**And** make the two refusals distinguishable — either distinct messages or,
|
||
better, a stable code:
|
||
|
||
```json
|
||
400 { "success": false, "code": "CONSIGNMENT_ALREADY_DELIVERED",
|
||
"message": "consignment already delivered" }
|
||
400 { "success": false, "code": "CONSIGNMENT_NOT_RELEASED",
|
||
"message": "consignment is not out for delivery" }
|
||
```
|
||
|
||
A machine-readable `code` on every 4xx across `/miler/*` would let the app stop
|
||
string-matching messages, which is fragile in exactly the way this bug proves.
|
||
|
||
---
|
||
|
||
## 3. P0 — `GET /miler/bookings` does not return `consignmentid`
|
||
|
||
> **✅ Shipped 21 Aug 2026.** Both `consignmentid` and `consignmentstatus` are
|
||
> on the list rows. The 17-of-29 gap is closed, and the consignment status now
|
||
> outranks the booking status the app draws the card from.
|
||
|
||
### Evidence — verified
|
||
24 booking rows returned for rider 38; **zero** contain any `consignment*` key.
|
||
`GET /miler/assignments` (list) does not carry it either. It appears **only** in
|
||
`GET /miler/assignments/:id`, nested inside `booking`:
|
||
|
||
```jsonc
|
||
// GET /miler/assignments/71
|
||
{ "data": { "assignment": {…}, "booking": { "bookingid": 75, …,
|
||
"consignmentid": 46 } } }
|
||
```
|
||
|
||
### This is not a performance concern — it blocks deliveries outright
|
||
|
||
**Verified 21 Aug 2026, rider 38:**
|
||
|
||
| Endpoint | Rows | Booking ids |
|
||
|---|---|---|
|
||
| `GET /miler/bookings` | **29** | 26, 28, 41, 51, **54–78**, 82 |
|
||
| `GET /miler/assignments` | **12** | 26, 28, 41, 51, 72–78, 82 |
|
||
|
||
**17 of 29 bookings have no assignment row at all.** `?status=Completed`,
|
||
`?status=all`, `?pagesize=200` and `?bookingid=59` all return the same 12 rows —
|
||
the endpoint ignores query parameters.
|
||
|
||
So for booking **59** (`DM-BK-501CB551-45130`, status
|
||
`Converted_To_Consignment`, sitting on the rider's Deliveries tab) every route
|
||
to its consignment id is a dead end:
|
||
|
||
1. the booking row — carries no consignment key *(none of the 29 do)*
|
||
2. the app's local record from `pickup-complete` — absent after a reinstall,
|
||
and absent entirely if that response did not carry the id
|
||
3. `GET /miler/assignments/:id` — **booking 59 is not in the assignments list**,
|
||
so there is no assignment id to fetch
|
||
4. `GET /miler/consignments/userlogs/:userid` — telemetry only (1 row, for an
|
||
unrelated consignment); carries no booking linkage
|
||
|
||
**There is no fifth route.** The rider is holding the parcel, the consignment
|
||
demonstrably exists (the booking is converted), and the app cannot name it — so
|
||
`deliver` can never be called and the stop can never be completed from the app.
|
||
|
||
This is the single highest-impact item in this document.
|
||
|
||
### Requested change
|
||
Add `consignmentid` (nullable) and `consignmentstatus` to every row of:
|
||
|
||
- `GET /miler/bookings`
|
||
- `GET /miler/assignments`
|
||
|
||
…and separately, **`GET /miler/assignments` should return every assignment the
|
||
rider still has work for**, not a 12-row subset. If that list is intentionally
|
||
scoped to active assignments, say so and we will stop treating it as a lookup
|
||
table — but then requirement (a) below becomes mandatory rather than merely
|
||
strongly preferred.
|
||
|
||
```jsonc
|
||
{ "bookingid": 75, "status": "Converted_To_Consignment",
|
||
"consignmentid": 46, "consignmentstatus": "Out_for_Delivery" }
|
||
```
|
||
|
||
This single change removes an N+1 call pattern **and** makes Request 2's read
|
||
unnecessary for the list case.
|
||
|
||
---
|
||
|
||
## 4. P1 — No upload route: proof of delivery cannot leave the phone
|
||
|
||
> **⏳ STILL OPEN.** The photo is captured, previewed and stored on the device,
|
||
> and it is shown on the Activity record. It cannot reach the hub until this
|
||
> route exists. This is now the highest-priority outstanding item.
|
||
|
||
### What the app now does
|
||
The rider taps **Delivered** → a proof screen opens the camera → he sees the
|
||
photo → **Mark as delivered** on the same screen. The photo is stored on the
|
||
handset and shown on the delivery record in Activity.
|
||
|
||
### The blocker
|
||
`POST /miler/consignments/:id/deliver` takes `photourl` — a **string**. Nothing
|
||
in the 38-route contract accepts a file: no multipart route, no signed-URL
|
||
endpoint, no attachment on any other call.
|
||
|
||
We deliberately **do not** send the device file path in `photourl`. A path from
|
||
somebody's phone is not a URL; writing one into the hub's record would store a
|
||
string that looks like evidence and resolves to nothing.
|
||
|
||
**So proof of delivery currently exists only on the rider's phone**, and the
|
||
hub cannot see it. For a delivery business this is the difference between
|
||
having evidence and believing you have it.
|
||
|
||
### Requested change — either shape works
|
||
|
||
**Option A — direct upload (simplest for us).**
|
||
```http
|
||
POST /miler/uploads
|
||
Content-Type: multipart/form-data
|
||
file: <binary> (jpeg, ≤ 5 MB)
|
||
purpose: "delivery_proof" ("pickup_proof" | "signature" | …)
|
||
consignmentid: 46 (optional, for association)
|
||
→ 201 { success, data: { url: "https://cdn.doormile.com/proof/46-abc.jpg" } }
|
||
```
|
||
|
||
**Option B — signed URL (cheaper server-side).**
|
||
```http
|
||
POST /miler/uploads/sign
|
||
{ "purpose": "delivery_proof", "contentType": "image/jpeg" }
|
||
→ 200 { success, data: { uploadUrl: "https://…?X-Amz-Signature=…",
|
||
url: "https://cdn.doormile.com/proof/…" } }
|
||
```
|
||
The app PUTs the bytes to `uploadUrl`, then sends `url` as `photourl`.
|
||
|
||
**Constraints from our side:** photos are JPEG, quality 70, max width 1280 —
|
||
typically 80–250 KB. Riders are on mobile data and often on 3G, so the upload
|
||
must be retryable and must **not** block the delivery: we will complete the
|
||
stop and upload in the background, then attach.
|
||
|
||
Please also confirm whether `receiversignatureurl` is expected to be fed from
|
||
the same mechanism.
|
||
|
||
---
|
||
|
||
## 5. P1 — "Arrived" does not persist: there is no booking state for it
|
||
|
||
> **✅ Shipped 21 Aug 2026.** `reached` persists `Arrived_At_Pickup`. The app
|
||
> parses it (and the older `At_Customer` spelling) to the *arrived* rung.
|
||
|
||
### Symptom (reported)
|
||
> *"If I click Arrived on the home page it is not updating as arrived."*
|
||
|
||
### Evidence
|
||
The app calls `POST /miler/bookings/:bookingid/reached` with the booking id —
|
||
correct per contract. But the booking enum is:
|
||
|
||
```
|
||
Pending_Pickup, Created, Miler_Assigned, Pickup_Scheduled,
|
||
Picked_Up, Converted_To_Consignment, Cancelled
|
||
```
|
||
|
||
**There is no `Arrived`.** Whatever `reached` writes, the operator's board maps
|
||
`pickup_scheduled → 'accepted'`, so an arrived rider is indistinguishable from
|
||
one who merely accepted the job and is still 20 km away.
|
||
|
||
### Requested change
|
||
Add `Arrived_At_Pickup` to the booking enum, written by `reached`, and return
|
||
the new status in the response so the app can confirm rather than assume:
|
||
|
||
```http
|
||
POST /miler/bookings/:bookingid/reached
|
||
→ 200 { success, data: { bookingid, status: "Arrived_At_Pickup",
|
||
reachedat: "2026-08-21T09:14:02Z" } }
|
||
```
|
||
|
||
**Every state-changing route should return the resulting entity state.** Today
|
||
most return `{success: true}` with no body, so the app cannot verify the
|
||
transition landed and must re-poll the list.
|
||
|
||
---
|
||
|
||
## 6. P1 — A rider cannot report a failed delivery
|
||
|
||
> **✅ Shipped 21 Aug 2026.** `skip` is accepted from `Collected_By_Miler` as
|
||
> well as `Out_for_Delivery`.
|
||
|
||
### Symptom (reported)
|
||
> *"I can't update it as … skipped/cancelled."*
|
||
|
||
### Evidence
|
||
- `POST /miler/consignments/:id/skip` requires `Out_for_Delivery` — so a
|
||
consignment in any other state cannot be skipped, including one the rider is
|
||
genuinely holding.
|
||
- `POST /miler/bookings/:id/cancel` is **"refused once picked up"** per the
|
||
contract. After `pickup-complete` there is therefore *no* route by which a
|
||
rider can say "this cannot be completed" — he is holding a parcel with no way
|
||
to report the failure.
|
||
|
||
### Requested change
|
||
1. Allow `skip` from `Collected_By_Miler` **and** `Out_for_Delivery` (see
|
||
Request 1).
|
||
2. Add a consignment-level failure route:
|
||
|
||
```http
|
||
POST /miler/consignments/:id/fail
|
||
{ "reason": "Customer refused", "lat": 11.0, "lon": 76.9 }
|
||
→ 200 { success, data: { status: "RTO_Initiated", attemptcount: 2 } }
|
||
```
|
||
|
||
3. Document the maximum `attemptcount` and what the hub does when it is hit —
|
||
the app should stop offering "skip" at that point rather than letting the
|
||
rider discover the ceiling by being refused.
|
||
|
||
---
|
||
|
||
## 7. P2 — The Home figures are estimates, not real data
|
||
|
||
**Direct answer to the question asked.** The four figures on Home
|
||
(`DURATION · DISTANCE · PARCELS · PAYMENT`) are:
|
||
|
||
| Figure | Source | Real? |
|
||
|---|---|---|
|
||
| **PARCELS** | sum of parcel counts on the booking rows | ✅ **Real** (backend data) |
|
||
| **PAYMENT** | sum of `collectionamt` on the booking rows | ✅ **Real** (backend data) |
|
||
| **DISTANCE** | *client-side* haversine: hub → each stop in order → hub, straight lines. Falls back to summing each row's `kms` when coordinates are missing | ⚠️ **Estimate** |
|
||
| **DURATION** | *client-side*: straight-line distance ÷ an assumed 20 km/h, **plus** a hardcoded service time per stop — 2m30 delivery / 4m00 pickup / 5m30 combined, +90s if cash, +25s per extra parcel | ⚠️ **Estimate** |
|
||
|
||
So two of the four are real and two are the app's own arithmetic. Straight-line
|
||
distance under-reads real road distance by roughly 20–40% in Coimbatore, and
|
||
the service times are guesses that have never been measured against actual
|
||
stop durations.
|
||
|
||
### Requested change
|
||
Return the routed figures on the assignment/trip, computed once server-side
|
||
(you already have the coordinates, and a routing engine gives real road
|
||
distance):
|
||
|
||
```jsonc
|
||
// GET /miler/assignments — per row, or a trip-level summary
|
||
{
|
||
"routedistancemeters": 21800, // real road distance for the leg
|
||
"routedurationseconds": 1620, // real drive time
|
||
"cumulativedistancemeters": 48200,
|
||
"cumulativedurationseconds": 5400
|
||
}
|
||
```
|
||
|
||
The assignment model already has `riderkms`, `previouskms`, `cumulativekms`,
|
||
`etaminutes` and `cumulativeeta` — **all of them return 0** today (verified on
|
||
all 24 rows). Populating those existing fields would be enough; no new schema
|
||
needed.
|
||
|
||
**Until then the app will keep showing estimates, because inventing precision
|
||
we do not have is worse than an honest approximation.**
|
||
|
||
---
|
||
|
||
## 8. P2 — Earnings has no real per-stop money
|
||
|
||
- `bonuspoints` is never written (acknowledged in your own doc).
|
||
- There is no per-stop payout anywhere in the API. `deliver` computes
|
||
`riderkms` and `ridercharges` server-side and returns **neither**;
|
||
`GET /miler/earnings` answers for a *period* only.
|
||
|
||
The app therefore shows `Stop payout · See Account` on the delivery record
|
||
rather than a number, because a figure derived from a rate the app does not
|
||
hold is the one invention a rider would act on and be wrong about.
|
||
|
||
### Requested change
|
||
Return the earnings the server already computed, on the response to `deliver`
|
||
and on the booking/consignment rows:
|
||
|
||
```jsonc
|
||
{ "riderkms": 4.6, "ridercharges": 38.50, "bonuspoints": 5 }
|
||
```
|
||
|
||
---
|
||
|
||
## 9. Battery percentage — direct answer
|
||
|
||
**Yes, it can reach the console today, and the plumbing already exists on both
|
||
sides.**
|
||
|
||
- The app reads the real battery level (`battery_plus`) in
|
||
`live_tracking_service.dart` and `foreground_service.dart`.
|
||
- It is sent on `POST /miler/logs` and `POST /miler/consignments/logs` as the
|
||
documented **string** field `battery`.
|
||
- The console already renders it — its rider-detail page (`riders.js`):
|
||
`riderLogsdata?.battery ? \`${battery}%\` : 'N/A'`.
|
||
|
||
**What to check on your side:** the console reads it from *rider logs*, so it
|
||
only appears while the app is posting telemetry (on duty, service running). If
|
||
operators see `N/A`, the likely causes in order are: (a) the rider is off duty
|
||
so nothing is being posted; (b) `GET /miler/logs` is returning the latest row
|
||
without `battery` populated; (c) the value is being stored but the console's
|
||
rider-detail query is not selecting it.
|
||
|
||
**One request:** expose the most recent telemetry row per rider in one call, so
|
||
the console does not have to page logs to find a current battery/GPS reading:
|
||
|
||
```http
|
||
GET /miler/riders/:userid/last-seen (or /admin equivalent)
|
||
→ { success, data: { latitude, longitude, battery, speed, status,
|
||
logdate, connection } }
|
||
```
|
||
|
||
---
|
||
|
||
## 10. Cross-cutting requests
|
||
|
||
> **✅ 10.1–10.4 shipped 21 Aug 2026.** Mutations return the resulting state,
|
||
> `pickup-complete` returns `consignment_id` and `next_action`, 4xx bodies
|
||
> carry a machine-readable `code`, and `Idempotency-Key` is honoured. 10.5
|
||
> (`requiredeliveryotp` on the tenant payload) is still open.
|
||
|
||
These are small and would remove whole classes of defect:
|
||
|
||
1. **Return the resulting state from every mutation.** `accept`, `reject`,
|
||
`reached`, `parcel`, `payment`, `pickup-complete`, `deliver`, `skip`,
|
||
`cancel` mostly return `{success: true}`. The app cannot confirm a
|
||
transition and must re-poll.
|
||
|
||
2. **`pickup-complete` must return the `consignmentid` it minted.** It is the
|
||
one moment the id is guaranteed to exist. The app records it locally from
|
||
the response today; when the response omits it, the delivery leg has to go
|
||
hunting (Request 3).
|
||
|
||
3. **Machine-readable error codes on every 4xx** (see Request 2). String
|
||
matching on `"consignment is not out for delivery"` is how the app ended up
|
||
telling riders the wrong thing.
|
||
|
||
4. **Idempotency.** `deliver`, `pickup-complete` and `accept` are not
|
||
idempotent. A dropped response on a bad connection means the rider retries
|
||
and gets a 400 for work that succeeded. Accepting an
|
||
`Idempotency-Key` header and replaying the original result would remove
|
||
this entire failure mode.
|
||
|
||
5. **Confirm `requiredeliveryotp` per tenant** is exposed to the app. We
|
||
currently send `otp` only when we have one and never fabricate a
|
||
placeholder; if the flag were on the profile/tenant payload we could show
|
||
or hide the OTP field correctly instead of inferring.
|
||
|
||
---
|
||
|
||
## 11. What the app already does correctly (no change needed)
|
||
|
||
Recorded so the backend team does not chase these:
|
||
|
||
- `deliver`/`skip` key on the **consignment id**, never the booking, assignment
|
||
or pickup id.
|
||
- `accept`/`reject` key on `bookingassignmentid`.
|
||
- `reached`/`parcel`/`payment`/`pickup-complete`/`cancel` key on `bookingid`.
|
||
- Telemetry sends lat/long/speed/heading/battery as **strings**, and never
|
||
sends `userid` in the body.
|
||
- Booking statuses and consignment statuses are handled as two separate
|
||
vocabularies; the app no longer infers a delivery state from a booking
|
||
status.
|
||
- Duplicate taps are collapsed client-side (one in-flight mutation per
|
||
resource+verb), but see Request 10.4 — that is not a substitute for
|
||
server-side idempotency.
|
||
|
||
---
|
||
|
||
## 11b. Request 13 — P0: the route sequence is exposed but never populated
|
||
|
||
**Re-verified live 21 Aug 2026** against rider `userid 23 / tenantid 1` —
|
||
`GET /miler/bookings` (29 rows) and `GET /miler/assignments` (11 rows).
|
||
|
||
### The transport is there. Thank you.
|
||
|
||
`step` is on **both** endpoints, alongside `sequencedat`, `etaminutes`,
|
||
`cumulativekms`, `cumulativeeta` and `stoptype`. That closes the client half of
|
||
this entirely: the app now reads `step` off the booking row, orders both legs
|
||
by it, and never re-sorts an assigned route.
|
||
|
||
### The data is empty
|
||
|
||
```
|
||
/miler/bookings step = 0 on all 29 rows
|
||
/miler/assignments step = 0 on all 11 rows
|
||
sequencedat = null on all 11 rows
|
||
etaminutes = 0, riderkms = 0, cumulativekms = 0
|
||
```
|
||
|
||
`sequencedat: null` on every assignment means **the route optimizer has never
|
||
run for this rider**. So there is no admin route to follow — not one the app is
|
||
dropping, one that does not exist.
|
||
|
||
### What the app does about it
|
||
|
||
It does not pretend. With no sequence anywhere in the set it falls back —
|
||
booked time first, then nearest-first — and **labels the queue on screen** with
|
||
which rule produced the order (`Hub route` vs `Nearest first`), so a rider is
|
||
never told the app's guess is his route. It is also logged per fetch:
|
||
|
||
```
|
||
[ROUTE][deliveries] 7 stop(s) with no assigned sequence — ordering is the
|
||
app's own fallback, not the hub's route
|
||
```
|
||
|
||
### Requested change
|
||
|
||
Populate it. Whatever writes `sequencedat` — batch assign, the console's
|
||
sequence action, a scheduled optimizer pass — is either not running or not
|
||
running for this tenant. Once `step` comes down non-zero, the app follows it on
|
||
both legs with no further change and no release needed.
|
||
|
||
**Please confirm:** is sequencing expected to be automatic on assignment, or is
|
||
it a deliberate action a hub manager has to take in the console? The answer
|
||
changes whether "no route assigned" is a normal state the rider should see or a
|
||
fault worth alerting on.
|
||
|
||
---
|
||
|
||
## 11c. Request 14 — P1: a failed delivery attempt has no outcome
|
||
|
||
`POST /miler/consignments/:id/skip` returns 200 and, as far as the app can
|
||
tell, leaves the consignment `Out_for_Delivery`. There is no state, field or
|
||
route that says *this stop was attempted and failed*.
|
||
|
||
That leaves the two systems unable to agree on whether the stop is still
|
||
actionable:
|
||
|
||
- The **rider** has been to the door. It is done with, for him, today.
|
||
- The **hub** still has an open `Out_for_Delivery` consignment.
|
||
|
||
The app refuses to resolve that by inventing a terminal state locally — it
|
||
reads the consignment after every skip and only files the stop as finished if
|
||
the server agrees it is closed. When the server keeps it open, the stop is
|
||
**parked**: still visible under SKIPPED with the rider's reason, still holding
|
||
its consignment mapping, resumable by him. Honest, but it is a workaround for a
|
||
missing contract.
|
||
|
||
### Requested change — any one of these
|
||
|
||
1. A consignment state for it — `Delivery_Failed` / `Attempt_Failed` — that
|
||
`skip` moves it to, with whatever reassignment or RTO the hub decides
|
||
happening from there.
|
||
2. An `attemptcount` + `lastattemptat` on the consignment, so the app can at
|
||
least show *"attempt 2 of 3"* and the hub can act on the count.
|
||
3. Documented confirmation that a skip is expected to leave the consignment
|
||
open and that the rider is meant to retry it in the same shift — in which
|
||
case the app will surface it as retryable rather than as a closed record.
|
||
|
||
Whichever you pick, the app needs to know **who owns the stop after a skip**.
|
||
|
||
---
|
||
|
||
## 11d. Request 15 — P0 BLOCKER: `reached` is a deployed no-op
|
||
|
||
**Reproduced against production 21 Aug 2026.** Rider `userid 23 / tenantid 1`,
|
||
booking 78, three calls in sequence:
|
||
|
||
```
|
||
GET /miler/bookings → booking 78 status = Miler_Assigned
|
||
|
||
POST /miler/bookings/78/reached
|
||
{"lat":11.0168,"lon":76.9558}
|
||
→ 200 {"success":true,"data":{"bookingid":78,"status":"Miler_Assigned"}}
|
||
|
||
GET /miler/bookings → booking 78 status = Miler_Assigned
|
||
```
|
||
|
||
The endpoint returns `success: true`, echoes the booking's **unchanged**
|
||
status, and writes nothing. `Arrived_At_Pickup` is not written and does not
|
||
appear on any of the 31 bookings in this tenant.
|
||
|
||
**This is the whole reason Arrived never reaches Admin.** It is not a client
|
||
bug: the app calls the documented endpoint with the documented body and gets a
|
||
200. It had been treating that 200 as proof of a transition, which it no longer
|
||
does — the app now reads the returned status and, when it is not
|
||
`Arrived_At_Pickup`, tells the rider his hub has not recorded the arrival
|
||
rather than drawing a tick.
|
||
|
||
### Requested change
|
||
|
||
`POST /miler/bookings/:bookingid/reached` must persist `Arrived_At_Pickup` on
|
||
the booking and return it:
|
||
|
||
```
|
||
POST /miler/bookings/:bookingid/reached
|
||
{ "lat": 11.0168, "lon": 76.9558 }
|
||
|
||
200 { "success": true,
|
||
"data": { "bookingid": 78, "status": "Arrived_At_Pickup" } }
|
||
```
|
||
|
||
**Please also confirm which is true**, because they need different fixes:
|
||
1. the handler was never wired to write, or
|
||
2. it writes only from `Pickup_Scheduled` and silently no-ops from
|
||
`Miler_Assigned` (booking 78 was assigned but not yet accepted).
|
||
|
||
If (2), say so and the app will stop offering **I've arrived** before the
|
||
accept lands. If it is meant to be callable from `Miler_Assigned`, it must
|
||
write from there too.
|
||
|
||
---
|
||
|
||
## 11e. Request 16 — P0: Admin has no mapping for the two new statuses
|
||
|
||
**Not a backend change — a console change.** Traced in
|
||
`Doormilexpress_console/src/utils/bookingStatus.js`:
|
||
|
||
```js
|
||
export const BOOKING_STATUS_TO_DELIVERY_STATUS = {
|
||
created: 'pending', pending_pickup: 'pending',
|
||
miler_assigned: 'pending', pickup_scheduled: 'accepted',
|
||
picked_up: 'picked', converted_to_consignment: 'picked',
|
||
out_for_delivery: 'active',
|
||
delivered: 'delivered', cancelled: 'cancelled'
|
||
};
|
||
```
|
||
|
||
Neither `arrived_at_pickup` nor `collected_by_miler` is in it. Unmapped
|
||
statuses "pass through lowercased", which the Deliveries page renders as an
|
||
unknown badge — and the file's own comment says the Arrived tab "will show a 0
|
||
count until the real enum is confirmed".
|
||
|
||
Two consequences the console team must act on:
|
||
|
||
1. **Even after request 15 ships, Admin still will not show Arrived.** Add
|
||
`arrived_at_pickup: 'arrived'`.
|
||
2. **Enabling `MILER_COLLECTED_STATE_ENABLED` today would make Admin worse,
|
||
not better.** With no `collected_by_miler` key, every freshly collected
|
||
parcel would render as an unknown badge instead of Active. Add
|
||
`collected_by_miler: 'picked'` **before** the flag is flipped.
|
||
|
||
### Ordering — this matters
|
||
|
||
```
|
||
1. console adds arrived_at_pickup + collected_by_miler mappings
|
||
2. backend fixes `reached` to persist Arrived_At_Pickup (request 15)
|
||
3. this app build ships — it already supports both lifecycles
|
||
4. only then flip MILER_COLLECTED_STATE_ENABLED = true
|
||
```
|
||
|
||
Flipping the flag before step 1 or step 3 breaks the rider's Picked rung.
|
||
|
||
---
|
||
|
||
## 11f. Request 17 — the one thing still unproven: what `/admin/bookings` puts in `status`
|
||
|
||
`GET /miler/bookings` reports the **booking** status: booking 77 is
|
||
`Converted_To_Consignment` with `consignmentstatus: Out_for_Delivery`.
|
||
|
||
The console reads `b.status` from `/admin/bookings` and maps it
|
||
(`api.js:611` and `api.js:664`). If that field held `Converted_To_Consignment`
|
||
too, the console would render **Picked** — which is not what we see; it shows
|
||
**Active**.
|
||
|
||
So exactly one of these is true, and we cannot tell which from the rider token:
|
||
|
||
| # | Possibility | How to confirm | Owner |
|
||
|---|---|---|---|
|
||
| A | `/admin/bookings` projects the consignment status onto `status` | one network call in the console's dev tools | backend |
|
||
| B | the backend also writes `Out_for_Delivery` onto the booking row for hyperlocal, and `/miler/bookings` reports something different | compare both endpoints for the same bookingid | backend |
|
||
| C | the deployed console is older than this source (the repo comment says `converted_to_consignment` "used to map to `accepted`") | check the deployed bundle | console |
|
||
|
||
**Please open the console's Deliveries page, look at the `/admin/bookings`
|
||
response for one collected booking, and tell us the value of `status`.** That
|
||
single value decides which of the three it is, and none of them is fixed in the
|
||
rider app.
|
||
|
||
Whichever it is, note that in compatibility mode **Active is not wrong** — the
|
||
consignment genuinely is `Out_for_Delivery`. The app now agrees with the
|
||
console instead of showing `Picked` over it. Picked/Collected becomes a real,
|
||
separate rung only once the flag is on.
|
||
|
||
---
|
||
|
||
## 12. Open questions for the backend team
|
||
|
||
~~1. Is `Collected_By_Miler` (Request 1 Option A) acceptable, or do you prefer
|
||
holding the booking at `Picked_Up` (Option B)?~~ **Answered: Option A, shipped.**
|
||
|
||
2. Which upload shape do you want to support — direct multipart or signed URL?
|
||
3. Is there an existing object store / CDN we should target for proof photos,
|
||
and what retention applies to them?
|
||
4. Can `riderkms` / `etaminutes` / `cumulativekms` be populated from the
|
||
routing engine, or is there no routing engine in the stack today?
|
||
5. What is the intended `attemptcount` ceiling, and what happens at it?
|
||
|
||
6. **Who owns the split into Trip 1 / Trip 2 / Trip 3?** The app groups a
|
||
rider's stops into up to three runs by *day part* — morning, afternoon,
|
||
evening — because nothing in the payload names a run. If the console assigns
|
||
runs, we should be following its ids rather than guessing, and a rider's
|
||
"Trip 2" and the hub's would then be the same object. Either answer is
|
||
workable; we need to know which:
|
||
|
||
- **The console owns it** → return `tripid` (and `slotid`, if separate) on
|
||
every row of `GET /miler/bookings`, stable for the day. The app follows it
|
||
and drops the day-part split entirely.
|
||
- **Nobody owns it** → say so, and the day-part split stays as the documented
|
||
client behaviour rather than an unlabelled guess.
|
||
|
||
7. **Is `step` ever written today?** Request 13 says the field exists and is `0`
|
||
on every live row. Before we spend more time on the fallback: is sequencing
|
||
generated automatically when assignments are created, or does an operator
|
||
have to trigger a route/optimizer action? If it is manual, the app should
|
||
probably say *unsequenced* rather than silently ordering the stops itself.
|
||
|
||
8. **Where should a re-quoted price go?** A logistics rider re-prices a shipment
|
||
at the door. `POST /bookings/:id/payment` records what was *collected*, and
|
||
`bookingserviceoptions.estimatedprice` is the customer's original figure and
|
||
is not rider-writable — so the new quote has nowhere to land and the booking
|
||
keeps an estimate nobody honoured. A rider-writable price field on the
|
||
booking, or an accepted field on `parcel`, closes it.
|
||
|
||
9. **Delivery OTP.** `deliver` takes an `otp` and the tenant flag
|
||
`requiredeliveryotp` gates it, but there is no delivery-OTP column on
|
||
consignments — so with the flag on there is nothing to check the code
|
||
against. Is the column planned? Until it exists the app records that a code
|
||
was *presented*, not that it was correct, and we would rather not draw a
|
||
verification the backend cannot perform.
|