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