The admin customer page collected an address (door no, street, suburb,
city, state, postcode, landmark, lat/lng) that the backend had nowhere to
store, so it was silently dropped on save. Add those fields flat onto
appcustomers, matching the reference console's shape, plus an applocationid
for zone scoping.
- GET /admin/customers: add ?applocationid= filter; emit firstname/lastname
split and the address fields alongside the existing joined name.
- GET /admin/customers/summary (new): stat-tile counts (total/active/blocked)
scoped like the list, so the client stops deriving them from the full page.
- PATCH /admin/customers/🆔 accept firstname/lastname directly (single name
still splits as a fallback) and persist every address field; pointer fields
so an omitted field is not confused with one cleared to empty.
Pagination and keyword search were already present. Additive, nullable
columns — AutoMigrate handles it, no data rewrite.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assignment decided who carried a booking but never what order to run
several of them in -- the one capability jupiter had that Doormile did
not. It turns out we already own the solver: routes.workolik.com is a
live in-house Route Optimization API backed by Valhalla road-network
routing, not the paid third party I had assumed. This is a client for
it, not a solver.
internal/routing posts a rider's active stops and writes back step,
previouskms, cumulativekms and ETA onto BookingAssignment. HubBatchAssign
calls it after committing a batch, which is exactly the case it exists
for: a rider used to walk away with several bookings and no order to run
them in. GetMilerAssignments now returns sequenced stops in step order,
falling back to newest-first for anything unsequenced.
Contract discovered by probing the live service -- the OpenAPI schema
types the body as a bare object array, so the field names are not
documented anywhere. They are pickuplat/pickuplong/deliverylat/
deliverylong, NOT pickuplatitude/deliverylatitude. Sending the wrong
names does not fail: it returns HTTP 200 with every coordinate defaulted
to "0.0", no reordering and all distances zero. That trap is recorded in
a comment so the next person does not lose an afternoon to it.
Numeric fields come back inconsistently typed -- previouskms as a number,
actualkms and eta as strings, some decimal -- so they are decoded loosely
and coerced, with tests pinning the coercion. Steps for deliveryids we
did not send are discarded rather than written, so an echoed or stale id
cannot reorder another rider's work.
Sequencing is best-effort throughout and runs after assignments commit.
The optimizer is a separate service over the network; it being down must
leave bookings assigned but unordered, never undo the batch. Step 0 means
"not sequenced", not "first".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Doormile should not be wired to jupiter's APIs at all. The ingest routes,
the legacy identity middleware and the worker-side fan-out existed to
copy jupiter's rider GPS into Doormile, which was never the goal:
Doormile needs its own JetStream in front of its own endpoints, not a
pipe from someone else's.
Removed here: /internal/miler/* ingest, LegacyMilerIdentity, and the
legacyuserid field on the miler update payload. The worker-side shadow
forward and its k8s secret are removed separately in the Kubernetes
repo; jupiter forwarding is untouched and verified still healthy.
MilerProfile.Legacyuserid is deliberately kept. Nothing reads it now,
but it records which jupiter rider each of the six migrated riders came
from, which is worth having during the cutover. Dropping a populated
column buys nothing and AutoMigrate would not drop it anyway.
Kept from that work because they are unrelated to jupiter and fix real
bugs: db.EnsureStreams (four subjects were publishing to no stream and
being dropped silently) and the tenantlocationid column.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Half of this binary's js.Publish calls were bound to no stream at all.
The streams were declared only by an external Python script on another
machine (Birock/doormile-bookings/setup_jetstream.py) and had drifted
from the code: booking.cancelled, booking.outcome and
booking.assignment_failed had no stream, and CHAT declared the literal
"chat.room.closed" while chat.go publishes "chat.room.closed.<id>",
which it does not match. Every publish site is best-effort
(`if db.Js != nil` + warn-log), so those events were failing and being
dropped silently — every cancellation, delivery outcome and assignment
failure since the streams were created.
db.EnsureStreams now declares the streams at startup from a map that
sits next to the code that publishes, so the contract cannot drift
again. It only ever adds: existing streams keep their storage type,
retention, limits and every subject they already have. Nothing is
deleted. Losing the create race against a sibling replica is expected
and reconciles rather than erroring.
Alongside that, /internal/miler/* ingests rider telemetry still arriving
over the jupiter NATS chain. The forwarding worker holds no rider JWT —
the rider app is still jupiter-shaped — so LegacyMilerIdentity resolves
an identity from a header into c.Locals("userid") behind the existing
X-Internal-Key guard. That lets the routes reuse the miler handlers
unchanged instead of growing a parallel set that would drift.
Identity comes from a header, never the body: the telemetry handlers
overwrite a body-supplied userid precisely so one rider cannot write
another's GPS trail, and reading it from the body here would reopen that
from behind the internal key. MilerProfile.Legacyuserid (nullable,
indexed) maps a jupiter userid to a Doormile one.
Only fire-and-forget telemetry is exposed. Transactional actions stay
synchronous — a rider needs a real answer from pickup-complete, which a
queue in front of it cannot give.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
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>
- 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>