// Package handler define HTTP handler functions for eventRT service package handler import ( "context" "errors" "net/http" "time" "eventRT/constants" "eventRT/database" "eventRT/event" "eventRT/logger" "github.com/gin-gonic/gin" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) var ( errEventNotFound = errors.New("event not found") errInvalidStatusTransition = errors.New("invalid status transition: precondition status mismatch") ) type updateStatusRequest struct { Op string `json:"op" binding:"required"` } // ConfirmEventHandler handles PATCH /events/:event_uuid/confirm // Transitions event status from EventStatusReported to EventStatusConfirmed. func ConfirmEventHandler(c *gin.Context) { handleStatusTransition(c, constants.EventStatusReported, constants.EventStatusConfirmed, "confirm", ) } // CloseEventHandler handles PATCH /events/:event_uuid/close // Transitions event status from EventStatusConfirmed to EventStatusClosed. func CloseEventHandler(c *gin.Context) { handleStatusTransition(c, constants.EventStatusConfirmed, constants.EventStatusClosed, "close", ) } func handleStatusTransition(c *gin.Context, requiredStatus, newStatus int, action string) { ctx := c.Request.Context() eventUUID := c.Param("event_uuid") var req updateStatusRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "field op is required"}) return } updated, err := transitionEventStatus(ctx, eventUUID, req.Op, requiredStatus, newStatus, action) if err != nil { switch { case errors.Is(err, errEventNotFound): c.JSON(http.StatusNotFound, gin.H{"error": "event not found"}) case errors.Is(err, errInvalidStatusTransition): c.JSON(http.StatusConflict, gin.H{ "error": "invalid status transition", "required_status": requiredStatus, }) default: logger.Error(ctx, "update event status failed", "event_uuid", eventUUID, "action", action, "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) } return } c.JSON(http.StatusOK, updated) } // transitionEventStatus atomically validates the precondition status and applies the transition. // Uses FindOneAndUpdate with a status filter so the check and update are a single MongoDB operation. func transitionEventStatus(ctx context.Context, eventUUID, op string, requiredStatus, newStatus int, action string) (*event.EventRecord, error) { col := database.GetMongoClient(). Database(constants.EventDBName). Collection(constants.EventCollectionName) opRecord := event.OperationRecord{ Action: action, Op: op, TS: time.Now().UnixNano() / int64(time.Millisecond), } filter := bson.M{ "event_uuid": eventUUID, "status": requiredStatus, } update := bson.M{ "$set": bson.M{"status": newStatus}, "$push": bson.M{"operations": opRecord}, } opts := options.FindOneAndUpdate().SetReturnDocument(options.After) var updated event.EventRecord err := col.FindOneAndUpdate(ctx, filter, update, opts).Decode(&updated) if err != nil { if !errors.Is(err, mongo.ErrNoDocuments) { return nil, err } // FindOneAndUpdate returned no documents: either the event doesn't exist // or the current status doesn't match the required precondition. // Do a follow-up read to distinguish the two cases. var existing event.EventRecord findErr := col.FindOne(ctx, bson.M{"event_uuid": eventUUID}).Decode(&existing) if errors.Is(findErr, mongo.ErrNoDocuments) { return nil, errEventNotFound } if findErr != nil { return nil, findErr } return nil, errInvalidStatusTransition } logger.Info(ctx, "event status transitioned", "event_uuid", eventUUID, "from", requiredStatus, "to", newStatus, "op", op) return &updated, nil }