rider creation
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"nearle/models"
|
||||||
"nearle/services"
|
"nearle/services"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -193,3 +194,119 @@ func (ctl *PartnerController) GetRiderInfo(c *fiber.Ctx) error {
|
|||||||
"details": result,
|
"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 rider.Tenantid == 0 {
|
||||||
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"code": http.StatusBadRequest,
|
||||||
|
"status": false,
|
||||||
|
"message": "tenantid is required",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -129,3 +129,110 @@ type RiderlogDetails struct {
|
|||||||
Breakhours float32 `json:"breakhours"`
|
Breakhours float32 `json:"breakhours"`
|
||||||
Logstatus int `json:"logstatus"`
|
Logstatus int `json:"logstatus"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewRider is one rider being onboarded, as the console sends it.
|
||||||
|
//
|
||||||
|
// A rider is three rows, not one. `app_users` holds the person, `ridersettings`
|
||||||
|
// the vehicle and licence, and `app_userpools` their place in the availability
|
||||||
|
// pool — and `getriders` INNER JOINs all three, so a rider missing any of them
|
||||||
|
// is not a partial rider, they are no rider at all. The flat shape here is
|
||||||
|
// deliberate: the caller should not have to know the table layout to hire
|
||||||
|
// somebody.
|
||||||
|
//
|
||||||
|
// `Tenantid` is filled in by the controller from the caller's scope, never read
|
||||||
|
// from the body — a store admin must not be able to put a rider on another
|
||||||
|
// merchant's books by editing a payload.
|
||||||
|
type NewRider struct {
|
||||||
|
Userid int `json:"userid"`
|
||||||
|
Firstname string `json:"firstname"`
|
||||||
|
Lastname string `json:"lastname"`
|
||||||
|
Contactno string `json:"contactno"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
Suburb string `json:"suburb"`
|
||||||
|
City string `json:"city"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Postcode string `json:"postcode"`
|
||||||
|
|
||||||
|
// Whose rider they are, where they ride, and who they ride for.
|
||||||
|
Tenantid int `json:"tenantid"`
|
||||||
|
Applocationid int `json:"applocationid"`
|
||||||
|
Partnerid int `json:"partnerid"`
|
||||||
|
Shiftid int `json:"shiftid"`
|
||||||
|
|
||||||
|
// The vehicle half — `ridersettings`.
|
||||||
|
Identificationno string `json:"identificationno"`
|
||||||
|
Vehiclename string `json:"vehiclename"`
|
||||||
|
Vehicleno string `json:"vehicleno"`
|
||||||
|
Licenseno string `json:"licenseno"`
|
||||||
|
Registrationno string `json:"registrationno"`
|
||||||
|
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RiderRosterRow is one rider in the directory.
|
||||||
|
//
|
||||||
|
// Distinct from RiderInfo, which is the ON-DUTY read: that one requires a
|
||||||
|
// riderlog stamped today, which is right for an assignment picker and wrong for
|
||||||
|
// a staff list, where somebody who has not started their shift must still
|
||||||
|
// appear. This carries the last log rather than requiring one.
|
||||||
|
type RiderRosterRow struct {
|
||||||
|
Userid int `json:"userid"`
|
||||||
|
Firstname string `json:"firstname"`
|
||||||
|
Lastname string `json:"lastname"`
|
||||||
|
Fullname string `json:"fullname"`
|
||||||
|
Contactno string `json:"contactno"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Tenantid int `json:"tenantid"`
|
||||||
|
Applocationid int `json:"applocationid"`
|
||||||
|
Applocation string `json:"applocation"`
|
||||||
|
Partnerid int `json:"partnerid"`
|
||||||
|
Partnername string `json:"partnername"`
|
||||||
|
Shiftid int `json:"shiftid"`
|
||||||
|
Shiftname string `json:"shiftname"`
|
||||||
|
|
||||||
|
Identificationno string `json:"identificationno"`
|
||||||
|
Vehiclename string `json:"vehiclename"`
|
||||||
|
Vehicleno string `json:"vehicleno"`
|
||||||
|
Licenseno string `json:"licenseno"`
|
||||||
|
Registrationno string `json:"registrationno"`
|
||||||
|
|
||||||
|
// Duty state, as facts rather than as a filter.
|
||||||
|
Onduty int `json:"onduty"`
|
||||||
|
Lastlogdate string `json:"lastlogdate"`
|
||||||
|
// True when there is a log dated today with logstatus 0 — on shift right now.
|
||||||
|
Isonduty bool `json:"isonduty"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ridersettings is the vehicle-and-licence half of a rider.
|
||||||
|
//
|
||||||
|
// `riderid` is DELIBERATELY ABSENT. It is a GENERATED ALWAYS identity column,
|
||||||
|
// and including it makes GORM send a zero, which Postgres rejects outright:
|
||||||
|
// "cannot insert a non-DEFAULT value into column riderid". Leaving it off the
|
||||||
|
// struct is what keeps the insert to the columns that are actually ours to set.
|
||||||
|
type Ridersettings struct {
|
||||||
|
Userid int `json:"userid"`
|
||||||
|
Partnerid int `json:"partnerid"`
|
||||||
|
Shiftid int `json:"shiftid"`
|
||||||
|
Identificationno string `json:"identificationno"`
|
||||||
|
Vehiclename string `json:"vehiclename"`
|
||||||
|
Vehicleno string `json:"vehicleno"`
|
||||||
|
Licenseno string `json:"licenseno"`
|
||||||
|
Registrationno string `json:"registrationno"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Appuserpools is a rider's place in the availability pool.
|
||||||
|
//
|
||||||
|
// `poolid` is absent for the same reason as `riderid` above — same identity
|
||||||
|
// column, same rejection.
|
||||||
|
//
|
||||||
|
// `Onduty` means "may be given work", not "on shift right now". The second
|
||||||
|
// question is answered by riderlogs, which the rider's own app writes.
|
||||||
|
type Appuserpools struct {
|
||||||
|
Userid int `json:"userid"`
|
||||||
|
Partnerid int `json:"partnerid"`
|
||||||
|
Onduty int `json:"onduty"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package repositories
|
package repositories
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"nearle/models"
|
"nearle/models"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -16,6 +18,9 @@ type PartnerRepository interface {
|
|||||||
GetRiderLogs(pid, aid int, fdate, tdate string) ([]models.RiderlogDetails, error)
|
GetRiderLogs(pid, aid int, fdate, tdate string) ([]models.RiderlogDetails, error)
|
||||||
GetRiderInfo(userid int) (models.RiderInfo, error)
|
GetRiderInfo(userid int) (models.RiderInfo, error)
|
||||||
GetFleetSummary(aid, tid int, fdate, tdate string) (models.FleetSummary, error)
|
GetFleetSummary(aid, tid int, fdate, tdate string) (models.FleetSummary, error)
|
||||||
|
CreateRider(rider models.NewRider) (int, error)
|
||||||
|
UpdateRider(rider models.NewRider) error
|
||||||
|
GetRiderRoster(tid, aid, pid int) ([]models.RiderRosterRow, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type partnerRepository struct {
|
type partnerRepository struct {
|
||||||
@@ -308,3 +313,281 @@ func (r *partnerRepository) GetFleetSummary(aid, tid int, fdate, tdate string) (
|
|||||||
|
|
||||||
return summary, nil
|
return summary, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateRider hires one rider, in one transaction.
|
||||||
|
//
|
||||||
|
// Three tables, all or nothing. Before this existed the only way to make a
|
||||||
|
// rider was POST /users/create, which writes `app_users` and stops — so it
|
||||||
|
// answered 201 Created and produced somebody every rider query ignored forever,
|
||||||
|
// because `getriders` INNER JOINs `ridersettings` and `app_userpools` as well.
|
||||||
|
// The old console papered over that by generating an SQL script for an operator
|
||||||
|
// to run by hand; the script named three columns that do not exist, so it never
|
||||||
|
// worked either.
|
||||||
|
//
|
||||||
|
// The guards below all catch the same class of fault: a write that succeeds and
|
||||||
|
// then cannot be seen. Postgres will not complain about any of them — a
|
||||||
|
// `shiftid` pointing nowhere is a perfectly good integer — but each one produces
|
||||||
|
// a rider who is invisible the moment the transaction commits.
|
||||||
|
func (r *partnerRepository) CreateRider(rider models.NewRider) (int, error) {
|
||||||
|
if strings.TrimSpace(rider.Firstname) == "" {
|
||||||
|
return 0, errors.New("the rider needs a name")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(rider.Contactno) == "" {
|
||||||
|
return 0, errors.New("the rider needs a contact number")
|
||||||
|
}
|
||||||
|
if rider.Applocationid == 0 {
|
||||||
|
return 0, errors.New("the rider needs a delivery region")
|
||||||
|
}
|
||||||
|
if rider.Shiftid == 0 {
|
||||||
|
return 0, errors.New("the rider needs a shift")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A shift that does not exist takes the rider out of every listing:
|
||||||
|
// `getriders` joins ridershifts through ridersettings.shiftid.
|
||||||
|
var shifts int64
|
||||||
|
if err := r.db.Table("ridershifts").Where("shiftid = ?", rider.Shiftid).Count(&shifts).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if shifts == 0 {
|
||||||
|
return 0, fmt.Errorf("shift %d does not exist", rider.Shiftid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same again for the region, which is joined twice — app_location AND
|
||||||
|
// app_locationconfig. A region with no config row hides every rider in it.
|
||||||
|
var configs int64
|
||||||
|
if err := r.db.Table("app_locationconfig").Where("applocationid = ?", rider.Applocationid).Count(&configs).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if configs == 0 {
|
||||||
|
return 0, fmt.Errorf("delivery region %d is not configured, so a rider added to it would not appear anywhere", rider.Applocationid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Riders are looked up by phone more than by anything else, and two accounts
|
||||||
|
// on one number is how the wrong person gets the job.
|
||||||
|
var clash int64
|
||||||
|
if err := r.db.Table("app_users").
|
||||||
|
Where("contactno = ? AND configid = 6", strings.TrimSpace(rider.Contactno)).
|
||||||
|
Count(&clash).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if clash > 0 {
|
||||||
|
return 0, fmt.Errorf("a rider already uses %s", strings.TrimSpace(rider.Contactno))
|
||||||
|
}
|
||||||
|
|
||||||
|
status := strings.TrimSpace(rider.Status)
|
||||||
|
if status == "" {
|
||||||
|
status = "Active"
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := r.db.Begin()
|
||||||
|
if tx.Error != nil {
|
||||||
|
return 0, tx.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// configid 6 is what identifies a rider — there is no Rider row in
|
||||||
|
// app_roles, and `getriders` keys on the configid rather than on a role.
|
||||||
|
user := models.User{
|
||||||
|
Firstname: strings.TrimSpace(rider.Firstname),
|
||||||
|
Lastname: strings.TrimSpace(rider.Lastname),
|
||||||
|
Contactno: strings.TrimSpace(rider.Contactno),
|
||||||
|
Email: strings.TrimSpace(rider.Email),
|
||||||
|
Password: rider.Password,
|
||||||
|
Address: rider.Address,
|
||||||
|
Suburb: rider.Suburb,
|
||||||
|
City: rider.City,
|
||||||
|
State: rider.State,
|
||||||
|
Postcode: rider.Postcode,
|
||||||
|
Configid: 6,
|
||||||
|
Tenantid: rider.Tenantid,
|
||||||
|
Applocationid: rider.Applocationid,
|
||||||
|
Partnerid: rider.Partnerid,
|
||||||
|
Shiftid: rider.Shiftid,
|
||||||
|
Status: status,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Table("app_users").Create(&user).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if user.Userid == 0 {
|
||||||
|
tx.Rollback()
|
||||||
|
return 0, errors.New("the rider account was written without an id")
|
||||||
|
}
|
||||||
|
|
||||||
|
settings := models.Ridersettings{
|
||||||
|
Userid: user.Userid,
|
||||||
|
Partnerid: rider.Partnerid,
|
||||||
|
Shiftid: rider.Shiftid,
|
||||||
|
Identificationno: rider.Identificationno,
|
||||||
|
Vehiclename: rider.Vehiclename,
|
||||||
|
Vehicleno: rider.Vehicleno,
|
||||||
|
Licenseno: rider.Licenseno,
|
||||||
|
Registrationno: rider.Registrationno,
|
||||||
|
}
|
||||||
|
if err := tx.Table("ridersettings").Create(&settings).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// onduty 1 means "available for work", not "on shift now" — that second
|
||||||
|
// question is answered by riderlogs, which the rider's own app writes when
|
||||||
|
// they clock on. A rider created here is therefore correctly absent from
|
||||||
|
// getriders until they start a shift.
|
||||||
|
pool := models.Appuserpools{
|
||||||
|
Userid: user.Userid,
|
||||||
|
Partnerid: rider.Partnerid,
|
||||||
|
Onduty: 1,
|
||||||
|
Status: status,
|
||||||
|
}
|
||||||
|
if err := tx.Table("app_userpools").Create(&pool).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.Userid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRiderRoster lists every rider, on duty or not.
|
||||||
|
//
|
||||||
|
// Distinct from GetActiveRiders, and the difference is the whole point. That
|
||||||
|
// one INNER JOINs a riderlog dated today with logstatus 0 — it answers "who can
|
||||||
|
// I give this delivery to right now", which is correct for an assignment picker
|
||||||
|
// and useless for a staff directory: somebody hired this morning, or simply not
|
||||||
|
// working today, is absent from it. A directory that hides the person you just
|
||||||
|
// created reads as a failed save.
|
||||||
|
//
|
||||||
|
// So every join here is LEFT except the rider's own settings, and the duty
|
||||||
|
// state is returned as facts — `onduty`, the last log date, and whether that
|
||||||
|
// log is today — rather than used as a filter.
|
||||||
|
func (r *partnerRepository) GetRiderRoster(tid, aid, pid int) ([]models.RiderRosterRow, error) {
|
||||||
|
var data []models.RiderRosterRow
|
||||||
|
|
||||||
|
q1 := `SELECT a.userid, a.firstname, a.lastname,
|
||||||
|
CONCAT(a.firstname, ' ', a.lastname) AS fullname,
|
||||||
|
a.contactno, a.email, a.tenantid, a.applocationid, a.partnerid, a.status,
|
||||||
|
f.locationname AS applocation,
|
||||||
|
p.partnername,
|
||||||
|
c.identificationno, c.vehiclename, c.vehicleno, c.licenseno, c.registrationno,
|
||||||
|
c.shiftid, CONCAT(d.starttime, ' - ', d.endtime) AS shiftname,
|
||||||
|
COALESCE(b.onduty, 0) AS onduty,
|
||||||
|
e.logdate AS lastlogdate,
|
||||||
|
COALESCE(e.logdate::date = CURRENT_DATE AND e.logstatus = 0, false) AS isonduty
|
||||||
|
FROM app_users a
|
||||||
|
INNER JOIN ridersettings c ON a.userid = c.userid
|
||||||
|
LEFT JOIN app_userpools b ON a.userid = b.userid
|
||||||
|
LEFT JOIN ridershifts d ON c.shiftid = d.shiftid
|
||||||
|
LEFT JOIN app_location f ON a.applocationid = f.applocationid
|
||||||
|
LEFT JOIN partnerinfo p ON a.partnerid = p.partnerid
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT r1.userid, r1.logdate, r1.logstatus
|
||||||
|
FROM riderlogs r1
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT userid, MAX(logdate) AS max_logdate FROM riderlogs GROUP BY userid
|
||||||
|
) r2 ON r1.userid = r2.userid AND r1.logdate = r2.max_logdate
|
||||||
|
) e ON a.userid = e.userid
|
||||||
|
WHERE a.configid = 6`
|
||||||
|
|
||||||
|
var args []interface{}
|
||||||
|
// Scoped by whichever id the caller has. Tenant first: it is the narrowest,
|
||||||
|
// and it is the scope a merchant's own directory wants.
|
||||||
|
if tid != 0 {
|
||||||
|
q1 += ` AND a.tenantid = ?`
|
||||||
|
args = append(args, tid)
|
||||||
|
} else if aid != 0 {
|
||||||
|
q1 += ` AND a.applocationid = ?`
|
||||||
|
args = append(args, aid)
|
||||||
|
} else if pid != 0 {
|
||||||
|
q1 += ` AND a.partnerid = ?`
|
||||||
|
args = append(args, pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
q1 += ` ORDER BY a.firstname, a.lastname`
|
||||||
|
|
||||||
|
if err := r.db.Raw(q1, args...).Scan(&data).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRider edits a rider across both of their tables.
|
||||||
|
//
|
||||||
|
// Only the fields a person can change from the console: their contact details,
|
||||||
|
// their vehicle and licence, their shift, and their status. The identity
|
||||||
|
// columns and the tenant are not editable — moving a rider between merchants is
|
||||||
|
// not an edit, and doing it silently through a profile form is how a rider ends
|
||||||
|
// up on somebody else's books.
|
||||||
|
func (r *partnerRepository) UpdateRider(rider models.NewRider) error {
|
||||||
|
if rider.Userid == 0 {
|
||||||
|
return errors.New("userid is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rider.Shiftid != 0 {
|
||||||
|
var shifts int64
|
||||||
|
if err := r.db.Table("ridershifts").Where("shiftid = ?", rider.Shiftid).Count(&shifts).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if shifts == 0 {
|
||||||
|
return fmt.Errorf("shift %d does not exist", rider.Shiftid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := r.db.Begin()
|
||||||
|
if tx.Error != nil {
|
||||||
|
return tx.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Built as a map rather than a struct: Updates() with a struct skips every
|
||||||
|
// zero value, so clearing a licence number or blanking an email would
|
||||||
|
// silently do nothing. A map says exactly what to write.
|
||||||
|
user := map[string]interface{}{
|
||||||
|
"firstname": strings.TrimSpace(rider.Firstname),
|
||||||
|
"lastname": strings.TrimSpace(rider.Lastname),
|
||||||
|
"contactno": strings.TrimSpace(rider.Contactno),
|
||||||
|
"email": strings.TrimSpace(rider.Email),
|
||||||
|
"address": rider.Address,
|
||||||
|
"suburb": rider.Suburb,
|
||||||
|
"city": rider.City,
|
||||||
|
"state": rider.State,
|
||||||
|
"postcode": rider.Postcode,
|
||||||
|
}
|
||||||
|
if rider.Shiftid != 0 {
|
||||||
|
user["shiftid"] = rider.Shiftid
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(rider.Status) != "" {
|
||||||
|
user["status"] = strings.TrimSpace(rider.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
res := tx.Table("app_users").Where("userid = ? AND configid = 6", rider.Userid).Updates(user)
|
||||||
|
if res.Error != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return res.Error
|
||||||
|
}
|
||||||
|
// An UPDATE matching no rows is not an SQL error, so editing a userid that
|
||||||
|
// is not a rider would report success and change nothing.
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("rider %d not found", rider.Userid)
|
||||||
|
}
|
||||||
|
|
||||||
|
settings := map[string]interface{}{
|
||||||
|
"identificationno": rider.Identificationno,
|
||||||
|
"vehiclename": rider.Vehiclename,
|
||||||
|
"vehicleno": rider.Vehicleno,
|
||||||
|
"licenseno": rider.Licenseno,
|
||||||
|
"registrationno": rider.Registrationno,
|
||||||
|
}
|
||||||
|
if rider.Shiftid != 0 {
|
||||||
|
settings["shiftid"] = rider.Shiftid
|
||||||
|
}
|
||||||
|
if err := tx.Table("ridersettings").Where("userid = ?", rider.Userid).Updates(settings).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit().Error
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,19 @@ func RegisterPartnerRoutes(api fiber.Router, f *facade.Facade) {
|
|||||||
partner.Get("/getriderlogs", f.PartnerController.GetRiderLogs)
|
partner.Get("/getriderlogs", f.PartnerController.GetRiderLogs)
|
||||||
partner.Get("/getfleetsummary", f.PartnerController.GetFleetSummary)
|
partner.Get("/getfleetsummary", f.PartnerController.GetFleetSummary)
|
||||||
|
|
||||||
|
// Hiring a rider, and the directory that shows them before they clock on.
|
||||||
|
//
|
||||||
|
// Until these existed the only way to create a rider was POST /users/create,
|
||||||
|
// which writes app_users and stops — it answered 201 and produced somebody
|
||||||
|
// every rider query ignored, because getriders joins ridersettings and
|
||||||
|
// app_userpools too.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
partner.Post("/createrider", f.PartnerController.CreateRider)
|
||||||
|
partner.Put("/updaterider", f.PartnerController.UpdateRider)
|
||||||
|
partner.Get("/getriderroster", f.PartnerController.GetRiderRoster)
|
||||||
|
|
||||||
partner = api.Group("/v1/mob/partners")
|
partner = api.Group("/v1/mob/partners")
|
||||||
|
|
||||||
partner.Get("/getpartners", f.PartnerController.GetPartners)
|
partner.Get("/getpartners", f.PartnerController.GetPartners)
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ type PartnerService interface {
|
|||||||
GetRiderLogs(pid, aid int, fdate, tdate string) ([]models.RiderlogDetails, error)
|
GetRiderLogs(pid, aid int, fdate, tdate string) ([]models.RiderlogDetails, error)
|
||||||
GetRiderInfo(userid int) (models.RiderInfo, error)
|
GetRiderInfo(userid int) (models.RiderInfo, error)
|
||||||
GetFleetSummary(aid, tid int, fdate, tdate string) (models.FleetSummary, error)
|
GetFleetSummary(aid, tid int, fdate, tdate string) (models.FleetSummary, error)
|
||||||
|
CreateRider(rider models.NewRider) (int, error)
|
||||||
|
UpdateRider(rider models.NewRider) error
|
||||||
|
GetRiderRoster(tid, aid, pid int) ([]models.RiderRosterRow, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type partnerService struct {
|
type partnerService struct {
|
||||||
@@ -58,3 +61,15 @@ func (s *partnerService) GetRiderInfo(userid int) (models.RiderInfo, error) {
|
|||||||
func (s *partnerService) GetFleetSummary(aid, tid int, fdate, tdate string) (models.FleetSummary, error) {
|
func (s *partnerService) GetFleetSummary(aid, tid int, fdate, tdate string) (models.FleetSummary, error) {
|
||||||
return s.repo.GetFleetSummary(aid, tid, fdate, tdate)
|
return s.repo.GetFleetSummary(aid, tid, fdate, tdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *partnerService) CreateRider(rider models.NewRider) (int, error) {
|
||||||
|
return s.repo.CreateRider(rider)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *partnerService) UpdateRider(rider models.NewRider) error {
|
||||||
|
return s.repo.UpdateRider(rider)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *partnerService) GetRiderRoster(tid, aid, pid int) ([]models.RiderRosterRow, error) {
|
||||||
|
return s.repo.GetRiderRoster(tid, aid, pid)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user