rider partner
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"nearle/models"
|
"nearle/models"
|
||||||
"nearle/services"
|
"nearle/services"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -218,11 +219,35 @@ func (ctl *PartnerController) CreateRider(c *fiber.Ctx) error {
|
|||||||
if tid, _ := strconv.Atoi(c.Query("tenantid")); tid != 0 {
|
if tid, _ := strconv.Atoi(c.Query("tenantid")); tid != 0 {
|
||||||
rider.Tenantid = tid
|
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{
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
||||||
"code": http.StatusBadRequest,
|
"code": http.StatusBadRequest,
|
||||||
"status": false,
|
"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,
|
"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"})
|
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",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,6 +61,15 @@ type Partnerinfo struct {
|
|||||||
Postcode string `json:"postcode"`
|
Postcode string `json:"postcode"`
|
||||||
Partnerinfo string `json:"partnerinfo"`
|
Partnerinfo string `json:"partnerinfo"`
|
||||||
Partnerimage string `json:"partnerimage"`
|
Partnerimage string `json:"partnerimage"`
|
||||||
|
// Read back so a directory can show who the partner is and whether they
|
||||||
|
// are trading. Both columns have always been on the table; the struct had
|
||||||
|
// no field for them, so `getpartners` could not report either.
|
||||||
|
Companyname string `json:"companyname"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
// Every region this partner covers, from `partnerlocations`. A partner is
|
||||||
|
// not confined to the one city on their own row — that column records where
|
||||||
|
// they were set up, and the link table records where they actually work.
|
||||||
|
Locations []PartnerLocation `json:"locations" gorm:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Ridershifts struct {
|
type Ridershifts struct {
|
||||||
@@ -156,7 +165,16 @@ type NewRider struct {
|
|||||||
Postcode string `json:"postcode"`
|
Postcode string `json:"postcode"`
|
||||||
|
|
||||||
// Whose rider they are, where they ride, and who they ride for.
|
// Whose rider they are, where they ride, and who they ride for.
|
||||||
|
//
|
||||||
|
// Tenantid and Partnerid are exclusive: a rider belongs to a shop or to a
|
||||||
|
// delivery partner, never to both and never to neither. The controller
|
||||||
|
// refuses either mistake.
|
||||||
|
//
|
||||||
|
// Locationid is the branch an OWN rider works out of. It is meaningless for
|
||||||
|
// a partner's rider — a partner supplies several merchants and is not tied
|
||||||
|
// to any one shop's outlet — so it is only ever set alongside a tenantid.
|
||||||
Tenantid int `json:"tenantid"`
|
Tenantid int `json:"tenantid"`
|
||||||
|
Locationid int `json:"locationid"`
|
||||||
Applocationid int `json:"applocationid"`
|
Applocationid int `json:"applocationid"`
|
||||||
Partnerid int `json:"partnerid"`
|
Partnerid int `json:"partnerid"`
|
||||||
Shiftid int `json:"shiftid"`
|
Shiftid int `json:"shiftid"`
|
||||||
@@ -185,6 +203,8 @@ type RiderRosterRow struct {
|
|||||||
Contactno string `json:"contactno"`
|
Contactno string `json:"contactno"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Tenantid int `json:"tenantid"`
|
Tenantid int `json:"tenantid"`
|
||||||
|
Locationid int `json:"locationid"`
|
||||||
|
Locationname string `json:"locationname"`
|
||||||
Applocationid int `json:"applocationid"`
|
Applocationid int `json:"applocationid"`
|
||||||
Applocation string `json:"applocation"`
|
Applocation string `json:"applocation"`
|
||||||
Partnerid int `json:"partnerid"`
|
Partnerid int `json:"partnerid"`
|
||||||
@@ -236,3 +256,61 @@ type Appuserpools struct {
|
|||||||
Onduty int `json:"onduty"`
|
Onduty int `json:"onduty"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PartnerLocation is one region a partner covers — a row of `partnerlocations`.
|
||||||
|
type PartnerLocation struct {
|
||||||
|
Partnerlocationid int `json:"partnerlocationid" gorm:"column:partnerlocationid"`
|
||||||
|
Partnerid int `json:"partnerid" gorm:"column:partnerid"`
|
||||||
|
Applocationid int `json:"applocationid" gorm:"column:applocationid"`
|
||||||
|
Applocation string `json:"applocation" gorm:"column:applocation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPartner is everything the console collects to onboard a delivery partner.
|
||||||
|
//
|
||||||
|
// Separate from `Partnerinfo`, which is the READ shape. Binding a create to the
|
||||||
|
// read model is how a caller ends up able to set columns nobody meant to expose
|
||||||
|
// — `allocationid`, `partnertypeid`, the billing links — because a form happened
|
||||||
|
// to send them.
|
||||||
|
//
|
||||||
|
// A partner is a company that supplies riders. It is onboarded by the platform,
|
||||||
|
// never by a merchant: the merchant's side of this is being ASSIGNED one, which
|
||||||
|
// is `AssignPartner` on the tenant.
|
||||||
|
type NewPartner struct {
|
||||||
|
Partnerid int `json:"partnerid"`
|
||||||
|
Partnername string `json:"partnername"`
|
||||||
|
Companyname string `json:"companyname"`
|
||||||
|
Registrationno string `json:"registrationno"`
|
||||||
|
Primarycontact string `json:"primarycontact"`
|
||||||
|
Primaryemail string `json:"primaryemail"`
|
||||||
|
Contactno string `json:"contactno"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
Suburb string `json:"suburb"`
|
||||||
|
City string `json:"city"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Postcode int `json:"postcode"`
|
||||||
|
Partnerinfo string `json:"partnerinfo"`
|
||||||
|
Partnerimage string `json:"partnerimage"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
|
||||||
|
/*
|
||||||
|
Where they work — ONE district, not a set.
|
||||||
|
|
||||||
|
`Applocationid` is the home region and goes on the partner row itself,
|
||||||
|
because `GetPartners` filters on it and the rider app reads it.
|
||||||
|
`Applocationids` is every region they cover and goes to
|
||||||
|
`partnerlocations` — one partner routinely serves several cities, and
|
||||||
|
that is the whole reason the link table exists.
|
||||||
|
*/
|
||||||
|
Applocationid int `json:"applocationid"`
|
||||||
|
/*
|
||||||
|
The district by NAME, for one that is not open yet.
|
||||||
|
|
||||||
|
Nearle runs three of Tamil Nadu's thirty-eight districts, and a partner
|
||||||
|
could only be placed in those three because `partnerinfo.applocationid`
|
||||||
|
has to point at an `app_location` row. Sending the name instead opens the
|
||||||
|
district — see `EnsureRegion` — so the form can offer all 38 and mean it.
|
||||||
|
|
||||||
|
Ignored when `Applocationid` is set, which is the ordinary case.
|
||||||
|
*/
|
||||||
|
District string `json:"district"`
|
||||||
|
}
|
||||||
|
|||||||
41
repositories/partnerOnboard_test.go
Normal file
41
repositories/partnerOnboard_test.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"nearle/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
One partner, one district.
|
||||||
|
|
||||||
|
The form used to ask for a home region AND a coverage set, which put the same
|
||||||
|
partner in `partnerinfo.applocationid` and several `partnerlocations` rows. A
|
||||||
|
partner works one district — that is the operating rule — so the two writes
|
||||||
|
carry the same single id and `regionsOf` is what guarantees they cannot
|
||||||
|
disagree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
func TestRegionsOfIsTheOneDistrict(t *testing.T) {
|
||||||
|
got := regionsOf(models.NewPartner{Applocationid: 2})
|
||||||
|
if len(got) != 1 || got[0] != 2 {
|
||||||
|
t.Fatalf("got %v, want [2]", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero is not a district. It arrives from a form field nobody filled in, and a
|
||||||
|
// partnerlocations row pointing at region 0 would join to nothing for ever.
|
||||||
|
func TestRegionsOfIgnoresZero(t *testing.T) {
|
||||||
|
if got := regionsOf(models.NewPartner{Applocationid: 0}); len(got) != 0 {
|
||||||
|
t.Fatalf("got %v, want none", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A district sent only by NAME has not been resolved yet — CreatePartner opens
|
||||||
|
// it and sets the id before this is reached. Until then there is no region, and
|
||||||
|
// saying so beats guessing one.
|
||||||
|
func TestRegionsOfDoesNotInventOneFromAName(t *testing.T) {
|
||||||
|
if got := regionsOf(models.NewPartner{District: "Erode"}); len(got) != 0 {
|
||||||
|
t.Fatalf("got %v, want none until the district is opened", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PartnerRepository interface {
|
type PartnerRepository interface {
|
||||||
@@ -21,6 +22,10 @@ type PartnerRepository interface {
|
|||||||
CreateRider(rider models.NewRider) (int, error)
|
CreateRider(rider models.NewRider) (int, error)
|
||||||
UpdateRider(rider models.NewRider) error
|
UpdateRider(rider models.NewRider) error
|
||||||
GetRiderRoster(tid, aid, pid int) ([]models.RiderRosterRow, error)
|
GetRiderRoster(tid, aid, pid int) ([]models.RiderRosterRow, error)
|
||||||
|
CreatePartner(input models.NewPartner) (int, error)
|
||||||
|
UpdatePartner(input models.NewPartner) error
|
||||||
|
GetPartnerLocations(partnerid int) ([]models.PartnerLocation, error)
|
||||||
|
EnsureRegion(district string) (int, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type partnerRepository struct {
|
type partnerRepository struct {
|
||||||
@@ -399,6 +404,7 @@ func (r *partnerRepository) CreateRider(rider models.NewRider) (int, error) {
|
|||||||
Postcode: rider.Postcode,
|
Postcode: rider.Postcode,
|
||||||
Configid: 6,
|
Configid: 6,
|
||||||
Tenantid: rider.Tenantid,
|
Tenantid: rider.Tenantid,
|
||||||
|
Locationid: rider.Locationid,
|
||||||
Applocationid: rider.Applocationid,
|
Applocationid: rider.Applocationid,
|
||||||
Partnerid: rider.Partnerid,
|
Partnerid: rider.Partnerid,
|
||||||
Shiftid: rider.Shiftid,
|
Shiftid: rider.Shiftid,
|
||||||
@@ -468,8 +474,9 @@ func (r *partnerRepository) GetRiderRoster(tid, aid, pid int) ([]models.RiderRos
|
|||||||
|
|
||||||
q1 := `SELECT a.userid, a.firstname, a.lastname,
|
q1 := `SELECT a.userid, a.firstname, a.lastname,
|
||||||
CONCAT(a.firstname, ' ', a.lastname) AS fullname,
|
CONCAT(a.firstname, ' ', a.lastname) AS fullname,
|
||||||
a.contactno, a.email, a.tenantid, a.applocationid, a.partnerid, a.status,
|
a.contactno, a.email, a.tenantid, a.locationid, a.applocationid, a.partnerid, a.status,
|
||||||
f.locationname AS applocation,
|
f.locationname AS applocation,
|
||||||
|
COALESCE(g.locationname, '') AS locationname,
|
||||||
p.partnername,
|
p.partnername,
|
||||||
c.identificationno, c.vehiclename, c.vehicleno, c.licenseno, c.registrationno,
|
c.identificationno, c.vehiclename, c.vehicleno, c.licenseno, c.registrationno,
|
||||||
c.shiftid, CONCAT(d.starttime, ' - ', d.endtime) AS shiftname,
|
c.shiftid, CONCAT(d.starttime, ' - ', d.endtime) AS shiftname,
|
||||||
@@ -482,6 +489,7 @@ func (r *partnerRepository) GetRiderRoster(tid, aid, pid int) ([]models.RiderRos
|
|||||||
LEFT JOIN ridershifts d ON c.shiftid = d.shiftid
|
LEFT JOIN ridershifts d ON c.shiftid = d.shiftid
|
||||||
LEFT JOIN app_location f ON a.applocationid = f.applocationid
|
LEFT JOIN app_location f ON a.applocationid = f.applocationid
|
||||||
LEFT JOIN partnerinfo p ON a.partnerid = p.partnerid
|
LEFT JOIN partnerinfo p ON a.partnerid = p.partnerid
|
||||||
|
LEFT JOIN tenantlocations g ON a.locationid = g.locationid
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT r1.userid, r1.logdate, r1.logstatus
|
SELECT r1.userid, r1.logdate, r1.logstatus
|
||||||
FROM riderlogs r1
|
FROM riderlogs r1
|
||||||
@@ -558,6 +566,12 @@ func (r *partnerRepository) UpdateRider(rider models.NewRider) error {
|
|||||||
if rider.Shiftid != 0 {
|
if rider.Shiftid != 0 {
|
||||||
user["shiftid"] = rider.Shiftid
|
user["shiftid"] = rider.Shiftid
|
||||||
}
|
}
|
||||||
|
// The branch an own rider works out of. Moving them between a merchant's
|
||||||
|
// outlets is an ordinary edit; moving them between MERCHANTS is not, which
|
||||||
|
// is why tenantid and partnerid stay out of this map.
|
||||||
|
if rider.Locationid != 0 {
|
||||||
|
user["locationid"] = rider.Locationid
|
||||||
|
}
|
||||||
if strings.TrimSpace(rider.Status) != "" {
|
if strings.TrimSpace(rider.Status) != "" {
|
||||||
user["status"] = strings.TrimSpace(rider.Status)
|
user["status"] = strings.TrimSpace(rider.Status)
|
||||||
}
|
}
|
||||||
@@ -591,3 +605,384 @@ func (r *partnerRepository) UpdateRider(rider models.NewRider) error {
|
|||||||
|
|
||||||
return tx.Commit().Error
|
return tx.Commit().Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Onboarding a delivery partner ────────────────────────────────────────────
|
||||||
|
|
||||||
|
A partner is a company that supplies riders, and until now the platform could
|
||||||
|
only READ them: `getpartners` has always existed and nothing could create one.
|
||||||
|
The five partners live today were inserted by hand, which is also why two of
|
||||||
|
them are named "Test".
|
||||||
|
|
||||||
|
Where a partner works is recorded twice, on purpose and not by accident:
|
||||||
|
|
||||||
|
partnerinfo.applocationid their home region — `GetPartners` filters on it
|
||||||
|
and the rider app reads it
|
||||||
|
partnerlocations every region they cover
|
||||||
|
|
||||||
|
Both are kept in step here. Writing only the first would confine a partner to
|
||||||
|
one city, and writing only the second would hide them from every existing
|
||||||
|
query. */
|
||||||
|
|
||||||
|
// CreatePartner onboards a delivery partner and records the regions they cover.
|
||||||
|
func (r *partnerRepository) CreatePartner(input models.NewPartner) (int, error) {
|
||||||
|
if strings.TrimSpace(input.Partnername) == "" {
|
||||||
|
return 0, errors.New("the partner needs a name")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(input.Primarycontact) == "" {
|
||||||
|
return 0, errors.New("the partner needs a contact number")
|
||||||
|
}
|
||||||
|
// A district that is not open yet is opened here rather than refused — the
|
||||||
|
// form offers all 38 and this is what makes that true. Resolved before the
|
||||||
|
// checks below, so everything after works on a region that exists.
|
||||||
|
if input.Applocationid == 0 && strings.TrimSpace(input.District) != "" {
|
||||||
|
opened, err := r.EnsureRegion(input.District)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
input.Applocationid = opened
|
||||||
|
}
|
||||||
|
if input.Applocationid == 0 {
|
||||||
|
return 0, errors.New("the partner needs a district to work in")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A region with no config row hides every rider placed in it — the same
|
||||||
|
// guard `CreateRider` applies, for the same reason.
|
||||||
|
regions := regionsOf(input)
|
||||||
|
for _, aid := range regions {
|
||||||
|
var configs int64
|
||||||
|
if err := r.db.Table("app_locationconfig").Where("applocationid = ?", aid).Count(&configs).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if configs == 0 {
|
||||||
|
return 0, fmt.Errorf("region %d is not configured, so riders placed in it would not appear anywhere", aid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two partners on one number is how the wrong company gets the work.
|
||||||
|
var clash int64
|
||||||
|
if err := r.db.Table("partnerinfo").
|
||||||
|
Where("primarycontact = ?", strings.TrimSpace(input.Primarycontact)).
|
||||||
|
Count(&clash).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if clash > 0 {
|
||||||
|
return 0, fmt.Errorf("a partner already uses %s", strings.TrimSpace(input.Primarycontact))
|
||||||
|
}
|
||||||
|
|
||||||
|
status := strings.TrimSpace(input.Status)
|
||||||
|
if status == "" {
|
||||||
|
status = "Active"
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := r.db.Begin()
|
||||||
|
if tx.Error != nil {
|
||||||
|
return 0, tx.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
row := map[string]any{
|
||||||
|
"partnername": strings.TrimSpace(input.Partnername),
|
||||||
|
"companyname": strings.TrimSpace(input.Companyname),
|
||||||
|
"registrationno": strings.TrimSpace(input.Registrationno),
|
||||||
|
"primarycontact": strings.TrimSpace(input.Primarycontact),
|
||||||
|
"primaryemail": strings.TrimSpace(input.Primaryemail),
|
||||||
|
"contactno": strings.TrimSpace(input.Contactno),
|
||||||
|
"address": input.Address,
|
||||||
|
"suburb": input.Suburb,
|
||||||
|
"city": input.City,
|
||||||
|
"state": input.State,
|
||||||
|
"postcode": input.Postcode,
|
||||||
|
"partnerinfo": input.Partnerinfo,
|
||||||
|
"partnerimage": input.Partnerimage,
|
||||||
|
"applocationid": regions[0],
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
|
|
||||||
|
var partnerid int
|
||||||
|
if err := tx.Table("partnerinfo").
|
||||||
|
Clauses(clause.Returning{Columns: []clause.Column{{Name: "partnerid"}}}).
|
||||||
|
Create(&row).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if id, ok := row["partnerid"]; ok {
|
||||||
|
partnerid = toInt(id)
|
||||||
|
}
|
||||||
|
if partnerid == 0 {
|
||||||
|
tx.Rollback()
|
||||||
|
return 0, errors.New("the partner was written without an id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := replaceLocations(tx, partnerid, regions, input); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return partnerid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePartner edits a partner and, when regions are supplied, re-states them.
|
||||||
|
//
|
||||||
|
// Regions are replaced rather than merged: the console sends the whole set it
|
||||||
|
// is showing, and a merge would make removing a region impossible. Sending none
|
||||||
|
// leaves them alone, so an edit that only changes a phone number cannot empty
|
||||||
|
// the list by omission.
|
||||||
|
func (r *partnerRepository) UpdatePartner(input models.NewPartner) error {
|
||||||
|
if input.Partnerid == 0 {
|
||||||
|
return errors.New("partnerid is required")
|
||||||
|
}
|
||||||
|
if input.Applocationid == 0 && strings.TrimSpace(input.District) != "" {
|
||||||
|
opened, err := r.EnsureRegion(input.District)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
input.Applocationid = opened
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := map[string]any{}
|
||||||
|
set := func(key, value string) {
|
||||||
|
if strings.TrimSpace(value) != "" {
|
||||||
|
fields[key] = strings.TrimSpace(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set("partnername", input.Partnername)
|
||||||
|
set("companyname", input.Companyname)
|
||||||
|
set("registrationno", input.Registrationno)
|
||||||
|
set("primarycontact", input.Primarycontact)
|
||||||
|
set("primaryemail", input.Primaryemail)
|
||||||
|
set("contactno", input.Contactno)
|
||||||
|
set("address", input.Address)
|
||||||
|
set("suburb", input.Suburb)
|
||||||
|
set("city", input.City)
|
||||||
|
set("state", input.State)
|
||||||
|
set("partnerinfo", input.Partnerinfo)
|
||||||
|
set("partnerimage", input.Partnerimage)
|
||||||
|
set("status", input.Status)
|
||||||
|
if input.Postcode > 0 {
|
||||||
|
fields["postcode"] = input.Postcode
|
||||||
|
}
|
||||||
|
if input.Applocationid > 0 {
|
||||||
|
fields["applocationid"] = input.Applocationid
|
||||||
|
}
|
||||||
|
|
||||||
|
regions := regionsOf(input)
|
||||||
|
if len(fields) == 0 && len(regions) == 0 {
|
||||||
|
return errors.New("nothing to update")
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := r.db.Begin()
|
||||||
|
if tx.Error != nil {
|
||||||
|
return tx.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fields) > 0 {
|
||||||
|
fields["updated"] = gorm.Expr("NOW()")
|
||||||
|
res := tx.Table("partnerinfo").Where("partnerid = ?", input.Partnerid).Updates(fields)
|
||||||
|
if res.Error != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return res.Error
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("no partner with partnerid %d", input.Partnerid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(regions) > 0 {
|
||||||
|
if err := replaceLocations(tx, input.Partnerid, regions, input); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit().Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPartnerLocations lists the regions a partner covers, named.
|
||||||
|
func (r *partnerRepository) GetPartnerLocations(partnerid int) ([]models.PartnerLocation, error) {
|
||||||
|
var data []models.PartnerLocation
|
||||||
|
err := r.db.Raw(`
|
||||||
|
SELECT a.partnerlocationid, a.partnerid, a.applocationid,
|
||||||
|
COALESCE(b.locationname, '') AS applocation
|
||||||
|
FROM partnerlocations a
|
||||||
|
LEFT JOIN app_location b ON a.applocationid = b.applocationid
|
||||||
|
WHERE a.partnerid = ?
|
||||||
|
ORDER BY b.locationname`, partnerid).Scan(&data).Error
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Helpers ─────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
// regionsOf is every region the partner covers, home region first and no
|
||||||
|
// duplicates. One list, so the row and the link table cannot disagree.
|
||||||
|
func regionsOf(input models.NewPartner) []int {
|
||||||
|
if input.Applocationid > 0 {
|
||||||
|
return []int{input.Applocationid}
|
||||||
|
}
|
||||||
|
return []int{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// replaceLocations re-states a partner's regions inside the caller's tx.
|
||||||
|
func replaceLocations(tx *gorm.DB, partnerid int, regions []int, input models.NewPartner) error {
|
||||||
|
if len(regions) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := tx.Table("partnerlocations").Where("partnerid = ?", partnerid).Delete(nil).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rows := make([]map[string]any, 0, len(regions))
|
||||||
|
for _, aid := range regions {
|
||||||
|
rows = append(rows, map[string]any{
|
||||||
|
"partnerid": partnerid,
|
||||||
|
"applocationid": aid,
|
||||||
|
"address": input.Address,
|
||||||
|
"suburb": input.Suburb,
|
||||||
|
"city": input.City,
|
||||||
|
"state": input.State,
|
||||||
|
"postcode": strconv.Itoa(input.Postcode),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return tx.Table("partnerlocations").Create(&rows).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// toInt reads the id a RETURNING clause handed back, whatever numeric type the
|
||||||
|
// driver chose for it.
|
||||||
|
func toInt(value any) int {
|
||||||
|
switch n := value.(type) {
|
||||||
|
case int:
|
||||||
|
return n
|
||||||
|
case int32:
|
||||||
|
return int(n)
|
||||||
|
case int64:
|
||||||
|
return int(n)
|
||||||
|
case float64:
|
||||||
|
return int(n)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Opening a district.
|
||||||
|
|
||||||
|
`app_location` is not a geography table — it is the list of places Nearle
|
||||||
|
actually runs, each with a radius, opening hours and an image. Three rows exist.
|
||||||
|
That is why a partner could only be placed in three of Tamil Nadu's thirty-eight
|
||||||
|
districts: `partnerinfo.applocationid` has to point at one of these rows, every
|
||||||
|
rider query joins through it, and `CreateRider` refuses a region with no
|
||||||
|
`app_locationconfig`.
|
||||||
|
|
||||||
|
So onboarding a partner in a new district OPENS the district: it writes both
|
||||||
|
rows, copying the operating defaults from a region already running rather than
|
||||||
|
inventing them. The alternative was a form that lists 38 districts and accepts
|
||||||
|
3, which is a form that lies.
|
||||||
|
|
||||||
|
── Why the id is computed rather than defaulted ────────────────────────────
|
||||||
|
|
||||||
|
`app_location.applocationid` is a plain bigint: no identity, no default, no
|
||||||
|
sequence — the same shape as `productcategories.categoryid`. Every insert has to
|
||||||
|
supply one, so two people onboarding partners at the same moment would both read
|
||||||
|
the same MAX and write the same id. The advisory lock serialises that, and the
|
||||||
|
name lookup inside it makes a repeat call return the existing row instead of a
|
||||||
|
second Erode.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// EnsureRegion returns the applocationid for a district, opening it if needed.
|
||||||
|
func (r *partnerRepository) EnsureRegion(district string) (int, error) {
|
||||||
|
name := strings.TrimSpace(district)
|
||||||
|
if name == "" {
|
||||||
|
return 0, errors.New("a district name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if id := r.regionByName(r.db, name); id > 0 {
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := r.db.Begin()
|
||||||
|
if tx.Error != nil {
|
||||||
|
return 0, tx.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// One writer at a time. The key is arbitrary and constant — it names this
|
||||||
|
// operation, not a row.
|
||||||
|
if err := tx.Exec(`SELECT pg_advisory_xact_lock(?)`, 8412771).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checked again INSIDE the lock: the request that was waiting for it may
|
||||||
|
// have been opening the same district.
|
||||||
|
if id := r.regionByName(tx, name); id > 0 {
|
||||||
|
tx.Rollback()
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextID int
|
||||||
|
if err := tx.Raw(`SELECT COALESCE(MAX(applocationid), 0) + 1 FROM app_location`).
|
||||||
|
Scan(&nextID).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Operating defaults copied from a region already running, so a new
|
||||||
|
// district behaves like the ones that work rather than like a blank row.
|
||||||
|
var template struct {
|
||||||
|
Countryid int
|
||||||
|
Radius int
|
||||||
|
Opentime string
|
||||||
|
Closetime string
|
||||||
|
}
|
||||||
|
if err := tx.Raw(`
|
||||||
|
SELECT COALESCE(countryid, 0) AS countryid, COALESCE(radius, 18) AS radius,
|
||||||
|
COALESCE(opentime, '08:00:00') AS opentime,
|
||||||
|
COALESCE(closetime, '23:59:00') AS closetime
|
||||||
|
FROM app_location WHERE status = 'Active' ORDER BY applocationid LIMIT 1`).
|
||||||
|
Scan(&template).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if template.Radius == 0 {
|
||||||
|
template.Radius = 18
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Exec(`
|
||||||
|
INSERT INTO app_location
|
||||||
|
(applocationid, countryid, locationname, city, state, radius,
|
||||||
|
deliveryradius, opentime, closetime, status)
|
||||||
|
VALUES (?, ?, ?, ?, 'Tamil Nadu', ?, ?, ?, ?, 'Active')`,
|
||||||
|
nextID, template.Countryid, name, name, template.Radius, template.Radius,
|
||||||
|
template.Opentime, template.Closetime).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without this row every rider in the district is invisible — `getriders`
|
||||||
|
// joins it, and `CreateRider` refuses a region that lacks it. Opening a
|
||||||
|
// district means both rows or neither.
|
||||||
|
var nextConfig int
|
||||||
|
if err := tx.Raw(`SELECT COALESCE(MAX(applocationconfigid), 0) + 1 FROM app_locationconfig`).
|
||||||
|
Scan(&nextConfig).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if err := tx.Exec(`
|
||||||
|
INSERT INTO app_locationconfig (applocationconfigid, applocationid, configid, status)
|
||||||
|
VALUES (?, ?, 1, 'Active')`, nextConfig, nextID).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return nextID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// regionByName finds a district by name, case-insensitively and space-tolerant.
|
||||||
|
func (r *partnerRepository) regionByName(db *gorm.DB, name string) int {
|
||||||
|
var id int
|
||||||
|
db.Raw(`SELECT applocationid FROM app_location
|
||||||
|
WHERE LOWER(TRIM(locationname)) = LOWER(TRIM(?)) LIMIT 1`, name).Scan(&id)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ type TenantRepository interface {
|
|||||||
CreateTenantUser(data models.Tenants) (bool, error)
|
CreateTenantUser(data models.Tenants) (bool, error)
|
||||||
GetUserByNo(cno string) models.UserInfo
|
GetUserByNo(cno string) models.UserInfo
|
||||||
GetTenantByID(tid int, locationid int, userid int) (models.Tenantinfo, error)
|
GetTenantByID(tid int, locationid int, userid int) (models.Tenantinfo, error)
|
||||||
|
AssignPartner(tenantID, partnerID int) error
|
||||||
GetTenantByKeyword(keyword string) ([]models.TenantSearch, error)
|
GetTenantByKeyword(keyword string) ([]models.TenantSearch, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -956,3 +957,43 @@ func (r *tenantRepository) UpdateOwnProfile(userID, tenantID int, fields map[str
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AssignPartner sets which delivery partner supplies a merchant's riders.
|
||||||
|
//
|
||||||
|
// A route of its own rather than a field on `updatetenant`, and that is the
|
||||||
|
// whole point. `partnerid` is deliberately absent from `editableTenantFields`:
|
||||||
|
// a merchant who could set it would move themselves under another partner's
|
||||||
|
// billing and pick up their riders. This is the platform's decision, so it gets
|
||||||
|
// the platform's endpoint.
|
||||||
|
//
|
||||||
|
// `partnerid` 0 is meaningful and allowed — it takes the partner away, which a
|
||||||
|
// merchant switching to their own riders needs. So the value is written rather
|
||||||
|
// than skipped when zero, unlike everywhere else in this file.
|
||||||
|
func (r *tenantRepository) AssignPartner(tenantID, partnerID int) error {
|
||||||
|
if tenantID <= 0 {
|
||||||
|
return errors.New("tenantid is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A partner that does not exist would silently orphan every rider lookup
|
||||||
|
// the merchant makes afterwards — `getriders?partnerid=` would answer 200
|
||||||
|
// with nothing, which reads as "no riders on duty".
|
||||||
|
if partnerID > 0 {
|
||||||
|
var found int64
|
||||||
|
if err := r.db.Table("partnerinfo").Where("partnerid = ?", partnerID).Count(&found).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if found == 0 {
|
||||||
|
return fmt.Errorf("no delivery partner with partnerid %d", partnerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res := r.db.Table("tenants").Where("tenantid = ?", tenantID).
|
||||||
|
Updates(map[string]any{"partnerid": partnerID, "updated": gorm.Expr("NOW()")})
|
||||||
|
if res.Error != nil {
|
||||||
|
return res.Error
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return fmt.Errorf("no business with tenantid %d", tenantID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,6 +26,18 @@ func RegisterPartnerRoutes(api fiber.Router, f *facade.Facade) {
|
|||||||
//
|
//
|
||||||
// Web group only. A rider is onboarded from the merchant's console, not from
|
// Web group only. A rider is onboarded from the merchant's console, not from
|
||||||
// a phone, so the mobile group below stays read-only as it was.
|
// a phone, so the mobile group below stays read-only as it was.
|
||||||
|
// Onboarding the COMPANY that supplies riders, as opposed to the riders
|
||||||
|
// themselves. `getpartners` has always existed and nothing could create
|
||||||
|
// one, which is why the five partners live today were inserted by hand —
|
||||||
|
// two of them are still named "Test".
|
||||||
|
//
|
||||||
|
// Web group only, and deliberately not in the mob group below: a partner is
|
||||||
|
// onboarded by the platform from a console, never from a phone and never by
|
||||||
|
// a merchant. A merchant's side of this is being assigned one.
|
||||||
|
partner.Post("/createpartner", f.PartnerController.CreatePartner)
|
||||||
|
partner.Put("/updatepartner", f.PartnerController.UpdatePartner)
|
||||||
|
partner.Get("/getpartnerlocations", f.PartnerController.GetPartnerLocations)
|
||||||
|
|
||||||
partner.Post("/createrider", f.PartnerController.CreateRider)
|
partner.Post("/createrider", f.PartnerController.CreateRider)
|
||||||
partner.Put("/updaterider", f.PartnerController.UpdateRider)
|
partner.Put("/updaterider", f.PartnerController.UpdateRider)
|
||||||
partner.Get("/getriderroster", f.PartnerController.GetRiderRoster)
|
partner.Get("/getriderroster", f.PartnerController.GetRiderRoster)
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ func RegisterTenantRoutes(api fiber.Router, f *facade.Facade) {
|
|||||||
// two was simply not found. A profile screen cannot be built on that.
|
// two was simply not found. A profile screen cannot be built on that.
|
||||||
tenant.Get("/gettenantinfo", f.TenantController.GetTenantInfo)
|
tenant.Get("/gettenantinfo", f.TenantController.GetTenantInfo)
|
||||||
|
|
||||||
|
// Which delivery partner supplies this merchant's riders.
|
||||||
|
//
|
||||||
|
// Its own route rather than a field on updatetenant: `partnerid` is kept
|
||||||
|
// out of the merchant-editable allowlist on purpose, because a merchant
|
||||||
|
// who could set it would move themselves under another partner.
|
||||||
|
tenant.Put("/assignpartner", f.TenantController.AssignPartner)
|
||||||
|
|
||||||
// A merchant editing their own business record. The first write path the
|
// A merchant editing their own business record. The first write path the
|
||||||
// `tenants` table has ever had — see UpdateTenantProfile for what may be
|
// `tenants` table has ever had — see UpdateTenantProfile for what may be
|
||||||
// set, and what deliberately may not.
|
// set, and what deliberately may not.
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ type PartnerService interface {
|
|||||||
CreateRider(rider models.NewRider) (int, error)
|
CreateRider(rider models.NewRider) (int, error)
|
||||||
UpdateRider(rider models.NewRider) error
|
UpdateRider(rider models.NewRider) error
|
||||||
GetRiderRoster(tid, aid, pid int) ([]models.RiderRosterRow, error)
|
GetRiderRoster(tid, aid, pid int) ([]models.RiderRosterRow, error)
|
||||||
|
CreatePartner(input models.NewPartner) (int, error)
|
||||||
|
UpdatePartner(input models.NewPartner) error
|
||||||
|
GetPartnerLocations(partnerid int) ([]models.PartnerLocation, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type partnerService struct {
|
type partnerService struct {
|
||||||
@@ -73,3 +76,19 @@ func (s *partnerService) UpdateRider(rider models.NewRider) error {
|
|||||||
func (s *partnerService) GetRiderRoster(tid, aid, pid int) ([]models.RiderRosterRow, error) {
|
func (s *partnerService) GetRiderRoster(tid, aid, pid int) ([]models.RiderRosterRow, error) {
|
||||||
return s.repo.GetRiderRoster(tid, aid, pid)
|
return s.repo.GetRiderRoster(tid, aid, pid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Partners ─────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
// CreatePartner onboards a delivery partner. Platform-side only — a merchant is
|
||||||
|
// ASSIGNED a partner, never allowed to invent one.
|
||||||
|
func (s *partnerService) CreatePartner(input models.NewPartner) (int, error) {
|
||||||
|
return s.repo.CreatePartner(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *partnerService) UpdatePartner(input models.NewPartner) error {
|
||||||
|
return s.repo.UpdatePartner(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *partnerService) GetPartnerLocations(partnerid int) ([]models.PartnerLocation, error) {
|
||||||
|
return s.repo.GetPartnerLocations(partnerid)
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type TenantService interface {
|
|||||||
GetTenantLocations(tid int) ([]models.Tenantlocations, error)
|
GetTenantLocations(tid int) ([]models.Tenantlocations, error)
|
||||||
GetTenantSlot() (models.Tenantslot, error)
|
GetTenantSlot() (models.Tenantslot, error)
|
||||||
CreateTenantCustomer(req models.CreateTenantCustomerRequest) (*models.Tenantcustomer, error)
|
CreateTenantCustomer(req models.CreateTenantCustomerRequest) (*models.Tenantcustomer, error)
|
||||||
|
AssignPartner(tenantID, partnerID int) error
|
||||||
GetCustomerTenants(customerID int, categoryID int, tenantFlag int) (*models.CustomerTenantResponse, error)
|
GetCustomerTenants(customerID int, categoryID int, tenantFlag int) (*models.CustomerTenantResponse, error)
|
||||||
GetTenantPricing(tid, aid int) (models.Tenantpricing, error)
|
GetTenantPricing(tid, aid int) (models.Tenantpricing, error)
|
||||||
UpdateLocation(input models.Tenantlocations) error
|
UpdateLocation(input models.Tenantlocations) error
|
||||||
@@ -259,3 +260,11 @@ func (s *tenantService) UpdateOwnProfile(userID, tenantID int, fields map[string
|
|||||||
}
|
}
|
||||||
return s.repo.UpdateOwnProfile(userID, tenantID, clean)
|
return s.repo.UpdateOwnProfile(userID, tenantID, clean)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AssignPartner records which delivery partner supplies a merchant's riders.
|
||||||
|
//
|
||||||
|
// Not part of UpdateTenantProfile: that endpoint is the merchant's own, and
|
||||||
|
// which partner they sit under is the platform's call. See the repository.
|
||||||
|
func (s *tenantService) AssignPartner(tenantID, partnerID int) error {
|
||||||
|
return s.repo.AssignPartner(tenantID, partnerID)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user