Files
Doormilexpress_console/doormile-flow.md
2026-08-12 12:55:18 +05:30

241 lines
8.6 KiB
Markdown

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