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 GetStaffs(tid int) ([]models.StaffInfo, error) CreateStaff(user models.User) 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 { return s.repo.CreateStaff(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)) }