Caught by testing the previous commit against production: creating a booking
with a resolved site failed with
pickupbookings_pickuplocationid_fkey
FOREIGN KEY (pickuplocationid) REFERENCES appcustomerlocations(...)
pickuplocationid is the *customer's* saved address, a B2C concept. It never
referred to the client company's own kitchens or branches. The pre-existing
code that validated an incoming pickuplocationid against TenantLocation was
wrong on the same point and would have 500'd for any caller that used it — it
had simply never been called with a value.
Adds tenantlocationid to pickupbookings and consignments (nullable, indexed,
additive via AutoMigrate), carried across at pickup, and points the reporting
filter, the by_location breakdown and the Unattributed bucket at it.
The booking request accepts tenantlocationid, and still accepts
pickuplocationid as an alias so anything written against the earlier docs
starts working instead of failing.
Also gofmt on the two model files touched; booking.go was already failing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Maps every jupiter endpoint we have replaced to its Doormile equivalent, for
the express console and the miler app only. Marks which jupiter paths were
confirmed from live network logs versus taken from the prior codebase
analysis, and states per row whether the Doormile side has been hit with a
real request or only compiles.
Includes the 11-way decomposition of PUT /deliveries/updatedelivery, the
behaviour changes that break a naive repoint, and the gaps jupiter covered
that Doormile does not yet — per-site reporting being the notable one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the five rider/tracking endpoints, the ?tenantid= staff filter and the
403-vs-404 refusal rules, and replaces the guesswork coverage note with what
was actually run against production on 2026-08-06.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
utils.Forbidden and utils.NotFound write the response and return c.JSON's
nil. Any helper that signalled refusal by returning one of them handed its
caller a nil error, so every `if err != nil { return err }` guard passed and
the handler carried straight on.
The observable result: GET /admin/milers?tenantid=14 as a DailyGrubs login
returned HTTP 403 with all 30 of the network's riders in the body. Status
line correct, payload leaked.
Three helpers were affected:
effectiveTenantID (yesterday, mine) — cross-tenant read returned the
unfiltered list under a 403
canAccessBooking (was assertBookingAccess, shipped in 6d9232f) — four
mutating booking handlers were unguarded
findMilerForConsole (was assertMilerAccess) — worse, callers went on to
dereference the nil profile
All three now return a bool and the caller writes the refusal itself, so the
control flow is visible at the call site instead of hiding in a helper.
Adds a test that pins utils.Forbidden/NotFound returning nil, so if that ever
changes the assumption breaks loudly rather than silently, plus table tests
for effectiveTenantID.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two halves of the same thing. A client login was already pinned to its own
tenant on most reads, but the roster, the B2C customer list and the dashboard
counters were not — a DailyGrubs login listed the whole network's riders.
The other half was missing entirely: Doormile's own staff had no way to look
at one client's slice. Reports accepted ?tenantid= but applied it only to the
consignment count, and bookings accepted it while milers, customers,
consignments and the dashboard ignored it.
effectiveTenantID(c) now resolves both cases in one place — the caller's own
tenant for a client login, the requested one for Doormile staff, 0 for the
whole network. A client asking for someone else's tenantid is refused rather
than silently handed their own data back under the wrong label.
Applied to: milers, customers, bookings, consignments, dashboard, reports and
the rider summary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A client login could list its own bookings but had no way to see what its
riders were actually doing. jupiter's console gave them getridersummary and
the rider/delivery logs; Doormile records all of it and exposed none of it.
New console endpoints, all tenant-scoped:
GET /admin/milers/summary roster with live state + range totals
GET /admin/milers/:id/logs GPS trail from the Redis telemetry index
GET /admin/milers/:id/activity one rider's assignments, duty and breaks
GET /admin/consignments/:id/logs event history + telemetry + proof
GET /admin/bookings/:id/track booking -> assignments -> parcel -> proof
Also closes a rider IDOR: GetMilers scoped the roster to the caller's own
fleet, but reading, editing, blocking, notifying or assigning a vehicle to a
single rider by id did not, so a client login could walk the whole network's
riders by incrementing the id. All five now go through assertMilerAccess.
And the client dashboard no longer reports milers/customers/exceptions as
zero — those have no tenant column, so they are counted through appusers,
bookings and consignments respectively.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first scoping pass covered tables carrying a tenantid column. These five
have no such column and were still returning every client's data to a client
login, which is what made creating info@dailygrubs.com unsafe:
- GET /admin/customers — scoped through the bookings placed for them, since a
customer carries no tenant of their own (the same person can order from two
clients).
- GET /admin/exceptions — scoped through the consignment the exception was
raised against.
- GET /admin/tenantcustomers — the legacy customers table predates tenant
attribution entirely, so no row can be proven to belong to a client. Returns
empty for client logins rather than handing over the whole list.
- GET /admin/tripsheets — a vehicle run routinely carries several clients'
parcels on one manifest, so there is no honest per-client view. Doormile
staff only.
- GET /admin/milers — scoped through appusers.tenantid, so a client sees their
own riders rather than the whole roster.
Also guards PUT /admin/consignments/:id/status, which took the id straight from
the path and would have let a client move another client's parcel through the
network.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
riderkms, ridercharges and bonuspoints appeared in exactly three places in the
codebase — all three GET /miler/earnings reading them. Nothing ever wrote them,
so every completed job reported zero distance and zero value and the earnings
screen was permanently empty.
On delivery completion:
- riderkms is the distance ridden for the booking, pickup point to where the
rider confirmed delivery, falling back to the booking's delivery coordinates
when the app sends no position.
- ridercharges is the order amount the tenant is billed, supplied at creation
as finalprice and already stored on the booking service option. Doormile does
not compute it; this is pass-through.
- bonuspoints deliberately left at zero pending a decision on what earns them.
Also moves the monthly earnings window onto utils.DBNow, so it doesn't put a
rider in the wrong month for 5h30m either side of a month boundary.
Note historical rows keep whatever completedat they were written with before
the image gained TZ=Asia/Kolkata, so figures spanning today are mixed; not
backfilled.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/api/v1/utils/users/redis exposed open CRUD — create, read, update and delete
on the cached-user store — to anonymous callers. The store is currently empty
and nothing in the console or apps calls it, so closing it breaks no client.
Third of the three route groups flagged in the 2026-07-27 audit. /crm/* stays
open by explicit decision: the field-sales Flutter app sends no credentials.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/api/v1/bookings/cache/* was open "for testing" but is live in production:
listing it returns real bookings including customer delivery addresses, and
/customer/:customer_id takes the customer straight from the URL, so anyone
could enumerate a given customer's bookings without credentials.
Now requires a console token (roles 1/3/4), matching the rest of the admin
surface. The /crm/* group stays open by deliberate decision — the field-sales
Flutter app authenticates with nothing and would break; revisit when that app
can send a key.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- POST /customer/reset-pin was unauthenticated and overwrote a customer's PIN
given only their phone number — which is the login identifier, not a secret —
so reset-pin followed by verify-pin took over any customer account. Exactly
the miler flaw fixed in fd7cf3e, on the B2C side. It now requires the account's
registered email to have been verified through the existing
send-email-otp/verify-email-otp flow; the verification is recorded in Redis
for 10 minutes and consumed on use, so one verification authorises one reset.
Accounts with no email on file are directed to support rather than left open.
- GET /customer/bookings/:id/price had no ownership check, unlike every other
customer booking route, so any signed-in customer could read the price quoted
on anyone else's booking by walking the id.
- GET /miler/consignments/userlogs/:userid took the rider from the URL and never
compared it to the caller, letting any miler read another miler's movement
history.
Verified as already correct while sweeping: miler assignment and booking-flow
handlers all scope by mileruserid/assignedmileruserid, customer booking detail
and cancel scope by appcustomerid, /internal sits behind InternalKeyAuth, and
CreateHubStaffAccount already refuses non-Doormile staff.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Security
- Express console had no tenant scoping at all: LoginAdmin hardcoded tenantid 0
into every JWT and none of the 85 admin handlers filtered by tenant, so any
client given a console login would read every other client's bookings,
customers, pricing and reports. Adds DoormileAuth.Tenantid (nil = Doormile
staff, unrestricted; set = client, scoped), emits it in the token, and scopes
reads, guards writes and pins tenantid on create.
- Miler telemetry (/miler/logs, /miler/status, /miler/consignments/logs) took
userid from the request body, letting any authenticated rider write another
rider's status and GPS trail — data the dispatch layer reasons over. Identity
now comes from the token.
- POST /miler/reset-pin was unauthenticated and overwrote a PIN given only a
phone number, so reset-pin + verify-pin took over any rider account. Now
requires admin/manager/executive auth.
Correctness
- Date ranges compared the container's UTC clock against timestamps the DB
writes as IST wall-clock (DSN sets TimeZone=Asia/Kolkata), so "today so far"
ended 5h30m in the past and silently dropped everything created after noon
IST from every report. Sets TZ in the image and adds utils.DBNow/DBToday,
which stay correct regardless of container timezone.
- CreateMiler never set Configid, so console-created riders got the column
default of 1 while LoginMiler looks up configid 1001 — every such rider was
unable to log in, reported as "no miler account found".
- Delivery wrote no consignment history row, so a tracking timeline never
showed the parcel arriving.
Features
- Delivery OTP is now real (crypto/rand, issued to the receiver, verified and
cleared on delivery) but opt-in per client via Tenant.Requiredeliveryotp,
defaulting off — friction worth it for a courier parcel, not a food order.
- Express bookings accept pickuplocationid, so the console can name a client
site (a DailyGrubs kitchen) instead of retyping its address; validated
against the tenant and carried through to the consignment.
- TenantLocation.Locationname, miler tenantid/hubid, Nagercoil (629) opened.
- PUT /miler/availability accepts both "status" and "availabilitystatus", and
/miler/location no longer drops speed/heading — both were contract
mismatches against the doc the Flutter dev was given.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Captures the working request bodies for the customer login/booking and the
admin/hub assign-miler endpoints, plus the constraints that are easy to hit
and hard to diagnose:
- CityGate only accepts pickup pincodes in 641/600/560/500
- hub assign-miler 403s unless milerprofiles.hubid matches the calling hub,
and most milers have hubid NULL
- CreateCustomerBooking auto-assigns in the background, so a booking is often
already on a miler before you assign one
- the routing watchdog sweeps unaccepted assignments within ~a minute and may
re-assign under a new bookingassignmentid, so accept IDs must be re-read
- reject takes its reason in the body, not the query string
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The hub console has its own subdomain; without it browsers block every
cross-origin call from the hub UI.
Co-Authored-By: Claude Opus 5 (1M context) <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>
Bugs found during live Coimbatore testing against production:
- GetMilerAssignments returned every assignment ever made to a miler with
no status filter, so weeks-old Rejected assignments still showed up as
actionable in the app. Now filters to Assigned/Accepted only.
- AcceptMilerAssignment overwrote assignmentstatus unconditionally, letting
a stale Rejected assignment be silently reactivated (and pushing its
booking back to Pickup_Scheduled). Now 400s unless currently Assigned,
reporting the actual status.
- RejectMilerAssignment read `reason` from the query string instead of the
JSON body, contradicting the API contract and every sibling endpoint.
- BookingPickupComplete resolved the origin hub via db.First(&hub) with no
Where clause — i.e. the lowest hub ID in the table, unrelated to the
booking or miler. Now uses the miler's own MilerProfile.Hubid, falling
back to the old behaviour with a warning only when unassigned.
- No code path ever set a consignment to Out_for_Delivery, making
MilerDeliverConsignment unreachable. BookingPickupComplete now goes
straight to Out_for_Delivery when pickup and delivery pincodes share a
3-digit postal-area prefix (same-miler hyperlocal), reusing the
hubPincodePrefix convention. Cross-hub still lands at Inwarded_at_Hub.
Also allow https://app.doormile.com in CORS, and drop a stray Windows-path
log file that was committed by accident.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds MilerDutyLog, MilerBreakLog, MilerSupportTicket models and earnings
fields on BookingAssignment, plus a new milerAppController.go wiring 10
endpoints under /api/v1/miler for the rider app: duty start/end/status,
break start/end, own-bookings listing, delivery confirmation (writes
DeliveryProof, completes the assignment, publishes booking.outcome via
NATS, and pushes an FCM delivery notification), earnings summaries
(daily/weekly/monthly), synthetic notifications, and support tickets.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds GET /hub/inbound, GET /hub/bookings (both new), and extends the
existing GET /hub/batches with optional from/to (YYYY-MM-DD, inclusive)
query params, backing the Hub Console's date-range picker on the Pickup
Requests, Receive Parcels, and Dispatch & Transfer pages. Reuses the same
range-parsing helper (renamed from parseHubDashboardRange to
parseHubDateRange) added for GET /hub/dashboard, defaulting to today when
omitted. The existing live endpoints (/inbound/today, /bookings/unassigned)
are untouched.
GET /hub/bookings also surfaces each booking's assignment status, mapped
to a small vocabulary (pending/assigned/picked_up/delivered/cancelled) via
the new hubBookingDisplayStatus, plus milername when assigned.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Previously always scoped parcels_received_today, batches_sent_today,
exceptions, and parcels_sorted to "since midnight today", ignoring any
from/to query params — so any non-default range silently returned
zeros. Now parses optional from/to (YYYY-MM-DD, same semantics as
GET /hub/report) and defaults to today when omitted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- models/agentdecision.go: AgentDecision GORM model (context/decision as jsonb, reasoning as text)
- migrations/migrate.go: AutoMigrate AgentDecision then ALTER TABLE to add vector(1536) column and CREATE ivfflat index via raw SQL
- controllers/agentDecisionController.go: CreateAgentDecision, FindSimilarDecisions (cosine distance), UpdateDecisionOutcome
- routes/routes.go: three routes under /api/v1/internal (InternalKeyAuth applied at group level)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- pricingid null: lookupDoormilePrice now returns matched rule ID; wired to BookingServiceOption.Pricingid
- Zone rename: Interstate→Regional, OtherState→National throughout (code + DB migrated)
- Zone from pincodes: CheckPrice now accepts pickup_pincode+delivery_pincode and auto-resolves zone
- Delivery geocoding: pincodeToLatLon() maps 3-digit prefix to city coords when lat/lon are 0
- Device tokens: device_token field added to PinVerify DTOs; saved on both customer and miler login
- Assignment retry: RejectMilerAssignment now re-triggers AssignCustomerMiler/AssignCRMMiler immediately
- Provider empty B2C: defaults to Doormile when no pricing provider row matches
- City gate 422→400: StatusUnprocessableEntity corrected to StatusBadRequest
- Miler GPS 0,0: WS tracking falls back to MilerProfile DB coords when Redis key is expired
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>