backend requirements onthe xustomer app

This commit is contained in:
2026-09-07 10:55:11 +05:30
parent 35675d8a9b
commit 1b2690b21a
56 changed files with 13342 additions and 1071 deletions

147
CLAUDE.md
View File

@@ -90,12 +90,14 @@ Five distinct front doors into this system. Only the backend API surface for
each has been directly inspected this session (via `routes.go`); the actual
client codebases (Flutter, React) have not been opened in this session.
1. **Customer app (B2C)** — Flutter. Auth via Firebase OTP (phone). Backend
surface: 19 customer routes **[verified this session]**
(`customer`/`customerAuth` groups) — booking creation, tracking,
`AppCustomer`/`AppCustomerLocation`. **[carried forward]**: reported built
and verified in prior sessions; the last live end-to-end test was
blocked here — see §9.
1. **Customer app (B2C)** — Flutter, being replaced: `doormile_customer_app`
(PIN auth, single-destination bookings) is retired in favour of
`doormile_cx`. Backend surface rebuilt 2026-09-05 to the Customer App v1
contract: **28 customer routes** (`customer`/`customerAuth` groups) — OTP
auth with refresh, serviceability/slots/limits, place proxy, fare estimate,
multi-destination pickups, per-order tracking, push devices. See §8.5. Auth
is a 4-digit OTP to phone or email, NOT Firebase and NOT the miler PIN flow;
the SMS gateway is still unplugged, which is the same wall §9 describes.
2. **Miler app** — Flutter, for delivery riders. Backend surface: 38 routes
**[verified this session]** (`miler`/`milerAuth` groups) — duty
start/stop, GPS pings, assignment accept/reject/cancel, delivery
@@ -462,6 +464,139 @@ has not run against a real DB. `go build`, `go vet` and `go test ./...` all pass
---
## 8.5 Customer app v1 — the `/customer/*` rebuild (2026-09-05)
**[verified this session]** — implements *Doormile — Backend Requirements
(Customer App v1)* for the new `doormile_cx` Flutter client. Full contract,
decisions and the written answers to the requirement doc's open questions:
[`docs/customer-app-api.md`](docs/customer-app-api.md). Spec:
[`docs/openapi-customer.yaml`](docs/openapi-customer.yaml).
**The structural change: a customer books a PICKUP, not a shipment.** One
booking → 1..N destinations → one consignment and one tracking number per
destination, minted when the miler completes the pickup. `pickupbookings`
carried exactly one delivery address in its own columns, so there was nowhere to
put a second; `bookingdestinations` is what closes that.
**The compatibility rule that makes it safe — do not break it:** destination 0
is mirrored onto the booking's flat `delivery*` columns. The miler app, the hub
console, the routing code and the hyperlocal check all read those columns and
none of them changed. A booking with **no** destination rows (every
console/express booking, every pre-existing row) produces exactly one
consignment through the same loop, byte-for-byte as before. Single-destination
is one leg, never a special case.
**Two prior surfaces were replaced, on Suriya's call (2026-09-05).** The PIN
auth (`/customer/register|login|verify-pin|reset-pin`, plus the email-OTP pair)
and the single-destination booking create/list/detail/cancel/price and
`/customer/track/:trackingno` are gone — `doormile_customer_app` is being
retired in favour of `doormile_cx`. `controllers/otpController.go` was deleted
with them. Customer routes: 19 → 28.
**Identifier formats changed platform-wide.** `generateBookingNo()` now mints
`DM-######` and `generateTrackingNo()` mints `DMX########`, both off Postgres
sequences (`cx_booking_reference_seq`, `cx_tracking_seq`, created in
`migrations/migrate.go`). The old generators used four random bytes; both
columns are `UNIQUE` and a random short id collides long before the space runs
out. Existing rows keep their `DM-BK-`/`DM-TRK-` strings — nothing parses either
format, so the two coexist and the console just shows the new one for new work.
### Conventions added — reuse these, don't reimplement
- **`utils.CxOK` / `CxCreated` / `CxList` / `CxFail`** (`utils/response_cx.go`)
are the ONLY response helpers for `/customer/*`. Deliberately separate from
`utils.OK`/`Fail`: the customer contract always sends `message` (empty on
success) and nests the code under `error.code`, while miler/console put `code`
at the top level. Never mix them on one surface.
- **`utils.EpochMillis(t)`** (`utils/epoch.go`) is the ONLY way a timestamp
leaves `/customer/*`. This DB stores IST wall-clock digits (see `DBNow`), so
`t.UnixMilli()` is off by 5h30m — the same defect that produced "yesterday's
work shown as today" on the miler app. `utils/epoch_test.go` asserts it for
both taggings the driver can produce.
- **`internal/cxstage`** is the ONE place a customer stage is written. `Record`
takes the caller's `*gorm.DB` — a stage event must commit or roll back with
the operational write it describes. It dedupes per (booking, destination,
stage), and `Notify` fires only after commit.
- **`renderCxBooking` + `loadCxBundle`** (`controllers/cxBookingView.go`) build
the canonical booking object. Every read that returns a booking goes through
them; `loadCxBundle` is a fixed number of queries regardless of page size.
- **`cxDestinationForConsignment(id)`** resolves a consignment to its booking.
Use it instead of `WHERE consignmentid = ?` on `pickupbookings` — that column
names only the FIRST order of a multi-destination pickup (see the bugs below).
- **`cxPickupLegs(tx, booking)`** splits a booking into the journeys to create
at pickup-complete. It is what decides single-vs-fan-out; nothing downstream
needs to know which it got.
### Stage derivation (the actual work)
Nine stages, lowercase snake_case, in `constants.CxStage*`. The client parses
them verbatim and **silently falls back to `booked` on an unknown key** — never
add or rename one without a client release. A booking rolls up from its
**slowest** order once parcels split, or a customer sees "Delivered" while a
parcel is still at a hub. Nothing is backfilled: a pre-existing booking gets a
short honest history rather than an invented one.
`cxstage.Release` is the one place a stage moves **backwards** — a miler
cancelling returns the pickup to the pool rather than cancelling it, and without
walking the stage back the customer keeps seeing a rider who is not coming.
### Four pre-existing bugs fixed in passing
All the same root cause, all found because the fan-out forced every consignment
lookup to be re-read. Each would have broken multi-destination pickups outright:
1. **`MilerDeliverConsignment` could not close orders 2..N** — its ownership
check was `WHERE consignmentid = ? AND assignedmileruserid = ?` on
`pickupbookings`, so a rider delivering the second parcel of a three-stop
visit got "assigned consignment not found" and could not complete at all.
2. **`MilerStartDelivery` notified nobody for orders 2..N** — same join, so no
push and no receiver OTP.
3. **`MilerInwardConsignmentAtHub` left assignments open for orders 2..N** — the
rider could not go off duty (`MilerEndDuty` refuses on an open assignment)
and the leg's distance/earnings recorded as zero.
4. **`GET /miler/bookings` showed only the first order** — one row per booking
keyed on that same column, so the fan-out would have minted orders no rider
could see or deliver. `milerStopsForBooking` now emits one stop per order
after collection, one visit before it, and exactly one row (unchanged) for a
booking with no destination rows.
Also: **`CityGateMiddleware` was a no-op for customer bookings.** It sniffs the
body for `pickuppincode`, which the new request shape does not carry, so every
customer booking sailed past the operating-city gate. Now checked in the handler
via the exported `middlewares.PincodeInOperatingCity`.
### Blockers and gaps — state these plainly if asked
- **No SMS provider exists.** `internal/sms` is the seam (a `Sender` interface,
a logging sink, `sms.Register()`); until a gateway is plugged in, OTP codes go
to the application log and nowhere else. **This is the single blocker on real
customer sign-in** — and it is the same wall §9's E2E test hit. Staging has
`CX_STAGING_OTP` (refused when `ENV=production`), which unblocks automated
tests.
- **No integration test has hit any of these endpoints.** `go build`, `go vet`
and `go test ./...` pass; new unit tests cover the pure logic (stage rollup,
epoch conversion, phone normalisation, weight fallback). None of that proves
behaviour against a real DB/Redis/NATS.
- **The migration has not run against a real database.** Additive, so it should
be safe — but that is not the same as having run.
- **Failed delivery is invisible to the customer.** `MilerSkipDelivery` works
operationally, but there is no tenth stage key for it and an unknown key
renders as `booked`, so a failed attempt leaves the parcel showing "Out for
delivery". Needs product + a client release.
- **Latency (p95 ≤ 400ms) is unmeasured.** Reads are batched and pricing is
Redis-warmed, but that is an argument, not a measurement.
- **No retention policy** for parcel photos or PII — nothing prunes either. The
30-minute signed-URL TTL limits link lifetime, not object lifetime.
### New env vars
`GEOCODER_URL`, `GEOCODER_EMAIL` (place proxy — the app is never handed a map
key, after the legacy rider app's key had to be revoked), `MILER_CALL_PROXY`
(masked calling; empty exposes the rider's real number — **set before launch**),
`CX_STAGING_OTP`, `CX_ALLOW_STAGE_OVERRIDE`.
---
## 9. Current blockers & open work (whole-project level)
**[carried forward]**