package controllers import ( "errors" "nearle/models" "nearle/services" "net/http" "strconv" "github.com/gofiber/fiber/v2" ) type PartnerController struct { partnerService services.PartnerService } func NewPartnerController(partnerService services.PartnerService) *PartnerController { return &PartnerController{partnerService: partnerService} } func (ctl *PartnerController) GetActiveRiders(c *fiber.Ctx) error { pid, _ := strconv.Atoi(c.Query("partnerid")) aid, _ := strconv.Atoi(c.Query("applocationid")) uid, _ := strconv.Atoi(c.Query("userid")) tid, _ := strconv.Atoi(c.Query("tenantid")) if pid == 0 && aid == 0 && uid == 0 && tid == 0 { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "status": false, "code": http.StatusBadRequest, "message": "At least one of tenantid, partnerid, applocationid or userid is required", }) } result, err := ctl.partnerService.GetActiveRiders(pid, aid, uid, tid) if err != nil { return c.JSON(fiber.Map{ "status": false, "code": http.StatusInternalServerError, "message": err.Error(), }) } return c.JSON(fiber.Map{ "status": true, "code": http.StatusOK, "message": "Successful", "details": result, }) } func (ctl *PartnerController) GetPartners(c *fiber.Ctx) error { pid, _ := strconv.Atoi(c.Query("partnerid")) aid, _ := strconv.Atoi(c.Query("applocationid")) uid, _ := strconv.Atoi(c.Query("userid")) result, err := ctl.partnerService.GetPartners(aid, pid, uid) if err != nil { return c.JSON(fiber.Map{ "status": false, "code": http.StatusInternalServerError, "message": err.Error(), }) } return c.JSON(fiber.Map{ "status": true, "code": http.StatusOK, "message": "Successful", "details": result, }) } // CreateRiderShift opens a working window in a delivery region. // // Riders cannot be hired without one, and until this existed the table could // only be read — a region that shipped with no shift rows was a region no rider // could ever be added to, with nothing in the product able to change that. // // The region comes from the body rather than the query because this is a write // and the whole shift is one object; `getridershifts` beside it reads the same // id from a param, which is the existing convention for reads here. func (ctl *PartnerController) CreateRiderShift(c *fiber.Ctx) error { var shift models.Ridershifts if err := c.BodyParser(&shift); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "status": false, "code": http.StatusBadRequest, "message": "Invalid request body", }) } result, err := ctl.partnerService.CreateRiderShift(shift) if err != nil { // 400, not 500. Every failure here is something the person typed — a // region that is not configured, a window that already exists, a time // that is not a time — and each message says which. return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "status": false, "code": http.StatusBadRequest, "message": err.Error(), }) } return c.Status(http.StatusCreated).JSON(fiber.Map{ "status": true, "code": http.StatusCreated, "message": "Shift created", "details": result, }) } func (ctl *PartnerController) GetRiderShifts(c *fiber.Ctx) error { aid, _ := strconv.Atoi(c.Query("applocationid")) result, err := ctl.partnerService.GetRiderShifts(aid) if err != nil { return c.JSON(fiber.Map{ "status": false, "code": http.StatusInternalServerError, "message": err.Error(), }) } return c.JSON(fiber.Map{ "status": true, "code": http.StatusOK, "message": "Successful", "details": result, }) } func (ctl *PartnerController) GetLocationConfig(c *fiber.Ctx) error { uid, _ := strconv.Atoi(c.Query("userid")) cid, _ := strconv.Atoi(c.Query("configid")) result, err := ctl.partnerService.GetLocationConfig(uid, cid) if err != nil { return c.JSON(fiber.Map{ "status": false, "code": http.StatusInternalServerError, "message": err.Error(), }) } return c.JSON(fiber.Map{ "status": true, "code": http.StatusOK, "message": "Successful", "details": result, }) } func (ctl *PartnerController) GetRiderLogs(c *fiber.Ctx) error { pid, _ := strconv.Atoi(c.Query("partnerid")) aid, _ := strconv.Atoi(c.Query("applocationid")) fdate := c.Query("fromdate") tdate := c.Query("todate") if pid == 0 && aid == 0 { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "status": false, "code": http.StatusBadRequest, "message": "At least one of partnerid or applocationid is required", }) } data, err := ctl.partnerService.GetRiderLogs(pid, aid, fdate, tdate) if err != nil { return c.JSON(fiber.Map{ "status": false, "code": http.StatusInternalServerError, "message": err.Error(), }) } return c.JSON(fiber.Map{ "status": true, "code": http.StatusOK, "message": "Successful", "details": data, }) } func (ctl *PartnerController) GetFleetSummary(c *fiber.Ctx) error { aid, _ := strconv.Atoi(c.Query("applocationid")) tid, _ := strconv.Atoi(c.Query("tenantid")) fdate := c.Query("fromdate") tdate := c.Query("todate") result, err := ctl.partnerService.GetFleetSummary(aid, tid, fdate, tdate) if err != nil { return c.JSON(fiber.Map{ "status": false, "code": http.StatusInternalServerError, "message": err.Error(), }) } return c.JSON(fiber.Map{ "status": true, "code": http.StatusOK, "message": "Successful", "details": result, }) } func (ctl *PartnerController) GetRiderInfo(c *fiber.Ctx) error { uid, _ := strconv.Atoi(c.Query("userid")) result, err := ctl.partnerService.GetRiderInfo(uid) if err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ "status": false, "code": http.StatusInternalServerError, "message": err.Error(), }) } return c.JSON(fiber.Map{ "status": true, "code": http.StatusOK, "message": "Successful", "details": result, }) } // CreateRider hires a rider — three tables in one transaction. // // `tenantid` comes from the query string, which is where this console's other // scoped writes take it from. It is required: a rider with no tenant belongs to // the region and to no merchant, which is the state the 84 existing riders are // already in and not one worth creating more of. func (ctl *PartnerController) CreateRider(c *fiber.Ctx) error { var rider models.NewRider if err := c.BodyParser(&rider); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "status": false, "message": "Invalid request body", }) } // Taken from the scope rather than trusted from the body. A store admin // must not be able to put a rider on another merchant's books by editing a // payload, and the body is the caller's to edit. if tid, _ := strconv.Atoi(c.Query("tenantid")); tid != 0 { rider.Tenantid = tid } if pid, _ := strconv.Atoi(c.Query("partnerid")); pid != 0 { rider.Partnerid = pid } // A rider belongs to a merchant OR to a delivery partner — never to // neither, and never to both. // // This used to demand a tenantid outright, which made a PARTNER's rider // impossible to create: a partner supplies riders to many merchants and // their riders sit under no single one. It is the same endpoint because it // is the same rider — the difference is only who they ride for, and that is // what the assign screen later branches on. // // Both at once is refused rather than silently preferred. A rider carrying // a tenantid AND a partnerid appears in two directories and two assign // pickers, and nothing downstream says which one owns them. switch { case rider.Tenantid == 0 && rider.Partnerid == 0: return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "status": false, "message": "a rider needs either a tenantid (the shop's own rider) " + "or a partnerid (a delivery partner's rider)", }) case rider.Tenantid != 0 && rider.Partnerid != 0: return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "status": false, "message": "a rider belongs to a shop or to a partner, not to both", }) } userid, err := ctl.partnerService.CreateRider(rider) if err != nil { // 400, not 500: every failure this can produce is something the caller // sent — a shift that does not exist, an unconfigured region, a phone // number already in use. Answering 500 would send them to look at the // server for a problem in their own form. return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "status": false, "message": err.Error(), }) } return c.Status(http.StatusCreated).JSON(fiber.Map{ "code": http.StatusCreated, "status": true, // Said plainly because it is the single most confusing thing about this // flow: the rider is created and will NOT show in the on-duty fleet // until they open the app and start a shift. "message": "Rider created. They appear in the fleet once they sign in and start a shift.", "details": fiber.Map{"userid": userid}, }) } func (ctl *PartnerController) UpdateRider(c *fiber.Ctx) error { var rider models.NewRider if err := c.BodyParser(&rider); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "status": false, "message": "Invalid request body", }) } if err := ctl.partnerService.UpdateRider(rider); 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": "Success", }) } // GetRiderRoster lists riders whether or not they are working today. // // The scope is required. Unscoped this returns every rider on the platform, // which is not a merchant's business — the same guard the other tenant-scoped // reads carry. func (ctl *PartnerController) GetRiderRoster(c *fiber.Ctx) error { tid, _ := strconv.Atoi(c.Query("tenantid")) aid, _ := strconv.Atoi(c.Query("applocationid")) pid, _ := strconv.Atoi(c.Query("partnerid")) if tid == 0 && aid == 0 && pid == 0 { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "status": false, "message": "One of tenantid, applocationid or partnerid is required", }) } result, err := ctl.partnerService.GetRiderRoster(tid, aid, pid) if err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ "code": http.StatusInternalServerError, "status": false, "message": err.Error(), }) } return c.JSON(fiber.Map{ "code": http.StatusOK, "status": true, "message": "Successful", "details": result, }) } /* ── Onboarding a delivery partner ──────────────────────────────────────────── Platform-side only. A merchant does not create the company that supplies their riders — they are assigned one, which is `AssignPartner` on the tenant. Both of these live in the web group for the same reason `createrider` does: a partner is onboarded from a console, not from a phone. */ // CreatePartner onboards a delivery partner and the regions they cover. func (ctl *PartnerController) CreatePartner(c *fiber.Ctx) error { var input models.NewPartner if err := c.BodyParser(&input); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "status": false, "message": "Invalid request body", }) } partnerid, err := ctl.partnerService.CreatePartner(input) if err != nil { // 400, not 500: every failure this produces is something the caller // sent — a missing name, an unconfigured region, a number already in // use. A 500 sends them to look at the server for their own form. return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "status": false, "message": err.Error(), }) } return c.Status(http.StatusCreated).JSON(fiber.Map{ "code": http.StatusCreated, "status": true, "message": "Successfully Created", "details": fiber.Map{"partnerid": partnerid}, }) } // UpdatePartner edits a partner and, when regions are sent, re-states them. func (ctl *PartnerController) UpdatePartner(c *fiber.Ctx) error { var input models.NewPartner if err := c.BodyParser(&input); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "status": false, "message": "Invalid request body", }) } if pid, _ := strconv.Atoi(c.Query("partnerid")); pid != 0 { input.Partnerid = pid } if err := ctl.partnerService.UpdatePartner(input); 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", }) } // GetPartnerLocations lists the regions one partner covers. func (ctl *PartnerController) GetPartnerLocations(c *fiber.Ctx) error { pid, _ := strconv.Atoi(c.Query("partnerid")) if pid == 0 { return c.Status(http.StatusBadRequest).JSON(fiber.Map{ "code": http.StatusBadRequest, "status": false, "message": "partnerid is required", }) } rows, err := ctl.partnerService.GetPartnerLocations(pid) if err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ "code": http.StatusInternalServerError, "status": false, "message": err.Error(), }) } return c.JSON(fiber.Map{ "code": http.StatusOK, "status": true, "message": "Success", "details": rows, }) } // riderOwner enforces that a rider belongs to a shop or to a partner. // // Its own function so the rule can be tested without a request, and so both // call sites — create today, anything that moves a rider tomorrow — cannot // drift apart on it. func riderOwner(tenantid, partnerid int) error { switch { case tenantid == 0 && partnerid == 0: return errors.New("a rider needs either a tenantid (the shop's own rider) " + "or a partnerid (a delivery partner's rider)") case tenantid != 0 && partnerid != 0: return errors.New("a rider belongs to a shop or to a partner, not to both") } return nil }