feat: miler app contract gaps — stop type, COD, pre-pickup skip, profile
Close the gaps the miler-app dev flagged against the deployed contract. - GET /miler/bookings: return stoptype (pickup|delivery, from status), step + road-optimized sequence (cumulativekms/etaminutes/cumulativeeta), and codamount/paymentmode. List sorted by step, unsequenced last. Lookups batched to avoid N+1. - POST /miler/bookings/:bookingid/skip: pre-pickup skip that keeps the booking assigned and resumable — the "route back" the consignment-only delivery skip couldn't give a not-yet-picked-up booking. - GET /miler/earnings: add cancelled_stops + total_stops for success rate. - PUT /miler/profile: persist email (to appusers, 409 on unique clash) and a new nullable milerprofiles.address column. - POST /miler/assignments/:id/reject: accept reason from body OR ?reason=. Notifications read-state and bonuspoints deliberately left as-is — both need a product/business decision, not code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"doormile/config"
|
||||
@@ -206,11 +207,16 @@ func UpdateMilerProfile(c *fiber.Ctx) error {
|
||||
return utils.NotFound(c, "miler profile not found")
|
||||
}
|
||||
|
||||
// Email and Address are pointers so an omitted field is left untouched rather
|
||||
// than blanked. Email lives on AppUser (the login identity), Address on the
|
||||
// profile — the edit screen collects both, so both are persisted here.
|
||||
type ProfileUpdate struct {
|
||||
Displayname string `json:"displayname"`
|
||||
Profilephotourl string `json:"profilephotourl"`
|
||||
Defaultvehicletype string `json:"defaultvehicletype"`
|
||||
Phone string `json:"phone"`
|
||||
Displayname string `json:"displayname"`
|
||||
Profilephotourl string `json:"profilephotourl"`
|
||||
Defaultvehicletype string `json:"defaultvehicletype"`
|
||||
Phone string `json:"phone"`
|
||||
Email *string `json:"email"`
|
||||
Address *string `json:"address"`
|
||||
}
|
||||
|
||||
req := new(ProfileUpdate)
|
||||
@@ -224,10 +230,31 @@ func UpdateMilerProfile(c *fiber.Ctx) error {
|
||||
if req.Phone != "" {
|
||||
profile.Phone = req.Phone
|
||||
}
|
||||
if req.Address != nil {
|
||||
profile.Address = *req.Address
|
||||
}
|
||||
profile.Profilephotourl = req.Profilephotourl
|
||||
profile.Defaultvehicletype = req.Defaultvehicletype
|
||||
profile.Updatedat = time.Now()
|
||||
|
||||
// Email is unique on appusers, so a collision must fail cleanly rather than
|
||||
// 500. Only touch it when a non-empty value that actually changed is sent.
|
||||
if req.Email != nil && *req.Email != "" {
|
||||
var user models.AppUser
|
||||
if err := db.DB.First(&user, milerUserID).Error; err == nil && !strings.EqualFold(user.Email, *req.Email) {
|
||||
var clash int64
|
||||
db.DB.Model(&models.AppUser{}).
|
||||
Where("email = ? AND userid <> ?", *req.Email, milerUserID).Count(&clash)
|
||||
if clash > 0 {
|
||||
return utils.Conflict(c, "that email is already in use")
|
||||
}
|
||||
if err := db.DB.Model(&models.AppUser{}).
|
||||
Where("userid = ?", milerUserID).Update("email", *req.Email).Error; err != nil {
|
||||
return utils.Internal(c, "failed to update email")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.DB.Save(&profile).Error; err != nil {
|
||||
return utils.Internal(c, "failed to update profile")
|
||||
}
|
||||
@@ -434,11 +461,15 @@ func RejectMilerAssignment(c *fiber.Ctx) error {
|
||||
return utils.BadRequest(c, "invalid assignment ID")
|
||||
}
|
||||
|
||||
// The reason may arrive in the JSON body or as a ?reason= query string — the
|
||||
// deployed contract and the app disagreed on which, so accept both and prefer
|
||||
// whichever is non-empty. An absent/invalid body is not an error here.
|
||||
var req struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
_ = c.BodyParser(&req)
|
||||
if req.Reason == "" {
|
||||
req.Reason = c.Query("reason")
|
||||
}
|
||||
if req.Reason == "" {
|
||||
req.Reason = "Rejected by rider"
|
||||
|
||||
Reference in New Issue
Block a user