Fix 9 backend bugs: pricing, zones, geocoding, device tokens, assignment retry
- pricingid null: lookupDoormilePrice now returns matched rule ID; wired to BookingServiceOption.Pricingid - Zone rename: Interstate→Regional, OtherState→National throughout (code + DB migrated) - Zone from pincodes: CheckPrice now accepts pickup_pincode+delivery_pincode and auto-resolves zone - Delivery geocoding: pincodeToLatLon() maps 3-digit prefix to city coords when lat/lon are 0 - Device tokens: device_token field added to PinVerify DTOs; saved on both customer and miler login - Assignment retry: RejectMilerAssignment now re-triggers AssignCustomerMiler/AssignCRMMiler immediately - Provider empty B2C: defaults to Doormile when no pricing provider row matches - City gate 422→400: StatusUnprocessableEntity corrected to StatusBadRequest - Miler GPS 0,0: WS tracking falls back to MilerProfile DB coords when Redis key is expired Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -151,6 +151,9 @@ func VerifyCustomerPin(cfg *config.Config) fiber.Handler {
|
|||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
customer.Lastloginat = &now
|
customer.Lastloginat = &now
|
||||||
|
if req.DeviceToken != "" {
|
||||||
|
customer.Devicetoken = req.DeviceToken
|
||||||
|
}
|
||||||
db.DB.Save(&customer)
|
db.DB.Save(&customer)
|
||||||
|
|
||||||
token, err := utils.GenerateToken(customer.Appcustomerid, customer.Phone, 9, 0, customer.Configid, cfg.JWTSecret)
|
token, err := utils.GenerateToken(customer.Appcustomerid, customer.Phone, 9, 0, customer.Configid, cfg.JWTSecret)
|
||||||
@@ -394,6 +397,14 @@ func CreateCustomerBooking(c *fiber.Ctx) error {
|
|||||||
return utils.BadRequest(c, "at least one parcel is required")
|
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()
|
tx := db.DB.Begin()
|
||||||
|
|
||||||
booking := models.PickupBooking{
|
booking := models.PickupBooking{
|
||||||
@@ -464,8 +475,10 @@ func CreateCustomerBooking(c *fiber.Ctx) error {
|
|||||||
itemCategory := normalizePricingCategory(req.Parcels[0].Itemcategory)
|
itemCategory := normalizePricingCategory(req.Parcels[0].Itemcategory)
|
||||||
|
|
||||||
var estimatedPrice float64
|
var estimatedPrice float64
|
||||||
if price, found := lookupDoormilePrice(zone, mapServiceTypeToPricing(serviceType), totalWeight, itemCategory); found {
|
var pricingID *int
|
||||||
|
if price, pid, found := lookupDoormilePrice(zone, mapServiceTypeToPricing(serviceType), totalWeight, itemCategory); found {
|
||||||
estimatedPrice = price
|
estimatedPrice = price
|
||||||
|
pricingID = pid
|
||||||
} else {
|
} else {
|
||||||
var distance float64
|
var distance float64
|
||||||
if booking.Deliverylatitude != 0 && booking.Deliverylongitude != 0 {
|
if booking.Deliverylatitude != 0 && booking.Deliverylongitude != 0 {
|
||||||
@@ -489,6 +502,7 @@ func CreateCustomerBooking(c *fiber.Ctx) error {
|
|||||||
Bookingid: booking.Bookingid,
|
Bookingid: booking.Bookingid,
|
||||||
Servicetype: serviceType,
|
Servicetype: serviceType,
|
||||||
Estimatedprice: estimatedPrice,
|
Estimatedprice: estimatedPrice,
|
||||||
|
Pricingid: pricingID,
|
||||||
Estimateddeliveryat: &estDelivery,
|
Estimateddeliveryat: &estDelivery,
|
||||||
Sladueat: &slaDue,
|
Sladueat: &slaDue,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ import (
|
|||||||
|
|
||||||
// Valid enum values
|
// Valid enum values
|
||||||
var validZones = map[string]bool{
|
var validZones = map[string]bool{
|
||||||
"Local": true,
|
"Local": true,
|
||||||
"Interstate": true,
|
"Regional": true,
|
||||||
"OtherState": true,
|
"National": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
var validCategories = map[string]bool{
|
var validCategories = map[string]bool{
|
||||||
@@ -158,10 +158,12 @@ func buildPriceResponse(zone, servicetype string, weight float64, rules []models
|
|||||||
// Body: { zone, service_type, weight, category? }
|
// Body: { zone, service_type, weight, category? }
|
||||||
func CheckPrice(c *fiber.Ctx) error {
|
func CheckPrice(c *fiber.Ctx) error {
|
||||||
type req struct {
|
type req struct {
|
||||||
Zone string `json:"zone"`
|
Zone string `json:"zone"`
|
||||||
ServiceType string `json:"service_type"`
|
ServiceType string `json:"service_type"`
|
||||||
Weight float64 `json:"weight"`
|
Weight float64 `json:"weight"`
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
|
PickupPincode string `json:"pickup_pincode"`
|
||||||
|
DeliveryPincode string `json:"delivery_pincode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
body := new(req)
|
body := new(req)
|
||||||
@@ -169,11 +171,16 @@ func CheckPrice(c *fiber.Ctx) error {
|
|||||||
return utils.BadRequest(c, "invalid request body")
|
return utils.BadRequest(c, "invalid request body")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-resolve zone from pincodes when not explicitly provided.
|
||||||
|
if body.Zone == "" && body.PickupPincode != "" && body.DeliveryPincode != "" {
|
||||||
|
body.Zone = resolveZone(body.PickupPincode, body.DeliveryPincode)
|
||||||
|
}
|
||||||
|
|
||||||
if !validZones[body.Zone] {
|
if !validZones[body.Zone] {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": "invalid zone",
|
"message": "invalid zone",
|
||||||
"valid_zones": []string{"Local", "Interstate", "OtherState"},
|
"valid_zones": []string{"Local", "Regional", "National"},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if !validServiceTypes[body.ServiceType] {
|
if !validServiceTypes[body.ServiceType] {
|
||||||
@@ -425,7 +432,7 @@ func WarmPricingCache() {
|
|||||||
utils.Info("WarmPricingCache: warming pricing cache from Postgres...")
|
utils.Info("WarmPricingCache: warming pricing cache from Postgres...")
|
||||||
|
|
||||||
// Iterate every zone × servicetype combination
|
// Iterate every zone × servicetype combination
|
||||||
zones := []string{"Local", "Interstate", "OtherState"}
|
zones := []string{"Local", "Regional", "National"}
|
||||||
serviceTypes := []string{"Normal", "Express"}
|
serviceTypes := []string{"Normal", "Express"}
|
||||||
|
|
||||||
warmed := 0
|
warmed := 0
|
||||||
@@ -451,8 +458,8 @@ func GetPricingMeta(c *fiber.Ctx) error {
|
|||||||
return utils.OK(c, fiber.Map{
|
return utils.OK(c, fiber.Map{
|
||||||
"zones": []fiber.Map{
|
"zones": []fiber.Map{
|
||||||
{"value": "Local", "label": "Local / Same City"},
|
{"value": "Local", "label": "Local / Same City"},
|
||||||
{"value": "Interstate", "label": "Interstate"},
|
{"value": "Regional", "label": "Regional / Same State"},
|
||||||
{"value": "OtherState", "label": "Other State"},
|
{"value": "National", "label": "National / Other State"},
|
||||||
},
|
},
|
||||||
"categories": []fiber.Map{
|
"categories": []fiber.Map{
|
||||||
{"value": "General", "label": "General Goods"},
|
{"value": "General", "label": "General Goods"},
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"doormile/constants"
|
"doormile/constants"
|
||||||
"doormile/db"
|
"doormile/db"
|
||||||
"doormile/dto"
|
"doormile/dto"
|
||||||
|
"doormile/internal/assignment"
|
||||||
"doormile/internal/notify"
|
"doormile/internal/notify"
|
||||||
"doormile/models"
|
"doormile/models"
|
||||||
"doormile/utils"
|
"doormile/utils"
|
||||||
@@ -114,6 +115,10 @@ func VerifyMilerPin(cfg *config.Config) fiber.Handler {
|
|||||||
}
|
}
|
||||||
db.DB.Create(&profile)
|
db.DB.Create(&profile)
|
||||||
}
|
}
|
||||||
|
if req.DeviceToken != "" && profile.Devicetoken != req.DeviceToken {
|
||||||
|
profile.Devicetoken = req.DeviceToken
|
||||||
|
db.DB.Model(&profile).Update("device_token", req.DeviceToken)
|
||||||
|
}
|
||||||
|
|
||||||
return c.JSON(fiber.Map{
|
return c.JSON(fiber.Map{
|
||||||
"success": true,
|
"success": true,
|
||||||
@@ -364,18 +369,18 @@ func RejectMilerAssignment(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
tx := db.DB.Begin()
|
tx := db.DB.Begin()
|
||||||
|
|
||||||
var assignment models.BookingAssignment
|
var ba models.BookingAssignment
|
||||||
if err := tx.Where("bookingassignmentid = ? AND mileruserid = ?", assignmentID, milerUserID).First(&assignment).Error; err != nil {
|
if err := tx.Where("bookingassignmentid = ? AND mileruserid = ?", assignmentID, milerUserID).First(&ba).Error; err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return utils.NotFound(c, "assignment not found")
|
return utils.NotFound(c, "assignment not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
assignment.Assignmentstatus = constants.AssignmentRejected
|
ba.Assignmentstatus = constants.AssignmentRejected
|
||||||
assignment.Remarks = c.Query("reason", "Rejected by rider")
|
ba.Remarks = c.Query("reason", "Rejected by rider")
|
||||||
tx.Save(&assignment)
|
tx.Save(&ba)
|
||||||
|
|
||||||
var booking models.PickupBooking
|
var booking models.PickupBooking
|
||||||
if err := tx.First(&booking, assignment.Bookingid).Error; err == nil {
|
if err := tx.First(&booking, ba.Bookingid).Error; err == nil {
|
||||||
booking.Status = constants.BookingCreated
|
booking.Status = constants.BookingCreated
|
||||||
booking.Assignedmileruserid = nil
|
booking.Assignedmileruserid = nil
|
||||||
booking.Updatedat = time.Now()
|
booking.Updatedat = time.Now()
|
||||||
@@ -385,6 +390,15 @@ func RejectMilerAssignment(c *fiber.Ctx) error {
|
|||||||
tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).Update("availabilitystatus", constants.MilerAvailable)
|
tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).Update("availabilitystatus", constants.MilerAvailable)
|
||||||
|
|
||||||
tx.Commit()
|
tx.Commit()
|
||||||
|
|
||||||
|
if booking.Bookingid != 0 {
|
||||||
|
if booking.Bookingsource == "CRM_Console" {
|
||||||
|
go assignment.AssignCRMMiler(booking.Bookingid)
|
||||||
|
} else {
|
||||||
|
go assignment.AssignCustomerMiler(booking.Bookingid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return utils.Message(c, "assignment rejected")
|
return utils.Message(c, "assignment rejected")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,17 +46,44 @@ func pincodeToState(pincode string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resolveZone determines the DoormilePricing zone from pickup and delivery pincodes.
|
// resolveZone determines the DoormilePricing zone from pickup and delivery pincodes.
|
||||||
// - Local — same 3-digit prefix (same city/sorting district)
|
// - Local — same 3-digit prefix (same city/sorting district)
|
||||||
// - Interstate — different city, same state
|
// - Regional — different city, same state
|
||||||
// - OtherState — different state
|
// - National — different state
|
||||||
func resolveZone(pickupPincode, deliveryPincode string) string {
|
func resolveZone(pickupPincode, deliveryPincode string) string {
|
||||||
if len(pickupPincode) >= 3 && len(deliveryPincode) >= 3 && pickupPincode[:3] == deliveryPincode[:3] {
|
if len(pickupPincode) >= 3 && len(deliveryPincode) >= 3 && pickupPincode[:3] == deliveryPincode[:3] {
|
||||||
return "Local"
|
return "Local"
|
||||||
}
|
}
|
||||||
if pincodeToState(pickupPincode) == pincodeToState(deliveryPincode) {
|
if pincodeToState(pickupPincode) == pincodeToState(deliveryPincode) {
|
||||||
return "Interstate"
|
return "Regional"
|
||||||
}
|
}
|
||||||
return "OtherState"
|
return "National"
|
||||||
|
}
|
||||||
|
|
||||||
|
// pincodeToLatLon returns approximate coordinates for a pincode using its
|
||||||
|
// 3-digit postal division. Returns ok=false for unrecognised pincodes.
|
||||||
|
func pincodeToLatLon(pincode string) (lat, lon float64, ok bool) {
|
||||||
|
if len(pincode) < 3 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p, err := strconv.Atoi(pincode[:3])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case p >= 500 && p <= 535:
|
||||||
|
return 17.385044, 78.486671, true // Hyderabad / Telangana
|
||||||
|
case p >= 560 && p <= 591:
|
||||||
|
return 12.971599, 77.594566, true // Bengaluru / Karnataka
|
||||||
|
case p >= 600 && p <= 643:
|
||||||
|
return 13.082680, 80.270718, true // Chennai / Tamil Nadu
|
||||||
|
case p >= 670 && p <= 695:
|
||||||
|
return 10.850516, 76.271080, true // Kerala
|
||||||
|
case p >= 380 && p <= 396:
|
||||||
|
return 23.022505, 72.571365, true // Ahmedabad / Gujarat
|
||||||
|
case p >= 400 && p <= 444:
|
||||||
|
return 19.075984, 72.877656, true // Mumbai / Maharashtra
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// normalizePricingCategory returns a DoormilePricing-compatible category string.
|
// normalizePricingCategory returns a DoormilePricing-compatible category string.
|
||||||
@@ -81,8 +108,8 @@ func mapServiceTypeToPricing(serviceType string) string {
|
|||||||
|
|
||||||
// lookupDoormilePrice fetches pricing for the given zone/serviceType/weight/category
|
// lookupDoormilePrice fetches pricing for the given zone/serviceType/weight/category
|
||||||
// from Redis first, falling back to Postgres on a cache miss.
|
// from Redis first, falling back to Postgres on a cache miss.
|
||||||
// Returns the midpoint of the matched price band and true, or 0 and false if no rule matches.
|
// Returns (midpoint price, pricingID, true) on match, or (0, nil, false) when no rule matches.
|
||||||
func lookupDoormilePrice(zone, pricingServiceType string, weight float64, category string) (float64, bool) {
|
func lookupDoormilePrice(zone, pricingServiceType string, weight float64, category string) (float64, *int, bool) {
|
||||||
var allRules []models.DoormilePricing
|
var allRules []models.DoormilePricing
|
||||||
|
|
||||||
if db.Rdb != nil {
|
if db.Rdb != nil {
|
||||||
@@ -102,7 +129,7 @@ func lookupDoormilePrice(zone, pricingServiceType string, weight float64, catego
|
|||||||
var err error
|
var err error
|
||||||
allRules, err = loadFromPostgres(zone, pricingServiceType)
|
allRules, err = loadFromPostgres(zone, pricingServiceType)
|
||||||
if err != nil || len(allRules) == 0 {
|
if err != nil || len(allRules) == 0 {
|
||||||
return 0, false
|
return 0, nil, false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,9 +138,10 @@ func lookupDoormilePrice(zone, pricingServiceType string, weight float64, catego
|
|||||||
matched = applyFilters(allRules, weight, "General")
|
matched = applyFilters(allRules, weight, "General")
|
||||||
}
|
}
|
||||||
if len(matched) == 0 {
|
if len(matched) == 0 {
|
||||||
return 0, false
|
return 0, nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
rule := matched[0]
|
rule := matched[0]
|
||||||
return (rule.Minprice + rule.Maxprice) / 2, true
|
id := rule.Doormile_pricing_id
|
||||||
|
return (rule.Minprice + rule.Maxprice) / 2, &id, true
|
||||||
}
|
}
|
||||||
|
|||||||
14
dto/auth.go
14
dto/auth.go
@@ -15,9 +15,10 @@ type CustomerLoginRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CustomerPinVerifyRequest struct {
|
type CustomerPinVerifyRequest struct {
|
||||||
Phone string `json:"phone" xml:"phone" form:"phone"`
|
Phone string `json:"phone" xml:"phone" form:"phone"`
|
||||||
Pin string `json:"pin" xml:"pin" form:"pin"`
|
Pin string `json:"pin" xml:"pin" form:"pin"`
|
||||||
Configid int `json:"configid"`
|
Configid int `json:"configid"`
|
||||||
|
DeviceToken string `json:"device_token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomerResetPinRequest struct {
|
type CustomerResetPinRequest struct {
|
||||||
@@ -32,9 +33,10 @@ type MilerLoginRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MilerPinVerifyRequest struct {
|
type MilerPinVerifyRequest struct {
|
||||||
Phone string `json:"phone" xml:"phone" form:"phone"`
|
Phone string `json:"phone" xml:"phone" form:"phone"`
|
||||||
Pin string `json:"pin" xml:"pin" form:"pin"`
|
Pin string `json:"pin" xml:"pin" form:"pin"`
|
||||||
Configid int `json:"configid"`
|
Configid int `json:"configid"`
|
||||||
|
DeviceToken string `json:"device_token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AdminLoginRequest struct {
|
type AdminLoginRequest struct {
|
||||||
|
|||||||
@@ -105,12 +105,12 @@ func tryCustomerAssign(bookingID int) (bool, error) {
|
|||||||
|
|
||||||
provider, providerFound := selectBestProvider(zone, category, serviceType, weight)
|
provider, providerFound := selectBestProvider(zone, category, serviceType, weight)
|
||||||
if !providerFound {
|
if !providerFound {
|
||||||
utils.Warn("B2CAssignment: no provider pricing match, proceeding without one",
|
utils.Warn("B2CAssignment: no provider pricing match, defaulting to Doormile",
|
||||||
"booking_id", bookingID,
|
"booking_id", bookingID,
|
||||||
"zone", zone,
|
"zone", zone,
|
||||||
"category", category,
|
"category", category,
|
||||||
)
|
)
|
||||||
provider = providerResult{}
|
provider = providerResult{company: "Doormile"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 6 — ETA based on miler-to-pickup distance.
|
// Step 6 — ETA based on miler-to-pickup distance.
|
||||||
@@ -359,9 +359,9 @@ func b2cResolveZone(pickupPincode, deliveryPincode string) string {
|
|||||||
return "Local"
|
return "Local"
|
||||||
}
|
}
|
||||||
if b2cPincodeToState(pickupPincode) == b2cPincodeToState(deliveryPincode) {
|
if b2cPincodeToState(pickupPincode) == b2cPincodeToState(deliveryPincode) {
|
||||||
return "Interstate"
|
return "Regional"
|
||||||
}
|
}
|
||||||
return "OtherState"
|
return "National"
|
||||||
}
|
}
|
||||||
|
|
||||||
func b2cNormalizePricingCategory(category string) string {
|
func b2cNormalizePricingCategory(category string) string {
|
||||||
|
|||||||
@@ -113,6 +113,17 @@ func TrackingHandler(c *websocket.Conn) {
|
|||||||
milerUserID := *booking.Assignedmileruserid
|
milerUserID := *booking.Assignedmileruserid
|
||||||
lat, lon, gpsOk := readMilerGPS(milerUserID)
|
lat, lon, gpsOk := readMilerGPS(milerUserID)
|
||||||
|
|
||||||
|
// Redis key expired or not yet set — fall back to last known DB coords.
|
||||||
|
if !gpsOk {
|
||||||
|
var mp models.MilerProfile
|
||||||
|
if db.DB.Where("userid = ?", milerUserID).First(&mp).Error == nil &&
|
||||||
|
(mp.Currentlatitude != 0 || mp.Currentlongitude != 0) {
|
||||||
|
lat = mp.Currentlatitude
|
||||||
|
lon = mp.Currentlongitude
|
||||||
|
gpsOk = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
frame := trackingFrame{
|
frame := trackingFrame{
|
||||||
Status: booking.Status,
|
Status: booking.Status,
|
||||||
MilerName: milerName(milerUserID),
|
MilerName: milerName(milerUserID),
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func CityGateMiddleware(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
"error": "We are not yet operating in your city. Stay tuned!",
|
"error": "We are not yet operating in your city. Stay tuned!",
|
||||||
"code": "CITY_NOT_SUPPORTED",
|
"code": "CITY_NOT_SUPPORTED",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package models
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
// Zone values: Local | Interstate | OtherState
|
// Zone values: Local | Regional | National
|
||||||
// Category values: General | Documents | Electronics | Clothing | Fragile | Medical | Automotive | Food
|
// Category values: General | Documents | Electronics | Clothing | Fragile | Medical | Automotive | Food
|
||||||
// ServiceType: Normal | Express
|
// ServiceType: Normal | Express
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user