fix: panic recovery, rate limiting, transaction error handling, pagination

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>
This commit is contained in:
Suriya
2026-07-27 12:13:39 +05:30
parent a2b9268189
commit 2c26cbe4ba
240 changed files with 753 additions and 76753 deletions

View File

@@ -117,9 +117,16 @@ func GetAdminDashboard(c *fiber.Ctx) error {
// --------------------
func GetAppUsers(c *fiber.Ctx) error {
page := utils.ParsePage(c)
var total int64
if err := db.DB.Model(&models.AppUser{}).Where("roleid != ?", 5).Count(&total).Error; err != nil {
return utils.Internal(c, "failed to count users")
}
var users []models.AppUser
// Exclude Milers (Roleid = 5) from the CRM user list
if err := db.DB.Where("roleid != ?", 5).Find(&users).Error; err != nil {
if err := page.Apply(db.DB.Where("roleid != ?", 5)).Find(&users).Error; err != nil {
return utils.Internal(c, "failed to fetch users")
}
@@ -145,7 +152,7 @@ func GetAppUsers(c *fiber.Ctx) error {
"status": u.Status,
})
}
return utils.List(c, response, int64(len(response)))
return utils.Paginated(c, response, total, page)
}
func CreateAppUser(c *fiber.Ctx) error {
@@ -935,7 +942,9 @@ func CreateMiler(c *fiber.Ctx) error {
return utils.Internal(c, "failed to create miler profile")
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to create miler")
}
return utils.Created(c, profile)
}
@@ -995,11 +1004,20 @@ func BlockMiler(c *fiber.Ctx) error {
profile.Availabilitystatus = constants.MilerBlocked
profile.Updatedat = time.Now()
tx.Save(&profile)
if err := tx.Save(&profile).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to block miler profile")
}
tx.Model(&models.AppUser{}).Where("userid = ?", profile.Userid).Update("status", "Blocked")
if err := tx.Model(&models.AppUser{}).Where("userid = ?", profile.Userid).
Update("status", "Blocked").Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to block miler account")
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to block miler")
}
return utils.Message(c, "miler blocked successfully")
}
@@ -1060,27 +1078,27 @@ func GetAdminBookings(c *fiber.Ctx) error {
func CreateCRMBooking(c *fiber.Ctx) error {
type AdminBookingRequest struct {
Appcustomerid int `json:"appcustomerid"`
CustomerPhone string `json:"customer_phone"`
CustomerName string `json:"customer_name"`
Pickupaddress string `json:"pickupaddress"`
Pickuppincode string `json:"pickuppincode"`
Pickuplatitude float64 `json:"pickuplatitude"`
Pickuplongitude float64 `json:"pickuplongitude"`
Deliveryaddress string `json:"deliveryaddress"`
Deliverypincode string `json:"deliverypincode"`
Deliverycity string `json:"deliverycity"`
Deliverylatitude float64 `json:"deliverylatitude"`
Deliverylongitude float64 `json:"deliverylongitude"`
Providercompany string `json:"providercompany"`
Providerlocation string `json:"providerlocation"`
Notes string `json:"notes"`
ServiceOption string `json:"service_option"`
Finalprice float64 `json:"finalprice"`
Insuranceamount float64 `json:"insuranceamount"`
Preferredpickupfrom *time.Time `json:"preferredpickupfrom"`
Preferredpickupto *time.Time `json:"preferredpickupto"`
Parcels []dto.ParcelRequest `json:"parcels"`
Appcustomerid int `json:"appcustomerid"`
CustomerPhone string `json:"customer_phone"`
CustomerName string `json:"customer_name"`
Pickupaddress string `json:"pickupaddress"`
Pickuppincode string `json:"pickuppincode"`
Pickuplatitude float64 `json:"pickuplatitude"`
Pickuplongitude float64 `json:"pickuplongitude"`
Deliveryaddress string `json:"deliveryaddress"`
Deliverypincode string `json:"deliverypincode"`
Deliverycity string `json:"deliverycity"`
Deliverylatitude float64 `json:"deliverylatitude"`
Deliverylongitude float64 `json:"deliverylongitude"`
Providercompany string `json:"providercompany"`
Providerlocation string `json:"providerlocation"`
Notes string `json:"notes"`
ServiceOption string `json:"service_option"`
Finalprice float64 `json:"finalprice"`
Insuranceamount float64 `json:"insuranceamount"`
Preferredpickupfrom *time.Time `json:"preferredpickupfrom"`
Preferredpickupto *time.Time `json:"preferredpickupto"`
Parcels []dto.ParcelRequest `json:"parcels"`
}
req := new(AdminBookingRequest)
@@ -1178,7 +1196,7 @@ func CreateCRMBooking(c *fiber.Ctx) error {
if p.Needsinsurance {
parcel.Insuranceamount = p.Declaredvalue * 0.01
}
// If explicit insurance amount is provided from CRM, apply it to the first parcel
if req.Insuranceamount > 0 && totalWeight == math.Max(p.Weight, calculateVolumetricWeight(p.Length, p.Width, p.Height)) {
parcel.Insuranceamount = req.Insuranceamount
@@ -1195,10 +1213,10 @@ func CreateCRMBooking(c *fiber.Ctx) error {
if booking.Deliverylatitude != 0 && booking.Deliverylongitude != 0 {
distance = calculateDistance(booking.Pickuplatitude, booking.Pickuplongitude, booking.Deliverylatitude, booking.Deliverylongitude)
}
var pricing models.Pricing
err := tx.Where("status = ? AND ? BETWEEN effectivefrom AND effectiveto", "Active", time.Now()).Order("priority DESC").First(&pricing).Error
var estimatedPrice float64
var pricingID *int
if err == nil {
@@ -1257,22 +1275,27 @@ func CreateCRMBooking(c *fiber.Ctx) error {
Reason: "Oversized package / heavy weight",
Status: "Required",
}
tx.Create(&reqVeh)
if err := tx.Create(&reqVeh).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to save vehicle requirement")
}
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to create booking")
}
go assignment.AssignCRMMiler(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,
"status": constants.BookingPendingPickup,
"created_at": time.Now().UnixMilli(),
"booking_id": booking.Bookingid,
"booking_no": booking.Bookingno,
"customer_id": booking.Appcustomerid,
"pickup_address": booking.Pickupaddress,
"pickup_pincode": booking.Pickuppincode,
"status": constants.BookingPendingPickup,
"created_at": time.Now().UnixMilli(),
}
if data, err := json.Marshal(payload); err == nil {
db.Js.Publish("api.v1.bookings.create", data)
@@ -1425,11 +1448,18 @@ func AdminCancelBooking(c *fiber.Ctx) error {
// --------------------
func GetAdminConsignments(c *fiber.Ctx) error {
page := utils.ParsePage(c)
var total int64
if err := db.DB.Model(&models.Consignment{}).Count(&total).Error; err != nil {
return utils.Internal(c, "failed to count consignments")
}
var list []models.Consignment
if err := db.DB.Find(&list).Error; err != nil {
if err := page.Apply(db.DB).Find(&list).Error; err != nil {
return utils.Internal(c, "failed to fetch consignments")
}
return utils.List(c, list, int64(len(list)))
return utils.Paginated(c, list, total, page)
}
func GetAdminConsignmentDetails(c *fiber.Ctx) error {
@@ -1478,7 +1508,10 @@ func AdminUpdateConsignmentStatus(c *fiber.Ctx) error {
consignment.Status = req.Status
consignment.Updatedat = time.Now()
tx.Save(&consignment)
if err := tx.Save(&consignment).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update consignment status")
}
adminUserID := c.Locals("userid").(int)
@@ -1488,9 +1521,14 @@ func AdminUpdateConsignmentStatus(c *fiber.Ctx) error {
Eventstatus: req.Status,
Remarks: req.Remarks,
}
tx.Create(&history)
if err := tx.Create(&history).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to record consignment history")
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to update consignment")
}
return utils.OK(c, consignment)
}
@@ -1499,11 +1537,18 @@ func AdminUpdateConsignmentStatus(c *fiber.Ctx) error {
// --------------------
func GetTripsheets(c *fiber.Ctx) error {
page := utils.ParsePage(c)
var total int64
if err := db.DB.Model(&models.Tripsheet{}).Where("deletedat IS NULL").Count(&total).Error; err != nil {
return utils.Internal(c, "failed to count tripsheets")
}
var list []models.Tripsheet
if err := db.DB.Where("deletedat IS NULL").Find(&list).Error; err != nil {
if err := page.Apply(db.DB.Where("deletedat IS NULL")).Find(&list).Error; err != nil {
return utils.Internal(c, "failed to fetch tripsheets")
}
return utils.List(c, list, int64(len(list)))
return utils.Paginated(c, list, total, page)
}
func CreateTripsheet(c *fiber.Ctx) error {
@@ -1592,26 +1637,38 @@ func DispatchTripsheet(c *fiber.Ctx) error {
tripsheet.Status = constants.TripsheetDispatched
tripsheet.Dispatchtime = &now
tripsheet.Updatedat = now
tx.Save(&tripsheet)
if err := tx.Save(&tripsheet).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to dispatch tripsheet")
}
// Fetch all loaded items
var items []models.TripsheetItem
tx.Where("tripsheetid = ?", id).Find(&items)
if err := tx.Where("tripsheetid = ?", id).Find(&items).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to load tripsheet items")
}
adminUserID := c.Locals("userid").(int)
for _, item := range items {
// Update item scanning
tx.Model(&item).Updates(map[string]interface{}{
if err := tx.Model(&item).Updates(map[string]interface{}{
"scanstatus": constants.ScanLoaded,
"scannedat": &now,
})
}).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update tripsheet item scan status")
}
// Update consignment status to In_Transit
tx.Model(&models.Consignment{}).Where("consignmentid = ?", item.Consignmentid).Updates(map[string]interface{}{
if err := tx.Model(&models.Consignment{}).Where("consignmentid = ?", item.Consignmentid).Updates(map[string]interface{}{
"status": constants.ConsignmentInTransit,
"updatedat": now,
})
}).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update consignment status")
}
// Log history
history := models.ConsignmentHistory{
@@ -1621,10 +1678,15 @@ func DispatchTripsheet(c *fiber.Ctx) error {
Eventstatus: constants.ConsignmentInTransit,
Remarks: fmt.Sprintf("Consignment dispatched on Tripsheet %s", tripsheet.Tripsheetno),
}
tx.Create(&history)
if err := tx.Create(&history).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to record consignment history")
}
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to dispatch tripsheet")
}
return utils.OK(c, tripsheet)
}
@@ -1642,26 +1704,38 @@ func ArriveTripsheet(c *fiber.Ctx) error {
tripsheet.Status = constants.TripsheetArrived
tripsheet.Arrivaltime = &now
tripsheet.Updatedat = now
tx.Save(&tripsheet)
if err := tx.Save(&tripsheet).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to mark tripsheet arrived")
}
// Fetch items
var items []models.TripsheetItem
tx.Where("tripsheetid = ?", id).Find(&items)
if err := tx.Where("tripsheetid = ?", id).Find(&items).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to load tripsheet items")
}
adminUserID := c.Locals("userid").(int)
for _, item := range items {
tx.Model(&item).Updates(map[string]interface{}{
if err := tx.Model(&item).Updates(map[string]interface{}{
"scanstatus": constants.ScanUnloaded,
"scannedat": &now,
})
}).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update tripsheet item scan status")
}
// Update consignment status back to Inwarded_at_Hub at destination
tx.Model(&models.Consignment{}).Where("consignmentid = ?", item.Consignmentid).Updates(map[string]interface{}{
if err := tx.Model(&models.Consignment{}).Where("consignmentid = ?", item.Consignmentid).Updates(map[string]interface{}{
"status": constants.ConsignmentInwardedAtHub,
"currenthubid": tripsheet.Destinationhubid,
"updatedat": now,
})
}).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update consignment status")
}
// Log history
history := models.ConsignmentHistory{
@@ -1672,10 +1746,15 @@ func ArriveTripsheet(c *fiber.Ctx) error {
Eventstatus: constants.ConsignmentInwardedAtHub,
Remarks: fmt.Sprintf("Consignment arrived at Hub on Tripsheet %s", tripsheet.Tripsheetno),
}
tx.Create(&history)
if err := tx.Create(&history).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to record consignment history")
}
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to mark tripsheet arrived")
}
return utils.OK(c, tripsheet)
}
@@ -1796,7 +1875,7 @@ func GetPricingQuoteSimulate(c *fiber.Ctx) error {
}
distance := calculateDistance(req.Pickuplatitude, req.Pickuplongitude, req.Deliverylatitude, req.Deliverylongitude)
var totalWeight float64
for _, p := range req.Parcels {
volumetric := calculateVolumetricWeight(p.Length, p.Width, p.Height)
@@ -1831,11 +1910,18 @@ func GetPricingQuoteSimulate(c *fiber.Ctx) error {
// --------------------
func GetExceptions(c *fiber.Ctx) error {
page := utils.ParsePage(c)
var total int64
if err := db.DB.Model(&models.ConsignmentException{}).Where("deletedat IS NULL").Count(&total).Error; err != nil {
return utils.Internal(c, "failed to count exceptions")
}
var list []models.ConsignmentException
if err := db.DB.Where("deletedat IS NULL").Find(&list).Error; err != nil {
if err := page.Apply(db.DB.Where("deletedat IS NULL")).Find(&list).Error; err != nil {
return utils.Internal(c, "failed to fetch exceptions")
}
return utils.List(c, list, int64(len(list)))
return utils.Paginated(c, list, total, page)
}
func CreateException(c *fiber.Ctx) error {
@@ -1878,8 +1964,12 @@ func CreateException(c *fiber.Ctx) error {
}
if cStatus != "" {
tx.Model(&models.Consignment{}).Where("consignmentid = ?", req.Consignmentid).Update("status", cStatus)
if err := tx.Model(&models.Consignment{}).Where("consignmentid = ?", req.Consignmentid).
Update("status", cStatus).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update consignment status")
}
// Log history
history := models.ConsignmentHistory{
Consignmentid: req.Consignmentid,
@@ -1889,10 +1979,15 @@ func CreateException(c *fiber.Ctx) error {
Eventstatus: cStatus,
Remarks: fmt.Sprintf("Exception reported: %s. Description: %s", req.Exceptiontype, req.Description),
}
tx.Create(&history)
if err := tx.Create(&history).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to record consignment history")
}
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to record exception")
}
return utils.Created(c, exception)
}
@@ -2231,18 +2326,24 @@ func InternalReassign(c *fiber.Ctx) error {
now := time.Now()
if booking.Assignedmileruserid != nil {
tx.Model(&models.MilerProfile{}).
if err := tx.Model(&models.MilerProfile{}).
Where("userid = ?", *booking.Assignedmileruserid).
Updates(map[string]interface{}{
"availabilitystatus": constants.MilerAvailable,
"updatedat": now,
})
}).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to free previous miler")
}
tx.Delete(&models.BookingAssignment{},
if err := tx.Delete(&models.BookingAssignment{},
"bookingid = ? AND assignmentstatus IN ?",
booking.Bookingid,
[]string{constants.AssignmentAssigned, constants.AssignmentAccepted},
)
).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to clear previous assignment")
}
}
booking.Status = constants.BookingCreated
@@ -2253,7 +2354,9 @@ func InternalReassign(c *fiber.Ctx) error {
return utils.Internal(c, "failed to reset booking")
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to reassign booking")
}
if booking.Bookingsource == "CRM_Console" {
go assignment.AssignCRMMiler(booking.Bookingid)

View File

@@ -50,7 +50,9 @@ func AssignMilerToBooking(bookingID, milerUserID int, assignedByUserID *int) (*m
return nil, fmt.Errorf("failed to update miler availability: %w", err)
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return nil, fmt.Errorf("failed to commit miler assignment: %w", err)
}
if db.Js != nil {
payload := map[string]interface{}{

View File

@@ -119,20 +119,39 @@ func RegisterClient(c *fiber.Ctx) error {
role = auth.Role
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to register client")
}
return utils.Created(c, buildClientResponse(client, email, role))
}
func GetClients(c *fiber.Ctx) error {
page := utils.ParsePage(c)
var total int64
if err := db.DB.Model(&models.DoormileClient{}).Count(&total).Error; err != nil {
return utils.Internal(c, "failed to count clients")
}
var clients []models.DoormileClient
if err := db.DB.Find(&clients).Error; err != nil {
if err := page.Apply(db.DB).Find(&clients).Error; err != nil {
return utils.Internal(c, "failed to fetch clients")
}
// Bulk load auth records once and map by client_id for O(1) lookup
// Bulk load auth records for just this page's clients, rather than the
// whole auth table, and map by client_id for O(1) lookup.
clientIDs := make([]uint64, 0, len(clients))
for _, client := range clients {
clientIDs = append(clientIDs, client.ID)
}
var auths []models.DoormileAuth
db.DB.Find(&auths)
if len(clientIDs) > 0 {
if err := db.DB.Where("client_id IN ?", clientIDs).Find(&auths).Error; err != nil {
return utils.Internal(c, "failed to fetch client credentials")
}
}
authByClientID := make(map[uint64]models.DoormileAuth, len(auths))
for _, a := range auths {
if a.ClientID != nil {
@@ -146,7 +165,7 @@ func GetClients(c *fiber.Ctx) error {
responses = append(responses, buildClientResponse(client, auth.Email, auth.Role))
}
return utils.List(c, responses, int64(len(responses)))
return utils.Paginated(c, responses, total, page)
}
func GetClientDetails(c *fiber.Ctx) error {
@@ -304,7 +323,9 @@ func UpdateClient(c *fiber.Ctx) error {
}
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to update client")
}
return utils.OK(c, buildClientResponse(client, email, role))
}
@@ -332,7 +353,9 @@ func DeleteClient(c *fiber.Ctx) error {
return utils.Internal(c, "failed to delete client")
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to delete client")
}
return utils.Message(c, "client deleted successfully")
}

View File

@@ -519,10 +519,15 @@ func CreateCustomerBooking(c *fiber.Ctx) error {
Reason: "Oversized package / heavy weight",
Status: "Required",
}
tx.Create(&reqVeh)
if err := tx.Create(&reqVeh).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to save vehicle requirement")
}
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to create booking")
}
go assignment.AssignCustomerMiler(booking.Bookingid)

View File

@@ -318,16 +318,24 @@ func MilerDeliverConsignment(c *fiber.Ctx) error {
consignment.Status = constants.ConsignmentDelivered
consignment.Updatedat = time.Now()
tx.Save(&consignment)
if err := tx.Save(&consignment).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to mark consignment delivered")
}
tx.Model(&models.BookingAssignment{}).
if err := tx.Model(&models.BookingAssignment{}).
Where("bookingid = ? AND mileruserid = ?", booking.Bookingid, milerUserID).
Updates(map[string]interface{}{
"assignmentstatus": constants.AssignmentCompleted,
"completedat": time.Now(),
})
}).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to close assignment")
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to confirm delivery")
}
if db.Js != nil {
payload := map[string]interface{}{

View File

@@ -336,19 +336,31 @@ func AcceptMilerAssignment(c *fiber.Ctx) error {
now := time.Now()
assignment.Assignmentstatus = constants.AssignmentAccepted
assignment.Acceptedat = &now
tx.Save(&assignment)
if err := tx.Save(&assignment).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to accept assignment")
}
var booking models.PickupBooking
if err := tx.First(&booking, assignment.Bookingid).Error; err == nil {
booking.Status = constants.BookingPickupScheduled
booking.Assignedmileruserid = &milerUserID
booking.Updatedat = now
tx.Save(&booking)
if err := tx.Save(&booking).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update booking")
}
}
tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).Update("availabilitystatus", constants.MilerAssigned)
if err := tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).
Update("availabilitystatus", constants.MilerAssigned).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update miler availability")
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to commit assignment acceptance")
}
if booking.Bookingid != 0 {
var customer models.AppCustomer
@@ -394,19 +406,31 @@ func RejectMilerAssignment(c *fiber.Ctx) error {
ba.Assignmentstatus = constants.AssignmentRejected
ba.Remarks = req.Reason
tx.Save(&ba)
if err := tx.Save(&ba).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to reject assignment")
}
var booking models.PickupBooking
if err := tx.First(&booking, ba.Bookingid).Error; err == nil {
booking.Status = constants.BookingCreated
booking.Assignedmileruserid = nil
booking.Updatedat = time.Now()
tx.Save(&booking)
if err := tx.Save(&booking).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to release booking")
}
}
tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).Update("availabilitystatus", constants.MilerAvailable)
if err := tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).
Update("availabilitystatus", constants.MilerAvailable).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update miler availability")
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to commit assignment rejection")
}
if booking.Bookingid != 0 {
if booking.Bookingsource == "CRM_Console" {
@@ -434,9 +458,15 @@ func BookingReachedCustomer(c *fiber.Ctx) error {
return utils.NotFound(c, "assigned booking not found")
}
tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).Update("availabilitystatus", constants.MilerAtCustomer)
if err := tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).
Update("availabilitystatus", constants.MilerAtCustomer).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update miler availability")
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to confirm arrival")
}
return utils.Message(c, "arrival at customer confirmed")
}
@@ -500,7 +530,7 @@ func BookingParcelConfirm(c *fiber.Ctx) error {
}
return utils.OK(c, fiber.Map{
"parcels": parcels,
"parcels": parcels,
"total_chargeable_weight": totalChargeable,
})
}
@@ -544,6 +574,19 @@ func BookingPaymentCollect(c *fiber.Ctx) error {
return utils.Created(c, payment)
}
// isHyperlocal reports whether a pickup and delivery pincode fall in the same
// 3-digit postal area, following the same zone-prefix convention as
// hubPincodePrefix in hubController.go. A same-area booking needs no
// hub-to-hub tripsheet leg, so the collecting miler can carry it straight to
// final-mile delivery. Pincodes shorter than 3 characters are treated as
// unknown rather than matching, so bad data falls back to the safe hub route.
func isHyperlocal(pickupPincode, deliveryPincode string) bool {
if len(pickupPincode) < 3 || len(deliveryPincode) < 3 {
return false
}
return pickupPincode[:3] == deliveryPincode[:3]
}
func BookingPickupComplete(c *fiber.Ctx) error {
milerUserID := c.Locals("userid").(int)
bookingID, err := strconv.Atoi(c.Params("bookingid"))
@@ -562,14 +605,20 @@ func BookingPickupComplete(c *fiber.Ctx) error {
now := time.Now()
booking.Status = constants.BookingPickedUp
booking.Updatedat = now
tx.Save(&booking)
if err := tx.Save(&booking).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update booking status")
}
var profile models.MilerProfile
if err := tx.Where("userid = ?", milerUserID).First(&profile).Error; err == nil {
profile.Totalcompletedpickups += 1
profile.Availabilitystatus = constants.MilerPickedUp
profile.Updatedat = now
tx.Save(&profile)
if err := tx.Save(&profile).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update miler profile")
}
}
var parcels []models.BookingParcel
@@ -612,8 +661,7 @@ func BookingPickupComplete(c *fiber.Ctx) error {
// no hub-to-hub tripsheet leg is needed, so the same miler goes straight
// to final-mile delivery instead of parking the consignment at the hub.
consignmentStatus := constants.ConsignmentInwardedAtHub
if len(booking.Pickuppincode) >= 3 && len(booking.Deliverypincode) >= 3 &&
booking.Pickuppincode[:3] == booking.Deliverypincode[:3] {
if isHyperlocal(booking.Pickuppincode, booking.Deliverypincode) {
consignmentStatus = constants.ConsignmentOutForDelivery
}
@@ -657,7 +705,10 @@ func BookingPickupComplete(c *fiber.Ctx) error {
booking.Consignmentid = &consignment.Consignmentid
booking.Status = constants.BookingConvertedConsignment
tx.Save(&booking)
if err := tx.Save(&booking).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to link booking to consignment")
}
history := models.ConsignmentHistory{
Consignmentid: consignment.Consignmentid,
@@ -666,11 +717,20 @@ func BookingPickupComplete(c *fiber.Ctx) error {
Eventstatus: consignmentStatus,
Remarks: "Package collected by miler and converted to consignment",
}
tx.Create(&history)
if err := tx.Create(&history).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to record consignment history")
}
tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).Update("availabilitystatus", constants.MilerAvailable)
if err := tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).
Update("availabilitystatus", constants.MilerAvailable).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to update miler availability")
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to complete pickup")
}
var customer models.AppCustomer
if err := db.DB.Where("appcustomerid = ?", booking.Appcustomerid).First(&customer).Error; err == nil && customer.Devicetoken != "" {
@@ -936,7 +996,9 @@ func PublishConsignmentLogs(c *fiber.Ctx) error {
return utils.Internal(c, "failed to publish logs to cache")
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to publish consignment logs")
}
return utils.Message(c, "consignment logs published successfully")
}

