feat(BE): approval_workflow, adjusment project_flocks, common, and migration

This commit is contained in:
Hafizh A. Y
2025-10-21 13:56:30 +07:00
parent 13c04460f0
commit 55b14f5fc7
30 changed files with 1379 additions and 159 deletions
@@ -0,0 +1,243 @@
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)
}