updates on the otp updates on the customer app
This commit is contained in:
@@ -4,6 +4,73 @@ This document provides a comprehensive log of the major features, architectural
|
||||
|
||||
---
|
||||
|
||||
## 0. Customer sign-in fix, config hardening & admin ordering (2026-09-11)
|
||||
|
||||
Six defects were reported against customer bookings. Five were real, one was
|
||||
not. Full detail in `CLAUDE.md` §8.6.
|
||||
|
||||
**Fixed**
|
||||
|
||||
- **`POST /customer/auth/otp/verify` now accepts `otp` as well as `code`.**
|
||||
The handler only ever read `code`; the app sent `otp`, because this repo's
|
||||
own quick-reference documented `otp` while `openapi-customer.yaml` said
|
||||
`code`. Every sign-in failed with a `400` — a correct code failed exactly
|
||||
like a wrong one. `code` remains the contract and wins when both are sent;
|
||||
`otp` is a deprecated alias kept so builds already installed keep working.
|
||||
- **A failed OTP send no longer leaves a live code behind.** `issueCxOtp` now
|
||||
rolls back the stored code, the resend cooldown and the rate-limit slot when
|
||||
SMS or email delivery fails, instead of charging the customer for the
|
||||
gateway's failure.
|
||||
- **`GET /admin/bookings` is ordered `bookingid DESC`.** It had no `ORDER BY`,
|
||||
so row order was unspecified — in practice oldest first, putting the newest
|
||||
booking on the last page and outside any client that reads a bounded number
|
||||
of pages. `OFFSET` paging over an unordered result was also unstable.
|
||||
- **Production hosts and credentials removed from `config.Load()` defaults.**
|
||||
`JWT_SECRET_KEY`, `NATS_URL`/`NATS_USER`/`NATS_PASSWORD`,
|
||||
`AI_LAYER_BASE_URL`, `ROUTE_OPTIMIZER_URL` and `DB_PASSWORD` all defaulted to
|
||||
real values, so a clone of this repo could mint a valid token for any account
|
||||
and any local run joined the live NATS stream. A second hardcoded production
|
||||
URL in `internal/assignment/ai_layer.go` was removed too.
|
||||
|
||||
**Behaviour change to be aware of when deploying**
|
||||
|
||||
`cfg.Validate()` now runs at startup and **the service refuses to boot when
|
||||
`JWT_SECRET_KEY` is unset and `ENV=production`**. Outside production an
|
||||
ephemeral per-process key is generated with a warning, so local development
|
||||
needs no configuration — but tokens no longer survive a restart unless you set
|
||||
the variable. Make sure `JWT_SECRET_KEY` is present in the production
|
||||
environment before the next deploy.
|
||||
|
||||
Unset `NATS_URL` now means "no NATS" rather than "production NATS": publishes
|
||||
are dropped and no consumer starts. Set it explicitly wherever NATS is wanted.
|
||||
|
||||
**Investigated and rejected**
|
||||
|
||||
A reported +5:30 timestamp drift (`DBNow()` vs `timestamp with time zone`
|
||||
columns) does **not** exist on production — the columns there are `timestamp
|
||||
without time zone`, which is what `DBNow()` assumes, confirmed by a round-trip
|
||||
with zero drift. Changing `DBNow()` would introduce the bug. The real risk is
|
||||
that GORM's `AutoMigrate` produces `timestamptz`, so a freshly built schema
|
||||
does not match production and every new dev environment shows a drift that
|
||||
production does not.
|
||||
|
||||
**Docs corrected**
|
||||
|
||||
`customer-app-api-crisp.md` carried three request shapes that did not match
|
||||
their parsers (`auth/otp/verify`, `fare/estimate`, `bookings`) plus a wrong
|
||||
booking response shape; `express-console-api.md` documented pagination as
|
||||
"default 500, cap 1000" when the code enforces default 20, cap 100. Every one
|
||||
of these failed silently through `BodyParser` or a page budget, never as an
|
||||
error.
|
||||
|
||||
**Still open**
|
||||
|
||||
Email OTP returns 500 on production (`SMTP_*` unset); `.env` and a live GCP
|
||||
service-account key remain committed and need rotating; `GET /api/v1/ready`
|
||||
returns 503 with a body that says `"status":"ready"`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Customer App v1 API Rebuild (`doormile_cx`)
|
||||
|
||||
The customer-facing surface was completely rebuilt from the legacy single-destination / PIN-based flow to the production Customer App v1 contract.
|
||||
|
||||
@@ -84,11 +84,22 @@
|
||||
## 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",
|
||||
"otp": "1234"
|
||||
"code": "1234"
|
||||
}
|
||||
|
||||
// Response (200 OK)
|
||||
@@ -109,26 +120,25 @@
|
||||
```
|
||||
|
||||
### 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": {
|
||||
"latitude": 13.0827,
|
||||
"longitude": 80.2707,
|
||||
"stateCode": "TN",
|
||||
"districtCode": "CHN"
|
||||
"lat": 13.0827,
|
||||
"lng": 80.2707
|
||||
},
|
||||
"destinations": [
|
||||
{
|
||||
"stateCode": "TN",
|
||||
"districtCode": "CHN",
|
||||
"packages": [{ "weightKg": 2.5 }]
|
||||
},
|
||||
{
|
||||
"stateCode": "KA",
|
||||
"districtCode": "BLR",
|
||||
"packages": [{ "weightKg": 1.0 }]
|
||||
}
|
||||
{ "stateCode": "TN", "districtCode": "CHN", "packageCount": 2 },
|
||||
{ "stateCode": "KA", "districtCode": "BLR", "packageCount": 1 }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -149,6 +159,20 @@
|
||||
```
|
||||
|
||||
### 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
|
||||
{
|
||||
@@ -156,26 +180,27 @@
|
||||
"pickup": {
|
||||
"title": "Home",
|
||||
"sub": "Flat 4B, Green Towers, Anna Nagar",
|
||||
"latitude": 13.0827,
|
||||
"longitude": 80.2707,
|
||||
"contactName": "Alex Kumar",
|
||||
"contactPhone": "+919876543210"
|
||||
"lat": 13.0827,
|
||||
"lng": 80.2707
|
||||
},
|
||||
"destinations": [
|
||||
{
|
||||
"recipientName": "Priya S",
|
||||
"recipientPhone": "+919840123456",
|
||||
"building": "12/A",
|
||||
"street": "MG Road",
|
||||
"landmark": "Near Metro",
|
||||
"districtCode": "CHN",
|
||||
"stateCode": "TN",
|
||||
"latitude": 13.0850,
|
||||
"longitude": 80.2100,
|
||||
"districtCode": "CHN",
|
||||
"packageCount": 1,
|
||||
"codAmount": 450
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -192,16 +217,18 @@
|
||||
"pickup": {
|
||||
"title": "Home",
|
||||
"sub": "Flat 4B, Green Towers, Anna Nagar",
|
||||
"latitude": 13.0827,
|
||||
"longitude": 80.2707
|
||||
"lat": 13.0827,
|
||||
"lng": 80.2707
|
||||
},
|
||||
"destinations": [
|
||||
{
|
||||
"index": 0,
|
||||
"stateCode": "TN",
|
||||
"stateName": "Tamil Nadu",
|
||||
"districtCode": "CHN",
|
||||
"districtName": "Chennai",
|
||||
"packageCount": 1,
|
||||
"codAmount": 450,
|
||||
"details": { "recipientName": "Priya S", "codAmount": 450 },
|
||||
"trackingId": null,
|
||||
"stage": null
|
||||
}
|
||||
|
||||
297
docs/customer-app-handover-2026-09-15.md
Normal file
297
docs/customer-app-handover-2026-09-15.md
Normal file
@@ -0,0 +1,297 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user