Files
backend_fiesta/services/tenantService.go
2026-09-01 13:17:52 +05:30

262 lines
9.5 KiB
Go

package services
import (
"errors"
"net/http"
"strings"
"nearle/models"
"nearle/repositories"
"nearle/utils"
)
type TenantService interface {
SearchTenant(status, keyword 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(req models.CreateTenantCustomerRequest) (*models.Tenantcustomer, error)
GetCustomerTenants(customerID int, categoryID int, tenantFlag int) (*models.CustomerTenantResponse, 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) map[string]interface{}
UpdateTenantLocation(data models.Tenantlocations) map[string]interface{}
CreateTenantUser(data models.Tenants) (models.UserInfo, error)
GetTenantByID(tid int, locationid int, userid int) (models.Tenantinfo, error)
GetTenantByKeyword(keyword string) ([]models.TenantSearch, error)
}
type tenantService struct {
repo repositories.TenantRepository
}
func NewTenantService(repo repositories.TenantRepository) TenantService {
return &tenantService{repo: repo}
}
func (s *tenantService) SearchTenant(status, keyword string) ([]models.Tenantinfo, error) {
return s.repo.SearchTenant(status, keyword)
}
func (s *tenantService) GetAllTenants(pageno, pagesize, aid int, status, tenanttype, keyword string) ([]models.Tenantinfo, error) {
return s.repo.GetAllTenants(pageno, pagesize, aid, status, tenanttype, keyword)
}
func (s *tenantService) GetTenantLocations(tid int) ([]models.Tenantlocations, error) {
return s.repo.GetTenantLocations(tid)
}
func (s *tenantService) GetTenantSlot() (models.Tenantslot, error) {
return s.repo.GetTenantSlot()
}
func (s *tenantService) CreateTenantCustomer(req models.CreateTenantCustomerRequest) (*models.Tenantcustomer, error) {
tenantCustomer := models.Tenantcustomer{
TenantID: req.TenantID,
LocationID: req.LocationID,
CustomerID: req.CustomerID,
CustomerLocationID: req.CustomerLocationID,
Status: req.Status,
}
return s.repo.CreateTenantCustomer(tenantCustomer)
}
func (s *tenantService) GetCustomerTenants(customerID int, categoryID int, tenantFlag int) (*models.CustomerTenantResponse, error) {
details, err := s.repo.GetCustomerTenants(customerID, categoryID, tenantFlag)
if err != nil {
return nil, err
}
return &models.CustomerTenantResponse{
Details: details,
}, nil
}
func (s *tenantService) GetTenantPricing(tid, aid int) (models.Tenantpricing, error) {
result, err := s.repo.GetTenantPricing(tid, aid)
if err != nil {
return models.Tenantpricing{}, err
}
return *result, nil
}
func (s *tenantService) UpdateLocation(input models.Tenantlocations) error {
return s.repo.UpdateLocation(input)
}
func (s *tenantService) CreateLocation(data models.Tenantlocations) error {
return s.repo.CreateLocation(data)
}
func (s *tenantService) DeleteLocation(locationid int, tenantid int) error {
return s.repo.DeleteLocation(locationid, tenantid)
}
func (s *tenantService) GetStaffs(tid int) ([]models.StaffInfo, error) {
return s.repo.GetStaffs(tid)
}
func (s *tenantService) CreateStaff(user models.User) error {
// Same two fields, same reason: without an authname and a console configid
// the account is created, listed, and refused at the login screen. This path
// and users/create both make back-office accounts, so both need it.
return s.repo.CreateStaff(PrepareNewAccount(user))
}
func (s *tenantService) UpdateStaff(user models.User) error {
return s.repo.UpdateStaff(user)
}
func (s *tenantService) CreateTenantLocation(data models.Tenantlocations) map[string]interface{} {
// A branch needs a location for the same reasons a tenant does — delivery
// radius is stored on this very row. Same rule: only when the caller sent
// nothing, and never fatal.
if strings.TrimSpace(data.Latitude) == "" && strings.TrimSpace(data.Longitude) == "" {
data.Latitude, data.Longitude = utils.GeocodeAddress(
data.Address, data.Suburb, data.City, data.State, data.Postcode)
}
created, err := s.repo.CreateTenantLocation(data)
if err != nil {
return map[string]interface{}{
"code": http.StatusConflict,
"message": err.Error(),
"status": false,
}
}
// "details" carries back the DB-assigned locationid so the frontend can
// build the store's QR code (tenantid+locationid) immediately after
// onboarding, instead of having to look the new location up separately.
return map[string]interface{}{
"code": http.StatusCreated,
"message": "Tenant Location Successfully Created",
"status": true,
"details": created,
}
}
func (s *tenantService) UpdateTenantLocation(data models.Tenantlocations) map[string]interface{} {
err := s.repo.UpdateTenantLocation(data)
if err != nil {
return map[string]interface{}{
"status": false,
"code": http.StatusConflict,
"message": err.Error(),
}
}
return map[string]interface{}{
"status": true,
"code": http.StatusAccepted,
"message": "Tenant Location update successful",
}
}
func (s *tenantService) CreateTenantUser(data models.Tenants) (models.UserInfo, error) {
// ✅ Check if tenant already exists
exists := s.repo.CheckTenantByNo(data.Primarycontact)
if exists != 0 {
return models.UserInfo{}, errors.New("Tenant Already Exists")
}
// Coordinates from the address, for the tenant and its primary outlet.
//
// `tenants.latitude` and `tenants.longitude` have existed since the schema
// was written and no caller has ever filled them, so every shop on the
// platform sits at no location at all. Resolved here rather than in the
// console because the key stays server-side and because a merchant created
// by any other client — the mobile app, a script — gets the same treatment.
//
// Only when the caller sent nothing: a console that lets an operator drag a
// pin is stating something a geocoder cannot know, and must win.
fillTenantCoordinates(&data)
// ✅ Create Tenant User
status, err := s.repo.CreateTenantUser(data)
if err != nil || !status {
return models.UserInfo{}, err
}
// ✅ Get user details by contact number
result := s.repo.GetUserByNo(data.Primarycontact)
return result, nil
}
func (s *tenantService) GetTenantByID(tid int, locationid int, userid int) (models.Tenantinfo, error) {
return s.repo.GetTenantByID(tid, locationid, userid)
}
func (s *tenantService) GetTenantByKeyword(keyword string) ([]models.TenantSearch, error) {
return s.repo.GetTenantByKeyword(keyword)
}
// fillTenantCoordinates geocodes a tenant and its nested primary outlet.
//
// Never returns an error and never blocks creation. A geocoder that is down,
// slow or rate-limited leaves the columns exactly as they are today — empty —
// and a merchant is still onboarded. The alternative, refusing to create a shop
// because a third party did not answer, trades a fixable blank field for a
// person stuck on the phone.
func fillTenantCoordinates(data *models.Tenants) {
if strings.TrimSpace(data.Latitude) == "" && strings.TrimSpace(data.Longitude) == "" {
data.Latitude, data.Longitude = utils.GeocodeAddress(
data.Address, data.Suburb, data.City, data.State, data.Postcode)
}
outlet := &data.Tenantlocations
if strings.TrimSpace(outlet.Latitude) != "" || strings.TrimSpace(outlet.Longitude) != "" {
return
}
// The first outlet is almost always at the address just typed, and the
// console already copies it across. Reuse the tenant's answer rather than
// asking twice for the same string — that is a second network round trip
// inside a request someone is waiting on, for a guaranteed identical result.
if sameAddress(*data) {
outlet.Latitude, outlet.Longitude = data.Latitude, data.Longitude
return
}
outlet.Latitude, outlet.Longitude = utils.GeocodeAddress(
outlet.Address, outlet.Suburb, outlet.City, outlet.State, outlet.Postcode)
}
func sameAddress(data models.Tenants) bool {
outlet := data.Tenantlocations
return strings.EqualFold(strings.TrimSpace(outlet.Address), strings.TrimSpace(data.Address)) &&
strings.EqualFold(strings.TrimSpace(outlet.City), strings.TrimSpace(data.City)) &&
strings.EqualFold(strings.TrimSpace(outlet.Postcode), strings.TrimSpace(data.Postcode))
}
func (s *tenantService) AssignStaffToBranch(tenantID, userID, locationID int) error {
return s.repo.AssignStaffToBranch(tenantID, userID, locationID)
}
// UpdateTenantProfile filters the request down to what a merchant owns, then
// writes it. The allowlist lives in tenantProfile.go with the reasoning.
func (s *tenantService) UpdateTenantProfile(tenantID int, fields map[string]any) error {
clean, err := TenantProfileUpdate(fields)
if err != nil {
return err
}
return s.repo.UpdateTenantProfile(tenantID, clean)
}
// UpdateOwnProfile filters a self-service edit down to identity fields, then
// writes it scoped to the person's own business. See ownProfile.go.
func (s *tenantService) UpdateOwnProfile(userID, tenantID int, fields map[string]any) error {
clean, err := OwnProfileUpdate(fields)
if err != nil {
return err
}
return s.repo.UpdateOwnProfile(userID, tenantID, clean)
}