eventRT/handler/event_status.go

134 lines
3.8 KiB
Go
Raw Normal View History

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