959 lines
30 KiB
Go
959 lines
30 KiB
Go
package repositories
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"nearle/models"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/jinzhu/copier"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type TenantRepository interface {
|
|
SearchTenant(status, searchstr string) ([]models.Tenantinfo, error)
|
|
GetAllTenants(pageno, pagesize, aid int, status, tenanttype, keyword string) ([]models.Tenantinfo, error)
|
|
GetTenantLocations(tid int) ([]models.Tenantlocations, error)
|
|
GetTenantSlot() (models.Tenantslot, error)
|
|
CreateTenantCustomer(customer models.Tenantcustomer) (*models.Tenantcustomer, error)
|
|
GetCustomerTenants(customerID int, categoryID int, tenantFlag int) ([]models.TenantInfo, error)
|
|
GetTenantPricing(tid, aid int) (*models.Tenantpricing, error)
|
|
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
|
|
CheckTenantByNo(cno string) int
|
|
CreateTenantUser(data models.Tenants) (bool, error)
|
|
GetUserByNo(cno string) models.UserInfo
|
|
GetTenantByID(tid int, locationid int, userid int) (models.Tenantinfo, error)
|
|
GetTenantByKeyword(keyword string) ([]models.TenantSearch, error)
|
|
}
|
|
|
|
type tenantRepository struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
func NewTenantRepository(db *gorm.DB) TenantRepository {
|
|
return &tenantRepository{db: db}
|
|
}
|
|
|
|
func (r *tenantRepository) SearchTenant(status, keyword string) ([]models.Tenantinfo, error) {
|
|
var data []models.Tenantinfo
|
|
var query string
|
|
|
|
searchStr := strings.ToLower(keyword)
|
|
|
|
if strings.ToLower(status) != "pending" {
|
|
query = `
|
|
SELECT a.*, b.subcategoryname, c.firstname, c.lastname,
|
|
CONCAT(c.firstname, ' ', c.lastname) AS accountname
|
|
FROM tenants a
|
|
INNER JOIN app_subcategory b ON a.subcategoryid = b.subcategoryid
|
|
LEFT JOIN app_users c ON c.userid = a.partneruserid
|
|
WHERE a.approved = 1
|
|
AND LOWER(a.status) = ?
|
|
AND LOWER(a.tenantname) LIKE ?
|
|
`
|
|
r.db.Raw(query, strings.ToLower(status), searchStr+"%").Scan(&data)
|
|
} else {
|
|
query = `
|
|
SELECT a.*, b.subcategoryname, c.firstname, c.lastname,
|
|
CONCAT(c.firstname, ' ', c.lastname) AS accountname
|
|
FROM tenants a
|
|
INNER JOIN app_subcategory b ON a.subcategoryid = b.subcategoryid
|
|
LEFT JOIN app_users c ON c.userid = a.partneruserid
|
|
WHERE a.approved = 0
|
|
AND LOWER(a.tenantname) LIKE ?
|
|
`
|
|
r.db.Raw(query, searchStr+"%").Scan(&data)
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func (r *tenantRepository) GetAllTenants(pageno, pagesize, aid int, status, tenanttype, keyword string) ([]models.Tenantinfo, error) {
|
|
offset := (pageno - 1) * pagesize
|
|
|
|
var data []models.Tenantinfo
|
|
|
|
base := `SELECT * FROM tenants a WHERE 1 = 1`
|
|
|
|
var (
|
|
conds []string
|
|
params []interface{}
|
|
)
|
|
|
|
switch strings.ToLower(status) {
|
|
case "active":
|
|
conds = append(conds, "a.approved = 1 AND a.status = 'Active'")
|
|
case "inactive":
|
|
conds = append(conds, "a.approved = 1 AND a.status = 'InActive'")
|
|
case "pending":
|
|
conds = append(conds, "a.approved = 0")
|
|
}
|
|
|
|
if aid != 0 {
|
|
conds = append(conds, "a.applocationid = ?")
|
|
params = append(params, aid)
|
|
}
|
|
|
|
if tenanttype != "" {
|
|
conds = append(conds, "a.tenanttype = ?")
|
|
params = append(params, tenanttype)
|
|
}
|
|
|
|
if keyword != "" {
|
|
|
|
kw := "%" + strings.ToLower(keyword) + "%"
|
|
|
|
conds = append(conds,
|
|
"(LOWER(a.tenantname) LIKE ? OR LOWER(a.primarycontact) LIKE ?)")
|
|
|
|
params = append(params, kw, kw)
|
|
}
|
|
|
|
if len(conds) > 0 {
|
|
base += " AND " + strings.Join(conds, " AND ")
|
|
}
|
|
|
|
base += " ORDER BY a.tenantid DESC LIMIT ? OFFSET ?"
|
|
params = append(params, pagesize, offset)
|
|
|
|
err := r.db.Raw(base, params...).Scan(&data).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func (r *tenantRepository) GetTenantLocations(tid int) ([]models.Tenantlocations, error) {
|
|
var data []models.Tenantlocations
|
|
|
|
q1 := `SELECT * FROM tenantlocations WHERE tenantid = ?`
|
|
|
|
if err := r.db.Raw(q1, tid).Find(&data).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i := range data {
|
|
data[i].Status = strings.ToLower(data[i].Status)
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func (r *tenantRepository) GetTenantSlot() (models.Tenantslot, error) {
|
|
var data models.Tenantslot
|
|
|
|
err := r.db.Raw(`SELECT * FROM tenantslot`).Find(&data).Error
|
|
|
|
if err != nil {
|
|
return models.Tenantslot{}, err
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func (r *tenantRepository) CreateTenantCustomer(customer models.Tenantcustomer) (*models.Tenantcustomer, error) {
|
|
var existing models.Tenantcustomer
|
|
|
|
// 🔍 Step 1: Check if a record already exists with same customerid and locationid
|
|
err := r.db.
|
|
Where("customerid = ? AND locationid = ?", customer.CustomerID, customer.LocationID).
|
|
First(&existing).Error
|
|
|
|
// If record found, prevent insertion
|
|
if err == nil {
|
|
return nil, fmt.Errorf("customer already exists for this location")
|
|
}
|
|
|
|
// If error other than record not found, return it
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, err
|
|
}
|
|
|
|
// ✅ Step 2: Insert new record if no duplicate found
|
|
if err := r.db.Create(&customer).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &customer, nil
|
|
}
|
|
|
|
func (r *tenantRepository) GetCustomerTenants(customerID int, categoryID int, tenantFlag int) ([]models.TenantInfo, error) {
|
|
var tenants []models.TenantInfo
|
|
|
|
query := `
|
|
SELECT a.customerid, a.locationid, b.tenantid,b.tenantname,b.address,b.licenseno,
|
|
b.primaryemail,b.primarycontact,b.applocationid,b.suburb,b.city,
|
|
b.latitude,b.longitude,b.postcode,b.tenantimage,b.subcategoryid,
|
|
b.categoryid,b.registrationno,d.userfcmtoken,c.locationname,
|
|
COALESCE(o.orderscount, 0) AS orderscount
|
|
FROM tenantcustomers a
|
|
LEFT JOIN tenants b ON a.tenantid = b.tenantid
|
|
INNER JOIN tenantlocations c ON a.locationid = c.locationid
|
|
LEFT JOIN (
|
|
SELECT tenantid, customerid, COUNT(*) AS orderscount
|
|
FROM orders
|
|
GROUP BY tenantid, customerid
|
|
) o ON b.tenantid = o.tenantid AND o.customerid = a.customerid
|
|
LEFT JOIN (
|
|
SELECT locationid,
|
|
MAX(userfcmtoken) AS userfcmtoken
|
|
FROM app_users
|
|
GROUP BY locationid
|
|
) d ON d.locationid = a.locationid
|
|
WHERE a.customerid = ?
|
|
AND b.tenantid IS NOT NULL
|
|
`
|
|
|
|
args := []interface{}{customerID}
|
|
|
|
if categoryID != 0 {
|
|
query += " AND b.categoryid = ?"
|
|
args = append(args, categoryID)
|
|
}
|
|
|
|
if tenantFlag == 1 {
|
|
query += " AND COALESCE(o.orderscount,0) > 0"
|
|
}
|
|
|
|
if err := r.db.Raw(query, args...).Scan(&tenants).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if tenants == nil {
|
|
return []models.TenantInfo{}, nil
|
|
}
|
|
|
|
// Attach top 5 subcategories
|
|
if len(tenants) > 0 {
|
|
var subcategories []models.ProductSubcategory
|
|
if err := r.db.Table("productsubcategories").
|
|
Order("subcatid ASC").
|
|
Limit(5).
|
|
Find(&subcategories).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i := range tenants {
|
|
tenants[i].ProductSubcategory = subcategories
|
|
}
|
|
}
|
|
|
|
print(tenants)
|
|
|
|
return tenants, nil
|
|
}
|
|
|
|
func (r *tenantRepository) GetTenantPricing(tid, aid int) (*models.Tenantpricing, error) {
|
|
var data models.Tenantpricing
|
|
var q1 string
|
|
|
|
if tid != 0 {
|
|
q1 = `SELECT *
|
|
FROM tenantpricing
|
|
WHERE pricingdate = (
|
|
SELECT MAX(pricingdate)
|
|
FROM tenantpricing
|
|
WHERE tenantid=` + strconv.Itoa(tid) + `
|
|
)
|
|
AND tenantid=?
|
|
ORDER BY tenantpricingid DESC`
|
|
}
|
|
|
|
if err := r.db.Raw(q1, tid).Find(&data).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &data, nil
|
|
}
|
|
|
|
func (r *tenantRepository) UpdateLocation(input models.Tenantlocations) error {
|
|
tx := r.db.Begin()
|
|
|
|
if err := tx.Where("locationid=?", input.Locationid).Updates(&input).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
|
|
if err := tx.Commit().Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *tenantRepository) DeleteLocation(locationid int, tenantid int) error {
|
|
tx := r.db.Begin()
|
|
|
|
if err := tx.Where("locationid=? AND tenantid=?", locationid, tenantid).Delete(&models.Tenantlocations{}).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
|
|
if err := tx.Commit().Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *tenantRepository) CreateLocation(data models.Tenantlocations) error {
|
|
if err := r.db.Create(&data).Error; err != nil {
|
|
return err
|
|
}
|
|
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
|
|
|
|
q1 := `SELECT a.userid,a.firstname,a.lastname,
|
|
CONCAT(a.firstname,' ',a.lastname) AS fullname,
|
|
a.email,a.contactno,a.address,a.suburb,a.city,
|
|
a.state,a.postcode,a.userfcmtoken,a.pin,a.applocationid,
|
|
a.roleid,a.partnerid,a.tenantid,a.locationid,
|
|
b.locationname,
|
|
COALESCE(c.rolename,'') AS rolename
|
|
FROM app_users a
|
|
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)
|
|
ORDER BY a.locationid NULLS FIRST, a.userid DESC`
|
|
|
|
if err := r.db.Raw(q1, tid).Scan(&data).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
// CreateStaff adds a person to a shop from the web console.
|
|
//
|
|
// Now subject to the same rules the till applies — see ValidateStaffUser. This
|
|
// wrote whatever it was handed, so a cashier could be created with a PIN the
|
|
// schema cannot store, a PIN somebody else already has, or no way to sign in at
|
|
// all. The failure surfaced at the counter rather than on the screen that
|
|
// caused it.
|
|
//
|
|
// `userid` is deliberately not set: it is a `GENERATED BY DEFAULT AS IDENTITY`
|
|
// column and Postgres allocates it. Computing one here would leave the sequence
|
|
// unadvanced and two allocators racing each other.
|
|
func (r *tenantRepository) CreateStaff(user models.User) error {
|
|
pin, err := ValidateStaffUser(&user)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
user.Pin = int(pin)
|
|
|
|
if pin > 0 && user.Tenantid > 0 && user.Locationid > 0 {
|
|
taken, err := posPinTaken(r.db, user.Tenantid, user.Locationid, pin, user.Userid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if taken {
|
|
return fmt.Errorf("another person at this outlet already uses that PIN")
|
|
}
|
|
}
|
|
|
|
if err := r.db.Table("app_users").Create(&user).Error; err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *tenantRepository) UpdateStaff(user models.User) error {
|
|
if err := r.db.Table("app_users").Where("userid = ?", user.Userid).Updates(&user).Error; err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *tenantRepository) CreateTenantLocation(data models.Tenantlocations) (models.Tenantlocations, error) {
|
|
var user models.Tenantuser
|
|
tx := r.db.Begin()
|
|
|
|
// Default to Active if the caller didn't specify — matches
|
|
// tenantlocations' own gorm default and the primary-location behavior
|
|
// from tenant onboarding. Forcing InActive here used to also block the
|
|
// spawned manager login (AppLogin checks account status before it ever
|
|
// gets to the "no password set" branch), so a new store's login could
|
|
// never reach the password-setup screen.
|
|
if data.Status == "" {
|
|
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.
|
|
if err := tx.Create(&data).Error; err != nil {
|
|
tx.Rollback()
|
|
return models.Tenantlocations{}, err
|
|
}
|
|
|
|
// 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
|
|
user.Contactno = data.Contactno
|
|
user.Address = data.Address
|
|
user.Suburb = data.Suburb
|
|
user.City = data.City
|
|
user.State = data.State
|
|
user.Postcode = data.Postcode
|
|
user.Partnerid = data.Partnerid
|
|
user.Tenantid = data.Tenantid
|
|
user.Locationid = data.Locationid
|
|
user.Applocationid = data.Applocationid
|
|
user.Configid = 1
|
|
user.Status = data.Status
|
|
user.Roleid = 0
|
|
user.Authmode = 0
|
|
user.Password = ""
|
|
user.Dialcode = "+91"
|
|
|
|
if err := tx.Table("app_users").Create(&user).Error; err != nil {
|
|
tx.Rollback()
|
|
return models.Tenantlocations{}, err
|
|
}
|
|
|
|
// Commit
|
|
if err := tx.Commit().Error; err != nil {
|
|
return models.Tenantlocations{}, err
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func (r *tenantRepository) UpdateTenantLocation(input models.Tenantlocations) error {
|
|
tx := r.db.Begin()
|
|
|
|
// ✅ Step 1: Prepare map for tenantlocations update
|
|
locationUpdate := make(map[string]interface{})
|
|
|
|
if input.Locationname != "" {
|
|
locationUpdate["locationname"] = input.Locationname
|
|
}
|
|
if input.Email != "" {
|
|
locationUpdate["email"] = input.Email
|
|
}
|
|
if input.Contactno != "" {
|
|
locationUpdate["contactno"] = input.Contactno
|
|
}
|
|
if input.Address != "" {
|
|
locationUpdate["address"] = input.Address
|
|
}
|
|
if input.Suburb != "" {
|
|
locationUpdate["suburb"] = input.Suburb
|
|
}
|
|
if input.City != "" {
|
|
locationUpdate["city"] = input.City
|
|
}
|
|
if input.State != "" {
|
|
locationUpdate["state"] = input.State
|
|
}
|
|
if input.Postcode != "" {
|
|
locationUpdate["postcode"] = input.Postcode
|
|
}
|
|
if input.Partnerid != 0 {
|
|
locationUpdate["partnerid"] = input.Partnerid
|
|
}
|
|
if input.Tenantid != 0 {
|
|
locationUpdate["tenantid"] = input.Tenantid
|
|
}
|
|
if input.Applocationid != 0 {
|
|
locationUpdate["applocationid"] = input.Applocationid
|
|
}
|
|
if input.Status != "" {
|
|
locationUpdate["status"] = input.Status
|
|
}
|
|
|
|
// ✅ Step 2: Update tenantlocations (only provided fields)
|
|
if len(locationUpdate) > 0 {
|
|
if err := tx.Table("tenantlocations").
|
|
Where("locationid = ?", input.Locationid).
|
|
Updates(locationUpdate).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
|
|
// ✅ Step 3: Prepare map for app_users update (only matching fields)
|
|
userUpdate := make(map[string]interface{})
|
|
|
|
if input.Locationname != "" {
|
|
userUpdate["firstname"] = input.Locationname
|
|
}
|
|
if input.Email != "" {
|
|
userUpdate["email"] = input.Email
|
|
}
|
|
if input.Contactno != "" {
|
|
userUpdate["contactno"] = input.Contactno
|
|
}
|
|
if input.Address != "" {
|
|
userUpdate["address"] = input.Address
|
|
}
|
|
if input.Suburb != "" {
|
|
userUpdate["suburb"] = input.Suburb
|
|
}
|
|
if input.City != "" {
|
|
userUpdate["city"] = input.City
|
|
}
|
|
if input.State != "" {
|
|
userUpdate["state"] = input.State
|
|
}
|
|
if input.Postcode != "" {
|
|
userUpdate["postcode"] = input.Postcode
|
|
}
|
|
if input.Tenantid != 0 {
|
|
userUpdate["tenantid"] = input.Tenantid
|
|
}
|
|
if input.Partnerid != 0 {
|
|
userUpdate["partnerid"] = input.Partnerid
|
|
}
|
|
if input.Applocationid != 0 {
|
|
userUpdate["applocationid"] = input.Applocationid
|
|
}
|
|
if input.Status != "" {
|
|
userUpdate["status"] = input.Status
|
|
}
|
|
// ✅ Step 4: Update app_users (only provided fields)
|
|
if len(userUpdate) > 0 {
|
|
if err := tx.Table("app_users").
|
|
Where("locationid = ?", input.Locationid).
|
|
Updates(userUpdate).Error; err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
}
|
|
|
|
// ✅ Commit transaction
|
|
if err := tx.Commit().Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ✅ Check if tenant exists
|
|
func (r *tenantRepository) CheckTenantByNo(cno string) int {
|
|
var id int
|
|
q1 := "SELECT tenantid FROM tenants WHERE primarycontact = '" + cno + `'`
|
|
r.db.Raw(q1).Find(&id)
|
|
return id
|
|
}
|
|
|
|
// ✅ Create tenant + user + customer records
|
|
func (r *tenantRepository) CreateTenantUser(data models.Tenants) (bool, error) {
|
|
var seq models.Ordersequences
|
|
var user models.User
|
|
var cust models.Customers
|
|
var custloc models.Customerlocations
|
|
var tcust models.Tenantcustomers
|
|
|
|
tx := r.db.Begin()
|
|
|
|
// Step 1: Insert into tenants
|
|
if err := tx.Create(&data).Error; err != nil {
|
|
tx.Rollback()
|
|
return false, errors.New("error in tenant creation")
|
|
}
|
|
|
|
// Step 2: Create order sequence
|
|
seq.Tenantid = data.Tenantid
|
|
if err := tx.Table("ordersequences").Create(&seq).Error; err != nil {
|
|
tx.Rollback()
|
|
return false, errors.New("error in sequence")
|
|
}
|
|
|
|
// Step 3: Create app_user
|
|
if err := copier.Copy(&user, &data); err != nil {
|
|
tx.Rollback()
|
|
return false, err
|
|
}
|
|
user.Userfcmtoken = data.Tenanttoken
|
|
user.Contactno = data.Primarycontact
|
|
user.Email = data.Primaryemail
|
|
user.Authname = data.Primaryemail
|
|
user.Deviceid = data.Deviceid
|
|
user.Tenantid = data.Tenantid
|
|
user.Locationid = data.Tenantlocations.Locationid
|
|
// A merchant's own administrator is an ADMIN (3), not a Super admin (1).
|
|
//
|
|
// This wrote 1, and `app_roles` calls 1 "Super admin" — so every shop on the
|
|
// platform was provisioned with an account that reads as a platform
|
|
// operator on its own Users & access screen. It never had platform access:
|
|
// that is `app_users.issuperadmin`, a separate column the console checks
|
|
// first, and no store account has it set. But a label saying "Super admin"
|
|
// on a merchant's staff list is a thing somebody will eventually act on.
|
|
//
|
|
// 3 also matches the old console's ladder, which this one is meant to
|
|
// follow: Super Admin = platform, Admin = the merchant group, Manager (4) =
|
|
// a single store. `rmartuser` and `Kmartuser` — the store users there — are
|
|
// both roleid 4.
|
|
//
|
|
// Existing tenants keep roleid 1 and keep working: `resolveRole` still
|
|
// treats 1 and 3 alike, deliberately, because changing that would lock out
|
|
// every merchant provisioned before today.
|
|
user.Roleid = 3
|
|
// The onboarding form never sends a tenant configid, so copier.Copy left
|
|
// this at zero — AppLogin's GetUserByAuthname always queries configid=1
|
|
// for the web login, so a zero here makes the account permanently
|
|
// unfindable by email no matter what's typed.
|
|
user.Configid = 1
|
|
|
|
if err := tx.Table("app_users").Create(&user).Error; err != nil {
|
|
tx.Rollback()
|
|
return false, errors.New("error in user creation")
|
|
}
|
|
|
|
// Step 4: Create / Update customers
|
|
cust.Configid = data.Configid
|
|
cust.Firstname = data.Tenantname
|
|
cust.Email = data.Primaryemail
|
|
cust.Contactno = data.Primarycontact
|
|
cust.Deviceid = data.Deviceid
|
|
cust.Devicetype = data.Devicetype
|
|
cust.Customertoken = data.Tenanttoken
|
|
cust.Profileimage = data.Tenantimage
|
|
cust.Address = data.Address
|
|
cust.Suburb = data.Suburb
|
|
cust.City = data.City
|
|
cust.State = data.State
|
|
cust.Postcode = data.Postcode
|
|
cust.Applocationid = data.Applocationid
|
|
cust.Latitude = data.Latitude
|
|
cust.Longitude = data.Longitude
|
|
cust.Primaryaddress = 1
|
|
|
|
cid := r.CheckCustomer(data.Primarycontact)
|
|
|
|
if cid == 0 {
|
|
if err := tx.Table("customers").Create(&cust).Error; err != nil {
|
|
tx.Rollback()
|
|
return false, errors.New("error in customer creation")
|
|
}
|
|
|
|
if err := copier.Copy(&custloc, &cust); err != nil {
|
|
tx.Rollback()
|
|
return false, err
|
|
}
|
|
|
|
if err := tx.Table("customerlocations").Create(&custloc).Error; err != nil {
|
|
tx.Rollback()
|
|
return false, errors.New("error in customer location")
|
|
}
|
|
} else {
|
|
if err := tx.Table("customers").Where("customerid=?", cid).Updates(&cust).Error; err != nil {
|
|
tx.Rollback()
|
|
return false, errors.New("error updating customer")
|
|
}
|
|
|
|
if err := copier.Copy(&custloc, &cust); err != nil {
|
|
tx.Rollback()
|
|
return false, err
|
|
}
|
|
|
|
if err := tx.Table("customerlocations").Where("customerid=?", cid).Updates(&custloc).Error; err != nil {
|
|
tx.Rollback()
|
|
return false, errors.New("error updating customer location")
|
|
}
|
|
}
|
|
|
|
// Step 5: Create tenant-customer link
|
|
tcust.Customerid = cust.Customerid
|
|
tcust.Tenantid = data.Tenantid
|
|
tcust.Locationid = data.Tenantlocations.Locationid
|
|
|
|
if err := tx.Table("tenantcustomers").Create(&tcust).Error; err != nil {
|
|
tx.Rollback()
|
|
return false, errors.New("error in tenant customer")
|
|
}
|
|
|
|
// ✅ Commit transaction
|
|
if err := tx.Commit().Error; err != nil {
|
|
return false, errors.New("error in tenant creation")
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// ✅ Check if customer exists
|
|
func (r *tenantRepository) CheckCustomer(cno string) int {
|
|
var id int
|
|
q := "SELECT customerid FROM customers WHERE contactno = '" + cno + `'`
|
|
r.db.Raw(q).Find(&id)
|
|
return id
|
|
}
|
|
|
|
// ✅ Get user by contact number
|
|
func (r *tenantRepository) GetUserByNo(cno string) models.UserInfo {
|
|
var user models.UserInfo
|
|
|
|
q1 := `SELECT a.userid,a.authname,a.email,a.configid,a.roleid,a.authmode,a.contactno,
|
|
a.firstname,a.lastname,CONCAT(a.firstname,' ',a.lastname) AS fullname,
|
|
a.userfcmtoken,a.pin,a.deviceid,a.devicetype,a.tenantid,a.locationid,
|
|
b.partnerid,b.moduleid,b.categoryid,b.subcategoryid,
|
|
b.applocationid,b.tenantname,b.address AS tenantaddress,b.state AS tenantstate,b.city AS tenantcity,
|
|
b.postcode AS tenantpostcode,b.latitude AS tenantlat,b.longitude AS tenantlong
|
|
FROM app_users a
|
|
LEFT JOIN tenants b ON a.tenantid = b.tenantid
|
|
WHERE a.contactno = '` + cno + `'`
|
|
|
|
r.db.Raw(q1).Find(&user)
|
|
return user
|
|
}
|
|
|
|
// GetTenantByID reads one business.
|
|
//
|
|
// Both master joins are LEFT, and that is the whole fix. They were INNER —
|
|
// `app_category` on `a.categoryid` and `app_location` on `a.applocationid` — so
|
|
// a tenant whose category or city master row is missing did not come back
|
|
// "without a category name", it did not come back AT ALL. The endpoint answered
|
|
// 200 with an all-zero record: tenantid 0, every string empty.
|
|
//
|
|
// Four of two hundred tenants have `categoryid = 0`, which matches no
|
|
// `app_category` row, and a newly onboarded shop is the likeliest to be one of
|
|
// them. The damage was silent and total: the shop-profile screen seeded an
|
|
// empty form over a business that had an address, and the setup walkthrough
|
|
// read every field as blank forever — so its first step could never complete
|
|
// however many times somebody saved it.
|
|
//
|
|
// The same INNER-JOIN-as-a-filter mistake has been fixed twice before in this
|
|
// file's neighbours, in GetStaffs and GetUserById. A master row is context; its
|
|
// absence must never delete the record it decorates.
|
|
func (r *tenantRepository) GetTenantByID(tid int, locationid int, userid int) (models.Tenantinfo, error) {
|
|
var data models.Tenantinfo
|
|
|
|
if locationid == 0 && userid > 0 {
|
|
var userLocationID int
|
|
err := r.db.Table("app_users").Select("locationid").Where("userid = ?", userid).Row().Scan(&userLocationID)
|
|
if err == nil && userLocationID > 0 {
|
|
locationid = userLocationID
|
|
}
|
|
}
|
|
|
|
q1 := `
|
|
SELECT a.*,b.categoryname,c.locationname AS applocation, d.allocationid AS allocationmode,e.typename AS allocationtype,e.mapid AS allocationid,f.locationid,
|
|
f.locationname, f.contactno as locationcontact
|
|
FROM tenants a
|
|
LEFT JOIN app_category b ON a.categoryid = b.categoryid
|
|
LEFT JOIN app_location c ON a.applocationid = c.applocationid
|
|
LEFT JOIN partnerinfo d ON a.partnerid = d.partnerid
|
|
LEFT JOIN app_types e ON d.allocationid = e.apptypeid
|
|
LEFT JOIN tenantlocations f ON a.tenantid = f.tenantid
|
|
WHERE a.tenantid = ?
|
|
`
|
|
|
|
var args []interface{}
|
|
args = append(args, tid)
|
|
|
|
if locationid != 0 {
|
|
q1 += " AND f.locationid = ?"
|
|
args = append(args, locationid)
|
|
}
|
|
|
|
if err := r.db.Raw(q1, args...).Find(&data).Error; err != nil {
|
|
return data, err
|
|
}
|
|
|
|
data.Status = strings.ToLower(data.Status)
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func (r *tenantRepository) GetTenantByKeyword(keyword string) ([]models.TenantSearch, error) {
|
|
var data []models.TenantSearch
|
|
|
|
kw := "%" + strings.ToLower(keyword) + "%"
|
|
|
|
query := `
|
|
SELECT a.tenantname, b.productname, c.subcatname
|
|
FROM tenants a
|
|
LEFT JOIN products b ON a.tenantid = b.tenantid
|
|
LEFT JOIN productsubcategories c ON b.subcategoryid = c.subcatid
|
|
WHERE c.categoryid = 2
|
|
AND (LOWER(a.tenantname) LIKE ? OR LOWER(b.productname) LIKE ? OR LOWER(c.subcatname) LIKE ?)
|
|
`
|
|
|
|
if err := r.db.Raw(query, kw, kw, kw).Scan(&data).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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
|
|
}
|