Files
doormile_backend/docs/customer-api-testing.md

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 code is whatever CX_STAGING_OTP is set to. Copy data.accessToken from 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 returns 400 "Up to 1 destinations per pickup". Returns reference (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. From picked_up onward returns 409.

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 }
}

0 is the destination's position. Any subset of fields; null clears 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 404 unless both ENV != production and CX_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

  1. GET /serviceability/states — proves the seed ran
  2. GET /serviceability/states/TN/districts
  3. GET /pickup-slots?lat=&lng= — copy a real slotId
  4. GET /config/booking-limits — confirm maxDestinations: 1
  5. POST /auth/otp/request with +919999900001
  6. POST /auth/otp/verify with the staging code — copy accessToken
  7. GET /auth/me
  8. POST /fare/estimate
  9. POST /bookings with the slot id from step 3
  10. GET /bookings/{reference} — the canonical object
  11. PATCH /bookings/{reference}/destinations/0
  12. POST /ops/bookings/{reference}/stage → delivered, then re-read step 10
  13. POST /bookings/{reference}/cancel on a fresh booking → expect 409 after picked_up, 200 before

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.