- POST /customer/reset-pin was unauthenticated and overwrote a customer's PIN
given only their phone number — which is the login identifier, not a secret —
so reset-pin followed by verify-pin took over any customer account. Exactly
the miler flaw fixed in fd7cf3e, on the B2C side. It now requires the account's
registered email to have been verified through the existing
send-email-otp/verify-email-otp flow; the verification is recorded in Redis
for 10 minutes and consumed on use, so one verification authorises one reset.
Accounts with no email on file are directed to support rather than left open.
- GET /customer/bookings/:id/price had no ownership check, unlike every other
customer booking route, so any signed-in customer could read the price quoted
on anyone else's booking by walking the id.
- GET /miler/consignments/userlogs/:userid took the rider from the URL and never
compared it to the caller, letting any miler read another miler's movement
history.
Verified as already correct while sweeping: miler assignment and booking-flow
handlers all scope by mileruserid/assignedmileruserid, customer booking detail
and cancel scope by appcustomerid, /internal sits behind InternalKeyAuth, and
CreateHubStaffAccount already refuses non-Doormile staff.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1259 lines
37 KiB
Go
1259 lines
37 KiB
Go
package controllers
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"time"
|
|
|
|
"doormile/config"
|
|
"doormile/constants"
|
|
"doormile/db"
|
|
"doormile/dto"
|
|
"doormile/internal/assignment"
|
|
"doormile/internal/notify"
|
|
"doormile/models"
|
|
"doormile/utils"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
func generateTrackingNo() string {
|
|
b := make([]byte, 4)
|
|
rand.Read(b)
|
|
return fmt.Sprintf("DM-TRK-%X-%d", b, time.Now().Unix()%100000)
|
|
}
|
|
|
|
func LoginMiler(cfg *config.Config) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
req := new(dto.MilerLoginRequest)
|
|
if err := c.BodyParser(req); err != nil {
|
|
return utils.BadRequest(c, "invalid request body")
|
|
}
|
|
|
|
if req.Phone == "" {
|
|
return utils.BadRequest(c, "phone is required")
|
|
}
|
|
|
|
configID := req.Configid
|
|
if configID == 0 {
|
|
configID = 1001
|
|
}
|
|
|
|
var user models.AppUser
|
|
if err := db.DB.Where("contactno = ? AND configid = ?", 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 c.JSON(fiber.Map{
|
|
"success": true,
|
|
"message": "PIN verification required",
|
|
"phone": req.Phone,
|
|
})
|
|
}
|
|
}
|
|
|
|
func VerifyMilerPin(cfg *config.Config) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
req := new(dto.MilerPinVerifyRequest)
|
|
if err := c.BodyParser(req); err != nil {
|
|
return utils.BadRequest(c, "invalid request body")
|
|
}
|
|
|
|
if req.Phone == "" || req.Pin == "" {
|
|
return utils.BadRequest(c, "phone and pin are required")
|
|
}
|
|
|
|
configID := req.Configid
|
|
if configID == 0 {
|
|
configID = 1001
|
|
}
|
|
|
|
var user models.AppUser
|
|
if err := db.DB.Where("contactno = ? AND configid = ?", 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")
|
|
}
|
|
|
|
if !utils.CheckPasswordHash(req.Pin, user.Password) {
|
|
return utils.Unauthorized(c, "incorrect PIN")
|
|
}
|
|
|
|
token, err := utils.GenerateToken(user.Userid, user.Email, user.Roleid, user.Tenantid, user.Configid, cfg.JWTSecret)
|
|
if err != nil {
|
|
return utils.Internal(c, "failed to generate token")
|
|
}
|
|
|
|
var profile models.MilerProfile
|
|
if err := db.DB.Where("userid = ?", user.Userid).First(&profile).Error; err != nil {
|
|
profile = models.MilerProfile{
|
|
Userid: user.Userid,
|
|
Displayname: user.Authname,
|
|
Phone: user.Contactno,
|
|
Availabilitystatus: constants.MilerOffline,
|
|
Rating: 5.0,
|
|
Applocationid: user.Applocationid,
|
|
}
|
|
db.DB.Create(&profile)
|
|
}
|
|
if req.DeviceToken != "" && profile.Devicetoken != req.DeviceToken {
|
|
profile.Devicetoken = req.DeviceToken
|
|
db.DB.Model(&profile).Update("device_token", req.DeviceToken)
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"success": true,
|
|
"token": token,
|
|
"user": fiber.Map{
|
|
"userid": user.Userid,
|
|
"authname": user.Authname,
|
|
"email": user.Email,
|
|
"contactno": user.Contactno,
|
|
"profile": profile,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
// ResetMilerPin lets a miler who forgot their PIN set a new one from just
|
|
// their phone number, matching ResetCustomerPin's flow exactly (protected
|
|
// only by authThrottle at the route level, same as the customer version —
|
|
// no OTP verification wired in here either, consistent with the existing
|
|
// pattern rather than a change to it).
|
|
func ResetMilerPin(c *fiber.Ctx) error {
|
|
req := new(dto.MilerResetPinRequest)
|
|
if err := c.BodyParser(req); err != nil {
|
|
return utils.BadRequest(c, "invalid request body")
|
|
}
|
|
|
|
if req.Phone == "" || req.NewPin == "" {
|
|
return utils.BadRequest(c, "phone and new_pin are required")
|
|
}
|
|
|
|
configID := req.Configid
|
|
if configID == 0 {
|
|
configID = 1001
|
|
}
|
|
|
|
var user models.AppUser
|
|
if err := db.DB.Where("contactno = ? AND configid = ?", 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")
|
|
}
|
|
|
|
pinHash, err := utils.HashPassword(req.NewPin)
|
|
if err != nil {
|
|
return utils.Internal(c, "failed to process PIN reset")
|
|
}
|
|
|
|
user.Password = pinHash
|
|
if err := db.DB.Save(&user).Error; err != nil {
|
|
return utils.Internal(c, "failed to reset PIN")
|
|
}
|
|
|
|
return utils.Message(c, "PIN reset successfully")
|
|
}
|
|
|
|
func GetMilerProfile(c *fiber.Ctx) error {
|
|
milerUserID := c.Locals("userid").(int)
|
|
|
|
var user models.AppUser
|
|
if err := db.DB.First(&user, milerUserID).Error; err != nil {
|
|
return utils.NotFound(c, "user not found")
|
|
}
|
|
|
|
var profile models.MilerProfile
|
|
if err := db.DB.Where("userid = ?", milerUserID).First(&profile).Error; err != nil {
|
|
return utils.NotFound(c, "miler profile not found")
|
|
}
|
|
|
|
return utils.OK(c, fiber.Map{
|
|
"userid": user.Userid,
|
|
"authname": user.Authname,
|
|
"email": user.Email,
|
|
"contactno": user.Contactno,
|
|
"profile": profile,
|
|
})
|
|
}
|
|
|
|
func UpdateMilerProfile(c *fiber.Ctx) error {
|
|
milerUserID := c.Locals("userid").(int)
|
|
|
|
var profile models.MilerProfile
|
|
if err := db.DB.Where("userid = ?", milerUserID).First(&profile).Error; err != nil {
|
|
return utils.NotFound(c, "miler profile not found")
|
|
}
|
|
|
|
type ProfileUpdate struct {
|
|
Displayname string `json:"displayname"`
|
|
Profilephotourl string `json:"profilephotourl"`
|
|
Defaultvehicletype string `json:"defaultvehicletype"`
|
|
Phone string `json:"phone"`
|
|
}
|
|
|
|
req := new(ProfileUpdate)
|
|
if err := c.BodyParser(req); err != nil {
|
|
return utils.BadRequest(c, "invalid request body")
|
|
}
|
|
|
|
if req.Displayname != "" {
|
|
profile.Displayname = req.Displayname
|
|
}
|
|
if req.Phone != "" {
|
|
profile.Phone = req.Phone
|
|
}
|
|
profile.Profilephotourl = req.Profilephotourl
|
|
profile.Defaultvehicletype = req.Defaultvehicletype
|
|
profile.Updatedat = time.Now()
|
|
|
|
if err := db.DB.Save(&profile).Error; err != nil {
|
|
return utils.Internal(c, "failed to update profile")
|
|
}
|
|
|
|
return utils.OK(c, profile)
|
|
}
|
|
|
|
func UpdateMilerLocation(c *fiber.Ctx) error {
|
|
milerUserID := c.Locals("userid").(int)
|
|
|
|
req := new(dto.MilerLocationUpdateRequest)
|
|
if err := c.BodyParser(req); err != nil {
|
|
return utils.BadRequest(c, "invalid request body")
|
|
}
|
|
|
|
if req.Latitude == 0 || req.Longitude == 0 {
|
|
return utils.BadRequest(c, "latitude and longitude are required")
|
|
}
|
|
|
|
var profile models.MilerProfile
|
|
if err := db.DB.Where("userid = ?", milerUserID).First(&profile).Error; err != nil {
|
|
return utils.NotFound(c, "miler profile not found")
|
|
}
|
|
|
|
profile.Currentlatitude = req.Latitude
|
|
profile.Currentlongitude = req.Longitude
|
|
profile.Currentpincode = req.Pincode
|
|
now := time.Now()
|
|
profile.Lastlocationupdatedat = &now
|
|
profile.Updatedat = now
|
|
|
|
if err := db.DB.Save(&profile).Error; err != nil {
|
|
return utils.Internal(c, "failed to update location")
|
|
}
|
|
|
|
if db.Rdb != nil {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
redisKey := fmt.Sprintf("miler:gps:%d", milerUserID)
|
|
val := fmt.Sprintf("%f,%f", req.Latitude, req.Longitude)
|
|
db.Rdb.Set(ctx, redisKey, val, 30*time.Minute)
|
|
|
|
db.Rdb.GeoAdd(ctx, "milers:locations", &redis.GeoLocation{
|
|
Name: strconv.Itoa(milerUserID),
|
|
Latitude: req.Latitude,
|
|
Longitude: req.Longitude,
|
|
})
|
|
}
|
|
|
|
return utils.OK(c, fiber.Map{
|
|
"latitude": req.Latitude,
|
|
"longitude": req.Longitude,
|
|
"pincode": req.Pincode,
|
|
})
|
|
}
|
|
|
|
func UpdateMilerAvailability(c *fiber.Ctx) error {
|
|
milerUserID := c.Locals("userid").(int)
|
|
|
|
req := new(dto.MilerAvailabilityRequest)
|
|
if err := c.BodyParser(req); err != nil {
|
|
return utils.BadRequest(c, "invalid request body")
|
|
}
|
|
|
|
status := req.ResolvedStatus()
|
|
if status == "" {
|
|
return utils.BadRequest(c, "status is required")
|
|
}
|
|
|
|
var profile models.MilerProfile
|
|
if err := db.DB.Where("userid = ?", milerUserID).First(&profile).Error; err != nil {
|
|
return utils.NotFound(c, "miler profile not found")
|
|
}
|
|
|
|
profile.Availabilitystatus = status
|
|
profile.Updatedat = time.Now()
|
|
if err := db.DB.Save(&profile).Error; err != nil {
|
|
return utils.Internal(c, "failed to update availability")
|
|
}
|
|
|
|
var user models.AppUser
|
|
if err := db.DB.First(&user, milerUserID).Error; err == nil {
|
|
if req.Status == constants.MilerOffline || req.Status == constants.MilerBlocked {
|
|
user.Onduty = 0
|
|
} else {
|
|
user.Onduty = 1
|
|
}
|
|
db.DB.Save(&user)
|
|
}
|
|
|
|
return utils.OK(c, profile)
|
|
}
|
|
|
|
func GetMilerAssignments(c *fiber.Ctx) error {
|
|
milerUserID := c.Locals("userid").(int)
|
|
|
|
var assignments []models.BookingAssignment
|
|
if err := db.DB.Where("mileruserid = ? AND assignmentstatus IN ?", milerUserID,
|
|
[]string{constants.AssignmentAssigned, constants.AssignmentAccepted}).
|
|
Order("assignedat DESC").Find(&assignments).Error; err != nil {
|
|
return utils.Internal(c, "failed to fetch assignments")
|
|
}
|
|
|
|
return utils.List(c, assignments, int64(len(assignments)))
|
|
}
|
|
|
|
func GetMilerAssignmentDetails(c *fiber.Ctx) error {
|
|
milerUserID := c.Locals("userid").(int)
|
|
assignmentID, err := strconv.Atoi(c.Params("id"))
|
|
if err != nil {
|
|
return utils.BadRequest(c, "invalid assignment ID")
|
|
}
|
|
|
|
var assignment models.BookingAssignment
|
|
if err := db.DB.Where("bookingassignmentid = ? AND mileruserid = ?", assignmentID, milerUserID).First(&assignment).Error; err != nil {
|
|
return utils.NotFound(c, "assignment not found")
|
|
}
|
|
|
|
var booking models.PickupBooking
|
|
db.DB.Preload("Parcels").Preload("ServiceOptions").First(&booking, assignment.Bookingid)
|
|
|
|
return utils.OK(c, fiber.Map{
|
|
"assignment": assignment,
|
|
"booking": booking,
|
|
})
|
|
}
|
|
|
|
func AcceptMilerAssignment(c *fiber.Ctx) error {
|
|
milerUserID := c.Locals("userid").(int)
|
|
assignmentID, err := strconv.Atoi(c.Params("id"))
|
|
if err != nil {
|
|
return utils.BadRequest(c, "invalid assignment ID")
|
|
}
|
|
|
|
tx := db.DB.Begin()
|
|
|
|
var assignment models.BookingAssignment
|
|
if err := tx.Where("bookingassignmentid = ? AND mileruserid = ?", assignmentID, milerUserID).First(&assignment).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.NotFound(c, "assignment not found")
|
|
}
|
|
|
|
if assignment.Assignmentstatus != constants.AssignmentAssigned {
|
|
tx.Rollback()
|
|
return utils.BadRequest(c, fmt.Sprintf("assignment is not pending acceptance (current status: %s)", assignment.Assignmentstatus))
|
|
}
|
|
|
|
now := time.Now()
|
|
assignment.Assignmentstatus = constants.AssignmentAccepted
|
|
assignment.Acceptedat = &now
|
|
if err := tx.Save(&assignment).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.Internal(c, "failed to accept assignment")
|
|
}
|
|
|
|
var booking models.PickupBooking
|
|
if err := tx.First(&booking, assignment.Bookingid).Error; err == nil {
|
|
booking.Status = constants.BookingPickupScheduled
|
|
booking.Assignedmileruserid = &milerUserID
|
|
booking.Updatedat = now
|
|
if err := tx.Save(&booking).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.Internal(c, "failed to update booking")
|
|
}
|
|
}
|
|
|
|
if err := tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).
|
|
Update("availabilitystatus", constants.MilerAssigned).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.Internal(c, "failed to update miler availability")
|
|
}
|
|
|
|
if err := tx.Commit().Error; err != nil {
|
|
return utils.Internal(c, "failed to commit assignment acceptance")
|
|
}
|
|
|
|
if booking.Bookingid != 0 {
|
|
var customer models.AppCustomer
|
|
if err := db.DB.Where("appcustomerid = ?", booking.Appcustomerid).First(&customer).Error; err == nil && customer.Devicetoken != "" {
|
|
if notifyErr := notify.SendToDevice(
|
|
customer.Devicetoken,
|
|
"Miler Accepted",
|
|
"Your miler has accepted and is coming",
|
|
map[string]string{"booking_id": strconv.Itoa(booking.Bookingid)},
|
|
); notifyErr != nil {
|
|
utils.Warn("FCM: failed to notify customer on accept", "booking_id", booking.Bookingid, "error", notifyErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
return utils.Message(c, "assignment accepted successfully")
|
|
}
|
|
|
|
func RejectMilerAssignment(c *fiber.Ctx) error {
|
|
milerUserID := c.Locals("userid").(int)
|
|
assignmentID, err := strconv.Atoi(c.Params("id"))
|
|
if err != nil {
|
|
return utils.BadRequest(c, "invalid assignment ID")
|
|
}
|
|
|
|
var req struct {
|
|
Reason string `json:"reason"`
|
|
}
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return utils.BadRequest(c, "invalid request body")
|
|
}
|
|
if req.Reason == "" {
|
|
req.Reason = "Rejected by rider"
|
|
}
|
|
|
|
tx := db.DB.Begin()
|
|
|
|
var ba models.BookingAssignment
|
|
if err := tx.Where("bookingassignmentid = ? AND mileruserid = ?", assignmentID, milerUserID).First(&ba).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.NotFound(c, "assignment not found")
|
|
}
|
|
|
|
ba.Assignmentstatus = constants.AssignmentRejected
|
|
ba.Remarks = req.Reason
|
|
if err := tx.Save(&ba).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.Internal(c, "failed to reject assignment")
|
|
}
|
|
|
|
var booking models.PickupBooking
|
|
if err := tx.First(&booking, ba.Bookingid).Error; err == nil {
|
|
booking.Status = constants.BookingCreated
|
|
booking.Assignedmileruserid = nil
|
|
booking.Updatedat = time.Now()
|
|
if err := tx.Save(&booking).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.Internal(c, "failed to release booking")
|
|
}
|
|
}
|
|
|
|
if err := tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).
|
|
Update("availabilitystatus", constants.MilerAvailable).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.Internal(c, "failed to update miler availability")
|
|
}
|
|
|
|
if err := tx.Commit().Error; err != nil {
|
|
return utils.Internal(c, "failed to commit assignment rejection")
|
|
}
|
|
|
|
if booking.Bookingid != 0 {
|
|
if booking.Bookingsource == "CRM_Console" {
|
|
go assignment.AssignCRMMiler(booking.Bookingid)
|
|
} else {
|
|
go assignment.AssignCustomerMiler(booking.Bookingid)
|
|
}
|
|
}
|
|
|
|
return utils.Message(c, "assignment rejected")
|
|
}
|
|
|
|
// MilerCancelAssignment lets a miler back out of a booking they've already
|
|
// accepted but not yet picked up (vehicle breakdown, can't reach the
|
|
// address, etc.). Distinct from RejectMilerAssignment, which only applies
|
|
// before acceptance — once the parcel is picked up the booking has become a
|
|
// consignment and this no longer applies (use MilerSkipDelivery instead for
|
|
// a failed delivery attempt on an in-flight consignment). Releases the
|
|
// booking for reassignment the same way RejectMilerAssignment does.
|
|
func MilerCancelAssignment(c *fiber.Ctx) error {
|
|
milerUserID := c.Locals("userid").(int)
|
|
bookingID, err := strconv.Atoi(c.Params("bookingid"))
|
|
if err != nil {
|
|
return utils.BadRequest(c, "invalid booking ID")
|
|
}
|
|
|
|
var req struct {
|
|
Reason string `json:"reason"`
|
|
}
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return utils.BadRequest(c, "invalid request body")
|
|
}
|
|
if req.Reason == "" {
|
|
req.Reason = "Cancelled by miler"
|
|
}
|
|
|
|
tx := db.DB.Begin()
|
|
|
|
var booking models.PickupBooking
|
|
if err := tx.Where("bookingid = ? AND assignedmileruserid = ?", bookingID, milerUserID).First(&booking).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.NotFound(c, "assigned booking not found")
|
|
}
|
|
|
|
if booking.Status == constants.BookingPickedUp || booking.Status == constants.BookingConvertedConsignment {
|
|
tx.Rollback()
|
|
return utils.BadRequest(c, "booking cannot be cancelled after pickup — the parcel is already in the network")
|
|
}
|
|
|
|
var ba models.BookingAssignment
|
|
if err := tx.Where("bookingid = ? AND mileruserid = ? AND assignmentstatus = ?",
|
|
bookingID, milerUserID, constants.AssignmentAccepted).First(&ba).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.BadRequest(c, "no accepted assignment found for this booking")
|
|
}
|
|
|
|
ba.Assignmentstatus = constants.AssignmentCancelled
|
|
ba.Remarks = req.Reason
|
|
if err := tx.Save(&ba).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.Internal(c, "failed to cancel assignment")
|
|
}
|
|
|
|
booking.Status = constants.BookingCreated
|
|
booking.Assignedmileruserid = nil
|
|
booking.Updatedat = time.Now()
|
|
if err := tx.Save(&booking).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.Internal(c, "failed to release booking")
|
|
}
|
|
|
|
if err := tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).
|
|
Update("availabilitystatus", constants.MilerAvailable).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.Internal(c, "failed to update miler availability")
|
|
}
|
|
|
|
if err := tx.Commit().Error; err != nil {
|
|
return utils.Internal(c, "failed to commit cancellation")
|
|
}
|
|
|
|
if booking.Bookingsource == "CRM_Console" {
|
|
go assignment.AssignCRMMiler(booking.Bookingid)
|
|
} else {
|
|
go assignment.AssignCustomerMiler(booking.Bookingid)
|
|
}
|
|
|
|
return utils.Message(c, "assignment cancelled and released for reassignment")
|
|
}
|
|
|
|
func BookingReachedCustomer(c *fiber.Ctx) error {
|
|
milerUserID := c.Locals("userid").(int)
|
|
bookingID, err := strconv.Atoi(c.Params("bookingid"))
|
|
if err != nil {
|
|
return utils.BadRequest(c, "invalid booking ID")
|
|
}
|
|
|
|
tx := db.DB.Begin()
|
|
|
|
var booking models.PickupBooking
|
|
if err := tx.Where("bookingid = ? AND assignedmileruserid = ?", bookingID, milerUserID).First(&booking).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.NotFound(c, "assigned booking not found")
|
|
}
|
|
|
|
if err := tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).
|
|
Update("availabilitystatus", constants.MilerAtCustomer).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.Internal(c, "failed to update miler availability")
|
|
}
|
|
|
|
if err := tx.Commit().Error; err != nil {
|
|
return utils.Internal(c, "failed to confirm arrival")
|
|
}
|
|
return utils.Message(c, "arrival at customer confirmed")
|
|
}
|
|
|
|
func BookingParcelConfirm(c *fiber.Ctx) error {
|
|
milerUserID := c.Locals("userid").(int)
|
|
bookingID, err := strconv.Atoi(c.Params("bookingid"))
|
|
if err != nil {
|
|
return utils.BadRequest(c, "invalid booking ID")
|
|
}
|
|
|
|
var booking models.PickupBooking
|
|
if err := db.DB.Where("bookingid = ? AND assignedmileruserid = ?", bookingID, milerUserID).First(&booking).Error; err != nil {
|
|
return utils.NotFound(c, "assigned booking not found")
|
|
}
|
|
|
|
type ParcelUpdate struct {
|
|
ParcelID int `json:"parcel_id"`
|
|
Weight float64 `json:"weight"`
|
|
Length float64 `json:"length"`
|
|
Width float64 `json:"width"`
|
|
Height float64 `json:"height"`
|
|
}
|
|
var req struct {
|
|
Parcels []ParcelUpdate `json:"parcels"`
|
|
}
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return utils.BadRequest(c, "invalid request body")
|
|
}
|
|
if len(req.Parcels) == 0 {
|
|
return utils.BadRequest(c, "parcels array is required")
|
|
}
|
|
|
|
var parcels []models.BookingParcel
|
|
if err := db.DB.Where("bookingid = ?", bookingID).Find(&parcels).Error; err != nil {
|
|
return utils.NotFound(c, "parcel details not found")
|
|
}
|
|
|
|
// Index loaded parcels by ID for O(1) lookup.
|
|
parcelMap := make(map[int]*models.BookingParcel, len(parcels))
|
|
for i := range parcels {
|
|
parcelMap[parcels[i].Bookingparcelid] = &parcels[i]
|
|
}
|
|
|
|
now := time.Now()
|
|
var totalChargeable float64
|
|
|
|
for _, upd := range req.Parcels {
|
|
p, ok := parcelMap[upd.ParcelID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
p.Weight = upd.Weight
|
|
p.Length = upd.Length
|
|
p.Width = upd.Width
|
|
p.Height = upd.Height
|
|
p.Updatedat = now
|
|
db.DB.Save(p)
|
|
|
|
volumetric := calculateVolumetricWeight(upd.Length, upd.Width, upd.Height)
|
|
totalChargeable += math.Max(upd.Weight, volumetric)
|
|
}
|
|
|
|
return utils.OK(c, fiber.Map{
|
|
"parcels": parcels,
|
|
"total_chargeable_weight": totalChargeable,
|
|
})
|
|
}
|
|
|
|
func BookingPaymentCollect(c *fiber.Ctx) error {
|
|
milerUserID := c.Locals("userid").(int)
|
|
bookingID, err := strconv.Atoi(c.Params("bookingid"))
|
|
if err != nil {
|
|
return utils.BadRequest(c, "invalid booking ID")
|
|
}
|
|
|
|
var booking models.PickupBooking
|
|
if err := db.DB.Where("bookingid = ? AND assignedmileruserid = ?", bookingID, milerUserID).First(&booking).Error; err != nil {
|
|
return utils.NotFound(c, "assigned booking not found")
|
|
}
|
|
|
|
req := new(dto.PaymentRequest)
|
|
if err := c.BodyParser(req); err != nil {
|
|
return utils.BadRequest(c, "invalid request body")
|
|
}
|
|
|
|
if req.Amount <= 0 {
|
|
return utils.BadRequest(c, "payment amount must be greater than zero")
|
|
}
|
|
|
|
now := time.Now()
|
|
payment := models.BookingPayment{
|
|
Bookingid: bookingID,
|
|
Amount: req.Amount,
|
|
Paymentmode: req.Paymentmode,
|
|
Paymentstatus: constants.PaymentStatusPaid,
|
|
Collectedbyuserid: &milerUserID,
|
|
Transactionref: req.Transactionref,
|
|
Paidat: &now,
|
|
}
|
|
|
|
if err := db.DB.Create(&payment).Error; err != nil {
|
|
return utils.Internal(c, "failed to record payment")
|
|
}
|
|
|
|
return utils.Created(c, payment)
|
|
}
|
|
|
|
// isHyperlocal reports whether a pickup and delivery pincode fall in the same
|
|
// 3-digit postal area, following the same zone-prefix convention as
|
|
// hubPincodePrefix in hubController.go. A same-area booking needs no
|
|
// hub-to-hub tripsheet leg, so the collecting miler can carry it straight to
|
|
// final-mile delivery. Pincodes shorter than 3 characters are treated as
|
|
// unknown rather than matching, so bad data falls back to the safe hub route.
|
|
func isHyperlocal(pickupPincode, deliveryPincode string) bool {
|
|
if len(pickupPincode) < 3 || len(deliveryPincode) < 3 {
|
|
return false
|
|
}
|
|
return pickupPincode[:3] == deliveryPincode[:3]
|
|
}
|
|
|
|
func BookingPickupComplete(c *fiber.Ctx) error {
|
|
milerUserID := c.Locals("userid").(int)
|
|
bookingID, err := strconv.Atoi(c.Params("bookingid"))
|
|
if err != nil {
|
|
return utils.BadRequest(c, "invalid booking ID")
|
|
}
|
|
|
|
tx := db.DB.Begin()
|
|
|
|
var booking models.PickupBooking
|
|
if err := tx.Where("bookingid = ? AND assignedmileruserid = ?", bookingID, milerUserID).First(&booking).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.NotFound(c, "assigned booking not found")
|
|
}
|
|
|
|
now := time.Now()
|
|
booking.Status = constants.BookingPickedUp
|
|
booking.Updatedat = now
|
|
if err := tx.Save(&booking).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.Internal(c, "failed to update booking status")
|
|
}
|
|
|
|
var profile models.MilerProfile
|
|
if err := tx.Where("userid = ?", milerUserID).First(&profile).Error; err == nil {
|
|
profile.Totalcompletedpickups += 1
|
|
profile.Availabilitystatus = constants.MilerPickedUp
|
|
profile.Updatedat = now
|
|
if err := tx.Save(&profile).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.Internal(c, "failed to update miler profile")
|
|
}
|
|
}
|
|
|
|
var parcels []models.BookingParcel
|
|
tx.Where("bookingid = ?", bookingID).Find(&parcels)
|
|
|
|
var totalDead, totalChargeable, maxL, maxW, maxH float64
|
|
for _, p := range parcels {
|
|
vol := calculateVolumetricWeight(p.Length, p.Width, p.Height)
|
|
totalDead += p.Weight
|
|
totalChargeable += math.Max(p.Weight, vol)
|
|
if p.Length > maxL {
|
|
maxL = p.Length
|
|
}
|
|
if p.Width > maxW {
|
|
maxW = p.Width
|
|
}
|
|
if p.Height > maxH {
|
|
maxH = p.Height
|
|
}
|
|
}
|
|
if len(parcels) == 0 {
|
|
totalDead = 0.5
|
|
totalChargeable = 0.5
|
|
}
|
|
|
|
trackingNo := generateTrackingNo()
|
|
|
|
var defaultHubID *int
|
|
if profile.Hubid != nil {
|
|
defaultHubID = profile.Hubid
|
|
} else {
|
|
utils.Warn("BookingPickupComplete: miler has no assigned hub, falling back to first hub row", "miler_user_id", milerUserID, "booking_id", bookingID)
|
|
var hub models.Hub
|
|
if tx.First(&hub).Error == nil {
|
|
defaultHubID = &hub.Hubid
|
|
}
|
|
}
|
|
|
|
// Hyperlocal shortcut: pickup and delivery in the same postal area mean
|
|
// no hub-to-hub tripsheet leg is needed, so the same miler goes straight
|
|
// to final-mile delivery instead of parking the consignment at the hub.
|
|
consignmentStatus := constants.ConsignmentInwardedAtHub
|
|
if isHyperlocal(booking.Pickuppincode, booking.Deliverypincode) {
|
|
consignmentStatus = constants.ConsignmentOutForDelivery
|
|
}
|
|
|
|
// The consignment's tenant is the booking's own tenant (set explicitly at
|
|
// CreateExpressBooking time), not the completing miler's tenantid claim — a
|
|
// miler can carry parcels for tenants other than their own, and using
|
|
// their JWT tenantid here mis-attributed every such consignment. Falls
|
|
// back to the miler's own tenantid only for B2C bookings that don't carry
|
|
// one yet, matching the previous behavior for that case.
|
|
consignmentTenantID := c.Locals("tenantid").(int)
|
|
if booking.Tenantid != nil {
|
|
consignmentTenantID = *booking.Tenantid
|
|
}
|
|
|
|
// Carried over so the consignment stays traceable to the client site it was
|
|
// collected from — for a food client that's the kitchen, and "how many
|
|
// parcels went out of which kitchen" is unanswerable without it.
|
|
consignment := models.Consignment{
|
|
Trackingno: trackingNo,
|
|
Tenantid: consignmentTenantID,
|
|
Pickuplocationid: booking.Pickuplocationid,
|
|
Pickuplatitude: booking.Pickuplatitude,
|
|
Pickuplongitude: booking.Pickuplongitude,
|
|
Deliverylatitude: booking.Deliverylatitude,
|
|
Deliverylongitude: booking.Deliverylongitude,
|
|
Pickuppincode: booking.Pickuppincode,
|
|
Deliverypincode: booking.Deliverypincode,
|
|
Length: maxL,
|
|
Width: maxW,
|
|
Height: maxH,
|
|
Deadweight: totalDead,
|
|
Volumetricweight: totalChargeable - totalDead,
|
|
Chargeableweight: totalChargeable,
|
|
Paymentmode: "Prepaid",
|
|
Status: consignmentStatus,
|
|
Estimateddeliveryat: nil,
|
|
Createdby: milerUserID,
|
|
Originhubid: defaultHubID,
|
|
Currenthubid: defaultHubID,
|
|
}
|
|
|
|
var payment models.BookingPayment
|
|
if tx.Where("bookingid = ?", bookingID).First(&payment).Error == nil {
|
|
if payment.Paymentstatus == constants.PaymentStatusPaid {
|
|
consignment.Codcollected = payment.Amount
|
|
} else {
|
|
consignment.Codamount = payment.Amount
|
|
consignment.Paymentmode = "COD"
|
|
}
|
|
}
|
|
|
|
// A hyperlocal parcel goes straight out for delivery, so its receiver OTP has
|
|
// to exist before this transaction commits. Anything routed via a hub gets
|
|
// its OTP when it actually leaves for the final mile instead. Only issued
|
|
// for clients that ask for it — see Tenant.Requiredeliveryotp.
|
|
if consignmentStatus == constants.ConsignmentOutForDelivery {
|
|
var tenant models.Tenant
|
|
if tx.Where("tenantid = ?", consignmentTenantID).First(&tenant).Error == nil && tenant.Requiredeliveryotp {
|
|
consignment.Deliveryotp = utils.GenerateNumericOTP(6)
|
|
}
|
|
}
|
|
|
|
if err := tx.Create(&consignment).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.Internal(c, "failed to convert booking to consignment")
|
|
}
|
|
|
|
booking.Consignmentid = &consignment.Consignmentid
|
|
booking.Status = constants.BookingConvertedConsignment
|
|
if err := tx.Save(&booking).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.Internal(c, "failed to link booking to consignment")
|
|
}
|
|
|
|
history := models.ConsignmentHistory{
|
|
Consignmentid: consignment.Consignmentid,
|
|
Hubid: defaultHubID,
|
|
Userid: &milerUserID,
|
|
Eventstatus: consignmentStatus,
|
|
Remarks: "Package collected by miler and converted to consignment",
|
|
}
|
|
if err := tx.Create(&history).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.Internal(c, "failed to record consignment history")
|
|
}
|
|
|
|
if err := tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).
|
|
Update("availabilitystatus", constants.MilerAvailable).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.Internal(c, "failed to update miler availability")
|
|
}
|
|
|
|
if err := tx.Commit().Error; err != nil {
|
|
return utils.Internal(c, "failed to complete pickup")
|
|
}
|
|
|
|
var customer models.AppCustomer
|
|
if err := db.DB.Where("appcustomerid = ?", booking.Appcustomerid).First(&customer).Error; err == nil && customer.Devicetoken != "" {
|
|
body := fmt.Sprintf("Parcel picked up — Tracking No: %s", trackingNo)
|
|
payload := map[string]string{
|
|
"booking_id": strconv.Itoa(bookingID),
|
|
"tracking_no": trackingNo,
|
|
}
|
|
// The OTP goes to the receiver and only the receiver — the rider has to
|
|
// be told it at the door, which is what makes it proof of handover.
|
|
if consignment.Deliveryotp != "" {
|
|
body = fmt.Sprintf("%s. Share OTP %s with the rider on delivery.", body, consignment.Deliveryotp)
|
|
payload["delivery_otp"] = consignment.Deliveryotp
|
|
}
|
|
if notifyErr := notify.SendToDevice(customer.Devicetoken, "Parcel Picked Up", body, payload); notifyErr != nil {
|
|
utils.Warn("FCM: failed to notify customer on pickup", "booking_id", bookingID, "error", notifyErr)
|
|
}
|
|
}
|
|
|
|
return utils.OK(c, fiber.Map{
|
|
"tracking_no": trackingNo,
|
|
"consignment_id": consignment.Consignmentid,
|
|
"booking_no": booking.Bookingno,
|
|
})
|
|
}
|
|
|
|
func BookingVehicleRequiredEscalate(c *fiber.Ctx) error {
|
|
milerUserID := c.Locals("userid").(int)
|
|
bookingID, err := strconv.Atoi(c.Params("bookingid"))
|
|
if err != nil {
|
|
return utils.BadRequest(c, "invalid booking ID")
|
|
}
|
|
|
|
var booking models.PickupBooking
|
|
if err := db.DB.Where("bookingid = ? AND assignedmileruserid = ?", bookingID, milerUserID).First(&booking).Error; err != nil {
|
|
return utils.NotFound(c, "assigned booking not found")
|
|
}
|
|
|
|
reqVeh := models.BookingVehicleRequirement{
|
|
Bookingid: bookingID,
|
|
Requiredvehicletype: c.Query("type", "truck"),
|
|
Reason: c.Query("reason", "Package is too large for bike rider"),
|
|
Status: "Required",
|
|
}
|
|
|
|
if err := db.DB.Create(&reqVeh).Error; err != nil {
|
|
return utils.Internal(c, "failed to register vehicle requirement")
|
|
}
|
|
|
|
return utils.Created(c, reqVeh)
|
|
}
|
|
|
|
func CreateMilerPeriodicLog(c *fiber.Ctx) error {
|
|
ctx := context.Background()
|
|
|
|
var log models.MilerLog
|
|
if err := c.BodyParser(&log); err != nil {
|
|
return utils.BadRequest(c, "invalid request body")
|
|
}
|
|
|
|
// The rider's identity comes from their token, never the body. Trusting a
|
|
// client-supplied userid let any authenticated miler write another miler's
|
|
// GPS trail, which feeds the location data dispatch reasons over.
|
|
log.UserID = c.Locals("userid").(int)
|
|
|
|
t, err := time.Parse("2006-01-02 15:04:05", log.LogDate)
|
|
if err != nil {
|
|
return utils.BadRequest(c, "invalid logdate format — expected YYYY-MM-DD HH:MM:SS")
|
|
}
|
|
|
|
timestamp := t.Unix()
|
|
logKey := fmt.Sprintf("miler_periodic_log:%d:%d", log.UserID, timestamp)
|
|
data, _ := json.Marshal(log)
|
|
|
|
if db.Rdb != nil {
|
|
if err := db.Rdb.Set(ctx, logKey, data, 0).Err(); err != nil {
|
|
return utils.Internal(c, "failed to store log")
|
|
}
|
|
|
|
userZsetKey := fmt.Sprintf("miler_periodic_logs:%d", log.UserID)
|
|
db.Rdb.ZAdd(ctx, userZsetKey, redis.Z{Score: float64(timestamp), Member: logKey})
|
|
db.Rdb.ZAdd(ctx, "miler_periodic_logs_all", redis.Z{Score: float64(timestamp), Member: logKey})
|
|
}
|
|
|
|
return utils.Message(c, "miler periodic log stored successfully")
|
|
}
|
|
|
|
func GetMilerPeriodicLogs(c *fiber.Ctx) error {
|
|
ctx := context.Background()
|
|
|
|
if db.Rdb == nil {
|
|
return utils.Internal(c, "cache service unavailable")
|
|
}
|
|
|
|
userID := c.Query("userid")
|
|
|
|
var keys []string
|
|
var err error
|
|
|
|
if userID != "" {
|
|
zsetKey := fmt.Sprintf("miler_periodic_logs:%s", userID)
|
|
keys, err = db.Rdb.ZRevRange(ctx, zsetKey, 0, 0).Result()
|
|
} else {
|
|
keys, err = db.Rdb.ZRevRange(ctx, "miler_periodic_logs_all", 0, 0).Result()
|
|
}
|
|
|
|
if err != nil {
|
|
return utils.Internal(c, "failed to fetch logs")
|
|
}
|
|
|
|
if len(keys) == 0 {
|
|
return utils.List(c, []interface{}{}, 0)
|
|
}
|
|
|
|
val, err := db.Rdb.Get(ctx, keys[0]).Result()
|
|
if err != nil {
|
|
return utils.Internal(c, "failed to retrieve log data")
|
|
}
|
|
|
|
var log map[string]interface{}
|
|
json.Unmarshal([]byte(val), &log)
|
|
|
|
return utils.OK(c, log)
|
|
}
|
|
|
|
func CreateMilerStatus(c *fiber.Ctx) error {
|
|
ctx := context.Background()
|
|
|
|
var status models.MilerStatus
|
|
if err := c.BodyParser(&status); err != nil {
|
|
return utils.BadRequest(c, "invalid request body")
|
|
}
|
|
|
|
// Identity from the token, not the body — otherwise one rider can set
|
|
// another rider's live status.
|
|
status.UserID = c.Locals("userid").(int)
|
|
|
|
if status.Status == "" {
|
|
return utils.BadRequest(c, "status is required")
|
|
}
|
|
|
|
key := fmt.Sprintf("miler_status:%d", status.UserID)
|
|
data, _ := json.Marshal(status)
|
|
|
|
if db.Rdb != nil {
|
|
if err := db.Rdb.Set(ctx, key, data, 0).Err(); err != nil {
|
|
return utils.Internal(c, "failed to store status")
|
|
}
|
|
|
|
db.Rdb.ZAdd(ctx, "miler_status_all", redis.Z{
|
|
Score: float64(time.Now().Unix()),
|
|
Member: key,
|
|
})
|
|
}
|
|
|
|
return utils.Message(c, "miler status updated successfully")
|
|
}
|
|
|
|
func GetMilerStatus(c *fiber.Ctx) error {
|
|
ctx := context.Background()
|
|
|
|
if db.Rdb == nil {
|
|
return utils.Internal(c, "cache service unavailable")
|
|
}
|
|
|
|
userIDStr := c.Query("userid")
|
|
|
|
if userIDStr != "" {
|
|
key := fmt.Sprintf("miler_status:%s", userIDStr)
|
|
|
|
val, err := db.Rdb.Get(ctx, key).Result()
|
|
if err != nil {
|
|
return utils.NotFound(c, "status not found for this miler")
|
|
}
|
|
|
|
var data map[string]interface{}
|
|
json.Unmarshal([]byte(val), &data)
|
|
|
|
return utils.OK(c, data)
|
|
}
|
|
|
|
pageStr := c.Query("page")
|
|
pageSizeStr := c.Query("pagesize")
|
|
|
|
page, _ := strconv.Atoi(pageStr)
|
|
pageSize, _ := strconv.Atoi(pageSizeStr)
|
|
|
|
var start, end int64
|
|
if page > 0 && pageSize > 0 {
|
|
offset := (page - 1) * pageSize
|
|
start = int64(offset)
|
|
end = int64(offset + pageSize - 1)
|
|
} else {
|
|
start = 0
|
|
end = -1
|
|
}
|
|
|
|
keys, err := db.Rdb.ZRevRange(ctx, "miler_status_all", start, end).Result()
|
|
if err != nil {
|
|
return utils.Internal(c, "failed to fetch statuses")
|
|
}
|
|
|
|
if len(keys) == 0 {
|
|
return utils.List(c, []interface{}{}, 0)
|
|
}
|
|
|
|
values, err := db.Rdb.MGet(ctx, keys...).Result()
|
|
if err != nil {
|
|
return utils.Internal(c, "failed to retrieve status data")
|
|
}
|
|
|
|
var result []map[string]interface{}
|
|
for _, val := range values {
|
|
if val == nil {
|
|
continue
|
|
}
|
|
var item map[string]interface{}
|
|
json.Unmarshal([]byte(val.(string)), &item)
|
|
result = append(result, item)
|
|
}
|
|
|
|
return utils.List(c, result, int64(len(result)))
|
|
}
|
|
|
|
func PublishConsignmentLogs(c *fiber.Ctx) error {
|
|
var input []models.ConsignmentLog
|
|
if err := c.BodyParser(&input); err != nil {
|
|
return utils.BadRequest(c, "invalid request body")
|
|
}
|
|
|
|
if len(input) == 0 {
|
|
return utils.BadRequest(c, "at least one log entry is required")
|
|
}
|
|
|
|
if db.Rdb == nil {
|
|
return utils.Internal(c, "cache service unavailable")
|
|
}
|
|
|
|
milerUserID := c.Locals("userid").(int)
|
|
|
|
pipe := db.Rdb.TxPipeline()
|
|
tx := db.DB.Begin()
|
|
|
|
for _, item := range input {
|
|
// Same rule as the other telemetry writers: the token owns the identity,
|
|
// so a batch can't be attributed to some other rider.
|
|
item.UserID = milerUserID
|
|
|
|
logTime, err := time.Parse("2006-01-02 15:04:05", item.LogDate)
|
|
if err != nil {
|
|
logTime = time.Now()
|
|
}
|
|
|
|
ts := logTime.Unix()
|
|
logKey := "Consignmentlogs:" + strconv.Itoa(item.ConsignmentID)
|
|
userIndexKey := "user:consignmentlogs:" + strconv.Itoa(item.UserID)
|
|
|
|
jsonData, _ := json.Marshal(item)
|
|
|
|
pipe.RPush(db.Ctx, logKey, jsonData)
|
|
pipe.ZAdd(db.Ctx, userIndexKey, redis.Z{
|
|
Score: float64(ts),
|
|
Member: item.ConsignmentID,
|
|
})
|
|
|
|
history := models.ConsignmentHistory{
|
|
Consignmentid: item.ConsignmentID,
|
|
Userid: &item.UserID,
|
|
Eventstatus: item.Status,
|
|
Remarks: fmt.Sprintf("GPS Update: Lat %s, Lon %s. Speed %s. Remarks: %s", item.Latitude, item.Longitude, item.Speed, item.Remarks),
|
|
Createdat: logTime,
|
|
}
|
|
if err := tx.Create(&history).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.Internal(c, "failed to persist consignment log")
|
|
}
|
|
}
|
|
|
|
if _, err := pipe.Exec(db.Ctx); err != nil {
|
|
tx.Rollback()
|
|
return utils.Internal(c, "failed to publish logs to cache")
|
|
}
|
|
|
|
if err := tx.Commit().Error; err != nil {
|
|
return utils.Internal(c, "failed to publish consignment logs")
|
|
}
|
|
return utils.Message(c, "consignment logs published successfully")
|
|
}
|
|
|
|
func GetConsignmentLogs(c *fiber.Ctx) error {
|
|
consignmentID, err := strconv.Atoi(c.Params("consignmentid"))
|
|
if err != nil {
|
|
return utils.BadRequest(c, "invalid consignment ID")
|
|
}
|
|
|
|
if db.Rdb == nil {
|
|
return utils.Internal(c, "cache service unavailable")
|
|
}
|
|
|
|
logKey := "Consignmentlogs:" + strconv.Itoa(consignmentID)
|
|
redisList, err := db.Rdb.LRange(db.Ctx, logKey, 0, -1).Result()
|
|
|
|
if err == nil && len(redisList) > 0 {
|
|
var logs []map[string]interface{}
|
|
for _, raw := range redisList {
|
|
var m map[string]interface{}
|
|
json.Unmarshal([]byte(raw), &m)
|
|
logs = append(logs, m)
|
|
}
|
|
return utils.List(c, logs, int64(len(logs)))
|
|
}
|
|
|
|
var history []models.ConsignmentHistory
|
|
if err := db.DB.Where("consignmentid = ?", consignmentID).Order("createdat ASC").Find(&history).Error; err != nil {
|
|
return utils.Internal(c, "failed to fetch consignment logs")
|
|
}
|
|
|
|
return utils.List(c, history, int64(len(history)))
|
|
}
|
|
|
|
func GetUserConsignmentLogs(c *fiber.Ctx) error {
|
|
userID, err := strconv.Atoi(c.Params("userid"))
|
|
if err != nil {
|
|
return utils.BadRequest(c, "invalid user ID")
|
|
}
|
|
|
|
// The path names a rider, so it has to be checked against the caller —
|
|
// otherwise any miler could read another miler's movement history simply by
|
|
// changing the number in the URL.
|
|
if userID != c.Locals("userid").(int) {
|
|
return utils.Forbidden(c, "you can only read your own consignment logs")
|
|
}
|
|
|
|
if db.Rdb == nil {
|
|
return utils.Internal(c, "cache service unavailable")
|
|
}
|
|
|
|
userIndexKey := "user:consignmentlogs:" + strconv.Itoa(userID)
|
|
members, err := db.Rdb.ZRevRange(db.Ctx, userIndexKey, 0, -1).Result()
|
|
if err != nil {
|
|
return utils.Internal(c, "failed to fetch consignment log index")
|
|
}
|
|
|
|
var logs []map[string]interface{}
|
|
for _, consignmentIDStr := range members {
|
|
logKey := "Consignmentlogs:" + consignmentIDStr
|
|
rawList, err := db.Rdb.LRange(db.Ctx, logKey, -1, -1).Result()
|
|
if err == nil && len(rawList) > 0 {
|
|
var m map[string]interface{}
|
|
json.Unmarshal([]byte(rawList[0]), &m)
|
|
logs = append(logs, m)
|
|
}
|
|
}
|
|
|
|
return utils.List(c, logs, int64(len(logs)))
|
|
}
|
|
|
|
func SaveMilerDeviceToken(c *fiber.Ctx) error {
|
|
milerUserID := c.Locals("userid").(int)
|
|
|
|
var req struct {
|
|
DeviceToken string `json:"device_token"`
|
|
}
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return utils.BadRequest(c, "invalid request body")
|
|
}
|
|
if req.DeviceToken == "" {
|
|
return utils.BadRequest(c, "device_token is required")
|
|
}
|
|
|
|
if err := db.DB.Model(&models.MilerProfile{}).
|
|
Where("userid = ?", milerUserID).
|
|
Update("device_token", req.DeviceToken).Error; err != nil {
|
|
return utils.Internal(c, "failed to save device token")
|
|
}
|
|
|
|
return utils.Message(c, "device token saved")
|
|
}
|