docs: the order-to-delivery flow, end to end

Payloads taken from the request structs rather than from documentation,
since the two had already drifted once. Records the things that are easy
to get wrong and hard to diagnose: configid 1001 on both rider auth
calls, tenantlocationid vs pickuplocationid pointing at different tables,
step 0 meaning 'not sequenced' rather than 'first', and admin miler
endpoints keying on milerprofileid while assign-miler takes a
mileruserid.

Also states plainly why bulk creation cannot sequence stops: assignment
picks who, sequencing picks the order, and the second needs the first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Suriya
2026-08-11 15:38:06 +05:30
parent cb2660a3da
commit ba800aee66

240
docs/doormile-flow.md Normal file
View File

@@ -0,0 +1,240 @@
# Doormile — order to delivery
Every endpoint in the DailyGrubs path, the payload that goes in, and what
actually changes when it does. Base URL `https://api.doormile.com/api/v1`.
Payload shapes are taken from the request structs in this repo, not from
documentation — where the two disagree, the code is right. Where a field is
optional it says so.
---
## Auth
Two separate logins.
**Console.** The token carries `tenantid`, which is what scopes a client to
their own data. A client passing another tenant's `?tenantid=` gets 403; reading
another tenant's resource by id gets 404, so ids aren't probeable.
```
POST /admin/login
{ "email": "info@dailygrubs.com", "password": "admin" }
→ { "token": "eyJ…", "user": { "id": 43, "tenantid": 13 } }
```
**Rider.** Two calls, and `configid` must be **1001** on both — a Doormile login
partition with no jupiter equivalent. Omitting it is the most common reason a
rider looks like they don't exist.
```
POST /miler/login
{ "phone": "9787698259", "configid": 1001 }
POST /miler/verify-pin
{ "phone": "9787698259", "pin": "1234", "configid": 1001, "device_token": "fcm-…" }
→ { "token": "eyJ…" }
```
`POST /miler/reset-pin` is **admin-only** despite sitting under `/miler`. A phone
number is the login identifier, not a secret — left open, reset-then-verify takes
over any rider account in two calls. The rider app must not call it.
---
## 1. Create a booking
```
POST /admin/expressbooking
{
"tenantid": 13,
"tenantlocationid": 20, // the kitchen. optional — inferred if omitted
"customer_phone": "9876500011",
"customer_name": "Priya R",
"pickupaddress": "DailyGrubs RS Puram Kitchen",
"pickuppincode": "641002",
"pickuplatitude": 11.004500, "pickuplongitude": 76.961200,
"deliveryaddress": "12 Bharathi Rd, Peelamedu",
"deliverypincode": "641004",
"deliverylatitude": 11.051000, "deliverylongitude": 76.930000,
"service_option": "Fast",
"finalprice": 65,
"parcels": [ { "itemcategory": "Food", "weight": 1.2 } ]
}
→ { "bookingid": 118, "bookingno": "DM-BK-…", "status": "Created" }
```
Writes a `pickupbookings` row plus its parcels and price.
**On `tenantlocationid`.** This is the client's own site — the kitchen. Omit it
and it's resolved from the pickup coordinates: nearest stored site within 150m,
falling back to an address match, nil when unsure. It is what per-kitchen
reporting groups by.
`pickuplocationid` is accepted only as a legacy alias and is never stored as
given — the column of that name foreign-keys to `appcustomerlocations` (a B2C
customer's saved address), so writing a client site id into it fails the insert.
**CityGate.** The pickup pincode prefix must be an open city: `641` Coimbatore,
`600` Chennai, `560` Bengaluru, `500` Hyderabad, `629` Nagercoil. Anything else
is refused at creation. jupiter had no such gate.
## 2. Or create them in bulk
```
POST /admin/expressbooking/bulk
{ "bookings": [ { …same shape… }, { … } ] } // max 200
→ { "results": [
{ "index": 0, "success": true, "bookingid": 119, "bookingno": "DM-BK-…" },
{ "index": 1, "success": false, "error": "pickup pincode not serviceable" }
] }
```
Per-row results, never all-or-nothing — one bad address doesn't lose the other
199. Each booking is its own transaction. A client login cannot use bulk to
smuggle in another tenant's id; `tenantid` is pinned to the caller's own.
## 3. A rider is found — automatic
No call needed. Creation publishes `booking.assignment_requested` to JetStream
after the transaction commits. A worker searches riders within 10km via Redis
GEO, scores them through the AI layer, and commits the assignment. If nobody is
available it retries **5 times, 2 minutes apart**, then publishes
`booking.assignment_failed` for the dispatch agent.
The retry state lives in NATS, not in process memory, so a pod restart no longer
loses a booking mid-wait.
To assign by hand instead:
```
POST /admin/bookings/:id/assign-miler
{ "mileruserid": 38 }
```
## 4. Clear a hub queue, and order the stops
```
POST /hub/bookings/batch-assign
{ "bookingids": [118, 119, 120], "max_per_rider": 5 }
→ { "assigned": 3, "skipped": 0, "riderssequenced": 1,
"results": [ { "bookingid": 118, "assigned": true,
"mileruserid": 38, "distance_km": 1.4 } ] }
```
**This is the only place stops get ordered.** After assigning, each affected
rider's whole active set is sent to the Route Optimization API
(`POST /api/v1/optimization/doormile/sequence` on `routes.workolik.com`), which
returns a road-network sequence via Valhalla — not straight-line distance. The
step, per-leg distance, cumulative distance and ETA are written onto
`bookingassignments`.
**Assignment picks _who_; sequencing picks _what order_.** They are separate,
and sequencing can only run once a rider is known — which is why bulk *creation*
cannot sequence anything, however many bookings you send. jupiter got away with
sequencing inside `createdeliveries` because that payload was already one
rider's run; Doormile's bulk endpoint is up to 200 bookings across many riders.
Sequencing is best-effort and runs after the assignments commit: the optimizer
is a separate service over the network, and it being down must leave bookings
assigned but unordered, never undo the batch.
## 5. The rider runs the route
```
GET /miler/assignments
→ [ { "bookingassignmentid": 555, "bookingid": 120,
"step": 1, "previouskms": 4.0, "cumulativekms": 4.0,
"etaminutes": 14, "cumulativeeta": 14 }, … ]
```
Returned in **step order**. `step: 0` means *not sequenced* — never *first* — and
sorts to the end. A rider with one stop is never sequenced, so 0 is common.
Then, per booking:
```
POST /miler/assignments/:id/accept
POST /miler/bookings/:bookingid/reached
POST /miler/bookings/:bookingid/parcel
{ "parcels": [ { "weight": 1.4, "length": 20, "width": 15, "height": 10 } ] }
POST /miler/bookings/:bookingid/payment
{ "amount": 65, "paymentmode": "Cash", "transactionref": "" }
POST /miler/bookings/:bookingid/pickup-complete
```
`pickup-complete` is the pivot the old system had no concept of. It converts the
booking into a **consignment**, recomputes chargeable weight from the dimensions
the rider actually measured, carries the kitchen across, and decides routing —
matching 3-digit pincode prefixes go straight to `Out_for_Delivery` (hyperlocal),
everything else routes via a hub.
Other rider actions: `vehicle-required` (needs a bigger vehicle), `cancel`
(before pickup).
## 6. Deliver
```
POST /miler/consignments/:id/deliver
{ "deliveredtoname": "Priya R",
"photourl": "https://…", "receiversignatureurl": "https://…",
"lat": 11.051, "lon": 76.93,
"otp": "418317" } // only when the tenant requires it
```
Marks the consignment delivered and writes a history row. **Delivery OTP is
opt-in per tenant** (`Tenant.Requiredeliveryotp`, default off — off for
DailyGrubs). When on it is verified server-side and never serialised outward:
returning it would hand the rider the code they are meant to be told.
Couldn't deliver? `POST /miler/consignments/:id/skip` increments `attemptcount`
rather than failing the parcel.
---
## Watching it
| What | Endpoint |
|---|---|
| One booking end to end | `GET /admin/bookings/:id/track` |
| A parcel's GPS trail + history | `GET /admin/consignments/:id/logs` |
| Riders today | `GET /admin/milers/summary?from=&to=` |
| One rider's logs | `GET /admin/milers/:id/logs` |
| Per kitchen | `GET /admin/locations/summary?tenantid=&locationid=` |
| Reports | `GET /admin/reports?from=&to=&tenantid=&locationid=&hubid=` |
Admin miler endpoints key on **`milerprofileid`**, while `assign-miler` takes a
**`mileruserid`** in the body — different identity spaces on adjacent endpoints.
Worth checking which one you have.
---
## State
| Capability | State |
|---|---|
| Booking create, bulk, tracking, reports | deployed |
| Tenant scoping for client logins | deployed |
| Per-kitchen attribution | deployed |
| Durable assignment retry (JetStream) | built, **not deployed** |
| Stop sequencing (Doormile side) | built, **not deployed** |
| `/optimization/doormile/sequence` (routes.workolik.com) | built, **not deployed** |
| Multi-stop optimizer service itself | live |
**Sequencing needs two deploys, not one** — the endpoint in the route-optimizer
service (docker-compose on `31.97.228.132`, behind Traefik) and Doormile's
client that calls it. Ship one without the other and sequencing fails quietly:
bookings stay assigned but unordered, and riders choose their own order.
Nothing on the client side has moved. The rider app and express console still
call `jupiter.nearle.app`. These endpoints exist and are tested; no production
traffic uses them yet.