rider creation

This commit is contained in:
2026-09-04 16:02:47 +05:30
parent e51ad615ed
commit 593b11f1b8
5 changed files with 535 additions and 0 deletions

View File

@@ -1,9 +1,11 @@
package repositories
import (
"errors"
"fmt"
"nearle/models"
"strconv"
"strings"
"gorm.io/gorm"
)
@@ -16,6 +18,9 @@ type PartnerRepository interface {
GetRiderLogs(pid, aid int, fdate, tdate string) ([]models.RiderlogDetails, error)
GetRiderInfo(userid int) (models.RiderInfo, 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 {
@@ -308,3 +313,281 @@ func (r *partnerRepository) GetFleetSummary(aid, tid int, fdate, tdate string) (
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
}