Files
doormile_backend/docs/jupiter2doormile.md
Suriya 0c407e5b27 feat: per-site reporting, and actually populate the site on a booking
jupiter's getreportsummary took a locationid — per kitchen, per branch. That
was the one report parameter with no Doormile equivalent, and for a food
client with 23 kitchens it is the difference between one number and a usable
report.

  GET /admin/reports?locationid=      narrows every figure to one site
  GET /admin/reports                  now carries a by_location block
  GET /admin/locations/summary        the standalone per-site table

The filter alone would have been useless: pickuplocationid was null on every
booking in the system, because the console sends a kitchen's address rather
than its id. createExpressBooking now resolves the site itself — nearest
stored location within 150m, falling back to an address match, nil when
nothing matches confidently, since a wrong attribution silently moves orders
between kitchens. An explicit pickuplocationid still wins.

Bookings that named no site are reported as their own "Unattributed" row
rather than dropped, so per-site rows add up to the summary total.

Two fixes found while in here:
- the payments join in the per-site query fanned out, counting a booking once
  per payment row; payments are now pre-aggregated per booking
- by_rider was empty for every client login, which reads as "your riders did
  nothing". Riders are tenant-scoped now, so a client sees its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 12:56:56 +05:30

251 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
- `configid` must 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:
- `getdeliveries` returned **every row 21×** (unconstrained `LEFT JOIN
tenantpricing`, `DISTINCT` over 87 columns that deduped nothing). Doormile's
list endpoints are paginated (`pageno`/`pagesize`, default 500, cap 1000) and
return one row per booking.
- `createdeliveries` had a quadratic insert bug — a slice declared outside the
loop kept accumulating, producing ~2× duplicate `deliveryqueues` rows
(66,446 deliveries → 132,826 rows, confirmed live). `createExpressBooking` is
a single transaction per booking; `/bulk` loops 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 depend on `pickuplocationid` being set
on the booking. Every booking created before 2026-08-06 has it null, and the
console sends a kitchen's *address*, not its id. So `createExpressBooking` now
resolves the site itself — nearest stored location within 150m, falling back to
an address match — and unattributed bookings are reported as their own
`"Unattributed"` row rather than dropped, so the per-site rows still add up to
the summary total. Sending `pickuplocationid` explicitly is still better 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` | **Gap** — no per-site rollup, same missing piece as `locationid` above | **Gap** |
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:
1. **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.
2. **`configid` 1001** on every miler auth call. No jupiter equivalent.
3. **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.
4. **Identity comes from the token, never the body.** jupiter's telemetry
endpoints took `userid` from the request body. Doormile ignores it.
5. **Telemetry lat/long/speed/battery are strings**, and
`POST /miler/consignments/logs` takes a **bare JSON array**.
6. **Delivery OTP is opt-in per tenant** (`Tenant.Requiredeliveryotp`), default
off. Off for DailyGrubs. When on, it's verified server-side.
7. **Dates**: `from`/`to`, not `fromdate`/`todate`. IST wall-clock throughout.
8. **CityGate**: a booking's pickup pincode prefix must be an open city — `641`
Coimbatore, `600` Chennai, `560` Bengaluru, `500` Hyderabad, `629` Nagercoil.
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.