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

@@ -467,6 +467,31 @@ type TenantInfo struct {
Categoryid int `json:"categoryid"`
Registrationno string `json:"registrationno"`
Orderscount int `json:"orderscount"`
/*
Whether this branch is taking orders, and why not when it is not.
`Isopen` and `Closeduntil` are the stored columns; `Isaccepting` and
`Closedreason` are worked out per request by models.StoreIsOpen, which
also folds in `status` and lets a dated close expire on its own.
THE APP SHOULD READ `isaccepting`, not `isopen`. The raw flag says what
the shopkeeper last pressed; the computed one is the answer — a branch
that said "back on Monday" has isopen false on Monday and is open.
The branch is still RETURNED when closed rather than filtered out. A shop
that vanishes reads to a regular customer as gone for good; one marked
"closed today" brings them back tomorrow. An app that would rather hide
it can, from this field.
*/
// The BRANCH status, aliased in the query so it does not collide with the
// tenant status already on this struct. Feeds StoreIsOpen.
Locationstatus string `json:"-" gorm:"column:locationstatus"`
Isopen bool `json:"isopen"`
Closeduntil string `json:"closeduntil"`
Isaccepting bool `json:"isaccepting" gorm:"-"`
Closedreason string `json:"closedreason" gorm:"-"`
// Products []Products `json:"products" gorm:"-"`
ProductSubcategory []ProductSubcategory `json:"productsubcategory" gorm:"-"`
}

89
models/storeopen.go Normal file
View File

@@ -0,0 +1,89 @@
package models
import (
"strings"
"time"
)
/*
Is a branch taking orders right now?
Two things have to be true: the branch exists as far as the platform is
concerned, and the shop has not closed itself for the day. They are different
questions with different owners — Nearle decommissions a branch, a shopkeeper
closes for an afternoon — and this is the only place they are answered
together.
── Reopening is worked out, not scheduled ──────────────────────────────────
A shop that said "back on Monday" is open on Monday because this function says
so, not because a job ran. Nothing has to fire, nothing can be missed, and a
server that was down over the weekend still gets it right. The alternative — a
nightly task flipping the flag — fails silently exactly once and leaves a shop
shut with nobody watching.
── Why the date means the day it REOPENS ───────────────────────────────────
"Closed until the 12th" reads, in English, as open on the 12th. So the date
names the first day back, and a shop closing for today alone sets tomorrow.
The console says "Reopens on" rather than "Closed until" so nobody has to
work that out.
*/
func StoreIsOpen(status string, isOpen bool, closedUntil string, now time.Time) bool {
// Decommissioned beats everything. A branch Nearle has switched off does
// not come back because a date passed.
if !strings.EqualFold(strings.TrimSpace(status), "active") {
return false
}
if isOpen {
return true
}
// Closed, with a date to come back on.
reopen, ok := parseStoreDate(closedUntil)
if !ok {
// Closed with no end in sight — a power cut nobody can put a date on.
// Stays closed until a person says otherwise, which is the safe way
// round: a shop wrongly shown open takes orders it cannot fill.
return false
}
today := now.Truncate(24 * time.Hour)
return !today.Before(reopen.Truncate(24 * time.Hour))
}
/*
Why a shopper cannot order, in words fit to show them.
Empty when the branch is open. Returned straight to the app, so it says what a
person needs to know and not what the database holds.
*/
func StoreClosedReason(status string, isOpen bool, closedUntil string, now time.Time) string {
if StoreIsOpen(status, isOpen, closedUntil, now) {
return ""
}
if !strings.EqualFold(strings.TrimSpace(status), "active") {
return "This store is not currently available"
}
if reopen, ok := parseStoreDate(closedUntil); ok {
return "This store is closed today and reopens on " + reopen.Format("2 Jan")
}
return "This store is closed today"
}
// Accepts a bare date or a timestamp, because the column is a DATE but GORM
// and the console have both been known to hand over a full RFC3339 string.
// Anything unreadable is "no date", never today.
func parseStoreDate(value string) (time.Time, bool) {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return time.Time{}, false
}
if len(trimmed) >= 10 {
if parsed, err := time.Parse("2006-01-02", trimmed[:10]); err == nil {
return parsed, true
}
}
return time.Time{}, false
}

111
models/storeopen_test.go Normal file
View File

