delivery slot updated in orders and deliveries
This commit is contained in:
218
docs/DELIVERY_SLOTS_APP.md
Normal file
218
docs/DELIVERY_SLOTS_APP.md
Normal 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.
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user