delivery slot updated in orders and deliveries

This commit is contained in:
2026-10-06 19:37:08 +05:30
parent 03ae7d310b
commit a30763d323
5 changed files with 274 additions and 6 deletions

218
docs/DELIVERY_SLOTS_APP.md Normal file
View File

@@ -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.

View File

@@ -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"`

View File

@@ -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"`

View File

@@ -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 {

View File

@@ -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,