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