// 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] + "…" }