app_users is the only thing the two products have in common, and the code was treating it as though it were the whole relationship. Both directions leaked. Back-office roles were leaking into the till. PosRoleCanManageStaff returned true for roleid 1 to 6, on the reasoning that somebody who already administers a shop from a browser is not made less privileged by standing at the counter. That sounds fine and is wrong: measured against live data it handed till-supervisor powers to 68 accounts, 59 of them Nearle Daily Super admins, not one of whom is the administrator of anybody's POS. Meanwhile the actual shop accounts carry roleid 0 and were refused, so the mapping was backwards from intent in both halves at once. Till accounts were leaking into the application. GetStaffs is WHERE tenantid with no role filter, so a Counter Cashier appeared in the tenant staff list beside the delivery riders — a row every action on that page would fail against, since a cashier has no app login, no rider shift and no back-office screen. So: eligibility for a till is now granted explicitly by provisioning a Supervisor or a Cashier, never inherited from a back-office role, and roles 7 and 8 are excluded from every Nearle Daily lookup. The exclusion lives in the queries rather than in a check after them, because a check bolted on afterwards has to be repeated at six call sites and is one edit away from being forgotten at one of them — and that one would be the hole. A till account is not rejected by the app login; it is not found. Two things this surfaced that were not visible before. A Supervisor could not open a till. PIN sign-in needs a session that already exists, so once back-office roles were refused, an outlet whose only POS accounts were PIN-only had no way in at all. Supervisors are now provisioned with a username and password as well as a PIN; cashiers deliberately get neither, because they sign on at a counter somebody has already opened and a second password would be one more credential to leak for no capability gained. UpdatePosUser silently dropped authname. It wrote the password, reported success, and left the account unreachable by either lookup — the failure surfaced at a counter as "not recognised" rather than on the screen that caused it. Contactno had the same gap. Verified against live rows rather than asserted, by scratch/posseparation: a provisioned supervisor signs in and gets the supervisor shell; five real back-office accounts including Super admins are refused; the supervisor is invisible to applogin, tenant weblogin and the password-setup lookup; and no till account appears in getallusers, while asking for role 7 by name still returns them so the console can read its own people. All five outlets that stock products now have a Supervisor and a Cashier. Also moves the loose markdown into docs/, which was already staged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
125 lines
4.9 KiB
Markdown
125 lines
4.9 KiB
Markdown
# Order Creation — Mobile Developer Verification Guide
|
|
|
|
## Why this exists
|
|
|
|
We found that orders placed through the app were being saved with **zero line
|
|
items** — the order header (tenant, location, customer) saved fine, but the
|
|
`items` array was silently getting dropped somewhere between the app and the
|
|
database. Because the stock check only runs over whatever's in `items`, an
|
|
order with no items also skipped stock validation entirely.
|
|
|
|
The backend now tolerates a few different request shapes and has a stock
|
|
check in place, but the app's actual request needs to be verified against
|
|
what's below to confirm it lines up.
|
|
|
|
## Endpoint
|
|
|
|
```
|
|
POST https://fiesta.nearle.app/live/api/v1/mob/orders/createorder
|
|
Content-Type: application/json
|
|
```
|
|
|
|
## Request shape — items MUST be inside `orders`, not a sibling of it
|
|
|
|
**Correct:**
|
|
```json
|
|
{
|
|
"orders": {
|
|
"tenantid": 1135,
|
|
"locationid": 1166,
|
|
"customerid": 42,
|
|
"items": [
|
|
{ "productid": 7060, "orderqty": 2, "price": 45.0 }
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**Also accepted (flat, no wrapper):**
|
|
```json
|
|
{
|
|
"tenantid": 1135,
|
|
"locationid": 1166,
|
|
"customerid": 42,
|
|
"items": [
|
|
{ "productid": 7060, "orderqty": 2, "price": 45.0 }
|
|
]
|
|
}
|
|
```
|
|
|
|
**This shape used to silently lose the items — avoid it:**
|
|
```json
|
|
{
|
|
"orders": { "tenantid": 1135, "locationid": 1166 },
|
|
"items": [ { "productid": 7060, "orderqty": 2 } ]
|
|
}
|
|
```
|
|
`items` as a sibling of `orders` (not nested inside it) is now handled as a
|
|
fallback server-side too, but don't rely on the fallback — put `items` inside
|
|
`orders` to match the primary/documented shape.
|
|
|
|
## Required fields per item
|
|
|
|
| field | type | required | notes |
|
|
|-------------|--------|----------|-------------------------------------------|
|
|
| `productid` | int | yes | must be a real product for the tenant |
|
|
| `orderqty` | number | yes | quantity being ordered |
|
|
| `price` | number | recommended | unit price at time of order |
|
|
| `locationid`| int | no | defaults to the order's own `locationid` if omitted |
|
|
|
|
## Expected responses — verify your app handles all of these
|
|
|
|
| Scenario | HTTP code | Body (key fields) |
|
|
|---|---|---|
|
|
| Order succeeds | `200` | `"status": true`, `"details": { "orderheaderid": ..., "items": [...] }` |
|
|
| No `tenantid` at all | `409` | `"status": false`, `"message": "Tenant ID is required"` |
|
|
| `items` missing/empty | `400` | `"status": false`, `"message": "Order must contain at least one item"` |
|
|
| Requested qty > available stock | `409` | `"status": false`, `"message": "insufficient stock for product '<name>': requested X, available Y"` |
|
|
|
|
**Important:** a `409` with "insufficient stock" is not a network/server
|
|
error — it's the correct, expected response when a customer tries to order
|
|
more than what's in stock at that store. The app should catch this
|
|
specifically (check the message text, or treat any `409` from this endpoint
|
|
as a stock problem) and show the customer a clear "not enough stock" message
|
|
rather than a generic error screen.
|
|
|
|
## How to verify end-to-end yourselves
|
|
|
|
1. Pick a real `tenantid` + `locationid` + `productid` combo you know has
|
|
stock (ask backend/ops for current numbers, or check via the merchant
|
|
web app's inventory view).
|
|
2. Place a normal order for 1 unit through the app. Confirm it returns `200`
|
|
and the response's `details.items` array is non-empty.
|
|
3. Place an order for a quantity larger than what's currently in stock for
|
|
that product/location. Confirm you get a `409` with an "insufficient
|
|
stock" message, and that the app surfaces this to the user instead of
|
|
silently failing or showing a generic error.
|
|
4. Cancel a successful order and confirm a follow-up stock check reflects
|
|
the restored quantity (ask backend to check, or place the same
|
|
over-quantity order again afterward — it should now succeed if
|
|
cancellation restored enough stock).
|
|
|
|
## Checking stock before the customer even taps "order"
|
|
|
|
`GET /live/api/v1/mob/products/getproductbyvariant` now accepts an optional
|
|
`locationid` query param:
|
|
|
|
```
|
|
GET /live/api/v1/mob/products/getproductbyvariant?tenantid=1135&variantid=44&locationid=1166
|
|
```
|
|
|
|
When `locationid` is passed, each returned product now carries two extra
|
|
live fields:
|
|
|
|
| field | meaning |
|
|
|---|---|
|
|
| `productstock` | live available quantity at that store — same SUM(in)-SUM(out) formula the order stock check uses |
|
|
| `locationstatus` | that store's status for this product, e.g. `"outofstock"` or `"available"`/`"Active"` |
|
|
|
|
**If `locationid` is omitted, both fields come back empty/zero** — this is
|
|
the old behavior preserved for backward compatibility, not new stock data.
|
|
Start passing `locationid` (the store the customer is browsing) to get real
|
|
numbers, and use it to show "out of stock" / gray out the add-to-cart button
|
|
*before* the customer tries to order, instead of only finding out from the
|
|
`409` response above.
|