// Can the accounts that may open a till actually complete the new sign-in? // // Read-only, and deliberately prints no numbers and no PINs — only whether each // account has one and whether the login would find it. // // The question this answers is narrower than "does it have a number". The login // matches `LOWER(TRIM(contactno)) = LOWER(TRIM($1))` where $1 has already been // reduced to ten digits, so the comparison is exact: a row holding // "+91 98765 43210" is invisible to somebody typing the same number, because // only one side of the comparison gets normalised. // // go run ./scratch/posloginready package main import ( "fmt" "log" "os" "strings" "nearle/models" "github.com/joho/godotenv" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" ) type row struct { Userid int Tenantid int Locationid int Roleid int Contactno string Pin int64 Status string } // normalise mirrors repositories.normalisePosPhone, which is unexported. func normalise(raw string) string { digits := strings.Map(func(r rune) rune { if r >= '0' && r <= '9' { return r } return -1 }, raw) if len(digits) == 12 && strings.HasPrefix(digits, "91") { digits = digits[2:] } else if len(digits) == 11 && strings.HasPrefix(digits, "0") { digits = digits[1:] } if len(digits) != 10 { return "" } return digits } func main() { _ = 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) } var rows []row if err := db.Raw(` SELECT userid, COALESCE(tenantid,0) AS tenantid, COALESCE(locationid,0) AS locationid, COALESCE(roleid,0) AS roleid, COALESCE(contactno,'') AS contactno, COALESCE(pin,0) AS pin, COALESCE(status,'') AS status FROM app_users WHERE COALESCE(roleid,0) IN (?, ?) ORDER BY tenantid, locationid, userid`, models.PosRoleSupervisor, models.PosRoleCashier).Scan(&rows).Error; err != nil { log.Fatal(err) } // What the login would see. Only active till accounts are candidates, and a // number matching more than one of them is refused outright. byPhone := map[string][]int{} for _, r := range rows { if strings.EqualFold(strings.TrimSpace(r.Status), "inactive") { continue } if stored := strings.TrimSpace(r.Contactno); stored != "" { byPhone[strings.ToLower(stored)] = append(byPhone[strings.ToLower(stored)], r.Userid) } } fmt.Printf("till accounts (roleid %d/%d): %d\n\n", models.PosRoleSupervisor, models.PosRoleCashier, len(rows)) fmt.Printf("%-8s %-8s %-9s %-5s %-9s %-8s %-7s %s\n", "userid", "tenant", "location", "role", "status", "number", "pin", "can sign in?") fmt.Println(strings.Repeat("-", 86)) var ready, noPhone, unnormalised, noPin, ambiguous, inactive int for _, r := range rows { stored := strings.TrimSpace(r.Contactno) norm := normalise(stored) phoneState := "missing" switch { case stored == "": phoneState = "missing" case norm == "": phoneState = "unusable" case norm != stored: phoneState = "STORED RAW" default: phoneState = "ok" } pinState := "missing" if r.Pin >= 1000 && r.Pin <= 9999 { pinState = "ok" } else if r.Pin != 0 { pinState = "unusable" } verdict := "yes" switch { case strings.EqualFold(r.Status, "inactive"): verdict, inactive = "no — inactive", inactive+1 case stored == "": verdict, noPhone = "no — no number", noPhone+1 case norm == "": verdict, noPhone = "no — number unusable", noPhone+1 case norm != stored: // The one that looks fine in the console and fails at the counter. verdict, unnormalised = "NO — number stored unnormalised", unnormalised+1 case pinState != "ok": verdict, noPin = "no — no usable PIN", noPin+1 case len(byPhone[strings.ToLower(stored)]) > 1: verdict, ambiguous = "NO — number shared with another till account", ambiguous+1 default: ready++ } fmt.Printf("%-8d %-8d %-9d %-5d %-9s %-8s %-7s %s\n", r.Userid, r.Tenantid, r.Locationid, r.Roleid, r.Status, phoneState, pinState, verdict) } fmt.Printf("\nready to sign in with number + PIN : %d of %d\n", ready, len(rows)) fmt.Printf(" blocked, no/unusable number : %d\n", noPhone) fmt.Printf(" blocked, number stored raw : %d\n", unnormalised) fmt.Printf(" blocked, no usable PIN : %d\n", noPin) fmt.Printf(" blocked, number not unique : %d\n", ambiguous) fmt.Printf(" inactive : %d\n", inactive) // Cross-tenant collisions are the failure creation cannot prevent: // posPhoneTaken scopes uniqueness to one tenant, the login does not scope at // all, so two tenants may each hold a number that neither can then use. fmt.Println("\nnumbers shared by more than one active till account:") found := false for _, users := range byPhone { if len(users) > 1 { found = true fmt.Printf(" userids %v — all refused with \"more than one account uses these sign-in details\"\n", users) } } if !found { fmt.Println(" none") } }