Add assignment engine, FCM, WebSockets, city gate, and internal APIs
- internal/assignment: GEORADIUS miler assignment with retry/escalation, customer-side provider scoring, FCM notifications on assign - internal/notify: Firebase Admin SDK (FCM) client initialisation - internal/ws: WebSocket handlers for live parcel tracking and customer↔miler chat - middlewares: city gate (pincode prefix validation), internal API key auth, WebSocket JWT auth - controllers: InternalNotify + InternalReassign for machine-to-machine calls; pricing helpers wired into CreateCustomerBooking and CreateCRMBooking - routes: /internal/*, /ws/bookings/:id/track, /ws/bookings/:id/chat - models/users, models/doormile_pricing: new fields for device tokens, assignment state, pricing bands - seed_data.sql: initial pricing seed rows .env and Firebase service-account JSON intentionally excluded. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,8 @@ import (
|
||||
"doormile/constants"
|
||||
"doormile/db"
|
||||
"doormile/dto"
|
||||
"doormile/internal/assignment"
|
||||
"doormile/internal/notify"
|
||||
"doormile/models"
|
||||
"doormile/utils"
|
||||
|
||||
@@ -1096,6 +1098,8 @@ func CreateCRMBooking(c *fiber.Ctx) error {
|
||||
|
||||
tx.Commit()
|
||||
|
||||
go assignment.AssignCRMMiler(booking.Bookingid)
|
||||
|
||||
if db.Js != nil {
|
||||
payload := map[string]interface{}{
|
||||
"booking_id": booking.Bookingid,
|
||||
@@ -1946,3 +1950,143 @@ func GetAdminProfile(c *fiber.Ctx) error {
|
||||
|
||||
return utils.OK(c, user)
|
||||
}
|
||||
|
||||
// InternalNotify sends FCM push notifications on behalf of the Python agent system.
|
||||
// The caller specifies target = "customer", "miler", or "both".
|
||||
// Auth: X-Internal-Key header (see InternalKeyAuth middleware).
|
||||
//
|
||||
// POST /api/v1/internal/notify
|
||||
func InternalNotify(c *fiber.Ctx) error {
|
||||
type req struct {
|
||||
BookingID int `json:"booking_id"`
|
||||
Target string `json:"target"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Data map[string]string `json:"data"`
|
||||
}
|
||||
|
||||
body := new(req)
|
||||
if err := c.BodyParser(body); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
if body.BookingID == 0 {
|
||||
return utils.BadRequest(c, "booking_id is required")
|
||||
}
|
||||
if body.Target != "customer" && body.Target != "miler" && body.Target != "both" {
|
||||
return utils.BadRequest(c, "target must be customer, miler, or both")
|
||||
}
|
||||
if body.Title == "" || body.Message == "" {
|
||||
return utils.BadRequest(c, "title and message are required")
|
||||
}
|
||||
|
||||
var booking models.PickupBooking
|
||||
if err := db.DB.First(&booking, body.BookingID).Error; err != nil {
|
||||
return utils.NotFound(c, "booking not found")
|
||||
}
|
||||
|
||||
data := body.Data
|
||||
if data == nil {
|
||||
data = map[string]string{}
|
||||
}
|
||||
data["booking_id"] = strconv.Itoa(booking.Bookingid)
|
||||
|
||||
sent := make([]string, 0, 2)
|
||||
|
||||
if body.Target == "customer" || body.Target == "both" {
|
||||
var customer models.AppCustomer
|
||||
if err := db.DB.Where("appcustomerid = ?", booking.Appcustomerid).First(&customer).Error; err == nil && customer.Devicetoken != "" {
|
||||
if err := notify.SendToDevice(customer.Devicetoken, body.Title, body.Message, data); err != nil {
|
||||
utils.Warn("InternalNotify: failed to notify customer", "booking_id", booking.Bookingid, "error", err)
|
||||
} else {
|
||||
sent = append(sent, "customer")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if body.Target == "miler" || body.Target == "both" {
|
||||
if booking.Assignedmileruserid != nil {
|
||||
var profile models.MilerProfile
|
||||
if err := db.DB.Where("userid = ?", *booking.Assignedmileruserid).First(&profile).Error; err == nil && profile.Devicetoken != "" {
|
||||
if err := notify.SendToDevice(profile.Devicetoken, body.Title, body.Message, data); err != nil {
|
||||
utils.Warn("InternalNotify: failed to notify miler", "booking_id", booking.Bookingid, "error", err)
|
||||
} else {
|
||||
sent = append(sent, "miler")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return utils.OK(c, fiber.Map{
|
||||
"sent": true,
|
||||
"targets": sent,
|
||||
})
|
||||
}
|
||||
|
||||
// InternalReassign releases the current miler assignment and re-triggers the
|
||||
// auto-assignment engine for a stalled booking. Only valid when the booking is
|
||||
// in Miler_Assigned or Pickup_Scheduled state.
|
||||
// Auth: X-Internal-Key header (see InternalKeyAuth middleware).
|
||||
//
|
||||
// POST /api/v1/internal/bookings/:id/reassign
|
||||
func InternalReassign(c *fiber.Ctx) error {
|
||||
id, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid booking ID")
|
||||
}
|
||||
|
||||
tx := db.DB.Begin()
|
||||
|
||||
var booking models.PickupBooking
|
||||
if err := tx.First(&booking, id).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.NotFound(c, "booking not found")
|
||||
}
|
||||
|
||||
if booking.Status != constants.BookingMilerAssigned && booking.Status != constants.BookingPickupScheduled {
|
||||
tx.Rollback()
|
||||
return utils.BadRequest(c, "booking must be in Miler_Assigned or Pickup_Scheduled state to reassign")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
if booking.Assignedmileruserid != nil {
|
||||
tx.Model(&models.MilerProfile{}).
|
||||
Where("userid = ?", *booking.Assignedmileruserid).
|
||||
Updates(map[string]interface{}{
|
||||
"availabilitystatus": constants.MilerAvailable,
|
||||
"updatedat": now,
|
||||
})
|
||||
|
||||
tx.Delete(&models.BookingAssignment{},
|
||||
"bookingid = ? AND assignmentstatus IN ?",
|
||||
booking.Bookingid,
|
||||
[]string{constants.AssignmentAssigned, constants.AssignmentAccepted},
|
||||
)
|
||||
}
|
||||
|
||||
booking.Status = constants.BookingCreated
|
||||
booking.Assignedmileruserid = nil
|
||||
booking.Updatedat = now
|
||||
if err := tx.Save(&booking).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to reset booking")
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
if booking.Bookingsource == "CRM_Console" {
|
||||
go assignment.AssignCRMMiler(booking.Bookingid)
|
||||
} else {
|
||||
go assignment.AssignCustomerMiler(booking.Bookingid)
|
||||
}
|
||||
|
||||
utils.Info("InternalReassign: reassignment triggered",
|
||||
"booking_id", booking.Bookingid,
|
||||
"booking_source", booking.Bookingsource,
|
||||
)
|
||||
|
||||
return utils.OK(c, fiber.Map{
|
||||
"reassignment": "triggered",
|
||||
"booking_id": booking.Bookingid,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"doormile/constants"
|
||||
"doormile/db"
|
||||
"doormile/dto"
|
||||
"doormile/internal/assignment"
|
||||
"doormile/models"
|
||||
"doormile/utils"
|
||||
|
||||
@@ -454,34 +455,23 @@ func CreateCustomerBooking(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
var distance float64
|
||||
if booking.Deliverylatitude != 0 && booking.Deliverylongitude != 0 {
|
||||
distance = calculateDistance(booking.Pickuplatitude, booking.Pickuplongitude, booking.Deliverylatitude, booking.Deliverylongitude)
|
||||
}
|
||||
|
||||
var pricing models.Pricing
|
||||
pricingErr := tx.Where("status = ? AND ? BETWEEN effectivefrom AND effectiveto", "Active", time.Now()).Order("priority DESC").First(&pricing).Error
|
||||
|
||||
var estimatedPrice float64
|
||||
var pricingID *int
|
||||
if pricingErr == nil {
|
||||
pricingID = &pricing.Pricingid
|
||||
kmExtra := math.Max(0, distance-pricing.Basedistance)
|
||||
kgExtra := math.Max(0, totalWeight-pricing.Baseweight)
|
||||
estimatedPrice = pricing.Baseprice + (kmExtra * pricing.Priceperkm) + (kgExtra * pricing.Priceperkg) + pricing.Handlingcharges
|
||||
} else {
|
||||
estimatedPrice = 50.0 + (distance * 5.0) + (totalWeight * 10.0)
|
||||
}
|
||||
|
||||
serviceType := req.ServiceOption
|
||||
if serviceType == "" {
|
||||
serviceType = "Normal"
|
||||
}
|
||||
|
||||
if serviceType == "Fast" {
|
||||
estimatedPrice *= 1.25
|
||||
} else if serviceType == "Superfast" {
|
||||
estimatedPrice *= 1.5
|
||||
zone := resolveZone(req.Pickuppincode, req.Deliverypincode)
|
||||
itemCategory := normalizePricingCategory(req.Parcels[0].Itemcategory)
|
||||
|
||||
var estimatedPrice float64
|
||||
if price, found := lookupDoormilePrice(zone, mapServiceTypeToPricing(serviceType), totalWeight, itemCategory); found {
|
||||
estimatedPrice = price
|
||||
} 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()
|
||||
@@ -501,7 +491,6 @@ func CreateCustomerBooking(c *fiber.Ctx) error {
|
||||
Estimatedprice: estimatedPrice,
|
||||
Estimateddeliveryat: &estDelivery,
|
||||
Sladueat: &slaDue,
|
||||
Pricingid: pricingID,
|
||||
}
|
||||
|
||||
if err := tx.Create(&srvOption).Error; err != nil {
|
||||
@@ -521,6 +510,8 @@ func CreateCustomerBooking(c *fiber.Ctx) error {
|
||||
|
||||
tx.Commit()
|
||||
|
||||
go assignment.AssignCustomerMiler(booking.Bookingid)
|
||||
|
||||
if db.Js != nil {
|
||||
payload := map[string]interface{}{
|
||||
"booking_id": booking.Bookingid,
|
||||
@@ -642,3 +633,25 @@ func TrackConsignment(c *fiber.Ctx) error {
|
||||
"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")
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"doormile/constants"
|
||||
"doormile/db"
|
||||
"doormile/dto"
|
||||
"doormile/internal/notify"
|
||||
"doormile/models"
|
||||
"doormile/utils"
|
||||
|
||||
@@ -336,6 +337,21 @@ func AcceptMilerAssignment(c *fiber.Ctx) error {
|
||||
tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).Update("availabilitystatus", constants.MilerAssigned)
|
||||
|
||||
tx.Commit()
|
||||
|
||||
if booking.Bookingid != 0 {
|
||||
var customer models.AppCustomer
|
||||
if err := db.DB.Where("appcustomerid = ?", booking.Appcustomerid).First(&customer).Error; err == nil && customer.Devicetoken != "" {
|
||||
if notifyErr := notify.SendToDevice(
|
||||
customer.Devicetoken,
|
||||
"Miler Accepted",
|
||||
"Your miler has accepted and is coming",
|
||||
map[string]string{"booking_id": strconv.Itoa(booking.Bookingid)},
|
||||
); notifyErr != nil {
|
||||
utils.Warn("FCM: failed to notify customer on accept", "booking_id", booking.Bookingid, "error", notifyErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return utils.Message(c, "assignment accepted successfully")
|
||||
}
|
||||
|
||||
@@ -406,30 +422,56 @@ func BookingParcelConfirm(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
type ParcelUpdate struct {
|
||||
Weight float64 `json:"weight"`
|
||||
Length float64 `json:"length"`
|
||||
Width float64 `json:"width"`
|
||||
Height float64 `json:"height"`
|
||||
ParcelID int `json:"parcel_id"`
|
||||
Weight float64 `json:"weight"`
|
||||
Length float64 `json:"length"`
|
||||
Width float64 `json:"width"`
|
||||
Height float64 `json:"height"`
|
||||
}
|
||||
|
||||
req := new(ParcelUpdate)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
var req struct {
|
||||
Parcels []ParcelUpdate `json:"parcels"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
if len(req.Parcels) == 0 {
|
||||
return utils.BadRequest(c, "parcels array is required")
|
||||
}
|
||||
|
||||
var parcel models.BookingParcel
|
||||
if err := db.DB.Where("bookingid = ?", bookingID).First(&parcel).Error; err != nil {
|
||||
var parcels []models.BookingParcel
|
||||
if err := db.DB.Where("bookingid = ?", bookingID).Find(&parcels).Error; err != nil {
|
||||
return utils.NotFound(c, "parcel details not found")
|
||||
}
|
||||
|
||||
parcel.Weight = req.Weight
|
||||
parcel.Length = req.Length
|
||||
parcel.Width = req.Width
|
||||
parcel.Height = req.Height
|
||||
parcel.Updatedat = time.Now()
|
||||
db.DB.Save(&parcel)
|
||||
// Index loaded parcels by ID for O(1) lookup.
|
||||
parcelMap := make(map[int]*models.BookingParcel, len(parcels))
|
||||
for i := range parcels {
|
||||
parcelMap[parcels[i].Bookingparcelid] = &parcels[i]
|
||||
}
|
||||
|
||||
return utils.OK(c, parcel)
|
||||
now := time.Now()
|
||||
var totalChargeable float64
|
||||
|
||||
for _, upd := range req.Parcels {
|
||||
p, ok := parcelMap[upd.ParcelID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
p.Weight = upd.Weight
|
||||
p.Length = upd.Length
|
||||
p.Width = upd.Width
|
||||
p.Height = upd.Height
|
||||
p.Updatedat = now
|
||||
db.DB.Save(p)
|
||||
|
||||
volumetric := calculateVolumetricWeight(upd.Length, upd.Width, upd.Height)
|
||||
totalChargeable += math.Max(upd.Weight, volumetric)
|
||||
}
|
||||
|
||||
return utils.OK(c, fiber.Map{
|
||||
"parcels": parcels,
|
||||
"total_chargeable_weight": totalChargeable,
|
||||
})
|
||||
}
|
||||
|
||||
func BookingPaymentCollect(c *fiber.Ctx) error {
|
||||
@@ -499,11 +541,28 @@ func BookingPickupComplete(c *fiber.Ctx) error {
|
||||
tx.Save(&profile)
|
||||
}
|
||||
|
||||
var parcel models.BookingParcel
|
||||
tx.Where("bookingid = ?", bookingID).First(&parcel)
|
||||
var parcels []models.BookingParcel
|
||||
tx.Where("bookingid = ?", bookingID).Find(&parcels)
|
||||
|
||||
volumetric := calculateVolumetricWeight(parcel.Length, parcel.Width, parcel.Height)
|
||||
chargeable := math.Max(parcel.Weight, volumetric)
|
||||
var totalDead, totalChargeable, maxL, maxW, maxH float64
|
||||
for _, p := range parcels {
|
||||
vol := calculateVolumetricWeight(p.Length, p.Width, p.Height)
|
||||
totalDead += p.Weight
|
||||
totalChargeable += math.Max(p.Weight, vol)
|
||||
if p.Length > maxL {
|
||||
maxL = p.Length
|
||||
}
|
||||
if p.Width > maxW {
|
||||
maxW = p.Width
|
||||
}
|
||||
if p.Height > maxH {
|
||||
maxH = p.Height
|
||||
}
|
||||
}
|
||||
if len(parcels) == 0 {
|
||||
totalDead = 0.5
|
||||
totalChargeable = 0.5
|
||||
}
|
||||
|
||||
trackingNo := generateTrackingNo()
|
||||
|
||||
@@ -522,12 +581,12 @@ func BookingPickupComplete(c *fiber.Ctx) error {
|
||||
Deliverylongitude: booking.Deliverylongitude,
|
||||
Pickuppincode: booking.Pickuppincode,
|
||||
Deliverypincode: booking.Deliverypincode,
|
||||
Length: parcel.Length,
|
||||
Width: parcel.Width,
|
||||
Height: parcel.Height,
|
||||
Deadweight: parcel.Weight,
|
||||
Volumetricweight: volumetric,
|
||||
Chargeableweight: chargeable,
|
||||
Length: maxL,
|
||||
Width: maxW,
|
||||
Height: maxH,
|
||||
Deadweight: totalDead,
|
||||
Volumetricweight: totalChargeable - totalDead,
|
||||
Chargeableweight: totalChargeable,
|
||||
Paymentmode: "Prepaid",
|
||||
Status: constants.ConsignmentInwardedAtHub,
|
||||
Estimateddeliveryat: nil,
|
||||
@@ -568,6 +627,21 @@ func BookingPickupComplete(c *fiber.Ctx) error {
|
||||
|
||||
tx.Commit()
|
||||
|
||||
var customer models.AppCustomer
|
||||
if err := db.DB.Where("appcustomerid = ?", booking.Appcustomerid).First(&customer).Error; err == nil && customer.Devicetoken != "" {
|
||||
if notifyErr := notify.SendToDevice(
|
||||
customer.Devicetoken,
|
||||
"Parcel Picked Up",
|
||||
fmt.Sprintf("Parcel picked up — Tracking No: %s", trackingNo),
|
||||
map[string]string{
|
||||
"booking_id": strconv.Itoa(bookingID),
|
||||
"tracking_no": trackingNo,
|
||||
},
|
||||
); notifyErr != nil {
|
||||
utils.Warn("FCM: failed to notify customer on pickup", "booking_id", bookingID, "error", notifyErr)
|
||||
}
|
||||
}
|
||||
|
||||
return utils.OK(c, fiber.Map{
|
||||
"tracking_no": trackingNo,
|
||||
"consignment_id": consignment.Consignmentid,
|
||||
@@ -881,3 +955,25 @@ func GetUserConsignmentLogs(c *fiber.Ctx) error {
|
||||
|
||||
return utils.List(c, logs, int64(len(logs)))
|
||||
}
|
||||
|
||||
func SaveMilerDeviceToken(c *fiber.Ctx) error {
|
||||
milerUserID := 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.MilerProfile{}).
|
||||
Where("userid = ?", milerUserID).
|
||||
Update("device_token", req.DeviceToken).Error; err != nil {
|
||||
return utils.Internal(c, "failed to save device token")
|
||||
}
|
||||
|
||||
return utils.Message(c, "device token saved")
|
||||
}
|
||||
|
||||
119
controllers/pricing_helpers.go
Normal file
119
controllers/pricing_helpers.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"doormile/db"
|
||||
"doormile/models"
|
||||
"doormile/utils"
|
||||
)
|
||||
|
||||
// pincodeToState maps an Indian pincode to a state key using its first 3 digits.
|
||||
// Covers the states Doormile operates in; other pincodes fall back to their 2-digit prefix.
|
||||
func pincodeToState(pincode string) string {
|
||||
if len(pincode) < 3 {
|
||||
return pincode
|
||||
}
|
||||
p, err := strconv.Atoi(pincode[:3])
|
||||
if err != nil {
|
||||
if len(pincode) >= 2 {
|
||||
return pincode[:2]
|
||||
}
|
||||
return pincode
|
||||
}
|
||||
switch {
|
||||
case p >= 500 && p <= 535:
|
||||
return "AP_TS" // Andhra Pradesh / Telangana
|
||||
case p >= 560 && p <= 591:
|
||||
return "KA" // Karnataka
|
||||
case p >= 600 && p <= 643:
|
||||
return "TN" // Tamil Nadu
|
||||
case p >= 670 && p <= 695:
|
||||
return "KL" // Kerala
|
||||
case p >= 380 && p <= 396:
|
||||
return "GJ" // Gujarat
|
||||
case p >= 400 && p <= 444:
|
||||
return "MH" // Maharashtra
|
||||
default:
|
||||
if len(pincode) >= 2 {
|
||||
return pincode[:2]
|
||||
}
|
||||
return pincode
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
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 "OtherState"
|
||||
}
|
||||
|
||||
// normalizePricingCategory returns a DoormilePricing-compatible category string.
|
||||
// Falls back to "General" for unknown or empty values.
|
||||
func normalizePricingCategory(category string) string {
|
||||
switch category {
|
||||
case "General", "Documents", "Electronics", "Clothing", "Fragile", "Medical", "Automotive", "Food":
|
||||
return category
|
||||
default:
|
||||
return "General"
|
||||
}
|
||||
}
|
||||
|
||||
// mapServiceTypeToPricing maps the booking service type to a DoormilePricing service type.
|
||||
// DoormilePricing has only Normal and Express.
|
||||
func mapServiceTypeToPricing(serviceType string) string {
|
||||
if serviceType == "Fast" || serviceType == "Superfast" {
|
||||
return "Express"
|
||||
}
|
||||
return "Normal"
|
||||
}
|
||||
|
||||
// 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) {
|
||||
var allRules []models.DoormilePricing
|
||||
|
||||
if db.Rdb != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
cached, err := db.Rdb.Get(ctx, pricingCacheKey(zone, pricingServiceType)).Result()
|
||||
cancel()
|
||||
if err == nil {
|
||||
if jerr := json.Unmarshal([]byte(cached), &allRules); jerr != nil {
|
||||
utils.Warn("Corrupt pricing cache in booking, evicting", "zone", zone, "servicetype", pricingServiceType)
|
||||
invalidatePricingCache(zone, pricingServiceType)
|
||||
allRules = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if allRules == nil {
|
||||
var err error
|
||||
allRules, err = loadFromPostgres(zone, pricingServiceType)
|
||||
if err != nil || len(allRules) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
matched := applyFilters(allRules, weight, category)
|
||||
if len(matched) == 0 && category != "General" {
|
||||
matched = applyFilters(allRules, weight, "General")
|
||||
}
|
||||
if len(matched) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
rule := matched[0]
|
||||
return (rule.Minprice + rule.Maxprice) / 2, true
|
||||
}
|
||||
Reference in New Issue
Block a user