View File

@@ -0,0 +1,94 @@
package controllers
import "testing"
func TestIsHyperlocal(t *testing.T) {
cases := []struct {
name string
pickup string
delivery string
want bool
}{
{
name: "same coimbatore postal area routes hyperlocal",
pickup: "641012", // Gandhipuram
delivery: "641004", // Peelamedu
want: true,
},
{
name: "identical pincode routes hyperlocal",
pickup: "641012",
delivery: "641012",
want: true,
},
{
name: "coimbatore to chennai is not hyperlocal",
pickup: "641012",
delivery: "600001",
want: false,
},
{
name: "adjacent prefixes are not hyperlocal",
pickup: "641012",
delivery: "642012",
want: false,
},
{
// Bad data must fall back to the hub route rather than sending a
// cross-city parcel out for local delivery.
name: "short pickup pincode is not hyperlocal",
pickup: "64",
delivery: "641012",
want: false,
},
{
name: "short delivery pincode is not hyperlocal",
pickup: "641012",
delivery: "64",
want: false,
},
{
name: "empty pincodes are not hyperlocal",
pickup: "",
delivery: "",
want: false,
},
{
name: "exactly three digits is enough to match",
pickup: "641",
delivery: "641999",
want: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := isHyperlocal(tc.pickup, tc.delivery); got != tc.want {
t.Errorf("isHyperlocal(%q, %q) = %v, want %v", tc.pickup, tc.delivery, got, tc.want)
}
})
}
}
func TestCalculateVolumetricWeight(t *testing.T) {
cases := []struct {
name string
l, w, h float64
want float64
}{
{name: "zero dimensions weigh nothing", l: 0, w: 0, h: 0, want: 0},
{name: "standard divisor of 5000", l: 50, w: 40, h: 30, want: 12},
{name: "one centimetre cube", l: 1, w: 1, h: 1, want: 1.0 / 5000.0},
{name: "large parcel", l: 100, w: 100, h: 100, want: 200},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := calculateVolumetricWeight(tc.l, tc.w, tc.h)
if got != tc.want {
t.Errorf("calculateVolumetricWeight(%v, %v, %v) = %v, want %v",
tc.l, tc.w, tc.h, got, tc.want)
}
})
}
}