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

8.6 KiB

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.