173 lines
5.4 KiB
Markdown
173 lines
5.4 KiB
Markdown
# Store open / closed — the app contract
|
|
|
|
A shop can now close itself for the day. When it does, customers must not be
|
|
able to order from it.
|
|
|
|
Two things to build: show the closed state, and handle the refusal if someone
|
|
orders anyway. Everything else is done.
|
|
|
|
---
|
|
|
|
## 1. Four new fields on the store list
|
|
|
|
```
|
|
GET https://fiesta.nearle.app/live/api/v1/mob/tenants/getcustomertenants
|
|
?customerid=&tenant=0&latitude=&longitude=&categoryid=
|
|
```
|
|
|
|
Every store in `details[]` now carries:
|
|
|
|
```json
|
|
{
|
|
"isopen": true,
|
|
"closeduntil": "",
|
|
"isaccepting": true,
|
|
"closedreason": ""
|
|
}
|
|
```
|
|
|
|
### Read `isaccepting`, not `isopen`
|
|
|
|
This is the one thing to get right.
|
|
|
|
| field | what it is |
|
|
|---|---|
|
|
| **`isaccepting`** | **the answer.** Can this store take an order right now? |
|
|
| `isopen` | the raw switch — what the shopkeeper last pressed |
|
|
| `closeduntil` | the first day back, `"YYYY-MM-DD"`, or `""` |
|
|
| `closedreason` | why not, written to show the customer. `""` when open |
|
|
|
|
They differ. A shop that closed on Friday "until Monday" has `isopen: false`
|
|
every day including Monday — the flag is never flipped back. On Monday the
|
|
server works out that the date has passed and sets `isaccepting: true`.
|
|
|
|
Branch the UI on `isaccepting`. `isopen` and `closeduntil` are there if you
|
|
want to say more, not to decide with.
|
|
|
|
### `closedreason` is already written for the customer
|
|
|
|
```
|
|
"This store is closed today"
|
|
"This store is closed today and reopens on 12 Oct"
|
|
"This store is not currently available"
|
|
```
|
|
|
|
Show it as it comes. Don't build the sentence from `closeduntil` — the server
|
|
already handles the cases where there is no date, where the branch has been
|
|
switched off by Nearle rather than by the shop, and where the date is
|
|
unreadable.
|
|
|
|
---
|
|
|
|
## 2. A closed store is still returned
|
|
|
|
It is **not** filtered out of the list. You decide whether to grey it or hide
|
|
it.
|
|
|
|
**I would grey it.** A shop that vanishes reads to a regular customer as gone
|
|
for good; one marked "Closed today — reopens 12 Oct" brings them back
|
|
tomorrow. But it is your call, and the data supports either.
|
|
|
|
What the store must not do is look ordinary. If `isaccepting` is false,
|
|
whatever you render has to stop a customer getting as far as a basket.
|
|
|
|
---
|
|
|
|
## 3. Delivery windows follow automatically
|
|
|
|
```
|
|
GET /v1/mob/deliveryslots/available?tenantid=&locationid=
|
|
```
|
|
|
|
A closed store now returns an **empty list**, plus the reason:
|
|
|
|
```json
|
|
{ "code": 200, "status": true, "details": [], "closedreason": "This store is closed today" }
|
|
```
|
|
|
|
You already handle an empty `details` as "no windows here" — see
|
|
`DELIVERY_SLOTS_APP.md` §2 — so this needs nothing new. It stops a customer
|
|
picking tomorrow morning from a shop that is shut and only finding out at
|
|
checkout.
|
|
|
|
---
|
|
|
|
## 4. The refusal
|
|
|
|
```
|
|
POST /v1/mob/orders/createorder
|
|
```
|
|
|
|
If the store is closed, the order is refused:
|
|
|
|
```json
|
|
{
|
|
"code": 409,
|
|
"status": false,
|
|
"message": "This store is closed today and reopens on 12 Oct"
|
|
}
|
|
```
|
|
|
|
**Handle this properly rather than as a generic failure.** It is not a bug or
|
|
an edge case — it happens to real people in normal use: a customer with the
|
|
app open when the shopkeeper closes, or a screen left open since this morning.
|
|
|
|
On 409: show the `message`, and refresh the store list so the UI catches up.
|
|
|
|
The server checks on every order because `/v1/mob/*` carries no session —
|
|
anything arriving is a claim. Hiding the store in the app is presentation;
|
|
this is the enforcement.
|
|
|
|
---
|
|
|
|
## 5. Nothing changes for an open store
|
|
|
|
Every store on the platform has `isopen: true` and `isaccepting: true` right
|
|
now. There was no backfill and there will not be one. A shop that never
|
|
touches the switch behaves exactly as it does today.
|
|
|
|
---
|
|
|
|
## 6. Done on our side
|
|
|
|
| | |
|
|
|---|---|
|
|
| `tenantlocations.isopen` + `closeduntil` | ✅ live, default open |
|
|
| Four fields on `getcustomertenants` | ✅ live, verified on all 9 stores |
|
|
| `PUT /v1/web/tenants/storeopen` (guarded) | ✅ live — 401 unauthenticated, absent from `/v1/mob` |
|
|
| `createorder` refuses a closed store | ✅ built, 409 with the shop's wording |
|
|
| Closed store returns no delivery windows | ✅ built |
|
|
| Merchant console: switch in the shop profile header | ✅ live |
|
|
| Nearle console: "Trading" column per branch | ✅ live |
|
|
| Auto-reopen on the date | ✅ computed on read — no job, nothing to miss |
|
|
|
|
**Verified against production:** the four fields, and that the write endpoint
|
|
is live and guarded.
|
|
|
|
**Not yet exercised against production:** the 409 and the empty window list.
|
|
Both are covered by unit tests and need a real store closed to confirm
|
|
end to end. If you want to test, ask for tenant `1141` / branch `1179` to be
|
|
closed — it is a test store with delivery windows already set.
|
|
|
|
---
|
|
|
|
## 7. Two rules worth knowing
|
|
|
|
**Closed with no date stays closed.** A power cut has no end date. The shop
|
|
reopens when a person says so, not on a timer. `closeduntil: ""` with
|
|
`isaccepting: false` is a normal state, not missing data.
|
|
|
|
**"Closed until the 12th" means open ON the 12th.** The date names the first
|
|
day back, which is how the phrase reads in English. The console says
|
|
"Reopens on" so nobody has to work it out.
|
|
|
|
---
|
|
|
|
## Questions
|
|
|
|
The rule lives in one function server-side (`models/storeopen.go`), and the
|
|
store list, order creation and the delivery-window endpoint all call it. So if
|
|
anything about open/closed looks inconsistent between those three, it is one
|
|
place to fix and not three to reconcile. Ask rather than working around it in
|
|
the app — a local override is how the two sides drift apart.
|