rider partner
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
41
controllers/riderOwnership_test.go
Normal file
41
controllers/riderOwnership_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user