// Creates a supervisor and a cashier at an outlet, then proves both can sign in. // // Exists because outlet 1135 — the one the terminal ships pointed at — had no // staff at all, so the till fell back to the three PINs compiled into the app. // Real staff here are what retire those. // // PINs are generated rather than chosen, from crypto/rand, and printed once so // they can be handed to the shop. They are deliberately not derived from // anything guessable. // // go run ./scratch/posstaffsetup plan 1087 1135 // go run ./scratch/posstaffsetup apply 1087 1135 package main import ( "crypto/rand" "fmt" "log" "math/big" "os" "strconv" "nearle/models" "nearle/repositories" "github.com/joho/godotenv" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" ) func main() { mode, tenantID, locationID := "plan", 1087, 1135 if len(os.Args) > 1 { mode = os.Args[1] } if len(os.Args) > 3 { tenantID, _ = strconv.Atoi(os.Args[2]) locationID, _ = strconv.Atoi(os.Args[3]) } _ = 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) } repo := repositories.NewPosRepository(db) var locName string db.Raw(`SELECT COALESCE(locationname,'') FROM tenantlocations WHERE locationid=? AND tenantid=?`, locationID, tenantID).Scan(&locName) if locName == "" { log.Fatalf("tenant %d has no outlet %d", tenantID, locationID) } // The configid the shop's other accounts use, so a new cashier is visible // to the same portal as everybody else at that outlet. var configID int db.Raw(`SELECT COALESCE(configid,0) FROM app_users WHERE tenantid=? AND COALESCE(configid,0) > 0 GROUP BY configid ORDER BY COUNT(*) DESC LIMIT 1`, tenantID).Scan(&configID) fmt.Printf("tenant %d, outlet %d (%s), configid %d\n\n", tenantID, locationID, locName, configID) existing, err := repo.ListPosUsers(tenantID, locationID, true) if err != nil { log.Fatal(err) } fmt.Printf("till users already at this outlet: %d\n", len(existing)) for _, u := range existing { fmt.Printf(" %-6d %-22s %-12s pin=%s %s\n", u.Userid, u.Fullname, u.Role, u.Pin, u.Status) } if len(existing) > 0 { fmt.Println("\nAlready set up. Nothing to do — this refuses to add duplicates.") return } wanted := []models.PosUserRequest{ {Fullname: "Store Supervisor", Role: "supervisor", Pin: newPin()}, {Fullname: "Counter Cashier", Role: "cashier", Pin: newPin()}, } for wanted[0].Pin == wanted[1].Pin { wanted[1].Pin = newPin() } fmt.Println("\nwould create:") for _, w := range wanted { fmt.Printf(" %-22s %-12s pin=%s\n", w.Fullname, w.Role, w.Pin) } if mode != "apply" { fmt.Println("\nNothing written — run `apply` to commit.") return } fmt.Println() for _, w := range wanted { created, err := repo.CreatePosUser(tenantID, locationID, configID, w) if err != nil { log.Fatalf("creating %s: %v", w.Fullname, err) } fmt.Printf(" created userid %-6d %-22s %-12s PIN %s\n", created.Userid, created.Fullname, created.Role, created.Pin) } // The point of the exercise: does the till now see real staff? staff, err := repo.PosStaff(tenantID, locationID) if err != nil { log.Fatal(err) } fmt.Printf("\n/pos/staff now returns %d person(s):\n", len(staff)) for _, s := range staff { fmt.Printf(" %-22s %-12s\n", s.Fullname, s.Role) } // And can they actually sign in? fmt.Println("\nPIN sign-in:") for _, w := range wanted { session, err := repo.PosLoginByPin(tenantID, locationID, w.Pin) if err != nil { fmt.Printf(" %-22s REFUSED: %v\n", w.Fullname, err) continue } fmt.Printf(" %-22s -> %s at %s, can_manage_staff=%v\n", w.Fullname, session.Role, session.Locationname, session.Canmanagestaff) } if _, err := repo.PosLoginByPin(tenantID, locationID, "5555"); err != nil { fmt.Printf("\n an unknown PIN is refused: %v\n", err) } else { fmt.Println("\n !! an unknown PIN was ACCEPTED") } fmt.Println("\n-- undo:") fmt.Printf("UPDATE app_users SET status='InActive' WHERE tenantid=%d AND locationid=%d AND roleid IN (%d,%d);\n", tenantID, locationID, models.PosRoleSupervisor, models.PosRoleCashier) } // newPin returns a four-digit PIN this schema can store, from crypto/rand. // // 1000–9999 because a leading zero cannot survive a bigint column, and the // obvious ones are rejected by validatePosPin anyway — retried here rather than // filtered, so the distribution stays even. func newPin() string { for { n, err := rand.Int(rand.Reader, big.NewInt(9000)) if err != nil { log.Fatal(err) } pin := strconv.FormatInt(n.Int64()+1000, 10) switch pin { case "1234", "1111", "2345", "3456", "4321", "9999", "2222": continue } return pin } }