Files
doormile_backend/docs/customer-app-api-crisp.md

319 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Doormile Customer App (`doormile_cx`) API — Quick Reference
> **Base URL:** `https://api.doormile.com/api/v1`
> **Namespace:** `/customer/*` | **Auth Role:** `9` (Customer) | **Data Envelope:** `{ "success": true, "data": { ... } }`
---
## 1. Global Conventions
| Aspect | Specification | Details / Rules |
|---|---|---|
| **Naming** | `camelCase` | All request & response JSON fields use camelCase. |
| **Timestamps** | Epoch milliseconds (UTC, `int64`) | Parse directly with `DateTime.fromMillisecondsSinceEpoch(ts)`. |
| **Identifiers** | Sequence-backed Feistel permutation | **Booking Reference:** `DM-482913`<br>**Tracking Number:** `DMX10482913` |
| **Headers** | `Authorization: Bearer <accessToken>`<br>`X-Client: doormile-cx/<version>+<build>`<br>`X-Platform: android | ios`<br>`Idempotency-Key: <uuid>` | • Required on all routes except pre-auth & catalogue.<br>• Client telemetry logged on every request.<br>• Idempotency supported on booking creation & OTP verification. |
| **Token Lifetime** | Access: `1 hour` \| Refresh: `60 days` | Refresh tokens rotate on every use. Replaying a revoked token revokes the entire chain. |
---
## 2. Complete Endpoint Reference (28 Routes)
### 🔐 Authentication (Pre-Auth & Session)
| Method | Endpoint | Auth | Description |
|---|---|:---:|---|
| `POST` | `/customer/auth/otp/request` | ❌ | Request OTP via SMS/Email (returns uniform success regardless of account existence). |
| `POST` | `/customer/auth/otp/verify` | ❌ | Verify OTP, returns JWT tokens & customer profile. |
| `POST` | `/customer/auth/signup` | ❌ | Register new customer or treat existing phone as sign-in. |
| `POST` | `/customer/auth/refresh` | ❌ | Rotate refresh token to issue a new access token. |
| `POST` | `/customer/auth/logout` | ✅ | Revoke session (omit `refreshToken` to sign out everywhere). |
| `GET` | `/customer/auth/me` | ✅ | Fetch currently authenticated customer identity. |
### 📦 Serviceability & Catalogue
| Method | Endpoint | Auth | Description |
|---|---|:---:|---|
| `GET` | `/customer/serviceability/states` | ❌ | List serviceable states (`ETag` / 304 supported). |
| `GET` | `/customer/serviceability/states/:code/districts` | ❌ | List serviceable districts in state (`ETag` / 304). |
| `GET` | `/customer/pickup-slots` | ❌ | List today's & tomorrow's time slots with zone capacity (`ETag` / 304). |
| `GET` | `/customer/config/booking-limits` | ❌ | Get booking limits (`maxDestinations`, `maxPackages`, `maxCodAmount`). |
### 📍 Places & Geocoding
| Method | Endpoint | Auth | Description |
|---|---|:---:|---|
| `GET` | `/customer/places/search?q=:query&lat=:lat&lng=:lng` | ✅ | Place search / autocomplete (empty query returns recent/saved). |
| `GET` | `/customer/places/reverse-geocode?lat=:lat&lng=:lng` | ✅ | Reverse geocode coordinates to structured address. |
### 💰 Fare Estimation
| Method | Endpoint | Auth | Description |
|---|---|:---:|---|
| `POST` | `/customer/fare/estimate` | ✅ | Compute estimated fare band (`minRupees`–`maxRupees`) & route distance. |
### 🚚 Bookings & Tracking
| Method | Endpoint | Auth | Description |
|---|---|:---:|---|
| `POST` | `/customer/bookings` | ✅ | Create multi-destination pickup booking (`Idempotency-Key` supported). |
| `GET` | `/customer/bookings?limit=20&cursor=:cursor&status=active` | ✅ | Keyset paginated customer bookings list. |
| `GET` | `/customer/bookings/:reference` | ✅ | Get full booking detail & live tracking snapshot (`ETag` supported). |
| `POST` | `/customer/bookings/:reference/cancel` | ✅ | Cancel booking (allowed strictly before rider status `arrived`). |
| `PATCH` | `/customer/bookings/:reference/destinations/:index` | ✅ | Update recipient details on a pending destination stop. |
| `GET` | `/customer/orders/:trackingId` | ✅ | Look up single order/parcel status by tracking number. |
### 👤 Profile & Saved Locations
| Method | Endpoint | Auth | Description |
|---|---|:---:|---|
| `GET` | `/customer/profile` | ✅ | Get customer profile details. |
| `PUT` | `/customer/profile` | ✅ | Update customer name/email. |
| `GET` | `/customer/locations` | ✅ | List up to 10 saved delivery/pickup addresses. |
| `POST` | `/customer/locations` | ✅ | Save a new location. |
| `PUT` | `/customer/locations/:id` | ✅ | Update an existing saved location. |
| `DELETE` | `/customer/locations/:id` | ✅ | Delete a saved location. |
### 🔔 Devices & Push Notifications
| Method | Endpoint | Auth | Description |
|---|---|:---:|---|
| `POST` | `/customer/devices` | ✅ | Register FCM device token for milestone push updates. |
| `DELETE` | `/customer/devices/:token` | ✅ | Unregister device token on logout. |
### 🛠️ Ops QA Testing (Non-Production Only)
| Method | Endpoint | Auth | Description |
|---|---|:---:|---|
| `POST` | `/customer/ops/bookings/:reference/stage` | ✅ | Double-gated test helper to walk a booking through stages. |
---
## 3. Core Request & Response Payloads
### 1) OTP Verification (`POST /customer/auth/otp/verify`)
> **The field is `code`, not `otp`.** This section said `otp` until 11 Sep 2026
> and the customer app was built against it, while the server only ever read
> `code`. Every sign-in therefore failed with a `400 "Enter the code we sent
> you"` — a correct code failed exactly like a wrong one. `openapi-customer.yaml`
> had it right all along; the two disagreed and this one was wrong.
>
> The server now also accepts `otp` as a **deprecated alias**, so builds already
> in customers' hands keep working. Send `code`. If both are present, `code`
> wins.
```json
// Request
{
"identifier": "+919876543210",
"code": "1234"
}
// Response (200 OK)
{
"success": true,
"data": {
"accessToken": "eyJhbGciOi...",
"refreshToken": "d8f1e2a3...",
"expiresIn": 3600,
"customer": {
"id": 1042,
"name": "Alex Kumar",
"phone": "+919876543210",
"email": "alex@example.com"
}
}
}
```
### 2) Fare Estimate (`POST /customer/fare/estimate`)
> **Corrected 11 Sep 2026** against `cxEstimateRequest`
> (`controllers/cxFareController.go`). The old shape used
> `pickup.latitude`/`longitude` (parsed as `lat`/`lng`), gave `pickup` a
> `stateCode`/`districtCode` it does not have, and described destinations as
> carrying a `packages` array of weights. The estimate is priced on
> `packageCount`; per-package weight is not known until the miler weighs it at
> the door.
```json
// Request
{
"pickup": {
"lat": 13.0827,
"lng": 80.2707
},
"destinations": [
{ "stateCode": "TN", "districtCode": "CHN", "packageCount": 2 },
{ "stateCode": "KA", "districtCode": "BLR", "packageCount": 1 }
]
}
// Response (200 OK)
{
"success": true,
"data": {
"minRupees": 240,
"maxRupees": 310,
"routeKm": 348.5,
"breakdown": {
"baseFare": 180,
"additionalStopsUplift": 60,
"estimatedTax": 0
}
}
}
```
### 3) Booking Creation (`POST /customer/bookings`)
> **Corrected 11 Sep 2026.** The shape below previously did not match the
> parser (`cxCreateBookingRequest`, `controllers/cxBookingController.go`), and
> every mismatch failed **silently** through `BodyParser` — no error, just a
> zero value:
>
> | Was documented | Actually parsed | Effect of following the old doc |
> |---|---|---|
> | `pickup.latitude` / `longitude` | `pickup.lat` / `lng` | pickup coordinates `0` |
> | `pickup.contactName` / `contactPhone` | *not read at all* | dropped |
> | destination fields flat | nested under `details` | every recipient/address field dropped |
> | destination `latitude` / `longitude` | `details.pin.lat` / `lng` | drop coordinates `0` |
> | `estimate` absent from the doc | **is** read | the quote shown to the customer was not recorded |
```json
// Request
{
"slotId": "slot_20260908_t2",
"pickup": {
"title": "Home",
"sub": "Flat 4B, Green Towers, Anna Nagar",
"lat": 13.0827,
"lng": 80.2707
},
"destinations": [
{
"stateCode": "TN",
"districtCode": "CHN",
"packageCount": 1,
"details": {
"street": "MG Road",
"building": "12/A",
"landmark": "Near Metro",
"recipientName": "Priya S",
"recipientPhone": "+919840123456",
"instructions": "Ring the bell",
"pin": { "lat": 13.0850, "lng": 80.2100 },
"codAmount": 450
}
}
],
"estimate": { "min": 240, "max": 310 },
"remarks": "Handle with care"
}
// Response (201 Created)
{
"success": true,
"data": {
"reference": "DM-482913",
"stage": "booked",
"status": "active",
"cancellable": true,
"createdAt": 1788775499000,
"slotId": "slot_20260908_t2",
"pickup": {
"title": "Home",
"sub": "Flat 4B, Green Towers, Anna Nagar",
"lat": 13.0827,
"lng": 80.2707
},
"destinations": [
{
"index": 0,
"stateCode": "TN",
"stateName": "Tamil Nadu",
"districtCode": "CHN",
"districtName": "Chennai",
"packageCount": 1,
"details": { "recipientName": "Priya S", "codAmount": 450 },
"trackingId": null,
"stage": null
}
]
}
}
```
---
## 4. Lifecycle & Stage Machine
### Stage Progression Sequence
```
[ booked ] ──► [ assigned ] ──► [ arrived ] ──► [ picked_up ] ──► [ order_created ]
│ (cancel window closes)
▼
[ delivered ] ◄── [ out_for_delivery ] ◄── [ in_transit ]
```
### Stage Vocabulary
| Stage Key | Meaning / Trigger | Cancellable? | Scope |
|---|---|:---:|---|
| `booked` | Order submitted by customer | ✅ Yes | Booking |
| `assigned` | Rider assigned to visit | ✅ Yes | Booking |
| `on_the_way` | Rider accepted assignment | ✅ Yes | Booking |
| `arrived` | Rider arrived at pickup point (**cancellation cutoff**) | ❌ No | Booking |
| `picked_up` | Parcels collected and weighed | ❌ No | Booking |
| `order_created` | Tracking IDs generated per destination | ❌ No | Per Destination |
| `in_transit` | Parcels sorted / inwarded at hub | ❌ No | Per Destination |
| `out_for_delivery` | Dispatched with delivery agent | ❌ No | Per Destination |
| `delivered` | Successfully delivered to recipient | ❌ No | Per Destination |
| `cancelled` | Cancelled by customer or ops before arrival | — | Terminal |
> [!IMPORTANT]
> **Rollup Rule:** The overall booking stage reflects the **slowest order**. If Destination 1 is `delivered` but Destination 2 is `in_transit`, the booking rollup remains `in_transit`.
---
## 5. Cross-App Impact & Compatibility
| Client / Component | Observable Change | Impact / Handling |
|---|---|---|
| **Miler App (Flutter)** | Multi-stop collection | Post-collection returns multiple stops sharing one `bookingid`. Keys must resolve by `consignmentid`. |
| **Admin Console** | Booking numbers & Search | Display format is now `DM-482913`. Searches match exact substring. |
| **Hub Console** | Tracking IDs & Inbound | Tracking numbers are now `DMX10482913`. |
| **Safety Mitigation** | `maxDestinations` Gate | Configured in DB (`customerbookinglimits.maxdestinations = 1`) to keep fanout single-destination until mobile updates deploy. |
---
## 6. Standard Error Codes & Envelopes
All errors return JSON in standard format:
```json
{
"success": false,
"error": {
"code": "SLOT_UNAVAILABLE",
"message": "That pickup time has passed — pick a new slot"
}
}
```
| HTTP Status | Error Code (`error.code`) | Meaning / Recommended Client Action |
|---|---|---|
| `400` | `INVALID_INPUT` / `SLOT_EXPIRED` | Validation error or expired slot date. Prompt user to re-select. |
| `401` | `UNAUTHORIZED` | Token missing or expired. Redirect to OTP login / refresh session. |
| `403` | `FORBIDDEN` | Caller lacks role 9 customer access. |
| `404` | `NOT_FOUND` | Booking reference or tracking ID does not exist. |
| `409` | `SLOT_CAPACITY_FULL` | Slot filled up during checkout race. Prompt user to choose another time. |
| `409` | `BOOKING_NOT_CANCELLABLE` | Customer attempted cancel after rider `arrived`. Show un-cancellable alert. |
| `422` | `UNSERVICEABLE_PINCODE` | Location is outside active operating zones. |
| `429` | `RATE_LIMITED` | OTP requests exceeded limit (max 5/hour). Show countdown timer. |
| `500` | `INTERNAL_ERROR` | Generic server error (`X-Request-Id` logged). |
---
## 7. Environment Variables & Deploy Flags
```bash
GEOCODER_URL=https://nominatim.openstreetmap.org # Geocoding proxy
GEOCODER_EMAIL=ops@doormile.com # Nominatim contact policy
MILER_CALL_PROXY= # Set to proxy number in production (protects rider PII)
CX_STAGING_OTP=1234 # Fixed OTP for staging QA (disabled if ENV=production)
CX_ID_SCRAMBLE_KEY= # Optional custom key for Feistel sequence permutation
CX_ALLOW_STAGE_OVERRIDE=false # Double-gated QA stage override tool
```