Files
backend_fiesta/scratch/posloginready/main.go
Suriyakumarvijayanayagam 369e9fc7b4 Add pending POS scratch checks and portfolio notes
Untracked in the working tree before today's work; committed so the
branch carries everything on disk except a stray duplicate
(docs/MOBILE_ORDER_VERIFICATION copy.md).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 17:04:34 +05:30

167 lines
5.1 KiB
Go

// 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")
}
}