393 lines
15 KiB
Markdown
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`.
|