package controllers import ( "crypto/rand" "encoding/json" "fmt" "math" "strconv" "time" "doormile/config" "doormile/constants" "doormile/db" "doormile/dto" "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 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") } 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") } } var distance float64 if booking.Deliverylatitude != 0 && booking.Deliverylongitude != 0 { distance = calculateDistance(booking.Pickuplatitude, booking.Pickuplongitude, booking.Deliverylatitude, booking.Deliverylongitude) } var pricing models.Pricing pricingErr := tx.Where("status = ? AND ? BETWEEN effectivefrom AND effectiveto", "Active", time.Now()).Order("priority DESC").First(&pricing).Error var estimatedPrice float64 var pricingID *int if pricingErr == nil { pricingID = &pricing.Pricingid kmExtra := math.Max(0, distance-pricing.Basedistance) kgExtra := math.Max(0, totalWeight-pricing.Baseweight) estimatedPrice = pricing.Baseprice + (kmExtra * pricing.Priceperkm) + (kgExtra * pricing.Priceperkg) + pricing.Handlingcharges } else { estimatedPrice = 50.0 + (distance * 5.0) + (totalWeight * 10.0) } serviceType := req.ServiceOption if serviceType == "" { serviceType = "Normal" } if serviceType == "Fast" { estimatedPrice *= 1.25 } else if serviceType == "Superfast" { estimatedPrice *= 1.5 } 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, Estimateddeliveryat: &estDelivery, Sladueat: &slaDue, Pricingid: pricingID, } 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", } tx.Create(&reqVeh) } tx.Commit() 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, }) }