15 KiB
Customer API — file reference & test guide
Companion to customer-app-api.md, which explains why.
This one is the flat reference: what each file is for, and how to call every
endpoint.
Base URL: https://api.doormile.com/api/v1
Staging: https://staging-api.doormile.com/api/v1
1. What seed_customer_app.sql is for
It is the only thing that makes the booking form work. The customer app's first four calls read from tables that ship empty, so without this file the app opens to a blank state picker and nothing can be booked.
Run it once, after the Go service has started at least once — the service
runs AutoMigrate on boot (main.go:97), and this file fills the tables that
creates. Running it before the first boot fails: the tables do not exist yet.
psql "postgres://admin:PASSWORD@HOST:5433/logistics" -f seed_customer_app.sql
It is idempotent — safe to re-run. Every insert is ON CONFLICT DO UPDATE
or guarded by WHERE NOT EXISTS.
What it inserts, and why each part matters
| Rows | Purpose |
|---|---|
| 5 states (TN, KL, KA, TG, PY) | GET /serviceability/states. Puducherry is seeded serviceable with no open district on purpose — it is the only way to exercise the app's "Opening soon" screen. |
| 16 districts | GET /serviceability/states/{code}/districts. Madurai, Kozhikode and Dakshina Kannada are seeded unavailable with a reason, because the app renders those names in a "coming soon" line. Each carries centrelatitude/centrelongitude — for most destinations that is the only geography the parcel has until the miler corrects it at the door, and it is what the fare estimate is priced against. |
| 6 pickup slot templates | GET /pickup-slots. Template t5 is seeded at capacity 0 — the only way to reach the "Fully booked" state in design QA without actually filling a window. |
| 1 booking-limits row | GET /config/booking-limits. maxdestinations is seeded at 1, not 5 — see §4 below. |
| 2 test customers | +919999900001, +919999900002. Pair with CX_STAGING_OTP=1234 so automated tests can sign in without a real handset. |
| A hub-attachment UPDATE | Links each district to its nearest active hub by proximity, so the destination card can name a serving base. Done by distance rather than hard-coded ids because hub ids differ per environment. |
It also mirrors the client's in-app mock exactly, because those cases back the app's 18 widget tests and its design QA. Change the seeded availability and a green client test suite stops meaning anything.
2. Changed files — what each one is for
New files
| File | What it is for |
|---|---|
models/customer_app.go |
The 9 new tables. BookingDestination is the one that makes multi-destination expressible. |
internal/cxstage/stage.go |
Writes the customer's stage timeline from the miler's operational writes. The one place a stage is recorded. |
internal/sms/sms.go |
The seam for an SMS gateway. No provider is wired in — codes go to the log until one is. |
controllers/cxAuthController.go |
OTP auth: request, signup, verify, refresh, logout, me. |
controllers/cxCatalogueController.go |
States, districts, pickup slots, booking limits. |
controllers/cxPlacesController.go |
Geocoder proxy + Redis cache. The app is never handed a map key. |
controllers/cxFareController.go |
Fare estimate for one pickup visit. |
controllers/cxBookingController.go |
Create, list, detail, cancel, patch destination, order-by-tracking-id. |
controllers/cxBookingView.go |
Builds the canonical booking JSON. Every read that returns a booking goes through it. |
controllers/cxPickupFanout.go |
Splits one pickup into N orders at pickup-complete. |
controllers/cxConsignmentHooks.go |
Maps a consignment status to a per-order customer stage. |
controllers/cxDeviceController.go |
Push token registration. |
controllers/cxIdentifiers.go |
Mints DM-###### and DMX######## from Postgres sequences. |
controllers/cxOpsController.go |
QA-only: force a booking to any stage. Double-gated. |
utils/response_cx.go |
The customer response envelope. Separate from utils.OK/Fail on purpose. |
utils/epoch.go |
IST→epoch-millis conversion. Every customer timestamp goes through it. |
middlewares/requestid.go |
Echoes X-Request-Id on every response. |
seed_customer_app.sql |
§1 above. |
docs/openapi-customer.yaml |
The spec — 24 paths, 28 operations. |
docs/customer-app-api.md |
The change record and reasoning. |
scratch/cx_readonly_probe.go |
Read-only check of whether the migration is additive against a real DB. Writes nothing. |
controllers/cxHttp_test.go |
HTTP status-code and envelope tests. |
controllers/cxCustomerApp_test.go |
Pure-logic and fan-out tests. |
routes/routes_customer_test.go |
Routing + the guard proving miler/console are untouched. |
internal/cxstage/stage_test.go, utils/epoch_test.go |
Stage rollup and timestamp tests. |
Modified files
| File | What changed |
|---|---|
routes/routes.go |
Customer routes 19 → 28. |
controllers/customerController.go |
PIN auth + old booking handlers deleted; profile and locations kept. |
controllers/milerController.go |
Pickup-complete now fans out; parcel-confirm takes photos; stage hooks. |
controllers/milerAppController.go |
Rider queue emits one stop per order; two broken lookups fixed. |
controllers/logisticsHandoverController.go |
Consignment→booking lookup fixed; in_transit recorded. |
controllers/adminController.go |
Ops cancel now reaches the customer's projection. |
controllers/booking_assignment_service.go |
Records assigned on manual assignment. |
internal/assignment/crm_assignment.go |
Records assigned on auto-assignment. |
models/booking.go |
10 new columns on PickupBooking, 1 on BookingParcel. |
constants/constants.go |
9 stage keys, statuses, actor types. |
migrations/migrate.go |
9 tables, 2 sequences, 2 indexes. |
middlewares/idempotency.go |
Anonymous-caller scoping (security fix). |
middlewares/city_gate.go |
Exported PincodeInOperatingCity. |
middlewares/logger.go |
Logs client, platform, requestid. |
internal/storage/spaces.go |
PresignGet for signed parcel photos. |
config/config.go |
GEOCODER_URL, GEOCODER_EMAIL. |
utils/helper.go |
GenerateTokenWithTTL. |
main.go |
Registers RequestID(). |
dto/auth.go |
Retired PIN DTOs removed. |
controllers/otpController.go |
Deleted — its two routes were customer-only and are superseded. |
3. Every endpoint, with a request you can paste
Every URL below is complete — copy and paste it. Production host shown; for
staging swap api.doormile.com for staging-api.doormile.com, nothing else
changes.
Authenticated calls need one header, using the accessToken returned by
/customer/auth/otp/verify:
Authorization: Bearer <accessToken>
3.1 Auth — no token required
1. Request a sign-in code
POST https://api.doormile.com/api/v1/customer/auth/otp/request
{ "identifier": "+919999900001" }
2. Sign up (creates the account AND sends the code)
POST https://api.doormile.com/api/v1/customer/auth/signup
{ "name": "Joe Oommen", "phone": "+919876543210", "email": "joe@example.com" }
3. Verify the code → returns the session
POST https://api.doormile.com/api/v1/customer/auth/otp/verify
Header: Idempotency-Key: <any unique string>
{ "identifier": "+919999900001", "code": "1234" }
On staging
codeis whateverCX_STAGING_OTPis set to. Copydata.accessTokenfrom the response — everything below needs it.
4. Rotate the session
POST https://api.doormile.com/api/v1/customer/auth/refresh
{ "refreshToken": "<refreshToken from verify>" }
5. Who am I — GET https://api.doormile.com/api/v1/customer/auth/me (token required, no body)
6. Sign out
POST https://api.doormile.com/api/v1/customer/auth/logout
{ "refreshToken": "<token>", "deviceToken": "<fcm token>" }
3.2 Catalogue — no token required
| # | Method | URL |
|---|---|---|
| 7 | GET | https://api.doormile.com/api/v1/customer/serviceability/states |
| 8 | GET | https://api.doormile.com/api/v1/customer/serviceability/states/TN/districts |
| 9 | GET | https://api.doormile.com/api/v1/customer/pickup-slots?lat=11.0168&lng=76.9558 |
| 10 | GET | https://api.doormile.com/api/v1/customer/config/booking-limits?lat=11.0168&lng=76.9558 |
No bodies. Call 9 first in any booking test — you need a real slotId from
it, and slot ids expire.
3.3 Places — token required
| # | Method | URL |
|---|---|---|
| 11 | GET | https://api.doormile.com/api/v1/customer/places/reverse-geocode?lat=11.0168&lng=76.9558 |
| 12 | GET | https://api.doormile.com/api/v1/customer/places/search?q=brookefields&lat=11.0168&lng=76.9558 |
Empty q on 12 returns the customer's saved and recent places.
3.4 Fare estimate — token required
13.
POST https://api.doormile.com/api/v1/customer/fare/estimate
{
"pickup": { "lat": 11.0168, "lng": 76.9558 },
"destinations": [
{ "stateCode": "TN", "districtCode": "TN-MAA", "packageCount": 2 },
{ "stateCode": "KL", "districtCode": "KL-EKM", "packageCount": 1 }
]
}
3.5 Bookings — token required
14. Create a pickup
POST https://api.doormile.com/api/v1/customer/bookings
Header: Idempotency-Key: <any unique string>
{
"pickup": {
"title": "12 Nehru Street",
"sub": "Gandhipuram, Coimbatore 641012",
"lat": 11.0168,
"lng": 76.9558
},
"slotId": "PASTE_A_REAL_ID_FROM_ENDPOINT_9",
"destinations": [
{
"stateCode": "TN",
"districtCode": "TN-MAA",
"packageCount": 2,
"details": {
"street": "12th Main",
"building": "3B",
"landmark": "Near bus stand",
"recipientName": "Meera S",
"recipientPhone": "+919884412210",
"instructions": "Call before delivery",
"codAmount": 1200,
"pin": { "lat": 13.0827, "lng": 80.2707 }
}
}
],
"estimate": { "min": 167, "max": 267 }
}
⚠️ One destination only while
maxdestinations = 1(§4). Adding a second returns400 "Up to 1 destinations per pickup". Returnsreference(DM-######) and no tracking number — those are minted when the miler completes the pickup.
15. List — GET https://api.doormile.com/api/v1/customer/bookings?status=active&limit=20
status is active | completed | cancelled. Page with &cursor=<nextCursor>.
16. Detail — GET https://api.doormile.com/api/v1/customer/bookings/DM-482913
The tracking screen and receipt both render from this. Supports If-None-Match.
17. Cancel
POST https://api.doormile.com/api/v1/customer/bookings/DM-482913/cancel
{ "reason": "Package not ready" }
Allowed through
arrived. Frompicked_uponward returns409.
18. Fill in a destination's details after booking
PATCH https://api.doormile.com/api/v1/customer/bookings/DM-482913/destinations/0
{
"street": "12th Main",
"landmark": "Near bus stand",
"recipientName": "Meera S",
"recipientPhone": "+919884412210",
"instructions": "Call before delivery",
"pin": { "lat": 13.08, "lng": 80.27 }
}
0is the destination's position. Any subset of fields;nullclears one.
19. One order by tracking number — GET https://api.doormile.com/api/v1/customer/orders/DMX10482913
3.6 Devices — token required
20. Register a push token
POST https://api.doormile.com/api/v1/customer/devices
{ "token": "<fcm-token>", "platform": "android", "appVersion": "1.0.0+12" }
21. Unregister — DELETE https://api.doormile.com/api/v1/customer/devices/<fcm-token>
3.7 Account — token required
22. Profile — GET https://api.doormile.com/api/v1/customer/profile
23. Update profile
PUT https://api.doormile.com/api/v1/customer/profile
{ "name": "Joe Oommen", "email": "joe@example.com", "defaultPincode": "641012" }
24. Saved addresses — GET https://api.doormile.com/api/v1/customer/locations
25. Save an address
POST https://api.doormile.com/api/v1/customer/locations
{
"label": "Home",
"address": "12 Nehru Street, Gandhipuram",
"landmark": "Near the bus stand",
"city": "Coimbatore",
"state": "Tamil Nadu",
"pincode": "641012",
"latitude": 11.0168,
"longitude": 76.9558,
"receivername": "Joe Oommen",
"receiverphone": "+919876543210",
"isdefault": true
}
26. Update an address — PUT https://api.doormile.com/api/v1/customer/locations/1 (same body)
27. Delete an address — DELETE https://api.doormile.com/api/v1/customer/locations/1
3.8 QA only — staging
28. Force a booking to any stage
POST https://api.doormile.com/api/v1/customer/ops/bookings/DM-482913/stage
{ "stage": "out_for_delivery", "reason": "design QA" }
Returns
404unless bothENV != productionandCX_ALLOW_STAGE_OVERRIDE=true. Valid stages:booked,assigned,on_the_way,arrived,picked_up,order_created,in_transit,out_for_delivery,delivered.
4. Before you test — two things that will bite you
maxdestinations is seeded at 1. Not a bug. The fan-out works server-side,
but the deployed rider app keys its stop list on orderid, which is
booking-level — all three stops of a three-destination pickup collapse into one
in its local store, and two parcels would have no stop and no way to be closed.
Raise it only once a rider build keying on consignmentid is live:
UPDATE customerbookinglimits SET maxdestinations = 5 WHERE applocationid IS NULL;
There is no SMS provider. OTP codes are written to the application log and
nowhere else. On staging set CX_STAGING_OTP=1234 and use the seeded test
accounts. It is refused when ENV=production.
5. Suggested test order
GET /serviceability/states— proves the seed ranGET /serviceability/states/TN/districtsGET /pickup-slots?lat=&lng=— copy a realslotIdGET /config/booking-limits— confirmmaxDestinations: 1POST /auth/otp/requestwith+919999900001POST /auth/otp/verifywith the staging code — copyaccessTokenGET /auth/mePOST /fare/estimatePOST /bookingswith the slot id from step 3GET /bookings/{reference}— the canonical objectPATCH /bookings/{reference}/destinations/0POST /ops/bookings/{reference}/stage→delivered, then re-read step 10POST /bookings/{reference}/cancelon a fresh booking → expect409afterpicked_up,200before
6. Reading the responses
Success:
{ "success": true, "data": { ... }, "message": "" }
List:
{ "success": true, "data": [ ... ], "total": 9, "nextCursor": "1042", "message": "" }
Failure:
{ "success": false, "message": "Add at least one destination", "error": { "code": "invalid" } }
Branch on error.code, never on the message text. Codes: invalid,
invalid_name, invalid_otp, unauthorized, forbidden, not_found,
conflict, unserviceable, rate_limited, server_error.