Add assignment engine, FCM, WebSockets, city gate, and internal APIs
- internal/assignment: GEORADIUS miler assignment with retry/escalation, customer-side provider scoring, FCM notifications on assign - internal/notify: Firebase Admin SDK (FCM) client initialisation - internal/ws: WebSocket handlers for live parcel tracking and customer↔miler chat - middlewares: city gate (pincode prefix validation), internal API key auth, WebSocket JWT auth - controllers: InternalNotify + InternalReassign for machine-to-machine calls; pricing helpers wired into CreateCustomerBooking and CreateCRMBooking - routes: /internal/*, /ws/bookings/:id/track, /ws/bookings/:id/chat - models/users, models/doormile_pricing: new fields for device tokens, assignment state, pricing bands - seed_data.sql: initial pricing seed rows .env and Firebase service-account JSON intentionally excluded. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,8 @@ import (
|
||||
"doormile/constants"
|
||||
"doormile/db"
|
||||
"doormile/dto"
|
||||
"doormile/internal/assignment"
|
||||
"doormile/internal/notify"
|
||||
"doormile/models"
|
||||
"doormile/utils"
|
||||
|
||||
@@ -1096,6 +1098,8 @@ func CreateCRMBooking(c *fiber.Ctx) error {
|
||||
|
||||
tx.Commit()
|
||||
|
||||
go assignment.AssignCRMMiler(booking.Bookingid)
|
||||
|
||||
if db.Js != nil {
|
||||
payload := map[string]interface{}{
|
||||
"booking_id": booking.Bookingid,
|
||||
@@ -1946,3 +1950,143 @@ func GetAdminProfile(c *fiber.Ctx) error {
|
||||
|
||||
return utils.OK(c, user)
|
||||
}
|
||||
|
||||
// InternalNotify sends FCM push notifications on behalf of the Python agent system.
|
||||
// The caller specifies target = "customer", "miler", or "both".
|
||||
// Auth: X-Internal-Key header (see InternalKeyAuth middleware).
|
||||
//
|
||||
// POST /api/v1/internal/notify
|
||||
func InternalNotify(c *fiber.Ctx) error {
|
||||
type req struct {
|
||||
BookingID int `json:"booking_id"`
|
||||
Target string `json:"target"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Data map[string]string `json:"data"`
|
||||
}
|
||||
|
||||
body := new(req)
|
||||
if err := c.BodyParser(body); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
if body.BookingID == 0 {
|
||||
return utils.BadRequest(c, "booking_id is required")
|
||||
}
|
||||
if body.Target != "customer" && body.Target != "miler" && body.Target != "both" {
|
||||
return utils.BadRequest(c, "target must be customer, miler, or both")
|
||||
}
|
||||
if body.Title == "" || body.Message == "" {
|
||||
return utils.BadRequest(c, "title and message are required")
|
||||
}
|
||||
|
||||
var booking models.PickupBooking
|
||||
if err := db.DB.First(&booking, body.BookingID).Error; err != nil {
|
||||
return utils.NotFound(c, "booking not found")
|
||||
}
|
||||
|
||||
data := body.Data
|
||||
if data == nil {
|
||||
data = map[string]string{}
|
||||
}
|
||||
data["booking_id"] = strconv.Itoa(booking.Bookingid)
|
||||
|
||||
sent := make([]string, 0, 2)
|
||||
|
||||
if body.Target == "customer" || body.Target == "both" {
|
||||
var customer models.AppCustomer
|
||||
if err := db.DB.Where("appcustomerid = ?", booking.Appcustomerid).First(&customer).Error; err == nil && customer.Devicetoken != "" {
|
||||
if err := notify.SendToDevice(customer.Devicetoken, body.Title, body.Message, data); err != nil {
|
||||
utils.Warn("InternalNotify: failed to notify customer", "booking_id", booking.Bookingid, "error", err)
|
||||
} else {
|
||||
sent = append(sent, "customer")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if body.Target == "miler" || body.Target == "both" {
|
||||
if booking.Assignedmileruserid != nil {
|
||||
var profile models.MilerProfile
|
||||
if err := db.DB.Where("userid = ?", *booking.Assignedmileruserid).First(&profile).Error; err == nil && profile.Devicetoken != "" {
|
||||
if err := notify.SendToDevice(profile.Devicetoken, body.Title, body.Message, data); err != nil {
|
||||
utils.Warn("InternalNotify: failed to notify miler", "booking_id", booking.Bookingid, "error", err)
|
||||
} else {
|
||||
sent = append(sent, "miler")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return utils.OK(c, fiber.Map{
|
||||
"sent": true,
|
||||
"targets": sent,
|
||||
})
|
||||
}
|
||||
|
||||
// InternalReassign releases the current miler assignment and re-triggers the
|
||||
// auto-assignment engine for a stalled booking. Only valid when the booking is
|
||||
// in Miler_Assigned or Pickup_Scheduled state.
|
||||
// Auth: X-Internal-Key header (see InternalKeyAuth middleware).
|
||||
//
|
||||
// POST /api/v1/internal/bookings/:id/reassign
|
||||
func InternalReassign(c *fiber.Ctx) error {
|
||||
id, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid booking ID")
|
||||
}
|
||||
|
||||
tx := db.DB.Begin()
|
||||
|
||||
var booking models.PickupBooking
|
||||
if err := tx.First(&booking, id).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.NotFound(c, "booking not found")
|
||||
}
|
||||
|
||||
if booking.Status != constants.BookingMilerAssigned && booking.Status != constants.BookingPickupScheduled {
|
||||
tx.Rollback()
|
||||
return utils.BadRequest(c, "booking must be in Miler_Assigned or Pickup_Scheduled state to reassign")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
if booking.Assignedmileruserid != nil {
|
||||
tx.Model(&models.MilerProfile{}).
|
||||
Where("userid = ?", *booking.Assignedmileruserid).
|
||||
Updates(map[string]interface{}{
|
||||
"availabilitystatus": constants.MilerAvailable,
|
||||
"updatedat": now,
|
||||
})
|
||||
|
||||
tx.Delete(&models.BookingAssignment{},
|
||||
"bookingid = ? AND assignmentstatus IN ?",
|
||||
booking.Bookingid,
|
||||
[]string{constants.AssignmentAssigned, constants.AssignmentAccepted},
|
||||
)
|
||||
}
|
||||
|
||||
booking.Status = constants.BookingCreated
|
||||
booking.Assignedmileruserid = nil
|
||||
booking.Updatedat = now
|
||||
if err := tx.Save(&booking).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to reset booking")
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
if booking.Bookingsource == "CRM_Console" {
|
||||
go assignment.AssignCRMMiler(booking.Bookingid)
|
||||
} else {
|
||||
go assignment.AssignCustomerMiler(booking.Bookingid)
|
||||
}
|
||||
|
||||
utils.Info("InternalReassign: reassignment triggered",
|
||||
"booking_id", booking.Bookingid,
|
||||
"booking_source", booking.Bookingsource,
|
||||
)
|
||||
|
||||
return utils.OK(c, fiber.Map{
|
||||
"reassignment": "triggered",
|
||||
"booking_id": booking.Bookingid,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user