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:
240
docs/doormile-flow.md
Normal file
240
docs/doormile-flow.md
Normal 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.
|
||||
Reference in New Issue
Block a user