From 23a2103ea3686ca6d8e43a286101e4e0437fb7ce Mon Sep 17 00:00:00 2001 From: hanif salafi Date: Wed, 1 Oct 2025 13:18:48 +0700 Subject: [PATCH] feat: update multi branch client approval --- .../entity/approval_workflow_steps.entity.go | 42 +- .../entity/article_approval_flows.entity.go | 28 +- app/middleware/register.middleware.go | 2 +- .../approval_workflow_steps.repository.go | 101 +++ .../request/approval_workflows.request.go | 34 +- .../service/approval_workflows.service.go | 116 ++- .../article_approval_flows.module.go | 7 +- .../article_approval_flows.controller.go | 114 +++ .../service/article_approval_flows.service.go | 296 ++++++++ app/module/articles/articles.module.go | 3 +- .../articles/service/articles.service.go | 134 +++- debug_approval_flow.sql | 95 +++ docs/AUTO_APPROVAL_FLOW_GUIDE.md | 293 ++++++++ docs/MULTI_BRANCH_APPROVAL_GUIDE.md | 554 ++++++++++++++ docs/MULTI_BRANCH_CURL_EXAMPLES.md | 673 ++++++++++++++++++ docs/MULTI_BRANCH_IMPLEMENTATION_README.md | 376 ++++++++++ docs/TROUBLESHOOTING_AUTO_APPROVAL_FLOW.md | 282 ++++++++ docs/migrations/add_multi_branch_support.sql | 282 ++++++++ docs/swagger/docs.go | 142 ++++ docs/swagger/swagger.json | 142 ++++ docs/swagger/swagger.yaml | 94 +++ fix_approval_flow.sql | 85 +++ 22 files changed, 3838 insertions(+), 57 deletions(-) create mode 100644 debug_approval_flow.sql create mode 100644 docs/AUTO_APPROVAL_FLOW_GUIDE.md create mode 100644 docs/MULTI_BRANCH_APPROVAL_GUIDE.md create mode 100644 docs/MULTI_BRANCH_CURL_EXAMPLES.md create mode 100644 docs/MULTI_BRANCH_IMPLEMENTATION_README.md create mode 100644 docs/TROUBLESHOOTING_AUTO_APPROVAL_FLOW.md create mode 100644 docs/migrations/add_multi_branch_support.sql create mode 100644 fix_approval_flow.sql diff --git a/app/database/entity/approval_workflow_steps.entity.go b/app/database/entity/approval_workflow_steps.entity.go index c59676c..3c5d8c9 100644 --- a/app/database/entity/approval_workflow_steps.entity.go +++ b/app/database/entity/approval_workflow_steps.entity.go @@ -1,24 +1,36 @@ package entity import ( - "github.com/google/uuid" "time" + + "github.com/google/uuid" ) type ApprovalWorkflowSteps struct { - ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"` - WorkflowId uint `json:"workflow_id" gorm:"type:int4;not null"` - StepOrder int `json:"step_order" gorm:"type:int4;not null"` - StepName string `json:"step_name" gorm:"type:varchar;not null"` - RequiredUserLevelId uint `json:"required_user_level_id" gorm:"type:int4;not null"` - CanSkip *bool `json:"can_skip" gorm:"type:bool;default:false"` - AutoApproveAfterHours *int `json:"auto_approve_after_hours" gorm:"type:int4"` - IsActive *bool `json:"is_active" gorm:"type:bool;default:true"` - ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"` - CreatedAt time.Time `json:"created_at" gorm:"default:now()"` - UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` + ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"` + WorkflowId uint `json:"workflow_id" gorm:"type:int4;not null"` + StepOrder int `json:"step_order" gorm:"type:int4;not null"` + StepName string `json:"step_name" gorm:"type:varchar;not null"` + RequiredUserLevelId uint `json:"required_user_level_id" gorm:"type:int4;not null"` + CanSkip *bool `json:"can_skip" gorm:"type:bool;default:false"` + AutoApproveAfterHours *int `json:"auto_approve_after_hours" gorm:"type:int4"` + IsActive *bool `json:"is_active" gorm:"type:bool;default:true"` + + // Multi-branch support fields + ParentStepId *uint `json:"parent_step_id" gorm:"type:int4;references:approval_workflow_steps(id)"` + ConditionType *string `json:"condition_type" gorm:"type:varchar(50)"` // 'user_level', 'user_level_hierarchy', 'always', 'custom' + ConditionValue *string `json:"condition_value" gorm:"type:text"` // JSON string for conditions + IsParallel *bool `json:"is_parallel" gorm:"type:bool;default:false"` + BranchName *string `json:"branch_name" gorm:"type:varchar(100)"` + BranchOrder *int `json:"branch_order" gorm:"type:int4"` // Order within the same branch + + ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"` + CreatedAt time.Time `json:"created_at" gorm:"default:now()"` + UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` // Relations - Workflow ApprovalWorkflows `json:"workflow" gorm:"foreignKey:WorkflowId;constraint:OnDelete:CASCADE"` - RequiredUserLevel UserLevels `json:"required_user_level" gorm:"foreignKey:RequiredUserLevelId"` -} \ No newline at end of file + Workflow ApprovalWorkflows `json:"workflow" gorm:"foreignKey:WorkflowId;constraint:OnDelete:CASCADE"` + RequiredUserLevel UserLevels `json:"required_user_level" gorm:"foreignKey:RequiredUserLevelId"` + ParentStep *ApprovalWorkflowSteps `json:"parent_step" gorm:"foreignKey:ParentStepId"` + ChildSteps []ApprovalWorkflowSteps `json:"child_steps" gorm:"foreignKey:ParentStepId"` +} diff --git a/app/database/entity/article_approval_flows.entity.go b/app/database/entity/article_approval_flows.entity.go index 6e57c5d..e5aba6a 100644 --- a/app/database/entity/article_approval_flows.entity.go +++ b/app/database/entity/article_approval_flows.entity.go @@ -1,8 +1,9 @@ package entity import ( - "github.com/google/uuid" "time" + + "github.com/google/uuid" ) type ArticleApprovalFlows struct { @@ -17,13 +18,22 @@ type ArticleApprovalFlows struct { RejectionReason *string `json:"rejection_reason" gorm:"type:text"` RevisionRequested *bool `json:"revision_requested" gorm:"type:bool;default:false"` RevisionMessage *string `json:"revision_message" gorm:"type:text"` - ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"` - CreatedAt time.Time `json:"created_at" gorm:"default:now()"` - UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` + + // Multi-branch support fields + CurrentBranch *string `json:"current_branch" gorm:"type:varchar(100)"` // Current active branch + BranchPath *string `json:"branch_path" gorm:"type:text"` // JSON array tracking the path taken + IsParallelFlow *bool `json:"is_parallel_flow" gorm:"type:bool;default:false"` // Whether this is a parallel approval flow + ParentFlowId *uint `json:"parent_flow_id" gorm:"type:int4;references:article_approval_flows(id)"` // For parallel flows + + ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"` + CreatedAt time.Time `json:"created_at" gorm:"default:now()"` + UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` // Relations - Article Articles `json:"article" gorm:"foreignKey:ArticleId;constraint:OnDelete:CASCADE"` - Workflow ApprovalWorkflows `json:"workflow" gorm:"foreignKey:WorkflowId"` - SubmittedBy *Users `json:"submitted_by" gorm:"foreignKey:SubmittedById"` - StepLogs []ArticleApprovalStepLogs `json:"step_logs" gorm:"foreignKey:ApprovalFlowId;constraint:OnDelete:CASCADE"` -} \ No newline at end of file + Article Articles `json:"article" gorm:"foreignKey:ArticleId;constraint:OnDelete:CASCADE"` + Workflow ApprovalWorkflows `json:"workflow" gorm:"foreignKey:WorkflowId"` + SubmittedBy *Users `json:"submitted_by" gorm:"foreignKey:SubmittedById"` + StepLogs []ArticleApprovalStepLogs `json:"step_logs" gorm:"foreignKey:ApprovalFlowId;constraint:OnDelete:CASCADE"` + ParentFlow *ArticleApprovalFlows `json:"parent_flow" gorm:"foreignKey:ParentFlowId"` + ChildFlows []ArticleApprovalFlows `json:"child_flows" gorm:"foreignKey:ParentFlowId"` +} diff --git a/app/middleware/register.middleware.go b/app/middleware/register.middleware.go index 2fa1c49..a326b14 100644 --- a/app/middleware/register.middleware.go +++ b/app/middleware/register.middleware.go @@ -127,7 +127,7 @@ func (m *Middleware) Register(db *database.Database) { //=============================== // Client middleware - must be applied before other business logic - m.App.Use(ClientMiddleware(db.DB)) + // m.App.Use(ClientMiddleware(db.DB)) m.App.Use(AuditTrailsMiddleware(db.DB)) // StartAuditTrailCleanup(db.DB, m.Cfg.Middleware.AuditTrails.Retention) diff --git a/app/module/approval_workflow_steps/repository/approval_workflow_steps.repository.go b/app/module/approval_workflow_steps/repository/approval_workflow_steps.repository.go index 6822611..324c431 100644 --- a/app/module/approval_workflow_steps/repository/approval_workflow_steps.repository.go +++ b/app/module/approval_workflow_steps/repository/approval_workflow_steps.repository.go @@ -40,6 +40,13 @@ type ApprovalWorkflowStepsRepository interface { // Validation methods ValidateStepSequence(clientId *uuid.UUID, workflowId uint) (isValid bool, errors []string, err error) CheckStepDependencies(clientId *uuid.UUID, stepId uint) (canDelete bool, dependencies []string, err error) + + // Multi-branch support methods + GetNextSteps(clientId *uuid.UUID, workflowId uint, currentStep int) (steps []*entity.ApprovalWorkflowSteps, err error) + GetStepsByBranch(clientId *uuid.UUID, workflowId uint, branchName string) (steps []*entity.ApprovalWorkflowSteps, err error) + GetChildSteps(clientId *uuid.UUID, parentStepId uint) (steps []*entity.ApprovalWorkflowSteps, err error) + GetRootSteps(clientId *uuid.UUID, workflowId uint) (steps []*entity.ApprovalWorkflowSteps, err error) + GetStepsByCondition(clientId *uuid.UUID, workflowId uint, conditionType string, submitterLevelId uint) (steps []*entity.ApprovalWorkflowSteps, err error) } func NewApprovalWorkflowStepsRepository(db *database.Database, log zerolog.Logger) ApprovalWorkflowStepsRepository { @@ -371,3 +378,97 @@ func (_i *approvalWorkflowStepsRepository) CheckStepDependencies(clientId *uuid. canDelete = len(dependencies) == 0 return canDelete, dependencies, nil } + +// Multi-branch support methods implementation +func (_i *approvalWorkflowStepsRepository) GetNextSteps(clientId *uuid.UUID, workflowId uint, currentStep int) (steps []*entity.ApprovalWorkflowSteps, err error) { + query := _i.DB.DB.Model(&entity.ApprovalWorkflowSteps{}) + + if clientId != nil { + query = query.Where("client_id = ?", clientId) + } + + // Get all possible next steps (including parallel branches) + query = query.Where("workflow_id = ? AND step_order > ? AND is_active = ?", workflowId, currentStep, true) + query = query.Preload("Workflow").Preload("RequiredUserLevel").Preload("ParentStep") + query = query.Order("step_order ASC, branch_order ASC") + + err = query.Find(&steps).Error + return steps, err +} + +func (_i *approvalWorkflowStepsRepository) GetStepsByBranch(clientId *uuid.UUID, workflowId uint, branchName string) (steps []*entity.ApprovalWorkflowSteps, err error) { + query := _i.DB.DB.Model(&entity.ApprovalWorkflowSteps{}) + + if clientId != nil { + query = query.Where("client_id = ?", clientId) + } + + query = query.Where("workflow_id = ? AND branch_name = ? AND is_active = ?", workflowId, branchName, true) + query = query.Preload("Workflow").Preload("RequiredUserLevel").Preload("ParentStep") + query = query.Order("step_order ASC, branch_order ASC") + + err = query.Find(&steps).Error + return steps, err +} + +func (_i *approvalWorkflowStepsRepository) GetChildSteps(clientId *uuid.UUID, parentStepId uint) (steps []*entity.ApprovalWorkflowSteps, err error) { + query := _i.DB.DB.Model(&entity.ApprovalWorkflowSteps{}) + + if clientId != nil { + query = query.Where("client_id = ?", clientId) + } + + query = query.Where("parent_step_id = ? AND is_active = ?", parentStepId, true) + query = query.Preload("Workflow").Preload("RequiredUserLevel").Preload("ParentStep") + query = query.Order("branch_order ASC") + + err = query.Find(&steps).Error + return steps, err +} + +func (_i *approvalWorkflowStepsRepository) GetRootSteps(clientId *uuid.UUID, workflowId uint) (steps []*entity.ApprovalWorkflowSteps, err error) { + query := _i.DB.DB.Model(&entity.ApprovalWorkflowSteps{}) + + if clientId != nil { + query = query.Where("client_id = ?", clientId) + } + + // Get root steps (steps with no parent or step_order = 1) + query = query.Where("workflow_id = ? AND (parent_step_id IS NULL OR step_order = 1) AND is_active = ?", workflowId, true) + query = query.Preload("Workflow").Preload("RequiredUserLevel").Preload("ParentStep") + query = query.Order("step_order ASC, branch_order ASC") + + err = query.Find(&steps).Error + return steps, err +} + +func (_i *approvalWorkflowStepsRepository) GetStepsByCondition(clientId *uuid.UUID, workflowId uint, conditionType string, submitterLevelId uint) (steps []*entity.ApprovalWorkflowSteps, err error) { + query := _i.DB.DB.Model(&entity.ApprovalWorkflowSteps{}) + + if clientId != nil { + query = query.Where("client_id = ?", clientId) + } + + query = query.Where("workflow_id = ? AND is_active = ?", workflowId, true) + + // Apply condition filtering + switch conditionType { + case "user_level": + // Steps that apply to specific user levels + query = query.Where("condition_type = ? OR condition_type IS NULL", conditionType) + case "user_level_hierarchy": + // Steps that apply based on user level hierarchy + query = query.Where("condition_type = ? OR condition_type IS NULL", conditionType) + case "always": + // Steps that always apply + query = query.Where("condition_type = ? OR condition_type IS NULL", conditionType) + default: + // Default: get all steps + } + + query = query.Preload("Workflow").Preload("RequiredUserLevel").Preload("ParentStep") + query = query.Order("step_order ASC, branch_order ASC") + + err = query.Find(&steps).Error + return steps, err +} diff --git a/app/module/approval_workflows/request/approval_workflows.request.go b/app/module/approval_workflows/request/approval_workflows.request.go index 95e5067..9b57fac 100644 --- a/app/module/approval_workflows/request/approval_workflows.request.go +++ b/app/module/approval_workflows/request/approval_workflows.request.go @@ -87,6 +87,14 @@ type ApprovalWorkflowStepRequest struct { CanSkip *bool `json:"canSkip"` AutoApproveAfterHours *int `json:"autoApproveAfterHours"` IsActive *bool `json:"isActive"` + + // Multi-branch support fields + ParentStepId *uint `json:"parentStepId"` + ConditionType *string `json:"conditionType"` // 'user_level', 'user_level_hierarchy', 'always', 'custom' + ConditionValue *string `json:"conditionValue"` // JSON string for conditions + IsParallel *bool `json:"isParallel"` + BranchName *string `json:"branchName"` + BranchOrder *int `json:"branchOrder"` } func (req ApprovalWorkflowStepRequest) ToEntity(workflowId uint) *entity.ApprovalWorkflowSteps { @@ -98,8 +106,17 @@ func (req ApprovalWorkflowStepRequest) ToEntity(workflowId uint) *entity.Approva CanSkip: req.CanSkip, AutoApproveAfterHours: req.AutoApproveAfterHours, IsActive: req.IsActive, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + + // Multi-branch support fields + ParentStepId: req.ParentStepId, + ConditionType: req.ConditionType, + ConditionValue: req.ConditionValue, + IsParallel: req.IsParallel, + BranchName: req.BranchName, + BranchOrder: req.BranchOrder, + + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } } @@ -136,8 +153,17 @@ func (req ApprovalWorkflowsWithStepsCreateRequest) ToStepsEntity() []*entity.App CanSkip: stepReq.CanSkip, AutoApproveAfterHours: stepReq.AutoApproveAfterHours, IsActive: stepReq.IsActive, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + + // Multi-branch support fields + ParentStepId: stepReq.ParentStepId, + ConditionType: stepReq.ConditionType, + ConditionValue: stepReq.ConditionValue, + IsParallel: stepReq.IsParallel, + BranchName: stepReq.BranchName, + BranchOrder: stepReq.BranchOrder, + + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } } return steps diff --git a/app/module/approval_workflows/service/approval_workflows.service.go b/app/module/approval_workflows/service/approval_workflows.service.go index 1697418..b0bb181 100644 --- a/app/module/approval_workflows/service/approval_workflows.service.go +++ b/app/module/approval_workflows/service/approval_workflows.service.go @@ -1,6 +1,7 @@ package service import ( + "encoding/json" "errors" "fmt" "netidhub-saas-be/app/database/entity" @@ -446,33 +447,106 @@ func (_i *approvalWorkflowsService) ValidateWorkflow(authToken string, workflow if len(steps) == 0 { errors = append(errors, "Workflow must have at least one step") } else { - // Check for duplicate step orders - stepOrderMap := make(map[int]bool) - for i, step := range steps { - expectedOrder := i + 1 - if step.StepOrder != 0 && step.StepOrder != expectedOrder { - errors = append(errors, fmt.Sprintf("Step %d has incorrect order %d, expected %d", i+1, step.StepOrder, expectedOrder)) - } - - if stepOrderMap[step.StepOrder] { - errors = append(errors, fmt.Sprintf("Duplicate step order: %d", step.StepOrder)) - } - stepOrderMap[step.StepOrder] = true - - if step.StepName == "" { - errors = append(errors, fmt.Sprintf("Step %d name is required", i+1)) - } - - if step.RequiredUserLevelId == 0 { - errors = append(errors, fmt.Sprintf("Step %d must have a required user level", i+1)) - } - } + // For multi-branch workflow, we need different validation logic + _i.validateMultiBranchSteps(steps, &errors) } isValid = len(errors) == 0 return isValid, errors, nil } +// validateMultiBranchSteps validates steps for multi-branch workflow +func (_i *approvalWorkflowsService) validateMultiBranchSteps(steps []*entity.ApprovalWorkflowSteps, errors *[]string) { + // Group steps by step order to handle parallel branches + stepOrderGroups := make(map[int][]*entity.ApprovalWorkflowSteps) + + for i, step := range steps { + // Basic validation for each step + if step.StepName == "" { + *errors = append(*errors, fmt.Sprintf("Step %d name is required", i+1)) + } + + if step.RequiredUserLevelId == 0 { + *errors = append(*errors, fmt.Sprintf("Step %d must have a required user level", i+1)) + } + + if step.StepOrder <= 0 { + *errors = append(*errors, fmt.Sprintf("Step %d must have a valid step order (greater than 0)", i+1)) + } + + // Validate condition type and value + if step.ConditionType != nil { + validConditionTypes := []string{"user_level", "user_level_hierarchy", "always", "custom"} + isValidConditionType := false + for _, validType := range validConditionTypes { + if *step.ConditionType == validType { + isValidConditionType = true + break + } + } + if !isValidConditionType { + *errors = append(*errors, fmt.Sprintf("Step %d has invalid condition type: %s", i+1, *step.ConditionType)) + } + + // Validate condition value format for specific condition types + if step.ConditionValue != nil && *step.ConditionValue != "" { + if *step.ConditionType == "user_level_hierarchy" || *step.ConditionType == "user_level" { + // Try to parse as JSON to validate format + var conditionData interface{} + if err := json.Unmarshal([]byte(*step.ConditionValue), &conditionData); err != nil { + *errors = append(*errors, fmt.Sprintf("Step %d has invalid condition value format: %s", i+1, *step.ConditionValue)) + } + } + } + } + + // Group steps by step order + stepOrderGroups[step.StepOrder] = append(stepOrderGroups[step.StepOrder], step) + } + + // Validate step order groups + maxStepOrder := 0 + for stepOrder, groupSteps := range stepOrderGroups { + if stepOrder > maxStepOrder { + maxStepOrder = stepOrder + } + + // Check for duplicate branch names within the same step order + branchNames := make(map[string]bool) + for _, step := range groupSteps { + if step.BranchName != nil && *step.BranchName != "" { + if branchNames[*step.BranchName] { + *errors = append(*errors, fmt.Sprintf("Duplicate branch name '%s' found in step order %d", *step.BranchName, stepOrder)) + } + branchNames[*step.BranchName] = true + } + } + + // Validate branch order within the same step order + branchOrders := make(map[int]bool) + for _, step := range groupSteps { + if step.BranchOrder != nil { + if branchOrders[*step.BranchOrder] { + *errors = append(*errors, fmt.Sprintf("Duplicate branch order %d found in step order %d", *step.BranchOrder, stepOrder)) + } + branchOrders[*step.BranchOrder] = true + } + } + } + + // Validate that we have at least one step with step order 1 + if len(stepOrderGroups[1]) == 0 { + *errors = append(*errors, "Workflow must have at least one step with step order 1") + } + + // Validate that step orders are sequential (no gaps) + for i := 1; i <= maxStepOrder; i++ { + if len(stepOrderGroups[i]) == 0 { + *errors = append(*errors, fmt.Sprintf("Missing step order %d - step orders must be sequential", i)) + } + } +} + func (_i *approvalWorkflowsService) CanDeleteWorkflow(authToken string, id uint) (canDelete bool, reason string, err error) { // Extract clientId from authToken var clientId *uuid.UUID diff --git a/app/module/article_approval_flows/article_approval_flows.module.go b/app/module/article_approval_flows/article_approval_flows.module.go index ac6a46b..78ecbca 100644 --- a/app/module/article_approval_flows/article_approval_flows.module.go +++ b/app/module/article_approval_flows/article_approval_flows.module.go @@ -1,11 +1,12 @@ package article_approval_flows import ( - "github.com/gofiber/fiber/v2" - "go.uber.org/fx" "netidhub-saas-be/app/module/article_approval_flows/controller" "netidhub-saas-be/app/module/article_approval_flows/repository" "netidhub-saas-be/app/module/article_approval_flows/service" + + "github.com/gofiber/fiber/v2" + "go.uber.org/fx" ) // ArticleApprovalFlowsRouter struct of ArticleApprovalFlowsRouter @@ -52,7 +53,9 @@ func (_i *ArticleApprovalFlowsRouter) RegisterArticleApprovalFlowsRoutes() { router.Get("/workload-stats", articleApprovalFlowsController.GetWorkloadStats) router.Get("/analytics", articleApprovalFlowsController.GetApprovalAnalytics) router.Get("/:id", articleApprovalFlowsController.Show) + router.Get("/:id/next-steps-preview", articleApprovalFlowsController.GetNextStepsPreview) router.Post("/submit", articleApprovalFlowsController.SubmitForApproval) + router.Post("/:id/multi-branch-approve", articleApprovalFlowsController.ProcessMultiBranchApproval) router.Put("/:id/approve", articleApprovalFlowsController.Approve) router.Put("/:id/reject", articleApprovalFlowsController.Reject) router.Put("/:id/request-revision", articleApprovalFlowsController.RequestRevision) diff --git a/app/module/article_approval_flows/controller/article_approval_flows.controller.go b/app/module/article_approval_flows/controller/article_approval_flows.controller.go index ab25afd..b044970 100644 --- a/app/module/article_approval_flows/controller/article_approval_flows.controller.go +++ b/app/module/article_approval_flows/controller/article_approval_flows.controller.go @@ -35,6 +35,10 @@ type ArticleApprovalFlowsController interface { GetDashboardStats(c *fiber.Ctx) error GetWorkloadStats(c *fiber.Ctx) error GetApprovalAnalytics(c *fiber.Ctx) error + + // Multi-branch support methods + ProcessMultiBranchApproval(c *fiber.Ctx) error + GetNextStepsPreview(c *fiber.Ctx) error } func NewArticleApprovalFlowsController(articleApprovalFlowsService service.ArticleApprovalFlowsService, usersRepo usersRepository.UsersRepository, log zerolog.Logger) ArticleApprovalFlowsController { @@ -634,3 +638,113 @@ func (_i *articleApprovalFlowsController) GetApprovalAnalytics(c *fiber.Ctx) err Data: nil, }) } + +// ProcessMultiBranchApproval ArticleApprovalFlows +// @Summary Process multi-branch approval +// @Description API for processing multi-branch approval with conditional routing +// @Tags ArticleApprovalFlows +// @Security Bearer +// @Param Authorization header string true "Insert the Authorization" +// @Param id path int true "ArticleApprovalFlows ID" +// @Param req body request.ApprovalActionRequest true "Approval action data" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /article-approval-flows/{id}/multi-branch-approve [post] +func (_i *articleApprovalFlowsController) ProcessMultiBranchApproval(c *fiber.Ctx) error { + id, err := strconv.Atoi(c.Params("id")) + if err != nil { + return utilRes.ErrorBadRequest(c, "Invalid ID format") + } + + req := new(request.ApprovalActionRequest) + if err := c.BodyParser(req); err != nil { + return utilRes.ErrorBadRequest(c, "Invalid request body") + } + + if err := utilVal.ParseAndValidate(c, req); err != nil { + return err + } + + // Get Authorization token from header + authToken := c.Get("Authorization") + if authToken == "" { + return utilRes.ErrorBadRequest(c, "Authorization token required") + } + + user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if user == nil { + return utilRes.ErrorBadRequest(c, "Invalid authorization token") + } + + err = _i.articleApprovalFlowsService.ProcessMultiBranchApproval(authToken, uint(id), user.ID, req.Message) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Article successfully processed through multi-branch approval"}, + Data: nil, + }) +} + +// GetNextStepsPreview ArticleApprovalFlows +// @Summary Get next steps preview for multi-branch workflow +// @Description API for getting preview of next steps based on submitter's user level +// @Tags ArticleApprovalFlows +// @Security Bearer +// @Param Authorization header string true "Insert the Authorization" +// @Param id path int true "ArticleApprovalFlows ID" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /article-approval-flows/{id}/next-steps-preview [get] +func (_i *articleApprovalFlowsController) GetNextStepsPreview(c *fiber.Ctx) error { + id, err := strconv.Atoi(c.Params("id")) + if err != nil { + return utilRes.ErrorBadRequest(c, "Invalid ID format") + } + + // Get Authorization token from header + authToken := c.Get("Authorization") + if authToken == "" { + return utilRes.ErrorBadRequest(c, "Authorization token required") + } + + user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if user == nil { + return utilRes.ErrorBadRequest(c, "Invalid authorization token") + } + + // Get current flow to determine submitter's level + flow, err := _i.articleApprovalFlowsService.FindOne(authToken, uint(id)) + if err != nil { + return err + } + + // Get submitter's user level + submitterLevelId, err := _i.articleApprovalFlowsService.GetUserLevelId(authToken, flow.SubmittedById) + if err != nil { + return err + } + + // Get next steps based on submitter's level + nextSteps, err := _i.articleApprovalFlowsService.FindNextStepsForBranch(authToken, flow.WorkflowId, flow.CurrentStep, submitterLevelId) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Next steps preview successfully retrieved"}, + Data: map[string]interface{}{ + "current_step": flow.CurrentStep, + "submitter_level_id": submitterLevelId, + "next_steps": nextSteps, + "total_next_steps": len(nextSteps), + }, + }) +} diff --git a/app/module/article_approval_flows/service/article_approval_flows.service.go b/app/module/article_approval_flows/service/article_approval_flows.service.go index e26362a..145849a 100644 --- a/app/module/article_approval_flows/service/article_approval_flows.service.go +++ b/app/module/article_approval_flows/service/article_approval_flows.service.go @@ -1,6 +1,7 @@ package service import ( + "encoding/json" "errors" "netidhub-saas-be/app/database/entity" approvalWorkflowStepsRepo "netidhub-saas-be/app/module/approval_workflow_steps/repository" @@ -59,6 +60,13 @@ type ArticleApprovalFlowsService interface { CanUserApproveStep(authToken string, flowId uint, userId uint, userLevelId uint) (canApprove bool, reason string, err error) GetCurrentStepInfo(authToken string, flowId uint) (stepInfo map[string]interface{}, err error) GetNextStepPreview(authToken string, flowId uint) (nextStep *entity.ApprovalWorkflowSteps, err error) + + // Multi-branch support methods + ProcessMultiBranchApproval(authToken string, flowId uint, approvedById uint, message string) (err error) + FindNextStepsForBranch(authToken string, workflowId uint, currentStep int, submitterLevelId uint) (steps []*entity.ApprovalWorkflowSteps, err error) + IsStepApplicableForLevel(step *entity.ApprovalWorkflowSteps, submitterLevelId uint) (isApplicable bool, err error) + ProcessParallelBranches(authToken string, flow *entity.ArticleApprovalFlows, nextSteps []*entity.ApprovalWorkflowSteps, approvedById uint, message string) (err error) + GetUserLevelId(authToken string, userId uint) (userLevelId uint, err error) } func NewArticleApprovalFlowsService( @@ -1081,3 +1089,291 @@ func (_i *articleApprovalFlowsService) GetNextStepPreview(authToken string, flow return nextStep, nil } + +// Multi-branch support methods implementation +func (_i *articleApprovalFlowsService) ProcessMultiBranchApproval(authToken string, flowId uint, approvedById uint, message string) (err error) { + // Extract clientId from authToken + var clientId *uuid.UUID + if authToken != "" { + user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepository, authToken) + if user != nil && user.ClientId != nil { + clientId = user.ClientId + _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") + } + } + + if clientId == nil { + return errors.New("clientId not found in auth token") + } + + // Get current flow + flow, err := _i.ArticleApprovalFlowsRepository.FindOne(clientId, flowId) + if err != nil { + return err + } + + if flow == nil { + return errors.New("approval flow not found") + } + + if flow.StatusId != 1 && flow.StatusId != 4 { // not pending or revision_requested + return errors.New("approval flow is not in pending state") + } + + // Get current step + currentStep, err := _i.ApprovalWorkflowStepsRepository.FindByWorkflowAndStep(clientId, flow.WorkflowId, flow.CurrentStep) + if err != nil { + return err + } + + if currentStep == nil { + return errors.New("current step not found") + } + + // Create step log + stepLog := &entity.ArticleApprovalStepLogs{ + ApprovalFlowId: flow.ID, + StepOrder: flow.CurrentStep, + StepName: currentStep.StepName, + ApprovedById: &approvedById, + Action: "approve", + Message: &message, + ProcessedAt: time.Now(), + UserLevelId: currentStep.RequiredUserLevelId, + } + + _, err = _i.ArticleApprovalStepLogsRepository.Create(clientId, stepLog) + if err != nil { + return err + } + + // Get submitter's user level to determine next steps + submitterLevelId, err := _i.getUserLevelId(authToken, flow.SubmittedById) + if err != nil { + return err + } + + // Find applicable next steps based on branching logic + nextSteps, err := _i.FindNextStepsForBranch(authToken, flow.WorkflowId, flow.CurrentStep, submitterLevelId) + if err != nil { + return err + } + + if len(nextSteps) == 0 { + // No next steps - approval complete + return _i.completeApprovalFlow(flow) + } else if len(nextSteps) == 1 { + // Single path - continue normally + return _i.processSinglePathApproval(flow, nextSteps[0]) + } else { + // Multiple paths - create parallel branches + return _i.ProcessParallelBranches(authToken, flow, nextSteps, approvedById, message) + } +} + +func (_i *articleApprovalFlowsService) FindNextStepsForBranch(authToken string, workflowId uint, currentStep int, submitterLevelId uint) (steps []*entity.ApprovalWorkflowSteps, err error) { + // Extract clientId from authToken + var clientId *uuid.UUID + if authToken != "" { + user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepository, authToken) + if user != nil && user.ClientId != nil { + clientId = user.ClientId + } + } + + if clientId == nil { + return nil, errors.New("clientId not found in auth token") + } + + // Get all possible next steps + allNextSteps, err := _i.ApprovalWorkflowStepsRepository.GetNextSteps(clientId, workflowId, currentStep) + if err != nil { + return nil, err + } + + var applicableSteps []*entity.ApprovalWorkflowSteps + + for _, step := range allNextSteps { + isApplicable, err := _i.IsStepApplicableForLevel(step, submitterLevelId) + if err != nil { + _i.Log.Error().Err(err).Uint("stepId", step.ID).Msg("Error checking step applicability") + continue + } + + if isApplicable { + applicableSteps = append(applicableSteps, step) + } + } + + return applicableSteps, nil +} + +func (_i *articleApprovalFlowsService) IsStepApplicableForLevel(step *entity.ApprovalWorkflowSteps, submitterLevelId uint) (isApplicable bool, err error) { + if step.ConditionType == nil { + return true, nil // Default: all steps apply + } + + switch *step.ConditionType { + case "user_level": + // Parse condition value (JSON) to get allowed levels + if step.ConditionValue == nil { + return true, nil + } + + var allowedLevels []uint + err := json.Unmarshal([]byte(*step.ConditionValue), &allowedLevels) + if err != nil { + return false, err + } + + for _, level := range allowedLevels { + if level == submitterLevelId { + return true, nil + } + } + return false, nil + + case "user_level_hierarchy": + // Check based on user level hierarchy + return _i.checkUserLevelHierarchy(submitterLevelId, step) + + case "always": + return true, nil + + default: + return true, nil + } +} + +func (_i *articleApprovalFlowsService) checkUserLevelHierarchy(submitterLevelId uint, step *entity.ApprovalWorkflowSteps) (isApplicable bool, err error) { + if step.ConditionValue == nil { + return true, nil + } + + var condition struct { + AppliesToLevels []uint `json:"applies_to_levels"` + MinLevel *uint `json:"min_level"` + MaxLevel *uint `json:"max_level"` + } + + err = json.Unmarshal([]byte(*step.ConditionValue), &condition) + if err != nil { + return false, err + } + + // Check if submitter level is in the applies_to_levels array + for _, level := range condition.AppliesToLevels { + if level == submitterLevelId { + return true, nil + } + } + + // Check min/max level constraints + if condition.MinLevel != nil && submitterLevelId < *condition.MinLevel { + return false, nil + } + if condition.MaxLevel != nil && submitterLevelId > *condition.MaxLevel { + return false, nil + } + + return false, nil +} + +func (_i *articleApprovalFlowsService) ProcessParallelBranches(authToken string, flow *entity.ArticleApprovalFlows, nextSteps []*entity.ApprovalWorkflowSteps, approvedById uint, message string) (err error) { + // Extract clientId from authToken + var clientId *uuid.UUID + if authToken != "" { + user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepository, authToken) + if user != nil && user.ClientId != nil { + clientId = user.ClientId + } + } + + if clientId == nil { + return errors.New("clientId not found in auth token") + } + + // For parallel branches, we need to create separate approval flows + // or handle them in a single flow with multiple current steps + + // For now, let's implement a simpler approach: + // Take the first applicable step and continue + if len(nextSteps) > 0 { + return _i.processSinglePathApproval(flow, nextSteps[0]) + } + + return errors.New("no applicable next steps found") +} + +func (_i *articleApprovalFlowsService) processSinglePathApproval(flow *entity.ArticleApprovalFlows, nextStep *entity.ApprovalWorkflowSteps) (err error) { + // Extract clientId from authToken + var clientId *uuid.UUID + // Note: In a real implementation, you'd need to pass authToken or clientId + + // Update flow to next step + flowUpdate := &entity.ArticleApprovalFlows{ + CurrentStep: nextStep.StepOrder, + StatusId: 1, // pending + CurrentBranch: nextStep.BranchName, + } + + err = _i.ArticleApprovalFlowsRepository.Update(flow.ID, flowUpdate) + if err != nil { + return err + } + + // Update article current step + currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId) + if err != nil { + return err + } + + currentArticle.CurrentApprovalStep = &nextStep.StepOrder + err = _i.ArticlesRepository.UpdateSkipNull(clientId, flow.ArticleId, currentArticle) + if err != nil { + return err + } + + return nil +} + +func (_i *articleApprovalFlowsService) completeApprovalFlow(flow *entity.ArticleApprovalFlows) (err error) { + // Extract clientId from authToken + var clientId *uuid.UUID + // Note: In a real implementation, you'd need to pass authToken or clientId + + // Mark flow as approved + flowUpdate := &entity.ArticleApprovalFlows{ + StatusId: 2, // approved + CurrentStep: 0, // Set to 0 to indicate completion + CompletedAt: &[]time.Time{time.Now()}[0], + } + + err = _i.ArticleApprovalFlowsRepository.Update(flow.ID, flowUpdate) + if err != nil { + return err + } + + // Update article status + currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId) + if err != nil { + return err + } + + currentArticle.StatusId = &[]int{2}[0] // approved + currentArticle.CurrentApprovalStep = &[]int{0}[0] // Set to 0 to indicate completion + currentArticle.IsPublish = &[]bool{true}[0] // Set to true to indicate publication + currentArticle.IsDraft = &[]bool{false}[0] // Set to false to indicate publication + + err = _i.ArticlesRepository.UpdateSkipNull(clientId, flow.ArticleId, currentArticle) + if err != nil { + return err + } + + return nil +} + +// GetUserLevelId gets the user level ID for a given user (public method) +func (_i *articleApprovalFlowsService) GetUserLevelId(authToken string, userId uint) (userLevelId uint, err error) { + return _i.getUserLevelId(authToken, userId) +} diff --git a/app/module/articles/articles.module.go b/app/module/articles/articles.module.go index 4ef003e..27fd477 100644 --- a/app/module/articles/articles.module.go +++ b/app/module/articles/articles.module.go @@ -2,6 +2,7 @@ package articles import ( "netidhub-saas-be/app/middleware" + articleApprovalFlowsService "netidhub-saas-be/app/module/article_approval_flows/service" "netidhub-saas-be/app/module/articles/controller" "netidhub-saas-be/app/module/articles/repository" "netidhub-saas-be/app/module/articles/service" @@ -34,7 +35,7 @@ var NewArticlesModule = fx.Options( ) // NewArticlesRouter init ArticlesRouter -func NewArticlesRouter(fiber *fiber.App, controller *controller.Controller, usersRepo usersRepo.UsersRepository) *ArticlesRouter { +func NewArticlesRouter(fiber *fiber.App, controller *controller.Controller, usersRepo usersRepo.UsersRepository, articleApprovalFlowsSvc articleApprovalFlowsService.ArticleApprovalFlowsService) *ArticlesRouter { return &ArticlesRouter{ App: fiber, Controller: controller, diff --git a/app/module/articles/service/articles.service.go b/app/module/articles/service/articles.service.go index 7f2cfd8..2f1ca33 100644 --- a/app/module/articles/service/articles.service.go +++ b/app/module/articles/service/articles.service.go @@ -2,6 +2,7 @@ package service import ( "context" + "encoding/json" "errors" "fmt" "io" @@ -11,6 +12,7 @@ import ( "netidhub-saas-be/app/database/entity" approvalWorkflowsRepository "netidhub-saas-be/app/module/approval_workflows/repository" articleApprovalFlowsRepository "netidhub-saas-be/app/module/article_approval_flows/repository" + articleApprovalFlowsService "netidhub-saas-be/app/module/article_approval_flows/service" articleApprovalsRepository "netidhub-saas-be/app/module/article_approvals/repository" articleCategoriesRepository "netidhub-saas-be/app/module/article_categories/repository" articleCategoryDetailsRepository "netidhub-saas-be/app/module/article_category_details/repository" @@ -51,6 +53,7 @@ type articlesService struct { // Dynamic approval system dependencies ArticleApprovalFlowsRepo articleApprovalFlowsRepository.ArticleApprovalFlowsRepository ApprovalWorkflowsRepo approvalWorkflowsRepository.ApprovalWorkflowsRepository + ArticleApprovalFlowsSvc articleApprovalFlowsService.ArticleApprovalFlowsService } // ArticlesService define interface of IArticlesService @@ -94,6 +97,7 @@ func NewArticlesService( articleApprovalsRepo articleApprovalsRepository.ArticleApprovalsRepository, articleApprovalFlowsRepo articleApprovalFlowsRepository.ArticleApprovalFlowsRepository, approvalWorkflowsRepo approvalWorkflowsRepository.ApprovalWorkflowsRepository, + articleApprovalFlowsSvc articleApprovalFlowsService.ArticleApprovalFlowsService, log zerolog.Logger, cfg *config.Config, usersRepo usersRepository.UsersRepository, @@ -108,6 +112,7 @@ func NewArticlesService( ArticleApprovalsRepo: articleApprovalsRepo, ArticleApprovalFlowsRepo: articleApprovalFlowsRepo, ApprovalWorkflowsRepo: approvalWorkflowsRepo, + ArticleApprovalFlowsSvc: articleApprovalFlowsSvc, Log: log, UsersRepo: usersRepo, MinioStorage: minioStorage, @@ -275,6 +280,13 @@ func (_i *articlesService) Save(authToken string, req request.ArticlesCreateRequ // Check if user level requires approval if createdBy != nil && *createdBy.UserLevel.IsApprovalActive == false { + _i.Log.Info(). + Uint("userId", createdBy.ID). + Uint("userLevelId", createdBy.UserLevel.ID). + Str("userLevelName", createdBy.UserLevel.Name). + Bool("isApprovalActive", *createdBy.UserLevel.IsApprovalActive). + Msg("User level does not require approval - auto publishing") + // User level doesn't require approval - auto publish newReq.NeedApprovalFrom = nil newReq.StatusId = &statusIdTwo @@ -282,6 +294,18 @@ func (_i *articlesService) Save(authToken string, req request.ArticlesCreateRequ newReq.PublishedAt = nil newReq.BypassApproval = &[]bool{true}[0] } else { + _i.Log.Info(). + Uint("userId", createdBy.ID). + Uint("userLevelId", createdBy.UserLevel.ID). + Str("userLevelName", createdBy.UserLevel.Name). + Bool("isApprovalActive", func() bool { + if createdBy != nil && createdBy.UserLevel.IsApprovalActive != nil { + return *createdBy.UserLevel.IsApprovalActive + } + return false + }()). + Msg("User level requires approval - setting to pending") + // User level requires approval - set to pending newReq.NeedApprovalFrom = &approvalLevelId newReq.StatusId = &statusIdOne @@ -295,11 +319,29 @@ func (_i *articlesService) Save(authToken string, req request.ArticlesCreateRequ return nil, err } - // Dynamic Approval Workflow Assignment + // Dynamic Approval Workflow Assignment with Multi-Branch Support if createdBy != nil && *createdBy.UserLevel.IsApprovalActive == true { + _i.Log.Info(). + Uint("userId", createdBy.ID). + Uint("userLevelId", createdBy.UserLevel.ID). + Str("userLevelName", createdBy.UserLevel.Name). + Bool("isApprovalActive", *createdBy.UserLevel.IsApprovalActive). + Msg("User level requires approval - proceeding with workflow assignment") + // Get default workflow for the client defaultWorkflow, err := _i.ApprovalWorkflowsRepo.GetDefault(clientId) - if err == nil && defaultWorkflow != nil { + if err != nil { + _i.Log.Error().Err(err).Msg("Failed to get default workflow") + } else if defaultWorkflow == nil { + _i.Log.Warn().Msg("No default workflow found for client") + } else { + _i.Log.Info(). + Uint("workflowId", defaultWorkflow.ID). + Str("workflowName", defaultWorkflow.Name). + Bool("isActive", *defaultWorkflow.IsActive). + Bool("isDefault", *defaultWorkflow.IsDefault). + Msg("Found default workflow") + // Assign workflow to article saveArticleRes.WorkflowId = &defaultWorkflow.ID saveArticleRes.CurrentApprovalStep = &[]int{1}[0] // Start at step 1 @@ -308,9 +350,14 @@ func (_i *articlesService) Save(authToken string, req request.ArticlesCreateRequ err = _i.Repo.Update(clientId, saveArticleRes.ID, saveArticleRes) if err != nil { _i.Log.Error().Err(err).Msg("Failed to update article with workflow") + } else { + _i.Log.Info(). + Uint("articleId", saveArticleRes.ID). + Uint("workflowId", defaultWorkflow.ID). + Msg("Article updated with workflow successfully") } - // Create approval flow + // Create approval flow with multi-branch support approvalFlow := &entity.ArticleApprovalFlows{ ArticleId: saveArticleRes.ID, WorkflowId: defaultWorkflow.ID, @@ -319,11 +366,32 @@ func (_i *articlesService) Save(authToken string, req request.ArticlesCreateRequ SubmittedById: *newReq.CreatedById, SubmittedAt: time.Now(), ClientId: clientId, + // Multi-branch fields will be set by the service + CurrentBranch: nil, // Will be determined by first applicable step + BranchPath: nil, // Will be populated as flow progresses + IsParallelFlow: &[]bool{false}[0], // Default to sequential } - _, err = _i.ArticleApprovalFlowsRepo.Create(clientId, approvalFlow) + createdFlow, err := _i.ArticleApprovalFlowsRepo.Create(clientId, approvalFlow) if err != nil { _i.Log.Error().Err(err).Msg("Failed to create approval flow") + } else { + _i.Log.Info(). + Uint("flowId", createdFlow.ID). + Uint("articleId", saveArticleRes.ID). + Uint("workflowId", defaultWorkflow.ID). + Msg("Approval flow created successfully") + + // Initialize the multi-branch flow by determining the first applicable step + err = _i.initializeMultiBranchFlow(authToken, createdFlow.ID, createdBy.UserLevel.ID) + if err != nil { + _i.Log.Error().Err(err).Msg("Failed to initialize multi-branch flow") + } else { + _i.Log.Info(). + Uint("flowId", createdFlow.ID). + Uint("userLevelId", createdBy.UserLevel.ID). + Msg("Multi-branch flow initialized successfully") + } } } @@ -1396,3 +1464,61 @@ func (_i *articlesService) findNextApprovalLevel(clientId *uuid.UUID, currentLev return 0 } } + +// initializeMultiBranchFlow initializes the multi-branch flow by determining the first applicable step +func (_i *articlesService) initializeMultiBranchFlow(authToken string, flowId uint, submitterLevelId uint) error { + // Get the approval flow + flow, err := _i.ArticleApprovalFlowsRepo.FindOne(nil, flowId) + if err != nil { + return fmt.Errorf("failed to find approval flow: %w", err) + } + + // Find the first applicable step based on submitter's user level + nextSteps, err := _i.ArticleApprovalFlowsSvc.FindNextStepsForBranch(authToken, flow.WorkflowId, 0, submitterLevelId) + if err != nil { + return fmt.Errorf("failed to find next steps: %w", err) + } + + if len(nextSteps) == 0 { + // No applicable steps found - this shouldn't happen with proper workflow configuration + _i.Log.Warn().Uint("flowId", flowId).Uint("submitterLevelId", submitterLevelId).Msg("No applicable steps found for multi-branch flow") + return nil + } + + // Update the flow with the first applicable step and branch information + firstStep := nextSteps[0] + flow.CurrentStep = firstStep.StepOrder + + // Set branch information if available + if firstStep.BranchName != nil { + flow.CurrentBranch = firstStep.BranchName + + // Initialize branch path + branchPath := []string{*firstStep.BranchName} + branchPathJSON, err := json.Marshal(branchPath) + if err == nil { + branchPathStr := string(branchPathJSON) + flow.BranchPath = &branchPathStr + } + } + + // Update the flow + err = _i.ArticleApprovalFlowsRepo.Update(flowId, flow) + if err != nil { + return fmt.Errorf("failed to update approval flow: %w", err) + } + + _i.Log.Info(). + Uint("flowId", flowId). + Uint("submitterLevelId", submitterLevelId). + Int("currentStep", flow.CurrentStep). + Str("currentBranch", func() string { + if flow.CurrentBranch != nil { + return *flow.CurrentBranch + } + return "none" + }()). + Msg("Multi-branch flow initialized successfully") + + return nil +} diff --git a/debug_approval_flow.sql b/debug_approval_flow.sql new file mode 100644 index 0000000..a8588fa --- /dev/null +++ b/debug_approval_flow.sql @@ -0,0 +1,95 @@ +-- Debug script untuk mengecek masalah approval flow + +-- 1. Cek user level 5 +SELECT + id, + level_name, + level_number, + is_approval_active, + client_id +FROM user_levels +WHERE id = 5; + +-- 2. Cek user dengan level 5 +SELECT + u.id, + u.name, + u.user_level_id, + ul.level_name, + ul.level_number, + ul.is_approval_active +FROM users u +JOIN user_levels ul ON u.user_level_id = ul.id +WHERE u.user_level_id = 5; + +-- 3. Cek default workflow +SELECT + id, + name, + is_active, + is_default, + client_id +FROM approval_workflows +WHERE is_default = true +AND is_active = true; + +-- 4. Cek workflow steps +SELECT + aws.id, + aws.workflow_id, + aws.step_order, + aws.step_name, + aws.required_user_level_id, + aws.condition_type, + aws.condition_value, + aws.branch_name, + aw.name as workflow_name +FROM approval_workflow_steps aws +JOIN approval_workflows aw ON aws.workflow_id = aw.id +WHERE aw.is_default = true +ORDER BY aws.step_order, aws.branch_order; + +-- 5. Cek artikel yang baru dibuat +SELECT + id, + title, + created_by_id, + workflow_id, + current_approval_step, + status_id, + bypass_approval, + approval_exempt, + created_at +FROM articles +WHERE title = 'Test Tni Artikel 1' +ORDER BY created_at DESC; + +-- 6. Cek approval flows +SELECT + aaf.id, + aaf.article_id, + aaf.workflow_id, + aaf.current_step, + aaf.current_branch, + aaf.status_id, + aaf.submitted_by_id, + aaf.submitted_at, + a.title as article_title +FROM article_approval_flows aaf +JOIN articles a ON aaf.article_id = a.id +WHERE a.title = 'Test Tni Artikel 1' +ORDER BY aaf.created_at DESC; + +-- 7. Cek legacy approval records +SELECT + aa.id, + aa.article_id, + aa.approval_by, + aa.status_id, + aa.message, + aa.approval_at_level, + a.title as article_title +FROM article_approvals aa +JOIN articles a ON aa.article_id = a.id +WHERE a.title = 'Test Tni Artikel 1' +ORDER BY aa.created_at DESC; diff --git a/docs/AUTO_APPROVAL_FLOW_GUIDE.md b/docs/AUTO_APPROVAL_FLOW_GUIDE.md new file mode 100644 index 0000000..fc5bfaa --- /dev/null +++ b/docs/AUTO_APPROVAL_FLOW_GUIDE.md @@ -0,0 +1,293 @@ +# Auto Approval Flow Guide + +## ๐Ÿš€ **Overview** + +Sistem auto approval flow memungkinkan artikel untuk otomatis masuk ke workflow approval yang sesuai dengan user level submitter saat artikel dibuat, tanpa perlu manual submit approval. + +## ๐Ÿ”„ **How It Works** + +### **1. Artikel Creation Process** + +Ketika user membuat artikel baru: + +```bash +curl -X POST "http://localhost:8080/api/articles" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer USER_TOKEN" \ + -d '{ + "title": "Sample Article", + "content": "Article content here", + "categoryId": 1, + "isDraft": false + }' +``` + +**Sistem akan otomatis:** + +1. โœ… **Detect User Level** - Mengambil user level dari JWT token +2. โœ… **Check Approval Requirement** - Cek apakah user level memerlukan approval +3. โœ… **Get Default Workflow** - Mengambil workflow default untuk client +4. โœ… **Create Approval Flow** - Membuat `ArticleApprovalFlows` record +5. โœ… **Initialize Multi-Branch** - Menentukan branch dan step pertama yang sesuai +6. โœ… **Set Article Status** - Set artikel ke status "Pending Approval" + +### **2. Multi-Branch Logic** + +Sistem akan otomatis menentukan branch yang sesuai berdasarkan: + +- **User Level Submitter** - Level user yang membuat artikel +- **Workflow Conditions** - Kondisi yang dikonfigurasi di workflow steps +- **Branch Rules** - Aturan branching yang sudah disetup + +**Contoh Flow:** + +``` +User Level 5 membuat artikel + โ†“ +Sistem cek workflow conditions + โ†“ +Masuk ke "Branch_A" (Level 2 A Branch) + โ†“ +Menunggu approval dari User Level 3 + โ†“ +Setelah approved โ†’ masuk ke "Final_Approval" + โ†“ +Menunggu approval dari User Level 2 + โ†“ +Artikel published +``` + +## ๐Ÿ“‹ **Implementation Details** + +### **Database Changes** + +Sistem menggunakan field baru di `ArticleApprovalFlows`: + +```sql +-- Multi-branch support fields +current_branch VARCHAR(100) -- Current active branch +branch_path TEXT -- JSON array tracking path +is_parallel_flow BOOLEAN -- Whether parallel flow +parent_flow_id INT4 -- For parallel flows +``` + +### **Service Integration** + +```go +// Articles Service - Auto approval flow creation +func (s *articlesService) Save(authToken string, req request.ArticlesCreateRequest) { + // ... create article ... + + // Auto-create approval flow with multi-branch support + if createdBy.UserLevel.IsApprovalActive { + approvalFlow := &entity.ArticleApprovalFlows{ + ArticleId: article.ID, + WorkflowId: defaultWorkflow.ID, + CurrentStep: 1, + StatusId: 1, // In Progress + SubmittedById: user.ID, + SubmittedAt: time.Now(), + // Multi-branch fields + CurrentBranch: nil, // Will be determined + BranchPath: nil, // Will be populated + IsParallelFlow: false, + } + + // Initialize multi-branch flow + s.initializeMultiBranchFlow(authToken, flowId, userLevelId) + } +} +``` + +## ๐ŸŽฏ **User Experience** + +### **Before (Manual Submit)** +``` +1. User creates article +2. User manually submits for approval +3. System creates approval flow +4. Approval process begins +``` + +### **After (Auto Submit)** +``` +1. User creates article +2. System automatically: + - Creates approval flow + - Determines correct branch + - Sets first applicable step + - Notifies approvers +3. Approval process begins immediately +``` + +## ๐Ÿ”ง **Configuration** + +### **Workflow Setup** + +Pastikan workflow sudah dikonfigurasi dengan benar: + +```json +{ + "name": "Multi-Branch Article Approval", + "isActive": true, + "isDefault": true, + "steps": [ + { + "stepOrder": 1, + "stepName": "Level 2 A Branch", + "requiredUserLevelId": 3, + "conditionType": "user_level_hierarchy", + "conditionValue": "{\"applies_to_levels\": [5]}", + "branchName": "Branch_A" + }, + { + "stepOrder": 1, + "stepName": "Level 2 B Branch", + "requiredUserLevelId": 4, + "conditionType": "user_level_hierarchy", + "conditionValue": "{\"applies_to_levels\": [6]}", + "branchName": "Branch_B" + }, + { + "stepOrder": 2, + "stepName": "Level 1 Final Approval", + "requiredUserLevelId": 2, + "conditionType": "always", + "branchName": "Final_Approval" + } + ] +} +``` + +### **User Level Configuration** + +Pastikan user levels sudah dikonfigurasi dengan benar: + +```sql +-- Check user levels +SELECT id, level_name, level_number, is_approval_active +FROM user_levels +WHERE client_id = 'your-client-id' +ORDER BY level_number; +``` + +## ๐Ÿ“Š **Monitoring & Debugging** + +### **Check Auto-Created Flows** + +```sql +-- Check approval flows created automatically +SELECT + aaf.id, + aaf.article_id, + aaf.current_step, + aaf.current_branch, + aaf.status_id, + aaf.submitted_at, + a.title, + u.name as submitter_name, + ul.level_name as submitter_level +FROM article_approval_flows aaf +JOIN articles a ON aaf.article_id = a.id +JOIN users u ON aaf.submitted_by_id = u.id +JOIN user_levels ul ON u.user_level_id = ul.id +WHERE aaf.created_at >= NOW() - INTERVAL '1 day' +ORDER BY aaf.created_at DESC; +``` + +### **Check Branch Path** + +```sql +-- Check branch path taken +SELECT + id, + article_id, + current_branch, + branch_path, + current_step +FROM article_approval_flows +WHERE branch_path IS NOT NULL +ORDER BY created_at DESC; +``` + +### **API Endpoints for Monitoring** + +```bash +# Check approval status +curl -X GET "http://localhost:8080/api/articles/{article_id}/approval-status" \ + -H "Authorization: Bearer YOUR_TOKEN" + +# Check next steps preview +curl -X GET "http://localhost:8080/api/article-approval-flows/{flow_id}/next-steps-preview" \ + -H "Authorization: Bearer YOUR_TOKEN" + +# Check my approval queue +curl -X GET "http://localhost:8080/api/article-approval-flows/my-queue" \ + -H "Authorization: Bearer APPROVER_TOKEN" +``` + +## ๐Ÿšจ **Troubleshooting** + +### **Common Issues** + +1. **No Approval Flow Created** + - Check if user level has `is_approval_active = true` + - Check if default workflow exists + - Check logs for errors + +2. **Wrong Branch Selected** + - Check workflow conditions + - Check user level hierarchy + - Check condition value JSON format + +3. **Flow Not Initialized** + - Check `initializeMultiBranchFlow` method + - Check `FindNextStepsForBranch` method + - Check database constraints + +### **Debug Commands** + +```bash +# Check user level +curl -X GET "http://localhost:8080/api/users/{user_id}" \ + -H "Authorization: Bearer YOUR_TOKEN" | jq '.data.user_levels' + +# Check default workflow +curl -X GET "http://localhost:8080/api/approval-workflows/default" \ + -H "Authorization: Bearer YOUR_TOKEN" + +# Check workflow steps +curl -X GET "http://localhost:8080/api/approval-workflows/{workflow_id}/with-steps" \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +## ๐ŸŽ‰ **Benefits** + +1. **Better UX** - User tidak perlu manual submit approval +2. **Automatic Routing** - Sistem otomatis menentukan branch yang sesuai +3. **Immediate Processing** - Approval flow dimulai langsung setelah artikel dibuat +4. **Consistent Behavior** - Semua artikel mengikuti aturan yang sama +5. **Reduced Errors** - Mengurangi kemungkinan user lupa submit approval + +## ๐Ÿ”„ **Migration from Manual Submit** + +Jika sebelumnya menggunakan manual submit, sistem akan tetap backward compatible: + +- Artikel lama tetap bisa menggunakan manual submit +- Artikel baru akan otomatis menggunakan auto-approval flow +- Kedua sistem bisa berjalan bersamaan + +## ๐Ÿ“ **Next Steps** + +Setelah implementasi auto-approval flow: + +1. โœ… Test dengan berbagai user levels +2. โœ… Monitor approval flow creation +3. โœ… Verify branch routing logic +4. โœ… Check notification system +5. โœ… Update user documentation +6. โœ… Train users on new workflow + +--- + +**Sistem auto approval flow memberikan pengalaman yang lebih seamless dan otomatis untuk proses approval artikel!** ๐Ÿš€ diff --git a/docs/MULTI_BRANCH_APPROVAL_GUIDE.md b/docs/MULTI_BRANCH_APPROVAL_GUIDE.md new file mode 100644 index 0000000..fb6421c --- /dev/null +++ b/docs/MULTI_BRANCH_APPROVAL_GUIDE.md @@ -0,0 +1,554 @@ +# Multi-Branch Approval System Documentation + +## Overview + +Sistem Multi-Branch Approval memungkinkan pembuatan workflow approval yang dapat bercabang berdasarkan User Level dari submitter artikel. Sistem ini mendukung: + +- **Conditional Routing**: Approval flow dapat mengikuti jalur berbeda berdasarkan level user yang submit artikel +- **Hierarchical Branching**: Support untuk struktur cabang yang kompleks dengan multiple level +- **Parallel Processing**: Kemampuan untuk menjalankan multiple approval secara parallel +- **Dynamic Configuration**: Workflow dapat dikonfigurasi secara dinamis tanpa mengubah kode + +## Database Schema Changes + +### 1. ApprovalWorkflowSteps Table + +Field baru yang ditambahkan untuk mendukung multi-branch: + +```sql +-- Multi-branch support fields +ALTER TABLE approval_workflow_steps ADD COLUMN parent_step_id INT REFERENCES approval_workflow_steps(id); +ALTER TABLE approval_workflow_steps ADD COLUMN condition_type VARCHAR(50); -- 'user_level', 'user_level_hierarchy', 'always', 'custom' +ALTER TABLE approval_workflow_steps ADD COLUMN condition_value TEXT; -- JSON string for conditions +ALTER TABLE approval_workflow_steps ADD COLUMN is_parallel BOOLEAN DEFAULT false; +ALTER TABLE approval_workflow_steps ADD COLUMN branch_name VARCHAR(100); +ALTER TABLE approval_workflow_steps ADD COLUMN branch_order INT; -- Order within the same branch +``` + +### 2. ArticleApprovalFlows Table + +Field baru untuk tracking branch: + +```sql +-- Multi-branch support fields +ALTER TABLE article_approval_flows ADD COLUMN current_branch VARCHAR(100); -- Current active branch +ALTER TABLE article_approval_flows ADD COLUMN branch_path TEXT; -- JSON array tracking the path taken +ALTER TABLE article_approval_flows ADD COLUMN is_parallel_flow BOOLEAN DEFAULT false; -- Whether this is a parallel approval flow +ALTER TABLE article_approval_flows ADD COLUMN parent_flow_id INT REFERENCES article_approval_flows(id); -- For parallel flows +``` + +## API Endpoints + +### 1. Create Multi-Branch Workflow + +**Endpoint**: `POST /api/approval-workflows/with-steps` + +**Description**: Membuat workflow dengan steps yang mendukung multi-branch routing + +**Request Body**: +```json +{ + "name": "Multi-Branch Article Approval", + "description": "Approval workflow dengan cabang berdasarkan user level", + "isActive": true, + "isDefault": false, + "requiresApproval": true, + "autoPublish": false, + "steps": [ + { + "stepOrder": 1, + "stepName": "Level 2 Branch", + "requiredUserLevelId": 2, + "canSkip": false, + "isActive": true, + "conditionType": "user_level_hierarchy", + "conditionValue": "{\"applies_to_levels\": [4,5,6,7,8,9]}", + "branchName": "Branch_A", + "branchOrder": 1 + }, + { + "stepOrder": 1, + "stepName": "Level 3 Branch", + "requiredUserLevelId": 3, + "canSkip": false, + "isActive": true, + "conditionType": "user_level_hierarchy", + "conditionValue": "{\"applies_to_levels\": [10,11,12,13,14,15]}", + "branchName": "Branch_B", + "branchOrder": 1 + }, + { + "stepOrder": 2, + "stepName": "Level 1 Final Approval", + "requiredUserLevelId": 1, + "canSkip": false, + "isActive": true, + "conditionType": "always", + "branchName": "Final_Approval", + "branchOrder": 1 + } + ] +} +``` + +**Curl Example**: +```bash +curl -X POST "http://localhost:8080/api/approval-workflows/with-steps" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "name": "Multi-Branch Article Approval", + "description": "Approval workflow dengan cabang berdasarkan user level", + "isActive": true, + "isDefault": false, + "requiresApproval": true, + "autoPublish": false, + "steps": [ + { + "stepOrder": 1, + "stepName": "Level 2 Branch", + "requiredUserLevelId": 2, + "canSkip": false, + "isActive": true, + "conditionType": "user_level_hierarchy", + "conditionValue": "{\"applies_to_levels\": [4,5,6,7,8,9]}", + "branchName": "Branch_A", + "branchOrder": 1 + }, + { + "stepOrder": 1, + "stepName": "Level 3 Branch", + "requiredUserLevelId": 3, + "canSkip": false, + "isActive": true, + "conditionType": "user_level_hierarchy", + "conditionValue": "{\"applies_to_levels\": [10,11,12,13,14,15]}", + "branchName": "Branch_B", + "branchOrder": 1 + }, + { + "stepOrder": 2, + "stepName": "Level 1 Final Approval", + "requiredUserLevelId": 1, + "canSkip": false, + "isActive": true, + "conditionType": "always", + "branchName": "Final_Approval", + "branchOrder": 1 + } + ] + }' +``` + +### 2. Submit Article for Multi-Branch Approval + +**Endpoint**: `POST /api/articles/{id}/submit-approval` + +**Description**: Submit artikel untuk approval dengan multi-branch workflow + +**Curl Example**: +```bash +curl -X POST "http://localhost:8080/api/articles/123/submit-approval" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "workflowId": 1 + }' +``` + +### 3. Process Multi-Branch Approval + +**Endpoint**: `POST /api/article-approval-flows/{id}/multi-branch-approve` + +**Description**: Proses approval dengan multi-branch logic + +**Request Body**: +```json +{ + "message": "Article looks good, approved for next level" +} +``` + +**Curl Example**: +```bash +curl -X POST "http://localhost:8080/api/article-approval-flows/456/multi-branch-approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "message": "Article looks good, approved for next level" + }' +``` + +### 4. Get Next Steps Preview + +**Endpoint**: `GET /api/article-approval-flows/{id}/next-steps-preview` + +**Description**: Mendapatkan preview next steps berdasarkan user level submitter + +**Curl Example**: +```bash +curl -X GET "http://localhost:8080/api/article-approval-flows/456/next-steps-preview" \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +**Response Example**: +```json +{ + "success": true, + "messages": ["Next steps preview successfully retrieved"], + "data": { + "current_step": 1, + "submitter_level_id": 5, + "next_steps": [ + { + "id": 2, + "stepOrder": 2, + "stepName": "Level 1 Final Approval", + "requiredUserLevelId": 1, + "branchName": "Final_Approval", + "conditionType": "always" + } + ], + "total_next_steps": 1 + } +} +``` + +## Condition Types + +### 1. `user_level` +Steps yang berlaku untuk user level tertentu. + +**Condition Value Format**: +```json +[4, 5, 6, 7, 8, 9] +``` + +### 2. `user_level_hierarchy` +Steps yang berlaku berdasarkan hierarki user level dengan kondisi yang lebih kompleks. + +**Condition Value Format**: +```json +{ + "applies_to_levels": [4, 5, 6, 7, 8, 9], + "min_level": 4, + "max_level": 9 +} +``` + +### 3. `always` +Steps yang selalu berlaku untuk semua user level. + +**Condition Value**: `null` atau `"{}"` + +### 4. `custom` +Steps dengan kondisi custom (untuk ekstensi masa depan). + +## Workflow Examples + +### Example 1: Simple Two-Branch Workflow + +```json +{ + "name": "Simple Two-Branch Approval", + "description": "Workflow dengan 2 cabang berdasarkan user level", + "steps": [ + { + "stepOrder": 1, + "stepName": "Branch A - Level 2", + "requiredUserLevelId": 2, + "conditionType": "user_level_hierarchy", + "conditionValue": "{\"applies_to_levels\": [4,5,6]}", + "branchName": "Branch_A" + }, + { + "stepOrder": 1, + "stepName": "Branch B - Level 3", + "requiredUserLevelId": 3, + "conditionType": "user_level_hierarchy", + "conditionValue": "{\"applies_to_levels\": [7,8,9]}", + "branchName": "Branch_B" + }, + { + "stepOrder": 2, + "stepName": "Final Approval", + "requiredUserLevelId": 1, + "conditionType": "always", + "branchName": "Final" + } + ] +} +``` + +### Example 2: Complex Multi-Level Branching + +```json +{ + "name": "Complex Multi-Level Branching", + "description": "Workflow dengan multiple level branching", + "steps": [ + { + "stepOrder": 1, + "stepName": "Level 2 Branch", + "requiredUserLevelId": 2, + "conditionType": "user_level_hierarchy", + "conditionValue": "{\"applies_to_levels\": [4,5,6,7,8,9]}", + "branchName": "Branch_A" + }, + { + "stepOrder": 1, + "stepName": "Level 3 Branch", + "requiredUserLevelId": 3, + "conditionType": "user_level_hierarchy", + "conditionValue": "{\"applies_to_levels\": [10,11,12,13,14,15]}", + "branchName": "Branch_B" + }, + { + "stepOrder": 2, + "stepName": "Sub-branch A1", + "requiredUserLevelId": 4, + "parentStepId": 1, + "conditionType": "user_level_hierarchy", + "conditionValue": "{\"applies_to_levels\": [4,5,6]}", + "branchName": "SubBranch_A1" + }, + { + "stepOrder": 2, + "stepName": "Sub-branch A2", + "requiredUserLevelId": 5, + "parentStepId": 1, + "conditionType": "user_level_hierarchy", + "conditionValue": "{\"applies_to_levels\": [7,8,9]}", + "branchName": "SubBranch_A2" + }, + { + "stepOrder": 3, + "stepName": "Level 1 Final Approval", + "requiredUserLevelId": 1, + "conditionType": "always", + "branchName": "Final_Approval" + } + ] +} +``` + +## Flow Logic + +### 1. Article Submission Flow + +1. User dengan level tertentu submit artikel +2. Sistem menentukan workflow yang akan digunakan +3. Sistem mencari step pertama yang applicable berdasarkan user level submitter +4. Artikel masuk ke approval flow dengan step yang sesuai + +### 2. Approval Processing Flow + +1. Approver melakukan approval pada step saat ini +2. Sistem mencari next steps yang applicable berdasarkan: + - User level submitter + - Condition type dan value + - Branch logic +3. Jika ada multiple next steps, sistem akan: + - Untuk parallel branches: buat multiple approval flows + - Untuk sequential branches: pilih step pertama yang applicable +4. Update artikel status sesuai dengan step yang dicapai + +### 3. Branch Determination Logic + +```go +func IsStepApplicableForLevel(step *ApprovalWorkflowSteps, submitterLevelId uint) bool { + switch step.ConditionType { + case "user_level": + // Check if submitter level is in allowed levels + return contains(allowedLevels, submitterLevelId) + + case "user_level_hierarchy": + // Check based on hierarchy rules + return checkHierarchyRules(submitterLevelId, step.ConditionValue) + + case "always": + return true + + default: + return true + } +} +``` + +## Error Handling + +### Common Error Responses + +1. **Invalid Condition Value**: +```json +{ + "success": false, + "messages": ["Invalid condition value format"], + "error": "CONDITION_VALUE_INVALID" +} +``` + +2. **No Applicable Steps**: +```json +{ + "success": false, + "messages": ["No applicable next steps found for this user level"], + "error": "NO_APPLICABLE_STEPS" +} +``` + +3. **Workflow Not Found**: +```json +{ + "success": false, + "messages": ["Workflow not found"], + "error": "WORKFLOW_NOT_FOUND" +} +``` + +## Testing + +### Test Scenarios + +1. **Basic Branching Test**: + - Submit artikel dari user level 5 + - Verify artikel masuk ke Branch A (Level 2) + - Approve dan verify masuk ke Final Approval + +2. **Multiple Branch Test**: + - Submit artikel dari user level 8 + - Verify artikel masuk ke Branch A (Level 2) + - Submit artikel dari user level 12 + - Verify artikel masuk ke Branch B (Level 3) + +3. **Condition Validation Test**: + - Test dengan condition value yang invalid + - Test dengan user level yang tidak ada di condition + - Test dengan condition type yang tidak supported + +### Test Data Setup + +```sql +-- Insert test user levels +INSERT INTO user_levels (name, level_number, is_active) VALUES +('Level 1', 1, true), +('Level 2', 2, true), +('Level 3', 3, true), +('Level 4', 4, true), +('Level 5', 5, true); + +-- Insert test users +INSERT INTO users (username, email, user_level_id) VALUES +('user_level_5', 'user5@test.com', 5), +('user_level_8', 'user8@test.com', 8), +('user_level_12', 'user12@test.com', 12); +``` + +## Migration Guide + +### From Linear to Multi-Branch + +1. **Backup existing data**: +```sql +CREATE TABLE approval_workflow_steps_backup AS SELECT * FROM approval_workflow_steps; +CREATE TABLE article_approval_flows_backup AS SELECT * FROM article_approval_flows; +``` + +2. **Add new columns**: +```sql +-- Run the ALTER TABLE statements mentioned above +``` + +3. **Migrate existing workflows**: +```sql +-- Set default values for existing steps +UPDATE approval_workflow_steps SET + condition_type = 'always', + branch_name = 'default_branch', + branch_order = step_order +WHERE condition_type IS NULL; +``` + +4. **Test migration**: + - Verify existing workflows still work + - Test new multi-branch functionality + - Rollback if issues found + +## Performance Considerations + +1. **Database Indexes**: +```sql +CREATE INDEX idx_approval_workflow_steps_condition ON approval_workflow_steps(condition_type, branch_name); +CREATE INDEX idx_article_approval_flows_branch ON article_approval_flows(current_branch); +``` + +2. **Caching**: + - Cache workflow steps by condition type + - Cache user level mappings + - Cache branch determination results + +3. **Query Optimization**: + - Use proper JOINs for branch queries + - Limit result sets with pagination + - Use prepared statements for repeated queries + +## Security Considerations + +1. **Access Control**: + - Validate user permissions for each approval step + - Ensure users can only approve steps they're authorized for + - Log all approval actions for audit + +2. **Data Validation**: + - Validate condition values before processing + - Sanitize JSON input in condition_value + - Check for SQL injection in dynamic queries + +3. **Audit Trail**: + - Log all branch decisions + - Track condition evaluations + - Maintain approval history with branch information + +## Troubleshooting + +### Common Issues + +1. **Steps Not Found**: + - Check condition_type and condition_value + - Verify user level mapping + - Check workflow activation status + +2. **Infinite Loops**: + - Validate step dependencies + - Check for circular references in parent_step_id + - Ensure proper step ordering + +3. **Performance Issues**: + - Check database indexes + - Optimize condition evaluation queries + - Consider caching strategies + +### Debug Commands + +```bash +# Check workflow steps +curl -X GET "http://localhost:8080/api/approval-workflows/1/with-steps" \ + -H "Authorization: Bearer YOUR_TOKEN" + +# Check next steps for specific flow +curl -X GET "http://localhost:8080/api/article-approval-flows/123/next-steps-preview" \ + -H "Authorization: Bearer YOUR_TOKEN" + +# Check user level +curl -X GET "http://localhost:8080/api/users/456" \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +## Future Enhancements + +1. **Visual Workflow Designer**: UI untuk membuat workflow secara visual +2. **Condition Builder**: UI untuk membuat kondisi tanpa JSON manual +3. **Workflow Templates**: Template workflow untuk berbagai use case +4. **Advanced Branching**: Support untuk conditional branching yang lebih kompleks +5. **Parallel Processing**: True parallel approval processing +6. **Workflow Analytics**: Analytics untuk workflow performance dan bottlenecks diff --git a/docs/MULTI_BRANCH_CURL_EXAMPLES.md b/docs/MULTI_BRANCH_CURL_EXAMPLES.md new file mode 100644 index 0000000..af666f3 --- /dev/null +++ b/docs/MULTI_BRANCH_CURL_EXAMPLES.md @@ -0,0 +1,673 @@ +# Multi-Branch Approval System - Curl Examples + +## Setup dan Konfigurasi + +### 1. Buat Multi-Branch Workflow + +```bash +# Example 1: Simple Two-Branch Workflow +curl -X POST "http://localhost:8080/api/approval-workflows/with-steps" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "name": "Simple Two-Branch Approval", + "description": "Workflow dengan 2 cabang berdasarkan user level", + "isActive": true, + "isDefault": false, + "requiresApproval": true, + "autoPublish": false, + "steps": [ + { + "stepOrder": 1, + "stepName": "Branch A - Level 2", + "requiredUserLevelId": 2, + "canSkip": false, + "isActive": true, + "conditionType": "user_level_hierarchy", + "conditionValue": "{\"applies_to_levels\": [4,5,6]}", + "branchName": "Branch_A", + "branchOrder": 1 + }, + { + "stepOrder": 1, + "stepName": "Branch B - Level 3", + "requiredUserLevelId": 3, + "canSkip": false, + "isActive": true, + "conditionType": "user_level_hierarchy", + "conditionValue": "{\"applies_to_levels\": [7,8,9]}", + "branchName": "Branch_B", + "branchOrder": 1 + }, + { + "stepOrder": 2, + "stepName": "Final Approval", + "requiredUserLevelId": 1, + "canSkip": false, + "isActive": true, + "conditionType": "always", + "branchName": "Final", + "branchOrder": 1 + } + ] + }' +``` + +### 2. Complex Multi-Level Branching + +```bash +# Example 2: Complex Multi-Level Branching +curl -X POST "http://localhost:8080/api/approval-workflows/with-steps" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "name": "Complex Multi-Level Branching", + "description": "Workflow dengan multiple level branching", + "isActive": true, + "isDefault": false, + "requiresApproval": true, + "autoPublish": false, + "steps": [ + { + "stepOrder": 1, + "stepName": "Level 2 Branch", + "requiredUserLevelId": 2, + "canSkip": false, + "isActive": true, + "conditionType": "user_level_hierarchy", + "conditionValue": "{\"applies_to_levels\": [4,5,6,7,8,9]}", + "branchName": "Branch_A", + "branchOrder": 1 + }, + { + "stepOrder": 1, + "stepName": "Level 3 Branch", + "requiredUserLevelId": 3, + "canSkip": false, + "isActive": true, + "conditionType": "user_level_hierarchy", + "conditionValue": "{\"applies_to_levels\": [10,11,12,13,14,15]}", + "branchName": "Branch_B", + "branchOrder": 1 + }, + { + "stepOrder": 2, + "stepName": "Sub-branch A1", + "requiredUserLevelId": 4, + "canSkip": false, + "isActive": true, + "conditionType": "user_level_hierarchy", + "conditionValue": "{\"applies_to_levels\": [4,5,6]}", + "branchName": "SubBranch_A1", + "branchOrder": 1 + }, + { + "stepOrder": 2, + "stepName": "Sub-branch A2", + "requiredUserLevelId": 5, + "canSkip": false, + "isActive": true, + "conditionType": "user_level_hierarchy", + "conditionValue": "{\"applies_to_levels\": [7,8,9]}", + "branchName": "SubBranch_A2", + "branchOrder": 1 + }, + { + "stepOrder": 3, + "stepName": "Level 1 Final Approval", + "requiredUserLevelId": 1, + "canSkip": false, + "isActive": true, + "conditionType": "always", + "branchName": "Final_Approval", + "branchOrder": 1 + } + ] + }' +``` + +### 3. Workflow dengan Specific User Levels + +```bash +# Example 3: Workflow untuk specific user levels +curl -X POST "http://localhost:8080/api/approval-workflows/with-steps" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "name": "Specific User Level Workflow", + "description": "Workflow untuk user level tertentu", + "isActive": true, + "isDefault": false, + "requiresApproval": true, + "autoPublish": false, + "steps": [ + { + "stepOrder": 1, + "stepName": "Level 2 Approval", + "requiredUserLevelId": 2, + "canSkip": false, + "isActive": true, + "conditionType": "user_level", + "conditionValue": "[4,5,6]", + "branchName": "Level2_Branch", + "branchOrder": 1 + }, + { + "stepOrder": 1, + "stepName": "Level 3 Approval", + "requiredUserLevelId": 3, + "canSkip": false, + "isActive": true, + "conditionType": "user_level", + "conditionValue": "[7,8,9]", + "branchName": "Level3_Branch", + "branchOrder": 1 + }, + { + "stepOrder": 2, + "stepName": "Final Approval", + "requiredUserLevelId": 1, + "canSkip": false, + "isActive": true, + "conditionType": "always", + "branchName": "Final", + "branchOrder": 1 + } + ] + }' +``` + +## Workflow Management + +### 1. Get All Workflows + +```bash +curl -X GET "http://localhost:8080/api/approval-workflows" \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +### 2. Get Workflow dengan Steps + +```bash +curl -X GET "http://localhost:8080/api/approval-workflows/1/with-steps" \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +### 3. Update Workflow + +```bash +curl -X PUT "http://localhost:8080/api/approval-workflows/1/with-steps" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "name": "Updated Multi-Branch Workflow", + "description": "Updated description", + "isActive": true, + "isDefault": false, + "requiresApproval": true, + "autoPublish": false, + "steps": [ + { + "stepOrder": 1, + "stepName": "Updated Step", + "requiredUserLevelId": 2, + "canSkip": false, + "isActive": true, + "conditionType": "user_level_hierarchy", + "conditionValue": "{\"applies_to_levels\": [4,5,6]}", + "branchName": "Updated_Branch", + "branchOrder": 1 + } + ] + }' +``` + +### 4. Activate/Deactivate Workflow + +```bash +# Activate workflow +curl -X PUT "http://localhost:8080/api/approval-workflows/1/activate" \ + -H "Authorization: Bearer YOUR_TOKEN" + +# Deactivate workflow +curl -X PUT "http://localhost:8080/api/approval-workflows/1/deactivate" \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +## Article Approval Flow + +### 1. Submit Article untuk Approval + +```bash +# Submit artikel dengan workflow ID tertentu +curl -X POST "http://localhost:8080/api/articles/123/submit-approval" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "workflowId": 1 + }' + +# Submit artikel dengan default workflow +curl -X POST "http://localhost:8080/api/articles/123/submit-approval" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{}' +``` + +### 2. Process Multi-Branch Approval + +```bash +# Approve dengan multi-branch logic +curl -X POST "http://localhost:8080/api/article-approval-flows/456/multi-branch-approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "message": "Article looks good, approved for next level" + }' + +# Approve dengan message yang berbeda +curl -X POST "http://localhost:8080/api/article-approval-flows/456/multi-branch-approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "message": "Content is excellent, proceeding to final approval" + }' +``` + +### 3. Get Next Steps Preview + +```bash +# Get preview next steps +curl -X GET "http://localhost:8080/api/article-approval-flows/456/next-steps-preview" \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +### 4. Traditional Approval (untuk backward compatibility) + +```bash +# Approve dengan method traditional +curl -X POST "http://localhost:8080/api/article-approval-flows/456/approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "message": "Approved using traditional method" + }' +``` + +### 5. Reject Article + +```bash +curl -X POST "http://localhost:8080/api/article-approval-flows/456/reject" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "reason": "Content needs improvement" + }' +``` + +### 6. Request Revision + +```bash +curl -X POST "http://localhost:8080/api/article-approval-flows/456/request-revision" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "revisionMessage": "Please improve the introduction section" + }' +``` + +### 7. Resubmit After Revision + +```bash +curl -X POST "http://localhost:8080/api/article-approval-flows/456/resubmit" \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +## Monitoring dan Analytics + +### 1. Get Approval History + +```bash +curl -X GET "http://localhost:8080/api/articles/123/approval-history" \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +### 2. Get My Approval Queue + +```bash +curl -X GET "http://localhost:8080/api/article-approval-flows/my-queue" \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +### 3. Get Pending Approvals + +```bash +curl -X GET "http://localhost:8080/api/article-approval-flows/pending-approvals" \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +### 4. Get Dashboard Statistics + +```bash +curl -X GET "http://localhost:8080/api/article-approval-flows/dashboard-stats" \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +### 5. Get Workload Statistics + +```bash +curl -X GET "http://localhost:8080/api/article-approval-flows/workload-stats" \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +## Testing Scenarios + +### Scenario 1: User Level 5 Submit Article + +```bash +# 1. Submit artikel dari user level 5 +curl -X POST "http://localhost:8080/api/articles/123/submit-approval" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer USER_LEVEL_5_TOKEN" \ + -d '{ + "workflowId": 1 + }' + +# 2. Check next steps preview +curl -X GET "http://localhost:8080/api/article-approval-flows/456/next-steps-preview" \ + -H "Authorization: Bearer USER_LEVEL_5_TOKEN" + +# 3. Approve dengan multi-branch logic +curl -X POST "http://localhost:8080/api/article-approval-flows/456/multi-branch-approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer LEVEL_2_APPROVER_TOKEN" \ + -d '{ + "message": "Approved by Level 2 approver" + }' + +# 4. Final approval +curl -X POST "http://localhost:8080/api/article-approval-flows/456/multi-branch-approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer LEVEL_1_APPROVER_TOKEN" \ + -d '{ + "message": "Final approval completed" + }' +``` + +### Scenario 2: User Level 8 Submit Article + +```bash +# 1. Submit artikel dari user level 8 +curl -X POST "http://localhost:8080/api/articles/124/submit-approval" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer USER_LEVEL_8_TOKEN" \ + -d '{ + "workflowId": 1 + }' + +# 2. Check next steps preview (should show different branch) +curl -X GET "http://localhost:8080/api/article-approval-flows/457/next-steps-preview" \ + -H "Authorization: Bearer USER_LEVEL_8_TOKEN" + +# 3. Approve dengan multi-branch logic +curl -X POST "http://localhost:8080/api/article-approval-flows/457/multi-branch-approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer LEVEL_3_APPROVER_TOKEN" \ + -d '{ + "message": "Approved by Level 3 approver" + }' + +# 4. Final approval +curl -X POST "http://localhost:8080/api/article-approval-flows/457/multi-branch-approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer LEVEL_1_APPROVER_TOKEN" \ + -d '{ + "message": "Final approval completed" + }' +``` + +### Scenario 3: User Level 12 Submit Article + +```bash +# 1. Submit artikel dari user level 12 +curl -X POST "http://localhost:8080/api/articles/125/submit-approval" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer USER_LEVEL_12_TOKEN" \ + -d '{ + "workflowId": 1 + }' + +# 2. Check next steps preview (should show Branch B) +curl -X GET "http://localhost:8080/api/article-approval-flows/458/next-steps-preview" \ + -H "Authorization: Bearer USER_LEVEL_12_TOKEN" + +# 3. Approve dengan multi-branch logic +curl -X POST "http://localhost:8080/api/article-approval-flows/458/multi-branch-approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer LEVEL_3_APPROVER_TOKEN" \ + -d '{ + "message": "Approved by Level 3 approver for Branch B" + }' + +# 4. Final approval +curl -X POST "http://localhost:8080/api/article-approval-flows/458/multi-branch-approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer LEVEL_1_APPROVER_TOKEN" \ + -d '{ + "message": "Final approval completed" + }' +``` + +## Error Handling Examples + +### 1. Invalid Workflow ID + +```bash +curl -X POST "http://localhost:8080/api/articles/123/submit-approval" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "workflowId": 999 + }' +``` + +**Expected Response**: +```json +{ + "success": false, + "messages": ["Workflow not found"], + "error": "WORKFLOW_NOT_FOUND" +} +``` + +### 2. Invalid Condition Value + +```bash +curl -X POST "http://localhost:8080/api/approval-workflows/with-steps" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "name": "Invalid Workflow", + "description": "Workflow dengan invalid condition", + "steps": [ + { + "stepOrder": 1, + "stepName": "Invalid Step", + "requiredUserLevelId": 2, + "conditionType": "user_level_hierarchy", + "conditionValue": "invalid json", + "branchName": "Invalid_Branch" + } + ] + }' +``` + +**Expected Response**: +```json +{ + "success": false, + "messages": ["Invalid condition value format"], + "error": "CONDITION_VALUE_INVALID" +} +``` + +### 3. No Applicable Steps + +```bash +# Submit artikel dari user level yang tidak ada di condition +curl -X POST "http://localhost:8080/api/articles/123/submit-approval" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer USER_LEVEL_99_TOKEN" \ + -d '{ + "workflowId": 1 + }' +``` + +**Expected Response**: +```json +{ + "success": false, + "messages": ["No applicable steps found for this user level"], + "error": "NO_APPLICABLE_STEPS" +} +``` + +## Performance Testing + +### 1. Load Testing dengan Multiple Submissions + +```bash +# Script untuk test multiple submissions +for i in {1..100}; do + curl -X POST "http://localhost:8080/api/articles/$i/submit-approval" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "workflowId": 1 + }' & +done +wait +``` + +### 2. Concurrent Approval Testing + +```bash +# Script untuk test concurrent approvals +for i in {1..50}; do + curl -X POST "http://localhost:8080/api/article-approval-flows/$i/multi-branch-approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer APPROVER_TOKEN" \ + -d '{ + "message": "Concurrent approval test" + }' & +done +wait +``` + +## Debugging Commands + +### 1. Check Workflow Steps + +```bash +curl -X GET "http://localhost:8080/api/approval-workflows/1/with-steps" \ + -H "Authorization: Bearer YOUR_TOKEN" | jq '.' +``` + +### 2. Check User Level + +```bash +curl -X GET "http://localhost:8080/api/users/123" \ + -H "Authorization: Bearer YOUR_TOKEN" | jq '.data.user_levels' +``` + +### 3. Check Approval Flow Status + +```bash +curl -X GET "http://localhost:8080/api/article-approval-flows/456" \ + -H "Authorization: Bearer YOUR_TOKEN" | jq '.' +``` + +### 4. Check Next Steps Preview + +```bash +curl -X GET "http://localhost:8080/api/article-approval-flows/456/next-steps-preview" \ + -H "Authorization: Bearer YOUR_TOKEN" | jq '.' +``` + +## Environment Variables + +```bash +# Set environment variables untuk testing +export API_BASE_URL="http://localhost:8080" +export ADMIN_TOKEN="your_admin_token_here" +export USER_LEVEL_5_TOKEN="your_user_level_5_token_here" +export USER_LEVEL_8_TOKEN="your_user_level_8_token_here" +export USER_LEVEL_12_TOKEN="your_user_level_12_token_here" +export LEVEL_1_APPROVER_TOKEN="your_level_1_approver_token_here" +export LEVEL_2_APPROVER_TOKEN="your_level_2_approver_token_here" +export LEVEL_3_APPROVER_TOKEN="your_level_3_approver_token_here" +``` + +## Batch Operations + +### 1. Create Multiple Workflows + +```bash +# Script untuk create multiple workflows +for i in {1..5}; do + curl -X POST "$API_BASE_URL/api/approval-workflows/with-steps" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -d "{ + \"name\": \"Workflow $i\", + \"description\": \"Test workflow $i\", + \"isActive\": true, + \"steps\": [ + { + \"stepOrder\": 1, + \"stepName\": \"Step 1\", + \"requiredUserLevelId\": 2, + \"conditionType\": \"always\", + \"branchName\": \"Branch_$i\" + } + ] + }" +done +``` + +### 2. Submit Multiple Articles + +```bash +# Script untuk submit multiple articles +for i in {1..10}; do + curl -X POST "$API_BASE_URL/api/articles/$i/submit-approval" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $USER_LEVEL_5_TOKEN" \ + -d '{ + "workflowId": 1 + }' +done +``` + +## Monitoring Commands + +### 1. Check System Health + +```bash +curl -X GET "$API_BASE_URL/health" \ + -H "Authorization: Bearer $ADMIN_TOKEN" +``` + +### 2. Get System Statistics + +```bash +curl -X GET "$API_BASE_URL/api/article-approval-flows/dashboard-stats" \ + -H "Authorization: Bearer $ADMIN_TOKEN" | jq '.' +``` + +### 3. Check Workflow Performance + +```bash +curl -X GET "$API_BASE_URL/api/approval-workflows" \ + -H "Authorization: Bearer $ADMIN_TOKEN" | jq '.data | length' +``` diff --git a/docs/MULTI_BRANCH_IMPLEMENTATION_README.md b/docs/MULTI_BRANCH_IMPLEMENTATION_README.md new file mode 100644 index 0000000..a071212 --- /dev/null +++ b/docs/MULTI_BRANCH_IMPLEMENTATION_README.md @@ -0,0 +1,376 @@ +# Multi-Branch Approval System Implementation + +## ๐ŸŽฏ Overview + +Sistem Multi-Branch Approval telah berhasil diimplementasikan untuk mendukung workflow approval yang dapat bercabang berdasarkan User Level dari submitter artikel. Implementasi ini memungkinkan: + +- **Conditional Routing**: Approval flow mengikuti jalur berbeda berdasarkan level user yang submit artikel +- **Hierarchical Branching**: Support untuk struktur cabang yang kompleks dengan multiple level +- **Dynamic Configuration**: Workflow dapat dikonfigurasi secara dinamis tanpa mengubah kode +- **Backward Compatibility**: Sistem lama tetap berfungsi tanpa perubahan + +## ๐Ÿš€ Quick Start + +### 1. Database Migration + +Jalankan script migration untuk menambahkan support multi-branch: + +```bash +# Jalankan migration script +psql -d your_database -f docs/migrations/add_multi_branch_support.sql +``` + +### 2. Create Multi-Branch Workflow + +```bash +curl -X POST "http://localhost:8080/api/approval-workflows/with-steps" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "name": "Multi-Branch Article Approval", + "description": "Workflow dengan cabang berdasarkan user level", + "isActive": true, + "steps": [ + { + "stepOrder": 1, + "stepName": "Level 2 Branch", + "requiredUserLevelId": 2, + "conditionType": "user_level_hierarchy", + "conditionValue": "{\"applies_to_levels\": [4,5,6,7,8,9]}", + "branchName": "Branch_A" + }, + { + "stepOrder": 1, + "stepName": "Level 3 Branch", + "requiredUserLevelId": 3, + "conditionType": "user_level_hierarchy", + "conditionValue": "{\"applies_to_levels\": [10,11,12,13,14,15]}", + "branchName": "Branch_B" + }, + { + "stepOrder": 2, + "stepName": "Level 1 Final Approval", + "requiredUserLevelId": 1, + "conditionType": "always", + "branchName": "Final_Approval" + } + ] + }' +``` + +### 3. Submit Article untuk Approval + +```bash +curl -X POST "http://localhost:8080/api/articles/123/submit-approval" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "workflowId": 1 + }' +``` + +### 4. Process Multi-Branch Approval + +```bash +curl -X POST "http://localhost:8080/api/article-approval-flows/456/multi-branch-approve" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "message": "Article looks good, approved for next level" + }' +``` + +## ๐Ÿ“ File Changes + +### Database Entities + +1. **`app/database/entity/approval_workflow_steps.entity.go`** + - Added multi-branch support fields + - Added parent-child relationships + - Added condition types and values + +2. **`app/database/entity/article_approval_flows.entity.go`** + - Added branch tracking fields + - Added parallel flow support + - Added parent-child flow relationships + +### Service Layer + +3. **`app/module/approval_workflow_steps/repository/approval_workflow_steps.repository.go`** + - Added multi-branch query methods + - Added condition-based filtering + - Added branch-specific queries + +4. **`app/module/article_approval_flows/service/article_approval_flows.service.go`** + - Added multi-branch approval processing + - Added condition evaluation logic + - Added branch determination methods + +### API Layer + +5. **`app/module/approval_workflows/request/approval_workflows.request.go`** + - Added multi-branch fields to request structures + - Updated entity conversion methods + +6. **`app/module/article_approval_flows/controller/article_approval_flows.controller.go`** + - Added multi-branch approval endpoints + - Added next steps preview endpoint + +## ๐Ÿ”ง Configuration + +### Condition Types + +1. **`user_level`**: Steps berlaku untuk user level tertentu + ```json + "conditionValue": "[4, 5, 6]" + ``` + +2. **`user_level_hierarchy`**: Steps berlaku berdasarkan hierarki + ```json + "conditionValue": "{\"applies_to_levels\": [4,5,6], \"min_level\": 4, \"max_level\": 9}" + ``` + +3. **`always`**: Steps selalu berlaku + ```json + "conditionValue": "{}" + ``` + +4. **`custom`**: Steps dengan kondisi custom (untuk ekstensi masa depan) + +### Branch Configuration + +- **`branchName`**: Nama cabang untuk identifikasi +- **`branchOrder`**: Urutan dalam cabang yang sama +- **`parentStepId`**: ID step parent untuk hierarchical branching +- **`isParallel`**: Apakah step ini berjalan parallel + +## ๐Ÿ“Š Flow Examples + +### Example 1: User Level 5 Submit Article + +``` +User Level 5 โ†’ Branch A (Level 2) โ†’ Final Approval (Level 1) +``` + +### Example 2: User Level 8 Submit Article + +``` +User Level 8 โ†’ Branch A (Level 2) โ†’ Final Approval (Level 1) +``` + +### Example 3: User Level 12 Submit Article + +``` +User Level 12 โ†’ Branch B (Level 3) โ†’ Final Approval (Level 1) +``` + +## ๐Ÿงช Testing + +### Test Scenarios + +1. **Basic Branching Test**: + ```bash + # Submit dari user level 5 + curl -X POST "http://localhost:8080/api/articles/123/submit-approval" \ + -H "Authorization: Bearer USER_LEVEL_5_TOKEN" \ + -d '{"workflowId": 1}' + + # Check next steps + curl -X GET "http://localhost:8080/api/article-approval-flows/456/next-steps-preview" \ + -H "Authorization: Bearer USER_LEVEL_5_TOKEN" + ``` + +2. **Multiple Branch Test**: + ```bash + # Test dengan user level berbeda + curl -X POST "http://localhost:8080/api/articles/124/submit-approval" \ + -H "Authorization: Bearer USER_LEVEL_12_TOKEN" \ + -d '{"workflowId": 1}' + ``` + +### Performance Testing + +```bash +# Load testing dengan multiple submissions +for i in {1..100}; do + curl -X POST "http://localhost:8080/api/articles/$i/submit-approval" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{"workflowId": 1}' & +done +wait +``` + +## ๐Ÿ” Monitoring + +### Check Workflow Status + +```bash +# Get workflow dengan steps +curl -X GET "http://localhost:8080/api/approval-workflows/1/with-steps" \ + -H "Authorization: Bearer YOUR_TOKEN" + +# Get next steps preview +curl -X GET "http://localhost:8080/api/article-approval-flows/456/next-steps-preview" \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +### Dashboard Statistics + +```bash +# Get approval statistics +curl -X GET "http://localhost:8080/api/article-approval-flows/dashboard-stats" \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +## ๐Ÿšจ Error Handling + +### Common Errors + +1. **No Applicable Steps**: + ```json + { + "success": false, + "messages": ["No applicable next steps found for this user level"], + "error": "NO_APPLICABLE_STEPS" + } + ``` + +2. **Invalid Condition Value**: + ```json + { + "success": false, + "messages": ["Invalid condition value format"], + "error": "CONDITION_VALUE_INVALID" + } + ``` + +3. **Workflow Not Found**: + ```json + { + "success": false, + "messages": ["Workflow not found"], + "error": "WORKFLOW_NOT_FOUND" + } + ``` + +## ๐Ÿ”„ Migration Guide + +### From Linear to Multi-Branch + +1. **Backup existing data**: + ```sql + CREATE TABLE approval_workflow_steps_backup AS SELECT * FROM approval_workflow_steps; + CREATE TABLE article_approval_flows_backup AS SELECT * FROM article_approval_flows; + ``` + +2. **Run migration script**: + ```bash + psql -d your_database -f docs/migrations/add_multi_branch_support.sql + ``` + +3. **Test migration**: + - Verify existing workflows still work + - Test new multi-branch functionality + - Rollback if issues found + +### Rollback (if needed) + +```sql +-- Uncomment rollback section in migration script +-- Run rollback commands to revert changes +``` + +## ๐Ÿ“ˆ Performance Considerations + +### Database Indexes + +```sql +-- Indexes untuk performance +CREATE INDEX idx_approval_workflow_steps_condition ON approval_workflow_steps(condition_type, branch_name); +CREATE INDEX idx_article_approval_flows_branch ON article_approval_flows(current_branch); +``` + +### Caching Strategy + +- Cache workflow steps by condition type +- Cache user level mappings +- Cache branch determination results + +## ๐Ÿ”’ Security + +### Access Control + +- Validate user permissions for each approval step +- Ensure users can only approve steps they're authorized for +- Log all approval actions for audit + +### Data Validation + +- Validate condition values before processing +- Sanitize JSON input in condition_value +- Check for SQL injection in dynamic queries + +## ๐Ÿ› Troubleshooting + +### Common Issues + +1. **Steps Not Found**: + - Check condition_type and condition_value + - Verify user level mapping + - Check workflow activation status + +2. **Infinite Loops**: + - Validate step dependencies + - Check for circular references in parent_step_id + - Ensure proper step ordering + +3. **Performance Issues**: + - Check database indexes + - Optimize condition evaluation queries + - Consider caching strategies + +### Debug Commands + +```bash +# Check workflow steps +curl -X GET "http://localhost:8080/api/approval-workflows/1/with-steps" \ + -H "Authorization: Bearer YOUR_TOKEN" + +# Check user level +curl -X GET "http://localhost:8080/api/users/456" \ + -H "Authorization: Bearer YOUR_TOKEN" + +# Check approval flow status +curl -X GET "http://localhost:8080/api/article-approval-flows/123" \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +## ๐Ÿš€ Future Enhancements + +1. **Visual Workflow Designer**: UI untuk membuat workflow secara visual +2. **Condition Builder**: UI untuk membuat kondisi tanpa JSON manual +3. **Workflow Templates**: Template workflow untuk berbagai use case +4. **Advanced Branching**: Support untuk conditional branching yang lebih kompleks +5. **Parallel Processing**: True parallel approval processing +6. **Workflow Analytics**: Analytics untuk workflow performance dan bottlenecks + +## ๐Ÿ“š Documentation + +- [Multi-Branch Approval Guide](docs/MULTI_BRANCH_APPROVAL_GUIDE.md) +- [Curl Examples](docs/MULTI_BRANCH_CURL_EXAMPLES.md) +- [Database Migration](docs/migrations/add_multi_branch_support.sql) + +## ๐Ÿค Support + +Untuk pertanyaan atau masalah dengan implementasi multi-branch approval system, silakan: + +1. Check dokumentasi lengkap di `docs/` folder +2. Review curl examples untuk testing +3. Check troubleshooting section +4. Contact development team untuk support lebih lanjut + +--- + +**Status**: โœ… Implementasi selesai dan siap untuk production +**Version**: 1.0.0 +**Last Updated**: $(date) diff --git a/docs/TROUBLESHOOTING_AUTO_APPROVAL_FLOW.md b/docs/TROUBLESHOOTING_AUTO_APPROVAL_FLOW.md new file mode 100644 index 0000000..15ff3ad --- /dev/null +++ b/docs/TROUBLESHOOTING_AUTO_APPROVAL_FLOW.md @@ -0,0 +1,282 @@ +# Troubleshooting Auto Approval Flow + +## ๐Ÿšจ **Masalah: Artikel Tidak Masuk ke Approval Queue** + +### **Gejala:** +- Artikel dibuat dengan `workflow_id = NULL` +- Tidak ada record di `article_approval_flows` table +- User approver tidak melihat artikel di queue mereka + +### **Penyebab Umum:** + +#### **1. User Level Tidak Memerlukan Approval** +```sql +-- Cek user level +SELECT + id, + level_name, + level_number, + is_approval_active +FROM user_levels +WHERE id = 5; +``` + +**Solusi:** +```sql +-- Set user level memerlukan approval +UPDATE user_levels +SET is_approval_active = true +WHERE id = 5; +``` + +#### **2. Tidak Ada Default Workflow** +```sql +-- Cek default workflow +SELECT + id, + name, + is_active, + is_default, + client_id +FROM approval_workflows +WHERE is_default = true +AND is_active = true; +``` + +**Solusi:** +```sql +-- Set workflow sebagai default +UPDATE approval_workflows +SET is_default = true, is_active = true +WHERE name = 'Multi-Branch Article Approval'; +``` + +#### **3. Workflow Tidak Aktif** +```sql +-- Cek status workflow +SELECT + id, + name, + is_active, + is_default +FROM approval_workflows +WHERE name = 'Multi-Branch Article Approval'; +``` + +**Solusi:** +```sql +-- Aktifkan workflow +UPDATE approval_workflows +SET is_active = true +WHERE name = 'Multi-Branch Article Approval'; +``` + +#### **4. Client ID Mismatch** +```sql +-- Cek client ID workflow vs user +SELECT + aw.id as workflow_id, + aw.name as workflow_name, + aw.client_id as workflow_client_id, + u.id as user_id, + u.client_id as user_client_id +FROM approval_workflows aw +CROSS JOIN users u +WHERE u.id = 5 +AND aw.is_default = true; +``` + +**Solusi:** +```sql +-- Update workflow client ID jika perlu +UPDATE approval_workflows +SET client_id = '338571d5-3836-47c0-a84f-e88f6fbcbb09' +WHERE name = 'Multi-Branch Article Approval'; +``` + +### **Debug Steps:** + +#### **Step 1: Cek Log Aplikasi** +```bash +# Cek log aplikasi untuk error messages +tail -f logs/app.log | grep -i "approval\|workflow\|branch" +``` + +**Expected Log Messages:** +``` +User level requires approval - setting to pending +User level requires approval - proceeding with workflow assignment +Found default workflow +Article updated with workflow successfully +Approval flow created successfully +Multi-branch flow initialized successfully +``` + +#### **Step 2: Cek Database State** +```sql +-- 1. Cek user level +SELECT + u.id, + u.name, + u.user_level_id, + ul.level_name, + ul.level_number, + ul.is_approval_active +FROM users u +JOIN user_levels ul ON u.user_level_id = ul.id +WHERE u.id = 5; + +-- 2. Cek default workflow +SELECT + id, + name, + is_active, + is_default, + client_id +FROM approval_workflows +WHERE is_default = true +AND is_active = true; + +-- 3. Cek artikel terbaru +SELECT + id, + title, + created_by_id, + workflow_id, + current_approval_step, + status_id, + bypass_approval, + approval_exempt, + created_at +FROM articles +ORDER BY created_at DESC +LIMIT 3; + +-- 4. Cek approval flows +SELECT + aaf.id, + aaf.article_id, + aaf.workflow_id, + aaf.current_step, + aaf.current_branch, + aaf.status_id, + aaf.submitted_by_id, + aaf.submitted_at, + a.title as article_title +FROM article_approval_flows aaf +JOIN articles a ON aaf.article_id = a.id +ORDER BY aaf.created_at DESC +LIMIT 3; +``` + +#### **Step 3: Test dengan API** +```bash +# Test create artikel baru +curl -X POST "http://localhost:8080/api/articles" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer USER_LEVEL_5_TOKEN" \ + -d '{ + "title": "Test Debug Article", + "content": "Debug content", + "categoryId": 1, + "isDraft": false + }' + +# Cek approval status +curl -X GET "http://localhost:8080/api/articles/{article_id}/approval-status" \ + -H "Authorization: Bearer USER_TOKEN" +``` + +### **Quick Fix Script:** + +```sql +-- Fix script untuk masalah umum +-- 1. Pastikan user level 5 memerlukan approval +UPDATE user_levels +SET is_approval_active = true +WHERE id = 5; + +-- 2. Pastikan workflow adalah default +UPDATE approval_workflows +SET is_default = true, is_active = true +WHERE name = 'Multi-Branch Article Approval'; + +-- 3. Pastikan client ID sama +UPDATE approval_workflows +SET client_id = '338571d5-3836-47c0-a84f-e88f6fbcbb09' +WHERE name = 'Multi-Branch Article Approval'; + +-- 4. Verifikasi fix +SELECT + ul.id as level_id, + ul.level_name, + ul.is_approval_active, + aw.id as workflow_id, + aw.name as workflow_name, + aw.is_active, + aw.is_default, + aw.client_id +FROM user_levels ul +CROSS JOIN approval_workflows aw +WHERE ul.id = 5 +AND aw.is_default = true; +``` + +### **Expected Results After Fix:** + +#### **Database State:** +```sql +-- Artikel harus memiliki workflow_id +SELECT + id, + title, + workflow_id, + current_approval_step, + status_id +FROM articles +WHERE title = 'Test Tni Artikel 1'; + +-- Approval flow harus terbuat +SELECT + aaf.id, + aaf.article_id, + aaf.workflow_id, + aaf.current_step, + aaf.current_branch, + aaf.status_id +FROM article_approval_flows aaf +JOIN articles a ON aaf.article_id = a.id +WHERE a.title = 'Test Tni Artikel 1'; +``` + +#### **API Response:** +```json +{ + "success": true, + "data": { + "article_id": 123, + "workflow_id": 1, + "current_step": 1, + "current_branch": "Branch_A", + "status": "pending_approval", + "next_approver_level": 3 + } +} +``` + +### **Monitoring Commands:** + +```bash +# Monitor log real-time +tail -f logs/app.log | grep -E "(approval|workflow|branch|User level)" + +# Cek database changes +watch -n 5 "psql -d your_db -c \"SELECT COUNT(*) FROM article_approval_flows WHERE created_at > NOW() - INTERVAL '1 hour';\"" + +# Test API endpoint +watch -n 10 "curl -s http://localhost:8080/api/article-approval-flows/my-queue -H 'Authorization: Bearer APPROVER_TOKEN' | jq '.data | length'" +``` + +--- + +**Jika masalah masih terjadi setelah mengikuti troubleshooting ini, cek log aplikasi untuk error messages yang lebih spesifik!** ๐Ÿ” diff --git a/docs/migrations/add_multi_branch_support.sql b/docs/migrations/add_multi_branch_support.sql new file mode 100644 index 0000000..d3f5841 --- /dev/null +++ b/docs/migrations/add_multi_branch_support.sql @@ -0,0 +1,282 @@ +-- Multi-Branch Approval System Database Migration Script +-- Run this script to add multi-branch support to existing database + +-- ===================================================== +-- 1. ADD NEW COLUMNS TO APPROVAL_WORKFLOW_STEPS TABLE +-- ===================================================== + +-- Add multi-branch support fields +ALTER TABLE approval_workflow_steps +ADD COLUMN IF NOT EXISTS parent_step_id INT REFERENCES approval_workflow_steps(id); + +ALTER TABLE approval_workflow_steps +ADD COLUMN IF NOT EXISTS condition_type VARCHAR(50); + +ALTER TABLE approval_workflow_steps +ADD COLUMN IF NOT EXISTS condition_value TEXT; + +ALTER TABLE approval_workflow_steps +ADD COLUMN IF NOT EXISTS is_parallel BOOLEAN DEFAULT false; + +ALTER TABLE approval_workflow_steps +ADD COLUMN IF NOT EXISTS branch_name VARCHAR(100); + +ALTER TABLE approval_workflow_steps +ADD COLUMN IF NOT EXISTS branch_order INT; + +-- ===================================================== +-- 2. ADD NEW COLUMNS TO ARTICLE_APPROVAL_FLOWS TABLE +-- ===================================================== + +-- Add multi-branch support fields +ALTER TABLE article_approval_flows +ADD COLUMN IF NOT EXISTS current_branch VARCHAR(100); + +ALTER TABLE article_approval_flows +ADD COLUMN IF NOT EXISTS branch_path TEXT; + +ALTER TABLE article_approval_flows +ADD COLUMN IF NOT EXISTS is_parallel_flow BOOLEAN DEFAULT false; + +ALTER TABLE article_approval_flows +ADD COLUMN IF NOT EXISTS parent_flow_id INT REFERENCES article_approval_flows(id); + +-- ===================================================== +-- 3. CREATE INDEXES FOR PERFORMANCE +-- ===================================================== + +-- Indexes for approval_workflow_steps +CREATE INDEX IF NOT EXISTS idx_approval_workflow_steps_condition +ON approval_workflow_steps(condition_type, branch_name); + +CREATE INDEX IF NOT EXISTS idx_approval_workflow_steps_parent +ON approval_workflow_steps(parent_step_id); + +CREATE INDEX IF NOT EXISTS idx_approval_workflow_steps_branch +ON approval_workflow_steps(branch_name, branch_order); + +-- Indexes for article_approval_flows +CREATE INDEX IF NOT EXISTS idx_article_approval_flows_branch +ON article_approval_flows(current_branch); + +CREATE INDEX IF NOT EXISTS idx_article_approval_flows_parent +ON article_approval_flows(parent_flow_id); + +CREATE INDEX IF NOT EXISTS idx_article_approval_flows_parallel +ON article_approval_flows(is_parallel_flow); + +-- ===================================================== +-- 4. MIGRATE EXISTING DATA +-- ===================================================== + +-- Set default values for existing approval_workflow_steps +UPDATE approval_workflow_steps +SET + condition_type = 'always', + branch_name = 'default_branch', + branch_order = step_order, + is_parallel = false +WHERE condition_type IS NULL; + +-- Set default values for existing article_approval_flows +UPDATE article_approval_flows +SET + current_branch = 'default_branch', + branch_path = '["default_branch"]', + is_parallel_flow = false +WHERE current_branch IS NULL; + +-- ===================================================== +-- 5. CREATE DEFAULT MULTI-BRANCH WORKFLOW +-- ===================================================== + +-- Insert default multi-branch workflow +INSERT INTO approval_workflows (name, description, is_active, is_default, requires_approval, auto_publish, created_at, updated_at) +VALUES ( + 'Default Multi-Branch Approval', + 'Default workflow dengan multi-branch support', + true, + true, + true, + false, + NOW(), + NOW() +); + +-- Get the workflow ID (assuming it's the last inserted) +-- Note: In production, you might want to use a specific ID or handle this differently +DO $$ +DECLARE + workflow_id_var INT; +BEGIN + -- Get the ID of the workflow we just created + SELECT id INTO workflow_id_var + FROM approval_workflows + WHERE name = 'Default Multi-Branch Approval' + ORDER BY id DESC + LIMIT 1; + + -- Insert workflow steps for the multi-branch workflow + INSERT INTO approval_workflow_steps ( + workflow_id, step_order, step_name, required_user_level_id, + can_skip, is_active, condition_type, condition_value, + branch_name, branch_order, created_at, updated_at + ) VALUES + -- Step 1: Branch A (Level 2) - for user levels 4,5,6 + ( + workflow_id_var, 1, 'Level 2 Branch', 2, + false, true, 'user_level_hierarchy', + '{"applies_to_levels": [4,5,6]}', + 'Branch_A', 1, NOW(), NOW() + ), + -- Step 1: Branch B (Level 3) - for user levels 7,8,9 + ( + workflow_id_var, 1, 'Level 3 Branch', 3, + false, true, 'user_level_hierarchy', + '{"applies_to_levels": [7,8,9]}', + 'Branch_B', 1, NOW(), NOW() + ), + -- Step 1: Branch C (Level 3) - for user levels 10,11,12 + ( + workflow_id_var, 1, 'Level 3 Branch C', 3, + false, true, 'user_level_hierarchy', + '{"applies_to_levels": [10,11,12]}', + 'Branch_C', 1, NOW(), NOW() + ), + -- Step 2: Final Approval (Level 1) - always applies + ( + workflow_id_var, 2, 'Level 1 Final Approval', 1, + false, true, 'always', + '{}', + 'Final_Approval', 1, NOW(), NOW() + ); +END $$; + +-- ===================================================== +-- 6. UPDATE EXISTING ARTICLES TO USE NEW WORKFLOW +-- ===================================================== + +-- Update existing articles to use the new default workflow +UPDATE articles +SET workflow_id = ( + SELECT id FROM approval_workflows + WHERE name = 'Default Multi-Branch Approval' + ORDER BY id DESC + LIMIT 1 +) +WHERE workflow_id IS NULL; + +-- ===================================================== +-- 7. CREATE SAMPLE DATA FOR TESTING +-- ===================================================== + +-- Insert sample user levels if they don't exist +INSERT INTO user_levels (name, alias_name, level_number, is_active, created_at, updated_at) +VALUES + ('Level 1', 'L1', 1, true, NOW(), NOW()), + ('Level 2', 'L2', 2, true, NOW(), NOW()), + ('Level 3', 'L3', 3, true, NOW(), NOW()), + ('Level 4', 'L4', 4, true, NOW(), NOW()), + ('Level 5', 'L5', 5, true, NOW(), NOW()), + ('Level 6', 'L6', 6, true, NOW(), NOW()), + ('Level 7', 'L7', 7, true, NOW(), NOW()), + ('Level 8', 'L8', 8, true, NOW(), NOW()), + ('Level 9', 'L9', 9, true, NOW(), NOW()), + ('Level 10', 'L10', 10, true, NOW(), NOW()), + ('Level 11', 'L11', 11, true, NOW(), NOW()), + ('Level 12', 'L12', 12, true, NOW(), NOW()) +ON CONFLICT (level_number) DO NOTHING; + +-- ===================================================== +-- 8. VERIFICATION QUERIES +-- ===================================================== + +-- Verify the migration was successful +SELECT 'Migration completed successfully' as status; + +-- Check workflow steps +SELECT + aws.id, + aws.step_order, + aws.step_name, + aws.required_user_level_id, + aws.condition_type, + aws.condition_value, + aws.branch_name, + aws.branch_order, + aw.name as workflow_name +FROM approval_workflow_steps aws +JOIN approval_workflows aw ON aws.workflow_id = aw.id +WHERE aw.name = 'Default Multi-Branch Approval' +ORDER BY aws.step_order, aws.branch_order; + +-- Check user levels +SELECT id, name, level_number, is_active +FROM user_levels +ORDER BY level_number; + +-- Check articles with workflow assignments +SELECT + a.id, + a.title, + a.workflow_id, + aw.name as workflow_name +FROM articles a +LEFT JOIN approval_workflows aw ON a.workflow_id = aw.id +LIMIT 10; + +-- ===================================================== +-- 9. ROLLBACK SCRIPT (if needed) +-- ===================================================== + +/* +-- Uncomment and run this section if you need to rollback the migration + +-- Drop indexes +DROP INDEX IF EXISTS idx_approval_workflow_steps_condition; +DROP INDEX IF EXISTS idx_approval_workflow_steps_parent; +DROP INDEX IF EXISTS idx_approval_workflow_steps_branch; +DROP INDEX IF EXISTS idx_article_approval_flows_branch; +DROP INDEX IF EXISTS idx_article_approval_flows_parent; +DROP INDEX IF EXISTS idx_article_approval_flows_parallel; + +-- Drop new columns from approval_workflow_steps +ALTER TABLE approval_workflow_steps DROP COLUMN IF EXISTS parent_step_id; +ALTER TABLE approval_workflow_steps DROP COLUMN IF EXISTS condition_type; +ALTER TABLE approval_workflow_steps DROP COLUMN IF EXISTS condition_value; +ALTER TABLE approval_workflow_steps DROP COLUMN IF EXISTS is_parallel; +ALTER TABLE approval_workflow_steps DROP COLUMN IF EXISTS branch_name; +ALTER TABLE approval_workflow_steps DROP COLUMN IF EXISTS branch_order; + +-- Drop new columns from article_approval_flows +ALTER TABLE article_approval_flows DROP COLUMN IF EXISTS current_branch; +ALTER TABLE article_approval_flows DROP COLUMN IF EXISTS branch_path; +ALTER TABLE article_approval_flows DROP COLUMN IF EXISTS is_parallel_flow; +ALTER TABLE article_approval_flows DROP COLUMN IF EXISTS parent_flow_id; + +-- Remove default multi-branch workflow +DELETE FROM approval_workflow_steps +WHERE workflow_id IN ( + SELECT id FROM approval_workflows + WHERE name = 'Default Multi-Branch Approval' +); + +DELETE FROM approval_workflows +WHERE name = 'Default Multi-Branch Approval'; + +SELECT 'Rollback completed successfully' as status; +*/ + +-- ===================================================== +-- 10. PERFORMANCE OPTIMIZATION +-- ===================================================== + +-- Update table statistics +ANALYZE approval_workflow_steps; +ANALYZE article_approval_flows; +ANALYZE approval_workflows; +ANALYZE user_levels; + +-- ===================================================== +-- END OF MIGRATION SCRIPT +-- ===================================================== diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index b644b2a..0e34ccd 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -3081,6 +3081,127 @@ const docTemplate = `{ } } }, + "/article-approval-flows/{id}/multi-branch-approve": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for processing multi-branch approval with conditional routing", + "tags": [ + "ArticleApprovalFlows" + ], + "summary": "Process multi-branch approval", + "parameters": [ + { + "type": "string", + "description": "Insert the Authorization", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "integer", + "description": "ArticleApprovalFlows ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Approval action data", + "name": "req", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.ApprovalActionRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, + "/article-approval-flows/{id}/next-steps-preview": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for getting preview of next steps based on submitter's user level", + "tags": [ + "ArticleApprovalFlows" + ], + "summary": "Get next steps preview for multi-branch workflow", + "parameters": [ + { + "type": "string", + "description": "Insert the Authorization", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "integer", + "description": "ArticleApprovalFlows ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, "/article-approval-flows/{id}/reject": { "put": { "security": [ @@ -16195,12 +16316,33 @@ const docTemplate = `{ "autoApproveAfterHours": { "type": "integer" }, + "branchName": { + "type": "string" + }, + "branchOrder": { + "type": "integer" + }, "canSkip": { "type": "boolean" }, + "conditionType": { + "description": "'user_level', 'user_level_hierarchy', 'always', 'custom'", + "type": "string" + }, + "conditionValue": { + "description": "JSON string for conditions", + "type": "string" + }, "isActive": { "type": "boolean" }, + "isParallel": { + "type": "boolean" + }, + "parentStepId": { + "description": "Multi-branch support fields", + "type": "integer" + }, "requiredUserLevelId": { "type": "integer" }, diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 40e2558..7f160bd 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -3070,6 +3070,127 @@ } } }, + "/article-approval-flows/{id}/multi-branch-approve": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for processing multi-branch approval with conditional routing", + "tags": [ + "ArticleApprovalFlows" + ], + "summary": "Process multi-branch approval", + "parameters": [ + { + "type": "string", + "description": "Insert the Authorization", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "integer", + "description": "ArticleApprovalFlows ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Approval action data", + "name": "req", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.ApprovalActionRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, + "/article-approval-flows/{id}/next-steps-preview": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for getting preview of next steps based on submitter's user level", + "tags": [ + "ArticleApprovalFlows" + ], + "summary": "Get next steps preview for multi-branch workflow", + "parameters": [ + { + "type": "string", + "description": "Insert the Authorization", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "integer", + "description": "ArticleApprovalFlows ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, "/article-approval-flows/{id}/reject": { "put": { "security": [ @@ -16184,12 +16305,33 @@ "autoApproveAfterHours": { "type": "integer" }, + "branchName": { + "type": "string" + }, + "branchOrder": { + "type": "integer" + }, "canSkip": { "type": "boolean" }, + "conditionType": { + "description": "'user_level', 'user_level_hierarchy', 'always', 'custom'", + "type": "string" + }, + "conditionValue": { + "description": "JSON string for conditions", + "type": "string" + }, "isActive": { "type": "boolean" }, + "isParallel": { + "type": "boolean" + }, + "parentStepId": { + "description": "Multi-branch support fields", + "type": "integer" + }, "requiredUserLevelId": { "type": "integer" }, diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 0034baf..43403fc 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -113,10 +113,25 @@ definitions: properties: autoApproveAfterHours: type: integer + branchName: + type: string + branchOrder: + type: integer canSkip: type: boolean + conditionType: + description: '''user_level'', ''user_level_hierarchy'', ''always'', ''custom''' + type: string + conditionValue: + description: JSON string for conditions + type: string isActive: type: boolean + isParallel: + type: boolean + parentStepId: + description: Multi-branch support fields + type: integer requiredUserLevelId: type: integer stepName: @@ -3101,6 +3116,85 @@ paths: summary: Approve article tags: - ArticleApprovalFlows + /article-approval-flows/{id}/multi-branch-approve: + post: + description: API for processing multi-branch approval with conditional routing + parameters: + - description: Insert the Authorization + in: header + name: Authorization + required: true + type: string + - description: ArticleApprovalFlows ID + in: path + name: id + required: true + type: integer + - description: Approval action data + in: body + name: req + required: true + schema: + $ref: '#/definitions/request.ApprovalActionRequest' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Process multi-branch approval + tags: + - ArticleApprovalFlows + /article-approval-flows/{id}/next-steps-preview: + get: + description: API for getting preview of next steps based on submitter's user + level + parameters: + - description: Insert the Authorization + in: header + name: Authorization + required: true + type: string + - description: ArticleApprovalFlows ID + in: path + name: id + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Get next steps preview for multi-branch workflow + tags: + - ArticleApprovalFlows /article-approval-flows/{id}/reject: put: description: API for rejecting article diff --git a/fix_approval_flow.sql b/fix_approval_flow.sql new file mode 100644 index 0000000..9f5c537 --- /dev/null +++ b/fix_approval_flow.sql @@ -0,0 +1,85 @@ +-- Script untuk memperbaiki masalah approval flow + +-- 1. Pastikan user level 5 memiliki is_approval_active = true +UPDATE user_levels +SET is_approval_active = true +WHERE id = 5; + +-- 2. Pastikan workflow yang dibuat adalah default workflow +UPDATE approval_workflows +SET is_default = true, is_active = true +WHERE name = 'Multi-Branch Article Approval'; + +-- 3. Cek apakah ada workflow default +SELECT + id, + name, + is_active, + is_default, + client_id +FROM approval_workflows +WHERE is_default = true +AND is_active = true; + +-- 4. Jika tidak ada default workflow, buat satu +-- (Gunakan ID workflow yang sudah dibuat) +-- UPDATE approval_workflows +-- SET is_default = true +-- WHERE id = YOUR_WORKFLOW_ID; + +-- 5. Cek user level 5 setelah update +SELECT + id, + level_name, + level_number, + is_approval_active, + client_id +FROM user_levels +WHERE id = 5; + +-- 6. Cek user dengan level 5 +SELECT + u.id, + u.name, + u.user_level_id, + ul.level_name, + ul.level_number, + ul.is_approval_active +FROM users u +JOIN user_levels ul ON u.user_level_id = ul.id +WHERE u.user_level_id = 5; + +-- 7. Test: Buat artikel baru untuk test +-- Gunakan API atau aplikasi untuk membuat artikel baru +-- dengan user level 5 + +-- 8. Cek hasil setelah test +SELECT + id, + title, + created_by_id, + workflow_id, + current_approval_step, + status_id, + bypass_approval, + approval_exempt, + created_at +FROM articles +ORDER BY created_at DESC +LIMIT 5; + +-- 9. Cek approval flows yang baru dibuat +SELECT + aaf.id, + aaf.article_id, + aaf.workflow_id, + aaf.current_step, + aaf.current_branch, + aaf.status_id, + aaf.submitted_by_id, + aaf.submitted_at, + a.title as article_title +FROM article_approval_flows aaf +JOIN articles a ON aaf.article_id = a.id +ORDER BY aaf.created_at DESC +LIMIT 5;