package handlers import ( "database/sql" "encoding/json" "net/http" "github.com/gin-gonic/gin" "armature/models" "armature/store" "armature/workflow" ) // ── Assignment Handlers ───────────────────── // WorkflowAssignmentHandler manages workflow assignment HTTP endpoints. type WorkflowAssignmentHandler struct { engine *workflow.Engine stores store.Stores } // NewWorkflowAssignmentHandler creates an assignment handler. func NewWorkflowAssignmentHandler(engine *workflow.Engine, stores store.Stores) *WorkflowAssignmentHandler { return &WorkflowAssignmentHandler{engine: engine, stores: stores} } // Claim claims an assignment for the current user. // POST /api/v1/assignments/:id/claim func (h *WorkflowAssignmentHandler) Claim(c *gin.Context) { userID := c.GetString("user_id") assignmentID := c.Param("id") ctx := c.Request.Context() // Claim first, then verify role. Rollback if role check fails. if err := h.stores.Workflows.ClaimAssignment(ctx, assignmentID, userID); err != nil { c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) return } // Role check: find the assignment in user's claimed list and verify role claimed, _ := h.stores.Workflows.ListAssignmentsByUser(ctx, userID, models.AssignmentStatusClaimed) for _, a := range claimed { if a.ID == assignmentID { if rerr := workflow.CheckClaimRole(ctx, h.stores, &a, userID); rerr != nil { // Rollback: unclaim h.stores.Workflows.UnclaimAssignment(ctx, assignmentID) c.JSON(http.StatusForbidden, gin.H{"error": rerr.Error()}) return } break } } c.JSON(http.StatusOK, gin.H{"claimed": true}) } // Unclaim releases a claimed assignment back to the queue. // POST /api/v1/assignments/:id/unclaim func (h *WorkflowAssignmentHandler) Unclaim(c *gin.Context) { userID := c.GetString("user_id") assignmentID := c.Param("id") // Verify current user is the claimer userAssignments, err := h.stores.Workflows.ListAssignmentsByUser(c.Request.Context(), userID, models.AssignmentStatusClaimed) if err == nil { found := false for _, a := range userAssignments { if a.ID == assignmentID { found = true break } } if !found { c.JSON(http.StatusForbidden, gin.H{"error": "assignment not claimed by you"}) return } } if err := h.stores.Workflows.UnclaimAssignment(c.Request.Context(), assignmentID); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"unclaimed": true}) } // Complete marks a claimed assignment as completed. // POST /api/v1/assignments/:id/complete func (h *WorkflowAssignmentHandler) Complete(c *gin.Context) { userID := c.GetString("user_id") assignmentID := c.Param("id") var body struct { ReviewData json.RawMessage `json:"review_data"` } if err := c.ShouldBindJSON(&body); err != nil && err.Error() != "EOF" { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"}) return } // Verify current user is the claimer userAssignments, err := h.stores.Workflows.ListAssignmentsByUser(c.Request.Context(), userID, models.AssignmentStatusClaimed) if err == nil { found := false for _, a := range userAssignments { if a.ID == assignmentID { found = true break } } if !found { c.JSON(http.StatusForbidden, gin.H{"error": "assignment not claimed by you"}) return } } if err := h.stores.Workflows.CompleteAssignment(c.Request.Context(), assignmentID, body.ReviewData); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // If the stage has auto_transition, advance the instance. // Look up the assignment's instance to get context. userAssignments, _ = h.stores.Workflows.ListAssignmentsByUser(c.Request.Context(), userID, models.AssignmentStatusCompleted) for _, a := range userAssignments { if a.ID == assignmentID { // Try to advance — engine will handle validation h.engine.Advance(c.Request.Context(), a.InstanceID, body.ReviewData, userID) break } } c.JSON(http.StatusOK, gin.H{"completed": true}) } // Cancel cancels an open assignment. // POST /api/v1/assignments/:id/cancel func (h *WorkflowAssignmentHandler) Cancel(c *gin.Context) { assignmentID := c.Param("id") if err := h.stores.Workflows.CancelAssignment(c.Request.Context(), assignmentID); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"cancelled": true}) } // ListByTeam returns assignments for a team. // GET /api/v1/teams/:teamId/assignments?status= func (h *WorkflowAssignmentHandler) ListByTeam(c *gin.Context) { teamID := c.Param("teamId") status := c.Query("status") result, err := h.stores.Workflows.ListAssignmentsByTeam(c.Request.Context(), teamID, status) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusOK, gin.H{"data": []struct{}{}}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list assignments"}) return } if result == nil { c.JSON(http.StatusOK, gin.H{"data": []struct{}{}}) return } c.JSON(http.StatusOK, gin.H{"data": result}) } // ListMine returns assignments claimed by or assigned to the current user. // GET /api/v1/assignments/mine?status= func (h *WorkflowAssignmentHandler) ListMine(c *gin.Context) { userID := c.GetString("user_id") status := c.Query("status") result, err := h.stores.Workflows.ListAssignmentsByUser(c.Request.Context(), userID, status) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list assignments"}) return } if result == nil { c.JSON(http.StatusOK, gin.H{"data": []struct{}{}}) return } c.JSON(http.StatusOK, gin.H{"data": result}) }