Add a read-only report of who can actually open a till at an outlet
Answers the question a shop asks on day one and nothing in the product could: what do I type into the terminal. It separates the two credentials because they are not interchangeable — a password opens a closed terminal and the account decides which shell it opens, a PIN only switches operator on one already open — and it prints the shell each account would land in rather than the raw roleid, since roleid 0 is not in app_roles and reads as nothing at all. `top` ranks outlets by products actually stocked *and* filters to ones somebody can sign in to. That filter is the point: the best-stocked outlets on the platform — Dilse at 471 products, Ninhao at 286 — have no account with a password, so a demo pointed at either opens a till nobody can unlock. Counts products through productlocations rather than per tenant, because a tenant with a full catalogue can still have an outlet stocking none of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
214
scratch/poslogins/main.go
Normal file
214
scratch/poslogins/main.go
Normal file
@@ -0,0 +1,214 @@
|
||||
// Reports who can actually sign in at an outlet, and by which of the two ways.
|
||||
//
|
||||
// Read-only. Answers the question a shop asks on day one — "what do I type into
|
||||
// the till?" — by separating the two credentials that exist, because they are
|
||||
// not interchangeable:
|
||||
//
|
||||
// - a password opens a *closed* terminal, and only a supervisor's does
|
||||
// anything useful, because the shell it opens is decided by the account;
|
||||
//
|
||||
// - a PIN switches operator on a terminal that is *already open*, and is
|
||||
// useless on its own.
|
||||
//
|
||||
// go run ./scratch/poslogins 1087 1135
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"nearle/models"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
type row struct {
|
||||
Userid int
|
||||
Fullname, Authname, Contactno string
|
||||
Password string
|
||||
Pin int64
|
||||
Roleid, Configid int
|
||||
Status string
|
||||
}
|
||||
|
||||
func main() {
|
||||
tenantID, locationID := 1087, 1135
|
||||
if len(os.Args) > 2 {
|
||||
tenantID, _ = strconv.Atoi(os.Args[1])
|
||||
locationID, _ = strconv.Atoi(os.Args[2])
|
||||
}
|
||||
|
||||
_ = godotenv.Load()
|
||||
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||
os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), os.Getenv("DB_USER"),
|
||||
os.Getenv("DB_PASSWORD"), os.Getenv("DB_NAME"))
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// `top` ranks outlets by what a till would actually have to sell, so a demo
|
||||
// is pointed at a shop with a catalogue rather than at one that opens empty.
|
||||
if len(os.Args) > 1 && os.Args[1] == "top" {
|
||||
type outletRow struct {
|
||||
Tenantid, Locationid int
|
||||
Tenantname, Locname string
|
||||
Products, Withpasswd int
|
||||
}
|
||||
var top []outletRow
|
||||
db.Raw(`
|
||||
SELECT t.tenantid, l.locationid,
|
||||
COALESCE(t.tenantname,'') AS tenantname,
|
||||
COALESCE(l.locationname,'') AS locname,
|
||||
COUNT(DISTINCT a.productid) AS products,
|
||||
(SELECT COUNT(*) FROM app_users u
|
||||
WHERE u.tenantid = t.tenantid
|
||||
AND COALESCE(u.locationid,0) IN (l.locationid, 0)
|
||||
AND COALESCE(u.password,'') <> ''
|
||||
AND LOWER(COALESCE(u.status,'')) <> 'inactive') AS withpasswd
|
||||
FROM tenantlocations l
|
||||
JOIN tenants t ON t.tenantid = l.tenantid
|
||||
JOIN productlocations b ON b.locationid = l.locationid AND b.tenantid = l.tenantid
|
||||
JOIN products a ON a.productid = b.productid AND a.tenantid = b.tenantid
|
||||
GROUP BY t.tenantid, l.locationid, t.tenantname, l.locationname
|
||||
ORDER BY products DESC LIMIT 400`).Scan(&top)
|
||||
|
||||
fmt.Printf("%-8s %-10s %-22s %-26s %-9s %s\n",
|
||||
"tenant", "outlet", "tenant name", "outlet name", "products", "can sign in")
|
||||
shown := 0
|
||||
for _, o := range top {
|
||||
// Only outlets a person can actually open. A big catalogue behind a
|
||||
// till nobody can sign in to is not a candidate for anything.
|
||||
if o.Withpasswd == 0 || shown >= 12 {
|
||||
continue
|
||||
}
|
||||
shown++
|
||||
fmt.Printf("%-8d %-10d %-22s %-26s %-9d %d\n",
|
||||
o.Tenantid, o.Locationid, trunc(o.Tenantname, 22),
|
||||
trunc(o.Locname, 26), o.Products, o.Withpasswd)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var tenant, outlet string
|
||||
db.Raw(`SELECT COALESCE(tenantname,'') FROM tenants WHERE tenantid=?`, tenantID).Scan(&tenant)
|
||||
db.Raw(`SELECT COALESCE(locationname,'') FROM tenantlocations WHERE locationid=? AND tenantid=?`,
|
||||
locationID, tenantID).Scan(&outlet)
|
||||
if outlet == "" {
|
||||
log.Fatalf("tenant %d has no outlet %d", tenantID, locationID)
|
||||
}
|
||||
|
||||
// Counted the way /pos/catalogue counts them — products joined to this
|
||||
// outlet — rather than per tenant. A tenant with a full catalogue can still
|
||||
// have an outlet that stocks none of it, and that outlet's till opens empty.
|
||||
var products int64
|
||||
db.Raw(`SELECT COUNT(*)
|
||||
FROM products a
|
||||
INNER JOIN productlocations b
|
||||
ON a.productid = b.productid AND a.tenantid = b.tenantid
|
||||
WHERE a.tenantid = ? AND b.locationid = ?`, tenantID, locationID).Scan(&products)
|
||||
|
||||
fmt.Printf("tenant %d %s\noutlet %d %s\nproducts stocked at this outlet: %d\n",
|
||||
tenantID, tenant, locationID, outlet, products)
|
||||
|
||||
// Everyone the outlet can see. locationid 0 is a tenant-wide account — a
|
||||
// proprietor who is not pinned to one shop — and those can open any of
|
||||
// their outlets, so they belong in this list too.
|
||||
var rows []row
|
||||
db.Raw(`
|
||||
SELECT userid,
|
||||
TRIM(COALESCE(firstname,'') || ' ' || COALESCE(lastname,'')) AS fullname,
|
||||
COALESCE(authname,'') AS authname,
|
||||
COALESCE(contactno,'') AS contactno,
|
||||
COALESCE(password,'') AS password,
|
||||
COALESCE(pin,0) AS pin,
|
||||
COALESCE(roleid,0) AS roleid,
|
||||
COALESCE(configid,0) AS configid,
|
||||
COALESCE(status,'') AS status
|
||||
FROM app_users
|
||||
WHERE tenantid = ?
|
||||
AND COALESCE(locationid,0) IN (?, 0)
|
||||
AND LOWER(COALESCE(status,'')) <> 'inactive'
|
||||
ORDER BY userid`, tenantID, locationID).Scan(&rows)
|
||||
|
||||
fmt.Printf("\n=== PASSWORD SIGN-IN (POST /v1/pos/login) — opens a closed terminal ===\n")
|
||||
fmt.Printf("%-7s %-24s %-30s %-12s %-6s %s\n",
|
||||
"userid", "name", "authname (the username)", "role", "shell", "password")
|
||||
any := false
|
||||
for _, r := range rows {
|
||||
if strings.TrimSpace(r.Password) == "" {
|
||||
continue
|
||||
}
|
||||
any = true
|
||||
shell := "cashier"
|
||||
if models.PosRoleCanManageStaff(r.Roleid) {
|
||||
shell = "SUPER"
|
||||
}
|
||||
role := models.PosRoleName(r.Roleid)
|
||||
if role == "" {
|
||||
role = fmt.Sprintf("(roleid %d)", r.Roleid)
|
||||
}
|
||||
id := r.Authname
|
||||
if id == "" {
|
||||
id = r.Contactno + " (phone)"
|
||||
}
|
||||
fmt.Printf("%-7d %-24s %-30s %-12s %-6s %s\n",
|
||||
r.Userid, trunc(r.Fullname, 24), trunc(id, 30), role, shell, r.Password)
|
||||
}
|
||||
if !any {
|
||||
fmt.Println(" (nobody at this outlet has a password — the till cannot be opened)")
|
||||
}
|
||||
|
||||
fmt.Printf("\n=== PIN SIGN-IN (POST /v1/pos/login/pin) — switches operator, terminal already open ===\n")
|
||||
fmt.Printf("%-7s %-24s %-12s %-6s %s\n", "userid", "name", "role", "shell", "pin")
|
||||
any = false
|
||||
seen := map[int64]int{}
|
||||
for _, r := range rows {
|
||||
if r.Pin == 0 {
|
||||
continue
|
||||
}
|
||||
seen[r.Pin]++
|
||||
}
|
||||
for _, r := range rows {
|
||||
if r.Pin == 0 {
|
||||
continue
|
||||
}
|
||||
any = true
|
||||
shell := "cashier"
|
||||
if models.PosRoleCanManageStaff(r.Roleid) {
|
||||
shell = "SUPER"
|
||||
}
|
||||
role := models.PosRoleName(r.Roleid)
|
||||
if role == "" {
|
||||
role = fmt.Sprintf("(roleid %d)", r.Roleid)
|
||||
}
|
||||
note := ""
|
||||
if seen[r.Pin] > 1 {
|
||||
note = " <- DUPLICATE, refused at sign-in"
|
||||
}
|
||||
if r.Pin < 1000 {
|
||||
note = " <- under 4 digits, cannot be typed"
|
||||
}
|
||||
fmt.Printf("%-7d %-24s %-12s %-6s %04d%s\n",
|
||||
r.Userid, trunc(r.Fullname, 24), role, shell, r.Pin, note)
|
||||
}
|
||||
if !any {
|
||||
fmt.Println(" (nobody at this outlet has a PIN)")
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func trunc(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[:n-1] + "…"
|
||||
}
|
||||
Reference in New Issue
Block a user