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>
241 lines
8.6 KiB
Markdown
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.
|