161 lines
5.6 KiB
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)
|
|
}
|