diff --git a/docs/STORE_OPEN_APP.md b/docs/STORE_OPEN_APP.md new file mode 100644 index 0000000..e9b926d --- /dev/null +++ b/docs/STORE_OPEN_APP.md @@ -0,0 +1,172 @@ +# 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. diff --git a/repositories/tenantRepository.go b/repositories/tenantRepository.go index 0c176d2..a547868 100644 --- a/repositories/tenantRepository.go +++ b/repositories/tenantRepository.go @@ -915,7 +915,13 @@ func (r *tenantRepository) GetTenantByID(tid int, locationid int, userid int) (m q1 := ` SELECT a.*,b.categoryname,c.locationname AS applocation, d.allocationid AS allocationmode,e.typename AS allocationtype,e.mapid AS allocationid,f.locationid, - f.locationname, f.contactno as locationcontact + f.locationname, f.contactno as locationcontact, + -- Whether the branch is trading today, so the shop profile can show + -- its own switch in the right position. Without these the console + -- reads no value, defaults the switch to open, and a shop that just + -- closed itself looks open again on the next page load. + COALESCE(f.isopen, true) AS isopen, + COALESCE(f.closeduntil::text, '') AS closeduntil FROM tenants a LEFT JOIN app_category b ON a.categoryid = b.categoryid LEFT JOIN app_location c ON a.applocationid = c.applocationid