package controllers import ( "os" "strings" "doormile/constants" "doormile/db" "doormile/internal/cxstage" "doormile/models" "doormile/utils" "github.com/gofiber/fiber/v2" ) // QA support — §11 of the deliverables. // // "A way to force a booking to any stage on staging. Every tracking state must // be reachable for QA — this is what lets us delete the debug stepper." // // The customer app currently walks its tracking screen through a hardcoded // stepper because no real backend could produce those states on demand. // Reaching out_for_delivery honestly needs a rider to accept, drive, weigh a // parcel, hand it to a hub and start a delivery run — which is not something // design QA can do before every screenshot. // // This endpoint is hard-gated. It refuses outright when ENV is production, and // it additionally requires CX_ALLOW_STAGE_OVERRIDE to be set: two independent // switches, because one of them being wrong on a production deploy would hand // anyone with a customer token the ability to mark their own parcel delivered. // cxStageOverrideEnabled reports whether the QA stage override may run at all. func cxStageOverrideEnabled() bool { if strings.EqualFold(strings.TrimSpace(os.Getenv("ENV")), "production") { return false } return strings.EqualFold(strings.TrimSpace(os.Getenv("CX_ALLOW_STAGE_OVERRIDE")), "true") } // ForceCxStage drives a booking to an arbitrary stage on staging. // // It writes through the same cxstage recorder every real transition uses, so // what QA sees is the real projection over real event rows — not a special // rendering path that could pass while the production one is broken. func ForceCxStage(c *fiber.Ctx) error { if !cxStageOverrideEnabled() { return utils.CxNotFound(c, "Not available") } customerID := c.Locals("userid").(int) reference := strings.TrimSpace(c.Params("reference")) var req struct { Stage string `json:"stage"` // Reason is recorded on the audit row so a forced transition is // distinguishable from a real one forever after. A staging database // that gets promoted, or an export read months later, must not present // invented history as observed history. Reason string `json:"reason"` } if err := c.BodyParser(&req); err != nil { return utils.CxBadRequest(c, "We could not read that request") } stage := strings.TrimSpace(req.Stage) if constants.CxStageRank(stage) < 0 { return utils.CxBadRequest(c, "Unknown stage") } booking, err := cxLoadBooking(customerID, reference) if err != nil { return utils.CxNotFound(c, "We could not find that pickup") } var destinations []models.BookingDestination if err := db.DB.Where("bookingid = ?", booking.Bookingid). Order("seq ASC").Find(&destinations).Error; err != nil { utils.Error("ForceCxStage: destination query failed", "booking_id", booking.Bookingid, "error", err) return utils.CxInternal(c) } reason := strings.TrimSpace(req.Reason) if reason == "" { reason = "forced on staging for QA" } tx := db.DB.Begin() // Walk every stage up to the target rather than jumping. A timeline with a // hole in it is not a state the production flow can produce, so testing // against one proves nothing about the screen that renders it. for _, s := range []string{ constants.CxStageBooked, constants.CxStageAssigned, constants.CxStageOnTheWay, constants.CxStageArrived, constants.CxStagePickedUp, constants.CxStageOrderCreated, constants.CxStageInTransit, constants.CxStageOutForDelivery, constants.CxStageDelivered, } { if constants.CxStageRank(s) > constants.CxStageRank(stage) { break } perOrder := constants.CxStageRank(s) >= constants.CxStageOrder[constants.CxStageInTransit] if perOrder && len(destinations) > 0 { for i := range destinations { id := destinations[i].Bookingdestinationid if err := cxstage.Record(tx, cxstage.Event{ BookingID: booking.Bookingid, DestinationID: &id, Stage: s, ActorType: constants.CxActorOps, ActorID: &customerID, Source: "POST /customer/ops/bookings/{reference}/stage", Remarks: reason, }); err != nil { tx.Rollback() utils.Error("ForceCxStage: record failed", "booking_id", booking.Bookingid, "stage", s, "error", err) return utils.CxInternal(c) } } continue } if err := cxstage.Record(tx, cxstage.Event{ BookingID: booking.Bookingid, Stage: s, ActorType: constants.CxActorOps, ActorID: &customerID, Source: "POST /customer/ops/bookings/{reference}/stage", Remarks: reason, }); err != nil { tx.Rollback() utils.Error("ForceCxStage: record failed", "booking_id", booking.Bookingid, "stage", s, "error", err) return utils.CxInternal(c) } } // Tracking numbers exist from order_created onward, so a forced booking // past that point needs them or the orders render with a null trackingId // and the deep-link path cannot be tested at all. if constants.CxStageRank(stage) >= constants.CxStageOrder[constants.CxStageOrderCreated] { for i := range destinations { if destinations[i].Trackingno != "" { continue } if err := tx.Model(&models.BookingDestination{}). Where("bookingdestinationid = ?", destinations[i].Bookingdestinationid). Update("trackingno", generateTrackingNo()).Error; err != nil { tx.Rollback() utils.Error("ForceCxStage: could not mint tracking number", "booking_id", booking.Bookingid, "error", err) return utils.CxInternal(c) } } } if err := tx.Commit().Error; err != nil { utils.Error("ForceCxStage: commit failed", "booking_id", booking.Bookingid, "error", err) return utils.CxInternal(c) } return cxRespondWithBooking(c, booking.Bookingid, fiber.StatusOK) }