Hold web-created staff to the same rules the till applies

Two paths write `app_users`: the console's `tenants/createstaff`, and the
terminal's `/pos/users`. Only one of them checked anything.

`createstaff` wrote whatever it was handed. A cashier could be created there
with PIN "0451" — which a bigint column stores as 451 — and would then type four
digits at the counter and be refused for ever, with nothing on either screen to
explain it. Or with 1234, which live data already has on eleven accounts. Or
with a PIN somebody at the same outlet already had, which attributes a bill to
whichever row is read first. Or with no way to sign in at all.

None of that surfaced where it was caused. It surfaced at a counter, days later,
as "the new person cannot log in".

So the rules move into `ValidateStaffUser`, and both paths use it: a name, a
role that is actually a role, a PIN the schema can hold and nobody guesses
first, and at least one way to sign in. The duplicate-PIN check runs too, when
the row names an outlet.

The handler also stops answering 500 with a body claiming 409. Every one of
these is something the person filling in the form can fix, so it is a 400
carrying the reason.

`GetStaffs` now returns `rolename` alongside `roleid`, so a console can show
"Supervisor" without mapping ids itself — `app_roles` has six rows for four
back-office roles and most accounts carry an id absent from it, so any mapping
written client-side would be wrong.

This is what makes the two role systems one. A supervisor or cashier created
from the web behaves at the till exactly like one created at the till, because
there is now a single definition of what those are.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Suriya
2026-08-06 20:37:28 +05:30
parent f343f4e86e
commit 6a62dbb9f3
5 changed files with 154 additions and 3 deletions

View File

@@ -435,3 +435,57 @@ func splitName(full string) (first, last string) {
}
return parts[0], strings.Join(parts[1:], " ")
}
// ValidateStaffUser applies the till's rules to a staff row from anywhere.
//
// Exported because the web console writes `app_users` too, through
// `tenants/createstaff`, and that path had no validation whatsoever — no PIN
// rules, no role check, no duplicate check. A cashier created there could be
// given "0451", which a bigint column stores as 451, and would then type four
// digits at the counter and be refused for ever with nothing to explain it.
//
// Two paths writing one table drift apart. This is the shared rule set, so a
// person created from a browser and a person created from a till are subject to
// the same constraints and behave the same way at the counter.
//
// Returns the parsed PIN, or an error a caller can show to whoever typed it.
func ValidateStaffUser(user *models.User) (int64, error) {
if strings.TrimSpace(user.Firstname+user.Lastname) == "" {
return 0, fmt.Errorf("a name is required")
}
// Only the roles this platform actually defines. `roleid` 0 is the one that
// matters: it is not a role, it is what a row carries when nobody set one,
// and live data has riders and shop accounts sharing it.
if user.Roleid <= 0 {
return 0, fmt.Errorf("a role is required")
}
pin := int64(user.Pin)
if pin != 0 {
parsed, err := validatePosPin(strconv.FormatInt(pin, 10))
if err != nil {
return 0, err
}
pin = parsed
}
if pin == 0 && strings.TrimSpace(user.Password) == "" {
return 0, fmt.Errorf("set a PIN, a password, or both — otherwise this person cannot sign in")
}
return pin, nil
}
// StaffPinAvailable reports whether a PIN is free at an outlet.
//
// Exported for the same reason as [ValidateStaffUser]: the web console needs
// the check the till already makes. Two people sharing a PIN would attribute a
// bill to whichever row happened to be read first.
func (r *posRepository) StaffPinAvailable(tenantID, locationID int, pin int64, exceptUser int) (bool, error) {
if pin == 0 {
return true, nil
}
taken, err := posPinTaken(r.db, tenantID, locationID, pin, exceptUser)
return !taken, err
}