134 lines
3.8 KiB
Go
134 lines
3.8 KiB
Go
|
|
// 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
|
||
|
|
}
|