diff --git a/controllers/customerController.go b/controllers/customerController.go index 2292970..469ea53 100644 --- a/controllers/customerController.go +++ b/controllers/customerController.go @@ -151,6 +151,9 @@ func VerifyCustomerPin(cfg *config.Config) fiber.Handler { 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) @@ -394,6 +397,14 @@ func CreateCustomerBooking(c *fiber.Ctx) error { 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{ @@ -464,8 +475,10 @@ func CreateCustomerBooking(c *fiber.Ctx) error { itemCategory := normalizePricingCategory(req.Parcels[0].Itemcategory) 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 + pricingID = pid } else { var distance float64 if booking.Deliverylatitude != 0 && booking.Deliverylongitude != 0 { @@ -489,6 +502,7 @@ func CreateCustomerBooking(c *fiber.Ctx) error { Bookingid: booking.Bookingid, Servicetype: serviceType, Estimatedprice: estimatedPrice, + Pricingid: pricingID, Estimateddeliveryat: &estDelivery, Sladueat: &slaDue, } diff --git a/controllers/doormilePricingController.go b/controllers/doormilePricingController.go index b374d45..2773263 100644 --- a/controllers/doormilePricingController.go +++ b/controllers/doormilePricingController.go @@ -16,9 +16,9 @@ import ( // Valid enum values var validZones = map[string]bool{ - "Local": true, - "Interstate": true, - "OtherState": true, + "Local": true, + "Regional": true, + "National": true, } var validCategories = map[string]bool{ @@ -158,10 +158,12 @@ func buildPriceResponse(zone, servicetype string, weight float64, rules []models // Body: { zone, service_type, weight, category? } func CheckPrice(c *fiber.Ctx) error { type req struct { - Zone string `json:"zone"` - ServiceType string `json:"service_type"` - Weight float64 `json:"weight"` - Category string `json:"category"` + Zone string `json:"zone"` + ServiceType string `json:"service_type"` + Weight float64 `json:"weight"` + Category string `json:"category"` + PickupPincode string `json:"pickup_pincode"` + DeliveryPincode string `json:"delivery_pincode"` } body := new(req) @@ -169,11 +171,16 @@ func CheckPrice(c *fiber.Ctx) error { 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] { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "success": false, "message": "invalid zone", - "valid_zones": []string{"Local", "Interstate", "OtherState"}, + "valid_zones": []string{"Local", "Regional", "National"}, }) } if !validServiceTypes[body.ServiceType] { @@ -425,7 +432,7 @@ func WarmPricingCache() { utils.Info("WarmPricingCache: warming pricing cache from Postgres...") // Iterate every zone × servicetype combination - zones := []string{"Local", "Interstate", "OtherState"} + zones := []string{"Local", "Regional", "National"} serviceTypes := []string{"Normal", "Express"} warmed := 0 @@ -451,8 +458,8 @@ func GetPricingMeta(c *fiber.Ctx) error { return utils.OK(c, fiber.Map{ "zones": []fiber.Map{ {"value": "Local", "label": "Local / Same City"}, - {"value": "Interstate", "label": "Interstate"}, - {"value": "OtherState", "label": "Other State"}, + {"value": "Regional", "label": "Regional / Same State"}, + {"value": "National", "label": "National / Other State"}, }, "categories": []fiber.Map{ {"value": "General", "label": "General Goods"}, diff --git a/controllers/milerController.go b/controllers/milerController.go index c1a2fc8..e3358c0 100644 --- a/controllers/milerController.go +++ b/controllers/milerController.go @@ -13,6 +13,7 @@ import ( "doormile/constants" "doormile/db" "doormile/dto" + "doormile/internal/assignment" "doormile/internal/notify" "doormile/models" "doormile/utils" @@ -114,6 +115,10 @@ func VerifyMilerPin(cfg *config.Config) fiber.Handler { } 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{ "success": true, @@ -364,18 +369,18 @@ func RejectMilerAssignment(c *fiber.Ctx) error { tx := db.DB.Begin() - var assignment models.BookingAssignment - if err := tx.Where("bookingassignmentid = ? AND mileruserid = ?", assignmentID, milerUserID).First(&assignment).Error; err != nil { + var ba models.BookingAssignment + if err := tx.Where("bookingassignmentid = ? AND mileruserid = ?", assignmentID, milerUserID).First(&ba).Error; err != nil { tx.Rollback() return utils.NotFound(c, "assignment not found") } - assignment.Assignmentstatus = constants.AssignmentRejected - assignment.Remarks = c.Query("reason", "Rejected by rider") - tx.Save(&assignment) + ba.Assignmentstatus = constants.AssignmentRejected + ba.Remarks = c.Query("reason", "Rejected by rider") + tx.Save(&ba) 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.Assignedmileruserid = nil 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.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") } diff --git a/controllers/pricing_helpers.go b/controllers/pricing_helpers.go index 6dd0293..7af59ea 100644 --- a/controllers/pricing_helpers.go +++ b/controllers/pricing_helpers.go @@ -46,17 +46,44 @@ func pincodeToState(pincode string) string { } // resolveZone determines the DoormilePricing zone from pickup and delivery pincodes. -// - Local — same 3-digit prefix (same city/sorting district) -// - Interstate — different city, same state -// - OtherState — different state +// - Local — same 3-digit prefix (same city/sorting district) +// - Regional — different city, same state +// - National — different state func resolveZone(pickupPincode, deliveryPincode string) string { if len(pickupPincode) >= 3 && len(deliveryPincode) >= 3 && pickupPincode[:3] == deliveryPincode[:3] { return "Local" } 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. @@ -81,8 +108,8 @@ func mapServiceTypeToPricing(serviceType string) string { // lookupDoormilePrice fetches pricing for the given zone/serviceType/weight/category // 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. -func lookupDoormilePrice(zone, pricingServiceType string, weight float64, category string) (float64, bool) { +// 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, *int, bool) { var allRules []models.DoormilePricing if db.Rdb != nil { @@ -102,7 +129,7 @@ func lookupDoormilePrice(zone, pricingServiceType string, weight float64, catego var err error allRules, err = loadFromPostgres(zone, pricingServiceType) 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") } if len(matched) == 0 { - return 0, false + return 0, nil, false } rule := matched[0] - return (rule.Minprice + rule.Maxprice) / 2, true + id := rule.Doormile_pricing_id + return (rule.Minprice + rule.Maxprice) / 2, &id, true } diff --git a/dto/auth.go b/dto/auth.go index d8b3a11..6e33ea3 100644 --- a/dto/auth.go +++ b/dto/auth.go @@ -15,9 +15,10 @@ type CustomerLoginRequest struct { } type CustomerPinVerifyRequest struct { - Phone string `json:"phone" xml:"phone" form:"phone"` - Pin string `json:"pin" xml:"pin" form:"pin"` - Configid int `json:"configid"` + Phone string `json:"phone" xml:"phone" form:"phone"` + Pin string `json:"pin" xml:"pin" form:"pin"` + Configid int `json:"configid"` + DeviceToken string `json:"device_token"` } type CustomerResetPinRequest struct { @@ -32,9 +33,10 @@ type MilerLoginRequest struct { } type MilerPinVerifyRequest struct { - Phone string `json:"phone" xml:"phone" form:"phone"` - Pin string `json:"pin" xml:"pin" form:"pin"` - Configid int `json:"configid"` + Phone string `json:"phone" xml:"phone" form:"phone"` + Pin string `json:"pin" xml:"pin" form:"pin"` + Configid int `json:"configid"` + DeviceToken string `json:"device_token"` } type AdminLoginRequest struct { diff --git a/internal/assignment/customer_assignment.go b/internal/assignment/customer_assignment.go index b1e2713..cb84865 100644 --- a/internal/assignment/customer_assignment.go +++ b/internal/assignment/customer_assignment.go @@ -105,12 +105,12 @@ func tryCustomerAssign(bookingID int) (bool, error) { provider, providerFound := selectBestProvider(zone, category, serviceType, weight) 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, "zone", zone, "category", category, ) - provider = providerResult{} + provider = providerResult{company: "Doormile"} } // Step 6 — ETA based on miler-to-pickup distance. @@ -359,9 +359,9 @@ func b2cResolveZone(pickupPincode, deliveryPincode string) string { return "Local" } if b2cPincodeToState(pickupPincode) == b2cPincodeToState(deliveryPincode) { - return "Interstate" + return "Regional" } - return "OtherState" + return "National" } func b2cNormalizePricingCategory(category string) string { diff --git a/internal/ws/tracking.go b/internal/ws/tracking.go index a6f7764..273dd74 100644 --- a/internal/ws/tracking.go +++ b/internal/ws/tracking.go @@ -113,6 +113,17 @@ func TrackingHandler(c *websocket.Conn) { milerUserID := *booking.Assignedmileruserid 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{ Status: booking.Status, MilerName: milerName(milerUserID), diff --git a/middlewares/city_gate.go b/middlewares/city_gate.go index ce52b5f..3961f8d 100644 --- a/middlewares/city_gate.go +++ b/middlewares/city_gate.go @@ -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!", "code": "CITY_NOT_SUPPORTED", }) diff --git a/models/doormile_pricing.go b/models/doormile_pricing.go index 23e7530..00b940e 100644 --- a/models/doormile_pricing.go +++ b/models/doormile_pricing.go @@ -2,7 +2,7 @@ package models import "time" -// Zone values: Local | Interstate | OtherState +// Zone values: Local | Regional | National // Category values: General | Documents | Electronics | Clothing | Fragile | Medical | Automotive | Food // ServiceType: Normal | Express