Files
doormile_backend/controllers/cxOpsController.go

161 lines
5.6 KiB
Go

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