backend requirements onthe xustomer app
This commit is contained in:
147
CLAUDE.md
147
CLAUDE.md
@@ -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]**
|
||||
|
||||
Reference in New Issue
Block a user