Reviewed the 10 merged customer-app/base-handover commits and fixed the
defects found:
- HIGH (money): CreateCxBooking let the request body's `estimate` set the
billed price with no server-side check; it flows into Estimatedprice →
ridercharges (miler pay + tenant bill) with no weight re-price, so
{min:1,max:1} settled a delivery at ₹1. Now the client estimate is honoured
only when it matches the server quote within 15%, else the server quote
stands.
- MED: base handover freed the rider and closed the booking-level assignment
after the FIRST parcel of a multi-destination pickup, dropping the remaining
stops and crediting one leg. Now finalized only when no consignment of the
booking is still in the rider's hands.
- MED: inwardedat/completedat were written with time.Now() (UTC) instead of
DBNow() (IST), skewing them ~5h30 vs createdat and the earnings/reconcile
windows. Fixed in the handover, inbound-scan, reconcile and pickup-complete
paths.
- MED: B2C customers got two "miler assigned" pushes on auto-assign (two token
stores) and none on manual assign. Reconciled to one cxstage.Notify on both
paths.
- LOW: ReconcileHubInbound now runs in a transaction and checks its audit
inserts (was returning 200 with a silently-missing history row); CxLogout no
longer reports signedOut when the token revoke fails; a rider-named handover
base far from their reported position is rejected instead of silently
rerouting the parcel to another city.
go build, go vet and go test ./... all pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRaFH5hMRqmUQvVPQsyjZD
Miler app P0 + contract gaps found in the live audit:
- GET /miler/bookings now returns consignmentid + consignmentstatus on every
row (nullable), so the app can call deliver/skip/start-delivery straight from
the list. /miler/assignments is the active-only queue, so this is the
authoritative fix for stops that have moved onto the delivery leg.
- GET /miler/bookings now returns sequencedat per row: non-null means the
console/optimizer fixed this stop's order and the app follows step exactly;
null means no route assigned and the app may fall back to nearest-first.
- Route sequencing (internal/routing) now runs automatically after every
assignment — customer auto-assign, express auto-assign, manual assign, and
accept — via SequenceMilerStopsAsync (fire-and-forget, no-op below two active
stops). Previously only hub batch-assign sequenced, so most riders saw step=0.
- GET /admin/bookings now surfaces the live consignmentstatus alongside the
frozen booking status, so a Converted_To_Consignment booking can still show
Out_for_Delivery / Delivered instead of a generic "Active".
Two-step hyperlocal flow (Arrived_At_Pickup, Collected_By_Miler, start-delivery)
stays gated behind MILER_COLLECTED_STATE_ENABLED (default off) until the app
ships; consignmentid/status, GET /miler/consignments/:id, stable error codes and
Idempotency-Key handling are unconditional and safe on the current app.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRaFH5hMRqmUQvVPQsyjZD
Adds the backend half of the ExpressDispatchAgent flow. Express orders can now
be created batch by batch (bulk create only accumulates them, unassigned), then
an operator hits one endpoint to hand the whole pending set to the agent for
tenant-scoped assignment + road sequencing. The normal B2C flow is untouched.
- POST /admin/expressbooking/dispatch: manual trigger. Console-auth, tenant-
scoped; gathers the tenant's pending unassigned express orders (or a chosen
subset) and publishes express.dispatch_requested.
- internal API for the agent: GET /internal/express/riders (tenant's available
riders), GET /internal/express/bookings, POST /internal/express/assign (writes
the agent's decided assignments with their sequence; re-checks the already-
assigned guard so the agent can't double-assign).
- booking_assignment_service.go: extracted a behavior-preserving assignMilerTx
core; AssignMilerToBooking is unchanged in behavior. assignExpressStops writes
a batch, one FCM per rider instead of one per stop.
- EXPRESS JetStream stream / express.dispatch_requested subject.
- Gated behind EXPRESS_AGENT_ENABLED (default off): deploying this changes
nothing until the agent is confirmed running and the flag is flipped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hardening pass over the API surface. No route's auth requirements change.
Resilience:
- Add recover middleware. There was none, so an unhandled panic in any
handler propagated out of the process instead of becoming a 500.
- Add a centralized ErrorHandler so errors and recovered panics return the
same {success,message} envelope as the utils helpers, not Fiber's default
plain-text body. 5xx responses are logged with method and path.
Rate limiting:
- Global 300/min per IP as an abuse backstop, exempting health/readiness
probes and websocket upgrades.
- 10/min shared across every credential endpoint (customer/miler/admin/hub
login, verify-pin, reset-pin, email OTP). PINs are 4 digits, so the whole
keyspace was previously walkable in seconds. One shared limiter instance
means rotating between endpoints doesn't reset the budget.
- Add TRUSTED_PROXIES config. Limits key on c.IP(), which behind a TLS
terminator is the proxy, collapsing every client into one bucket. When set,
X-Forwarded-For is honoured only from those proxies so the header can't be
spoofed to dodge the limit. Logs a warning when unset.
Transactions:
- Check the error on all 51 previously-unchecked tx.Save/Create/Delete/
Model(...).Update/Commit calls across 6 controllers. A failed write inside
a transaction was silently ignored and the request still reported success;
an unchecked Commit could fail with the caller told everything worked.
Each site now rolls back and returns a specific message.
Pagination:
- Add utils.ParsePage/Paginated, reusing the pageno/pagesize convention
GetAdminBookings already established. Default 500, hard cap 1000.
- Apply to the previously unbounded consignments, tripsheets, exceptions,
app-users and clients endpoints. Defaults are high so existing consoles
that don't paginate keep working; the cap only stops a growing table from
being loaded wholesale. total is now a real COUNT, not len(data).
- GetClients also loaded the entire auth table to join in memory; it now
fetches only the current page's rows.
Tests (first in the repo):
- Extract the hyperlocal pincode rule out of BookingPickupComplete into
isHyperlocal so it is testable, covering the short/empty pincode fallback.
- Cover calculateVolumetricWeight and the ParsePage clamping rules.
Repo hygiene:
- Tag scratch/*.go with //go:build ignore. Each declared its own main(), so
`go build ./...` failed on redeclaration; it now passes repo-wide.
- Untrack scratch/node_modules (216 files) and ignore node_modules, test
artifacts, and the `doormile` binary `go build .` emits.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>