backend requirements onthe xustomer app
This commit is contained in:
@@ -1,29 +1,26 @@
|
||||
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)
|
||||
}
|
||||
// Customer profile and saved addresses.
|
||||
//
|
||||
// The rest of the customer surface — auth, catalogue, estimate, bookings,
|
||||
// tracking, places, devices — lives in the cx*Controller.go files and answers
|
||||
// in the customer envelope (utils.CxOK / utils.CxFail). The PIN login,
|
||||
// single-destination booking create/list/detail/cancel and the /customer/track
|
||||
// read that used to live here were replaced by that surface, not moved: a
|
||||
// customer books a pickup with 1..N destinations now, and there is no shape in
|
||||
// which the old single-address request is still a valid booking.
|
||||
|
||||
func calculateDistance(lat1, lon1, lat2, lon2 float64) float64 {
|
||||
const R = 6371.0
|
||||
@@ -40,190 +37,15 @@ 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.CxNotFound(c, "We could not find your profile")
|
||||
}
|
||||
|
||||
return utils.OK(c, customer)
|
||||
return utils.CxOK(c, renderCustomer(&customer))
|
||||
}
|
||||
|
||||
func UpdateCustomerProfile(c *fiber.Ctx) error {
|
||||
@@ -231,76 +53,91 @@ func UpdateCustomerProfile(c *fiber.Ctx) error {
|
||||
|
||||
var customer models.AppCustomer
|
||||
if err := db.DB.First(&customer, customerID).Error; err != nil {
|
||||
return utils.NotFound(c, "profile not found")
|
||||
return utils.CxNotFound(c, "We could not find your profile")
|
||||
}
|
||||
|
||||
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"`
|
||||
// Pointer fields: an omitted key leaves the stored value alone, an explicit
|
||||
// value overwrites it. The previous version cleared email and lastname on
|
||||
// every call that did not resend them, which quietly wiped a customer's
|
||||
// email the first time they edited their name.
|
||||
var req struct {
|
||||
Name *string `json:"name"`
|
||||
Email *string `json:"email"`
|
||||
Defaultlatitude *float64 `json:"defaultLatitude"`
|
||||
Defaultlongitude *float64 `json:"defaultLongitude"`
|
||||
Defaultpincode *string `json:"defaultPincode"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return utils.CxBadRequest(c, "We could not read that request")
|
||||
}
|
||||
|
||||
req := new(ProfileUpdate)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
if req.Name != nil {
|
||||
first, last := splitName(*req.Name)
|
||||
if first == "" {
|
||||
return utils.CxFail(c, fiber.StatusBadRequest, utils.CxErrInvalidName, "Enter your full name")
|
||||
}
|
||||
customer.Firstname, customer.Lastname = first, last
|
||||
}
|
||||
|
||||
if req.Firstname != "" {
|
||||
customer.Firstname = req.Firstname
|
||||
if req.Email != nil {
|
||||
customer.Email = *req.Email
|
||||
}
|
||||
customer.Lastname = req.Lastname
|
||||
customer.Email = req.Email
|
||||
if req.Defaultlatitude != 0 {
|
||||
customer.Defaultlatitude = req.Defaultlatitude
|
||||
if req.Defaultlatitude != nil {
|
||||
customer.Defaultlatitude = *req.Defaultlatitude
|
||||
}
|
||||
if req.Defaultlongitude != 0 {
|
||||
customer.Defaultlongitude = req.Defaultlongitude
|
||||
if req.Defaultlongitude != nil {
|
||||
customer.Defaultlongitude = *req.Defaultlongitude
|
||||
}
|
||||
if req.Defaultpincode != "" {
|
||||
customer.Defaultpincode = req.Defaultpincode
|
||||
if req.Defaultpincode != nil {
|
||||
customer.Defaultpincode = *req.Defaultpincode
|
||||
}
|
||||
|
||||
if err := db.DB.Save(&customer).Error; err != nil {
|
||||
return utils.Internal(c, "failed to update profile")
|
||||
utils.Error("UpdateCustomerProfile: save failed", "customer_id", customerID, "error", err)
|
||||
return utils.CxInternal(c)
|
||||
}
|
||||
|
||||
return utils.OK(c, customer)
|
||||
return utils.CxOK(c, renderCustomer(&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")
|
||||
if err := db.DB.Where("appcustomerid = ? AND status = ?", customerID, "Active").
|
||||
Order("isdefault DESC, appcustomerlocationid DESC").Find(&locations).Error; err != nil {
|
||||
utils.Error("GetCustomerLocations: query failed", "customer_id", customerID, "error", err)
|
||||
return utils.CxInternal(c)
|
||||
}
|
||||
|
||||
return utils.List(c, locations, int64(len(locations)))
|
||||
out := make([]fiber.Map, 0, len(locations))
|
||||
for i := range locations {
|
||||
out = append(out, renderSavedAddress(&locations[i]))
|
||||
}
|
||||
return utils.CxList(c, out, len(out), nil)
|
||||
}
|
||||
|
||||
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)
|
||||
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")
|
||||
return utils.CxBadRequest(c, "You can save up to 10 addresses")
|
||||
}
|
||||
|
||||
req := new(dto.LocationCreateRequest)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
return utils.CxBadRequest(c, "We could not read that request")
|
||||
}
|
||||
|
||||
if req.Address == "" || req.Pincode == "" || req.Latitude == 0 || req.Longitude == 0 {
|
||||
return utils.BadRequest(c, "address, pincode, latitude, and longitude are required")
|
||||
return utils.CxBadRequest(c, "An address needs a street, a pincode and a map location")
|
||||
}
|
||||
|
||||
if req.Isdefault {
|
||||
db.DB.Model(&models.AppCustomerLocation{}).Where("appcustomerid = ?", customerID).Update("isdefault", false)
|
||||
db.DB.Model(&models.AppCustomerLocation{}).
|
||||
Where("appcustomerid = ?", customerID).Update("isdefault", false)
|
||||
}
|
||||
|
||||
location := models.AppCustomerLocation{
|
||||
@@ -320,27 +157,29 @@ func CreateCustomerLocation(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
if err := db.DB.Create(&location).Error; err != nil {
|
||||
return utils.Internal(c, "failed to save location")
|
||||
utils.Error("CreateCustomerLocation: insert failed", "customer_id", customerID, "error", err)
|
||||
return utils.CxInternal(c)
|
||||
}
|
||||
|
||||
return utils.Created(c, location)
|
||||
return utils.CxCreated(c, renderSavedAddress(&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")
|
||||
return utils.CxBadRequest(c, "That address could not be found")
|
||||
}
|
||||
|
||||
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")
|
||||
if err := db.DB.Where("appcustomerlocationid = ? AND appcustomerid = ?", locationID, customerID).
|
||||
First(&location).Error; err != nil {
|
||||
return utils.CxNotFound(c, "That address could not be found")
|
||||
}
|
||||
|
||||
req := new(dto.LocationCreateRequest)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
return utils.CxBadRequest(c, "We could not read that request")
|
||||
}
|
||||
|
||||
if req.Label != "" {
|
||||
@@ -366,392 +205,58 @@ func UpdateCustomerLocation(c *fiber.Ctx) error {
|
||||
location.Isdefault = req.Isdefault
|
||||
|
||||
if req.Isdefault {
|
||||
db.DB.Model(&models.AppCustomerLocation{}).Where("appcustomerid = ?", customerID).Update("isdefault", false)
|
||||
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")
|
||||
utils.Error("UpdateCustomerLocation: save failed", "location_id", locationID, "error", err)
|
||||
return utils.CxInternal(c)
|
||||
}
|
||||
|
||||
return utils.OK(c, location)
|
||||
return utils.CxOK(c, renderSavedAddress(&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")
|
||||
return utils.CxBadRequest(c, "That address could not be found")
|
||||
}
|
||||
|
||||
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")
|
||||
if err := db.DB.Where("appcustomerlocationid = ? AND appcustomerid = ?", locationID, customerID).
|
||||
First(&location).Error; err != nil {
|
||||
return utils.CxNotFound(c, "That address could not be found")
|
||||
}
|
||||
|
||||
location.Status = "InActive"
|
||||
db.DB.Save(&location)
|
||||
if err := db.DB.Save(&location).Error; err != nil {
|
||||
utils.Error("DeleteCustomerLocation: save failed", "location_id", locationID, "error", err)
|
||||
return utils.CxInternal(c)
|
||||
}
|
||||
|
||||
return utils.Message(c, "location deleted successfully")
|
||||
return utils.CxOK(c, fiber.Map{"id": strconv.Itoa(location.Appcustomerlocationid), "deleted": true})
|
||||
}
|
||||
|
||||
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")
|
||||
// renderSavedAddress is the one shape a saved address is returned in, matching
|
||||
// the two-line title/sub the pickup search and the booking pickup block use —
|
||||
// so an address picked from Saved and one picked from search are the same
|
||||
// object to the client.
|
||||
func renderSavedAddress(l *models.AppCustomerLocation) fiber.Map {
|
||||
title := l.Label
|
||||
if title == "" {
|
||||
title = l.Address
|
||||
}
|
||||
|
||||
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",
|
||||
// A B2C booking is always collected at the sender's own door, so the source
|
||||
// type is recorded rather than left blank — the rider app titles the stop
|
||||
// with the sender's name and address instead of grouping it under the
|
||||
// rider's own base.
|
||||
Pickupsourcetype: constants.PickupSourceCustomer,
|
||||
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)
|
||||
}
|
||||
|
||||
// fillConsignmentFacts attaches the consignment's tracking number and live
|
||||
// status to each booking that has one.
|
||||
//
|
||||
// A booking freezes at Converted_To_Consignment the moment it is collected,
|
||||
// while the parcel keeps moving on the consignment — so without this a customer
|
||||
// sees a status that stopped updating the instant their parcel was picked up.
|
||||
//
|
||||
// The tracking number matters more: GET /customer/track/:trackingno is keyed on
|
||||
// it, and nothing else the customer app can read exposes it. Tracking was
|
||||
// unreachable from the app not because the endpoint was missing but because the
|
||||
// number never travelled to it.
|
||||
//
|
||||
// Batched — one query for the whole page, not one per row.
|
||||
func fillConsignmentFacts(bookings []models.PickupBooking) {
|
||||
ids := make([]int, 0, len(bookings))
|
||||
for _, b := range bookings {
|
||||
if b.Consignmentid != nil {
|
||||
ids = append(ids, *b.Consignmentid)
|
||||
}
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var consignments []models.Consignment
|
||||
if err := db.DB.Select("consignmentid, trackingno, status").
|
||||
Where("consignmentid IN ?", ids).Find(&consignments).Error; err != nil {
|
||||
utils.Warn("fillConsignmentFacts: could not load consignments", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
byID := make(map[int]models.Consignment, len(consignments))
|
||||
for _, cn := range consignments {
|
||||
byID[cn.Consignmentid] = cn
|
||||
}
|
||||
for i := range bookings {
|
||||
if bookings[i].Consignmentid == nil {
|
||||
continue
|
||||
}
|
||||
if cn, ok := byID[*bookings[i].Consignmentid]; ok {
|
||||
bookings[i].Trackingno = cn.Trackingno
|
||||
bookings[i].Consignmentstatus = cn.Status
|
||||
}
|
||||
return fiber.Map{
|
||||
"id": strconv.Itoa(l.Appcustomerlocationid),
|
||||
"label": l.Label,
|
||||
"title": title,
|
||||
"sub": joinNonEmpty(", ", l.Address, l.Landmark, l.City, l.Pincode),
|
||||
"recipientName": l.Receivername,
|
||||
"recipientPhone": l.Receiverphone,
|
||||
"lat": l.Latitude,
|
||||
"lng": l.Longitude,
|
||||
"isDefault": l.Isdefault,
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
fillConsignmentFacts(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")
|
||||
}
|
||||
|
||||
one := []models.PickupBooking{booking}
|
||||
fillConsignmentFacts(one)
|
||||
booking = one[0]
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user