status updated

This commit is contained in:
2026-10-09 16:27:51 +05:30
parent ed32a2620e
commit f0361d8343
12 changed files with 559 additions and 23 deletions

View File

@@ -12,10 +12,17 @@ import (
type DeliverySlotController struct {
service services.DeliverySlotService
// Asked whether the shop is trading at all. Held here rather than inside
// the slot service, which answers one question — is this window still
// running — and should not also have to know about shop closures.
tenants services.TenantService
}
func NewDeliverySlotController(service services.DeliverySlotService) *DeliverySlotController {
return &DeliverySlotController{service: service}
func NewDeliverySlotController(
service services.DeliverySlotService,
tenants services.TenantService,
) *DeliverySlotController {
return &DeliverySlotController{service: service, tenants: tenants}
}
/*
@@ -128,6 +135,28 @@ func (ctl *DeliverySlotController) AvailableDeliverySlots(c *fiber.Ctx) error {
})
}
/*
A shut shop offers nothing.
Without this, a branch closed for the day still hands the app tomorrow
morning — and the shopper picks it, reaches checkout, and is refused by
the closed-shop guard in order creation with no warning anything was
wrong. The empty list is already the app's "no windows here" path, so
this needs nothing new on their side.
The reason rides along so the app can say WHY rather than just showing
nothing, on an endpoint it is already calling.
*/
if reason := ctl.tenants.StoreClosedReason(tenantID, locationID); reason != "" {
return c.Status(http.StatusOK).JSON(fiber.Map{
"code": http.StatusOK,
"message": "Success",
"status": true,
"details": []models.AvailableDeliverySlot{},
"closedreason": reason,
})
}
slots, err := ctl.service.Available(tenantID, locationID)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{

View File

@@ -20,15 +20,21 @@ type OrderController struct {
// than reimplemented, so the app's list and this check can never disagree
// about where a window ends.
deliverySlotService services.DeliverySlotService
// Asked whether the shop is trading. Same reasoning: the customer-facing
// store list and this check run the same rule, so a shop shown as open is
// never refused here.
tenantService services.TenantService
}
func NewOrderController(
orderService services.OrderService,
deliverySlotService services.DeliverySlotService,
tenantService services.TenantService,
) *OrderController {
return &OrderController{
orderService: orderService,
deliverySlotService: deliverySlotService,
tenantService: tenantService,
}
}
@@ -358,6 +364,24 @@ func (ctl *OrderController) CreateOrderv3(c *fiber.Ctx) error {
data.Deliverytime = time.Now().Format("2006-01-02 15:04:05")
}
/*
Is the shop even trading?
Checked HERE and not only in the app, because /v1/mob/* carries no
session: anything arriving is a claim. The ordinary case is innocent and
still has to be caught — a shopper with the app open when the shopkeeper
closes, or a stale screen from this morning.
409 with the shop's own reason, written to be shown as-is.
*/
if reason := ctl.tenantService.StoreClosedReason(data.Tenantid, data.Locationid); reason != "" {
return c.Status(http.StatusConflict).JSON(fiber.Map{
"code": http.StatusConflict,
"message": reason,
"status": false,
})
}
// The chosen delivery window, re-decided here.
//
// The app sends back what /v1/mob/deliveryslots/available handed it, but

View File

@@ -871,3 +871,53 @@ func (ctl *TenantController) ResendInvite(c *fiber.Ctx) error {
"code": http.StatusOK, "status": true, "message": "Invitation sent.",
})
}
/*
PUT /v1/web/tenants/storeopen
Open or close a branch for the day.
── Why this is on the guarded group ────────────────────────────────────────
Closing a shop stops it taking money. On /v1/mob/* — which carries no session
at all — anyone who could guess a locationid could shut any shop on the
platform. WebAuth checks the tenant scope, so a business can only reach its
own branches.
A business objection — a date in the past, a branch that is not yours — is 409
and not 500: the request was well formed, and the message is written to be
shown to the person who typed it.
*/
func (ctl *TenantController) SetStoreOpen(c *fiber.Ctx) error {
var req struct {
Tenantid int `json:"tenantid"`
Locationid int `json:"locationid"`
// A POINTER, so "not sent" and "sent as false" stay different answers.
// A plain bool would read a missing field as "close this shop".
Isopen *bool `json:"isopen"`
Closeduntil string `json:"closeduntil"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"code": http.StatusBadRequest, "message": "Invalid request body", "status": false,
})
}
if req.Isopen == nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"code": http.StatusBadRequest, "message": "isopen is required", "status": false,
})
}
if err := ctl.tenantService.SetStoreOpen(
req.Tenantid, req.Locationid, *req.Isopen, req.Closeduntil,
); err != nil {
return c.Status(http.StatusConflict).JSON(fiber.Map{
"code": http.StatusConflict, "message": err.Error(), "status": false,
})
}
return c.Status(http.StatusOK).JSON(fiber.Map{
"code": http.StatusOK, "message": "Successfully Updated", "status": true,
})
}