diff --git a/docs/doormile-flow.md b/docs/doormile-flow.md new file mode 100644 index 0000000..4c76e9d --- /dev/null +++ b/docs/doormile-flow.md @@ -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.