- 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>
701 lines
20 KiB
Go
701 lines
20 KiB
Go
package controllers
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"time"
|
|
|
|
"doormile/config"
|
|
"doormile/constants"
|
|
"doormile/db"
|
|
"doormile/dto"
|
|
"doormile/internal/assignment"
|
|
"doormile/models"
|
|
"doormile/utils"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
)
|
|
|
|
func generateBookingNo() string {
|
|
b := make([]byte, 4)
|
|
rand.Read(b)
|
|
return fmt.Sprintf("DM-BK-%X-%d", b, time.Now().Unix()%100000)
|
|
}
|
|
|
|
func calculateDistance(lat1, lon1, lat2, lon2 float64) float64 {
|
|
const R = 6371.0
|
|
dLat := (lat2 - lat1) * math.Pi / 180.0
|
|
dLon := (lon2 - lon1) * math.Pi / 180.0
|
|
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
|
|
math.Cos(lat1*math.Pi/180.0)*math.Cos(lat2*math.Pi/180.0)*
|
|
math.Sin(dLon/2)*math.Sin(dLon/2)
|
|
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
|
|
return R * c
|
|
}
|
|
|
|
func calculateVolumetricWeight(length, width, height float64) float64 {
|
|
return (length * width * height) / 5000.0
|
|
}
|
|
|
|
func RegisterCustomer(cfg *config.Config) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
req := new(dto.CustomerRegisterRequest)
|
|
if err := c.BodyParser(req); err != nil {
|
|
return utils.BadRequest(c, "invalid request body")
|
|
}
|
|
|
|
if req.Phone == "" || req.Firstname == "" || req.Pin == "" {
|
|
return utils.BadRequest(c, "phone, firstname, and pin are required")
|
|
}
|
|
|
|
pinHash, err := utils.HashPassword(req.Pin)
|
|
if err != nil {
|
|
return utils.Internal(c, "failed to process registration")
|
|
}
|
|
|
|
configID := req.Configid
|
|
if configID == 0 {
|
|
configID = 1001
|
|
}
|
|
|
|
var existing models.AppCustomer
|
|
if err := db.DB.Where("phone = ? AND configid = ?", req.Phone, configID).First(&existing).Error; err == nil {
|
|
return utils.Conflict(c, "a customer with this phone number already exists")
|
|
}
|
|
|
|
customer := models.AppCustomer{
|
|
Firstname: req.Firstname,
|
|
Lastname: req.Lastname,
|
|
Phone: req.Phone,
|
|
Email: req.Email,
|
|
Loginpinhash: pinHash,
|
|
Status: "Active",
|
|
Configid: configID,
|
|
}
|
|
|
|
if err := db.DB.Create(&customer).Error; err != nil {
|
|
return utils.Internal(c, "failed to register customer")
|
|
}
|
|
|
|
token, err := utils.GenerateToken(customer.Appcustomerid, customer.Phone, 9, 0, customer.Configid, cfg.JWTSecret)
|
|
if err != nil {
|
|
return utils.Internal(c, "registration successful but failed to generate token")
|
|
}
|
|
|
|
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
|
|
"success": true,
|
|
"token": token,
|
|
"user": customer,
|
|
})
|
|
}
|
|
}
|
|
|
|
func LoginCustomer(c *fiber.Ctx) error {
|
|
req := new(dto.CustomerLoginRequest)
|
|
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 customer models.AppCustomer
|
|
if err := db.DB.Where("phone = ? AND configid = ?", req.Phone, configID).First(&customer).Error; err != nil {
|
|
return utils.NotFound(c, "no account found for this phone number")
|
|
}
|
|
|
|
if customer.Status == "Blocked" {
|
|
return utils.Forbidden(c, "this account has been blocked")
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"success": true,
|
|
"message": "PIN verification required",
|
|
"phone": req.Phone,
|
|
})
|
|
}
|
|
|
|
func VerifyCustomerPin(cfg *config.Config) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
req := new(dto.CustomerPinVerifyRequest)
|
|
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 customer models.AppCustomer
|
|
if err := db.DB.Where("phone = ? AND configid = ?", req.Phone, configID).First(&customer).Error; err != nil {
|
|
return utils.NotFound(c, "customer not found")
|
|
}
|
|
|
|
if !utils.CheckPasswordHash(req.Pin, customer.Loginpinhash) {
|
|
return utils.Unauthorized(c, "incorrect PIN")
|
|
}
|
|
|
|
now := time.Now()
|
|
customer.Lastloginat = &now
|
|
if req.DeviceToken != "" {
|
|
customer.Devicetoken = req.DeviceToken
|
|
}
|
|
db.DB.Save(&customer)
|
|
|
|
token, err := utils.GenerateToken(customer.Appcustomerid, customer.Phone, 9, 0, customer.Configid, cfg.JWTSecret)
|
|
if err != nil {
|
|
return utils.Internal(c, "failed to generate token")
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"success": true,
|
|
"token": token,
|
|
"user": customer,
|
|
})
|
|
}
|
|
}
|
|
|
|
func ResetCustomerPin(c *fiber.Ctx) error {
|
|
req := new(dto.CustomerResetPinRequest)
|
|
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 customer models.AppCustomer
|
|
if err := db.DB.Where("phone = ? AND configid = ?", req.Phone, configID).First(&customer).Error; err != nil {
|
|
return utils.NotFound(c, "customer not found")
|
|
}
|
|
|
|
// Proof of identity is required before overwriting a login credential.
|
|
// Without it this endpoint reset any customer's PIN from their phone number
|
|
// alone — and phone numbers are the login identifier, not a secret — so
|
|
// reset-pin followed by verify-pin was a complete account takeover.
|
|
// The caller must first pass /customer/send-email-otp and
|
|
// /customer/verify-email-otp for this account's registered address.
|
|
if customer.Email == "" {
|
|
return utils.Forbidden(c, "this account has no registered email to verify against — contact support to reset the PIN")
|
|
}
|
|
if !ConsumeEmailVerification(customer.Email) {
|
|
return utils.Forbidden(c, "verify your registered email first via /customer/send-email-otp and /customer/verify-email-otp")
|
|
}
|
|
|
|
pinHash, err := utils.HashPassword(req.NewPin)
|
|
if err != nil {
|
|
return utils.Internal(c, "failed to process PIN reset")
|
|
}
|
|
|
|
customer.Loginpinhash = pinHash
|
|
if err := db.DB.Save(&customer).Error; err != nil {
|
|
return utils.Internal(c, "failed to reset PIN")
|
|
}
|
|
|
|
return utils.Message(c, "PIN reset successfully")
|
|
}
|
|
|
|
func GetCustomerProfile(c *fiber.Ctx) error {
|
|
customerID := c.Locals("userid").(int)
|
|
|
|
var customer models.AppCustomer
|
|
if err := db.DB.First(&customer, customerID).Error; err != nil {
|
|
return utils.NotFound(c, "profile not found")
|
|
}
|
|
|
|
return utils.OK(c, customer)
|
|
}
|
|
|
|
func UpdateCustomerProfile(c *fiber.Ctx) error {
|
|
customerID := c.Locals("userid").(int)
|
|
|
|
var customer models.AppCustomer
|
|
if err := db.DB.First(&customer, customerID).Error; err != nil {
|
|
return utils.NotFound(c, "profile not found")
|
|
}
|
|
|
|
type ProfileUpdate struct {
|
|
Firstname string `json:"firstname"`
|
|
Lastname string `json:"lastname"`
|
|
Email string `json:"email"`
|
|
Defaultlatitude float64 `json:"defaultlatitude"`
|
|
Defaultlongitude float64 `json:"defaultlongitude"`
|
|
Defaultpincode string `json:"defaultpincode"`
|
|
}
|
|
|
|
req := new(ProfileUpdate)
|
|
if err := c.BodyParser(req); err != nil {
|
|
return utils.BadRequest(c, "invalid request body")
|
|
}
|
|
|
|
if req.Firstname != "" {
|
|
customer.Firstname = req.Firstname
|
|
}
|
|
customer.Lastname = req.Lastname
|
|
customer.Email = req.Email
|
|
if req.Defaultlatitude != 0 {
|
|
customer.Defaultlatitude = req.Defaultlatitude
|
|
}
|
|
if req.Defaultlongitude != 0 {
|
|
customer.Defaultlongitude = req.Defaultlongitude
|
|
}
|
|
if req.Defaultpincode != "" {
|
|
customer.Defaultpincode = req.Defaultpincode
|
|
}
|
|
|
|
if err := db.DB.Save(&customer).Error; err != nil {
|
|
return utils.Internal(c, "failed to update profile")
|
|
}
|
|
|
|
return utils.OK(c, customer)
|
|
}
|
|
|
|
func GetCustomerLocations(c *fiber.Ctx) error {
|
|
customerID := c.Locals("userid").(int)
|
|
|
|
var locations []models.AppCustomerLocation
|
|
if err := db.DB.Where("appcustomerid = ? AND status = ?", customerID, "Active").Find(&locations).Error; err != nil {
|
|
return utils.Internal(c, "failed to fetch locations")
|
|
}
|
|
|
|
return utils.List(c, locations, int64(len(locations)))
|
|
}
|
|
|
|
func CreateCustomerLocation(c *fiber.Ctx) error {
|
|
customerID := c.Locals("userid").(int)
|
|
|
|
var count int64
|
|
db.DB.Model(&models.AppCustomerLocation{}).Where("appcustomerid = ? AND status = ?", customerID, "Active").Count(&count)
|
|
if count >= 10 {
|
|
return utils.BadRequest(c, "maximum of 10 saved locations allowed")
|
|
}
|
|
|
|
req := new(dto.LocationCreateRequest)
|
|
if err := c.BodyParser(req); err != nil {
|
|
return utils.BadRequest(c, "invalid request body")
|
|
}
|
|
|
|
if req.Address == "" || req.Pincode == "" || req.Latitude == 0 || req.Longitude == 0 {
|
|
return utils.BadRequest(c, "address, pincode, latitude, and longitude are required")
|
|
}
|
|
|
|
if req.Isdefault {
|
|
db.DB.Model(&models.AppCustomerLocation{}).Where("appcustomerid = ?", customerID).Update("isdefault", false)
|
|
}
|
|
|
|
location := models.AppCustomerLocation{
|
|
Appcustomerid: customerID,
|
|
Label: req.Label,
|
|
Receivername: req.Receivername,
|
|
Receiverphone: req.Receiverphone,
|
|
Address: req.Address,
|
|
Landmark: req.Landmark,
|
|
City: req.City,
|
|
State: req.State,
|
|
Pincode: req.Pincode,
|
|
Latitude: req.Latitude,
|
|
Longitude: req.Longitude,
|
|
Isdefault: req.Isdefault,
|
|
Status: "Active",
|
|
}
|
|
|
|
if err := db.DB.Create(&location).Error; err != nil {
|
|
return utils.Internal(c, "failed to save location")
|
|
}
|
|
|
|
return utils.Created(c, location)
|
|
}
|
|
|
|
func UpdateCustomerLocation(c *fiber.Ctx) error {
|
|
customerID := c.Locals("userid").(int)
|
|
locationID, err := strconv.Atoi(c.Params("id"))
|
|
if err != nil {
|
|
return utils.BadRequest(c, "invalid location ID")
|
|
}
|
|
|
|
var location models.AppCustomerLocation
|
|
if err := db.DB.Where("appcustomerlocationid = ? AND appcustomerid = ?", locationID, customerID).First(&location).Error; err != nil {
|
|
return utils.NotFound(c, "location not found")
|
|
}
|
|
|
|
req := new(dto.LocationCreateRequest)
|
|
if err := c.BodyParser(req); err != nil {
|
|
return utils.BadRequest(c, "invalid request body")
|
|
}
|
|
|
|
if req.Label != "" {
|
|
location.Label = req.Label
|
|
}
|
|
location.Receivername = req.Receivername
|
|
location.Receiverphone = req.Receiverphone
|
|
if req.Address != "" {
|
|
location.Address = req.Address
|
|
}
|
|
location.Landmark = req.Landmark
|
|
location.City = req.City
|
|
location.State = req.State
|
|
if req.Pincode != "" {
|
|
location.Pincode = req.Pincode
|
|
}
|
|
if req.Latitude != 0 {
|
|
location.Latitude = req.Latitude
|
|
}
|
|
if req.Longitude != 0 {
|
|
location.Longitude = req.Longitude
|
|
}
|
|
location.Isdefault = req.Isdefault
|
|
|
|
if req.Isdefault {
|
|
db.DB.Model(&models.AppCustomerLocation{}).Where("appcustomerid = ?", customerID).Update("isdefault", false)
|
|
}
|
|
|
|
if err := db.DB.Save(&location).Error; err != nil {
|
|
return utils.Internal(c, "failed to update location")
|
|
}
|
|
|
|
return utils.OK(c, location)
|
|
}
|
|
|
|
func DeleteCustomerLocation(c *fiber.Ctx) error {
|
|
customerID := c.Locals("userid").(int)
|
|
locationID, err := strconv.Atoi(c.Params("id"))
|
|
if err != nil {
|
|
return utils.BadRequest(c, "invalid location ID")
|
|
}
|
|
|
|
var location models.AppCustomerLocation
|
|
if err := db.DB.Where("appcustomerlocationid = ? AND appcustomerid = ?", locationID, customerID).First(&location).Error; err != nil {
|
|
return utils.NotFound(c, "location not found")
|
|
}
|
|
|
|
location.Status = "InActive"
|
|
db.DB.Save(&location)
|
|
|
|
return utils.Message(c, "location deleted successfully")
|
|
}
|
|
|
|
func CreateCustomerBooking(c *fiber.Ctx) error {
|
|
customerID := c.Locals("userid").(int)
|
|
|
|
req := new(dto.PickupBookingRequest)
|
|
if err := c.BodyParser(req); err != nil {
|
|
return utils.BadRequest(c, "invalid request body")
|
|
}
|
|
|
|
if req.Pickupaddress == "" || req.Pickuppincode == "" {
|
|
return utils.BadRequest(c, "pickup address and pincode are required")
|
|
}
|
|
|
|
if len(req.Parcels) == 0 {
|
|
return utils.BadRequest(c, "at least one parcel is required")
|
|
}
|
|
|
|
// Geocode delivery pincode to lat/lon when the app doesn't supply coordinates.
|
|
if req.Deliverylatitude == 0 && req.Deliverylongitude == 0 && req.Deliverypincode != "" {
|
|
if lat, lon, ok := pincodeToLatLon(req.Deliverypincode); ok {
|
|
req.Deliverylatitude = lat
|
|
req.Deliverylongitude = lon
|
|
}
|
|
}
|
|
|
|
tx := db.DB.Begin()
|
|
|
|
booking := models.PickupBooking{
|
|
Bookingno: generateBookingNo(),
|
|
Appcustomerid: customerID,
|
|
Pickuplocationid: req.Pickuplocationid,
|
|
Pickupaddress: req.Pickupaddress,
|
|
Pickuppincode: req.Pickuppincode,
|
|
Pickuplatitude: req.Pickuplatitude,
|
|
Pickuplongitude: req.Pickuplongitude,
|
|
Deliveryaddress: req.Deliveryaddress,
|
|
Deliverypincode: req.Deliverypincode,
|
|
Deliverylatitude: req.Deliverylatitude,
|
|
Deliverylongitude: req.Deliverylongitude,
|
|
Bookingsource: "Customer_App",
|
|
Status: constants.BookingPendingPickup,
|
|
Preferredpickupfrom: req.Preferredpickupfrom,
|
|
Preferredpickupto: req.Preferredpickupto,
|
|
}
|
|
|
|
if err := tx.Create(&booking).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.Internal(c, "failed to create booking")
|
|
}
|
|
|
|
var totalWeight float64
|
|
var totalVolume float64
|
|
var requiresLargeVehicle bool
|
|
for _, p := range req.Parcels {
|
|
volumetric := calculateVolumetricWeight(p.Length, p.Width, p.Height)
|
|
totalWeight += math.Max(p.Weight, volumetric)
|
|
totalVolume += p.Length * p.Width * p.Height
|
|
|
|
parcel := models.BookingParcel{
|
|
Bookingid: booking.Bookingid,
|
|
Itemcategory: p.Itemcategory,
|
|
Itemdescription: p.Itemdescription,
|
|
Declaredvalue: p.Declaredvalue,
|
|
Weight: p.Weight,
|
|
Length: p.Length,
|
|
Width: p.Width,
|
|
Height: p.Height,
|
|
Isfragile: p.Isfragile,
|
|
Needsinsurance: p.Needsinsurance,
|
|
Requireslargevehicle: p.Requireslargevehicle,
|
|
}
|
|
|
|
if p.Requireslargevehicle {
|
|
requiresLargeVehicle = true
|
|
}
|
|
|
|
if p.Needsinsurance {
|
|
parcel.Insuranceamount = p.Declaredvalue * 0.01
|
|
}
|
|
|
|
if err := tx.Create(&parcel).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.Internal(c, "failed to save parcel details")
|
|
}
|
|
}
|
|
|
|
serviceType := req.ServiceOption
|
|
if serviceType == "" {
|
|
serviceType = "Normal"
|
|
}
|
|
|
|
zone := resolveZone(req.Pickuppincode, req.Deliverypincode)
|
|
itemCategory := normalizePricingCategory(req.Parcels[0].Itemcategory)
|
|
|
|
var estimatedPrice float64
|
|
var pricingID *int
|
|
if price, pid, found := lookupDoormilePrice(zone, mapServiceTypeToPricing(serviceType), totalWeight, itemCategory); found {
|
|
estimatedPrice = price
|
|
pricingID = pid
|
|
} else {
|
|
var distance float64
|
|
if booking.Deliverylatitude != 0 && booking.Deliverylongitude != 0 {
|
|
distance = calculateDistance(booking.Pickuplatitude, booking.Pickuplongitude, booking.Deliverylatitude, booking.Deliverylongitude)
|
|
}
|
|
estimatedPrice = 50.0 + (distance * 5.0) + (totalWeight * 10.0)
|
|
}
|
|
|
|
now := time.Now()
|
|
estDelivery := now.Add(24 * time.Hour)
|
|
slaDue := now.Add(36 * time.Hour)
|
|
if serviceType == "Fast" {
|
|
estDelivery = now.Add(12 * time.Hour)
|
|
slaDue = now.Add(18 * time.Hour)
|
|
} else if serviceType == "Superfast" {
|
|
estDelivery = now.Add(6 * time.Hour)
|
|
slaDue = now.Add(9 * time.Hour)
|
|
}
|
|
|
|
srvOption := models.BookingServiceOption{
|
|
Bookingid: booking.Bookingid,
|
|
Servicetype: serviceType,
|
|
Estimatedprice: estimatedPrice,
|
|
Pricingid: pricingID,
|
|
Estimateddeliveryat: &estDelivery,
|
|
Sladueat: &slaDue,
|
|
}
|
|
|
|
if err := tx.Create(&srvOption).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.Internal(c, "failed to save service option")
|
|
}
|
|
|
|
if requiresLargeVehicle || totalVolume > 0 && totalWeight > 20.0 {
|
|
reqVeh := models.BookingVehicleRequirement{
|
|
Bookingid: booking.Bookingid,
|
|
Requiredvehicletype: "truck",
|
|
Reason: "Oversized package / heavy weight",
|
|
Status: "Required",
|
|
}
|
|
if err := tx.Create(&reqVeh).Error; err != nil {
|
|
tx.Rollback()
|
|
return utils.Internal(c, "failed to save vehicle requirement")
|
|
}
|
|
}
|
|
|
|
if err := tx.Commit().Error; err != nil {
|
|
return utils.Internal(c, "failed to create booking")
|
|
}
|
|
|
|
go assignment.AssignCustomerMiler(booking.Bookingid)
|
|
|
|
if db.Js != nil {
|
|
payload := map[string]interface{}{
|
|
"booking_id": booking.Bookingid,
|
|
"booking_no": booking.Bookingno,
|
|
"customer_id": booking.Appcustomerid,
|
|
"pickup_address": booking.Pickupaddress,
|
|
"pickup_pincode": booking.Pickuppincode,
|
|
"delivery_address": booking.Deliveryaddress,
|
|
"delivery_pincode": booking.Deliverypincode,
|
|
"status": constants.BookingPendingPickup,
|
|
"created_at": time.Now().UnixMilli(),
|
|
}
|
|
if data, err := json.Marshal(payload); err == nil {
|
|
if _, err := db.Js.Publish("api.v1.bookings.create", data); err != nil {
|
|
utils.Warn("Failed to publish booking.create to NATS", "booking_id", booking.Bookingid, "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
db.DB.Preload("Parcels").Preload("ServiceOptions").First(&booking, booking.Bookingid)
|
|
|
|
return utils.Created(c, booking)
|
|
}
|
|
|
|
func GetCustomerBookings(c *fiber.Ctx) error {
|
|
customerID := c.Locals("userid").(int)
|
|
|
|
var bookings []models.PickupBooking
|
|
if err := db.DB.Preload("Parcels").Preload("ServiceOptions").Where("appcustomerid = ?", customerID).Order("createdat DESC").Find(&bookings).Error; err != nil {
|
|
return utils.Internal(c, "failed to fetch bookings")
|
|
}
|
|
|
|
return utils.List(c, bookings, int64(len(bookings)))
|
|
}
|
|
|
|
func GetCustomerBookingDetails(c *fiber.Ctx) error {
|
|
customerID := 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.Preload("Parcels").Preload("ServiceOptions").Preload("Payments").Where("bookingid = ? AND appcustomerid = ?", bookingID, customerID).First(&booking).Error; err != nil {
|
|
return utils.NotFound(c, "booking not found")
|
|
}
|
|
|
|
return utils.OK(c, booking)
|
|
}
|
|
|
|
func CancelCustomerBooking(c *fiber.Ctx) error {
|
|
customerID := 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 appcustomerid = ?", bookingID, customerID).First(&booking).Error; err != nil {
|
|
return utils.NotFound(c, "booking not found")
|
|
}
|
|
|
|
if booking.Status == constants.BookingPickedUp || booking.Status == constants.BookingConvertedConsignment {
|
|
return utils.BadRequest(c, "booking cannot be cancelled after the package has been picked up")
|
|
}
|
|
|
|
booking.Status = constants.BookingCancelled
|
|
booking.Updatedat = time.Now()
|
|
db.DB.Save(&booking)
|
|
|
|
if db.Js != nil {
|
|
payload := map[string]interface{}{
|
|
"booking_id": booking.Bookingid,
|
|
"booking_no": booking.Bookingno,
|
|
"customer_id": booking.Appcustomerid,
|
|
"status": "Cancelled",
|
|
"cancelled_at": time.Now().UnixMilli(),
|
|
}
|
|
if data, err := json.Marshal(payload); err == nil {
|
|
if _, err := db.Js.Publish("api.v1.bookings.cancel", data); err != nil {
|
|
utils.Warn("Failed to publish booking.cancel to NATS", "booking_id", booking.Bookingid, "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return utils.OK(c, booking)
|
|
}
|
|
|
|
func GetCustomerBookingQuote(c *fiber.Ctx) error {
|
|
customerID := c.Locals("userid").(int)
|
|
bookingID, err := strconv.Atoi(c.Params("bookingid"))
|
|
if err != nil {
|
|
return utils.BadRequest(c, "invalid booking ID")
|
|
}
|
|
|
|
// Ownership is checked here as it is on the other booking routes — without
|
|
// it any signed-in customer could read the price quoted on anyone else's
|
|
// booking just by walking the id.
|
|
var booking models.PickupBooking
|
|
if err := db.DB.Select("bookingid").
|
|
Where("bookingid = ? AND appcustomerid = ?", bookingID, customerID).
|
|
First(&booking).Error; err != nil {
|
|
return utils.NotFound(c, "booking not found")
|
|
}
|
|
|
|
var serviceOpt models.BookingServiceOption
|
|
if err := db.DB.Where("bookingid = ?", bookingID).Order("createdat DESC").First(&serviceOpt).Error; err != nil {
|
|
return utils.NotFound(c, "price quote not found for this booking")
|
|
}
|
|
|
|
return utils.OK(c, serviceOpt)
|
|
}
|
|
|
|
func TrackConsignment(c *fiber.Ctx) error {
|
|
trackingNo := c.Params("trackingno")
|
|
if trackingNo == "" {
|
|
return utils.BadRequest(c, "tracking number is required")
|
|
}
|
|
|
|
var consignment models.Consignment
|
|
if err := db.DB.Where("trackingno = ?", trackingNo).First(&consignment).Error; err != nil {
|
|
return utils.NotFound(c, "no shipment found for this tracking number")
|
|
}
|
|
|
|
var history []models.ConsignmentHistory
|
|
db.DB.Where("consignmentid = ?", consignment.Consignmentid).Order("createdat DESC").Find(&history)
|
|
|
|
return utils.OK(c, fiber.Map{
|
|
"consignment": consignment,
|
|
"history": history,
|
|
})
|
|
}
|
|
|
|
func SaveCustomerDeviceToken(c *fiber.Ctx) error {
|
|
customerID := 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.AppCustomer{}).
|
|
Where("appcustomerid = ?", customerID).
|
|
Update("device_token", req.DeviceToken).Error; err != nil {
|
|
return utils.Internal(c, "failed to save device token")
|
|
}
|
|
|
|
return utils.Message(c, "device token saved")
|
|
}
|