874 lines
25 KiB
Go
874 lines
25 KiB
Go
package controllers
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"nearle/middleware"
|
|
"nearle/models"
|
|
"nearle/services"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
)
|
|
|
|
type TenantController struct {
|
|
tenantService services.TenantService
|
|
}
|
|
|
|
func NewTenantController(tenantService services.TenantService) *TenantController {
|
|
return &TenantController{tenantService: tenantService}
|
|
}
|
|
|
|
func (ctl *TenantController) SearchTenant(c *fiber.Ctx) error {
|
|
status := c.Query("status")
|
|
searchStr := c.Query("keyword")
|
|
|
|
data, err := ctl.tenantService.SearchTenant(status, searchStr)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"code": http.StatusInternalServerError,
|
|
"message": fmt.Sprintf("Error searching tenants: %v", err),
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"code": http.StatusOK,
|
|
"message": "Success",
|
|
"status": true,
|
|
"details": data,
|
|
})
|
|
}
|
|
|
|
func (ctl *TenantController) GetAllTenants(c *fiber.Ctx) error {
|
|
pageno, _ := strconv.Atoi(c.Query("pageno"))
|
|
pagesize, _ := strconv.Atoi(c.Query("pagesize"))
|
|
|
|
// Paging is defaulted, not required.
|
|
//
|
|
// The repository builds LIMIT/OFFSET from these directly, so a caller that
|
|
// omitted either — or sent pageno=0 — got an empty result reported as
|
|
// `code 200, status true, message "Success"`. "There are no tenants on the
|
|
// platform" and "you forgot a query parameter" are very different answers
|
|
// and this endpoint gave the first for the second.
|
|
//
|
|
// Defaulted rather than rejected with a 400: every existing caller that
|
|
// works today keeps working, and a platform list with no paging asked for
|
|
// has an obvious right answer — the first page.
|
|
if pageno < 1 {
|
|
pageno = 1
|
|
}
|
|
if pagesize < 1 {
|
|
pagesize = 50
|
|
}
|
|
|
|
status := c.Query("status")
|
|
aid, _ := strconv.Atoi(c.Query("applocationid"))
|
|
tenanttype := c.Query("tenanttype")
|
|
keyword := c.Query("keyword")
|
|
|
|
details, err := ctl.tenantService.GetAllTenants(pageno, pagesize, aid, status, tenanttype, keyword)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"code": http.StatusInternalServerError,
|
|
"message": fmt.Sprintf("Error getting all tenants: %v", err),
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"code": http.StatusOK,
|
|
"message": "Success",
|
|
"status": true,
|
|
"details": details,
|
|
})
|
|
}
|
|
|
|
func (ctl *TenantController) GetTenantLocations(c *fiber.Ctx) error {
|
|
tidStr := c.Query("tenantid")
|
|
if tidStr == "" {
|
|
tidStr = c.Query("tenantId")
|
|
}
|
|
if tidStr == "" {
|
|
tidStr = c.Query("tenantID")
|
|
}
|
|
if tidStr == "" {
|
|
tidStr = c.Query("tenant_id")
|
|
}
|
|
tid, _ := strconv.Atoi(strings.TrimSpace(tidStr))
|
|
|
|
data, err := ctl.tenantService.GetTenantLocations(tid)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"code": http.StatusInternalServerError,
|
|
"message": fmt.Sprintf("Error getting tenant locations: %v", err),
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"code": http.StatusOK,
|
|
"message": "Success",
|
|
"status": true,
|
|
"details": data,
|
|
})
|
|
}
|
|
|
|
func (ctl *TenantController) GetTenantSlot(c *fiber.Ctx) error {
|
|
|
|
data, err := ctl.tenantService.GetTenantSlot()
|
|
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"code": http.StatusInternalServerError,
|
|
"message": fmt.Sprintf("Error getting tenant slots: %v", err),
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"code": http.StatusOK,
|
|
"message": "Success",
|
|
"status": true,
|
|
"details": data,
|
|
})
|
|
}
|
|
|
|
func (ctl *TenantController) CreateTenantCustomer(c *fiber.Ctx) error {
|
|
var req models.CreateTenantCustomerRequest
|
|
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": 400,
|
|
"status": false,
|
|
"message": "Invalid request body",
|
|
"data": fiber.Map{},
|
|
})
|
|
}
|
|
|
|
tenantCustomer, err := ctl.tenantService.CreateTenantCustomer(req)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "already exists for this location") {
|
|
return c.JSON(fiber.Map{
|
|
"code": 409,
|
|
"status": false,
|
|
"message": "Customer already assigned to this location",
|
|
"data": fiber.Map{},
|
|
})
|
|
}
|
|
|
|
log.Println("Error inserting tenant customer:", err)
|
|
return c.JSON(fiber.Map{
|
|
"code": 500,
|
|
"status": false,
|
|
"message": "Failed to create tenant customer",
|
|
"data": fiber.Map{},
|
|
})
|
|
}
|
|
|
|
return c.Status(http.StatusOK).JSON(fiber.Map{
|
|
"code": 200,
|
|
"status": true,
|
|
"message": "Tenant customer created successfully",
|
|
"data": tenantCustomer,
|
|
})
|
|
}
|
|
|
|
func (ctl *TenantController) GetCustomerTenants(c *fiber.Ctx) error {
|
|
customerID, err := strconv.Atoi(c.Query("customerid"))
|
|
if err != nil || customerID == 0 {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": 400,
|
|
"message": "Invalid customerid",
|
|
"status": false,
|
|
"details": []interface{}{},
|
|
})
|
|
}
|
|
|
|
categoryID, _ := strconv.Atoi(c.Query("categoryid"))
|
|
tenantFlag, _ := strconv.Atoi(c.Query("tenant")) // 0 = all tenants, 1 = tenants with orders
|
|
|
|
data, err := ctl.tenantService.GetCustomerTenants(customerID, categoryID, tenantFlag)
|
|
if err != nil {
|
|
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
|
|
"code": 500,
|
|
"message": err.Error(),
|
|
"status": false,
|
|
"details": []interface{}{},
|
|
})
|
|
}
|
|
|
|
return c.Status(http.StatusOK).JSON(fiber.Map{
|
|
"code": http.StatusOK,
|
|
"message": "Success",
|
|
"status": true,
|
|
"details": data.Details,
|
|
})
|
|
}
|
|
|
|
func (ctl *TenantController) GetTenantPricing(c *fiber.Ctx) error {
|
|
tid, _ := strconv.Atoi(c.Query("tenantid"))
|
|
aid, _ := strconv.Atoi(c.Query("applocationid"))
|
|
|
|
data, err := ctl.tenantService.GetTenantPricing(tid, aid)
|
|
if err != nil {
|
|
return c.JSON(fiber.Map{
|
|
"code": http.StatusInternalServerError,
|
|
"message": err.Error(),
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"code": http.StatusOK,
|
|
"message": "Success",
|
|
"status": true,
|
|
"details": data,
|
|
})
|
|
}
|
|
|
|
func (ctl *TenantController) UpdateLocation(c *fiber.Ctx) error {
|
|
var data models.Tenantlocations
|
|
|
|
if err := c.BodyParser(&data); err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"status": false,
|
|
"code": http.StatusBadRequest,
|
|
"message": "Invalid request body",
|
|
})
|
|
}
|
|
|
|
if err := ctl.tenantService.UpdateLocation(data); err != nil {
|
|
return c.JSON(fiber.Map{
|
|
"status": false,
|
|
"code": http.StatusConflict,
|
|
"message": err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"status": true,
|
|
"code": http.StatusAccepted,
|
|
"message": "Location update successful",
|
|
})
|
|
}
|
|
|
|
func (ctl *TenantController) CreateLocation(c *fiber.Ctx) error {
|
|
var data models.Tenantlocations
|
|
|
|
if err := c.BodyParser(&data); err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest,
|
|
"message": "Invalid request body",
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
err := ctl.tenantService.CreateLocation(data)
|
|
if err != nil {
|
|
return c.Status(http.StatusConflict).JSON(fiber.Map{
|
|
"code": http.StatusConflict,
|
|
"message": err.Error(),
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
return c.Status(http.StatusCreated).JSON(fiber.Map{
|
|
"code": http.StatusCreated,
|
|
"message": "Location Successfully Created",
|
|
"status": true,
|
|
})
|
|
}
|
|
|
|
func (ctl *TenantController) DeleteLocation(c *fiber.Ctx) error {
|
|
locationid, err := strconv.Atoi(c.Query("locationid"))
|
|
if err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest,
|
|
"message": "Invalid location ID",
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
tenantid, err := strconv.Atoi(c.Query("tenantid"))
|
|
if err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest,
|
|
"message": "Invalid tenant ID",
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
if err := ctl.tenantService.DeleteLocation(locationid, tenantid); err != nil {
|
|
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
|
|
"code": http.StatusInternalServerError,
|
|
"message": err.Error(),
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"code": http.StatusOK,
|
|
"message": "Location Successfully Deleted",
|
|
"status": true,
|
|
})
|
|
}
|
|
|
|
func (ctl *TenantController) GetStaffs(c *fiber.Ctx) error {
|
|
tid, _ := strconv.Atoi(c.Query("tenantid"))
|
|
|
|
data, err := ctl.tenantService.GetStaffs(tid)
|
|
if err != nil {
|
|
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
|
|
"code": http.StatusInternalServerError,
|
|
"message": err.Error(),
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"code": http.StatusOK,
|
|
"message": "Success",
|
|
"status": true,
|
|
"details": data,
|
|
})
|
|
}
|
|
|
|
func (ctl *TenantController) CreateStaff(c *fiber.Ctx) error {
|
|
var data models.User
|
|
|
|
if err := c.BodyParser(&data); err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest,
|
|
"message": "Invalid request body",
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
invite, err := ctl.tenantService.CreateStaff(data)
|
|
if err != nil {
|
|
// A rejected PIN, a missing name, a role nobody set — these are things
|
|
// the person filling in the form can fix, so they come back as 400 with
|
|
// the reason. This answered 500 with a body claiming 409, which told a
|
|
// console nothing it could act on and told the operator less.
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest,
|
|
"message": err.Error(),
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
// The person was hired either way. Whether they were emailed their
|
|
// first-password link is reported beside that rather than folded into
|
|
// `status`: this account is created with no password and the link is the only
|
|
// way in, so an operator who is not told cannot know they have added somebody
|
|
// who cannot sign in.
|
|
return c.JSON(fiber.Map{
|
|
"code": http.StatusCreated,
|
|
"message": "Staff created successfully",
|
|
"status": true,
|
|
"invited": invite.Sent,
|
|
"invitereason": invite.Reason,
|
|
})
|
|
}
|
|
|
|
func (ctl *TenantController) UpdateStaff(c *fiber.Ctx) error {
|
|
var data models.User
|
|
|
|
if err := c.BodyParser(&data); err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"status": false,
|
|
"code": http.StatusBadRequest,
|
|
"message": "Invalid request body",
|
|
})
|
|
}
|
|
|
|
if err := ctl.tenantService.UpdateStaff(data); err != nil {
|
|
return c.Status(http.StatusConflict).JSON(fiber.Map{
|
|
"status": false,
|
|
"code": http.StatusConflict,
|
|
"message": err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"status": true,
|
|
"code": http.StatusAccepted,
|
|
"message": "Staff updated successfully",
|
|
})
|
|
}
|
|
|
|
func (ctl *TenantController) CreateTenantLocation(c *fiber.Ctx) error {
|
|
var data models.Tenantlocations
|
|
|
|
if err := c.BodyParser(&data); err != nil {
|
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
|
"code": 400,
|
|
"message": "Invalid request body",
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
resp := ctl.tenantService.CreateTenantLocation(data)
|
|
return c.Status(fiber.StatusOK).JSON(resp)
|
|
}
|
|
|
|
func (ctl *TenantController) UpdateTenantLocation(c *fiber.Ctx) error {
|
|
var data models.Tenantlocations
|
|
|
|
// Parse JSON body
|
|
if err := c.BodyParser(&data); err != nil {
|
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
|
"status": false,
|
|
"code": 400,
|
|
"message": "Invalid request body",
|
|
})
|
|
}
|
|
|
|
// Call service layer
|
|
resp := ctl.tenantService.UpdateTenantLocation(data)
|
|
|
|
// Always return HTTP 200 (as per your API pattern)
|
|
return c.Status(fiber.StatusOK).JSON(resp)
|
|
}
|
|
|
|
func (ctl *TenantController) CreateTenantUser(c *fiber.Ctx) error {
|
|
var data models.Tenants
|
|
|
|
if err := c.BodyParser(&data); err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": 400,
|
|
"status": false,
|
|
"message": "Invalid request body",
|
|
})
|
|
}
|
|
|
|
result, invite, err := ctl.tenantService.CreateTenantUser(data)
|
|
if err != nil {
|
|
if err.Error() == "Tenant Already Exists" {
|
|
return c.Status(http.StatusConflict).JSON(fiber.Map{
|
|
"code": 409,
|
|
"status": false,
|
|
"message": err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
|
|
"code": 500,
|
|
"status": false,
|
|
"message": err.Error(),
|
|
})
|
|
}
|
|
|
|
// The tenant was created either way. The invitation is reported beside it
|
|
// rather than folded into `status`, because a merchant who exists and has
|
|
// not been emailed is a task for the operator — resend, or correct the
|
|
// address — and not a failed onboarding to be retried.
|
|
//
|
|
// `invited: false` with a reason is the state the platform console shows on
|
|
// the tenant, so it never has to guess whether the email went.
|
|
return c.Status(http.StatusCreated).JSON(fiber.Map{
|
|
"code": 201,
|
|
"status": true,
|
|
"message": "Successfully Created",
|
|
"details": result,
|
|
"invited": invite.Sent,
|
|
// Omitted when it sent, so a successful onboarding carries no apology.
|
|
"invitereason": invite.Reason,
|
|
})
|
|
}
|
|
|
|
func (ctl *TenantController) GetTenantInfo(c *fiber.Ctx) error {
|
|
log.Printf("[DEBUG] GetTenantInfo OriginalURL: %s, Headers: %v", c.OriginalURL(), c.GetReqHeaders())
|
|
|
|
// Parse tenant ID
|
|
tidStr := c.Query("tenantid")
|
|
if tidStr == "" {
|
|
tidStr = c.Query("tenantId")
|
|
}
|
|
if tidStr == "" {
|
|
tidStr = c.Query("tenantID")
|
|
}
|
|
if tidStr == "" {
|
|
tidStr = c.Query("tenant_id")
|
|
}
|
|
if tidStr == "" {
|
|
tidStr = c.Get("tenantid")
|
|
}
|
|
if tidStr == "" {
|
|
tidStr = c.Get("tenantId")
|
|
}
|
|
if tidStr == "" {
|
|
tidStr = c.Get("tenantID")
|
|
}
|
|
if tidStr == "" {
|
|
tidStr = c.Get("tenant_id")
|
|
}
|
|
tid, _ := strconv.Atoi(strings.TrimSpace(tidStr))
|
|
|
|
// Parse location ID
|
|
lidStr := c.Query("locationid")
|
|
if lidStr == "" {
|
|
lidStr = c.Query("locationId")
|
|
}
|
|
if lidStr == "" {
|
|
lidStr = c.Query("locationID")
|
|
}
|
|
if lidStr == "" {
|
|
lidStr = c.Query("location_id")
|
|
}
|
|
if lidStr == "" {
|
|
lidStr = c.Get("locationid")
|
|
}
|
|
if lidStr == "" {
|
|
lidStr = c.Get("locationId")
|
|
}
|
|
if lidStr == "" {
|
|
lidStr = c.Get("locationID")
|
|
}
|
|
if lidStr == "" {
|
|
lidStr = c.Get("location_id")
|
|
}
|
|
locationid, _ := strconv.Atoi(strings.TrimSpace(lidStr))
|
|
|
|
// Parse user ID
|
|
uidStr := c.Query("userid")
|
|
if uidStr == "" {
|
|
uidStr = c.Query("userId")
|
|
}
|
|
if uidStr == "" {
|
|
uidStr = c.Query("userID")
|
|
}
|
|
if uidStr == "" {
|
|
uidStr = c.Query("user_id")
|
|
}
|
|
if uidStr == "" {
|
|
uidStr = c.Query("appuserid")
|
|
}
|
|
if uidStr == "" {
|
|
uidStr = c.Query("appuserId")
|
|
}
|
|
if uidStr == "" {
|
|
uidStr = c.Query("appuserID")
|
|
}
|
|
if uidStr == "" {
|
|
uidStr = c.Query("appuser_id")
|
|
}
|
|
if uidStr == "" {
|
|
uidStr = c.Get("userid")
|
|
}
|
|
if uidStr == "" {
|
|
uidStr = c.Get("userId")
|
|
}
|
|
if uidStr == "" {
|
|
uidStr = c.Get("userID")
|
|
}
|
|
if uidStr == "" {
|
|
uidStr = c.Get("user_id")
|
|
}
|
|
if uidStr == "" {
|
|
uidStr = c.Get("appuserid")
|
|
}
|
|
if uidStr == "" {
|
|
uidStr = c.Get("appuserId")
|
|
}
|
|
if uidStr == "" {
|
|
uidStr = c.Get("appuserID")
|
|
}
|
|
if uidStr == "" {
|
|
uidStr = c.Get("appuser_id")
|
|
}
|
|
userid, _ := strconv.Atoi(strings.TrimSpace(uidStr))
|
|
|
|
data, err := ctl.tenantService.GetTenantByID(tid, locationid, userid)
|
|
if err != nil {
|
|
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
|
|
"code": http.StatusInternalServerError,
|
|
"message": "Error fetching tenant info",
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"code": http.StatusOK,
|
|
"message": "Success",
|
|
"status": true,
|
|
"details": data,
|
|
})
|
|
}
|
|
|
|
func (ctl *TenantController) GetTenantByKeyword(c *fiber.Ctx) error {
|
|
keyword := c.Query("keyword")
|
|
|
|
data, err := ctl.tenantService.GetTenantByKeyword(keyword)
|
|
if err != nil {
|
|
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
|
|
"code": 500,
|
|
"message": "Error searching tenants by keyword",
|
|
"status": false,
|
|
})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"code": http.StatusOK,
|
|
"message": "Success",
|
|
"status": true,
|
|
"details": data,
|
|
})
|
|
}
|
|
|
|
// AssignStaff moves one of a merchant's people to a branch, or takes them off.
|
|
//
|
|
// `locationid` 0 unassigns, and is a real instruction rather than a missing
|
|
// value — somebody can leave a shop before the next one opens, and the console
|
|
// needs a way to say that which is not "delete the account".
|
|
//
|
|
// The tenant comes from the request and every check is scoped to it in the
|
|
// query, so a userid belonging to another business matches nothing and the call
|
|
// fails rather than moving a stranger's staff.
|
|
func (ctl *TenantController) AssignStaff(c *fiber.Ctx) error {
|
|
var input struct {
|
|
Tenantid int `json:"tenantid"`
|
|
Userid int `json:"userid"`
|
|
Locationid int `json:"locationid"`
|
|
Unassign bool `json:"unassign"`
|
|
}
|
|
if err := c.BodyParser(&input); err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest, "status": false, "message": "Invalid input",
|
|
})
|
|
}
|
|
if input.Tenantid <= 0 || input.Userid <= 0 {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest, "status": false,
|
|
"message": "tenantid and userid are required",
|
|
})
|
|
}
|
|
|
|
// The rule lives in services.ResolveAssignment so it can be tested without
|
|
// a request: a zero locationid must never be read as "unassign", because a
|
|
// dropped field looks exactly like one.
|
|
location, err := services.ResolveAssignment(input.Locationid, input.Unassign)
|
|
if err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest, "status": false, "message": err.Error(),
|
|
})
|
|
}
|
|
|
|
if err := ctl.tenantService.AssignStaffToBranch(input.Tenantid, input.Userid, location); err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest, "status": false, "message": err.Error(),
|
|
})
|
|
}
|
|
return c.JSON(fiber.Map{"code": 200, "status": true, "message": "Success"})
|
|
}
|
|
|
|
// UpdateTenantProfile lets a merchant change their own business record.
|
|
//
|
|
// The body is read as a free-form map rather than into `models.Tenants`,
|
|
// deliberately. Binding to the struct would make every column on the table a
|
|
// candidate for writing and leave "which of these may a merchant set?" answered
|
|
// by whichever fields a form happened to send. The allowlist in
|
|
// services.TenantProfileUpdate answers it in one place instead, and everything
|
|
// absent from a request is left alone rather than blanked.
|
|
func (ctl *TenantController) UpdateTenantProfile(c *fiber.Ctx) error {
|
|
fields := map[string]any{}
|
|
if err := c.BodyParser(&fields); err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest, "status": false, "message": "Invalid input",
|
|
})
|
|
}
|
|
|
|
// The row to write is named by `tenantid`, and it is the one value in the
|
|
// body that is never a value to write.
|
|
tenantID := 0
|
|
switch id := fields["tenantid"].(type) {
|
|
case float64:
|
|
tenantID = int(id)
|
|
case int:
|
|
tenantID = id
|
|
}
|
|
if tenantID <= 0 {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest, "status": false,
|
|
"message": "tenantid is required",
|
|
})
|
|
}
|
|
|
|
if err := ctl.tenantService.UpdateTenantProfile(tenantID, fields); err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest, "status": false, "message": err.Error(),
|
|
})
|
|
}
|
|
return c.JSON(fiber.Map{"code": 200, "status": true, "message": "Success"})
|
|
}
|
|
|
|
// UpdateOwnProfile lets somebody change their own name, mobile or email.
|
|
//
|
|
// Read as a map rather than into `models.User` for the same reason as the shop
|
|
// profile: `app_users` keeps identity next to authorisation, so binding to the
|
|
// struct would make `roleid`, `locationid`, `status`, `password` and `pin`
|
|
// candidates for writing. The allowlist in services.OwnProfileUpdate answers
|
|
// "what may a person change about themselves?" in one place.
|
|
//
|
|
// Scoped by userid AND tenantid — the existing `users/update` checks only the
|
|
// userid, which is why the store user's account page has been read-only rather
|
|
// than editable.
|
|
func (ctl *TenantController) UpdateOwnProfile(c *fiber.Ctx) error {
|
|
fields := map[string]any{}
|
|
if err := c.BodyParser(&fields); err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest, "status": false, "message": "Invalid input",
|
|
})
|
|
}
|
|
|
|
readID := func(key string) int {
|
|
switch id := fields[key].(type) {
|
|
case float64:
|
|
return int(id)
|
|
case int:
|
|
return id
|
|
}
|
|
return 0
|
|
}
|
|
userID, tenantID := readID("userid"), readID("tenantid")
|
|
if userID <= 0 || tenantID <= 0 {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest, "status": false,
|
|
"message": "userid and tenantid are both required",
|
|
})
|
|
}
|
|
|
|
if err := ctl.tenantService.UpdateOwnProfile(userID, tenantID, fields); err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest, "status": false, "message": err.Error(),
|
|
})
|
|
}
|
|
return c.JSON(fiber.Map{"code": 200, "status": true, "message": "Success"})
|
|
}
|
|
|
|
// AssignPartner puts a merchant under a delivery partner, or takes them out of
|
|
// one.
|
|
//
|
|
// `partnerid` 0 is a real instruction here — it means "this merchant uses their
|
|
// own riders" — so it is read as sent rather than treated as absent. Everywhere
|
|
// else in the tenant API a zero means "not supplied"; this is the exception and
|
|
// it is the reason the route exists separately.
|
|
func (ctl *TenantController) AssignPartner(c *fiber.Ctx) error {
|
|
var body struct {
|
|
Tenantid int `json:"tenantid"`
|
|
Partnerid *int `json:"partnerid"`
|
|
}
|
|
|
|
if err := c.BodyParser(&body); err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest, "status": false, "message": "Invalid request body",
|
|
})
|
|
}
|
|
if tid, _ := strconv.Atoi(c.Query("tenantid")); tid != 0 {
|
|
body.Tenantid = tid
|
|
}
|
|
if body.Tenantid <= 0 {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest, "status": false, "message": "tenantid is required",
|
|
})
|
|
}
|
|
// A pointer, so "no partner" and "field omitted" are different requests.
|
|
// Sent as a plain int, an omitted field would read as 0 and quietly unassign
|
|
// a merchant's partner.
|
|
if body.Partnerid == nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest, "status": false,
|
|
"message": "partnerid is required — send 0 to take the partner away",
|
|
})
|
|
}
|
|
|
|
if err := ctl.tenantService.AssignPartner(body.Tenantid, *body.Partnerid); err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest, "status": false, "message": err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"code": http.StatusOK, "status": true, "message": "Successfully Updated",
|
|
})
|
|
}
|
|
|
|
// ResendInvite re-issues a merchant's first-password link.
|
|
//
|
|
// ── Why this is platform staff only ─────────────────────────────────────────
|
|
//
|
|
// It mints a credential. `middleware.WebAuth` already pins a merchant's session
|
|
// to their own tenant, so a shop could at most re-invite itself — but the
|
|
// account it would be inviting is the one signing in to ask, which can only
|
|
// happen if that account already has a password, and the service refuses that
|
|
// case outright.
|
|
//
|
|
// So the only caller this is for is Nearle's own staff, chasing a merchant who
|
|
// never received the mail. Saying so explicitly is better than relying on two
|
|
// other checks to make the wrong case impossible.
|
|
func (ctl *TenantController) ResendInvite(c *fiber.Ctx) error {
|
|
claims, ok := middleware.WebClaimsFrom(c)
|
|
if !ok || !claims.IsPlatformAccount() {
|
|
return c.Status(http.StatusForbidden).JSON(fiber.Map{
|
|
"code": http.StatusForbidden, "status": false,
|
|
"message": "Only Nearle staff can resend an invitation.",
|
|
})
|
|
}
|
|
|
|
// Either a tenant — meaning its owner, the one account onboarding created —
|
|
// or one named person. Staff added later and the login every branch spawns
|
|
// are created with no password too, and a business has many of them, so
|
|
// "the tenant's invitation" cannot reach them.
|
|
var req struct {
|
|
Tenantid int `json:"tenantid"`
|
|
Userid int `json:"userid"`
|
|
}
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest, "status": false, "message": "Invalid request body",
|
|
})
|
|
}
|
|
if req.Tenantid <= 0 && req.Userid <= 0 {
|
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
|
"code": http.StatusBadRequest, "status": false,
|
|
"message": "Send a tenantid to re-invite the owner, or a userid to re-invite one person.",
|
|
})
|
|
}
|
|
|
|
// `userid` wins when both arrive. It is the more specific of the two, and a
|
|
// caller that sent a person's id meant that person — silently emailing the
|
|
// owner instead would be the wrong mailbox with no sign anything was off.
|
|
var (
|
|
outcome services.InviteOutcome
|
|
err error
|
|
)
|
|
if req.Userid > 0 {
|
|
outcome, err = ctl.tenantService.ResendInviteToUser(req.Userid)
|
|
} else {
|
|
outcome, err = ctl.tenantService.ResendInvite(req.Tenantid)
|
|
}
|
|
if err != nil {
|
|
// 409, not 500. Every failure here is a business fact the operator can
|
|
// act on — no such tenant, an address that matches no login, a merchant
|
|
// already set up — rather than a fault in the server.
|
|
return c.Status(http.StatusConflict).JSON(fiber.Map{
|
|
"code": http.StatusConflict, "status": false, "message": err.Error(),
|
|
})
|
|
}
|
|
if !outcome.Sent {
|
|
// The tenant is fine and the mail did not go. Reported as a failure
|
|
// because the operator pressed a button expecting an email to leave,
|
|
// and the reason names what to fix.
|
|
return c.Status(http.StatusConflict).JSON(fiber.Map{
|
|
"code": http.StatusConflict, "status": false, "message": outcome.Reason,
|
|
})
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"code": http.StatusOK, "status": true, "message": "Invitation sent.",
|
|
})
|
|
}
|