Hardening pass over the API surface. No route's auth requirements change.
Resilience:
- Add recover middleware. There was none, so an unhandled panic in any
handler propagated out of the process instead of becoming a 500.
- Add a centralized ErrorHandler so errors and recovered panics return the
same {success,message} envelope as the utils helpers, not Fiber's default
plain-text body. 5xx responses are logged with method and path.
Rate limiting:
- Global 300/min per IP as an abuse backstop, exempting health/readiness
probes and websocket upgrades.
- 10/min shared across every credential endpoint (customer/miler/admin/hub
login, verify-pin, reset-pin, email OTP). PINs are 4 digits, so the whole
keyspace was previously walkable in seconds. One shared limiter instance
means rotating between endpoints doesn't reset the budget.
- Add TRUSTED_PROXIES config. Limits key on c.IP(), which behind a TLS
terminator is the proxy, collapsing every client into one bucket. When set,
X-Forwarded-For is honoured only from those proxies so the header can't be
spoofed to dodge the limit. Logs a warning when unset.
Transactions:
- Check the error on all 51 previously-unchecked tx.Save/Create/Delete/
Model(...).Update/Commit calls across 6 controllers. A failed write inside
a transaction was silently ignored and the request still reported success;
an unchecked Commit could fail with the caller told everything worked.
Each site now rolls back and returns a specific message.
Pagination:
- Add utils.ParsePage/Paginated, reusing the pageno/pagesize convention
GetAdminBookings already established. Default 500, hard cap 1000.
- Apply to the previously unbounded consignments, tripsheets, exceptions,
app-users and clients endpoints. Defaults are high so existing consoles
that don't paginate keep working; the cap only stops a growing table from
being loaded wholesale. total is now a real COUNT, not len(data).
- GetClients also loaded the entire auth table to join in memory; it now
fetches only the current page's rows.
Tests (first in the repo):
- Extract the hyperlocal pincode rule out of BookingPickupComplete into
isHyperlocal so it is testable, covering the short/empty pincode fallback.
- Cover calculateVolumetricWeight and the ParsePage clamping rules.
Repo hygiene:
- Tag scratch/*.go with //go:build ignore. Each declared its own main(), so
`go build ./...` failed on redeclaration; it now passes repo-wide.
- Untrack scratch/node_modules (216 files) and ignore node_modules, test
artifacts, and the `doormile` binary `go build .` emits.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
677 lines
19 KiB
Go
677 lines
19 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")
|
|
}
|
|
|
|
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 {
|
|
bookingID, err := strconv.Atoi(c.Params("bookingid"))
|
|
if err != nil {
|
|
return utils.BadRequest(c, "invalid booking ID")
|
|
}
|
|
|
|
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")
|
|
}
|