Compare commits
4 Commits
c577d47b75
...
bd6427f5db
| Author | SHA1 | Date | |
|---|---|---|---|
| bd6427f5db | |||
| 7c55b523af | |||
| 9d409a0d85 | |||
| c91c887726 |
1
.env
1
.env
@@ -2,6 +2,7 @@
|
||||
APP_PORT=8081
|
||||
ENV=development
|
||||
JWT_SECRET_KEY=DoormileSuperSecretJWTKey2026!
|
||||
INTERNAL_API_KEY=doormile-internal-2024
|
||||
|
||||
# PostgreSQL Database Configuration
|
||||
DB_HOST=31.97.228.132
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
APP_PORT=8081
|
||||
ENV=development
|
||||
JWT_SECRET_KEY=DoormileSuperSecretJWTKey2026!
|
||||
INTERNAL_API_KEY=doormile-internal-2024
|
||||
|
||||
# PostgreSQL Database Configuration
|
||||
DB_HOST=31.97.228.132
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# First Stage: Build the binary using golang base image
|
||||
FROM golang:1.24
|
||||
FROM golang:1.25
|
||||
|
||||
RUN mkdir /app
|
||||
ADD . /app/
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
156
controllers/agentDecisionController.go
Normal file
156
controllers/agentDecisionController.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"doormile/db"
|
||||
"doormile/models"
|
||||
"doormile/utils"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// POST /api/v1/internal/agent-decisions
|
||||
func CreateAgentDecision(c *fiber.Ctx) error {
|
||||
type req struct {
|
||||
DecisionType string `json:"decision_type"`
|
||||
BookingID *uint64 `json:"booking_id"`
|
||||
Context map[string]interface{} `json:"context"`
|
||||
Decision map[string]interface{} `json:"decision"`
|
||||
Reasoning string `json:"reasoning"`
|
||||
ContextEmbedding []float32 `json:"context_embedding"`
|
||||
}
|
||||
|
||||
body := new(req)
|
||||
if err := c.BodyParser(body); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
if body.DecisionType == "" {
|
||||
return utils.BadRequest(c, "decision_type is required")
|
||||
}
|
||||
|
||||
contextJSON, err := json.Marshal(body.Context)
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid context JSON")
|
||||
}
|
||||
decisionJSON, err := json.Marshal(body.Decision)
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid decision JSON")
|
||||
}
|
||||
|
||||
record := models.AgentDecision{
|
||||
DecisionType: body.DecisionType,
|
||||
BookingID: body.BookingID,
|
||||
Context: string(contextJSON),
|
||||
Decision: string(decisionJSON),
|
||||
Reasoning: body.Reasoning,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := db.DB.Create(&record).Error; err != nil {
|
||||
utils.Error("CreateAgentDecision: insert failed", "error", err)
|
||||
return utils.Internal(c, "failed to create agent decision")
|
||||
}
|
||||
|
||||
if len(body.ContextEmbedding) > 0 {
|
||||
parts := make([]string, len(body.ContextEmbedding))
|
||||
for i, v := range body.ContextEmbedding {
|
||||
parts[i] = strconv.FormatFloat(float64(v), 'f', -1, 32)
|
||||
}
|
||||
embeddingStr := "[" + strings.Join(parts, ",") + "]"
|
||||
if err := db.DB.Exec(
|
||||
"UPDATE agent_decisions SET context_embedding = ? WHERE id = ?",
|
||||
embeddingStr, record.ID,
|
||||
).Error; err != nil {
|
||||
utils.Warn("CreateAgentDecision: embedding update failed", "id", record.ID, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
return utils.Created(c, fiber.Map{"id": record.ID})
|
||||
}
|
||||
|
||||
// GET /api/v1/internal/agent-decisions/similar
|
||||
func FindSimilarDecisions(c *fiber.Ctx) error {
|
||||
decisionType := c.Query("decision_type")
|
||||
limit, err := strconv.Atoi(c.Query("limit", "5"))
|
||||
if err != nil || limit < 1 {
|
||||
limit = 5
|
||||
}
|
||||
|
||||
type req struct {
|
||||
Embedding []float32 `json:"embedding"`
|
||||
}
|
||||
body := new(req)
|
||||
if err := c.BodyParser(body); err != nil || len(body.Embedding) == 0 {
|
||||
return utils.BadRequest(c, "embedding array is required")
|
||||
}
|
||||
|
||||
parts := make([]string, len(body.Embedding))
|
||||
for i, v := range body.Embedding {
|
||||
parts[i] = strconv.FormatFloat(float64(v), 'f', -1, 32)
|
||||
}
|
||||
embeddingStr := "[" + strings.Join(parts, ",") + "]"
|
||||
|
||||
type row struct {
|
||||
Decision string `json:"decision"`
|
||||
Reasoning string `json:"reasoning"`
|
||||
Outcome *string `json:"outcome"`
|
||||
Distance float64 `json:"distance"`
|
||||
}
|
||||
var results []row
|
||||
|
||||
if err := db.DB.Raw(
|
||||
`SELECT decision, reasoning, outcome,
|
||||
context_embedding <=> ? AS distance
|
||||
FROM agent_decisions
|
||||
WHERE decision_type = ? AND outcome IS NOT NULL
|
||||
ORDER BY context_embedding <=> ?
|
||||
LIMIT ?`,
|
||||
embeddingStr, decisionType, embeddingStr, limit,
|
||||
).Scan(&results).Error; err != nil {
|
||||
utils.Error("FindSimilarDecisions: query failed", "error", err)
|
||||
return utils.Internal(c, "failed to query similar decisions")
|
||||
}
|
||||
|
||||
return utils.List(c, results, int64(len(results)))
|
||||
}
|
||||
|
||||
// PATCH /api/v1/internal/agent-decisions/:id/outcome
|
||||
func UpdateDecisionOutcome(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid decision ID")
|
||||
}
|
||||
|
||||
type req struct {
|
||||
Outcome string `json:"outcome"`
|
||||
}
|
||||
body := new(req)
|
||||
if err := c.BodyParser(body); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
if body.Outcome == "" {
|
||||
return utils.BadRequest(c, "outcome is required")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
result := db.DB.Model(&models.AgentDecision{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"outcome": body.Outcome,
|
||||
"outcome_recorded_at": now,
|
||||
})
|
||||
|
||||
if result.Error != nil {
|
||||
utils.Error("UpdateDecisionOutcome: update failed", "id", id, "error", result.Error)
|
||||
return utils.Internal(c, "failed to update outcome")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return utils.NotFound(c, "agent decision not found")
|
||||
}
|
||||
|
||||
return utils.OK(c, fiber.Map{"id": id, "outcome": body.Outcome})
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"doormile/constants"
|
||||
"doormile/db"
|
||||
"doormile/dto"
|
||||
"doormile/internal/assignment"
|
||||
"doormile/models"
|
||||
"doormile/utils"
|
||||
|
||||
@@ -150,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)
|
||||
@@ -393,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{
|
||||
@@ -454,34 +466,25 @@ 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
|
||||
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 {
|
||||
distance = calculateDistance(booking.Pickuplatitude, booking.Pickuplongitude, booking.Deliverylatitude, booking.Deliverylongitude)
|
||||
}
|
||||
estimatedPrice = 50.0 + (distance * 5.0) + (totalWeight * 10.0)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
@@ -499,9 +502,9 @@ func CreateCustomerBooking(c *fiber.Ctx) error {
|
||||
Bookingid: booking.Bookingid,
|
||||
Servicetype: serviceType,
|
||||
Estimatedprice: estimatedPrice,
|
||||
Pricingid: pricingID,
|
||||
Estimateddeliveryat: &estDelivery,
|
||||
Sladueat: &slaDue,
|
||||
Pricingid: pricingID,
|
||||
}
|
||||
|
||||
if err := tx.Create(&srvOption).Error; err != nil {
|
||||
@@ -521,6 +524,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 +647,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")
|
||||
}
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -13,6 +13,8 @@ import (
|
||||
"doormile/constants"
|
||||
"doormile/db"
|
||||
"doormile/dto"
|
||||
"doormile/internal/assignment"
|
||||
"doormile/internal/notify"
|
||||
"doormile/models"
|
||||
"doormile/utils"
|
||||
|
||||
@@ -113,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,
|
||||
@@ -336,6 +342,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")
|
||||
}
|
||||
|
||||
@@ -348,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()
|
||||
@@ -369,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")
|
||||
}
|
||||
|
||||
@@ -406,30 +436,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 +555,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 +595,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 +641,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 +969,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")
|
||||
}
|
||||
|
||||
147
controllers/pricing_helpers.go
Normal file
147
controllers/pricing_helpers.go
Normal file
@@ -0,0 +1,147 @@
|
||||
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)
|
||||
// - 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 "Regional"
|
||||
}
|
||||
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.
|
||||
// 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 (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 {
|
||||
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, nil, false
|
||||
}
|
||||
}
|
||||
|
||||
matched := applyFilters(allRules, weight, category)
|
||||
if len(matched) == 0 && category != "General" {
|
||||
matched = applyFilters(allRules, weight, "General")
|
||||
}
|
||||
if len(matched) == 0 {
|
||||
return 0, nil, false
|
||||
}
|
||||
|
||||
rule := matched[0]
|
||||
id := rule.Doormile_pricing_id
|
||||
return (rule.Minprice + rule.Maxprice) / 2, &id, true
|
||||
}
|
||||
13
doormile-abee7-66e9b8b66fb4.json
Normal file
13
doormile-abee7-66e9b8b66fb4.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"type": "service_account",
|
||||
"project_id": "doormile-abee7",
|
||||
"private_key_id": "66e9b8b66fb40961271f095dd924935eff19452b",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDLBD8DlrQwvwo8\ngkW9j/z1+eLPZYAT2TA4xWT1l+i8LV5he8wZ2TjxJT385OGS+zjIVUHQKtvStdDB\no9/0NXnSC6n88puNhQp9xCRjxEU+FLiSnqJGRUzSO4pu+0TuukGsouDII6QhJFNf\ndmtTOMmjV4V+wAOLfbE3qI5kZQ5trqf8wNBPZKxp9fhrqHdP080HUbOs14jiSrHl\n3OYRj0ZhDTsavboV6QTbdJn5ivH19nboFfdzuJrS5ApMhLqIGdoSEqjjQj8t6f6I\nm1RgQMfztIZLRPliA5MyJieEDZZdy5mmDQDnqCP6zFtH+WRVBe9Lo2A7F+Rf1vv4\n8/5FRd3HAgMBAAECggEACmEEsbPGMYnKxa4pT5gpaA/m6xB23EzpvLVGxJGIGfq9\nzQENvbKPyTBMu32eFKwYSpGlRDW0uFCIRCYTIIKNYFItVhu3HSSUlTpuW9VgbtyT\nVRecFziaxVK68JKTAxttmRxYnpLH5NPdGU/OC9qm4F1smz7Iz5xU75ID+Zj7BFtm\nkjZsgU9ALT7ikm205KhjYepc16ZQODIvvGW8vdNwxaxNl+5+RpkXdq+NK3KiAjK4\noqsuvDRRcchHttXU/qhri7f4nT0VBIuMnPDtRWGEaWtgCt4urLFAD4fZE91i1SHe\n7vV0iYdS9lzjrwl3Umzcf4k0pSPN8Sky1ZjN9hvF5QKBgQD+p2NMePwHY4uksA98\nVWT6y6haM+rIGxSsBL06UnCGO4E1gEWd++G5osDNc9hQk29xFg64pyIPKovw/Qez\nZ+gyCySC8KJoC4q740mxqn1EmANHXAvxGUMgPHmeiX+j3i1MwJWG1w7uyLwQ2216\nEgHy6Mqzk3jLUnD8vXf08CAUxQKBgQDMFvrRwRwpqsLR2yHWiu8uWykU276m1mKI\n8saR/iQ+jXQ9NZsgVdOjaV9EIv7jLwzSwEcXHVh9+SB5pnlCOhovGmmiSIdLVr5f\n1XOyWY6n2NzM6tm2RXjWOnwpVviOtdAwVVWXQNwaNbRLMZZkv+0bpOwk7i/ONv2z\n1bNP5pHJGwKBgQCo8LeJxkG9TScZTMwZOjXRxEaeAQ9jTcP7EvHOvV51Twi7S6vj\n0XTAyN6L7qy1V/p41Z9SlV6dHSbV66euN+LtQEMqsyWrTOzvFSyQt4B/fzG9p60s\neAv2WOh+as3JEt6Oqr0IRahcdcN+k3Qfpc6xLgs78zwR91GJ+tCYKK2/2QKBgBNX\nfi/Q0F12jH4c+Wj+w8ot44fRh5ECMlZuQ7lkT4UKHOEMKoZ2+9a5zsiDVIqtU9bJ\nWDNtmYxexuAgkc9f+ElMhIRXwVK3htVIQm29pRJtMAfPnhBzJw+OfWPhlS7ZNkDO\nZY0vHWzyeALEUU99DPiYubSKSkeN2J9pelPxMxHPAoGBAOOTd5XJRQOnFpd0eVwE\nlJ65zeQyT82V1OX/OPLKOcFwzNNKYF0Toto98nLK5rqqAGNUPp0fBMdUi7b76i79\nwve7aEZtCQWbhQsOUXsWocZyr8w5EexYtbxWa6q7jzmJaL/UfagE8pZWUI2k59CN\nRajHiJG83UxGMUN8KbSgv7h7\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "firebase-adminsdk-fbsvc@doormile-abee7.iam.gserviceaccount.com",
|
||||
"client_id": "108510619961572391178",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40doormile-abee7.iam.gserviceaccount.com",
|
||||
"universe_domain": "googleapis.com"
|
||||
}
|
||||
14
dto/auth.go
14
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 {
|
||||
|
||||
61
go.mod
61
go.mod
@@ -1,6 +1,6 @@
|
||||
module doormile
|
||||
|
||||
go 1.24.0
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/gofiber/fiber/v2 v2.52.10
|
||||
@@ -9,16 +9,45 @@ require (
|
||||
github.com/lib/pq v1.12.3
|
||||
github.com/redis/go-redis/v9 v9.16.0
|
||||
go.uber.org/zap v1.27.1
|
||||
golang.org/x/crypto v0.31.0
|
||||
golang.org/x/crypto v0.51.0
|
||||
gorm.io/driver/postgres v1.5.11
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.25.2 // indirect
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
cloud.google.com/go/auth v0.20.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
cloud.google.com/go/firestore v1.22.0 // indirect
|
||||
cloud.google.com/go/iam v1.11.0 // indirect
|
||||
cloud.google.com/go/longrunning v1.0.0 // indirect
|
||||
cloud.google.com/go/monitoring v1.29.0 // indirect
|
||||
cloud.google.com/go/storage v1.62.1 // indirect
|
||||
firebase.google.com/go/v4 v4.20.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0 // indirect
|
||||
github.com/MicahParks/keyfunc v1.9.0 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
|
||||
github.com/fasthttp/websocket v1.5.3 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gofiber/websocket/v2 v2.2.1 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.22.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||
@@ -32,12 +61,34 @@ require (
|
||||
github.com/nats-io/nats.go v1.31.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.5 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/net v0.54.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.44.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
google.golang.org/api v0.279.0 // indirect
|
||||
google.golang.org/appengine/v2 v2.0.6 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260511170946-3700d4141b60 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect
|
||||
google.golang.org/grpc v1.81.1 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
141
go.sum
141
go.sum
@@ -1,3 +1,33 @@
|
||||
cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
|
||||
cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||
cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
|
||||
cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
cloud.google.com/go/firestore v1.22.0 h1:avooeboIq37vKXobrbPUFhFBxS/c3FqmWoX0xs8dO6E=
|
||||
cloud.google.com/go/firestore v1.22.0/go.mod h1:PaM4i7i7ruALSKmlpHXXZaPObcZw0W7ie5UOPr72iTU=
|
||||
cloud.google.com/go/iam v1.11.0 h1:KieQ9Pb+LLPak1O3Rv3GgCxhnmkYf7Xyh0P5HfF1jFM=
|
||||
cloud.google.com/go/iam v1.11.0/go.mod h1:KP+nKGugNJW4LcLx1uEZcq1ok5sQHFaQehQNl4QDgV4=
|
||||
cloud.google.com/go/longrunning v1.0.0 h1:lwzWEYD8+NkYV7dhexOz6kmlvajZA70+bW/xMhRVVdY=
|
||||
cloud.google.com/go/longrunning v1.0.0/go.mod h1:8nqFBPOO1U/XkhWl0I19AMZEphrHi73VNABIpKYaTwM=
|
||||
cloud.google.com/go/monitoring v1.29.0 h1:AHhDsFaSax1/4k+qlIDX/SDGe6hggnfXJ9dkgD9qBPY=
|
||||
cloud.google.com/go/monitoring v1.29.0/go.mod h1:72NOVjJXHY/HBfoLT0+qlCZBT059+9VXLeAnL2PeeVM=
|
||||
cloud.google.com/go/storage v1.62.1 h1:Os0G3XbUbjZumkpDUf2Y0rLoXJTCF1kU2kWUujKYXD8=
|
||||
cloud.google.com/go/storage v1.62.1/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA=
|
||||
firebase.google.com/go/v4 v4.20.0 h1:ighpjeAC45rY/95cUQ+ojIKlKcTnz2YC0ldam56z2YU=
|
||||
firebase.google.com/go/v4 v4.20.0/go.mod h1:hqhkQtZkThGH42TnaYi7A8EFR1E0FEuB5oHvJ1Q57t8=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0 h1:O2sXMyJh8b7devAGdE+163xtRurt0RVpB6DIzX5vGfg=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0/go.mod h1:hEpiGU18xf70qb3jbTcIggWAiEfX/cOIVc2OTe4OegA=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0 h1:0YP0+/ixwu+Uqeu/FGiBZNQ19huiUxxiPXIc9WsLKuQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0/go.mod h1:6ZZMQhZKDvUvkJw2rc+oDP90tMMzuU/J+5HG1ZmPOmE=
|
||||
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
|
||||
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
@@ -6,17 +36,49 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
|
||||
github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQtdek=
|
||||
github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5elY=
|
||||
github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w=
|
||||
github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
||||
github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4=
|
||||
github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
@@ -48,6 +110,8 @@ github.com/nats-io/nkeys v0.4.5 h1:Zdz2BUlFm4fJlierwvGK+yl20IAKUm7eV6AAZXEhkPk=
|
||||
github.com/nats-io/nkeys v0.4.5/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4=
|
||||
@@ -55,6 +119,10 @@ github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@@ -66,22 +134,95 @@ github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1S
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.279.0 h1:hsx2M2OaRcaKtVYK6vXEUnQvdjnend7ZYES+lYaot74=
|
||||
google.golang.org/api v0.279.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ=
|
||||
google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw=
|
||||
google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
|
||||
google.golang.org/genproto v0.0.0-20260511170946-3700d4141b60 h1:rhBdfmsOlOZIvz3Y5/BdUzPg2CkO8L7QQPKj96B8554=
|
||||
google.golang.org/genproto v0.0.0-20260511170946-3700d4141b60/go.mod h1:8xo2Pj1b20ZOCpzlU3B9qieMwVIAXx1QVZWLMlPL6sM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 h1:3WsB1FAbiRIf2tOxscWKs3pQBD9he1NsrnbhMuWfekc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60/go.mod h1:7yoXV7RIh5gblj/xVYoogxAWvA9wUeVbpsK/M694l00=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
269
internal/assignment/crm_assignment.go
Normal file
269
internal/assignment/crm_assignment.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package assignment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"doormile/constants"
|
||||
"doormile/db"
|
||||
"doormile/models"
|
||||
"doormile/utils"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
const (
|
||||
maxRetries = 5
|
||||
retryDelay = 2 * time.Minute
|
||||
geoRadiusKm = 10.0
|
||||
geoMaxCount = 10
|
||||
maxActive = 3
|
||||
)
|
||||
|
||||
type milerCandidate struct {
|
||||
profile models.MilerProfile
|
||||
distanceKm float64
|
||||
activeBookings int64
|
||||
}
|
||||
|
||||
// AssignCRMMiler finds the best available nearby miler for a CRM booking and assigns them.
|
||||
// It retries up to maxRetries times (retryDelay apart) before logging NO_MILER_AVAILABLE.
|
||||
// Must be called as a goroutine after tx.Commit() in CreateCRMBooking.
|
||||
func AssignCRMMiler(bookingID int) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
utils.Error("CRMAssignment: panic recovered", "booking_id", bookingID, "error", r)
|
||||
}
|
||||
}()
|
||||
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
if attempt > 1 {
|
||||
time.Sleep(retryDelay)
|
||||
}
|
||||
|
||||
utils.Info("CRMAssignment: attempting assignment", "booking_id", bookingID, "attempt", attempt)
|
||||
|
||||
done, err := tryAssign(bookingID)
|
||||
if err != nil {
|
||||
utils.Error("CRMAssignment: attempt error", "booking_id", bookingID, "attempt", attempt, "error", err)
|
||||
continue
|
||||
}
|
||||
if done {
|
||||
return
|
||||
}
|
||||
|
||||
utils.Warn("CRMAssignment: no eligible miler found on attempt",
|
||||
"booking_id", bookingID,
|
||||
"attempt", attempt,
|
||||
"remaining", maxRetries-attempt,
|
||||
)
|
||||
}
|
||||
|
||||
utils.Error("CRMAssignment: NO_MILER_AVAILABLE — all retries exhausted",
|
||||
"booking_id", bookingID,
|
||||
"max_retries", maxRetries,
|
||||
)
|
||||
}
|
||||
|
||||
// tryAssign performs a single attempt: queries Redis GEO, scores candidates, commits.
|
||||
// Returns (true, nil) on success or when the booking no longer needs assignment.
|
||||
// Returns (false, nil) when no eligible miler was found (retry warranted).
|
||||
// Returns (false, err) on hard errors (booking missing, DB failure).
|
||||
func tryAssign(bookingID int) (bool, error) {
|
||||
var booking models.PickupBooking
|
||||
if err := db.DB.First(&booking, bookingID).Error; err != nil {
|
||||
return false, fmt.Errorf("load booking: %w", err)
|
||||
}
|
||||
|
||||
// If the booking was cancelled or already assigned between retries, stop.
|
||||
if booking.Status == constants.BookingCancelled || booking.Assignedmileruserid != nil {
|
||||
utils.Info("CRMAssignment: booking no longer needs assignment",
|
||||
"booking_id", bookingID,
|
||||
"status", booking.Status,
|
||||
)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if booking.Pickuplatitude == 0 || booking.Pickuplongitude == 0 {
|
||||
return false, fmt.Errorf("booking %d has no pickup coordinates", bookingID)
|
||||
}
|
||||
|
||||
nearby, err := queryNearbyMilers(booking.Pickuplatitude, booking.Pickuplongitude)
|
||||
if err != nil {
|
||||
utils.Warn("CRMAssignment: GEO query failed", "booking_id", bookingID, "error", err)
|
||||
return false, nil
|
||||
}
|
||||
if len(nearby) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
candidate, found := pickBestMiler(nearby)
|
||||
if !found {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := commitAssignment(&booking, candidate); err != nil {
|
||||
return false, fmt.Errorf("commit: %w", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// queryNearbyMilers runs GEOSEARCH on milers:locations and returns up to geoMaxCount
|
||||
// milers within geoRadiusKm km, sorted nearest-first, with distances populated.
|
||||
func queryNearbyMilers(lat, lon float64) ([]redis.GeoLocation, error) {
|
||||
if db.Rdb == nil {
|
||||
return nil, fmt.Errorf("Redis not available")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
locs, err := db.Rdb.GeoSearchLocation(ctx, "milers:locations", &redis.GeoSearchLocationQuery{
|
||||
GeoSearchQuery: redis.GeoSearchQuery{
|
||||
Longitude: lon,
|
||||
Latitude: lat,
|
||||
Radius: geoRadiusKm,
|
||||
RadiusUnit: "km",
|
||||
Sort: "ASC",
|
||||
Count: geoMaxCount,
|
||||
},
|
||||
WithDist: true,
|
||||
}).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return locs, nil
|
||||
}
|
||||
|
||||
// pickBestMiler filters the nearby list for eligibility, then returns the candidate
|
||||
// with the lowest score. score = distance_km*1.0 + active_bookings*2.0 - rating*0.5
|
||||
func pickBestMiler(nearby []redis.GeoLocation) (*milerCandidate, bool) {
|
||||
var best *milerCandidate
|
||||
bestScore := 1e18
|
||||
|
||||
for _, loc := range nearby {
|
||||
milerUserID, err := strconv.Atoi(loc.Name)
|
||||
if err != nil {
|
||||
utils.Warn("CRMAssignment: skipping non-numeric GEO member", "name", loc.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
var profile models.MilerProfile
|
||||
if err := db.DB.Where("userid = ?", milerUserID).First(&profile).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if profile.Availabilitystatus != constants.MilerAvailable {
|
||||
continue
|
||||
}
|
||||
|
||||
var activeCount int64
|
||||
db.DB.Model(&models.BookingAssignment{}).
|
||||
Where("mileruserid = ? AND assignmentstatus IN ?", milerUserID, []string{
|
||||
constants.AssignmentAssigned,
|
||||
constants.AssignmentAccepted,
|
||||
}).
|
||||
Count(&activeCount)
|
||||
|
||||
if activeCount >= maxActive {
|
||||
continue
|
||||
}
|
||||
|
||||
score := loc.Dist*1.0 + float64(activeCount)*2.0 - profile.Rating*0.5
|
||||
|
||||
if score < bestScore {
|
||||
bestScore = score
|
||||
best = &milerCandidate{
|
||||
profile: profile,
|
||||
distanceKm: loc.Dist,
|
||||
activeBookings: activeCount,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return best, best != nil
|
||||
}
|
||||
|
||||
// commitAssignment writes the BookingAssignment row, updates the booking and the
|
||||
// miler's availability status in a single transaction, then publishes to NATS.
|
||||
func commitAssignment(booking *models.PickupBooking, candidate *milerCandidate) error {
|
||||
milerUserID := candidate.profile.Userid
|
||||
|
||||
tx := db.DB.Begin()
|
||||
|
||||
assignment := models.BookingAssignment{
|
||||
Bookingid: booking.Bookingid,
|
||||
Mileruserid: milerUserID,
|
||||
Assignmentstatus: constants.AssignmentAssigned,
|
||||
Assignedat: time.Now(),
|
||||
}
|
||||
if err := tx.Create(&assignment).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("create BookingAssignment: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if err := tx.Model(booking).Updates(map[string]interface{}{
|
||||
"assignedmileruserid": milerUserID,
|
||||
"status": constants.BookingMilerAssigned,
|
||||
"updatedat": now,
|
||||
}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("update PickupBooking: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Model(&models.MilerProfile{}).
|
||||
Where("userid = ?", milerUserID).
|
||||
Update("availabilitystatus", constants.MilerAssigned).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("update MilerProfile availability: %w", err)
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
utils.Info("CRMAssignment: assigned",
|
||||
"booking_id", booking.Bookingid,
|
||||
"miler_id", milerUserID,
|
||||
"distance_km", candidate.distanceKm,
|
||||
"active_bookings", candidate.activeBookings,
|
||||
"score", candidate.distanceKm*1.0+float64(candidate.activeBookings)*2.0-candidate.profile.Rating*0.5,
|
||||
)
|
||||
|
||||
publishAssignment(booking, milerUserID)
|
||||
notifyMilerNewAssignment(candidate.profile, booking.Bookingid)
|
||||
notifyCustomerMilerAssigned(booking, candidate.profile.Displayname)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// publishAssignment sends the booking.assigned event to NATS JetStream.
|
||||
// Non-fatal: logs a warning and returns if NATS is unavailable or publish fails.
|
||||
func publishAssignment(booking *models.PickupBooking, milerUserID int) {
|
||||
if db.Js == nil {
|
||||
return
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"booking_id": booking.Bookingid,
|
||||
"booking_no": booking.Bookingno,
|
||||
"miler_id": milerUserID,
|
||||
"provider_company": booking.Providercompany,
|
||||
"provider_hub": booking.Providerlocation,
|
||||
"assigned_at": time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
utils.Warn("CRMAssignment: failed to marshal NATS payload", "booking_id", booking.Bookingid, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := db.Js.Publish("booking.assigned", data); err != nil {
|
||||
utils.Warn("CRMAssignment: NATS publish failed", "booking_id", booking.Bookingid, "error", err)
|
||||
}
|
||||
}
|
||||
391
internal/assignment/customer_assignment.go
Normal file
391
internal/assignment/customer_assignment.go
Normal file
@@ -0,0 +1,391 @@
|
||||
package assignment
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"doormile/constants"
|
||||
"doormile/db"
|
||||
"doormile/models"
|
||||
"doormile/utils"
|
||||
)
|
||||
|
||||
// providerResult holds the chosen logistics provider and their pricing details.
|
||||
type providerResult struct {
|
||||
company string
|
||||
estimatedPrice float64
|
||||
reliability float64
|
||||
}
|
||||
|
||||
// AssignCustomerMiler is the B2C goroutine entry point.
|
||||
// It selects both a miler (first-mile pickup) and a provider (delivery routing),
|
||||
// then commits the assignment. Retries up to maxRetries times with retryDelay in between.
|
||||
// Must be called as a goroutine after tx.Commit() in CreateCustomerBooking.
|
||||
func AssignCustomerMiler(bookingID int) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
utils.Error("B2CAssignment: panic recovered", "booking_id", bookingID, "error", r)
|
||||
}
|
||||
}()
|
||||
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
if attempt > 1 {
|
||||
time.Sleep(retryDelay)
|
||||
}
|
||||
|
||||
utils.Info("B2CAssignment: attempting assignment", "booking_id", bookingID, "attempt", attempt)
|
||||
|
||||
done, err := tryCustomerAssign(bookingID)
|
||||
if err != nil {
|
||||
utils.Error("B2CAssignment: attempt error", "booking_id", bookingID, "attempt", attempt, "error", err)
|
||||
continue
|
||||
}
|
||||
if done {
|
||||
return
|
||||
}
|
||||
|
||||
utils.Warn("B2CAssignment: no eligible miler on attempt",
|
||||
"booking_id", bookingID,
|
||||
"attempt", attempt,
|
||||
"remaining", maxRetries-attempt,
|
||||
)
|
||||
}
|
||||
|
||||
utils.Error("B2CAssignment: NO_MILER_AVAILABLE — all retries exhausted",
|
||||
"booking_id", bookingID,
|
||||
"max_retries", maxRetries,
|
||||
)
|
||||
}
|
||||
|
||||
// tryCustomerAssign performs one full attempt: GEO miler search → provider selection → commit.
|
||||
// Returns (true, nil) on success or when the booking no longer needs assignment.
|
||||
// Returns (false, nil) when no eligible miler was found (retry warranted).
|
||||
// Returns (false, err) on hard errors.
|
||||
func tryCustomerAssign(bookingID int) (bool, error) {
|
||||
var booking models.PickupBooking
|
||||
if err := db.DB.Preload("Parcels").Preload("ServiceOptions").First(&booking, bookingID).Error; err != nil {
|
||||
return false, fmt.Errorf("load booking: %w", err)
|
||||
}
|
||||
|
||||
if booking.Status == constants.BookingCancelled || booking.Assignedmileruserid != nil {
|
||||
utils.Info("B2CAssignment: booking no longer needs assignment",
|
||||
"booking_id", bookingID,
|
||||
"status", booking.Status,
|
||||
)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if booking.Pickuplatitude == 0 || booking.Pickuplongitude == 0 {
|
||||
return false, fmt.Errorf("booking %d has no pickup coordinates", bookingID)
|
||||
}
|
||||
|
||||
// Step 1 — Find the best nearby miler (shared GEO logic from crm_assignment.go).
|
||||
nearby, err := queryNearbyMilers(booking.Pickuplatitude, booking.Pickuplongitude)
|
||||
if err != nil {
|
||||
utils.Warn("B2CAssignment: GEO query failed", "booking_id", bookingID, "error", err)
|
||||
return false, nil
|
||||
}
|
||||
if len(nearby) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
miler, found := pickBestMiler(nearby)
|
||||
if !found {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Step 2 — Select the best logistics provider from DoormilePricing.
|
||||
zone := b2cResolveZone(booking.Pickuppincode, booking.Deliverypincode)
|
||||
category := b2cNormalizePricingCategory(b2cFirstParcelCategory(&booking))
|
||||
serviceType := b2cFirstServiceType(&booking)
|
||||
weight := b2cChargeableWeight(&booking)
|
||||
|
||||
provider, providerFound := selectBestProvider(zone, category, serviceType, weight)
|
||||
if !providerFound {
|
||||
utils.Warn("B2CAssignment: no provider pricing match, defaulting to Doormile",
|
||||
"booking_id", bookingID,
|
||||
"zone", zone,
|
||||
"category", category,
|
||||
)
|
||||
provider = providerResult{company: "Doormile"}
|
||||
}
|
||||
|
||||
// Step 6 — ETA based on miler-to-pickup distance.
|
||||
etaMinutes := calculateETA(miler.distanceKm)
|
||||
|
||||
// Steps 3–5 — Commit to DB and publish to NATS.
|
||||
if err := commitCustomerAssignment(&booking, miler, provider, etaMinutes); err != nil {
|
||||
return false, fmt.Errorf("commit: %w", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// ─── Provider selection ───────────────────────────────────────────────────────
|
||||
|
||||
// selectBestProvider queries all active DoormilePricing rows that cover the given
|
||||
// zone / serviceType / weight and picks the provider with the lowest composite score.
|
||||
//
|
||||
// Scoring (lower = better):
|
||||
//
|
||||
// score = avg_price - reliability_score * 2.0
|
||||
//
|
||||
// A reliability point is worth 2 rupees — price dominates for large spreads, but
|
||||
// a highly reliable provider can edge out a marginally cheaper one.
|
||||
func selectBestProvider(zone, category, serviceType string, weight float64) (providerResult, bool) {
|
||||
pricingServiceType := b2cMapServiceTypeToPricing(serviceType)
|
||||
|
||||
var rules []models.DoormilePricing
|
||||
db.DB.Where(
|
||||
"zone = ? AND servicetype = ? AND status = ? AND deletedat IS NULL"+
|
||||
" AND min_weight <= ? AND max_weight >= ?",
|
||||
zone, pricingServiceType, "Active", weight, weight,
|
||||
).Find(&rules)
|
||||
|
||||
if len(rules) == 0 {
|
||||
return providerResult{}, false
|
||||
}
|
||||
|
||||
// Prefer exact category match; fall back to General; use all rows as last resort.
|
||||
matched := b2cFilterByCategory(rules, category)
|
||||
if len(matched) == 0 && category != "General" {
|
||||
matched = b2cFilterByCategory(rules, "General")
|
||||
}
|
||||
if len(matched) == 0 {
|
||||
matched = rules
|
||||
}
|
||||
|
||||
var best *providerResult
|
||||
bestScore := math.MaxFloat64
|
||||
|
||||
for _, rule := range matched {
|
||||
avgPrice := (rule.Minprice + rule.Maxprice) / 2.0
|
||||
score := avgPrice - rule.Reliabilityscore*2.0
|
||||
|
||||
if score < bestScore {
|
||||
bestScore = score
|
||||
r := providerResult{
|
||||
company: rule.Providercompany,
|
||||
estimatedPrice: avgPrice,
|
||||
reliability: rule.Reliabilityscore,
|
||||
}
|
||||
best = &r
|
||||
}
|
||||
}
|
||||
|
||||
if best == nil {
|
||||
return providerResult{}, false
|
||||
}
|
||||
return *best, true
|
||||
}
|
||||
|
||||
// ─── ETA ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
// calculateETA returns the estimated arrival time in minutes.
|
||||
// Formula: (miler_to_pickup_distance_km / 20.0) * 60 + 10-minute pickup buffer.
|
||||
func calculateETA(distanceKm float64) float64 {
|
||||
return math.Round((distanceKm/20.0)*60.0 + 10.0)
|
||||
}
|
||||
|
||||
// ─── Commit ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// commitCustomerAssignment writes the full assignment in a single DB transaction:
|
||||
// - BookingAssignment row (status: Assigned)
|
||||
// - PickupBooking: assignedmileruserid, status → Miler_Assigned, providercompany (if found)
|
||||
// - MilerProfile: availabilitystatus → Assigned
|
||||
//
|
||||
// Publishes to NATS after a successful commit.
|
||||
func commitCustomerAssignment(
|
||||
booking *models.PickupBooking,
|
||||
miler *milerCandidate,
|
||||
provider providerResult,
|
||||
etaMinutes float64,
|
||||
) error {
|
||||
milerUserID := miler.profile.Userid
|
||||
|
||||
tx := db.DB.Begin()
|
||||
|
||||
ba := models.BookingAssignment{
|
||||
Bookingid: booking.Bookingid,
|
||||
Mileruserid: milerUserID,
|
||||
Assignmentstatus: constants.AssignmentAssigned,
|
||||
Assignedat: time.Now(),
|
||||
}
|
||||
if err := tx.Create(&ba).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("create BookingAssignment: %w", err)
|
||||
}
|
||||
|
||||
bookingUpdates := map[string]interface{}{
|
||||
"assignedmileruserid": milerUserID,
|
||||
"status": constants.BookingMilerAssigned,
|
||||
"updatedat": time.Now(),
|
||||
}
|
||||
if provider.company != "" {
|
||||
bookingUpdates["providercompany"] = provider.company
|
||||
}
|
||||
|
||||
if err := tx.Model(booking).Updates(bookingUpdates).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("update PickupBooking: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Model(&models.MilerProfile{}).
|
||||
Where("userid = ?", milerUserID).
|
||||
Update("availabilitystatus", constants.MilerAssigned).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("update MilerProfile availability: %w", err)
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
utils.Info("B2CAssignment: assigned",
|
||||
"booking_id", booking.Bookingid,
|
||||
"miler_id", milerUserID,
|
||||
"provider", provider.company,
|
||||
"estimated_price", provider.estimatedPrice,
|
||||
"eta_minutes", etaMinutes,
|
||||
"miler_distance_km", miler.distanceKm,
|
||||
)
|
||||
|
||||
publishCustomerAssignment(booking, milerUserID, provider, etaMinutes)
|
||||
notifyMilerNewAssignment(miler.profile, booking.Bookingid)
|
||||
notifyCustomerMilerAssigned(booking, miler.profile.Displayname)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// publishCustomerAssignment sends the booking.assigned event to NATS JetStream.
|
||||
// Non-fatal: logs a warning and returns if NATS is unavailable or publish fails.
|
||||
func publishCustomerAssignment(
|
||||
booking *models.PickupBooking,
|
||||
milerUserID int,
|
||||
provider providerResult,
|
||||
etaMinutes float64,
|
||||
) {
|
||||
if db.Js == nil {
|
||||
return
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"booking_id": booking.Bookingid,
|
||||
"booking_no": booking.Bookingno,
|
||||
"miler_id": milerUserID,
|
||||
"selected_provider": provider.company,
|
||||
"estimated_price": provider.estimatedPrice,
|
||||
"eta_minutes": etaMinutes,
|
||||
"assigned_at": time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
utils.Warn("B2CAssignment: failed to marshal NATS payload", "booking_id", booking.Bookingid, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := db.Js.Publish("booking.assigned", data); err != nil {
|
||||
utils.Warn("B2CAssignment: NATS publish failed", "booking_id", booking.Bookingid, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Booking field extractors ─────────────────────────────────────────────────
|
||||
|
||||
func b2cFirstParcelCategory(booking *models.PickupBooking) string {
|
||||
if len(booking.Parcels) > 0 {
|
||||
return booking.Parcels[0].Itemcategory
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func b2cFirstServiceType(booking *models.PickupBooking) string {
|
||||
if len(booking.ServiceOptions) > 0 {
|
||||
return booking.ServiceOptions[0].Servicetype
|
||||
}
|
||||
return "Normal"
|
||||
}
|
||||
|
||||
// b2cChargeableWeight sums max(actual, volumetric) per parcel across the booking.
|
||||
func b2cChargeableWeight(booking *models.PickupBooking) float64 {
|
||||
var total float64
|
||||
for _, p := range booking.Parcels {
|
||||
volumetric := (p.Length * p.Width * p.Height) / 5000.0
|
||||
total += math.Max(p.Weight, volumetric)
|
||||
}
|
||||
if total == 0 {
|
||||
return 0.5 // minimum chargeable to avoid zero-weight lookup
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// ─── Zone / category helpers (self-contained; no import of controllers pkg) ──
|
||||
|
||||
func b2cPincodeToState(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"
|
||||
case p >= 560 && p <= 591:
|
||||
return "KA"
|
||||
case p >= 600 && p <= 643:
|
||||
return "TN"
|
||||
case p >= 670 && p <= 695:
|
||||
return "KL"
|
||||
case p >= 380 && p <= 396:
|
||||
return "GJ"
|
||||
case p >= 400 && p <= 444:
|
||||
return "MH"
|
||||
default:
|
||||
if len(pincode) >= 2 {
|
||||
return pincode[:2]
|
||||
}
|
||||
return pincode
|
||||
}
|
||||
}
|
||||
|
||||
func b2cResolveZone(pickupPincode, deliveryPincode string) string {
|
||||
if len(pickupPincode) >= 3 && len(deliveryPincode) >= 3 && pickupPincode[:3] == deliveryPincode[:3] {
|
||||
return "Local"
|
||||
}
|
||||
if b2cPincodeToState(pickupPincode) == b2cPincodeToState(deliveryPincode) {
|
||||
return "Regional"
|
||||
}
|
||||
return "National"
|
||||
}
|
||||
|
||||
func b2cNormalizePricingCategory(category string) string {
|
||||
switch category {
|
||||
case "General", "Documents", "Electronics", "Clothing", "Fragile", "Medical", "Automotive", "Food":
|
||||
return category
|
||||
default:
|
||||
return "General"
|
||||
}
|
||||
}
|
||||
|
||||
func b2cMapServiceTypeToPricing(serviceType string) string {
|
||||
if serviceType == "Fast" || serviceType == "Superfast" || serviceType == "Express" {
|
||||
return "Express"
|
||||
}
|
||||
return "Normal"
|
||||
}
|
||||
|
||||
func b2cFilterByCategory(rules []models.DoormilePricing, category string) []models.DoormilePricing {
|
||||
out := make([]models.DoormilePricing, 0, len(rules))
|
||||
for _, r := range rules {
|
||||
if r.Category == category {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
55
internal/assignment/notify.go
Normal file
55
internal/assignment/notify.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package assignment
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"doormile/db"
|
||||
"doormile/internal/notify"
|
||||
"doormile/models"
|
||||
"doormile/utils"
|
||||
)
|
||||
|
||||
// notifyMilerNewAssignment sends an FCM push to the miler when a booking is assigned.
|
||||
// Non-fatal: failure is logged but never blocks the assignment flow.
|
||||
func notifyMilerNewAssignment(profile models.MilerProfile, bookingID int) {
|
||||
if profile.Devicetoken == "" {
|
||||
return
|
||||
}
|
||||
if err := notify.SendToDevice(
|
||||
profile.Devicetoken,
|
||||
"New Pickup Assigned",
|
||||
"New booking assigned — tap to view details",
|
||||
map[string]string{"booking_id": strconv.Itoa(bookingID)},
|
||||
); err != nil {
|
||||
utils.Warn("FCM: failed to notify miler on assignment",
|
||||
"miler_id", profile.Userid,
|
||||
"booking_id", bookingID,
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// notifyCustomerMilerAssigned sends an FCM push to the customer when their miler is assigned.
|
||||
// Loads the customer's device token from DB. Non-fatal on any failure.
|
||||
func notifyCustomerMilerAssigned(booking *models.PickupBooking, milerName string) {
|
||||
var customer models.AppCustomer
|
||||
if err := db.DB.Where("appcustomerid = ?", booking.Appcustomerid).First(&customer).Error; err != nil {
|
||||
return
|
||||
}
|
||||
if customer.Devicetoken == "" {
|
||||
return
|
||||
}
|
||||
if err := notify.SendToDevice(
|
||||
customer.Devicetoken,
|
||||
"Miler Assigned",
|
||||
fmt.Sprintf("Miler assigned — %s is on the way", milerName),
|
||||
map[string]string{"booking_id": strconv.Itoa(booking.Bookingid)},
|
||||
); err != nil {
|
||||
utils.Warn("FCM: failed to notify customer on assignment",
|
||||
"customer_id", booking.Appcustomerid,
|
||||
"booking_id", booking.Bookingid,
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
}
|
||||
74
internal/notify/fcm.go
Normal file
74
internal/notify/fcm.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
firebase "firebase.google.com/go/v4"
|
||||
"firebase.google.com/go/v4/messaging"
|
||||
"doormile/utils"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
var (
|
||||
fcmClient *messaging.Client
|
||||
fcmOnce sync.Once
|
||||
)
|
||||
|
||||
// InitFCM initializes the Firebase Admin SDK using the service account JSON file
|
||||
// at FIREBASE_SERVICE_ACCOUNT_PATH. Safe to call from main() at startup.
|
||||
// If the env var is unset or the file is invalid, FCM is disabled for the process
|
||||
// lifetime — all SendToDevice calls become no-ops.
|
||||
func InitFCM() {
|
||||
fcmOnce.Do(func() {
|
||||
path := os.Getenv("FIREBASE_SERVICE_ACCOUNT_PATH")
|
||||
if path == "" {
|
||||
utils.Warn("FCM: FIREBASE_SERVICE_ACCOUNT_PATH not set — push notifications disabled")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
app, err := firebase.NewApp(ctx, nil, option.WithCredentialsFile(path))
|
||||
if err != nil {
|
||||
utils.Error("FCM: failed to initialize Firebase app", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
client, err := app.Messaging(ctx)
|
||||
if err != nil {
|
||||
utils.Error("FCM: failed to get Messaging client", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
fcmClient = client
|
||||
utils.Info("FCM: initialized successfully")
|
||||
})
|
||||
}
|
||||
|
||||
// SendToDevice sends an FCM notification to a single device token.
|
||||
// Returns nil (no-op) if FCM was not initialized or the token is empty.
|
||||
// The caller should log errors but must not block the booking flow on failure.
|
||||
func SendToDevice(token, title, body string, data map[string]string) error {
|
||||
if fcmClient == nil || token == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
msg := &messaging.Message{
|
||||
Notification: &messaging.Notification{
|
||||
Title: title,
|
||||
Body: body,
|
||||
},
|
||||
Data: data,
|
||||
Token: token,
|
||||
}
|
||||
|
||||
_, err := fcmClient.Send(ctx, msg)
|
||||
return err
|
||||
}
|
||||
271
internal/ws/chat.go
Normal file
271
internal/ws/chat.go
Normal file
@@ -0,0 +1,271 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"doormile/db"
|
||||
"doormile/models"
|
||||
"doormile/utils"
|
||||
|
||||
"github.com/gofiber/websocket/v2"
|
||||
)
|
||||
|
||||
// chatMessage is the JSON frame broadcast to the receiving participant.
|
||||
type chatMessage struct {
|
||||
Sender string `json:"sender"`
|
||||
Message string `json:"message"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
|
||||
// chatConn wraps a single WebSocket connection with a write mutex so concurrent
|
||||
// senders (broadcast from the other peer, poller close) never interleave frames.
|
||||
type chatConn struct {
|
||||
conn *websocket.Conn
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (cc *chatConn) send(data []byte) {
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
cc.conn.WriteMessage(websocket.TextMessage, data) //nolint:errcheck — best-effort delivery
|
||||
}
|
||||
|
||||
func (cc *chatConn) close() {
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
cc.conn.Close() //nolint:errcheck
|
||||
}
|
||||
|
||||
// chatRoom holds at most two named slots: "customer" and "miler".
|
||||
// All slot mutations are protected by mu.
|
||||
type chatRoom struct {
|
||||
bookingID int
|
||||
mu sync.Mutex
|
||||
slots map[string]*chatConn
|
||||
closeOnce sync.Once // ensures shutdown() body runs exactly once
|
||||
closeCh chan struct{} // closed by shutdown()
|
||||
pollerOnce sync.Once // ensures exactly one status-poller goroutine per room
|
||||
}
|
||||
|
||||
// rooms is the process-wide registry of active chat rooms, keyed by bookingID.
|
||||
var rooms sync.Map // map[int]*chatRoom
|
||||
|
||||
// ChatHandler is the WebSocket handler for the ephemeral per-booking chat room.
|
||||
//
|
||||
// Route: GET /ws/bookings/:bookingid/chat
|
||||
// Params: role=customer|miler (query)
|
||||
// token=<JWT> (query, validated by WsChatAuth middleware)
|
||||
//
|
||||
// At most two participants (one customer, one miler) may occupy a room.
|
||||
// Messages received from one participant are forwarded to the other.
|
||||
// The room closes automatically when the booking reaches a terminal status.
|
||||
func ChatHandler(c *websocket.Conn) {
|
||||
bookingID, err := strconv.Atoi(c.Params("bookingid"))
|
||||
if err != nil {
|
||||
sendError(c, "invalid booking ID")
|
||||
return
|
||||
}
|
||||
|
||||
role := c.Query("role")
|
||||
if role != "customer" && role != "miler" {
|
||||
sendError(c, "role must be 'customer' or 'miler'")
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the booking exists before letting anyone into the room.
|
||||
var booking models.PickupBooking
|
||||
if err := db.DB.First(&booking, bookingID).Error; err != nil {
|
||||
sendError(c, "booking not found")
|
||||
return
|
||||
}
|
||||
if isTerminalStatus(booking.Status) {
|
||||
sendError(c, "chat is closed — booking is already completed")
|
||||
return
|
||||
}
|
||||
|
||||
room, err := joinRoom(bookingID, role, c)
|
||||
if err != nil {
|
||||
sendError(c, err.Error())
|
||||
return
|
||||
}
|
||||
defer room.leave(role)
|
||||
|
||||
utils.Info("WS/Chat: participant joined", "booking_id", bookingID, "role", role)
|
||||
|
||||
// Block here reading client messages. Exits on client disconnect or room close.
|
||||
for {
|
||||
_, raw, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
room.broadcast(role, string(raw))
|
||||
}
|
||||
|
||||
utils.Info("WS/Chat: participant left", "booking_id", bookingID, "role", role)
|
||||
}
|
||||
|
||||
// ─── Room lifecycle ───────────────────────────────────────────────────────────
|
||||
|
||||
// joinRoom finds or creates the room for bookingID, registers the connection in
|
||||
// the given role slot, and starts the booking-status poller on first join.
|
||||
// Returns an error if the slot is already occupied or the room is shutting down.
|
||||
func joinRoom(bookingID int, role string, c *websocket.Conn) (*chatRoom, error) {
|
||||
val, _ := rooms.LoadOrStore(bookingID, &chatRoom{
|
||||
bookingID: bookingID,
|
||||
slots: make(map[string]*chatConn),
|
||||
closeCh: make(chan struct{}),
|
||||
})
|
||||
room := val.(*chatRoom)
|
||||
|
||||
room.mu.Lock()
|
||||
defer room.mu.Unlock()
|
||||
|
||||
// If shutdown already started (e.g., race with a closing poller), reject.
|
||||
select {
|
||||
case <-room.closeCh:
|
||||
return nil, fmt.Errorf("chat room is closed")
|
||||
default:
|
||||
}
|
||||
|
||||
if _, exists := room.slots[role]; exists {
|
||||
return nil, fmt.Errorf("%s is already connected to this room", role)
|
||||
}
|
||||
|
||||
room.slots[role] = &chatConn{conn: c}
|
||||
|
||||
// Start the status poller the first time any participant joins.
|
||||
room.pollerOnce.Do(func() { go room.pollBookingStatus() })
|
||||
|
||||
return room, nil
|
||||
}
|
||||
|
||||
// leave removes the participant from the room and shuts the room down if empty.
|
||||
// Called via defer in ChatHandler — safe even after a force-close.
|
||||
func (r *chatRoom) leave(role string) {
|
||||
r.mu.Lock()
|
||||
delete(r.slots, role)
|
||||
isEmpty := len(r.slots) == 0
|
||||
r.mu.Unlock()
|
||||
|
||||
if isEmpty {
|
||||
r.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
// broadcast wraps the raw text in a chatMessage frame and sends it to every
|
||||
// participant other than the sender.
|
||||
func (r *chatRoom) broadcast(senderRole, text string) {
|
||||
frame := chatMessage{
|
||||
Sender: senderRole,
|
||||
Message: text,
|
||||
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
data, err := json.Marshal(frame)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
targets := make([]*chatConn, 0, 1)
|
||||
for role, cc := range r.slots {
|
||||
if role != senderRole {
|
||||
targets = append(targets, cc)
|
||||
}
|
||||
}
|
||||
r.mu.Unlock()
|
||||
|
||||
for _, cc := range targets {
|
||||
cc.send(data)
|
||||
}
|
||||
}
|
||||
|
||||
// shutdown closes the done channel and removes the room from the global registry.
|
||||
// Protected by closeOnce — safe to call from both the poller and connection handlers.
|
||||
func (r *chatRoom) shutdown() {
|
||||
r.closeOnce.Do(func() {
|
||||
close(r.closeCh)
|
||||
rooms.Delete(r.bookingID)
|
||||
utils.Info("WS/Chat: room removed from registry", "booking_id", r.bookingID)
|
||||
})
|
||||
}
|
||||
|
||||
// forceClose closes every open WebSocket connection then shuts the room down.
|
||||
// Called by the status poller when the booking reaches a terminal status.
|
||||
// Each handler's c.ReadMessage() will return an error, causing it to break
|
||||
// its read loop and call room.leave() — which hits shutdown() again (no-op).
|
||||
func (r *chatRoom) forceClose() {
|
||||
r.mu.Lock()
|
||||
conns := make([]*chatConn, 0, len(r.slots))
|
||||
for _, cc := range r.slots {
|
||||
conns = append(conns, cc)
|
||||
}
|
||||
r.mu.Unlock()
|
||||
|
||||
for _, cc := range conns {
|
||||
cc.close()
|
||||
}
|
||||
|
||||
r.shutdown()
|
||||
}
|
||||
|
||||
// ─── Status poller ────────────────────────────────────────────────────────────
|
||||
|
||||
// pollBookingStatus runs in a goroutine for the room's lifetime.
|
||||
// Every 5 s it reloads the booking; when it hits a terminal status it publishes
|
||||
// the NATS event, force-closes all connections, and exits.
|
||||
func (r *chatRoom) pollBookingStatus() {
|
||||
t := time.NewTicker(5 * time.Second)
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-r.closeCh:
|
||||
return
|
||||
|
||||
case <-t.C:
|
||||
var booking models.PickupBooking
|
||||
if err := db.DB.First(&booking, r.bookingID).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if isTerminalStatus(booking.Status) {
|
||||
utils.Info("WS/Chat: closing room — booking reached terminal status",
|
||||
"booking_id", r.bookingID,
|
||||
"status", booking.Status,
|
||||
)
|
||||
publishChatRoomClosed(r.bookingID)
|
||||
r.forceClose()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// publishChatRoomClosed publishes a NATS event so downstream services know the
|
||||
// chat session has ended. Non-fatal — logs a warning on failure.
|
||||
func publishChatRoomClosed(bookingID int) {
|
||||
if db.Js == nil {
|
||||
return
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"booking_id": bookingID,
|
||||
"closed_at": time.Now().UnixMilli(),
|
||||
}
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("chat.room.closed.%d", bookingID)
|
||||
if _, err := db.Js.Publish(subject, data); err != nil {
|
||||
utils.Warn("WS/Chat: failed to publish room-closed event",
|
||||
"booking_id", bookingID,
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
}
|
||||
215
internal/ws/tracking.go
Normal file
215
internal/ws/tracking.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"doormile/constants"
|
||||
"doormile/db"
|
||||
"doormile/models"
|
||||
"doormile/utils"
|
||||
|
||||
"github.com/gofiber/websocket/v2"
|
||||
)
|
||||
|
||||
// trackingFrame is the JSON payload pushed to the client every 2 seconds.
|
||||
type trackingFrame struct {
|
||||
Lat float64 `json:"lat"`
|
||||
Lon float64 `json:"lon"`
|
||||
EtaMinutes float64 `json:"eta_minutes"`
|
||||
Status string `json:"status"`
|
||||
MilerName string `json:"miler_name"`
|
||||
}
|
||||
|
||||
// TrackingHandler streams live miler location for a booking over WebSocket.
|
||||
//
|
||||
// Route: GET /ws/bookings/:bookingid/track (no auth — public tracking link)
|
||||
//
|
||||
// The handler:
|
||||
// 1. Validates the booking ID and loads the booking.
|
||||
// 2. Spawns a reader goroutine that closes `done` on client disconnect.
|
||||
// 3. Every 2 s: re-fetches booking status, reads miler GPS from Redis,
|
||||
// computes ETA (haversine miler→pickup, 20 km/h urban average), and
|
||||
// sends a JSON frame.
|
||||
// 4. Exits (closing the WS) when status is Picked_Up, Converted_To_Consignment,
|
||||
// or Cancelled, or when the client disconnects.
|
||||
func TrackingHandler(c *websocket.Conn) {
|
||||
bookingID, err := strconv.Atoi(c.Params("bookingid"))
|
||||
if err != nil {
|
||||
sendError(c, "invalid booking ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Initial booking load — fail fast if it doesn't exist.
|
||||
var booking models.PickupBooking
|
||||
if err := db.DB.First(&booking, bookingID).Error; err != nil {
|
||||
sendError(c, "booking not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Reader goroutine: detect client disconnect via any read error.
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
for {
|
||||
if _, _, err := c.ReadMessage(); err != nil {
|
||||
close(done)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Cache miler display names to avoid redundant DB hits each tick.
|
||||
nameCache := make(map[int]string)
|
||||
milerName := func(userID int) string {
|
||||
if n, ok := nameCache[userID]; ok {
|
||||
return n
|
||||
}
|
||||
var p models.MilerProfile
|
||||
if err := db.DB.Where("userid = ?", userID).First(&p).Error; err == nil {
|
||||
nameCache[userID] = p.Displayname
|
||||
return p.Displayname
|
||||
}
|
||||
return "Miler"
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
// Re-fetch booking on every tick so status changes are reflected.
|
||||
if err := db.DB.First(&booking, bookingID).Error; err != nil {
|
||||
utils.Warn("WS/Tracking: failed to reload booking", "booking_id", bookingID, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Terminal state — send one final frame then close.
|
||||
if isTerminalStatus(booking.Status) {
|
||||
frame := trackingFrame{Status: booking.Status}
|
||||
if booking.Assignedmileruserid != nil {
|
||||
frame.MilerName = milerName(*booking.Assignedmileruserid)
|
||||
}
|
||||
sendJSON(c, frame)
|
||||
return
|
||||
}
|
||||
|
||||
// No miler assigned yet — send status-only frame and keep waiting.
|
||||
if booking.Assignedmileruserid == nil {
|
||||
if err := sendJSON(c, trackingFrame{Status: booking.Status}); err != nil {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
if gpsOk {
|
||||
frame.Lat = lat
|
||||
frame.Lon = lon
|
||||
frame.EtaMinutes = haversineETA(
|
||||
lat, lon,
|
||||
booking.Pickuplatitude, booking.Pickuplongitude,
|
||||
)
|
||||
}
|
||||
|
||||
if err := sendJSON(c, frame); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
func isTerminalStatus(status string) bool {
|
||||
return status == constants.BookingPickedUp ||
|
||||
status == constants.BookingConvertedConsignment ||
|
||||
status == constants.BookingCancelled
|
||||
}
|
||||
|
||||
// readMilerGPS fetches the miler's last-known position from Redis.
|
||||
// The key "miler:gps:{id}" is written by UpdateMilerLocation as "{lat},{lon}".
|
||||
func readMilerGPS(milerUserID int) (lat, lon float64, ok bool) {
|
||||
if db.Rdb == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
val, err := db.Rdb.Get(ctx, fmt.Sprintf("miler:gps:%d", milerUserID)).Result()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.SplitN(strings.TrimSpace(val), ",", 2)
|
||||
if len(parts) != 2 {
|
||||
return
|
||||
}
|
||||
|
||||
lat, err = strconv.ParseFloat(strings.TrimSpace(parts[0]), 64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
lon, err = strconv.ParseFloat(strings.TrimSpace(parts[1]), 64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return lat, lon, true
|
||||
}
|
||||
|
||||
// haversineETA returns the estimated arrival time in minutes from (milerLat, milerLon)
|
||||
// to (destLat, destLon), assuming 20 km/h average urban speed plus a 2-minute buffer.
|
||||
func haversineETA(milerLat, milerLon, destLat, destLon float64) float64 {
|
||||
const earthRadiusKm = 6371.0
|
||||
dLat := (destLat - milerLat) * math.Pi / 180.0
|
||||
dLon := (destLon - milerLon) * math.Pi / 180.0
|
||||
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
|
||||
math.Cos(milerLat*math.Pi/180.0)*math.Cos(destLat*math.Pi/180.0)*
|
||||
math.Sin(dLon/2)*math.Sin(dLon/2)
|
||||
distKm := earthRadiusKm * 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
|
||||
return math.Round((distKm/20.0)*60.0 + 2.0)
|
||||
}
|
||||
|
||||
// sendJSON marshals v and writes it as a text WebSocket frame.
|
||||
// Returns a non-nil error only when the write fails (client gone).
|
||||
func sendJSON(c *websocket.Conn, v any) error {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil // marshal failure is a code bug, not a client issue
|
||||
}
|
||||
return c.WriteMessage(websocket.TextMessage, data)
|
||||
}
|
||||
|
||||
// sendError sends a single error frame and ignores the write result
|
||||
// (the handler is about to return regardless).
|
||||
func sendError(c *websocket.Conn, msg string) {
|
||||
data, _ := json.Marshal(map[string]string{"error": msg})
|
||||
c.WriteMessage(websocket.TextMessage, data) //nolint:errcheck
|
||||
}
|
||||
2
main.go
2
main.go
@@ -9,6 +9,7 @@ import (
|
||||
"doormile/config"
|
||||
"doormile/controllers"
|
||||
"doormile/db"
|
||||
"doormile/internal/notify"
|
||||
"doormile/internal/worker"
|
||||
"doormile/middlewares"
|
||||
"doormile/migrations"
|
||||
@@ -31,6 +32,7 @@ func main() {
|
||||
db.Connect(cfg)
|
||||
db.InitRedis(cfg)
|
||||
db.InitNATS(cfg)
|
||||
notify.InitFCM()
|
||||
|
||||
// 3. Run GORM migrations for logistics tables
|
||||
if db.DB != nil {
|
||||
|
||||
40
middlewares/city_gate.go
Normal file
40
middlewares/city_gate.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// operatingCityPrefixes maps supported 3-digit pincode prefixes to city names.
|
||||
var operatingCityPrefixes = map[string]string{
|
||||
"641": "Coimbatore",
|
||||
"600": "Chennai",
|
||||
"560": "Bengaluru",
|
||||
"500": "Hyderabad",
|
||||
}
|
||||
|
||||
// CityGateMiddleware rejects bookings from pincodes outside Doormile's operating cities.
|
||||
// It reads pickuppincode from the JSON body without consuming it, so the downstream
|
||||
// controller can still call c.BodyParser() as usual.
|
||||
func CityGateMiddleware(c *fiber.Ctx) error {
|
||||
var body struct {
|
||||
Pickuppincode string `json:"pickuppincode"`
|
||||
}
|
||||
if err := json.Unmarshal(c.Body(), &body); err != nil || body.Pickuppincode == "" {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
pincode := strings.TrimSpace(body.Pickuppincode)
|
||||
if len(pincode) >= 3 {
|
||||
if _, ok := operatingCityPrefixes[pincode[:3]]; ok {
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "We are not yet operating in your city. Stay tuned!",
|
||||
"code": "CITY_NOT_SUPPORTED",
|
||||
})
|
||||
}
|
||||
21
middlewares/internal_auth.go
Normal file
21
middlewares/internal_auth.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// InternalKeyAuth guards machine-to-machine endpoints with a static API key.
|
||||
// The key is read from INTERNAL_API_KEY env var at request time so it can be
|
||||
// rotated without redeployment. Returns 401 if the header is missing or wrong.
|
||||
func InternalKeyAuth(c *fiber.Ctx) error {
|
||||
expected := os.Getenv("INTERNAL_API_KEY")
|
||||
if expected == "" || c.Get("X-Internal-Key") != expected {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"message": "unauthorized",
|
||||
})
|
||||
}
|
||||
return c.Next()
|
||||
}
|
||||
36
middlewares/ws_auth.go
Normal file
36
middlewares/ws_auth.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"doormile/config"
|
||||
"doormile/utils"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// WsChatAuth validates a JWT passed as a ?token= query parameter.
|
||||
//
|
||||
// WebSocket clients (both browsers and Flutter) cannot set the Authorization
|
||||
// header during the initial HTTP upgrade handshake, so the token is sent as a
|
||||
// query parameter instead. Fiber locals set here are forwarded into the
|
||||
// WebSocket handler via the gofiber/websocket package.
|
||||
func WsChatAuth(cfg *config.Config) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
token := c.Query("token")
|
||||
if token == "" {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "token query parameter is required",
|
||||
})
|
||||
}
|
||||
|
||||
claims, err := utils.ParseToken(token, cfg.JWTSecret)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "invalid or expired token",
|
||||
})
|
||||
}
|
||||
|
||||
c.Locals("userid", claims.UserID)
|
||||
c.Locals("roleid", claims.RoleID)
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ func Migrate(db *gorm.DB) error {
|
||||
&models.CompetitorBranch{},
|
||||
&models.CarrierPricing{},
|
||||
&models.DoormilePricing{},
|
||||
&models.AgentDecision{},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@@ -48,5 +49,18 @@ func Migrate(db *gorm.DB) error {
|
||||
}
|
||||
|
||||
utils.Info("✅ Database migration completed successfully!")
|
||||
|
||||
if res := db.Exec(`ALTER TABLE agent_decisions ADD COLUMN IF NOT EXISTS context_embedding vector(1536)`); res.Error != nil {
|
||||
utils.Error("❌ Failed to add context_embedding column", "error", res.Error)
|
||||
} else {
|
||||
utils.Info("✅ context_embedding vector column ready")
|
||||
}
|
||||
|
||||
if res := db.Exec(`CREATE INDEX IF NOT EXISTS idx_agent_decisions_embedding ON agent_decisions USING ivfflat (context_embedding vector_cosine_ops) WITH (lists = 100)`); res.Error != nil {
|
||||
utils.Error("❌ Failed to create ivfflat index on context_embedding", "error", res.Error)
|
||||
} else {
|
||||
utils.Info("✅ ivfflat index on context_embedding ready")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
15
models/agentdecision.go
Normal file
15
models/agentdecision.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type AgentDecision struct {
|
||||
ID uint64 `gorm:"primaryKey"`
|
||||
DecisionType string `gorm:"size:50;index"`
|
||||
BookingID *uint64 `gorm:"index"`
|
||||
Context string `gorm:"type:jsonb"`
|
||||
Decision string `gorm:"type:jsonb"`
|
||||
Reasoning string `gorm:"type:text"`
|
||||
Outcome *string `gorm:"size:30;index"`
|
||||
OutcomeRecordedAt *time.Time
|
||||
CreatedAt time.Time
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -16,6 +16,8 @@ type DoormilePricing struct {
|
||||
Minprice float64 `json:"min_price" gorm:"column:min_price;not null"`
|
||||
Maxprice float64 `json:"max_price" gorm:"column:max_price;not null"`
|
||||
Currency string `json:"currency" gorm:"column:currency;default:INR"`
|
||||
Providercompany string `json:"provider_company" gorm:"column:provider_company;default:Doormile"`
|
||||
Reliabilityscore float64 `json:"reliability_score" gorm:"column:reliability_score;default:5.0"`
|
||||
Status string `json:"status" gorm:"column:status;default:Active"`
|
||||
Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"`
|
||||
Updatedat time.Time `json:"updatedat" gorm:"column:updatedat;default:CURRENT_TIMESTAMP"`
|
||||
|
||||
@@ -83,24 +83,25 @@ func (AppUser) TableName() string {
|
||||
}
|
||||
|
||||
type MilerProfile struct {
|
||||
Milerprofileid int `json:"milerprofileid" gorm:"primaryKey;column:milerprofileid"`
|
||||
Userid int `json:"userid" gorm:"column:userid;unique;not null"`
|
||||
Applocationid int `json:"applocationid" gorm:"column:applocationid;default:1"`
|
||||
Displayname string `json:"displayname" gorm:"column:displayname;not null"`
|
||||
Phone string `json:"phone" gorm:"column:phone;not null"`
|
||||
Profilephotourl string `json:"profilephotourl" gorm:"column:profilephotourl"`
|
||||
Vehicleid *int `json:"vehicleid" gorm:"column:vehicleid"`
|
||||
Defaultvehicletype string `json:"defaultvehicletype" gorm:"column:defaultvehicletype"`
|
||||
Currentlatitude float64 `json:"currentlatitude" gorm:"column:currentlatitude"`
|
||||
Currentlongitude float64 `json:"currentlongitude" gorm:"column:currentlongitude"`
|
||||
Currentpincode string `json:"currentpincode" gorm:"column:currentpincode"`
|
||||
Availabilitystatus string `json:"availabilitystatus" gorm:"column:availabilitystatus;default:Offline"` // Offline, Available, Assigned, On_Pickup, At_Customer, Picked_Up, On_Delivery, Break, Blocked
|
||||
Rating float64 `json:"rating" gorm:"column:rating;default:5.00"`
|
||||
Totalcompletedpickups int `json:"totalcompletedpickups" gorm:"column:totalcompletedpickups;default:0"`
|
||||
Totalcancelledpickups int `json:"totalcancelledpickups" gorm:"column:totalcancelledpickups;default:0"`
|
||||
Lastlocationupdatedat *time.Time `json:"lastlocationupdatedat" gorm:"column:lastlocationupdatedat"`
|
||||
Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"`
|
||||
Updatedat time.Time `json:"updatedat" gorm:"column:updatedat;default:CURRENT_TIMESTAMP"`
|
||||
Milerprofileid int `json:"milerprofileid" gorm:"primaryKey;column:milerprofileid"`
|
||||
Userid int `json:"userid" gorm:"column:userid;unique;not null"`
|
||||
Applocationid int `json:"applocationid" gorm:"column:applocationid;default:1"`
|
||||
Displayname string `json:"displayname" gorm:"column:displayname;not null"`
|
||||
Phone string `json:"phone" gorm:"column:phone;not null"`
|
||||
Profilephotourl string `json:"profilephotourl" gorm:"column:profilephotourl"`
|
||||
Vehicleid *int `json:"vehicleid" gorm:"column:vehicleid"`
|
||||
Defaultvehicletype string `json:"defaultvehicletype" gorm:"column:defaultvehicletype"`
|
||||
Currentlatitude float64 `json:"currentlatitude" gorm:"column:currentlatitude"`
|
||||
Currentlongitude float64 `json:"currentlongitude" gorm:"column:currentlongitude"`
|
||||
Currentpincode string `json:"currentpincode" gorm:"column:currentpincode"`
|
||||
Availabilitystatus string `json:"availabilitystatus" gorm:"column:availabilitystatus;default:Offline"` // Offline, Available, Assigned, On_Pickup, At_Customer, Picked_Up, On_Delivery, Break, Blocked
|
||||
Rating float64 `json:"rating" gorm:"column:rating;default:5.00"`
|
||||
Totalcompletedpickups int `json:"totalcompletedpickups" gorm:"column:totalcompletedpickups;default:0"`
|
||||
Totalcancelledpickups int `json:"totalcancelledpickups" gorm:"column:totalcancelledpickups;default:0"`
|
||||
Devicetoken string `json:"device_token,omitempty" gorm:"column:device_token"`
|
||||
Lastlocationupdatedat *time.Time `json:"lastlocationupdatedat" gorm:"column:lastlocationupdatedat"`
|
||||
Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"`
|
||||
Updatedat time.Time `json:"updatedat" gorm:"column:updatedat;default:CURRENT_TIMESTAMP"`
|
||||
}
|
||||
|
||||
func (MilerProfile) TableName() string {
|
||||
@@ -117,6 +118,7 @@ type AppCustomer struct {
|
||||
Defaultlatitude float64 `json:"defaultlatitude" gorm:"column:defaultlatitude"`
|
||||
Defaultlongitude float64 `json:"defaultlongitude" gorm:"column:defaultlongitude"`
|
||||
Defaultpincode string `json:"defaultpincode" gorm:"column:defaultpincode"`
|
||||
Devicetoken string `json:"device_token,omitempty" gorm:"column:device_token"`
|
||||
Status string `json:"status" gorm:"column:status;default:Active"` // Active, Blocked, Deleted
|
||||
Configid int `json:"configid" gorm:"column:configid;default:1"`
|
||||
Lastloginat *time.Time `json:"lastloginat" gorm:"column:lastloginat"`
|
||||
|
||||
@@ -7,9 +7,11 @@ import (
|
||||
"doormile/config"
|
||||
"doormile/controllers"
|
||||
"doormile/db"
|
||||
"doormile/internal/ws"
|
||||
"doormile/middlewares"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/websocket/v2"
|
||||
)
|
||||
|
||||
func RegisterRoutes(app *fiber.App, cfg *config.Config) {
|
||||
@@ -73,7 +75,9 @@ func RegisterRoutes(app *fiber.App, cfg *config.Config) {
|
||||
customerAuth.Put("/locations/:id", controllers.UpdateCustomerLocation)
|
||||
customerAuth.Delete("/locations/:id", controllers.DeleteCustomerLocation)
|
||||
|
||||
customerAuth.Post("/bookings", controllers.CreateCustomerBooking)
|
||||
customerAuth.Put("/device-token", controllers.SaveCustomerDeviceToken)
|
||||
|
||||
customerAuth.Post("/bookings", middlewares.CityGateMiddleware, controllers.CreateCustomerBooking)
|
||||
customerAuth.Get("/bookings", controllers.GetCustomerBookings)
|
||||
customerAuth.Get("/bookings/:bookingid", controllers.GetCustomerBookingDetails)
|
||||
customerAuth.Post("/bookings/:bookingid/cancel", controllers.CancelCustomerBooking)
|
||||
@@ -92,6 +96,8 @@ func RegisterRoutes(app *fiber.App, cfg *config.Config) {
|
||||
milerAuth.Get("/profile", controllers.GetMilerProfile)
|
||||
milerAuth.Put("/profile", controllers.UpdateMilerProfile)
|
||||
|
||||
milerAuth.Put("/device-token", controllers.SaveMilerDeviceToken)
|
||||
|
||||
milerAuth.Put("/location", controllers.UpdateMilerLocation)
|
||||
milerAuth.Put("/availability", controllers.UpdateMilerAvailability)
|
||||
|
||||
@@ -178,7 +184,7 @@ func RegisterRoutes(app *fiber.App, cfg *config.Config) {
|
||||
|
||||
// Bookings
|
||||
adminAuth.Get("/bookings", controllers.GetAdminBookings)
|
||||
adminAuth.Post("/crmbooking", controllers.CreateCRMBooking)
|
||||
adminAuth.Post("/crmbooking", middlewares.CityGateMiddleware, controllers.CreateCRMBooking)
|
||||
adminAuth.Get("/bookings/:id", controllers.GetAdminBookingDetails)
|
||||
adminAuth.Post("/bookings/:id/assign-miler", controllers.AdminAssignMiler)
|
||||
adminAuth.Post("/bookings/:id/assign-vehicle", controllers.AdminAssignVehicle)
|
||||
@@ -261,4 +267,26 @@ func RegisterRoutes(app *fiber.App, cfg *config.Config) {
|
||||
crm.Get("/clients/:id", controllers.GetClientDetails)
|
||||
crm.Put("/clients/:id", controllers.UpdateClient)
|
||||
crm.Delete("/clients/:id", controllers.DeleteClient)
|
||||
|
||||
// --------------------
|
||||
// INTERNAL — machine-to-machine endpoints (API key auth, no JWT)
|
||||
// --------------------
|
||||
internal := api.Group("/internal", middlewares.InternalKeyAuth)
|
||||
internal.Post("/notify", controllers.InternalNotify)
|
||||
internal.Post("/bookings/:id/reassign", controllers.InternalReassign)
|
||||
internal.Post("/agent-decisions", controllers.CreateAgentDecision)
|
||||
internal.Get("/agent-decisions/similar", controllers.FindSimilarDecisions)
|
||||
internal.Patch("/agent-decisions/:id/outcome", controllers.UpdateDecisionOutcome)
|
||||
|
||||
// --------------------
|
||||
// WEBSOCKET — live miler tracking (no auth, public tracking link)
|
||||
// --------------------
|
||||
app.Use("/ws", func(c *fiber.Ctx) error {
|
||||
if websocket.IsWebSocketUpgrade(c) {
|
||||
return c.Next()
|
||||
}
|
||||
return fiber.ErrUpgradeRequired
|
||||
})
|
||||
app.Get("/ws/bookings/:bookingid/track", websocket.New(ws.TrackingHandler))
|
||||
app.Get("/ws/bookings/:bookingid/chat", middlewares.WsChatAuth(cfg), websocket.New(ws.ChatHandler))
|
||||
}
|
||||
|
||||
5
scratch/icon_class.svg
Normal file
5
scratch/icon_class.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="3" y1="9" x2="21" y2="9"></line>
|
||||
<line x1="9" y1="21" x2="9" y2="9"></line>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 316 B |
3
scratch/icon_data.svg
Normal file
3
scratch/icon_data.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 236 B |
5
scratch/icon_decision.svg
Normal file
5
scratch/icon_decision.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<path d="M12 16v-4"></path>
|
||||
<path d="M12 8h.01"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 264 B |
141
scratch/pptx_output.txt
Normal file
141
scratch/pptx_output.txt
Normal file
@@ -0,0 +1,141 @@
|
||||
--- SLIDE 2 ---
|
||||
Shape 0: AUTO_SHAPE (1) | Text 0 | has_text_frame: True
|
||||
Text: '02 / MISSION'
|
||||
Shape 1: AUTO_SHAPE (1) | Text 1 | has_text_frame: True
|
||||
Text: 'About DoorMile'
|
||||
Shape 2: AUTO_SHAPE (1) | Text 2 | has_text_frame: True
|
||||
Text: 'Pioneering the next evolution of logistics through precision engineering and sustainable innovation.'
|
||||
Shape 3: AUTO_SHAPE (1) | Shape 3 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 4: AUTO_SHAPE (1) | Shape 4 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 5: PICTURE (13) | Image 0 | has_text_frame: False
|
||||
Picture found
|
||||
Shape 6: AUTO_SHAPE (1) | Text 5 | has_text_frame: True
|
||||
Text: 'First Mile Delivery'
|
||||
Shape 7: AUTO_SHAPE (1) | Text 6 | has_text_frame: True
|
||||
Text: 'Seamless integration from manufacturing to hub.'
|
||||
Shape 8: AUTO_SHAPE (1) | Shape 7 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 9: AUTO_SHAPE (1) | Shape 8 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 10: PICTURE (13) | Image 1 | has_text_frame: False
|
||||
Picture found
|
||||
Shape 11: AUTO_SHAPE (1) | Text 9 | has_text_frame: True
|
||||
Text: 'Mid Mile Transportation'
|
||||
Shape 12: AUTO_SHAPE (1) | Text 10 | has_text_frame: True
|
||||
Text: 'Optimized transit between strategic distribution nodes.'
|
||||
Shape 13: AUTO_SHAPE (1) | Shape 11 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 14: AUTO_SHAPE (1) | Shape 12 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 15: PICTURE (13) | Image 2 | has_text_frame: False
|
||||
Picture found
|
||||
Shape 16: AUTO_SHAPE (1) | Text 13 | has_text_frame: True
|
||||
Text: 'Last Mile Delivery'
|
||||
Shape 17: AUTO_SHAPE (1) | Text 14 | has_text_frame: True
|
||||
Text: 'Hyper-local fulfillment with surgical precision.'
|
||||
Shape 18: AUTO_SHAPE (1) | Shape 15 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 19: AUTO_SHAPE (1) | Shape 16 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 20: PICTURE (13) | Image 3 | has_text_frame: False
|
||||
Picture found
|
||||
Shape 21: AUTO_SHAPE (1) | Text 17 | has_text_frame: True
|
||||
Text: 'EV Native Logistics'
|
||||
Shape 22: AUTO_SHAPE (1) | Text 18 | has_text_frame: True
|
||||
Text: 'Zero-emission fleet operations by design.'
|
||||
Shape 23: AUTO_SHAPE (1) | Shape 19 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 24: AUTO_SHAPE (1) | Shape 20 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 25: PICTURE (13) | Image 4 | has_text_frame: False
|
||||
Picture found
|
||||
Shape 26: AUTO_SHAPE (1) | Text 21 | has_text_frame: True
|
||||
Text: 'AI Driven Operations'
|
||||
Shape 27: AUTO_SHAPE (1) | Text 22 | has_text_frame: True
|
||||
Text: 'Self-healing supply chains powered by neural networks.'
|
||||
Shape 28: AUTO_SHAPE (1) | Shape 23 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 29: AUTO_SHAPE (1) | Text 24 | has_text_frame: True
|
||||
Text: 'Goal: Create an intelligent and efficient logistics ecosystem.'
|
||||
Shape 30: PICTURE (13) | Image 5 | has_text_frame: False
|
||||
Picture found
|
||||
Shape 31: AUTO_SHAPE (1) | Shape 25 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 32: AUTO_SHAPE (1) | Shape 26 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 33: AUTO_SHAPE (1) | Text 27 | has_text_frame: True
|
||||
Text: '● System Operational'
|
||||
Shape 34: AUTO_SHAPE (1) | Text 28 | has_text_frame: True
|
||||
Text: 'Visualizing the hyper-connected flow of our Supply Chain.'
|
||||
Shape 35: AUTO_SHAPE (1) | Shape 29 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 36: AUTO_SHAPE (1) | Text 30 | has_text_frame: True
|
||||
Text: 'EFFICIENCY +42%'
|
||||
Shape 37: AUTO_SHAPE (1) | Text 31 | has_text_frame: True
|
||||
Text: '02/10'
|
||||
|
||||
--- SLIDE 7 ---
|
||||
Shape 0: AUTO_SHAPE (1) | Text 0 | has_text_frame: True
|
||||
Text: '07 / INNOVATION FRAMEWORK'
|
||||
Shape 1: AUTO_SHAPE (1) | Text 1 | has_text_frame: True
|
||||
Text: 'AI Pipeline I Will Work On'
|
||||
Shape 2: AUTO_SHAPE (1) | Text 2 | has_text_frame: True
|
||||
Text: 'End-to-end pipeline transforming unstructured global shipping data into actionable real-time decisio'
|
||||
Shape 3: PICTURE (13) | Image 0 | has_text_frame: False
|
||||
Picture found
|
||||
Shape 4: AUTO_SHAPE (1) | Shape 3 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 5: AUTO_SHAPE (1) | Shape 4 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 6: AUTO_SHAPE (1) | Text 5 | has_text_frame: True
|
||||
Text: 'Classification Algorithms'
|
||||
Shape 7: AUTO_SHAPE (1) | Text 6 | has_text_frame: True
|
||||
Text: 'Random Forests, Gradient Boosted Trees, and Neural Networks to categorize delivery status and risk p'
|
||||
Shape 8: AUTO_SHAPE (1) | Shape 7 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 9: AUTO_SHAPE (1) | Text 8 | has_text_frame: True
|
||||
Text: 'TENSORFLOW'
|
||||
Shape 10: AUTO_SHAPE (1) | Shape 9 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 11: AUTO_SHAPE (1) | Text 10 | has_text_frame: True
|
||||
Text: 'SCIKIT-LEARN'
|
||||
Shape 12: AUTO_SHAPE (1) | Shape 11 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 13: AUTO_SHAPE (1) | Shape 12 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 14: AUTO_SHAPE (1) | Text 13 | has_text_frame: True
|
||||
Text: 'Data Preprocessing'
|
||||
Shape 15: AUTO_SHAPE (1) | Text 14 | has_text_frame: True
|
||||
Text: 'Automated cleaning cycles handling outlier detection, missing value imputation, and noise filtering '
|
||||
Shape 16: AUTO_SHAPE (1) | Shape 15 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 17: AUTO_SHAPE (1) | Text 16 | has_text_frame: True
|
||||
Text: 'PANDAS'
|
||||
Shape 18: AUTO_SHAPE (1) | Shape 17 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 19: AUTO_SHAPE (1) | Text 18 | has_text_frame: True
|
||||
Text: 'NUMPY'
|
||||
Shape 20: AUTO_SHAPE (1) | Shape 19 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 21: AUTO_SHAPE (1) | Shape 20 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 22: AUTO_SHAPE (1) | Text 21 | has_text_frame: True
|
||||
Text: 'Decision Intelligence'
|
||||
Shape 23: AUTO_SHAPE (1) | Text 22 | has_text_frame: True
|
||||
Text: 'Translating model outputs into prescriptive actions, optimizing route efficiency and predictive main'
|
||||
Shape 24: AUTO_SHAPE (1) | Shape 23 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 25: AUTO_SHAPE (1) | Text 24 | has_text_frame: True
|
||||
Text: 'PYTORCH'
|
||||
Shape 26: AUTO_SHAPE (1) | Shape 25 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 27: AUTO_SHAPE (1) | Text 26 | has_text_frame: True
|
||||
Text: 'FAISS'
|
||||
Shape 28: AUTO_SHAPE (1) | Shape 27 | has_text_frame: True
|
||||
Text: ''
|
||||
Shape 29: AUTO_SHAPE (1) | Text 28 | has_text_frame: True
|
||||
Text: '● LIVE TELEMETRY — Analyzing over 4.2M data points daily across our global distribution network. '
|
||||
Shape 30: AUTO_SHAPE (1) | Text 29 | has_text_frame: True
|
||||
Text: '07/10'
|
||||
102
scratch/pptx_output_boxes.txt
Normal file
102
scratch/pptx_output_boxes.txt
Normal file
@@ -0,0 +1,102 @@
|
||||
--- SLIDE 2 ---
|
||||
Shape 0: AUTO_SHAPE (1) | Text 0 | L:365760, T:182880, W:2743200, H:201168
|
||||
Shape 1: AUTO_SHAPE (1) | Text 1 | L:365760, T:384048, W:5029200, H:502920
|
||||
Shape 2: AUTO_SHAPE (1) | Text 2 | L:365760, T:896112, W:4937760, H:384048
|
||||
Shape 3: AUTO_SHAPE (1) | Shape 3 | L:365760, T:1371600, W:4937760, H:585216
|
||||
Shape 4: AUTO_SHAPE (1) | Shape 4 | L:365760, T:1371600, W:475488, H:585216
|
||||
Shape 5: PICTURE (13) | Image 0 | L:420624, T:1490472, W:347472, H:347472
|
||||
Shape 6: AUTO_SHAPE (1) | Text 5 | L:969264, T:1444752, W:4206240, H:219456
|
||||
Shape 7: AUTO_SHAPE (1) | Text 6 | L:969264, T:1664208, W:4206240, H:219456
|
||||
Shape 8: AUTO_SHAPE (1) | Shape 7 | L:365760, T:2029968, W:4937760, H:585216
|
||||
Shape 9: AUTO_SHAPE (1) | Shape 8 | L:365760, T:2029968, W:475488, H:585216
|
||||
Shape 10: PICTURE (13) | Image 1 | L:420624, T:2148840, W:347472, H:347472
|
||||
Shape 11: AUTO_SHAPE (1) | Text 9 | L:969264, T:2103120, W:4206240, H:219456
|
||||
Shape 12: AUTO_SHAPE (1) | Text 10 | L:969264, T:2322576, W:4206240, H:219456
|
||||
Shape 13: AUTO_SHAPE (1) | Shape 11 | L:365760, T:2688336, W:4937760, H:585216
|
||||
Shape 14: AUTO_SHAPE (1) | Shape 12 | L:365760, T:2688336, W:475488, H:585216
|
||||
Shape 15: PICTURE (13) | Image 2 | L:420624, T:2807208, W:347472, H:347472
|
||||
Shape 16: AUTO_SHAPE (1) | Text 13 | L:969264, T:2761488, W:4206240, H:219456
|
||||
Shape 17: AUTO_SHAPE (1) | Text 14 | L:969264, T:2980944, W:4206240, H:219456
|
||||
Shape 18: AUTO_SHAPE (1) | Shape 15 | L:365760, T:3346704, W:4937760, H:585216
|
||||
Shape 19: AUTO_SHAPE (1) | Shape 16 | L:365760, T:3346704, W:475488, H:585216
|
||||
Shape 20: PICTURE (13) | Image 3 | L:420624, T:3465576, W:347472, H:347472
|
||||
Shape 21: AUTO_SHAPE (1) | Text 17 | L:969264, T:3419856, W:4206240, H:219456
|
||||
Shape 22: AUTO_SHAPE (1) | Text 18 | L:969264, T:3639312, W:4206240, H:219456
|
||||
Shape 23: AUTO_SHAPE (1) | Shape 19 | L:365760, T:4005072, W:4937760, H:585216
|
||||
Shape 24: AUTO_SHAPE (1) | Shape 20 | L:365760, T:4005072, W:475488, H:585216
|
||||
Shape 25: PICTURE (13) | Image 4 | L:420624, T:4123944, W:347472, H:347472
|
||||
Shape 26: AUTO_SHAPE (1) | Text 21 | L:969264, T:4078224, W:4206240, H:219456
|
||||
Shape 27: AUTO_SHAPE (1) | Text 22 | L:969264, T:4297680, W:4206240, H:219456
|
||||
Shape 28: AUTO_SHAPE (1) | Shape 23 | L:365760, T:4709160, W:4937760, H:274320
|
||||
Shape 29: AUTO_SHAPE (1) | Text 24 | L:365760, T:4709160, W:4937760, H:274320
|
||||
Shape 30: PICTURE (13) | Image 5 | L:5532120, T:274320, W:3383280, H:4206240
|
||||
Shape 31: AUTO_SHAPE (1) | Shape 25 | L:5532120, T:274320, W:3383280, H:4206240
|
||||
Shape 32: AUTO_SHAPE (1) | Shape 26 | L:5623560, T:3657600, W:3200400, H:685800
|
||||
Shape 33: AUTO_SHAPE (1) | Text 27 | L:5715000, T:3703320, W:3017520, H:201168
|
||||
Shape 34: AUTO_SHAPE (1) | Text 28 | L:5715000, T:3904488, W:3017520, H:347472
|
||||
Shape 35: AUTO_SHAPE (1) | Shape 29 | L:8138160, T:274320, W:777240, H:347472
|
||||
Shape 36: AUTO_SHAPE (1) | Text 30 | L:8138160, T:274320, W:777240, H:347472
|
||||
Shape 37: AUTO_SHAPE (1) | Text 31 | L:8549640, T:4937760, W:548640, H:164592
|
||||
|
||||
--- SLIDE 7 ---
|
||||
Shape 0: AUTO_SHAPE (1) | Text 0 | L:320040, T:164592, W:4572000, H:201168
|
||||
Text: '07 / INNOVATION FRAMEWORK'
|
||||
Shape 1: AUTO_SHAPE (1) | Text 1 | L:320040, T:347472, W:8503920, H:475488
|
||||
Text: 'AI Pipeline I Will Work On'
|
||||
Shape 2: AUTO_SHAPE (1) | Text 2 | L:320040, T:804672, W:8229600, H:274320
|
||||
Text: 'End-to-end pipeline transforming unstructured global shipping data into actionable real-time decisio'
|
||||
Shape 3: PICTURE (13) | Image 0 | L:320040, T:1188720, W:8503920, H:1143000
|
||||
Shape 4: AUTO_SHAPE (1) | Shape 3 | L:320040, T:2487168, W:2816352, H:1828800
|
||||
Text: ''
|
||||
Shape 5: AUTO_SHAPE (1) | Shape 4 | L:457200, T:2578608, W:228600, H:228600
|
||||
Text: ''
|
||||
Shape 6: AUTO_SHAPE (1) | Text 5 | L:777240, T:2542032, W:2240280, H:329184
|
||||
Text: 'Classification Algorithms'
|
||||
Shape 7: AUTO_SHAPE (1) | Text 6 | L:457200, T:2926080, W:2578608, H:822960
|
||||
Text: 'Random Forests, Gradient Boosted Trees, and Neural Networks to categorize delivery status and risk p'
|
||||
Shape 8: AUTO_SHAPE (1) | Shape 7 | L:457200, T:3840480, W:1188720, H:274320
|
||||
Text: ''
|
||||
Shape 9: AUTO_SHAPE (1) | Text 8 | L:457200, T:3840480, W:1188720, H:274320
|
||||
Text: 'TENSORFLOW'
|
||||
Shape 10: AUTO_SHAPE (1) | Shape 9 | L:1737360, T:3840480, W:1188720, H:274320
|
||||
Text: ''
|
||||
Shape 11: AUTO_SHAPE (1) | Text 10 | L:1737360, T:3840480, W:1188720, H:274320
|
||||
Text: 'SCIKIT-LEARN'
|
||||
Shape 12: AUTO_SHAPE (1) | Shape 11 | L:3264408, T:2487168, W:2816352, H:1828800
|
||||
Text: ''
|
||||
Shape 13: AUTO_SHAPE (1) | Shape 12 | L:3401568, T:2578608, W:228600, H:228600
|
||||
Text: ''
|
||||
Shape 14: AUTO_SHAPE (1) | Text 13 | L:3721608, T:2542032, W:2240280, H:329184
|
||||
Text: 'Data Preprocessing'
|
||||
Shape 15: AUTO_SHAPE (1) | Text 14 | L:3401568, T:2926080, W:2578608, H:822960
|
||||
Text: 'Automated cleaning cycles handling outlier detection, missing value imputation, and noise filtering '
|
||||
Shape 16: AUTO_SHAPE (1) | Shape 15 | L:3401568, T:3840480, W:1188720, H:274320
|
||||
Text: ''
|
||||
Shape 17: AUTO_SHAPE (1) | Text 16 | L:3401568, T:3840480, W:1188720, H:274320
|
||||
Text: 'PANDAS'
|
||||
Shape 18: AUTO_SHAPE (1) | Shape 17 | L:4681728, T:3840480, W:1188720, H:274320
|
||||
Text: ''
|
||||
Shape 19: AUTO_SHAPE (1) | Text 18 | L:4681728, T:3840480, W:1188720, H:274320
|
||||
Text: 'NUMPY'
|
||||
Shape 20: AUTO_SHAPE (1) | Shape 19 | L:6208776, T:2487168, W:2816352, H:1828800
|
||||
Text: ''
|
||||
Shape 21: AUTO_SHAPE (1) | Shape 20 | L:6345936, T:2578608, W:228600, H:228600
|
||||
Text: ''
|
||||
Shape 22: AUTO_SHAPE (1) | Text 21 | L:6665976, T:2542032, W:2240280, H:329184
|
||||
Text: 'Decision Intelligence'
|
||||
Shape 23: AUTO_SHAPE (1) | Text 22 | L:6345936, T:2926080, W:2578608, H:822960
|
||||
Text: 'Translating model outputs into prescriptive actions, optimizing route efficiency and predictive main'
|
||||
Shape 24: AUTO_SHAPE (1) | Shape 23 | L:6345936, T:3840480, W:1188720, H:274320
|
||||
Text: ''
|
||||
Shape 25: AUTO_SHAPE (1) | Text 24 | L:6345936, T:3840480, W:1188720, H:274320
|
||||
Text: 'PYTORCH'
|
||||
Shape 26: AUTO_SHAPE (1) | Shape 25 | L:7626096, T:3840480, W:1188720, H:274320
|
||||
Text: ''
|
||||
Shape 27: AUTO_SHAPE (1) | Text 26 | L:7626096, T:3840480, W:1188720, H:274320
|
||||
Text: 'FAISS'
|
||||
Shape 28: AUTO_SHAPE (1) | Shape 27 | L:320040, T:4462272, W:8503920, H:502920
|
||||
Text: ''
|
||||
Shape 29: AUTO_SHAPE (1) | Text 28 | L:457200, T:4462272, W:8321040, H:502920
|
||||
Text: '● LIVE TELEMETRY — Analyzing over 4.2M data points daily across our global distribution network. '
|
||||
Shape 30: AUTO_SHAPE (1) | Text 29 | L:8549640, T:4956048, W:548640, H:164592
|
||||
Text: '07/10'
|
||||
367
seed_data.sql
Normal file
367
seed_data.sql
Normal file
@@ -0,0 +1,367 @@
|
||||
-- ============================================================
|
||||
-- Doormile Seed Data — 4 Operating Zones
|
||||
-- Idempotent: safe to re-run (uses WHERE NOT EXISTS guards)
|
||||
-- ============================================================
|
||||
|
||||
-- ── STEP 0A: Fix sequences that drifted from explicit-ID inserts ──────────
|
||||
SELECT setval('applocations_applocationid_seq',
|
||||
GREATEST((SELECT COALESCE(MAX(applocationid), 0) FROM applocations), 1), true);
|
||||
|
||||
SELECT setval('tenants_tenantid_seq',
|
||||
GREATEST((SELECT COALESCE(MAX(tenantid), 0) FROM tenants), 1), true);
|
||||
|
||||
SELECT setval('hubs_hubid_seq',
|
||||
GREATEST((SELECT COALESCE(MAX(hubid), 0) FROM hubs), 1), true);
|
||||
|
||||
SELECT setval('appusers_userid_seq',
|
||||
GREATEST((SELECT COALESCE(MAX(userid), 0) FROM appusers), 1), true);
|
||||
|
||||
SELECT setval('milerprofiles_milerprofileid_seq',
|
||||
GREATEST((SELECT COALESCE(MAX(milerprofileid), 0) FROM milerprofiles), 1), true);
|
||||
|
||||
SELECT setval('tenantlocations_tenantlocationid_seq',
|
||||
GREATEST((SELECT COALESCE(MAX(tenantlocationid), 0) FROM tenantlocations), 1), true);
|
||||
|
||||
-- ── STEP 0B: Add columns the Go backend expects but DB is missing ─────────
|
||||
ALTER TABLE doormile_pricing
|
||||
ADD COLUMN IF NOT EXISTS provider_company text DEFAULT 'Doormile';
|
||||
|
||||
ALTER TABLE doormile_pricing
|
||||
ADD COLUMN IF NOT EXISTS reliability_score numeric DEFAULT 5.0;
|
||||
|
||||
ALTER TABLE milerprofiles
|
||||
ADD COLUMN IF NOT EXISTS device_token text;
|
||||
|
||||
ALTER TABLE appcustomers
|
||||
ADD COLUMN IF NOT EXISTS device_token text;
|
||||
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
-- DATA INSERTS — all guarded with WHERE NOT EXISTS
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
BEGIN;
|
||||
|
||||
-- ── SECTION 1: APPLOCATION — Chennai ─────────────────────────────────────
|
||||
INSERT INTO applocations (applocationname, status)
|
||||
SELECT 'Chennai', 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM applocations WHERE applocationname = 'Chennai');
|
||||
|
||||
-- ── SECTION 2: TENANTS ───────────────────────────────────────────────────
|
||||
|
||||
-- Chennai operational tenant
|
||||
INSERT INTO tenants (tenantname, primaryemail, primarycontact, status)
|
||||
SELECT 'Doormile Chennai Logistics', 'chn-ops@doormile.com', '9876543215', 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM tenants WHERE tenantname = 'Doormile Chennai Logistics');
|
||||
|
||||
-- Provider/partner tenants (2 per zone, 8 total)
|
||||
INSERT INTO tenants (tenantname, primaryemail, primarycontact, status)
|
||||
SELECT 'KPM Travels', 'ops@kpmtravels.in', '9876500100', 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM tenants WHERE tenantname = 'KPM Travels');
|
||||
|
||||
INSERT INTO tenants (tenantname, primaryemail, primarycontact, status)
|
||||
SELECT 'Sri Murugan Logistics', 'ops@srimuruganlogistics.in', '9876500101', 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM tenants WHERE tenantname = 'Sri Murugan Logistics');
|
||||
|
||||
INSERT INTO tenants (tenantname, primaryemail, primarycontact, status)
|
||||
SELECT 'Namma Logistics', 'ops@nammalogistics.in', '9876500102', 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM tenants WHERE tenantname = 'Namma Logistics');
|
||||
|
||||
INSERT INTO tenants (tenantname, primaryemail, primarycontact, status)
|
||||
SELECT 'Silicon Valley Couriers', 'ops@svccouriers.in', '9876500103', 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM tenants WHERE tenantname = 'Silicon Valley Couriers');
|
||||
|
||||
INSERT INTO tenants (tenantname, primaryemail, primarycontact, status)
|
||||
SELECT 'Deccan Express', 'ops@deccanexpress.in', '9876500104', 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM tenants WHERE tenantname = 'Deccan Express');
|
||||
|
||||
INSERT INTO tenants (tenantname, primaryemail, primarycontact, status)
|
||||
SELECT 'Charminar Logistics', 'ops@charminarlogs.in', '9876500105', 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM tenants WHERE tenantname = 'Charminar Logistics');
|
||||
|
||||
INSERT INTO tenants (tenantname, primaryemail, primarycontact, status)
|
||||
SELECT 'Marina Logistics', 'ops@marinalogistics.in', '9876500106', 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM tenants WHERE tenantname = 'Marina Logistics');
|
||||
|
||||
INSERT INTO tenants (tenantname, primaryemail, primarycontact, status)
|
||||
SELECT 'Ponni Couriers', 'ops@ponnicouriers.in', '9876500107', 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM tenants WHERE tenantname = 'Ponni Couriers');
|
||||
|
||||
-- ── SECTION 3: TENANT LOCATIONS ──────────────────────────────────────────
|
||||
INSERT INTO tenantlocations (tenantid, address, city, state, pincode, latitude, longitude, isprimary, status)
|
||||
SELECT t.tenantid, v.address, v.city, v.state, v.pincode, v.lat, v.lon, true, 'Active'
|
||||
FROM tenants t
|
||||
JOIN (VALUES
|
||||
('KPM Travels', 'Gandhipuram, Coimbatore', 'Coimbatore', 'Tamil Nadu', '641012', 11.0168, 76.9558),
|
||||
('Sri Murugan Logistics', 'Peelamedu, Coimbatore', 'Coimbatore', 'Tamil Nadu', '641004', 11.0301, 77.0034),
|
||||
('Namma Logistics', 'Koramangala, Bengaluru', 'Bengaluru', 'Karnataka', '560034', 12.9352, 77.6244),
|
||||
('Silicon Valley Couriers', 'Whitefield, Bengaluru', 'Bengaluru', 'Karnataka', '560066', 12.9698, 77.7499),
|
||||
('Deccan Express', 'Gachibowli, Hyderabad', 'Hyderabad', 'Telangana', '500032', 17.4483, 78.3741),
|
||||
('Charminar Logistics', 'Banjara Hills, Hyderabad', 'Hyderabad', 'Telangana', '500034', 17.4153, 78.4384),
|
||||
('Marina Logistics', 'T Nagar, Chennai', 'Chennai', 'Tamil Nadu', '600017', 13.0418, 80.2341),
|
||||
('Ponni Couriers', 'Velachery, Chennai', 'Chennai', 'Tamil Nadu', '600042', 12.9815, 80.2180)
|
||||
) AS v(name, address, city, state, pincode, lat, lon)
|
||||
ON t.tenantname = v.name
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM tenantlocations tl WHERE tl.tenantid = t.tenantid
|
||||
);
|
||||
|
||||
-- ── SECTION 4: HUBS ──────────────────────────────────────────────────────
|
||||
-- Coimbatore: RS Puram + Saravanampatti (existing: Gandhipuram sorting + Peelamedu delivery)
|
||||
INSERT INTO hubs (hubname, hubtype, applocationid, address, latitude, longitude, pincode, status, contactno)
|
||||
SELECT 'Coimbatore RS Puram Hub', 'delivery_hub', 1, 'RS Puram, Coimbatore', 11.0051, 76.9602, '641002', 'Active', '9876500200'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM hubs WHERE hubname = 'Coimbatore RS Puram Hub');
|
||||
|
||||
INSERT INTO hubs (hubname, hubtype, applocationid, address, latitude, longitude, pincode, status, contactno)
|
||||
SELECT 'Coimbatore Saravanampatti Hub', 'delivery_hub', 1, 'Saravanampatti, Coimbatore', 11.0639, 77.0109, '641035', 'Active', '9876500201'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM hubs WHERE hubname = 'Coimbatore Saravanampatti Hub');
|
||||
|
||||
-- Hyderabad: Banjara Hills + Secunderabad (existing: Gachibowli sorting + Madhapur delivery)
|
||||
INSERT INTO hubs (hubname, hubtype, applocationid, address, latitude, longitude, pincode, status, contactno)
|
||||
SELECT 'Hyderabad Banjara Hills Hub', 'delivery_hub', 2, 'Banjara Hills, Hyderabad', 17.4153, 78.4384, '500034', 'Active', '9876500202'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM hubs WHERE hubname = 'Hyderabad Banjara Hills Hub');
|
||||
|
||||
INSERT INTO hubs (hubname, hubtype, applocationid, address, latitude, longitude, pincode, status, contactno)
|
||||
SELECT 'Hyderabad Secunderabad Hub', 'delivery_hub', 2, 'Secunderabad, Hyderabad', 17.4399, 78.4983, '500003', 'Active', '9876500203'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM hubs WHERE hubname = 'Hyderabad Secunderabad Hub');
|
||||
|
||||
-- Bengaluru: Whitefield + Electronic City (existing: Koramangala sorting + Indiranagar delivery)
|
||||
INSERT INTO hubs (hubname, hubtype, applocationid, address, latitude, longitude, pincode, status, contactno)
|
||||
SELECT 'Bangalore Whitefield Hub', 'delivery_hub', 3, 'Whitefield, Bengaluru', 12.9698, 77.7499, '560066', 'Active', '9876500204'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM hubs WHERE hubname = 'Bangalore Whitefield Hub');
|
||||
|
||||
INSERT INTO hubs (hubname, hubtype, applocationid, address, latitude, longitude, pincode, status, contactno)
|
||||
SELECT 'Bangalore Electronic City Hub', 'delivery_hub', 3, 'Electronic City, Bengaluru', 12.8399, 77.6770, '560100', 'Active', '9876500205'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM hubs WHERE hubname = 'Bangalore Electronic City Hub');
|
||||
|
||||
-- Chennai: 1 sorting_center + 3 delivery_hubs (all new)
|
||||
INSERT INTO hubs (hubname, hubtype, applocationid, address, latitude, longitude, pincode, status, contactno)
|
||||
SELECT 'Chennai T Nagar Hub', 'sorting_center',
|
||||
(SELECT applocationid FROM applocations WHERE applocationname = 'Chennai'),
|
||||
'T Nagar, Chennai', 13.0418, 80.2341, '600017', 'Active', '9876500206'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM hubs WHERE hubname = 'Chennai T Nagar Hub');
|
||||
|
||||
INSERT INTO hubs (hubname, hubtype, applocationid, address, latitude, longitude, pincode, status, contactno)
|
||||
SELECT 'Chennai Anna Nagar Hub', 'delivery_hub',
|
||||
(SELECT applocationid FROM applocations WHERE applocationname = 'Chennai'),
|
||||
'Anna Nagar, Chennai', 13.0891, 80.2094, '600040', 'Active', '9876500207'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM hubs WHERE hubname = 'Chennai Anna Nagar Hub');
|
||||
|
||||
INSERT INTO hubs (hubname, hubtype, applocationid, address, latitude, longitude, pincode, status, contactno)
|
||||
SELECT 'Chennai Velachery Hub', 'delivery_hub',
|
||||
(SELECT applocationid FROM applocations WHERE applocationname = 'Chennai'),
|
||||
'Velachery, Chennai', 12.9815, 80.2180, '600042', 'Active', '9876500208'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM hubs WHERE hubname = 'Chennai Velachery Hub');
|
||||
|
||||
INSERT INTO hubs (hubname, hubtype, applocationid, address, latitude, longitude, pincode, status, contactno)
|
||||
SELECT 'Chennai Guindy Hub', 'delivery_hub',
|
||||
(SELECT applocationid FROM applocations WHERE applocationname = 'Chennai'),
|
||||
'Guindy, Chennai', 13.0067, 80.2206, '600032', 'Active', '9876500209'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM hubs WHERE hubname = 'Chennai Guindy Hub');
|
||||
|
||||
-- ── SECTION 5: MILERS ────────────────────────────────────────────────────
|
||||
-- Password = Test@1234 (bcrypt hash matches existing milers in the DB)
|
||||
-- Pattern: INSERT appusers WHERE phone not taken → RETURNING userid → INSERT milerprofiles
|
||||
|
||||
-- ── COIMBATORE — 3 new (existing: Ramesh 9876543255, Rajesh 9876543256, Karthi 9876543257) ──
|
||||
|
||||
WITH u AS (
|
||||
INSERT INTO appusers (authname, email, contactno, password, roleid, applocationid, tenantid, configid, status)
|
||||
SELECT 'Murugan Palani', 'murugan@doormile.com', '9876500001',
|
||||
'$2a$10$UUfjthIGXgA3y.1tFwpB..qx40ZmmUTHTQup5NQuJxHopVdNSgV4u', 5, 1, 1, 1001, 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM appusers WHERE contactno = '9876500001')
|
||||
RETURNING userid
|
||||
)
|
||||
INSERT INTO milerprofiles (userid, displayname, phone, defaultvehicletype, currentlatitude, currentlongitude, currentpincode, availabilitystatus, rating, applocationid)
|
||||
SELECT u.userid, 'Murugan CBE Rider', '9876500001', 'Bike', 11.0055, 76.9608, '641002', 'Available', 5.00, 1 FROM u
|
||||
WHERE NOT EXISTS (SELECT 1 FROM milerprofiles WHERE phone = '9876500001');
|
||||
|
||||
WITH u AS (
|
||||
INSERT INTO appusers (authname, email, contactno, password, roleid, applocationid, tenantid, configid, status)
|
||||
SELECT 'Selvam Raja', 'selvam@doormile.com', '9876500002',
|
||||
'$2a$10$UUfjthIGXgA3y.1tFwpB..qx40ZmmUTHTQup5NQuJxHopVdNSgV4u', 5, 1, 1, 1001, 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM appusers WHERE contactno = '9876500002')
|
||||
RETURNING userid
|
||||
)
|
||||
INSERT INTO milerprofiles (userid, displayname, phone, defaultvehicletype, currentlatitude, currentlongitude, currentpincode, availabilitystatus, rating, applocationid)
|
||||
SELECT u.userid, 'Selvam CBE Rider', '9876500002', 'Bike', 11.0645, 77.0115, '641035', 'Available', 5.00, 1 FROM u
|
||||
WHERE NOT EXISTS (SELECT 1 FROM milerprofiles WHERE phone = '9876500002');
|
||||
|
||||
WITH u AS (
|
||||
INSERT INTO appusers (authname, email, contactno, password, roleid, applocationid, tenantid, configid, status)
|
||||
SELECT 'Karthikeyan Vel', 'karthikvel@doormile.com', '9876500003',
|
||||
'$2a$10$UUfjthIGXgA3y.1tFwpB..qx40ZmmUTHTQup5NQuJxHopVdNSgV4u', 5, 1, 1, 1001, 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM appusers WHERE contactno = '9876500003')
|
||||
RETURNING userid
|
||||
)
|
||||
INSERT INTO milerprofiles (userid, displayname, phone, defaultvehicletype, currentlatitude, currentlongitude, currentpincode, availabilitystatus, rating, applocationid)
|
||||
SELECT u.userid, 'Karthikeyan CBE Rider', '9876500003', 'Bike', 11.0042, 76.9595, '641002', 'Available', 5.00, 1 FROM u
|
||||
WHERE NOT EXISTS (SELECT 1 FROM milerprofiles WHERE phone = '9876500003');
|
||||
|
||||
-- ── HYDERABAD — 3 new (existing: Suresh 9876543261, Ali 9876543262, Venkatesh 9876543263) ──
|
||||
|
||||
WITH u AS (
|
||||
INSERT INTO appusers (authname, email, contactno, password, roleid, applocationid, tenantid, configid, status)
|
||||
SELECT 'Prasad Reddy', 'prasad@doormile.com', '9876500004',
|
||||
'$2a$10$UUfjthIGXgA3y.1tFwpB..qx40ZmmUTHTQup5NQuJxHopVdNSgV4u', 5, 2, 2, 1001, 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM appusers WHERE contactno = '9876500004')
|
||||
RETURNING userid
|
||||
)
|
||||
INSERT INTO milerprofiles (userid, displayname, phone, defaultvehicletype, currentlatitude, currentlongitude, currentpincode, availabilitystatus, rating, applocationid)
|
||||
SELECT u.userid, 'Prasad HYD Rider', '9876500004', 'Bike', 17.4158, 78.4390, '500034', 'Available', 5.00, 2 FROM u
|
||||
WHERE NOT EXISTS (SELECT 1 FROM milerprofiles WHERE phone = '9876500004');
|
||||
|
||||
WITH u AS (
|
||||
INSERT INTO appusers (authname, email, contactno, password, roleid, applocationid, tenantid, configid, status)
|
||||
SELECT 'Naveen Kumar', 'naveen@doormile.com', '9876500005',
|
||||
'$2a$10$UUfjthIGXgA3y.1tFwpB..qx40ZmmUTHTQup5NQuJxHopVdNSgV4u', 5, 2, 2, 1001, 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM appusers WHERE contactno = '9876500005')
|
||||
RETURNING userid
|
||||
)
|
||||
INSERT INTO milerprofiles (userid, displayname, phone, defaultvehicletype, currentlatitude, currentlongitude, currentpincode, availabilitystatus, rating, applocationid)
|
||||
SELECT u.userid, 'Naveen HYD Rider', '9876500005', 'Bike', 17.4405, 78.4990, '500003', 'Available', 5.00, 2 FROM u
|
||||
WHERE NOT EXISTS (SELECT 1 FROM milerprofiles WHERE phone = '9876500005');
|
||||
|
||||
WITH u AS (
|
||||
INSERT INTO appusers (authname, email, contactno, password, roleid, applocationid, tenantid, configid, status)
|
||||
SELECT 'Ravi Shankar', 'ravishankar@doormile.com', '9876500006',
|
||||
'$2a$10$UUfjthIGXgA3y.1tFwpB..qx40ZmmUTHTQup5NQuJxHopVdNSgV4u', 5, 2, 2, 1001, 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM appusers WHERE contactno = '9876500006')
|
||||
RETURNING userid
|
||||
)
|
||||
INSERT INTO milerprofiles (userid, displayname, phone, defaultvehicletype, currentlatitude, currentlongitude, currentpincode, availabilitystatus, rating, applocationid)
|
||||
SELECT u.userid, 'Ravi HYD Rider', '9876500006', 'Auto', 17.4162, 78.4378, '500034', 'Available', 5.00, 2 FROM u
|
||||
WHERE NOT EXISTS (SELECT 1 FROM milerprofiles WHERE phone = '9876500006');
|
||||
|
||||
-- ── BENGALURU — 3 new (existing: Sanjay 9876543266, Anil 9876543271, Vijay 9876543272) ──
|
||||
|
||||
WITH u AS (
|
||||
INSERT INTO appusers (authname, email, contactno, password, roleid, applocationid, tenantid, configid, status)
|
||||
SELECT 'Ravi Gowda', 'ravigowda@doormile.com', '9876500007',
|
||||
'$2a$10$UUfjthIGXgA3y.1tFwpB..qx40ZmmUTHTQup5NQuJxHopVdNSgV4u', 5, 3, 3, 1001, 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM appusers WHERE contactno = '9876500007')
|
||||
RETURNING userid
|
||||
)
|
||||
INSERT INTO milerprofiles (userid, displayname, phone, defaultvehicletype, currentlatitude, currentlongitude, currentpincode, availabilitystatus, rating, applocationid)
|
||||
SELECT u.userid, 'Ravi BLR Rider', '9876500007', 'Bike', 12.9705, 77.7508, '560066', 'Available', 5.00, 3 FROM u
|
||||
WHERE NOT EXISTS (SELECT 1 FROM milerprofiles WHERE phone = '9876500007');
|
||||
|
||||
WITH u AS (
|
||||
INSERT INTO appusers (authname, email, contactno, password, roleid, applocationid, tenantid, configid, status)
|
||||
SELECT 'Suresh Naik', 'sureshnaik@doormile.com', '9876500008',
|
||||
'$2a$10$UUfjthIGXgA3y.1tFwpB..qx40ZmmUTHTQup5NQuJxHopVdNSgV4u', 5, 3, 3, 1001, 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM appusers WHERE contactno = '9876500008')
|
||||
RETURNING userid
|
||||
)
|
||||
INSERT INTO milerprofiles (userid, displayname, phone, defaultvehicletype, currentlatitude, currentlongitude, currentpincode, availabilitystatus, rating, applocationid)
|
||||
SELECT u.userid, 'Suresh BLR Rider', '9876500008', 'Bike', 12.8405, 77.6778, '560100', 'Available', 5.00, 3 FROM u
|
||||
WHERE NOT EXISTS (SELECT 1 FROM milerprofiles WHERE phone = '9876500008');
|
||||
|
||||
WITH u AS (
|
||||
INSERT INTO appusers (authname, email, contactno, password, roleid, applocationid, tenantid, configid, status)
|
||||
SELECT 'Mahesh Nair', 'maheshnair@doormile.com', '9876500009',
|
||||
'$2a$10$UUfjthIGXgA3y.1tFwpB..qx40ZmmUTHTQup5NQuJxHopVdNSgV4u', 5, 3, 3, 1001, 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM appusers WHERE contactno = '9876500009')
|
||||
RETURNING userid
|
||||
)
|
||||
INSERT INTO milerprofiles (userid, displayname, phone, defaultvehicletype, currentlatitude, currentlongitude, currentpincode, availabilitystatus, rating, applocationid)
|
||||
SELECT u.userid, 'Mahesh BLR Rider', '9876500009', 'Bike', 12.9712, 77.7495, '560066', 'Available', 5.00, 3 FROM u
|
||||
WHERE NOT EXISTS (SELECT 1 FROM milerprofiles WHERE phone = '9876500009');
|
||||
|
||||
-- ── CHENNAI — 6 new milers (all new city) ────────────────────────────────
|
||||
|
||||
WITH u AS (
|
||||
INSERT INTO appusers (authname, email, contactno, password, roleid, applocationid, tenantid, configid, status)
|
||||
SELECT 'Prakash Kumar', 'prakash@doormile.com', '9876500010',
|
||||
'$2a$10$UUfjthIGXgA3y.1tFwpB..qx40ZmmUTHTQup5NQuJxHopVdNSgV4u', 5,
|
||||
(SELECT applocationid FROM applocations WHERE applocationname='Chennai'),
|
||||
(SELECT tenantid FROM tenants WHERE tenantname='Doormile Chennai Logistics'),
|
||||
1001, 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM appusers WHERE contactno = '9876500010')
|
||||
RETURNING userid
|
||||
)
|
||||
INSERT INTO milerprofiles (userid, displayname, phone, defaultvehicletype, currentlatitude, currentlongitude, currentpincode, availabilitystatus, rating, applocationid)
|
||||
SELECT u.userid, 'Prakash CHN Rider', '9876500010', 'Bike', 13.0422, 80.2348, '600017', 'Available', 5.00,
|
||||
(SELECT applocationid FROM applocations WHERE applocationname='Chennai')
|
||||
FROM u
|
||||
WHERE NOT EXISTS (SELECT 1 FROM milerprofiles WHERE phone = '9876500010');
|
||||
|
||||
WITH u AS (
|
||||
INSERT INTO appusers (authname, email, contactno, password, roleid, applocationid, tenantid, configid, status)
|
||||
SELECT 'Senthil Nathan', 'senthil@doormile.com', '9876500011',
|
||||
'$2a$10$UUfjthIGXgA3y.1tFwpB..qx40ZmmUTHTQup5NQuJxHopVdNSgV4u', 5,
|
||||
(SELECT applocationid FROM applocations WHERE applocationname='Chennai'),
|
||||
(SELECT tenantid FROM tenants WHERE tenantname='Doormile Chennai Logistics'),
|
||||
1001, 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM appusers WHERE contactno = '9876500011')
|
||||
RETURNING userid
|
||||
)
|
||||
INSERT INTO milerprofiles (userid, displayname, phone, defaultvehicletype, currentlatitude, currentlongitude, currentpincode, availabilitystatus, rating, applocationid)
|
||||
SELECT u.userid, 'Senthil CHN Rider', '9876500011', 'Bike', 13.0898, 80.2101, '600040', 'Available', 5.00,
|
||||
(SELECT applocationid FROM applocations WHERE applocationname='Chennai')
|
||||
FROM u
|
||||
WHERE NOT EXISTS (SELECT 1 FROM milerprofiles WHERE phone = '9876500011');
|
||||
|
||||
WITH u AS (
|
||||
INSERT INTO appusers (authname, email, contactno, password, roleid, applocationid, tenantid, configid, status)
|
||||
SELECT 'Mani Raja', 'maniraja@doormile.com', '9876500012',
|
||||
'$2a$10$UUfjthIGXgA3y.1tFwpB..qx40ZmmUTHTQup5NQuJxHopVdNSgV4u', 5,
|
||||
(SELECT applocationid FROM applocations WHERE applocationname='Chennai'),
|
||||
(SELECT tenantid FROM tenants WHERE tenantname='Doormile Chennai Logistics'),
|
||||
1001, 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM appusers WHERE contactno = '9876500012')
|
||||
RETURNING userid
|
||||
)
|
||||
INSERT INTO milerprofiles (userid, displayname, phone, defaultvehicletype, currentlatitude, currentlongitude, currentpincode, availabilitystatus, rating, applocationid)
|
||||
SELECT u.userid, 'Mani CHN Rider', '9876500012', 'Bike', 12.9820, 80.2185, '600042', 'Available', 5.00,
|
||||
(SELECT applocationid FROM applocations WHERE applocationname='Chennai')
|
||||
FROM u
|
||||
WHERE NOT EXISTS (SELECT 1 FROM milerprofiles WHERE phone = '9876500012');
|
||||
|
||||
WITH u AS (
|
||||
INSERT INTO appusers (authname, email, contactno, password, roleid, applocationid, tenantid, configid, status)
|
||||
SELECT 'Deepak Shankar', 'deepak@doormile.com', '9876500013',
|
||||
'$2a$10$UUfjthIGXgA3y.1tFwpB..qx40ZmmUTHTQup5NQuJxHopVdNSgV4u', 5,
|
||||
(SELECT applocationid FROM applocations WHERE applocationname='Chennai'),
|
||||
(SELECT tenantid FROM tenants WHERE tenantname='Doormile Chennai Logistics'),
|
||||
1001, 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM appusers WHERE contactno = '9876500013')
|
||||
RETURNING userid
|
||||
)
|
||||
INSERT INTO milerprofiles (userid, displayname, phone, defaultvehicletype, currentlatitude, currentlongitude, currentpincode, availabilitystatus, rating, applocationid)
|
||||
SELECT u.userid, 'Deepak CHN Rider', '9876500013', 'Auto', 13.0072, 80.2210, '600032', 'Available', 5.00,
|
||||
(SELECT applocationid FROM applocations WHERE applocationname='Chennai')
|
||||
FROM u
|
||||
WHERE NOT EXISTS (SELECT 1 FROM milerprofiles WHERE phone = '9876500013');
|
||||
|
||||
WITH u AS (
|
||||
INSERT INTO appusers (authname, email, contactno, password, roleid, applocationid, tenantid, configid, status)
|
||||
SELECT 'Kiran Vel', 'kiranvel@doormile.com', '9876500014',
|
||||
'$2a$10$UUfjthIGXgA3y.1tFwpB..qx40ZmmUTHTQup5NQuJxHopVdNSgV4u', 5,
|
||||
(SELECT applocationid FROM applocations WHERE applocationname='Chennai'),
|
||||
(SELECT tenantid FROM tenants WHERE tenantname='Doormile Chennai Logistics'),
|
||||
1001, 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM appusers WHERE contactno = '9876500014')
|
||||
RETURNING userid
|
||||
)
|
||||
INSERT INTO milerprofiles (userid, displayname, phone, defaultvehicletype, currentlatitude, currentlongitude, currentpincode, availabilitystatus, rating, applocationid)
|
||||
SELECT u.userid, 'Kiran CHN Rider', '9876500014', 'Bike', 13.0430, 80.2352, '600017', 'Available', 5.00,
|
||||
(SELECT applocationid FROM applocations WHERE applocationname='Chennai')
|
||||
FROM u
|
||||
WHERE NOT EXISTS (SELECT 1 FROM milerprofiles WHERE phone = '9876500014');
|
||||
|
||||
WITH u AS (
|
||||
INSERT INTO appusers (authname, email, contactno, password, roleid, applocationid, tenantid, configid, status)
|
||||
SELECT 'Arjun Raj', 'arjunraj@doormile.com', '9876500015',
|
||||
'$2a$10$UUfjthIGXgA3y.1tFwpB..qx40ZmmUTHTQup5NQuJxHopVdNSgV4u', 5,
|
||||
(SELECT applocationid FROM applocations WHERE applocationname='Chennai'),
|
||||
(SELECT tenantid FROM tenants WHERE tenantname='Doormile Chennai Logistics'),
|
||||
1001, 'Active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM appusers WHERE contactno = '9876500015')
|
||||
RETURNING userid
|
||||
)
|
||||
INSERT INTO milerprofiles (userid, displayname, phone, defaultvehicletype, currentlatitude, currentlongitude, currentpincode, availabilitystatus, rating, applocationid)
|
||||
SELECT u.userid, 'Arjun CHN Rider', '9876500015', 'Bike', 13.0902, 80.2105, '600040', 'Available', 5.00,
|
||||
(SELECT applocationid FROM applocations WHERE applocationname='Chennai')
|
||||
FROM u
|
||||
WHERE NOT EXISTS (SELECT 1 FROM milerprofiles WHERE phone = '9876500015');
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user