pos login edited with phone number
This commit is contained in:
@@ -97,6 +97,15 @@ func (r *posRepository) CreatePosUser(tenantID, locationID, configID int, req mo
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The number this person signs in with. Optional at the schema level so a
|
||||
// shop can still be provisioned before it has collected them, but the
|
||||
// console asks for it because the till's own sign-in is moving to it —
|
||||
// an account with no number can only ever log in by username.
|
||||
phone, err := normalisePosPhone(req.Contactno)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
password := strings.TrimSpace(req.Password)
|
||||
authname := strings.ToLower(strings.TrimSpace(req.Authname))
|
||||
|
||||
@@ -154,6 +163,20 @@ func (r *posRepository) CreatePosUser(tenantID, locationID, configID int, req mo
|
||||
}
|
||||
}
|
||||
|
||||
// Checked under the same advisory lock as the PIN, and for the same
|
||||
// reason: two supervisors provisioning at once would otherwise both see
|
||||
// the number free and both write it, leaving a login that resolves to
|
||||
// two people and therefore to nobody.
|
||||
if phone != "" {
|
||||
taken, err := posPhoneTaken(tx, tenantID, phone, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if taken {
|
||||
return fmt.Errorf("another till account in this business already signs in with %s", phone)
|
||||
}
|
||||
}
|
||||
|
||||
// Uniqueness is checked against `authname` and `email` together because
|
||||
// the insert below writes the same value to both, and
|
||||
// `app_users_email_unique` is a real constraint — a clash there fails the
|
||||
@@ -208,12 +231,12 @@ func (r *posRepository) CreatePosUser(tenantID, locationID, configID int, req mo
|
||||
if err := tx.Raw(`
|
||||
INSERT INTO app_users
|
||||
(firstname, lastname, authname, email, contactno, password,
|
||||
pin, roleid, configid, tenantid, locationid, status)
|
||||
pin, shiftid, roleid, configid, tenantid, locationid, status)
|
||||
VALUES (?, ?, NULLIF(?, ''), NULLIF(?, ''), NULLIF(?, ''), NULLIF(?, ''),
|
||||
NULLIF(?, 0), ?, ?, ?, ?, 'Active')
|
||||
NULLIF(?, 0), NULLIF(?, 0), ?, ?, ?, ?, 'Active')
|
||||
RETURNING userid`,
|
||||
first, last, authname, authname, strings.TrimSpace(req.Contactno),
|
||||
password, pin, roleID, configID, tenantID, locationID,
|
||||
first, last, authname, authname, phone,
|
||||
password, pin, req.Shiftid, roleID, configID, tenantID, locationID,
|
||||
).Scan(&nextID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -227,7 +250,8 @@ func (r *posRepository) CreatePosUser(tenantID, locationID, configID int, req mo
|
||||
Firstname: first,
|
||||
Lastname: last,
|
||||
Authname: authname,
|
||||
Contactno: strings.TrimSpace(req.Contactno),
|
||||
Contactno: phone,
|
||||
Shiftid: req.Shiftid,
|
||||
Roleid: roleID,
|
||||
Role: models.PosRoleName(roleID),
|
||||
Pin: posPinString(pin),
|
||||
@@ -301,9 +325,27 @@ func (r *posRepository) UpdatePosUser(tenantID, locationID int, req models.PosUs
|
||||
args = append(args, authname)
|
||||
}
|
||||
|
||||
if contactno := strings.TrimSpace(req.Contactno); contactno != "" {
|
||||
// Normalised on the way in, exactly as on create — a number edited to
|
||||
// "+91 98765 43210" would otherwise stop matching the login that reduces
|
||||
// what is typed to ten digits.
|
||||
phone := ""
|
||||
if strings.TrimSpace(req.Contactno) != "" {
|
||||
p, err := normalisePosPhone(req.Contactno)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
phone = p
|
||||
sets = append(sets, "contactno = ?")
|
||||
args = append(args, contactno)
|
||||
args = append(args, phone)
|
||||
}
|
||||
|
||||
// Zero means "not specified" and leaves the shift alone. Clearing one is
|
||||
// therefore not expressible here, which is deliberate: every other field on
|
||||
// this endpoint behaves the same way, and a sentinel that only one field
|
||||
// honours is the kind of asymmetry that gets forgotten.
|
||||
if req.Shiftid > 0 {
|
||||
sets = append(sets, "shiftid = ?")
|
||||
args = append(args, req.Shiftid)
|
||||
}
|
||||
|
||||
if password := strings.TrimSpace(req.Password); password != "" {
|
||||
@@ -335,6 +377,18 @@ func (r *posRepository) UpdatePosUser(tenantID, locationID int, req models.PosUs
|
||||
}
|
||||
}
|
||||
|
||||
// The person being edited is excluded, so re-saving an unchanged number
|
||||
// is not reported as a clash with themselves.
|
||||
if phone != "" {
|
||||
taken, err := posPhoneTaken(tx, tenantID, phone, req.Userid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if taken {
|
||||
return fmt.Errorf("another till account in this business already signs in with %s", phone)
|
||||
}
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(
|
||||
`UPDATE app_users SET %s WHERE userid = ? AND tenantid = ? AND locationid = ?`,
|
||||
strings.Join(sets, ", "))
|
||||
@@ -377,18 +431,38 @@ func (r *posRepository) ListPosUsers(tenantID, locationID int, includeInactive b
|
||||
Pin int64
|
||||
Haspassword bool
|
||||
Status string
|
||||
Shiftid int
|
||||
Shiftname string
|
||||
Shiftstart string
|
||||
Shiftend string
|
||||
}, 0)
|
||||
|
||||
// The shift is LEFT JOINed and matched on the outlet as well as the id.
|
||||
//
|
||||
// `app_users.shiftid` predates this table and points at `ridershifts` for
|
||||
// riders, so the same number means different things depending on the row's
|
||||
// role. Joining on tenant and location too means a rider shift id can never
|
||||
// resolve to a staff shift that happens to share it — an unmatched id just
|
||||
// comes back blank, which is the honest answer for an account created
|
||||
// before shifts existed.
|
||||
query := `
|
||||
SELECT userid,
|
||||
COALESCE(firstname,'') AS firstname, COALESCE(lastname,'') AS lastname,
|
||||
COALESCE(authname,'') AS authname, COALESCE(contactno,'') AS contactno,
|
||||
COALESCE(roleid,0) AS roleid, COALESCE(pin,0) AS pin,
|
||||
(COALESCE(password,'') <> '') AS haspassword,
|
||||
COALESCE(status,'') AS status
|
||||
FROM app_users
|
||||
WHERE tenantid = ? AND locationid = ?
|
||||
AND COALESCE(roleid,0) IN (?, ?)`
|
||||
SELECT a.userid,
|
||||
COALESCE(a.firstname,'') AS firstname, COALESCE(a.lastname,'') AS lastname,
|
||||
COALESCE(a.authname,'') AS authname, COALESCE(a.contactno,'') AS contactno,
|
||||
COALESCE(a.roleid,0) AS roleid, COALESCE(a.pin,0) AS pin,
|
||||
(COALESCE(a.password,'') <> '') AS haspassword,
|
||||
COALESCE(a.status,'') AS status,
|
||||
COALESCE(s.staffshiftid,0) AS shiftid,
|
||||
COALESCE(s.name,'') AS shiftname,
|
||||
COALESCE(s.starttime,'') AS shiftstart,
|
||||
COALESCE(s.endtime,'') AS shiftend
|
||||
FROM app_users a
|
||||
LEFT JOIN staffshifts s
|
||||
ON s.staffshiftid = a.shiftid
|
||||
AND s.tenantid = a.tenantid
|
||||
AND s.locationid = a.locationid
|
||||
WHERE a.tenantid = ? AND a.locationid = ?
|
||||
AND COALESCE(a.roleid,0) IN (?, ?)`
|
||||
params := []interface{}{tenantID, locationID, models.PosRoleSupervisor, models.PosRoleCashier}
|
||||
|
||||
if !includeInactive {
|
||||
@@ -415,6 +489,10 @@ func (r *posRepository) ListPosUsers(tenantID, locationID int, includeInactive b
|
||||
Haspassword: row.Haspassword,
|
||||
Locationid: locationID,
|
||||
Status: row.Status,
|
||||
Shiftid: row.Shiftid,
|
||||
Shiftname: row.Shiftname,
|
||||
Shiftstart: row.Shiftstart,
|
||||
Shiftend: row.Shiftend,
|
||||
})
|
||||
}
|
||||
return users, nil
|
||||
@@ -499,6 +577,64 @@ func posPinTaken(tx *gorm.DB, tenantID, locationID int, pin int64, exceptUser in
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// normalisePosPhone reduces a mobile number to the ten digits stored on the row.
|
||||
//
|
||||
// The till signs in with this, so what is stored and what is typed have to
|
||||
// agree exactly. A number arrives as "+91 98765 43210", "098765-43210" or
|
||||
// "9876543210" depending on who typed it, and matching those as free text means
|
||||
// a cashier who is certain of their own number cannot get in.
|
||||
//
|
||||
// Reduced to digits, then a leading 91 or 0 is dropped once. Anything that is
|
||||
// not ten digits afterwards is refused rather than stored — a number that
|
||||
// cannot be typed back identically is not a credential.
|
||||
func normalisePosPhone(raw string) (string, error) {
|
||||
digits := strings.Map(func(r rune) rune {
|
||||
if r >= '0' && r <= '9' {
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}, raw)
|
||||
|
||||
if digits == "" {
|
||||
return "", nil
|
||||
}
|
||||
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 "", fmt.Errorf("mobile number must be 10 digits; got %q", raw)
|
||||
}
|
||||
return digits, nil
|
||||
}
|
||||
|
||||
// posPhoneTaken reports whether another till account already signs in with this
|
||||
// number.
|
||||
//
|
||||
// Scoped to the tenant rather than the outlet, unlike the PIN check: a PIN is
|
||||
// typed at one counter and only has to be unique there, but a phone number is a
|
||||
// login and must resolve to exactly one person across the whole chain. A person
|
||||
// working two shops of the same tenant is one account, not two.
|
||||
//
|
||||
// Only till roles are counted, matching what the login itself looks at — 34
|
||||
// numbers are already shared among 104 back-office accounts, and a cashier must
|
||||
// not be blocked by a tenant admin who happens to share their number.
|
||||
func posPhoneTaken(tx *gorm.DB, tenantID int, phone string, exceptUser int) (bool, error) {
|
||||
if phone == "" {
|
||||
return false, nil
|
||||
}
|
||||
var count int64
|
||||
err := tx.Raw(`
|
||||
SELECT COUNT(1) FROM app_users
|
||||
WHERE tenantid = ? AND TRIM(COALESCE(contactno,'')) = ? AND userid <> ?
|
||||
AND COALESCE(roleid,0) IN (?, ?)
|
||||
AND LOWER(COALESCE(status,'active')) <> 'inactive'`,
|
||||
tenantID, phone, exceptUser, models.PosRoleSupervisor, models.PosRoleCashier,
|
||||
).Scan(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// validatePosPin checks a PIN is one this schema can store faithfully.
|
||||
func validatePosPin(raw string) (int64, error) {
|
||||
pin := strings.TrimSpace(raw)
|
||||
|
||||
Reference in New Issue
Block a user