rider partner

This commit is contained in:
2026-09-09 15:44:27 +05:30
parent 62a441d798
commit e7577fe0cf
11 changed files with 812 additions and 3 deletions

View File

@@ -1,6 +1,7 @@
package controllers
import (
"errors"
"nearle/models"
"nearle/services"
"net/http"
@@ -218,11 +219,35 @@ func (ctl *PartnerController) CreateRider(c *fiber.Ctx) error {
if tid, _ := strconv.Atoi(c.Query("tenantid")); tid != 0 {
rider.Tenantid = tid
}
if rider.Tenantid == 0 {
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": "tenantid is required",
"message": "a rider belongs to a shop or to a partner, not to both",
})
}
@@ -310,3 +335,97 @@ func (ctl *PartnerController) GetRiderRoster(c *fiber.Ctx) error {
"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
}

View File

@@ -0,0 +1,41 @@
package controllers
import "testing"
/*
A rider belongs to a shop or to a delivery partner — never to neither, never to
both.
`CreateRider` 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. The two ids are now exclusive, and both mistakes
are refused rather than one being silently preferred — a rider carrying both
appears in two directories and two assign pickers, with nothing downstream
saying which owns them.
`riderOwner` is the rule on its own so it can be checked without a request.
*/
func TestRiderOwnerAcceptsAShopsOwnRider(t *testing.T) {
if err := riderOwner(1147, 0); err != nil {
t.Fatalf("a tenant's own rider is valid: %v", err)
}
}
func TestRiderOwnerAcceptsAPartnersRider(t *testing.T) {
if err := riderOwner(0, 44); err != nil {
t.Fatalf("a partner's rider is valid: %v", err)
}
}
func TestRiderOwnerRefusesNeither(t *testing.T) {
if err := riderOwner(0, 0); err == nil {
t.Fatal("a rider owned by nobody appears in no directory and must be refused")
}
}
func TestRiderOwnerRefusesBoth(t *testing.T) {
if err := riderOwner(1147, 44); err == nil {
t.Fatal("a rider owned by both appears in two pickers and must be refused")
}
}

View File

@@ -710,3 +710,50 @@ func (ctl *TenantController) UpdateOwnProfile(c *fiber.Ctx) 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",
})
}