feat: hub console backend — complete API surface
This commit is contained in:
@@ -111,6 +111,79 @@ func tryAssign(bookingID int) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// AutoAssignResult carries enough detail for a synchronous caller (the hub
|
||||
// console's manual "auto-assign" trigger) to report what happened, since that
|
||||
// caller can't rely on the fire-and-forget logging AssignCRMMiler normally uses.
|
||||
type AutoAssignResult struct {
|
||||
Assigned bool
|
||||
Escalated bool
|
||||
MilerUserID int
|
||||
MilerName string
|
||||
DistanceKm float64
|
||||
Reasoning string
|
||||
SearchedRadiusKm float64
|
||||
CandidatesFound int
|
||||
}
|
||||
|
||||
// TryAssignOnce performs a single assignment attempt (GEOSEARCH → AI decision
|
||||
// → commit) with no retry loop, for a booking that wasn't auto-assigned at
|
||||
// creation time. AssignCRMMiler/AssignCustomerMiler retry over ~10 minutes,
|
||||
// which is too slow for a hub staff member waiting on a synchronous response;
|
||||
// this wraps the same single-attempt core (tryAssign) used internally by both.
|
||||
func TryAssignOnce(bookingID int) (AutoAssignResult, error) {
|
||||
var booking models.PickupBooking
|
||||
if err := db.DB.Preload("Parcels").Preload("ServiceOptions").First(&booking, bookingID).Error; err != nil {
|
||||
return AutoAssignResult{}, fmt.Errorf("load booking: %w", err)
|
||||
}
|
||||
|
||||
if booking.Status == constants.BookingCancelled || booking.Assignedmileruserid != nil {
|
||||
return AutoAssignResult{Assigned: true}, nil
|
||||
}
|
||||
|
||||
if booking.Pickuplatitude == 0 || booking.Pickuplongitude == 0 {
|
||||
return AutoAssignResult{}, fmt.Errorf("booking %d has no pickup coordinates", bookingID)
|
||||
}
|
||||
|
||||
nearby, err := queryNearbyMilers(booking.Pickuplatitude, booking.Pickuplongitude)
|
||||
if err != nil {
|
||||
return AutoAssignResult{Escalated: true, Reasoning: "miler location search unavailable", SearchedRadiusKm: geoRadiusKm}, nil
|
||||
}
|
||||
if len(nearby) == 0 {
|
||||
return AutoAssignResult{Escalated: true, Reasoning: "no milers within search radius", SearchedRadiusKm: geoRadiusKm}, nil
|
||||
}
|
||||
|
||||
candidate, agentDecisionID, found := selectMilerWithAI(&booking, nearby)
|
||||
if !found {
|
||||
return AutoAssignResult{
|
||||
Escalated: true,
|
||||
Reasoning: "no eligible miler after evaluation",
|
||||
SearchedRadiusKm: geoRadiusKm,
|
||||
CandidatesFound: len(nearby),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if err := commitAssignment(&booking, candidate, agentDecisionID); err != nil {
|
||||
return AutoAssignResult{}, fmt.Errorf("commit: %w", err)
|
||||
}
|
||||
|
||||
reasoning := ""
|
||||
if agentDecisionID != nil {
|
||||
var ad models.AgentDecision
|
||||
if db.DB.Where("id = ?", *agentDecisionID).First(&ad).Error == nil {
|
||||
reasoning = ad.Reasoning
|
||||
}
|
||||
}
|
||||
|
||||
return AutoAssignResult{
|
||||
Assigned: true,
|
||||
MilerUserID: candidate.profile.Userid,
|
||||
MilerName: candidate.profile.Displayname,
|
||||
DistanceKm: candidate.distanceKm,
|
||||
Reasoning: reasoning,
|
||||
CandidatesFound: len(nearby),
|
||||
}, 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) {
|
||||
|
||||
Reference in New Issue
Block a user