production
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Miler App — API requirements for the backend team
|
||||
|
||||
**From:** Miler rider-app engineering
|
||||
**Date:** 21 Aug 2026
|
||||
**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`
|
||||
|
||||
@@ -61,6 +61,228 @@ cannot leave the phone until this exists), **13** (populate the sequence),
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
@@ -74,11 +296,20 @@ cannot leave the phone until this exists), **13** (populate the sequence),
|
||||
| 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 |
|
||||
| 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** | ❓ Unproven — one console network call answers it |
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
@@ -493,7 +724,7 @@ sides.**
|
||||
`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 — `src/pages/nearle/riders/riders.js`:
|
||||
- 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
|
||||
@@ -787,3 +1018,36 @@ separate rung only once the flag is on.
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user