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:
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