Caught by testing the previous commit against production: creating a booking with a resolved site failed with pickupbookings_pickuplocationid_fkey FOREIGN KEY (pickuplocationid) REFERENCES appcustomerlocations(...) pickuplocationid is the *customer's* saved address, a B2C concept. It never referred to the client company's own kitchens or branches. The pre-existing code that validated an incoming pickuplocationid against TenantLocation was wrong on the same point and would have 500'd for any caller that used it — it had simply never been called with a value. Adds tenantlocationid to pickupbookings and consignments (nullable, indexed, additive via AutoMigrate), carried across at pickup, and points the reporting filter, the by_location breakdown and the Unattributed bucket at it. The booking request accepts tenantlocationid, and still accepts pickuplocationid as an alias so anything written against the earlier docs starts working instead of failing. Also gofmt on the two model files touched; booking.go was already failing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
13 KiB
jupiter → Doormile
What the old Nearle/jupiter API did, and what replaces it in Doormile. Two surfaces only — the express console and the miler app. Hub console, CRM and the B2C customer app are out of scope here.
Base URLs:
| jupiter | Doormile | |
|---|---|---|
| API | jupiter.nearle.app/live/api/v1 |
api.doormile.com/api/v1 |
| Write path | queue.workolik.com (TLS verify off, hardcoded IP pin) |
same host, no side channel |
Confidence marking. Paths marked ✅ were read off real network logs from the live jupiter console. Paths marked ~ come from the prior-session analysis of the jupiter codebase and have not been re-confirmed against a live request — check the exact spelling before wiring anything to them.
Status: Done = built and hit with a real request · Built = written and compiled, never called · Gap = nothing replaces it yet · Dropped = deliberately not migrated.
1. Auth
| jupiter | Doormile | Status |
|---|---|---|
| ~ console login (undocumented in jupiter's own API docs — found only by reading the console source) | POST /admin/login → {email, password} |
Done |
| ~ rider login | POST /miler/login then POST /miler/verify-pin |
Done |
Two real differences:
- Doormile splits rider login into phone → PIN, two calls. jupiter did it in one.
- The Doormile console token carries
tenantid. jupiter had no tenant concept on the login at all; every console user saw everything. This is the single biggest behavioural change for a client account. configidmust be 1001 on both miler calls. There is no jupiter equivalent — it's a Doormile login partition.
2. Express console
2.1 Rider screens
| jupiter | Doormile | Status |
|---|---|---|
✅ GET /deliveries/getridersummary/?applocationid=&fromdate=&todate= |
GET /admin/milers/summary?applocationid=&from=&to=&tenantid=&hubid= |
Done |
| ~ rider list | GET /admin/milers?applocationid=&hubid=&tenantid= |
Done |
| ~ rider detail | GET /admin/milers/:id |
Done |
~ riderlogs (the 1.17M-row, zero-index table) |
GET /admin/milers/:id/logs?from=&to=&limit= |
Done |
✅ getriderlocationsummary (name confirmed, path inferred) |
covered by milers/summary (currentlatitude/longitude, lastpingat) and milers/:id/logs |
Done |
| — (no jupiter equivalent) | GET /admin/milers/:id/activity?from=&to= |
Done |
| ~ rider create/edit | POST /admin/milers, PUT /admin/milers/:id |
Done |
| ~ block rider | PUT /admin/milers/:id/block |
Built |
| ~ assign vehicle | PUT /admin/milers/:id/assign-vehicle |
Built |
| — | POST /admin/milers/:id/notify |
Done |
Parameter translation: jupiter used fromdate/todate, Doormile uses
from/to. Both YYYY-MM-DD. jupiter's applocationid=0 meant "all cities";
Doormile means the same by omitting the param.
2.2 Orders / deliveries
| jupiter | Doormile | Status |
|---|---|---|
✅ GET /deliveries/getdeliveries/ |
GET /admin/bookings + GET /admin/consignments |
Done |
~ getdelivery / getorders |
GET /admin/bookings/:id, GET /admin/consignments/:id |
Done |
~ POST /deliveries/createdeliveries |
POST /admin/expressbooking |
Done |
~ createdeliveries in bulk |
POST /admin/expressbooking/bulk (max 200, per-row results) |
Built |
~ PUT /deliveries/updatedelivery |
split into 11 endpoints — see §4 | Done / partial |
| — | GET /admin/bookings/:id/track |
Done |
| — | GET /admin/consignments/:id/logs |
Done |
| — | GET /admin/consignments/track/:trackingno |
Built |
Two jupiter bugs that do not carry over, by construction:
getdeliveriesreturned every row 21× (unconstrainedLEFT JOIN tenantpricing,DISTINCTover 87 columns that deduped nothing). Doormile's list endpoints are paginated (pageno/pagesize, default 500, cap 1000) and return one row per booking.createdeliverieshad a quadratic insert bug — a slice declared outside the loop kept accumulating, producing ~2× duplicatedeliveryqueuesrows (66,446 deliveries → 132,826 rows, confirmed live).createExpressBookingis a single transaction per booking;/bulkloops it and reports per-row.
2.3 Reporting
| jupiter | Doormile | Status |
|---|---|---|
✅ GET /deliveries/getreportsummary/?applocationid=&tenantid=&locationid=&fromdate=&todate= |
GET /admin/reports?from=&to=&tenantid=&locationid=&hubid= |
Done |
~ getlocationsummary |
GET /admin/locations/summary?tenantid=&locationid=&from=&to= |
Done |
| — | GET /admin/dashboard?tenantid= |
Done |
locationid is supported: it narrows every figure to one client site, and
/admin/reports now carries a by_location block alongside by_hub,
by_tenant and by_rider.
Attribution caveat. Per-site figures group by tenantlocationid on the
booking — a column added 2026-08-06. The pre-existing pickuplocationid column
is not it: that one foreign-keys to appcustomerlocations, the B2C customer's
saved address, so writing a client-site id into it fails the insert. Every
booking created before 2026-08-06 has no site at all.
Since the console sends a kitchen's address rather than its id,
createExpressBooking resolves the site itself — nearest stored location within
150m, falling back to an address match. Bookings with no site are reported as
their own "Unattributed" row rather than dropped, so per-site rows still add
up to the summary total. Sending tenantlocationid explicitly is exact and
always wins.
applocationid (city) is still not a report parameter. jupiter had it;
Doormile filters by hubid instead. Only matters once one client runs in more
than one city.
2.4 Tenants and their sites
| jupiter | Doormile | Status |
|---|---|---|
✅ GET /tenants/gettenants/ |
GET /admin/tenants |
Done |
✅ GET /tenants/gettenantlocations/ |
GET /admin/tenants/:id/locations |
Done |
~ getlocations / getlocation / getlocationdetails |
same as above | Done |
| ~ tenant create/edit | POST /admin/tenants, PUT /admin/tenants/:id |
Done |
| ~ location create/edit | POST /admin/tenants/:id/locations, PUT /admin/tenantlocations/:id |
Done |
~ getbranches |
GET /admin/hubs — jupiter "branches" ≈ Doormile hubs; verify this is the same concept before relying on it |
Built |
~ getlocationsummary |
GET /admin/locations/summary — see §2.3 |
Done |
Doormile adds locationname on a tenant location. jupiter identified a site by
its address alone, which does not distinguish two branches on one street.
3. Miler app
jupiter's rider app drove almost everything through one overloaded endpoint. Doormile gives each action its own route.
| jupiter action | Doormile | Status |
|---|---|---|
| ~ rider login | POST /miler/login + POST /miler/verify-pin |
Done |
| ~ PIN reset | POST /miler/reset-pin — now admin-only, see §5 |
Done |
| ~ location ping | PUT /miler/location |
Done |
| ~ availability toggle | PUT /miler/availability |
Done |
| ~ assignment list | GET /miler/assignments, GET /miler/assignments/:id |
Done |
| ~ accept | POST /miler/assignments/:id/accept |
Done |
| ~ reject | POST /miler/assignments/:id/reject |
Built |
| ~ rider logs write | POST /miler/logs, POST /miler/status |
Done |
| ~ per-delivery logs | POST /miler/consignments/logs |
Done |
| — | POST /miler/duty/start, PUT /miler/duty/end, GET /miler/duty/current |
Done |
| — | POST /miler/breaks/start, PUT /miler/breaks/end |
Done |
| — | GET /miler/earnings |
Done |
| — | POST /miler/support, GET /miler/support |
Built |
| — | GET /miler/notifications |
Done |
| — | PATCH /miler/notifications/:id/read |
Gap — stub, persists nothing |
4. PUT /deliveries/updatedelivery — the 11-way split
This is the centre of the migration. jupiter overloaded one endpoint for 11 distinct real actions, distinguished only by which JSON fields happened to be non-empty. Each is now its own route with its own validation and its own status transition.
Eight rider actions:
| Action | Doormile |
|---|---|
| reached pickup | POST /miler/bookings/:bookingid/reached |
| confirm parcel + dimensions | POST /miler/bookings/:bookingid/parcel |
| collect payment | POST /miler/bookings/:bookingid/payment |
| pickup complete | POST /miler/bookings/:bookingid/pickup-complete |
| needs a bigger vehicle | POST /miler/bookings/:bookingid/vehicle-required |
| cancel before pickup | POST /miler/bookings/:bookingid/cancel |
| deliver | POST /miler/consignments/:id/deliver |
| skip / failed attempt | POST /miler/consignments/:id/skip |
Three console actions that were bundled into the same rider endpoint:
| Action | Doormile |
|---|---|
| assign a rider | POST /admin/bookings/:id/assign-miler |
| change status | PUT /admin/bookings/:id/status · PUT /admin/consignments/:id/status |
| cancel | POST /admin/bookings/:id/cancel · POST /admin/bookings/bulk-cancel |
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 entered, and decides routing — matching 3-digit pincode prefixes go
straight to Out_for_Delivery (hyperlocal), everything else routes via a hub.
5. Behaviour changes that break a naive repoint
Response shapes are completely different — flat 87- and 92-column jupiter rows versus nested Doormile JSON. Every screen that parses a response needs rewriting, not repointing. Beyond that:
- Tenant scoping is real now. A client console login sees only its own
tenant.
?tenantid=narrows for Doormile staff; a client passing another tenant's id gets 403. Cross-tenant reads of a single resource return 404, not 403, so ids aren't probeable. jupiter had none of this. configid1001 on every miler auth call. No jupiter equivalent.- PIN reset is admin-only. jupiter let anyone reset a rider PIN with just a
phone number, which is the login identifier, not a secret. Two calls took over
any account. The rider app must not call
/miler/reset-pin— route resets through ops. - Identity comes from the token, never the body. jupiter's telemetry
endpoints took
useridfrom the request body. Doormile ignores it. - Telemetry lat/long/speed/battery are strings, and
POST /miler/consignments/logstakes a bare JSON array. - Delivery OTP is opt-in per tenant (
Tenant.Requiredeliveryotp), default off. Off for DailyGrubs. When on, it's verified server-side. - Dates:
from/to, notfromdate/todate. IST wall-clock throughout. - CityGate: a booking's pickup pincode prefix must be an open city —
641Coimbatore,600Chennai,560Bengaluru,500Hyderabad,629Nagercoil. jupiter had no such gate.
6. Gaps — jupiter did this, Doormile does not yet
| What | Detail |
|---|---|
applocationid on reports |
jupiter could filter a report by city. Doormile filters by hubid. Only bites when one client operates in several cities. |
| Route optimisation | jupiter used external paid services (routes.workolik.com) for multi-stop sequencing. Nothing in Doormile replaces true stop-ordering. HubBatchAssign decides who gets a booking, not what order to run stops in. |
| Notifications read-state | PATCH /miler/notifications/:id/read is a stub; no table exists. |
riderkms / ridercharges backfill |
Populated on new deliveries only. Rows completed before 2026-08-06 read 0 and will not backfill themselves. |
7. Dropped on purpose
| What | Why |
|---|---|
/v1/substitutions CRUD |
Rider substitutions. Low traffic in the old system; Suriya's call. Revisit if it turns out to matter. |
| jupiter's v2 endpoints | They wrote only to Redis, invisible to the v1/v3 Postgres reads — genuine split-brain, with a Redis INCR id space that could collide with the Postgres sequence. Doormile keeps Redis for ephemeral telemetry only; durable state is always Postgres. |
queue.workolik.com write path |
Separate host with TLS verification disabled and a hardcoded IP pin. Not reproduced. |
~20 never-populated columns on orders, 6 lat/lng pairs for 3 real points on deliveries, status spread across 6 text+timestamp column pairs |
Replaced by a normalised schema with a real event-log table (consignmenthistory). |
8. What is not migrated at all
Nothing on the client side has moved. The rider Flutter app and
doormile_express_console still call jupiter.nearle.app. Doormile having the
endpoint does not mean traffic uses it.
Suggested order: pick one net-new console screen (the rider summary, or reports) and wire it to Doormile first — it replaces nothing live, so it is the cheapest real proof the cutover works. Then the higher-traffic screens (deliveries list, rider status updates), then the rider app.