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>
167 lines
5.1 KiB
Go
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")
|
|
}
|
|
}
|