@@ -0,0 +1,111 @@
package models
import (
"strings"
"testing"
"time"
)
func at(day string) time.Time {
parsed, err := time.Parse("2006-01-02", day)
if err != nil {
panic(err)
}
return parsed
}
func TestOpenByDefault(t *testing.T) {
// Every branch trading today has isopen true and no date. None of them may
// go dark the moment this ships.
if !StoreIsOpen("Active", true, "", at("2026-10-09")) {
t.Error("a normal trading branch was reported closed")
}
}
func TestClosedWithNoDateStaysClosed(t *testing.T) {
// A power cut nobody can put an end to. It stays shut until a person says
// otherwise — the safe way round, because a shop wrongly shown open takes
// orders it cannot fill.
if StoreIsOpen("Active", false, "", at("2026-10-09")) {
t.Error("a branch closed with no reopen date was reported open")
}
}
func TestReopensOnTheDateItNamed(t *testing.T) {
// "Closed until the 12th" means open ON the 12th.
cases := []struct {
today string
want bool
}{
{"2026-10-10", false}, // still closed
{"2026-10-11", false}, // last closed day
{"2026-10-12", true}, // back
{"2026-10-13", true}, // and stays back
}
for _, tc := range cases {
got := StoreIsOpen("Active", false, "2026-10-12", at(tc.today))
if got != tc.want {
t.Errorf("on %s: open=%v, want %v", tc.today, got, tc.want)
}
}
}
func TestReopeningNeedsNobodyToRemember(t *testing.T) {
// The whole reason this is computed and not scheduled: no job ran, nothing
// flipped the flag, and the shop is open anyway.
if !StoreIsOpen("Active", false, "2026-10-01", at("2026-12-25")) {
t.Error("a branch whose reopen date passed months ago is still shut")
}
}
func TestDecommissionedBeatsEverything(t *testing.T) {
// Status is Nearle's word for whether the branch exists. A date passing
// must not bring back a branch that was switched off.
if StoreIsOpen("Inactive", true, "", at("2026-10-09")) {
t.Error("an inactive branch was reported open")
}
if StoreIsOpen("Inactive", false, "2026-10-01", at("2026-12-25")) {
t.Error("an inactive branch came back because a date passed")
}
}
func TestUnreadableDateIsNotTreatedAsToday(t *testing.T) {
// A zero time would be before today and quietly reopen the shop.
if StoreIsOpen("Active", false, "not a date", at("2026-10-09")) {
t.Error("an unreadable reopen date reopened the branch")
}
}
func TestAcceptsATimestampAsWellAsADate(t *testing.T) {
// The column is a DATE, but GORM and the console have both been known to
// hand over a full timestamp.
if !StoreIsOpen("Active", false, "2026-10-12T00:00:00Z", at("2026-10-12")) {
t.Error("a timestamp form of the reopen date was not understood")
}
}
func TestReasonIsWrittenForAShopper(t *testing.T) {
if msg := StoreClosedReason("Active", true, "", at("2026-10-09")); msg != "" {
t.Errorf("an open store gave a reason: %q", msg)
}
msg := StoreClosedReason("Active", false, "2026-10-12", at("2026-10-09"))
if !strings.Contains(msg, "12 Oct") {
t.Errorf("the reason does not say when it reopens: %q", msg)
}
// No date: say it is closed, and do not invent a return.
plain := StoreClosedReason("Active", false, "", at("2026-10-09"))
if plain == "" || strings.Contains(plain, "reopens") {
t.Errorf("a close with no date promised a return: %q", plain)
}
}
func TestStatusIsNotCaseSensitive(t *testing.T) {
// The column holds "Active" today, but nothing enforces the capital.
for _, s := range []string{"Active", "active", "ACTIVE", " Active "} {
if !StoreIsOpen(s, true, "", at("2026-10-09")) {
t.Errorf("status %q read as not active", s)
}
}
}

View File

@@ -31,8 +31,8 @@ type Tenantinfo struct {
// result set and thrown away for want of a field to scan into. A shop
// profile that cannot say how long the business has been on the platform is
// missing a fact the database has held all along.
Created time.Time `json:"created"`
Tenantinfo string `json:"tenantinfo"`
Created time.Time `json:"created"`
Tenantinfo string `json:"tenantinfo"`
// The FSSAI or trade licence.
//
// The column has always existed and `GetCustomerTenants` already selects it
@@ -63,14 +63,14 @@ type Tenantinfo struct {
// `b.categoryname`, and the struct had nowhere to put it — so the value was
// computed on every read and discarded, leaving the console to print
// "Category 2" at a merchant who trades under Daily Needs.
Categoryname string `json:"categoryname"`
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
Accountname string `json:"Accountname"`
Status string `json:"status"`
Allocationid int `json:"allocationid"`
Allocationtype string `json:"allocationtype"`
Allocationmode int `json:"allocationmode"`
Categoryname string `json:"categoryname"`
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
Accountname string `json:"Accountname"`
Status string `json:"status"`
Allocationid int `json:"allocationid"`
Allocationtype string `json:"allocationtype"`
Allocationmode int `json:"allocationmode"`
// How many outlets this merchant has.
//
@@ -105,6 +105,35 @@ type Tenantlocations struct {
Cancelsecs int `json:"cancelsecs"`
Status string `json:"status" gorm:"default:Active"`
/*
Is this branch trading today?
SEPARATE FROM `Status`, deliberately. Status is the platform's word for
whether a branch exists at all — setting it to Inactive is how a branch is
decommissioned, and the staff console reads it that way. A shopkeeper
closing for an afternoon because the power went out is saying something
completely different, and the two must not share a field: a day off would
read to Nearle as a shop that had shut down.
Default true. Every branch trading today keeps trading.
*/
Isopen bool `json:"isopen" gorm:"column:isopen;default:true"`
/*
The date it opens again, when the shop knows it.
"Closed until the 12th" means closed on the 11th and OPEN on the 12th —
the date names the first day back, which is how the phrase reads.
Nothing flips `Isopen` back on a timer. The answer is worked out on read
(see StoreIsOpen), so a branch that said "back on Monday" is open on
Monday whether or not anybody remembered. A job would drift; this cannot.
Empty means closed until somebody says otherwise, which is the right
default for a power cut nobody can put an end date on.
*/
Closeduntil string `json:"closeduntil" gorm:"column:closeduntil"`
// Who will run this outlet, when the caller already has somebody in mind.
//
// `gorm:"-"` because it is not a column — it names an existing `app_users`