diff --git a/docs/DELIVERY_SLOTS_APP.md b/docs/DELIVERY_SLOTS_APP.md new file mode 100644 index 0000000..0be209b --- /dev/null +++ b/docs/DELIVERY_SLOTS_APP.md @@ -0,0 +1,218 @@ +# Delivery windows — the app contract + +Shoppers now choose when their order arrives: one of three windows a day — +morning, afternoon, evening — set per branch by the shop. + +Two things to build: show the choice at checkout, and send it with the order. +Everything else is done. + +--- + +## 1. What a shopper may pick + +``` +GET https://fiesta.nearle.app/live/api/v1/mob/deliveryslots/available?tenantid=&locationid= +``` + +No authentication. Call it at checkout, once the branch is known. + +**Real response** (branch 1179, taken at 19:02 IST): + +```json +{ + "code": 200, + "message": "Success", + "status": true, + "details": [ + { "deliveryslotid": 3, "slotkey": "evening", "name": "Evening", + "starttime": "17:00", "endtime": "20:00", + "slotdate": "2026-10-06", "istomorrow": false }, + { "deliveryslotid": 1, "slotkey": "morning", "name": "Morning", + "starttime": "08:00", "endtime": "10:00", + "slotdate": "2026-10-07", "istomorrow": true }, + { "deliveryslotid": 2, "slotkey": "afternoon", "name": "Afternoon", + "starttime": "12:00", "endtime": "15:00", + "slotdate": "2026-10-07", "istomorrow": true }, + { "deliveryslotid": 3, "slotkey": "evening", "name": "Evening", + "starttime": "17:00", "endtime": "20:00", + "slotdate": "2026-10-07", "istomorrow": true } + ] +} +``` + +Morning and afternoon are absent from today because both had ended by 19:02. +**That filtering is already done — render the list as given.** + +### Do no time arithmetic + +Do not compare `starttime`/`endtime` against the device clock to decide what to +show. The server owns that rule, and the device's clock, timezone and locale are +all things we do not control. If the app re-derives it, the two will disagree +and the shopper will be offered a window the server then rejects. + +The fields are there to display — "Evening, 5–8pm" — not to filter on. + +### Ordering + +Already sorted: today's remaining windows first, then tomorrow's, each by start +time. Render in the order given. + +`istomorrow` is there so you can put "Tomorrow" beside a name without comparing +dates yourself. + +--- + +## 2. An empty list is normal + +```json +{ "code": 200, "message": "Success", "status": true, "details": [] } +``` + +**This is not an error, and it is the common case today.** Most branches have +not set windows yet, and they are trading normally right now. + +When `details` is empty: + +- Do not show the window picker +- Do not show an error, a retry, or "this shop is closed" +- **Let the order go through with no window**, exactly as before this feature + +The whole rollout depends on this. A branch with no windows is an ordinary +branch, and treating it as broken would take every shop on the platform offline. + +It is always `[]`, never `null`. + +--- + +## 3. Sending the choice + +``` +POST https://fiesta.nearle.app/live/api/v1/mob/orders/createorder +``` + +Two new **optional** fields on the existing body: + +```json +{ + "deliveryslotid": 3, + "deliveryslotdate": "2026-10-06" +} +``` + +Send both or neither. Copy them straight from the chosen entry — do not +recompute the date. + +Omitting them creates an order with no window, which is valid and unchanged +from today's behaviour. + +--- + +## 4. The one error to handle + +A window takes orders **right up until it ends**, then stops. So a shopper who +opens checkout at 09:58 and pays at 10:02 has chosen a window that closed while +they were deciding. + +The server re-checks on every order and answers: + +```json +{ + "code": 409, + "status": false, + "message": "the morning window has closed for today — please choose another" +} +``` + +**On 409:** re-fetch `available`, show the fresh list, ask again. The `message` +is written to be shown to the shopper as-is. + +Other 409 messages from the same check, all safe to display: + +- `that delivery window is not one this shop offers` +- `the evening window is not currently available` — the shop switched it off +- `that delivery window has already passed` +- `a delivery date is required with a delivery window` + +This is worth handling properly rather than as a generic failure. It is the one +case that will happen to real people in normal use. + +--- + +## 5. Reading it back + +Orders carry what was chosen: + +```json +{ "deliveryslotid": 3, "deliveryslotdate": "2026-10-06" } +``` + +Both absent or `0`/empty on orders placed without a window. Show the window on +the confirmation screen and in order history; treat absence as "no window was +asked for", never as missing data. + +--- + +## 6. What the window means + +**A preference, not a promise.** + +- Every order is accepted. A window never fills up and never blocks a sale. +- There is no capacity limit, and no "slots remaining". +- It tells the shop when to group the drop, and the shopper roughly when to + expect it. + +Please do not word it in the app as a guaranteed delivery time. "Arrives +between 5 and 8pm" is right; "Guaranteed by 8pm" is not something the backend +can honour. + +--- + +## 7. Done on our side + +| | | +|---|---| +| `deliveryslots` table, per branch | ✅ live | +| `GET /v1/mob/deliveryslots/available` | ✅ live, filtering and dating already applied | +| `orders.deliveryslotid` + `deliveryslotdate` | ✅ live | +| `createorder` accepts and validates both | ✅ live, 409 on a closed window | +| Server timezone (IST) | ✅ fixed — windows close on the shop's clock | +| Shops set their windows at onboarding | ✅ live in both consoles | +| Shops edit them later (Store profile → Settings) | ✅ live | +| Window shown on the order in the console | ✅ live | + +Verified end to end on live data: saved through the console, served to the app +already filtered, with today's closed windows correctly absent. + +**Not done:** grouping the dispatch queue by window. That is a console concern +and does not affect anything above. + +--- + +## 8. A branch you can test against + +**Tenant `1141`, branch `1179`** — three windows configured: + +| | | +|---|---| +| Morning | 08:00–10:00 | +| Afternoon | 12:00–15:00 | +| Evening | 17:00–20:00 | + +``` +GET /live/api/v1/mob/deliveryslots/available?tenantid=1141&locationid=1179 +``` + +Call it at different times of day and the list shortens as windows close — +that is the easiest way to see the rule working. + +For the empty-list path, use any other branch: most have no windows set, which +is exactly the case you need to handle. + +--- + +## Questions + +The rule lives in one place server-side (`services/deliverySlotService.go`), so +if anything about open/closed looks wrong, it is one function and not a +disagreement between us. Ask rather than working around it in the app — a +workaround on the device is how the two clocks drift apart. diff --git a/models/deliveries.go b/models/deliveries.go index 0c23e40..acbfe27 100644 --- a/models/deliveries.go +++ b/models/deliveries.go @@ -166,6 +166,18 @@ type Ridersummary struct { } type Deliveryinfo struct { + // The delivery window the customer asked for, joined from the order. + // + // Zero on every delivery whose order named no window, which is most of + // them — treat absence as "none was asked for", never as missing data. + // Read-only: the window lives on orders, not here. + Deliveryslotid int `json:"deliveryslotid" gorm:"->"` + Deliveryslotdate string `json:"deliveryslotdate" gorm:"->"` + Slotkey string `json:"slotkey" gorm:"->"` + Deliveryslotname string `json:"deliveryslotname" gorm:"->"` + Deliveryslotstart string `json:"deliveryslotstart" gorm:"->"` + Deliveryslotend string `json:"deliveryslotend" gorm:"->"` + Deliveryid int `json:"deliveryid"` Orderheaderid int `json:"orderheaderid"` Applocationid int `json:"applocationid"` diff --git a/models/order.go b/models/order.go index 9ba820d..e2f887c 100644 --- a/models/order.go +++ b/models/order.go @@ -255,8 +255,17 @@ type Orders struct { // Distinct from `Deliverytime` above, which is a TIMESTAMP of what happened // and is defaulted to now() a few lines into CreateOrderv3. These two say // what was asked for; that one says what occurred. - Deliveryslotid int `json:"deliveryslotid" gorm:"column:deliveryslotid"` - Deliveryslotdate string `json:"deliveryslotdate" gorm:"column:deliveryslotdate"` + Deliveryslotid int `json:"deliveryslotid" gorm:"column:deliveryslotid"` + Deliveryslotdate string `json:"deliveryslotdate" gorm:"column:deliveryslotdate"` + // Joined from deliveryslots, not stored on the order. + // + // So a shop that renames "Evening" to "After work" sees the new name on + // orders already placed — the window they chose has not changed, only what + // it is called. Read-only: nothing writes these back. + Slotkey string `json:"slotkey" gorm:"->"` + Deliveryslotname string `json:"deliveryslotname" gorm:"->"` + Deliveryslotstart string `json:"deliveryslotstart" gorm:"->"` + Deliveryslotend string `json:"deliveryslotend" gorm:"->"` Orderstatus string `json:"orderstatus"` Pending string `json:"pending"` Processing string `json:"processing"` diff --git a/repositories/deliveriesRepository.go b/repositories/deliveriesRepository.go index f4abb7b..c6c2f24 100644 --- a/repositories/deliveriesRepository.go +++ b/repositories/deliveriesRepository.go @@ -120,13 +120,26 @@ const ( a.riderslat,a.riderslon,a.deliveryamt,a.kms,a.actualkms,a.riderkms,a.deliverycharges,a.deliverytype,a.paymenttype,a.smsdelivery, a.expecteddeliverytime,a.profit,a.transitminutes,a.calculationdistancekm, a.notes,a.ordernotes,b.tenantname,b.primarycontact as tenantcontactno,b.tenanttoken,b.suburb as tenantsuburb,b.city as tenantcity, - c.firstname AS ridername,c.userfcmtoken,e.locationname,e.suburb AS locationsuburb,e.contactno AS locationcontactno + c.firstname AS ridername,c.userfcmtoken,e.locationname,e.suburb AS locationsuburb,e.contactno AS locationcontactno, + -- The delivery window, read from the ORDER. + -- + -- The deliveries table has no window column and deliberately gets none: + -- the window is a fact about what the customer asked for, and copying it + -- here would be a second truth that can drift from the first. The + -- dispatch board is exactly where drift would be noticed and exactly where + -- it would cost the most, so it is joined. + o.deliveryslotid, o.deliveryslotdate, s.slotkey, s.name AS deliveryslotname, + s.starttime AS deliveryslotstart, s.endtime AS deliveryslotend FROM deliveries a INNER JOIN tenants b ON a.tenantid=b.tenantid INNER JOIN app_users c ON a.userid=c.userid INNER JOIN tenantlocations e ON a.locationid=e.locationid INNER JOIN app_location f ON a.applocationid = f.applocationid - INNER JOIN app_locationconfig g ON f.applocationid = g.applocationid` + INNER JOIN app_locationconfig g ON f.applocationid = g.applocationid + -- Both LEFT. Most deliveries have no window, and every one of them must + -- still appear on the board — an INNER JOIN here would empty dispatch. + LEFT JOIN orders o ON a.orderheaderid = o.orderheaderid + LEFT JOIN deliveryslots s ON o.deliveryslotid = s.slotid` ) func (r *deliveriesRepository) CreateDeliveries(data []models.Deliveries) error { diff --git a/repositories/orderRepository.go b/repositories/orderRepository.go index 5e9c61c..82a2247 100644 --- a/repositories/orderRepository.go +++ b/repositories/orderRepository.go @@ -102,14 +102,30 @@ const ( a.deliveryid AS deliverycustomerid, a.deliveryid, a.deliveryaddress, a.deliverylat, a.deliverylong, a.deliverytype, a.deliverycustomer,a.deliverycontactno,a.deliverylocation as deliverysuburb, a.deliverycity, a.paymenttype, a.smsdelivery, b.customertoken, c.tenantname, c.tenanttoken, c.primarycontact AS tenantcontactno, c.postcode AS tenantpostcode, c.suburb AS tenantsuburb, c.city AS tenantcity, - d.locationname, d.contactno AS locationcontactno, d.postcode AS locationpostcode, d.suburb AS locationsuburb, d.city AS locationcity + d.locationname, d.contactno AS locationcontactno, d.postcode AS locationpostcode, d.suburb AS locationsuburb, d.city AS locationcity, + -- The delivery window the customer asked for. + -- + -- Selected EXPLICITLY, like everything else here: this select names its + -- columns, so a field added to the Go struct and not added to this line + -- is simply absent from every row, with nothing anywhere saying so. That + -- exact gap shipped once on products.showhealthscore and made every + -- reading taken from the list meaningless. + -- + -- The NAME is joined rather than stored on the order, so a shop that + -- renames "Evening" to "After work" sees the new name on old orders -- + -- the window they chose has not changed, only what it is called. + a.deliveryslotid, a.deliveryslotdate, s.slotkey, s.name AS deliveryslotname, + s.starttime AS deliveryslotstart, s.endtime AS deliveryslotend FROM orders a LEFT JOIN customers b ON a.customerid = b.customerid LEFT JOIN tenants c ON a.tenantid = c.tenantid LEFT JOIN tenantlocations d ON a.locationid = d.locationid LEFT JOIN app_location h ON a.applocationid = h.applocationid - LEFT JOIN app_locationconfig i ON a.applocationid = i.applocationid` + LEFT JOIN app_locationconfig i ON a.applocationid = i.applocationid + -- LEFT, because the overwhelming majority of orders have no window and + -- must still appear. An INNER JOIN here would silently empty the list. + LEFT JOIN deliveryslots s ON a.deliveryslotid = s.slotid` orderdetails = `SELECT DISTINCT a.orderheaderid, a.applocationid, a.tenantid, a.locationid, a.partnerid, a.configid, a.categoryid, a.subcategoryid, a.moduleid,