A till signs in with a mobile number and a PIN, but createposuser still
accepted an account without a number. Such an account cannot reach the
sign-in screen at all, and the failure surfaces at a counter in front of
a queue rather than at the point of creation. Enforced in CreatePosUser,
so both doors are covered: POST /pos/users from the terminal and
createposuser from the console share that path.
Two checks, not one. normalisePosPhone answers ("", nil) rather than an
error for a value holding no digits, so "abc" would have passed an
emptiness check, then been written as a blank and skipped the uniqueness
check below it — which is the hole this closes.
Scope is new accounts only. The column stays nullable and UpdatePosUser
still reads an empty contactno as "leave alone", so the accounts that
predate the number keep working through the backfill and cannot have
theirs cleared. The PIN stays optional at creation.
Also in this change:
- docs: correct both phone-login handovers, which claimed creation
already required a number. The sequencing note in the PIN handover
said step 2 was a backfill that could never be finished; it now is
one, and POS_LOGIN.md says which half of the pair creation enforces.
- docs: remove credentials from the examples. POS_PHONE_LOGIN_HANDOVER
carried a real-looking back-office pair and a generated till password,
and POS_LOGIN.md a second one.
- posController.Staff: the comment justified scoping by token because
"the answer carries PINs". It has not since the PIN left the wire. The
scoping is still right for a different reason, which the comment now
gives.
- scratch/posstaffsetup: takes both mobile numbers as arguments and
refuses to run without them. Generating stand-ins would have produced
exactly what this change prevents. Validated before the database is
opened, in plan mode too, so a dry run cannot print a plan that apply
would reject halfway through and leave half a shop set up.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
222 lines
7.9 KiB
Markdown
222 lines
7.9 KiB
Markdown
# POS sign-in by mobile number, and shift assignment — handover to the terminal team
|
|
|
|
> ## ⚠️ Superseded by [POS_PHONE_PIN_LOGIN_HANDOVER.md](POS_PHONE_PIN_LOGIN_HANDOVER.md)
|
|
>
|
|
> `POST /pos/login` now takes a mobile number and a **PIN**, not a mobile number
|
|
> and a password, and the session no longer carries `staff[].pin`. Read the new
|
|
> handover instead — build against this one and the sign-in screen will be
|
|
> wrong.
|
|
>
|
|
> Still accurate here, and not repeated there: the `createposuser` /
|
|
> `updateposuser` / `getposusers` field changes (§2), the number format rules
|
|
> (§3), and shifts (§4). **Not** accurate here: §2's claim that `shift_*`
|
|
> appears on `staff[]` in the `/pos/login` session — it never did.
|
|
|
|
Backend is done and builds clean. **Nothing about this breaks the current app** —
|
|
username sign-in keeps working exactly as it does today. Read §5 before you ship
|
|
anything, because the switch has one ordering rule that will lock out every
|
|
cashier if it is done in the wrong order.
|
|
|
|
---
|
|
|
|
## 1. What changed, in one line
|
|
|
|
`POST /pos/login` now accepts a **mobile number** as well as a username, only
|
|
till accounts are candidates, and a till account can carry a **shift**.
|
|
|
|
---
|
|
|
|
## 2. Endpoints — what was edited and how
|
|
|
|
### `POST /live/api/v1/pos/login` — CHANGED (backwards compatible)
|
|
|
|
The request body already had both fields. **Nothing in the contract changed.**
|
|
What changed is behaviour behind it.
|
|
|
|
```jsonc
|
|
// Sign in by mobile — the new way
|
|
{ "contactno": "9876543210", "password": "…" }
|
|
|
|
// Sign in by username — still works, unchanged
|
|
{ "authname": "cashier.1135@pos.nearle.in", "password": "…" }
|
|
```
|
|
|
|
`authname` wins if both are sent. The response is unchanged.
|
|
|
|
**Behavioural change:** the account lookup is now restricted to till roles
|
|
(Supervisor `7`, Cashier `8`).
|
|
|
|
*Why it matters to you:* previously a number shared with a back-office account
|
|
returned two candidates and the login was refused outright. On live data **34
|
|
numbers are shared by 104 accounts** — one by eleven — so without this,
|
|
sign-in by phone would simply have failed for a large number of people. Now a
|
|
number only has to be unique among till accounts.
|
|
|
|
A back-office user who types their own password at a till still gets the
|
|
specific `403 "this account is not set up for the till"` rather than a vague
|
|
rejection. That did not change.
|
|
|
|
### `POST /live/api/v1/web/tenants/createposuser` — CHANGED (two new fields)
|
|
|
|
```jsonc
|
|
{
|
|
"tenantid": 1087,
|
|
"locationid": 1135,
|
|
"full_name": "Priya Raman",
|
|
"role": "cashier",
|
|
"pin": "4731",
|
|
"contactno": "9876543210", // NEW — the sign-in number, and required
|
|
"shift_id": 3 // NEW — optional, 0/omitted = unassigned
|
|
}
|
|
```
|
|
|
|
The response gains `shift_id`, and `contactno` now comes back **normalised to
|
|
ten digits**.
|
|
|
|
### `PUT /live/api/v1/web/tenants/updateposuser` — CHANGED (two new fields)
|
|
|
|
Accepts `contactno` and `shift_id`. Every field stays optional; only what is
|
|
sent is written.
|
|
|
|
### `GET /live/api/v1/web/tenants/getposusers` — CHANGED (four new fields)
|
|
|
|
Each user in `details.users[]` now also carries:
|
|
|
|
```jsonc
|
|
{
|
|
"contactno": "9876543210",
|
|
"shift_id": 3,
|
|
"shift_name": "Morning",
|
|
"shift_start": "07:00",
|
|
"shift_end": "15:00"
|
|
}
|
|
```
|
|
|
|
Blank on accounts created before shifts existed. **`shift_*` also appears on
|
|
`staff[]` in the `/pos/login` session**, so the terminal gets it for free.
|
|
|
|
### `GET/POST/PUT /live/api/v1/web/tenants/{getstaffshifts,createstaffshift,updatestaffshift}` — NEW
|
|
|
|
Console-only. The terminal does not need to call these; shifts arrive with the
|
|
session. Documented for completeness:
|
|
|
|
```jsonc
|
|
// POST createstaffshift
|
|
{ "tenantid": 1087, "locationid": 1135,
|
|
"name": "Morning", "start_time": "07:00", "end_time": "15:00",
|
|
"weekdays": "1111100" } // 7 chars from Monday; empty = every day
|
|
```
|
|
|
|
Also mirrored under `/v1/mob/tenants/*`.
|
|
|
|
---
|
|
|
|
## 3. Number format — the one thing to get exactly right
|
|
|
|
The server reduces every number to **ten digits** before storing or matching:
|
|
non-digits are stripped, then a leading `91` or `0` is dropped once. Anything
|
|
that is not ten digits afterwards is **rejected**, not stored.
|
|
|
|
So all of these are the same account:
|
|
|
|
```
|
|
"+91 98765 43210" → 9876543210
|
|
"098765-43210" → 9876543210
|
|
"9876543210" → 9876543210
|
|
```
|
|
|
|
**What you should send:** the ten digits, or anything in the list above — the
|
|
server normalises either way. **Do not** send a country code the user did not
|
|
type, and do not reject `+91` locally; let it through and let the server reduce
|
|
it. What matters is that you never send something that normalises to a
|
|
*different* number than what the console stored.
|
|
|
|
---
|
|
|
|
## 4. Shift is informational
|
|
|
|
`shift_id` / `shift_name` / `shift_start` / `shift_end` are for **display**.
|
|
Nothing on the server refuses a bill rung outside a shift window, and you should
|
|
not add that check on the device either. A cashier locked out mid-queue by a
|
|
clock is a worse failure than a bill filed against the wrong window. Show whose
|
|
shift it is; do not gate on it.
|
|
|
|
Times are 24-hour `HH:MM`. A shift may legitimately wrap midnight (`22:00`
|
|
→ `06:00`) — do not assume `end > start`.
|
|
|
|
---
|
|
|
|
## 5. 🔴 Sequencing — read this before shipping
|
|
|
|
**Every existing POS account has no mobile number.** All 12 live accounts have
|
|
`contactno = ""`:
|
|
|
|
```
|
|
supervisor.1135@pos.nearle.in phone=""
|
|
cashier.1135@pos.nearle.in phone=""
|
|
… 12 of 12
|
|
```
|
|
|
|
If the app ships sign-in-by-phone **only**, every cashier in every shop is
|
|
locked out on the next app update.
|
|
|
|
**Required order:**
|
|
|
|
1. Backend deploys. *(Nothing changes for the app — username login is untouched.)*
|
|
2. Back office adds a mobile number to every existing till account through the
|
|
console. New accounts already require one — `createposuser` refuses a
|
|
request without it.
|
|
3. **Only then** the app makes mobile the primary sign-in field.
|
|
|
|
**Recommendation for the app:** keep both. One field labelled *"Mobile number or
|
|
username"* — if the value is all digits send it as `contactno`, otherwise as
|
|
`authname`. That is a handful of lines, works before and after the backfill, and
|
|
means a shop with one un-backfilled account is not stranded.
|
|
|
|
---
|
|
|
|
## 6. Errors you should handle
|
|
|
|
| Message | Meaning | What the app should do |
|
|
|---|---|---|
|
|
| generic 401 rejection | wrong number/username or wrong password | "Check your details" — do **not** say which was wrong |
|
|
| `this account is not set up for the till…` | a back-office login was used | Show it verbatim; it names the fix |
|
|
| `more than one account uses these sign-in details…` | ambiguous match | Show verbatim; it needs the back office |
|
|
| `this account has no password set…` | provisioned without a password | Show verbatim |
|
|
| `another till account in this business already signs in with 9876543210` | console-side only | Not seen by the app |
|
|
|
|
---
|
|
|
|
## 7. What did **not** change
|
|
|
|
- The response shape of `/pos/login`, including `token`, `expires_at`,
|
|
`can_manage_staff`, `locations[]` and `staff[]`
|
|
- `POST /pos/login/pin`
|
|
- Token format, TTL (30 days) and the `PosAuth` guard
|
|
- `POST /pos/orders`, `/pos/customers`, `/pos/health`, `GET /pos/catalogue`
|
|
- `POS_AUTH_REQUIRED` still defaults to off
|
|
|
|
---
|
|
|
|
## 8. Verify after deploy
|
|
|
|
```bash
|
|
B=https://fiesta.nearle.app/live/api/v1
|
|
|
|
# username sign-in still works (regression check — run this first)
|
|
curl -s -X POST "$B/pos/login" -H 'Content-Type: application/json' \
|
|
-d '{"authname":"supervisor.1135@pos.nearle.in","password":"…"}' \
|
|
| grep -o '"can_manage_staff":[a-z]*'
|
|
# expect: "can_manage_staff":true
|
|
|
|
# after a number is set on that account, the same account by phone
|
|
curl -s -X POST "$B/pos/login" -H 'Content-Type: application/json' \
|
|
-d '{"contactno":"9876543210","password":"…"}' \
|
|
| grep -o '"can_manage_staff":[a-z]*'
|
|
|
|
# a back-office account is still refused with the specific message
|
|
curl -s -X POST "$B/pos/login" -H 'Content-Type: application/json' \
|
|
-d '{"authname":"<a back-office account>","password":"…"}'
|
|
# expect: 403 "this account is not set up for the till"
|
|
```
|