internal modifications
This commit is contained in:
@@ -65,6 +65,11 @@ func AssignCRMMiler(bookingID int) {
|
|||||||
"booking_id", bookingID,
|
"booking_id", bookingID,
|
||||||
"max_retries", maxRetries,
|
"max_retries", maxRetries,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Terminal failure — same handoff as the B2C path. Both entry points must
|
||||||
|
// publish, or failures arriving via the CRM console stay invisible to the
|
||||||
|
// DispatchAgent.
|
||||||
|
publishAssignmentFailed(bookingID, reasonNoMilerAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
// tryAssign performs a single attempt: queries Redis GEO, scores candidates, commits.
|
// tryAssign performs a single attempt: queries Redis GEO, scores candidates, commits.
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ func AssignCustomerMiler(bookingID int) {
|
|||||||
"booking_id", bookingID,
|
"booking_id", bookingID,
|
||||||
"max_retries", maxRetries,
|
"max_retries", maxRetries,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Terminal failure — hand off to the AI layer's DispatchAgent, which owns
|
||||||
|
// what happens next (coverage sweep, escalation). Reached only after every
|
||||||
|
// retry is exhausted, so it fires at most once per booking.
|
||||||
|
publishAssignmentFailed(bookingID, reasonNoMilerAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
// tryCustomerAssign performs one full attempt: GEO miler search → provider selection → commit.
|
// tryCustomerAssign performs one full attempt: GEO miler search → provider selection → commit.
|
||||||
|
|||||||
79
internal/assignment/failure_event.go
Normal file
79
internal/assignment/failure_event.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package assignment
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"doormile/db"
|
||||||
|
"doormile/models"
|
||||||
|
"doormile/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// assignmentFailedSubject is the JetStream subject the AI layer's DispatchAgent
|
||||||
|
// binds its durable consumer to. It must be present on the ASSIGNMENTS stream
|
||||||
|
// before anything publishes here — JetStream drops messages on subjects the
|
||||||
|
// stream does not cover, silently and with a successful-looking publish.
|
||||||
|
const assignmentFailedSubject = "booking.assignment_failed"
|
||||||
|
|
||||||
|
// Reasons carried on the failure event. Kept coarse on purpose: the consumer
|
||||||
|
// branches on booking_id and coordinates, and treats reason as diagnostic only.
|
||||||
|
const (
|
||||||
|
reasonNoMilerAvailable = "NO_MILER_AVAILABLE"
|
||||||
|
)
|
||||||
|
|
||||||
|
// publishAssignmentFailed emits booking.assignment_failed after auto-assignment
|
||||||
|
// has genuinely given up on a booking.
|
||||||
|
//
|
||||||
|
// Call this only from the terminal branch of an assignment entry point — after
|
||||||
|
// every retry is exhausted — never from inside a single attempt. The per-attempt
|
||||||
|
// paths (no eligible candidates, decision-engine escalation) fire up to
|
||||||
|
// maxRetries times for a booking that may still succeed on a later attempt, and
|
||||||
|
// publishing there would report failures that never happened.
|
||||||
|
//
|
||||||
|
// Mirrors publishAssignment/publishCustomerAssignment: non-fatal, so a NATS
|
||||||
|
// outage degrades to a log line rather than breaking the assignment goroutine.
|
||||||
|
func publishAssignmentFailed(bookingID int, reason string) {
|
||||||
|
if db.Js == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"booking_id": bookingID,
|
||||||
|
"reason": reason,
|
||||||
|
"failed_at": time.Now().UnixMilli(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pickup coordinates drive the consumer's coverage sweep. They aren't in
|
||||||
|
// scope at the call site, so re-read the booking — one indexed lookup on a
|
||||||
|
// path that only runs after retries have already been exhausted. If the
|
||||||
|
// read fails we still publish: the consumer treats missing coordinates as
|
||||||
|
// "skip the sweep" and degrades safely rather than losing the event.
|
||||||
|
var booking models.PickupBooking
|
||||||
|
if err := db.DB.Select("pickuplatitude", "pickuplongitude", "bookingno").
|
||||||
|
First(&booking, bookingID).Error; err != nil {
|
||||||
|
utils.Warn("Assignment: could not load booking for failure event; publishing without coordinates",
|
||||||
|
"booking_id", bookingID, "error", err)
|
||||||
|
} else {
|
||||||
|
payload["booking_no"] = booking.Bookingno
|
||||||
|
if booking.Pickuplatitude != 0 || booking.Pickuplongitude != 0 {
|
||||||
|
payload["lat"] = booking.Pickuplatitude
|
||||||
|
payload["lon"] = booking.Pickuplongitude
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
utils.Warn("Assignment: failed to marshal assignment-failed payload",
|
||||||
|
"booking_id", bookingID, "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := db.Js.Publish(assignmentFailedSubject, data); err != nil {
|
||||||
|
utils.Warn("Assignment: NATS publish failed for assignment-failed event",
|
||||||
|
"booking_id", bookingID, "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.Info("Assignment: published "+assignmentFailedSubject,
|
||||||
|
"booking_id", bookingID, "reason", reason)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user