Files
doormile_backend/docs/customer-app-handover-2026-09-15.md

298 lines
9.7 KiB
Markdown

# Doormile backend → customer app · what changed
**For:** the `doormile_cx` app team
**From:** Doormile backend
**Date:** 15 Sep 2026
**Verified against:** production Postgres + Redis, and the code in this repo
---
## Read this first
Two things decide what you can do today:
1. **Send the verification code as `code`, not `otp`.** That works against
production right now. The server-side `otp` alias described below is written
but **not deployed yet** — do not rely on it until we confirm it has shipped.
2. **Booking creation is currently blocked on our side**, for a reason that has
nothing to do with your app. See [Still blocked](#still-blocked-on-our-side).
Sign-in will work before booking does.
Everything else here is context for why your existing integration was failing.
---
## 1. Sign-in was broken by a field name, and it was our documentation's fault
Your app posted the code as `otp`. The server only ever read `code`. So the
parsed value was always empty, the "no code supplied" branch always fired, and
**every** sign-in returned:
```
400 {"error":{"code":"invalid"},"message":"Enter the code we sent you"}
```
A correct code failed exactly the same way as a wrong one. No amount of SMS
gateway credit would have changed it.
**This was our fault, not yours.** Two of our documents disagreed:
| Document | Said | Correct? |
|---|---|---|
| `customer-app-api-crisp.md` | `otp` | ❌ wrong — you built against this |
| `openapi-customer.yaml` | `code` | ✅ right |
`customer-app-api-crisp.md` has been corrected.
### What to send
```jsonc
POST /api/v1/customer/auth/otp/verify
{
"identifier": "+919876543210",
"code": "1234" // ← `code`, always
}
```
**Response 200:**
```jsonc
{
"success": true,
"data": {
"accessToken": "eyJhbGciOi...",
"refreshToken": "d8f1e2a3...", // 64 hex chars
"expiresIn": 3600, // seconds
"customer": { "id": "cust_294", "name": "...", "phone": "+91...", "email": "" }
}
}
```
### About the `otp` alias
We are adding server-side acceptance of `otp` as a **deprecated alias**, so
builds already on customers' phones start working without an app release. When
both keys are present, `code` wins.
**It is not deployed yet.** Treat it as a safety net for old installs, not as a
reason to keep sending `otp`. Please migrate to `code`.
---
## 2. Three request shapes in our docs did not match the server
Every one of these failed **silently** — our parser ignores unknown keys, so a
wrong field name produced a zero value, not an error. No 400, no log, just a
booking with coordinates of `0` or a missing recipient.
If you built any of these from `customer-app-api-crisp.md` before 11 Sep, they
need changing.
### 2.1 `POST /customer/auth/otp/verify`
| Was documented | Server actually reads |
|---|---|
| `otp` | `code` |
### 2.2 `POST /customer/fare/estimate`
| Was documented | Server actually reads |
|---|---|
| `pickup.latitude` / `pickup.longitude` | `pickup.lat` / `pickup.lng` |
| `pickup.stateCode` / `districtCode` | *not read — pickup has only lat/lng* |
| `destinations[].packages[].weightKg` | `destinations[].packageCount` |
Per-package weight is not an input. The estimate is priced on package **count**;
real weight is not known until the miler weighs it at the door.
**Correct request:**
```jsonc
{
"pickup": { "lat": 13.0827, "lng": 80.2707 },
"destinations": [
{ "stateCode": "TN", "districtCode": "CHN", "packageCount": 2 },
{ "stateCode": "KA", "districtCode": "BLR", "packageCount": 1 }
]
}
```
### 2.3 `POST /customer/bookings` — the one with the most wrong fields
| Was documented | Server actually reads | If you send the old shape |
|---|---|---|
| `pickup.latitude` / `longitude` | `pickup.lat` / `lng` | pickup coordinates become **0** |
| `pickup.contactName` / `contactPhone` | *not read at all* | silently dropped |
| destination fields **flat** | nested under `details` | **every** recipient/address field dropped |
| destination `latitude` / `longitude` | `details.pin.lat` / `lng` | drop coordinates become **0** |
| `estimate` not documented | **is** read and stored | the quote shown to the customer is not recorded |
**Correct request:**
```jsonc
{
"slotId": "slot_20260916_t2",
"pickup": {
"title": "Home",
"sub": "Flat 4B, Green Towers, Anna Nagar",
"lat": 13.0827,
"lng": 80.2707
},
"destinations": [
{
"stateCode": "TN",
"districtCode": "CHN",
"packageCount": 2,
"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"
}
```
**The booking *response* was also documented wrong** — it returns
`pickup.lat` / `lng`, not `latitude` / `longitude`. If you parse the response
for coordinates, check that too.
---
## 3. `remarks` now actually saves
The top-level `remarks` field you were already sending was being dropped — the
server's request struct had no field for it, so `BodyParser` discarded it and
the booking's note was empty for every customer-app booking. The admin console
displays and searches that column, so operators saw nothing.
Fixed and **merged to main**. Keep sending it exactly as you are.
---
## 4. Auth flow, confirmed working end to end
We created a test customer through the live API and verified the whole
sequence. There is no separate "request OTP" step after signup — signup sends
the code itself.
```
POST /api/v1/customer/auth/signup { name, phone, email? } → 200 {sent, resendAfterSeconds}
POST /api/v1/customer/auth/otp/verify { identifier, code } → 200 {accessToken, refreshToken, ...}
```
For an existing account:
```
POST /api/v1/customer/auth/otp/request { identifier } → 200
POST /api/v1/customer/auth/otp/verify { identifier, code } → 200
```
Two behaviours worth coding for, both confirmed by testing:
- **The code is single-use.** After a successful verify it is deleted. Logging
in again requires a fresh `otp/request` first — re-sending the same code
returns `401 invalid_otp`.
- **There is a 30-second resend cooldown.** Calling `otp/request` inside that
window does not issue a new code. Respect `resendAfterSeconds` from the
response rather than retrying blindly.
### Refresh
```
POST /api/v1/customer/auth/refresh { refreshToken }
```
Refresh tokens **rotate** — the presented one is revoked and replaced. Replaying
an already-used refresh token **revokes every session for that customer**, so
never keep an old one around as a fallback. Store only the newest.
Access tokens last 1 hour (`expiresIn: 3600`).
---
## 5. Do not offer the Email tab yet
```
POST /api/v1/customer/auth/otp/request {"identifier":"someone@example.com"}
→ 500 {"error":{"code":"server_error"},"message":"Something went wrong"}
```
SMTP is not configured on production (`SMTP_HOST`, `SMTP_USER`,
`SMTP_PASSWORD` are all unset). Email sign-in fails every time.
**Please hide or disable the Email option** until we confirm SMTP is live.
Offering a path that always fails is worse than not offering it.
---
## Still blocked on our side
**You will not be able to create a booking yet, no matter what you send.**
Both serviceability tables are empty on production:
```
serviceablestates 0 rows
serviceabledistricts 0 rows
```
`CreateCxBooking` validates every destination against that catalogue, so with
zero rows every booking is rejected with:
```
400 {"message":"Every destination needs a serviceable state and district"}
```
And `GET /customer/serviceability/states` returns `200` with an **empty list**,
so your state picker has nothing to show in the first place.
This is ours to fix — the seed data exists (`seed_customer_app.sql`, 5 states
including Tamil Nadu / Kerala / Karnataka / Telangana / Puducherry, 22
districts) and simply has not been applied to production. We will confirm when
it has.
**Until then:** sign-in and the catalogue endpoints are what you can integrate
against. Booking creation will return a 400 that is not your bug.
---
## Summary — what you need to change
| # | Change | Priority |
|---|---|---|
| 1 | Send the verification code as **`code`**, not `otp` | **Required** — nothing works without it |
| 2 | Fare estimate: `pickup.lat`/`lng`, `packageCount` (no `packages[].weightKg`) | Required |
| 3 | Booking: `pickup.lat`/`lng`, destination details nested under `details`, coords at `details.pin` | Required |
| 4 | Parse the booking response's `pickup.lat`/`lng` (not `latitude`/`longitude`) | Required |
| 5 | Send `estimate: {min, max}` on booking create | Recommended — it is the dispute record |
| 6 | Hide the Email sign-in tab | Recommended |
| 7 | Handle single-use codes + the 30s resend cooldown | Recommended |
| 8 | Store only the newest refresh token, never replay an old one | Recommended |
---
## Status of the backend changes referenced here
| Change | State |
|---|---|
| `remarks` saved on booking create | **Merged to main** |
| Doc corrections (`customer-app-api-crisp.md`) | **In review** |
| `otp` accepted as alias for `code` | **In review — not deployed** |
| Failed OTP send no longer burns the cooldown / rate limit | **In review** |
| Serviceability seed applied to production | **Not done** |
| SMTP configured for email OTP | **Not done** |
"In review" means written and tested but not yet on `api.doormile.com`. Build
against `code` and the corrected shapes — those are correct regardless of
deployment order. We will confirm when the alias and the seed are live.
Questions → the backend team.