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

393 lines
15 KiB
Markdown

# Customer API — file reference & test guide
Companion to [`customer-app-api.md`](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.
```bash
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
```
```json
{ "identifier": "+919999900001" }
```
**2. Sign up (creates the account AND sends the code)**
```
POST https://api.doormile.com/api/v1/customer/auth/signup
```
```json
{ "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>
```
```json
{ "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
```
```json
{ "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
```
```json
{ "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
```
```json
{
"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>
```
```json
{
"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
```
```json
{ "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
```
```json
{
"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
```
```json
{ "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
```
```json
{ "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
```
```json
{
"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
```
```json
{ "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:
```sql
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:
```json
{ "success": true, "data": { ... }, "message": "" }
```
List:
```json
{ "success": true, "data": [ ... ], "total": 9, "nextCursor": "1042", "message": "" }
```
Failure:
```json
{ "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`.