package approvals import ( "errors" "fmt" "strings" "sync" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" ) type ApprovalStep uint16 type ApprovalWorkflowKey string func (k ApprovalWorkflowKey) String() string { return string(k) } type NextStepCallback func(current ApprovalStep, decision entity.ApprovalAction) (ApprovalStep, bool) var ( approvalActions = map[entity.ApprovalAction]struct{}{ entity.ApprovalActionApproved: {}, entity.ApprovalActionRejected: {}, entity.ApprovalActionCreated: {}, entity.ApprovalActionUpdated: {}, } approvalWorkflows = make(map[ApprovalWorkflowKey]map[ApprovalStep]string) approvalWorkflowsMu sync.RWMutex ) // WorkflowConstants prepares the registered workflows for exposure via constants endpoints. func WorkflowConstants() map[string]map[string]string { approvalWorkflowsMu.RLock() defer approvalWorkflowsMu.RUnlock() if len(approvalWorkflows) == 0 { return nil } result := make(map[string]map[string]string, len(approvalWorkflows)) for workflow, steps := range approvalWorkflows { if len(steps) == 0 { continue } stepMap := make(map[string]string, len(steps)) for step, label := range steps { stepMap[fmt.Sprintf("%d", step)] = label } result[workflow.String()] = stepMap } if len(result) == 0 { return nil } return result } // RegisterWorkflowSteps stores the available steps for a workflow key (usually matching approvable type). func RegisterWorkflowSteps(workflow ApprovalWorkflowKey, steps map[ApprovalStep]string) error { workflowStr := strings.TrimSpace(workflow.String()) if workflowStr == "" { return errors.New("workflow key is required") } if len(steps) == 0 { return fmt.Errorf("no steps defined for workflow %q", workflowStr) } copied := make(map[ApprovalStep]string, len(steps)) for step, label := range steps { if step == 0 { return fmt.Errorf("workflow %q contains step 0 which is not allowed", workflowStr) } trimmed := strings.TrimSpace(label) if trimmed == "" { return fmt.Errorf("workflow %q contains empty label for step %d", workflowStr, step) } copied[step] = trimmed } approvalWorkflowsMu.Lock() defer approvalWorkflowsMu.Unlock() approvalWorkflows[ApprovalWorkflowKey(workflowStr)] = copied return nil } // WorkflowSteps returns the steps registered for the given workflow key. func WorkflowSteps(workflow ApprovalWorkflowKey) map[ApprovalStep]string { approvalWorkflowsMu.RLock() defer approvalWorkflowsMu.RUnlock() workflowStr := strings.TrimSpace(workflow.String()) if workflowStr == "" { return nil } steps, ok := approvalWorkflows[ApprovalWorkflowKey(workflowStr)] if !ok || len(steps) == 0 { return nil } copied := make(map[ApprovalStep]string, len(steps)) for step, label := range steps { copied[step] = label } return copied } // ApprovalStepName fetches the label for the target step inside the workflow. func ApprovalStepName(workflow ApprovalWorkflowKey, step ApprovalStep) (string, bool) { steps := WorkflowSteps(workflow) if len(steps) == 0 { return "", false } label, ok := steps[step] return label, ok } // ValidateApprovalStep ensures the workflow contains the provided step. func ValidateApprovalStep(workflow ApprovalWorkflowKey, step ApprovalStep) error { if _, ok := ApprovalStepName(workflow, step); ok { return nil } return fmt.Errorf("invalid approval step %d for workflow %s", step, workflow) } // IsValidApprovalAction reports whether the action is supported. func IsValidApprovalAction(action entity.ApprovalAction) bool { _, ok := approvalActions[action] return ok } // NewApproval creates an approval record for the given approvable target. func NewApproval(workflow ApprovalWorkflowKey, approvableId uint, step ApprovalStep, action *entity.ApprovalAction, actorId uint, note *string) (*entity.Approval, error) { if approvableId == 0 { return nil, errors.New("approvable id is required") } workflowStr := strings.TrimSpace(workflow.String()) if workflowStr == "" { return nil, errors.New("approval workflow key is required") } key := ApprovalWorkflowKey(workflowStr) if err := ValidateApprovalStep(key, step); err != nil { return nil, err } var actionPtr *entity.ApprovalAction if action != nil { if !IsValidApprovalAction(*action) { return nil, fmt.Errorf("invalid approval action %q", *action) } actionCopy := *action actionPtr = &actionCopy } if actorId == 0 { return nil, errors.New("actor id is required") } var notes *string if note != nil { trimmed := strings.TrimSpace(*note) if trimmed != "" { notes = &trimmed } } actor := actorId var stepName string if label, ok := ApprovalStepName(key, step); ok { labelCopy := label stepName = labelCopy } return &entity.Approval{ ApprovableType: workflowStr, ApprovableId: approvableId, StepNumber: uint16(step), StepName: stepName, Action: actionPtr, Notes: notes, ActionBy: &actor, }, nil } // SetApprovalAction updates the approval action, notes, and optionally advances to another step. func SetApprovalAction(approval *entity.Approval, action entity.ApprovalAction, actorId uint, note *string, nextStep NextStepCallback) error { if approval == nil { return errors.New("approval is nil") } if !IsValidApprovalAction(action) { return fmt.Errorf("invalid approval action %q", action) } if actorId == 0 { return errors.New("actor id is required for approval decision") } act := action approval.Action = &act approval.ActionBy = &actorId if note != nil { trimmed := strings.TrimSpace(*note) if trimmed == "" { approval.Notes = nil } else { approval.Notes = &trimmed } } else { approval.Notes = nil } if nextStep != nil { current := ApprovalStep(approval.StepNumber) if proposed, ok := nextStep(current, action); ok { if err := ValidateApprovalStep(ApprovalWorkflowKey(approval.ApprovableType), proposed); err != nil { return err } approval.StepNumber = uint16(proposed) } } if label, ok := ApprovalStepName(ApprovalWorkflowKey(approval.ApprovableType), ApprovalStep(approval.StepNumber)); ok { labelCopy := label approval.StepName = labelCopy } return nil } // Approve marks the approval as approved by the given actor, applying the optional step callback. func Approve(approval *entity.Approval, actorId uint, note *string, nextStep NextStepCallback) error { return SetApprovalAction(approval, entity.ApprovalActionApproved, actorId, note, nextStep) } // Reject marks the approval as rejected by the given actor, applying the optional step callback. func Reject(approval *entity.Approval, actorId uint, note *string, nextStep NextStepCallback) error { return SetApprovalAction(approval, entity.ApprovalActionRejected, actorId, note, nextStep) }