feat: update multi branch client approval
This commit is contained in:
parent
a18d5991b7
commit
23a2103ea3
|
|
@ -1,8 +1,9 @@
|
||||||
package entity
|
package entity
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/google/uuid"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ApprovalWorkflowSteps struct {
|
type ApprovalWorkflowSteps struct {
|
||||||
|
|
@ -14,6 +15,15 @@ type ApprovalWorkflowSteps struct {
|
||||||
CanSkip *bool `json:"can_skip" gorm:"type:bool;default:false"`
|
CanSkip *bool `json:"can_skip" gorm:"type:bool;default:false"`
|
||||||
AutoApproveAfterHours *int `json:"auto_approve_after_hours" gorm:"type:int4"`
|
AutoApproveAfterHours *int `json:"auto_approve_after_hours" gorm:"type:int4"`
|
||||||
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
|
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"`
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
||||||
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
|
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
|
||||||
|
|
@ -21,4 +31,6 @@ type ApprovalWorkflowSteps struct {
|
||||||
// Relations
|
// Relations
|
||||||
Workflow ApprovalWorkflows `json:"workflow" gorm:"foreignKey:WorkflowId;constraint:OnDelete:CASCADE"`
|
Workflow ApprovalWorkflows `json:"workflow" gorm:"foreignKey:WorkflowId;constraint:OnDelete:CASCADE"`
|
||||||
RequiredUserLevel UserLevels `json:"required_user_level" gorm:"foreignKey:RequiredUserLevelId"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
package entity
|
package entity
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/google/uuid"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ArticleApprovalFlows struct {
|
type ArticleApprovalFlows struct {
|
||||||
|
|
@ -17,6 +18,13 @@ type ArticleApprovalFlows struct {
|
||||||
RejectionReason *string `json:"rejection_reason" gorm:"type:text"`
|
RejectionReason *string `json:"rejection_reason" gorm:"type:text"`
|
||||||
RevisionRequested *bool `json:"revision_requested" gorm:"type:bool;default:false"`
|
RevisionRequested *bool `json:"revision_requested" gorm:"type:bool;default:false"`
|
||||||
RevisionMessage *string `json:"revision_message" gorm:"type:text"`
|
RevisionMessage *string `json:"revision_message" gorm:"type:text"`
|
||||||
|
|
||||||
|
// 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"`
|
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||||
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
||||||
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
|
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
|
||||||
|
|
@ -26,4 +34,6 @@ type ArticleApprovalFlows struct {
|
||||||
Workflow ApprovalWorkflows `json:"workflow" gorm:"foreignKey:WorkflowId"`
|
Workflow ApprovalWorkflows `json:"workflow" gorm:"foreignKey:WorkflowId"`
|
||||||
SubmittedBy *Users `json:"submitted_by" gorm:"foreignKey:SubmittedById"`
|
SubmittedBy *Users `json:"submitted_by" gorm:"foreignKey:SubmittedById"`
|
||||||
StepLogs []ArticleApprovalStepLogs `json:"step_logs" gorm:"foreignKey:ApprovalFlowId;constraint:OnDelete:CASCADE"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|
@ -127,7 +127,7 @@ func (m *Middleware) Register(db *database.Database) {
|
||||||
//===============================
|
//===============================
|
||||||
|
|
||||||
// Client middleware - must be applied before other business logic
|
// 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))
|
m.App.Use(AuditTrailsMiddleware(db.DB))
|
||||||
// StartAuditTrailCleanup(db.DB, m.Cfg.Middleware.AuditTrails.Retention)
|
// StartAuditTrailCleanup(db.DB, m.Cfg.Middleware.AuditTrails.Retention)
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,13 @@ type ApprovalWorkflowStepsRepository interface {
|
||||||
// Validation methods
|
// Validation methods
|
||||||
ValidateStepSequence(clientId *uuid.UUID, workflowId uint) (isValid bool, errors []string, err error)
|
ValidateStepSequence(clientId *uuid.UUID, workflowId uint) (isValid bool, errors []string, err error)
|
||||||
CheckStepDependencies(clientId *uuid.UUID, stepId uint) (canDelete bool, dependencies []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 {
|
func NewApprovalWorkflowStepsRepository(db *database.Database, log zerolog.Logger) ApprovalWorkflowStepsRepository {
|
||||||
|
|
@ -371,3 +378,97 @@ func (_i *approvalWorkflowStepsRepository) CheckStepDependencies(clientId *uuid.
|
||||||
canDelete = len(dependencies) == 0
|
canDelete = len(dependencies) == 0
|
||||||
return canDelete, dependencies, nil
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,14 @@ type ApprovalWorkflowStepRequest struct {
|
||||||
CanSkip *bool `json:"canSkip"`
|
CanSkip *bool `json:"canSkip"`
|
||||||
AutoApproveAfterHours *int `json:"autoApproveAfterHours"`
|
AutoApproveAfterHours *int `json:"autoApproveAfterHours"`
|
||||||
IsActive *bool `json:"isActive"`
|
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 {
|
func (req ApprovalWorkflowStepRequest) ToEntity(workflowId uint) *entity.ApprovalWorkflowSteps {
|
||||||
|
|
@ -98,6 +106,15 @@ func (req ApprovalWorkflowStepRequest) ToEntity(workflowId uint) *entity.Approva
|
||||||
CanSkip: req.CanSkip,
|
CanSkip: req.CanSkip,
|
||||||
AutoApproveAfterHours: req.AutoApproveAfterHours,
|
AutoApproveAfterHours: req.AutoApproveAfterHours,
|
||||||
IsActive: req.IsActive,
|
IsActive: req.IsActive,
|
||||||
|
|
||||||
|
// 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(),
|
CreatedAt: time.Now(),
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
@ -136,6 +153,15 @@ func (req ApprovalWorkflowsWithStepsCreateRequest) ToStepsEntity() []*entity.App
|
||||||
CanSkip: stepReq.CanSkip,
|
CanSkip: stepReq.CanSkip,
|
||||||
AutoApproveAfterHours: stepReq.AutoApproveAfterHours,
|
AutoApproveAfterHours: stepReq.AutoApproveAfterHours,
|
||||||
IsActive: stepReq.IsActive,
|
IsActive: stepReq.IsActive,
|
||||||
|
|
||||||
|
// 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(),
|
CreatedAt: time.Now(),
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"netidhub-saas-be/app/database/entity"
|
"netidhub-saas-be/app/database/entity"
|
||||||
|
|
@ -446,33 +447,106 @@ func (_i *approvalWorkflowsService) ValidateWorkflow(authToken string, workflow
|
||||||
if len(steps) == 0 {
|
if len(steps) == 0 {
|
||||||
errors = append(errors, "Workflow must have at least one step")
|
errors = append(errors, "Workflow must have at least one step")
|
||||||
} else {
|
} else {
|
||||||
// Check for duplicate step orders
|
// For multi-branch workflow, we need different validation logic
|
||||||
stepOrderMap := make(map[int]bool)
|
_i.validateMultiBranchSteps(steps, &errors)
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isValid = len(errors) == 0
|
isValid = len(errors) == 0
|
||||||
return isValid, errors, nil
|
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) {
|
func (_i *approvalWorkflowsService) CanDeleteWorkflow(authToken string, id uint) (canDelete bool, reason string, err error) {
|
||||||
// Extract clientId from authToken
|
// Extract clientId from authToken
|
||||||
var clientId *uuid.UUID
|
var clientId *uuid.UUID
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
package article_approval_flows
|
package article_approval_flows
|
||||||
|
|
||||||
import (
|
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/controller"
|
||||||
"netidhub-saas-be/app/module/article_approval_flows/repository"
|
"netidhub-saas-be/app/module/article_approval_flows/repository"
|
||||||
"netidhub-saas-be/app/module/article_approval_flows/service"
|
"netidhub-saas-be/app/module/article_approval_flows/service"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"go.uber.org/fx"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ArticleApprovalFlowsRouter struct of ArticleApprovalFlowsRouter
|
// ArticleApprovalFlowsRouter struct of ArticleApprovalFlowsRouter
|
||||||
|
|
@ -52,7 +53,9 @@ func (_i *ArticleApprovalFlowsRouter) RegisterArticleApprovalFlowsRoutes() {
|
||||||
router.Get("/workload-stats", articleApprovalFlowsController.GetWorkloadStats)
|
router.Get("/workload-stats", articleApprovalFlowsController.GetWorkloadStats)
|
||||||
router.Get("/analytics", articleApprovalFlowsController.GetApprovalAnalytics)
|
router.Get("/analytics", articleApprovalFlowsController.GetApprovalAnalytics)
|
||||||
router.Get("/:id", articleApprovalFlowsController.Show)
|
router.Get("/:id", articleApprovalFlowsController.Show)
|
||||||
|
router.Get("/:id/next-steps-preview", articleApprovalFlowsController.GetNextStepsPreview)
|
||||||
router.Post("/submit", articleApprovalFlowsController.SubmitForApproval)
|
router.Post("/submit", articleApprovalFlowsController.SubmitForApproval)
|
||||||
|
router.Post("/:id/multi-branch-approve", articleApprovalFlowsController.ProcessMultiBranchApproval)
|
||||||
router.Put("/:id/approve", articleApprovalFlowsController.Approve)
|
router.Put("/:id/approve", articleApprovalFlowsController.Approve)
|
||||||
router.Put("/:id/reject", articleApprovalFlowsController.Reject)
|
router.Put("/:id/reject", articleApprovalFlowsController.Reject)
|
||||||
router.Put("/:id/request-revision", articleApprovalFlowsController.RequestRevision)
|
router.Put("/:id/request-revision", articleApprovalFlowsController.RequestRevision)
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,10 @@ type ArticleApprovalFlowsController interface {
|
||||||
GetDashboardStats(c *fiber.Ctx) error
|
GetDashboardStats(c *fiber.Ctx) error
|
||||||
GetWorkloadStats(c *fiber.Ctx) error
|
GetWorkloadStats(c *fiber.Ctx) error
|
||||||
GetApprovalAnalytics(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 {
|
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,
|
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),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"netidhub-saas-be/app/database/entity"
|
"netidhub-saas-be/app/database/entity"
|
||||||
approvalWorkflowStepsRepo "netidhub-saas-be/app/module/approval_workflow_steps/repository"
|
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)
|
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)
|
GetCurrentStepInfo(authToken string, flowId uint) (stepInfo map[string]interface{}, err error)
|
||||||
GetNextStepPreview(authToken string, flowId uint) (nextStep *entity.ApprovalWorkflowSteps, 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(
|
func NewArticleApprovalFlowsService(
|
||||||
|
|
@ -1081,3 +1089,291 @@ func (_i *articleApprovalFlowsService) GetNextStepPreview(authToken string, flow
|
||||||
|
|
||||||
return nextStep, nil
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package articles
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"netidhub-saas-be/app/middleware"
|
"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/controller"
|
||||||
"netidhub-saas-be/app/module/articles/repository"
|
"netidhub-saas-be/app/module/articles/repository"
|
||||||
"netidhub-saas-be/app/module/articles/service"
|
"netidhub-saas-be/app/module/articles/service"
|
||||||
|
|
@ -34,7 +35,7 @@ var NewArticlesModule = fx.Options(
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewArticlesRouter init ArticlesRouter
|
// 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{
|
return &ArticlesRouter{
|
||||||
App: fiber,
|
App: fiber,
|
||||||
Controller: controller,
|
Controller: controller,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
@ -11,6 +12,7 @@ import (
|
||||||
"netidhub-saas-be/app/database/entity"
|
"netidhub-saas-be/app/database/entity"
|
||||||
approvalWorkflowsRepository "netidhub-saas-be/app/module/approval_workflows/repository"
|
approvalWorkflowsRepository "netidhub-saas-be/app/module/approval_workflows/repository"
|
||||||
articleApprovalFlowsRepository "netidhub-saas-be/app/module/article_approval_flows/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"
|
articleApprovalsRepository "netidhub-saas-be/app/module/article_approvals/repository"
|
||||||
articleCategoriesRepository "netidhub-saas-be/app/module/article_categories/repository"
|
articleCategoriesRepository "netidhub-saas-be/app/module/article_categories/repository"
|
||||||
articleCategoryDetailsRepository "netidhub-saas-be/app/module/article_category_details/repository"
|
articleCategoryDetailsRepository "netidhub-saas-be/app/module/article_category_details/repository"
|
||||||
|
|
@ -51,6 +53,7 @@ type articlesService struct {
|
||||||
// Dynamic approval system dependencies
|
// Dynamic approval system dependencies
|
||||||
ArticleApprovalFlowsRepo articleApprovalFlowsRepository.ArticleApprovalFlowsRepository
|
ArticleApprovalFlowsRepo articleApprovalFlowsRepository.ArticleApprovalFlowsRepository
|
||||||
ApprovalWorkflowsRepo approvalWorkflowsRepository.ApprovalWorkflowsRepository
|
ApprovalWorkflowsRepo approvalWorkflowsRepository.ApprovalWorkflowsRepository
|
||||||
|
ArticleApprovalFlowsSvc articleApprovalFlowsService.ArticleApprovalFlowsService
|
||||||
}
|
}
|
||||||
|
|
||||||
// ArticlesService define interface of IArticlesService
|
// ArticlesService define interface of IArticlesService
|
||||||
|
|
@ -94,6 +97,7 @@ func NewArticlesService(
|
||||||
articleApprovalsRepo articleApprovalsRepository.ArticleApprovalsRepository,
|
articleApprovalsRepo articleApprovalsRepository.ArticleApprovalsRepository,
|
||||||
articleApprovalFlowsRepo articleApprovalFlowsRepository.ArticleApprovalFlowsRepository,
|
articleApprovalFlowsRepo articleApprovalFlowsRepository.ArticleApprovalFlowsRepository,
|
||||||
approvalWorkflowsRepo approvalWorkflowsRepository.ApprovalWorkflowsRepository,
|
approvalWorkflowsRepo approvalWorkflowsRepository.ApprovalWorkflowsRepository,
|
||||||
|
articleApprovalFlowsSvc articleApprovalFlowsService.ArticleApprovalFlowsService,
|
||||||
log zerolog.Logger,
|
log zerolog.Logger,
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
usersRepo usersRepository.UsersRepository,
|
usersRepo usersRepository.UsersRepository,
|
||||||
|
|
@ -108,6 +112,7 @@ func NewArticlesService(
|
||||||
ArticleApprovalsRepo: articleApprovalsRepo,
|
ArticleApprovalsRepo: articleApprovalsRepo,
|
||||||
ArticleApprovalFlowsRepo: articleApprovalFlowsRepo,
|
ArticleApprovalFlowsRepo: articleApprovalFlowsRepo,
|
||||||
ApprovalWorkflowsRepo: approvalWorkflowsRepo,
|
ApprovalWorkflowsRepo: approvalWorkflowsRepo,
|
||||||
|
ArticleApprovalFlowsSvc: articleApprovalFlowsSvc,
|
||||||
Log: log,
|
Log: log,
|
||||||
UsersRepo: usersRepo,
|
UsersRepo: usersRepo,
|
||||||
MinioStorage: minioStorage,
|
MinioStorage: minioStorage,
|
||||||
|
|
@ -275,6 +280,13 @@ func (_i *articlesService) Save(authToken string, req request.ArticlesCreateRequ
|
||||||
|
|
||||||
// Check if user level requires approval
|
// Check if user level requires approval
|
||||||
if createdBy != nil && *createdBy.UserLevel.IsApprovalActive == false {
|
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
|
// User level doesn't require approval - auto publish
|
||||||
newReq.NeedApprovalFrom = nil
|
newReq.NeedApprovalFrom = nil
|
||||||
newReq.StatusId = &statusIdTwo
|
newReq.StatusId = &statusIdTwo
|
||||||
|
|
@ -282,6 +294,18 @@ func (_i *articlesService) Save(authToken string, req request.ArticlesCreateRequ
|
||||||
newReq.PublishedAt = nil
|
newReq.PublishedAt = nil
|
||||||
newReq.BypassApproval = &[]bool{true}[0]
|
newReq.BypassApproval = &[]bool{true}[0]
|
||||||
} else {
|
} 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
|
// User level requires approval - set to pending
|
||||||
newReq.NeedApprovalFrom = &approvalLevelId
|
newReq.NeedApprovalFrom = &approvalLevelId
|
||||||
newReq.StatusId = &statusIdOne
|
newReq.StatusId = &statusIdOne
|
||||||
|
|
@ -295,11 +319,29 @@ func (_i *articlesService) Save(authToken string, req request.ArticlesCreateRequ
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamic Approval Workflow Assignment
|
// Dynamic Approval Workflow Assignment with Multi-Branch Support
|
||||||
if createdBy != nil && *createdBy.UserLevel.IsApprovalActive == true {
|
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
|
// Get default workflow for the client
|
||||||
defaultWorkflow, err := _i.ApprovalWorkflowsRepo.GetDefault(clientId)
|
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
|
// Assign workflow to article
|
||||||
saveArticleRes.WorkflowId = &defaultWorkflow.ID
|
saveArticleRes.WorkflowId = &defaultWorkflow.ID
|
||||||
saveArticleRes.CurrentApprovalStep = &[]int{1}[0] // Start at step 1
|
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)
|
err = _i.Repo.Update(clientId, saveArticleRes.ID, saveArticleRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_i.Log.Error().Err(err).Msg("Failed to update article with workflow")
|
_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{
|
approvalFlow := &entity.ArticleApprovalFlows{
|
||||||
ArticleId: saveArticleRes.ID,
|
ArticleId: saveArticleRes.ID,
|
||||||
WorkflowId: defaultWorkflow.ID,
|
WorkflowId: defaultWorkflow.ID,
|
||||||
|
|
@ -319,11 +366,32 @@ func (_i *articlesService) Save(authToken string, req request.ArticlesCreateRequ
|
||||||
SubmittedById: *newReq.CreatedById,
|
SubmittedById: *newReq.CreatedById,
|
||||||
SubmittedAt: time.Now(),
|
SubmittedAt: time.Now(),
|
||||||
ClientId: clientId,
|
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 {
|
if err != nil {
|
||||||
_i.Log.Error().Err(err).Msg("Failed to create approval flow")
|
_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
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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!** 🚀
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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'
|
||||||
|
```
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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!** 🔍
|
||||||
|
|
@ -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
|
||||||
|
-- =====================================================
|
||||||
|
|
@ -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": {
|
"/article-approval-flows/{id}/reject": {
|
||||||
"put": {
|
"put": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|
@ -16195,12 +16316,33 @@ const docTemplate = `{
|
||||||
"autoApproveAfterHours": {
|
"autoApproveAfterHours": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"branchName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"branchOrder": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"canSkip": {
|
"canSkip": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"conditionType": {
|
||||||
|
"description": "'user_level', 'user_level_hierarchy', 'always', 'custom'",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"conditionValue": {
|
||||||
|
"description": "JSON string for conditions",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"isActive": {
|
"isActive": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"isParallel": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"parentStepId": {
|
||||||
|
"description": "Multi-branch support fields",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"requiredUserLevelId": {
|
"requiredUserLevelId": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
"/article-approval-flows/{id}/reject": {
|
||||||
"put": {
|
"put": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|
@ -16184,12 +16305,33 @@
|
||||||
"autoApproveAfterHours": {
|
"autoApproveAfterHours": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"branchName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"branchOrder": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"canSkip": {
|
"canSkip": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"conditionType": {
|
||||||
|
"description": "'user_level', 'user_level_hierarchy', 'always', 'custom'",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"conditionValue": {
|
||||||
|
"description": "JSON string for conditions",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"isActive": {
|
"isActive": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"isParallel": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"parentStepId": {
|
||||||
|
"description": "Multi-branch support fields",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"requiredUserLevelId": {
|
"requiredUserLevelId": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -113,10 +113,25 @@ definitions:
|
||||||
properties:
|
properties:
|
||||||
autoApproveAfterHours:
|
autoApproveAfterHours:
|
||||||
type: integer
|
type: integer
|
||||||
|
branchName:
|
||||||
|
type: string
|
||||||
|
branchOrder:
|
||||||
|
type: integer
|
||||||
canSkip:
|
canSkip:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
conditionType:
|
||||||
|
description: '''user_level'', ''user_level_hierarchy'', ''always'', ''custom'''
|
||||||
|
type: string
|
||||||
|
conditionValue:
|
||||||
|
description: JSON string for conditions
|
||||||
|
type: string
|
||||||
isActive:
|
isActive:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
isParallel:
|
||||||
|
type: boolean
|
||||||
|
parentStepId:
|
||||||
|
description: Multi-branch support fields
|
||||||
|
type: integer
|
||||||
requiredUserLevelId:
|
requiredUserLevelId:
|
||||||
type: integer
|
type: integer
|
||||||
stepName:
|
stepName:
|
||||||
|
|
@ -3101,6 +3116,85 @@ paths:
|
||||||
summary: Approve article
|
summary: Approve article
|
||||||
tags:
|
tags:
|
||||||
- ArticleApprovalFlows
|
- 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:
|
/article-approval-flows/{id}/reject:
|
||||||
put:
|
put:
|
||||||
description: API for rejecting article
|
description: API for rejecting article
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
Loading…
Reference in New Issue