guide changes
This commit is contained in:
@@ -22,8 +22,11 @@ type TenantRepository interface {
|
||||
UpdateLocation(input models.Tenantlocations) error
|
||||
CreateLocation(data models.Tenantlocations) error
|
||||
DeleteLocation(locationid int, tenantid int) error
|
||||
UpdateTenantProfile(tenantID int, fields map[string]any) error
|
||||
UpdateOwnProfile(userID, tenantID int, fields map[string]any) error
|
||||
GetStaffs(tid int) ([]models.StaffInfo, error)
|
||||
CreateStaff(user models.User) error
|
||||
AssignStaffToBranch(tenantID, userID, locationID int) error
|
||||
UpdateStaff(user models.User) error
|
||||
CreateTenantLocation(data models.Tenantlocations) (models.Tenantlocations, error)
|
||||
UpdateTenantLocation(data models.Tenantlocations) error
|
||||
@@ -312,6 +315,18 @@ func (r *tenantRepository) CreateLocation(data models.Tenantlocations) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetStaffs lists a merchant's people, INCLUDING the ones not yet given a
|
||||
// branch.
|
||||
//
|
||||
// The join was INNER, which excluded exactly the state this list exists to
|
||||
// show. A person hired before their outlet opens — or moved off a branch, or
|
||||
// created and not yet placed — has `locationid` 0, matches no `tenantlocations`
|
||||
// row, and vanished from the only screen that could assign them one. They could
|
||||
// sign in (and were met with "No store assigned"), they simply could not be
|
||||
// seen by the person able to fix it.
|
||||
//
|
||||
// LEFT, and unassigned first: they are the ones needing an action, and a list
|
||||
// sorted by branch buries them under everybody already settled.
|
||||
func (r *tenantRepository) GetStaffs(tid int) ([]models.StaffInfo, error) {
|
||||
var data []models.StaffInfo
|
||||
|
||||
@@ -323,10 +338,11 @@ func (r *tenantRepository) GetStaffs(tid int) ([]models.StaffInfo, error) {
|
||||
b.locationname,
|
||||
COALESCE(c.rolename,'') AS rolename
|
||||
FROM app_users a
|
||||
INNER JOIN tenantlocations b ON a.locationid = b.locationid
|
||||
LEFT JOIN tenantlocations b ON a.locationid = b.locationid
|
||||
LEFT JOIN app_roles c ON c.roleid = a.roleid
|
||||
WHERE a.tenantid = ?
|
||||
AND COALESCE(a.roleid, 0) NOT IN (7, 8)`
|
||||
AND COALESCE(a.roleid, 0) NOT IN (7, 8)
|
||||
ORDER BY a.locationid NULLS FIRST, a.userid DESC`
|
||||
|
||||
if err := r.db.Raw(q1, tid).Scan(&data).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -390,6 +406,21 @@ func (r *tenantRepository) CreateTenantLocation(data models.Tenantlocations) (mo
|
||||
data.Status = "Active"
|
||||
}
|
||||
|
||||
// An outlet nobody can sign in to is a dead end, and a silent one — it
|
||||
// appears in every list and every branch picker, and the first person to
|
||||
// notice is whoever is standing in the shop.
|
||||
//
|
||||
// So a branch must arrive with an operator, one way or the other: an
|
||||
// existing person named in Operatorid, or an email to spawn a login from.
|
||||
// Neither used to be checked, and a create with a blank email produced an
|
||||
// account whose authname was the empty string — a row that can never
|
||||
// authenticate.
|
||||
if data.Operatorid <= 0 && strings.TrimSpace(data.Email) == "" {
|
||||
tx.Rollback()
|
||||
return models.Tenantlocations{}, errors.New(
|
||||
"a branch needs somebody to run it: name an existing user in operatorid, or give an email to create a login from")
|
||||
}
|
||||
|
||||
// Step 1: Insert into tenantlocations. GORM writes the DB-assigned
|
||||
// locationid back onto data, which callers need to build the store's
|
||||
// QR code (payload is just {tenantid, locationid}) right after onboarding.
|
||||
@@ -398,7 +429,42 @@ func (r *tenantRepository) CreateTenantLocation(data models.Tenantlocations) (mo
|
||||
return models.Tenantlocations{}, err
|
||||
}
|
||||
|
||||
// Step 2: Insert into app_users
|
||||
// Step 2a: bind an existing person, when one was named.
|
||||
//
|
||||
// Scoped to this tenant in the WHERE clause rather than checked first: a
|
||||
// userid belonging to another merchant then matches no row, and the branch
|
||||
// is refused rather than handed to a stranger. Doing it as one guarded
|
||||
// UPDATE also means the check and the write cannot drift apart under a
|
||||
// concurrent reassignment.
|
||||
if data.Operatorid > 0 {
|
||||
res := tx.Table("app_users").
|
||||
Where("userid = ? AND tenantid = ? AND COALESCE(roleid, 0) NOT IN (7, 8)",
|
||||
data.Operatorid, data.Tenantid).
|
||||
Updates(map[string]any{"locationid": data.Locationid})
|
||||
if res.Error != nil {
|
||||
tx.Rollback()
|
||||
return models.Tenantlocations{}, res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
// Either the person does not exist, belongs to another merchant, or
|
||||
// is a till account. All three are the same answer to the caller,
|
||||
// and none of them should leave a branch standing.
|
||||
tx.Rollback()
|
||||
return models.Tenantlocations{}, fmt.Errorf(
|
||||
"user %d cannot run this branch — they belong to another business, do not exist, or are a till account",
|
||||
data.Operatorid)
|
||||
}
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return models.Tenantlocations{}, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Step 2b: no person named — spawn a login, as before.
|
||||
//
|
||||
// Kept so every existing caller behaves exactly as it did. The account it
|
||||
// makes is named after the shop and sits on the shop's email, which is why
|
||||
// naming a real person above is the better path where the caller has one.
|
||||
user.Authname = data.Email
|
||||
user.Firstname = data.Locationname
|
||||
user.Email = data.Email
|
||||
@@ -754,3 +820,104 @@ func (r *tenantRepository) GetTenantByKeyword(keyword string) ([]models.TenantSe
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// AssignStaffToBranch moves one of a merchant's people to a branch, or takes
|
||||
// them off one.
|
||||
//
|
||||
// `locationid` of 0 unassigns — a real state, not a missing value. Somebody
|
||||
// leaves a shop before the next one opens, and the alternative to holding them
|
||||
// unassigned is deleting the account and losing who did what.
|
||||
//
|
||||
// Both the person and the branch are checked against the tenant IN THE QUERY
|
||||
// rather than beforehand. A userid from another merchant then matches no row
|
||||
// and the call fails, instead of one business quietly moving another's staff —
|
||||
// and the check cannot drift from the write under a concurrent edit.
|
||||
//
|
||||
// Till accounts (roleids 7 and 8) are excluded for the same reason GetStaffs
|
||||
// hides them: they are POS people with no back-office screen, and their branch
|
||||
// is managed by the till console, not here.
|
||||
func (r *tenantRepository) AssignStaffToBranch(tenantID, userID, locationID int) error {
|
||||
if locationID > 0 {
|
||||
var owned int64
|
||||
if err := r.db.Raw(
|
||||
`SELECT COUNT(1) FROM tenantlocations WHERE locationid = ? AND tenantid = ?`,
|
||||
locationID, tenantID).Scan(&owned).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if owned == 0 {
|
||||
return fmt.Errorf("branch %d does not belong to this business", locationID)
|
||||
}
|
||||
}
|
||||
|
||||
res := r.db.Table("app_users").
|
||||
Where("userid = ? AND tenantid = ? AND COALESCE(roleid, 0) NOT IN (7, 8)",
|
||||
userID, tenantID).
|
||||
Updates(map[string]any{"locationid": locationID})
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return fmt.Errorf(
|
||||
"user %d is not one of this business's people", userID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateTenantProfile writes a merchant's own business record.
|
||||
//
|
||||
// The FIRST write path this table has ever had. Every field on `tenants` was
|
||||
// set once at onboarding by a Nearle Admin and could never be changed again, by
|
||||
// anybody — which is why, across 200 merchants, 18 had a shop photograph and
|
||||
// none had a licence number.
|
||||
//
|
||||
// `fields` has already been reduced to the merchant-editable columns by
|
||||
// services.TenantProfileUpdate. This deliberately does not take a struct: GORM
|
||||
// would then decide what to write from which values happen to be non-zero, and
|
||||
// the set of columns a merchant may touch would be implied by a form rather
|
||||
// than stated anywhere.
|
||||
func (r *tenantRepository) UpdateTenantProfile(tenantID int, fields map[string]any) error {
|
||||
if tenantID <= 0 {
|
||||
return errors.New("tenantid is required")
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
return errors.New("nothing to update")
|
||||
}
|
||||
res := r.db.Table("tenants").Where("tenantid = ?", tenantID).Updates(fields)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return fmt.Errorf("no business with tenantid %d", tenantID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateOwnProfile writes the fields a person owns about themselves.
|
||||
//
|
||||
// Scoped by userid AND tenantid together, in the WHERE clause. `UpdateStaff`
|
||||
// checks only the userid, so a request naming somebody else's account is
|
||||
// carried out — which is survivable while the only caller is an admin screen,
|
||||
// and is not once a person can edit their own profile.
|
||||
//
|
||||
// `fields` has already been reduced by services.OwnProfileUpdate to identity
|
||||
// columns. Role, branch, tenant, status, password and PIN are not in it: this
|
||||
// table keeps who-you-are next to what-you-may-do, and only the first half
|
||||
// belongs to the person.
|
||||
func (r *tenantRepository) UpdateOwnProfile(userID, tenantID int, fields map[string]any) error {
|
||||
if userID <= 0 || tenantID <= 0 {
|
||||
return errors.New("userid and tenantid are both required")
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
return errors.New("nothing to update")
|
||||
}
|
||||
res := r.db.Table("app_users").
|
||||
Where("userid = ? AND tenantid = ?", userID, tenantID).
|
||||
Updates(fields)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return fmt.Errorf("no account %d in this business", userID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user