80 lines
2.9 KiB
Go
80 lines
2.9 KiB
Go
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)
|
|
}
|