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) }