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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user