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

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.
