updates on the admincontroller and the milercontroller and changes in the creaet single order calculations as well
This commit is contained in:
@@ -1945,17 +1945,51 @@ func CreateMiler(c *fiber.Ctx) error {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
tx := db.DB.Begin()
|
||||
// The console form checks these too; the server is what every caller
|
||||
// (imports, scripts, other tools) actually goes through.
|
||||
req.Authname = strings.TrimSpace(req.Authname)
|
||||
req.Displayname = strings.TrimSpace(req.Displayname)
|
||||
req.Email = strings.ToLower(strings.TrimSpace(req.Email))
|
||||
req.Contactno = normalisePhone(req.Contactno)
|
||||
if req.Authname == "" {
|
||||
return utils.BadRequest(c, "enter the rider's login name")
|
||||
}
|
||||
if req.Displayname == "" {
|
||||
req.Displayname = req.Authname
|
||||
}
|
||||
if !indianMobile.MatchString(req.Contactno) {
|
||||
return utils.BadRequest(c, "enter a valid 10-digit Indian mobile number")
|
||||
}
|
||||
if req.Email == "" || !strings.Contains(req.Email, "@") {
|
||||
return utils.BadRequest(c, "enter a valid email address")
|
||||
}
|
||||
vehicle, ok := canonicalVehicleType(req.Defaultvehicletype)
|
||||
if !ok {
|
||||
return utils.BadRequest(c, "vehicle type must be one of "+strings.Join(milerVehicleTypes, ", "))
|
||||
}
|
||||
|
||||
appLocID := req.Applocationid
|
||||
if appLocID == 0 {
|
||||
appLocID = 1
|
||||
}
|
||||
var city models.AppLocation
|
||||
if err := db.DB.Where("applocationid = ?", appLocID).First(&city).Error; err != nil {
|
||||
return utils.BadRequest(c, "that city does not exist")
|
||||
}
|
||||
if msg := checkMilerHub(req.Hubid, appLocID); msg != "" {
|
||||
return utils.BadRequest(c, msg)
|
||||
}
|
||||
|
||||
// A client login may only create riders under its own tenant.
|
||||
tenantID := req.Tenantid
|
||||
if own := consoleTenantID(c); own != 0 {
|
||||
tenantID = own
|
||||
} else if tenantID != 0 {
|
||||
var n int64
|
||||
db.DB.Model(&models.Tenant{}).Where("tenantid = ?", tenantID).Count(&n)
|
||||
if n == 0 {
|
||||
return utils.BadRequest(c, "that client does not exist")
|
||||
}
|
||||
}
|
||||
|
||||
// Configid must match what LoginMiler looks up by — it queries
|
||||
@@ -1967,6 +2001,18 @@ func CreateMiler(c *fiber.Ctx) error {
|
||||
configID = 1001
|
||||
}
|
||||
|
||||
// One rider per phone number: login finds the rider by it.
|
||||
if milerPhoneTaken(req.Contactno, configID, 0) {
|
||||
return utils.Conflict(c, "a rider with this phone number already exists")
|
||||
}
|
||||
var emailUsers int64
|
||||
db.DB.Model(&models.AppUser{}).Where("LOWER(email) = ?", req.Email).Count(&emailUsers)
|
||||
if emailUsers > 0 {
|
||||
return utils.Conflict(c, "this email is already used by another login")
|
||||
}
|
||||
|
||||
tx := db.DB.Begin()
|
||||
|
||||
user := models.AppUser{
|
||||
Authname: req.Authname,
|
||||
Email: req.Email,
|
||||
@@ -1984,6 +2030,9 @@ func CreateMiler(c *fiber.Ctx) error {
|
||||
|
||||
if err := tx.Create(&user).Error; err != nil {
|
||||
tx.Rollback()
|
||||
if isUniqueViolation(err) {
|
||||
return utils.Conflict(c, "this email is already used by another login")
|
||||
}
|
||||
return utils.Internal(c, "failed to create miler account")
|
||||
}
|
||||
|
||||
@@ -1991,7 +2040,7 @@ func CreateMiler(c *fiber.Ctx) error {
|
||||
Userid: user.Userid,
|
||||
Displayname: req.Displayname,
|
||||
Phone: req.Contactno,
|
||||
Defaultvehicletype: req.Defaultvehicletype,
|
||||
Defaultvehicletype: vehicle,
|
||||
Availabilitystatus: constants.MilerOffline,
|
||||
Rating: 5.00,
|
||||
Applocationid: appLocID,
|
||||
@@ -2067,9 +2116,10 @@ func UpdateMiler(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
type MilerUpdate struct {
|
||||
Displayname string `json:"displayname"`
|
||||
Defaultvehicletype string `json:"defaultvehicletype"`
|
||||
Hubid *int `json:"hubid"`
|
||||
Displayname string `json:"displayname"`
|
||||
Defaultvehicletype string `json:"defaultvehicletype"`
|
||||
Hubid *int `json:"hubid"`
|
||||
Contactno *string `json:"contactno"`
|
||||
}
|
||||
|
||||
req := new(MilerUpdate)
|
||||
@@ -2077,18 +2127,53 @@ func UpdateMiler(c *fiber.Ctx) error {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if req.Displayname != "" {
|
||||
profile.Displayname = req.Displayname
|
||||
var user models.AppUser
|
||||
if err := db.DB.Where("userid = ?", profile.Userid).First(&user).Error; err != nil {
|
||||
return utils.NotFound(c, "miler not found")
|
||||
}
|
||||
|
||||
if name := strings.TrimSpace(req.Displayname); name != "" {
|
||||
profile.Displayname = name
|
||||
}
|
||||
if req.Defaultvehicletype != "" {
|
||||
profile.Defaultvehicletype = req.Defaultvehicletype
|
||||
vehicle, ok := canonicalVehicleType(req.Defaultvehicletype)
|
||||
if !ok {
|
||||
return utils.BadRequest(c, "vehicle type must be one of "+strings.Join(milerVehicleTypes, ", "))
|
||||
}
|
||||
profile.Defaultvehicletype = vehicle
|
||||
}
|
||||
if req.Hubid != nil {
|
||||
if req.Hubid != nil && (profile.Hubid == nil || *profile.Hubid != *req.Hubid) {
|
||||
// Only a CHANGED hub is checked: an older rider whose current hub is in
|
||||
// another city (or since deleted) must still be editable.
|
||||
if msg := checkMilerHub(req.Hubid, profile.Applocationid); msg != "" {
|
||||
return utils.BadRequest(c, msg)
|
||||
}
|
||||
profile.Hubid = req.Hubid
|
||||
}
|
||||
// The phone is the rider's login: a wrong or duplicate number could not be
|
||||
// corrected at all before, so the rider could never sign in.
|
||||
phone := user.Contactno
|
||||
if req.Contactno != nil {
|
||||
phone = normalisePhone(*req.Contactno)
|
||||
if !indianMobile.MatchString(phone) {
|
||||
return utils.BadRequest(c, "enter a valid 10-digit Indian mobile number")
|
||||
}
|
||||
if phone != user.Contactno && milerPhoneTaken(phone, user.Configid, user.Userid) {
|
||||
return utils.Conflict(c, "a rider with this phone number already exists")
|
||||
}
|
||||
profile.Phone = phone
|
||||
}
|
||||
profile.Updatedat = time.Now()
|
||||
|
||||
if err := db.DB.Save(profile).Error; err != nil {
|
||||
err := db.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Save(profile).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// appusers carries the login phone and the hub the hub console reads.
|
||||
return tx.Model(&models.AppUser{}).Where("userid = ?", user.Userid).
|
||||
Updates(map[string]interface{}{"contactno": phone, "hubid": profile.Hubid}).Error
|
||||
})
|
||||
if err != nil {
|
||||
return utils.Internal(c, "failed to update miler")
|
||||
}
|
||||
return utils.OK(c, profile)
|
||||
|
||||
135
controllers/milerAccount.go
Normal file
135
controllers/milerAccount.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"doormile/constants"
|
||||
"doormile/db"
|
||||
"doormile/models"
|
||||
"doormile/utils"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Rider (miler) accounts: the rules for creating, editing, blocking and
|
||||
// signing in that the console and the rider app both depend on.
|
||||
//
|
||||
// Before this file, CreateMiler checked nothing: two riders could share a phone
|
||||
// number (login then picked the oldest row, often a blocked or old account, and
|
||||
// answered "miler account is not active"), a duplicate email surfaced as a
|
||||
// bare 500, a Coimbatore rider could be attached to a Hyderabad hub, and a
|
||||
// blocked rider who was still signed in could start duty and put themselves
|
||||
// back to Available.
|
||||
|
||||
// milerVehicleTypes are the vehicle types the console offers; matched without
|
||||
// regard to case and stored in this spelling.
|
||||
var milerVehicleTypes = []string{"Bike", "Scooter", "Bicycle", "Car", "Van"}
|
||||
|
||||
func canonicalVehicleType(v string) (string, bool) {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
return "Bike", true
|
||||
}
|
||||
for _, t := range milerVehicleTypes {
|
||||
if strings.EqualFold(t, v) {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// milerLoginLookup finds the rider account for a phone number. Several rows
|
||||
// can share a number (riders created before duplicates were refused). The
|
||||
// order is: an Active miler that already has a PIN (the account the rider
|
||||
// actually uses), then an Active miler without one, then any miler row, then
|
||||
// anything else; oldest first within each, which is what the plain lookup
|
||||
// used to return. Ranking a PIN-less duplicate first would answer "incorrect
|
||||
// PIN" to a rider who signed in yesterday, and let set-pin claim the duplicate.
|
||||
func milerLoginLookup(phone string, configID int) *gorm.DB {
|
||||
return db.DB.Where("contactno = ? AND configid = ?", normalisePhone(phone), configID).
|
||||
Order(`CASE WHEN roleid = 5 AND status = 'Active' AND COALESCE(password, '') <> '' THEN 0
|
||||
WHEN roleid = 5 AND status = 'Active' THEN 1
|
||||
WHEN roleid = 5 THEN 2 ELSE 3 END, userid ASC`)
|
||||
}
|
||||
|
||||
// milerNotActiveMessage is what a rider is told when their account cannot sign
|
||||
// in; a blocked rider is told so, rather than a generic "not active".
|
||||
func milerNotActiveMessage(status string) string {
|
||||
if strings.EqualFold(status, constants.MilerBlocked) {
|
||||
return "your account is blocked — contact your manager"
|
||||
}
|
||||
return "miler account is not active"
|
||||
}
|
||||
|
||||
// milerIsBlocked reports whether ops have blocked this rider. Checked on the
|
||||
// rider-app actions that would otherwise undo a block for a rider who was
|
||||
// already signed in when it happened (starting duty, setting availability).
|
||||
func milerIsBlocked(milerUserID int) bool {
|
||||
var p models.MilerProfile
|
||||
if db.DB.Select("availabilitystatus").Where("userid = ?", milerUserID).First(&p).Error == nil &&
|
||||
strings.EqualFold(p.Availabilitystatus, constants.MilerBlocked) {
|
||||
return true
|
||||
}
|
||||
var u models.AppUser
|
||||
return db.DB.Select("status").Where("userid = ?", milerUserID).First(&u).Error == nil &&
|
||||
strings.EqualFold(u.Status, constants.MilerBlocked)
|
||||
}
|
||||
|
||||
// milerPhoneTaken reports whether another rider already signs in with this
|
||||
// number (exceptUserID is the rider being edited, 0 when creating).
|
||||
func milerPhoneTaken(phone string, configID, exceptUserID int) bool {
|
||||
var n int64
|
||||
db.DB.Model(&models.AppUser{}).
|
||||
Where("contactno = ? AND configid = ? AND roleid = 5 AND userid <> ?", phone, configID, exceptUserID).
|
||||
Count(&n)
|
||||
return n > 0
|
||||
}
|
||||
|
||||
// checkMilerHub makes sure a hub exists and sits in the rider's city. A nil hub
|
||||
// (no base) is always fine.
|
||||
func checkMilerHub(hubID *int, cityID int) string {
|
||||
if hubID == nil {
|
||||
return ""
|
||||
}
|
||||
var hub models.Hub
|
||||
if err := db.DB.Select("hubid", "applocationid").Where("hubid = ? AND deletedat IS NULL", *hubID).First(&hub).Error; err != nil {
|
||||
return "that hub does not exist"
|
||||
}
|
||||
if hub.Applocationid != cityID {
|
||||
return "that hub is in a different city from the rider — choose a hub in the rider's city"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// UnblockMiler — PUT /admin/milers/:id/unblock
|
||||
// The counterpart of BlockMiler: the rider can sign in again and is Offline
|
||||
// until they start duty. Same scoping as block (a client login, its own riders).
|
||||
func UnblockMiler(c *fiber.Ctx) error {
|
||||
id, _ := strconv.Atoi(c.Params("id"))
|
||||
profile, ok := findMilerForConsole(c, id)
|
||||
if !ok {
|
||||
return utils.NotFound(c, "miler not found")
|
||||
}
|
||||
if !milerIsBlocked(profile.Userid) {
|
||||
return utils.BadRequest(c, "this miler is not blocked")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
err := db.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&models.MilerProfile{}).Where("milerprofileid = ?", profile.Milerprofileid).
|
||||
Updates(map[string]interface{}{"availabilitystatus": constants.MilerOffline, "updatedat": now}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&models.AppUser{}).Where("userid = ? AND status = ?", profile.Userid, constants.MilerBlocked).
|
||||
Update("status", "Active").Error
|
||||
})
|
||||
if err != nil {
|
||||
return utils.Internal(c, "failed to unblock miler")
|
||||
}
|
||||
profile.Availabilitystatus = constants.MilerOffline
|
||||
profile.Updatedat = now
|
||||
return utils.OK(c, profile)
|
||||
}
|
||||
@@ -33,6 +33,12 @@ func MilerStartDuty(c *fiber.Ctx) error {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
// A rider still signed in when ops blocked them used to come straight back
|
||||
// to Available here.
|
||||
if milerIsBlocked(milerUserID) {
|
||||
return utils.Forbidden(c, milerNotActiveMessage(constants.MilerBlocked))
|
||||
}
|
||||
|
||||
midnight := todayMidnight()
|
||||
var existing models.MilerDutyLog
|
||||
if err := db.DB.Where("userid = ? AND onduty = ? AND logoutat IS NULL AND loginat >= ?", milerUserID, true, midnight).
|
||||
@@ -237,7 +243,9 @@ func MilerEndBreak(c *fiber.Ctx) error {
|
||||
return utils.Internal(c, "failed to end break")
|
||||
}
|
||||
|
||||
db.DB.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).Update("availabilitystatus", constants.MilerAvailable)
|
||||
// Ending a break never lifts a block ops applied meanwhile.
|
||||
db.DB.Model(&models.MilerProfile{}).Where("userid = ? AND availabilitystatus <> ?", milerUserID, constants.MilerBlocked).
|
||||
Update("availabilitystatus", constants.MilerAvailable)
|
||||
|
||||
return utils.OK(c, fiber.Map{
|
||||
"breaklogid": breakLog.Breaklogid,
|
||||
|
||||
@@ -44,7 +44,7 @@ func LoginMiler(cfg *config.Config) fiber.Handler {
|
||||
}
|
||||
|
||||
var user models.AppUser
|
||||
if err := db.DB.Where("contactno = ? AND configid = ?", req.Phone, configID).First(&user).Error; err != nil {
|
||||
if err := milerLoginLookup(req.Phone, configID).First(&user).Error; err != nil {
|
||||
return utils.NotFound(c, "no miler account found for this phone number")
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ func LoginMiler(cfg *config.Config) fiber.Handler {
|
||||
}
|
||||
|
||||
if user.Status != "Active" {
|
||||
return utils.Forbidden(c, "miler account is not active")
|
||||
return utils.Forbidden(c, milerNotActiveMessage(user.Status))
|
||||
}
|
||||
|
||||
// pin_set tells the app which screen to show next: true → enter-PIN
|
||||
@@ -102,7 +102,7 @@ func VerifyMilerPin(cfg *config.Config) fiber.Handler {
|
||||
}
|
||||
|
||||
var user models.AppUser
|
||||
if err := db.DB.Where("contactno = ? AND configid = ?", req.Phone, configID).First(&user).Error; err != nil {
|
||||
if err := milerLoginLookup(req.Phone, configID).First(&user).Error; err != nil {
|
||||
return utils.NotFound(c, "no miler account found for this phone number")
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ func VerifyMilerPin(cfg *config.Config) fiber.Handler {
|
||||
}
|
||||
|
||||
if user.Status != "Active" {
|
||||
return utils.Forbidden(c, "miler account is not active")
|
||||
return utils.Forbidden(c, milerNotActiveMessage(user.Status))
|
||||
}
|
||||
|
||||
if !utils.CheckPasswordHash(req.Pin, user.Password) {
|
||||
@@ -192,7 +192,7 @@ func ResetMilerPin(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
var user models.AppUser
|
||||
if err := db.DB.Where("contactno = ? AND configid = ?", req.Phone, configID).First(&user).Error; err != nil {
|
||||
if err := milerLoginLookup(req.Phone, configID).First(&user).Error; err != nil {
|
||||
return utils.NotFound(c, "no miler account found for this phone number")
|
||||
}
|
||||
|
||||
@@ -238,14 +238,14 @@ func SetMilerPin(cfg *config.Config) fiber.Handler {
|
||||
}
|
||||
|
||||
var user models.AppUser
|
||||
if err := db.DB.Where("contactno = ? AND configid = ?", req.Phone, configID).First(&user).Error; err != nil {
|
||||
if err := milerLoginLookup(req.Phone, configID).First(&user).Error; err != nil {
|
||||
return utils.NotFound(c, "no miler account found for this phone number")
|
||||
}
|
||||
if user.Roleid != 5 {
|
||||
return utils.Forbidden(c, "this endpoint is restricted to miler accounts")
|
||||
}
|
||||
if user.Status != "Active" {
|
||||
return utils.Forbidden(c, "miler account is not active")
|
||||
return utils.Forbidden(c, milerNotActiveMessage(user.Status))
|
||||
}
|
||||
if user.Password != "" {
|
||||
return utils.Conflict(c, "a PIN is already set for this account; use verify-pin to log in")
|
||||
@@ -428,6 +428,14 @@ func UpdateMilerAvailability(c *fiber.Ctx) error {
|
||||
if status == "" {
|
||||
return utils.BadRequest(c, "status is required")
|
||||
}
|
||||
// Blocking is an ops decision: a rider can neither lift it (Available
|
||||
// from a session that was open when the block happened) nor set it.
|
||||
if strings.EqualFold(status, constants.MilerBlocked) {
|
||||
return utils.BadRequest(c, "riders cannot set this status")
|
||||
}
|
||||
if milerIsBlocked(milerUserID) {
|
||||
return utils.Forbidden(c, milerNotActiveMessage(constants.MilerBlocked))
|
||||
}
|
||||
|
||||
var profile models.MilerProfile
|
||||
if err := db.DB.Where("userid = ?", milerUserID).First(&profile).Error; err != nil {
|
||||
@@ -702,7 +710,7 @@ func RejectMilerAssignment(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).
|
||||
if err := tx.Model(&models.MilerProfile{}).Where("userid = ? AND availabilitystatus <> ?", milerUserID, constants.MilerBlocked).
|
||||
Update("availabilitystatus", constants.MilerAvailable).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to update miler availability")
|
||||
@@ -782,7 +790,7 @@ func MilerCancelAssignment(c *fiber.Ctx) error {
|
||||
return utils.Internal(c, "failed to release booking")
|
||||
}
|
||||
|
||||
if err := tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).
|
||||
if err := tx.Model(&models.MilerProfile{}).Where("userid = ? AND availabilitystatus <> ?", milerUserID, constants.MilerBlocked).
|
||||
Update("availabilitystatus", constants.MilerAvailable).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to update miler availability")
|
||||
@@ -1526,7 +1534,7 @@ func BookingPickupComplete(c *fiber.Ctx) error {
|
||||
return utils.Internal(c, "failed to close assignment")
|
||||
}
|
||||
}
|
||||
if err := tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).
|
||||
if err := tx.Model(&models.MilerProfile{}).Where("userid = ? AND availabilitystatus <> ?", milerUserID, constants.MilerBlocked).
|
||||
Update("availabilitystatus", postPickupAvailability).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to update miler availability")
|
||||
|
||||
Reference in New Issue
Block a user