production

This commit is contained in:
2026-08-28 11:13:15 +05:30
parent d7348e253f
commit 5723d373b2
162 changed files with 17924 additions and 7026 deletions

View File

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