# Contoh Implementasi Kode - Sistem Approval Artikel Dinamis ## 1. Database Entities ### ApprovalWorkflows Entity ```go // app/database/entity/approval_workflows.entity.go package entity import ( "github.com/google/uuid" "time" ) type ApprovalWorkflows struct { ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"` Name string `json:"name" gorm:"type:varchar;not null"` Description *string `json:"description" gorm:"type:text"` IsActive *bool `json:"is_active" gorm:"type:bool;default:true"` ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"` CreatedAt time.Time `json:"created_at" gorm:"default:now()"` UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` // Relations Steps []ApprovalWorkflowSteps `json:"steps" gorm:"foreignKey:WorkflowId;references:ID"` } ``` ### ApprovalWorkflowSteps Entity ```go // app/database/entity/approval_workflow_steps.entity.go package entity import ( "github.com/google/uuid" "time" ) type ApprovalWorkflowSteps struct { ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"` WorkflowId uint `json:"workflow_id" gorm:"type:int4;not null"` StepOrder int `json:"step_order" gorm:"type:int4;not null"` UserLevelId uint `json:"user_level_id" gorm:"type:int4;not null"` IsRequired *bool `json:"is_required" gorm:"type:bool;default:true"` CanSkip *bool `json:"can_skip" gorm:"type:bool;default:false"` ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"` CreatedAt time.Time `json:"created_at" gorm:"default:now()"` UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` // Relations Workflow *ApprovalWorkflows `json:"workflow" gorm:"foreignKey:WorkflowId;references:ID"` UserLevel *UserLevels `json:"user_level" gorm:"foreignKey:UserLevelId;references:ID"` } ``` ### ArticleApprovalFlows Entity ```go // app/database/entity/article_approval_flows.entity.go package entity import ( "github.com/google/uuid" "time" ) type ArticleApprovalFlows struct { ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"` ArticleId uint `json:"article_id" gorm:"type:int4;not null"` WorkflowId uint `json:"workflow_id" gorm:"type:int4;not null"` CurrentStep int `json:"current_step" gorm:"type:int4;default:1"` StatusId int `json:"status_id" gorm:"type:int4;default:1"` // 1=In Progress, 2=Completed, 3=Rejected ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"` CreatedAt time.Time `json:"created_at" gorm:"default:now()"` UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` // Relations Article *Articles `json:"article" gorm:"foreignKey:ArticleId;references:ID"` Workflow *ApprovalWorkflows `json:"workflow" gorm:"foreignKey:WorkflowId;references:ID"` StepLogs []ArticleApprovalStepLogs `json:"step_logs" gorm:"foreignKey:ArticleFlowId;references:ID"` } ``` ### ArticleApprovalStepLogs Entity ```go // app/database/entity/article_approval_step_logs.entity.go package entity import ( "github.com/google/uuid" "time" users "netidhub-saas-be/app/database/entity/users" ) type ArticleApprovalStepLogs struct { ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"` ArticleFlowId uint `json:"article_flow_id" gorm:"type:int4;not null"` StepOrder int `json:"step_order" gorm:"type:int4;not null"` UserLevelId uint `json:"user_level_id" gorm:"type:int4;not null"` ApprovedBy uint `json:"approved_by" gorm:"type:int4;not null"` Action string `json:"action" gorm:"type:varchar(50);not null"` // 'approved', 'rejected', 'revision_requested' Message *string `json:"message" gorm:"type:text"` ApprovedAt *time.Time `json:"approved_at" gorm:"type:timestamp"` ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"` CreatedAt time.Time `json:"created_at" gorm:"default:now()"` // Relations ArticleFlow *ArticleApprovalFlows `json:"article_flow" gorm:"foreignKey:ArticleFlowId;references:ID"` UserLevel *UserLevels `json:"user_level" gorm:"foreignKey:UserLevelId;references:ID"` Approver *users.Users `json:"approver" gorm:"foreignKey:ApprovedBy;references:ID"` } ``` ## 2. Service Layer Implementation ### ApprovalWorkflowService ```go // app/module/approval_workflows/service/approval_workflows.service.go package service import ( "errors" "github.com/google/uuid" "github.com/rs/zerolog" "netidhub-saas-be/app/database/entity" "netidhub-saas-be/app/module/approval_workflows/repository" "netidhub-saas-be/app/module/approval_workflows/request" "netidhub-saas-be/app/module/approval_workflows/response" "netidhub-saas-be/utils/paginator" ) type approvalWorkflowService struct { Repo repository.ApprovalWorkflowRepository Log zerolog.Logger } type ApprovalWorkflowService interface { All(clientId *uuid.UUID, req request.ApprovalWorkflowQueryRequest) ([]*response.ApprovalWorkflowResponse, paginator.Pagination, error) Show(clientId *uuid.UUID, id uint) (*response.ApprovalWorkflowResponse, error) Save(clientId *uuid.UUID, req request.ApprovalWorkflowCreateRequest) (*entity.ApprovalWorkflows, error) Update(clientId *uuid.UUID, id uint, req request.ApprovalWorkflowUpdateRequest) error Delete(clientId *uuid.UUID, id uint) error GetActiveWorkflow(clientId *uuid.UUID) (*entity.ApprovalWorkflows, error) ValidateWorkflow(workflowId uint) error } func NewApprovalWorkflowService(repo repository.ApprovalWorkflowRepository, log zerolog.Logger) ApprovalWorkflowService { return &approvalWorkflowService{ Repo: repo, Log: log, } } func (s *approvalWorkflowService) Save(clientId *uuid.UUID, req request.ApprovalWorkflowCreateRequest) (*entity.ApprovalWorkflows, error) { // Validate workflow steps if len(req.Steps) == 0 { return nil, errors.New("workflow must have at least one step") } // Validate step order sequence for i, step := range req.Steps { if step.StepOrder != i+1 { return nil, errors.New("step order must be sequential starting from 1") } } workflow := req.ToEntity() workflow.ClientId = clientId return s.Repo.Create(workflow) } func (s *approvalWorkflowService) ValidateWorkflow(workflowId uint) error { workflow, err := s.Repo.FindOneWithSteps(workflowId) if err != nil { return err } if workflow.IsActive == nil || !*workflow.IsActive { return errors.New("workflow is not active") } if len(workflow.Steps) == 0 { return errors.New("workflow has no steps defined") } return nil } ``` ### Dynamic Article Approval Service ```go // app/module/article_approval_flows/service/article_approval_flows.service.go package service import ( "errors" "fmt" "github.com/google/uuid" "github.com/rs/zerolog" "time" "netidhub-saas-be/app/database/entity" users "netidhub-saas-be/app/database/entity/users" "netidhub-saas-be/app/module/article_approval_flows/repository" "netidhub-saas-be/app/module/articles/repository" as articlesRepo "netidhub-saas-be/app/module/approval_workflows/service" as workflowService ) type articleApprovalFlowService struct { Repo repository.ArticleApprovalFlowRepository ArticlesRepo articlesRepo.ArticlesRepository WorkflowService workflowService.ApprovalWorkflowService Log zerolog.Logger } type ArticleApprovalFlowService interface { InitiateApprovalFlow(clientId *uuid.UUID, articleId uint, workflowId uint) (*entity.ArticleApprovalFlows, error) ProcessApprovalStep(clientId *uuid.UUID, articleId uint, action string, message *string, approver *users.Users) error GetPendingApprovals(clientId *uuid.UUID, userLevelId uint, filters ApprovalFilters) ([]*entity.ArticleApprovalFlows, paginator.Pagination, error) GetMyApprovalQueue(clientId *uuid.UUID, userLevelId uint, filters ApprovalFilters) ([]*DetailedApprovalFlow, paginator.Pagination, error) GetApprovalHistory(clientId *uuid.UUID, articleId uint) ([]*entity.ArticleApprovalStepLogs, error) GetApprovalStatistics(clientId *uuid.UUID, userLevelId uint) (*ApprovalStatistics, error) GetWorkloadDistribution(clientId *uuid.UUID, userLevelId uint) (*WorkloadDistribution, error) BulkApprovalAction(clientId *uuid.UUID, articleIds []uint, action string, message *string, approver *users.Users) (*BulkActionResult, error) } // Supporting structs for enhanced approver dashboard type ApprovalFilters struct { Page int Limit int Priority *string CategoryId *uint Search *string DateFrom *time.Time DateTo *time.Time SortBy string SortOrder string WorkflowId *uint UrgencyOnly bool IncludePreview bool } type DetailedApprovalFlow struct { ID uint `json:"id"` Article DetailedArticleInfo `json:"article"` ApprovalContext ApprovalContext `json:"approval_context"` WorkflowProgress WorkflowProgress `json:"workflow_progress"` } type DetailedArticleInfo struct { ID uint `json:"id"` Title string `json:"title"` ContentPreview *string `json:"content_preview"` FullContentAvailable bool `json:"full_content_available"` Author AuthorInfo `json:"author"` Category CategoryInfo `json:"category"` SubmissionNotes *string `json:"submission_notes"` Tags []string `json:"tags"` WordCount int `json:"word_count"` EstimatedReadTime string `json:"estimated_read_time"` SEOScore int `json:"seo_score"` ReadabilityScore string `json:"readability_score"` CreatedAt time.Time `json:"created_at"` SubmittedAt time.Time `json:"submitted_at"` } type AuthorInfo struct { ID uint `json:"id"` Name string `json:"name"` Email string `json:"email"` ProfilePicture *string `json:"profile_picture"` ReputationScore float64 `json:"reputation_score"` ArticlesPublished int `json:"articles_published"` ApprovalSuccessRate float64 `json:"approval_success_rate"` } type CategoryInfo struct { ID uint `json:"id"` Name string `json:"name"` Color string `json:"color"` } type ApprovalContext struct { MyRoleInWorkflow string `json:"my_role_in_workflow"` StepDescription string `json:"step_description"` ExpectedAction string `json:"expected_action"` Deadline *time.Time `json:"deadline"` IsOverdue bool `json:"is_overdue"` EscalationAvailable bool `json:"escalation_available"` } type WorkflowProgress struct { CompletedSteps int `json:"completed_steps"` TotalSteps int `json:"total_steps"` ProgressPercentage float64 `json:"progress_percentage"` NextApprover string `json:"next_approver"` } type ApprovalStatistics struct { PendingCount int `json:"pending_count"` OverdueCount int `json:"overdue_count"` ApprovedToday int `json:"approved_today"` ApprovedThisWeek int `json:"approved_this_week"` ApprovedThisMonth int `json:"approved_this_month"` RejectedThisMonth int `json:"rejected_this_month"` RevisionRequestsThisMonth int `json:"revision_requests_this_month"` AverageApprovalTimeHours float64 `json:"average_approval_time_hours"` FastestApprovalMinutes int `json:"fastest_approval_minutes"` SlowestApprovalHours int `json:"slowest_approval_hours"` ApprovalRatePercentage float64 `json:"approval_rate_percentage"` CategoriesBreakdown []CategoryBreakdown `json:"categories_breakdown"` } type CategoryBreakdown struct { CategoryName string `json:"category_name"` Pending int `json:"pending"` ApprovedThisMonth int `json:"approved_this_month"` } type WorkloadDistribution struct { MyLevel LevelWorkload `json:"my_level"` TeamComparison []TeamMember `json:"team_comparison"` Bottlenecks []BottleneckInfo `json:"bottlenecks"` } type LevelWorkload struct { LevelName string `json:"level_name"` LevelNumber int `json:"level_number"` PendingArticles int `json:"pending_articles"` AvgDailyApprovals float64 `json:"avg_daily_approvals"` } type TeamMember struct { ApproverName string `json:"approver_name"` LevelName string `json:"level_name"` PendingArticles int `json:"pending_articles"` AvgResponseTimeHours float64 `json:"avg_response_time_hours"` } type BottleneckInfo struct { LevelName string `json:"level_name"` PendingCount int `json:"pending_count"` AvgWaitingTimeHours float64 `json:"avg_waiting_time_hours"` IsBottleneck bool `json:"is_bottleneck"` } type BulkActionResult struct { ProcessedCount int `json:"processed_count"` SuccessfulCount int `json:"successful_count"` FailedCount int `json:"failed_count"` Results []BulkActionItemResult `json:"results"` } type BulkActionItemResult struct { ArticleId uint `json:"article_id"` Status string `json:"status"` NextStep string `json:"next_step"` Error *string `json:"error,omitempty"` } func NewArticleApprovalFlowService( repo repository.ArticleApprovalFlowRepository, articlesRepo articlesRepo.ArticlesRepository, workflowService workflowService.ApprovalWorkflowService, log zerolog.Logger, ) ArticleApprovalFlowService { return &articleApprovalFlowService{ Repo: repo, ArticlesRepo: articlesRepo, WorkflowService: workflowService, Log: log, } } func (s *articleApprovalFlowService) InitiateApprovalFlow(clientId *uuid.UUID, articleId uint, workflowId uint) (*entity.ArticleApprovalFlows, error) { // Validate workflow err := s.WorkflowService.ValidateWorkflow(workflowId) if err != nil { return nil, fmt.Errorf("invalid workflow: %v", err) } // Check if article already has active approval flow existingFlow, _ := s.Repo.FindActiveByArticleId(articleId) if existingFlow != nil { return nil, errors.New("article already has active approval flow") } // Create new approval flow approvalFlow := &entity.ArticleApprovalFlows{ ArticleId: articleId, WorkflowId: workflowId, CurrentStep: 1, StatusId: 1, // In Progress ClientId: clientId, } // Update article status statusId := 1 // Pending approval err = s.ArticlesRepo.UpdateSkipNull(clientId, articleId, &entity.Articles{ StatusId: &statusId, WorkflowId: &workflowId, CurrentApprovalStep: &[]int{1}[0], }) if err != nil { return nil, fmt.Errorf("failed to update article status: %v", err) } return s.Repo.Create(approvalFlow) } func (s *articleApprovalFlowService) ProcessApprovalStep(clientId *uuid.UUID, articleId uint, action string, message *string, approver *users.Users) error { // Get active approval flow approvalFlow, err := s.Repo.FindActiveByArticleId(articleId) if err != nil { return fmt.Errorf("approval flow not found: %v", err) } // Get workflow with steps workflow, err := s.WorkflowService.GetWorkflowWithSteps(approvalFlow.WorkflowId) if err != nil { return fmt.Errorf("workflow not found: %v", err) } // Find current step var currentStep *entity.ApprovalWorkflowSteps for _, step := range workflow.Steps { if step.StepOrder == approvalFlow.CurrentStep { currentStep = &step break } } if currentStep == nil { return errors.New("current step not found in workflow") } // Validate approver has permission for this step if approver.UserLevelId != currentStep.UserLevelId { return errors.New("approver does not have permission for this approval step") } // Create step log now := time.Now() stepLog := &entity.ArticleApprovalStepLogs{ ArticleFlowId: approvalFlow.ID, StepOrder: approvalFlow.CurrentStep, UserLevelId: currentStep.UserLevelId, ApprovedBy: approver.ID, Action: action, Message: message, ApprovedAt: &now, ClientId: clientId, } err = s.Repo.CreateStepLog(stepLog) if err != nil { return fmt.Errorf("failed to create step log: %v", err) } // Process action switch action { case "approved": return s.processApproval(clientId, approvalFlow, workflow) case "rejected": return s.processRejection(clientId, approvalFlow) case "revision_requested": return s.processRevisionRequest(clientId, approvalFlow) default: return errors.New("invalid action") } } func (s *articleApprovalFlowService) processApproval(clientId *uuid.UUID, approvalFlow *entity.ArticleApprovalFlows, workflow *entity.ApprovalWorkflows) error { // Check if this is the last step maxStep := 0 for _, step := range workflow.Steps { if step.StepOrder > maxStep { maxStep = step.StepOrder } } if approvalFlow.CurrentStep >= maxStep { // Final approval - publish article return s.finalizeApproval(clientId, approvalFlow) } else { // Move to next step return s.moveToNextStep(clientId, approvalFlow) } } func (s *articleApprovalFlowService) finalizeApproval(clientId *uuid.UUID, approvalFlow *entity.ArticleApprovalFlows) error { // Update approval flow status approvalFlow.StatusId = 2 // Completed err := s.Repo.Update(approvalFlow.ID, approvalFlow) if err != nil { return err } // Update article status to published isPublish := true statusId := 2 // Published now := time.Now() err = s.ArticlesRepo.UpdateSkipNull(clientId, approvalFlow.ArticleId, &entity.Articles{ StatusId: &statusId, IsPublish: &isPublish, PublishedAt: &now, CurrentApprovalStep: nil, // Clear approval step }) return err } func (s *articleApprovalFlowService) moveToNextStep(clientId *uuid.UUID, approvalFlow *entity.ArticleApprovalFlows) error { // Move to next step approvalFlow.CurrentStep++ err := s.Repo.Update(approvalFlow.ID, approvalFlow) if err != nil { return err } // Update article current step err = s.ArticlesRepo.UpdateSkipNull(clientId, approvalFlow.ArticleId, &entity.Articles{ CurrentApprovalStep: &approvalFlow.CurrentStep, }) return err } func (s *articleApprovalFlowService) processRejection(clientId *uuid.UUID, approvalFlow *entity.ArticleApprovalFlows) error { // Update approval flow status approvalFlow.StatusId = 3 // Rejected err := s.Repo.Update(approvalFlow.ID, approvalFlow) if err != nil { return err } // Update article status statusId := 3 // Rejected err = s.ArticlesRepo.UpdateSkipNull(clientId, approvalFlow.ArticleId, &entity.Articles{ StatusId: &statusId, CurrentApprovalStep: nil, }) return err } func (s *articleApprovalFlowService) processRevisionRequest(clientId *uuid.UUID, approvalFlow *entity.ArticleApprovalFlows) error { // Reset approval flow to step 0 (back to contributor) approvalFlow.CurrentStep = 0 approvalFlow.StatusId = 4 // Revision Requested err := s.Repo.Update(approvalFlow.ID, approvalFlow) if err != nil { return err } // Update article status statusId := 4 // Revision Requested err = s.ArticlesRepo.UpdateSkipNull(clientId, approvalFlow.ArticleId, &entity.Articles{ StatusId: &statusId, CurrentApprovalStep: &[]int{0}[0], }) return err } // Enhanced methods for approver dashboard func (s *articleApprovalFlowService) GetMyApprovalQueue(clientId *uuid.UUID, userLevelId uint, filters ApprovalFilters) ([]*DetailedApprovalFlow, paginator.Pagination, error) { // Build query with filters query := s.Repo.BuildApprovalQueueQuery(clientId, userLevelId, filters) // Get paginated results approvalFlows, pagination, err := s.Repo.GetPaginatedApprovalQueue(query, filters.Page, filters.Limit) if err != nil { return nil, paginator.Pagination{}, err } // Transform to detailed approval flows detailedFlows := make([]*DetailedApprovalFlow, 0, len(approvalFlows)) for _, flow := range approvalFlows { detailedFlow, err := s.transformToDetailedFlow(flow, filters.IncludePreview) if err != nil { s.Log.Error().Err(err).Uint("flow_id", flow.ID).Msg("Failed to transform approval flow") continue } detailedFlows = append(detailedFlows, detailedFlow) } return detailedFlows, pagination, nil } func (s *articleApprovalFlowService) GetApprovalStatistics(clientId *uuid.UUID, userLevelId uint) (*ApprovalStatistics, error) { stats := &ApprovalStatistics{} // Get pending count pendingCount, err := s.Repo.GetPendingCountByLevel(clientId, userLevelId) if err != nil { return nil, err } stats.PendingCount = pendingCount // Get overdue count overdueCount, err := s.Repo.GetOverdueCountByLevel(clientId, userLevelId) if err != nil { return nil, err } stats.OverdueCount = overdueCount // Get approval counts by time period now := time.Now() startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) startOfWeek := startOfDay.AddDate(0, 0, -int(now.Weekday())) startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) stats.ApprovedToday, _ = s.Repo.GetApprovedCountByPeriod(clientId, userLevelId, startOfDay, now) stats.ApprovedThisWeek, _ = s.Repo.GetApprovedCountByPeriod(clientId, userLevelId, startOfWeek, now) stats.ApprovedThisMonth, _ = s.Repo.GetApprovedCountByPeriod(clientId, userLevelId, startOfMonth, now) stats.RejectedThisMonth, _ = s.Repo.GetRejectedCountByPeriod(clientId, userLevelId, startOfMonth, now) stats.RevisionRequestsThisMonth, _ = s.Repo.GetRevisionRequestCountByPeriod(clientId, userLevelId, startOfMonth, now) // Get timing statistics timingStats, err := s.Repo.GetApprovalTimingStats(clientId, userLevelId) if err == nil { stats.AverageApprovalTimeHours = timingStats.AverageHours stats.FastestApprovalMinutes = timingStats.FastestMinutes stats.SlowestApprovalHours = timingStats.SlowestHours } // Calculate approval rate totalProcessed := stats.ApprovedThisMonth + stats.RejectedThisMonth if totalProcessed > 0 { stats.ApprovalRatePercentage = float64(stats.ApprovedThisMonth) / float64(totalProcessed) * 100 } // Get categories breakdown categoriesBreakdown, err := s.Repo.GetCategoriesBreakdown(clientId, userLevelId) if err == nil { stats.CategoriesBreakdown = categoriesBreakdown } return stats, nil } func (s *articleApprovalFlowService) GetWorkloadDistribution(clientId *uuid.UUID, userLevelId uint) (*WorkloadDistribution, error) { distribution := &WorkloadDistribution{} // Get my level workload myLevel, err := s.Repo.GetLevelWorkload(clientId, userLevelId) if err != nil { return nil, err } distribution.MyLevel = *myLevel // Get team comparison (same level users) teamMembers, err := s.Repo.GetTeamWorkloadComparison(clientId, userLevelId) if err == nil { distribution.TeamComparison = teamMembers } // Identify bottlenecks bottlenecks, err := s.Repo.GetWorkflowBottlenecks(clientId) if err == nil { distribution.Bottlenecks = bottlenecks } return distribution, nil } func (s *articleApprovalFlowService) BulkApprovalAction(clientId *uuid.UUID, articleIds []uint, action string, message *string, approver *users.Users) (*BulkActionResult, error) { result := &BulkActionResult{ ProcessedCount: len(articleIds), Results: make([]BulkActionItemResult, 0, len(articleIds)), } for _, articleId := range articleIds { itemResult := BulkActionItemResult{ ArticleId: articleId, } err := s.ProcessApprovalStep(clientId, articleId, action, message, approver) if err != nil { itemResult.Status = "failed" errorMsg := err.Error() itemResult.Error = &errorMsg result.FailedCount++ } else { itemResult.Status = "success" // Get next step info approvalFlow, err := s.Repo.FindActiveByArticleId(articleId) if err == nil { if approvalFlow.StatusId == 2 { itemResult.NextStep = "published" } else { itemResult.NextStep = fmt.Sprintf("%d", approvalFlow.CurrentStep) } } result.SuccessfulCount++ } result.Results = append(result.Results, itemResult) } return result, nil } func (s *articleApprovalFlowService) transformToDetailedFlow(flow *entity.ArticleApprovalFlows, includePreview bool) (*DetailedApprovalFlow, error) { // This method would transform the basic approval flow to detailed view // Implementation would include: // 1. Fetch article details with author info // 2. Calculate approval context (deadlines, role description) // 3. Build workflow progress information // 4. Add content preview if requested detailedFlow := &DetailedApprovalFlow{ ID: flow.ID, // Article details would be populated from database joins // ApprovalContext would be calculated based on workflow step // WorkflowProgress would show current position in workflow } return detailedFlow, nil } ``` ## 3. Controller Implementation ### Article Approval Controller ```go // app/module/articles/controller/articles_approval.controller.go package controller import ( "github.com/gofiber/fiber/v2" "github.com/rs/zerolog" "strconv" "netidhub-saas-be/app/middleware" "netidhub-saas-be/app/module/article_approval_flows/service" "netidhub-saas-be/app/module/articles/request" usersRepository "netidhub-saas-be/app/module/users/repository" utilRes "netidhub-saas-be/utils/response" utilSvc "netidhub-saas-be/utils/service" utilVal "netidhub-saas-be/utils/validator" ) type articleApprovalController struct { approvalFlowService service.ArticleApprovalFlowService usersRepo usersRepository.UsersRepository log zerolog.Logger } type ArticleApprovalController interface { SubmitForApproval(c *fiber.Ctx) error ApproveStep(c *fiber.Ctx) error RejectArticle(c *fiber.Ctx) error RequestRevision(c *fiber.Ctx) error GetPendingApprovals(c *fiber.Ctx) error GetApprovalHistory(c *fiber.Ctx) error } func NewArticleApprovalController( approvalFlowService service.ArticleApprovalFlowService, usersRepo usersRepository.UsersRepository, log zerolog.Logger, ) ArticleApprovalController { return &articleApprovalController{ approvalFlowService: approvalFlowService, usersRepo: usersRepo, log: log, } } // SubmitForApproval submit article for approval // @Summary Submit article for approval // @Description API for submitting article for approval workflow // @Tags Articles // @Security Bearer // @Param id path int true "Article ID" // @Param payload body request.SubmitApprovalRequest true "Required payload" // @Success 200 {object} response.Response // @Failure 400 {object} response.BadRequestError // @Failure 401 {object} response.UnauthorizedError // @Failure 500 {object} response.InternalServerError // @Router /articles/{id}/submit-approval [post] func (ctrl *articleApprovalController) SubmitForApproval(c *fiber.Ctx) error { clientId := middleware.GetClientId(c) id, err := strconv.ParseUint(c.Params("id"), 10, 0) if err != nil { return err } req := new(request.SubmitApprovalRequest) if err := utilVal.ParseAndValidate(c, req); err != nil { return err } approvalFlow, err := ctrl.approvalFlowService.InitiateApprovalFlow(clientId, uint(id), req.WorkflowId) if err != nil { return err } return utilRes.Resp(c, utilRes.Response{ Success: true, Messages: utilRes.Messages{"Article submitted for approval successfully"}, Data: approvalFlow, }) } // ApproveStep approve current approval step // @Summary Approve article step // @Description API for approving current approval step // @Tags Articles // @Security Bearer // @Param id path int true "Article ID" // @Param payload body request.ApprovalActionRequest true "Required payload" // @Success 200 {object} response.Response // @Failure 400 {object} response.BadRequestError // @Failure 401 {object} response.UnauthorizedError // @Failure 500 {object} response.InternalServerError // @Router /articles/{id}/approve [post] func (ctrl *articleApprovalController) ApproveStep(c *fiber.Ctx) error { clientId := middleware.GetClientId(c) authToken := c.Get("Authorization") id, err := strconv.ParseUint(c.Params("id"), 10, 0) if err != nil { return err } req := new(request.ApprovalActionRequest) if err := utilVal.ParseAndValidate(c, req); err != nil { return err } // Get current user info approver := utilSvc.GetUserInfo(ctrl.log, ctrl.usersRepo, authToken) err = ctrl.approvalFlowService.ProcessApprovalStep(clientId, uint(id), "approved", req.Message, approver) if err != nil { return err } return utilRes.Resp(c, utilRes.Response{ Success: true, Messages: utilRes.Messages{"Article approved successfully"}, }) } // RequestRevision request revision for article // @Summary Request article revision // @Description API for requesting article revision // @Tags Articles // @Security Bearer // @Param id path int true "Article ID" // @Param payload body request.ApprovalActionRequest true "Required payload" // @Success 200 {object} response.Response // @Failure 400 {object} response.BadRequestError // @Failure 401 {object} response.UnauthorizedError // @Failure 500 {object} response.InternalServerError // @Router /articles/{id}/request-revision [post] func (ctrl *articleApprovalController) RequestRevision(c *fiber.Ctx) error { clientId := middleware.GetClientId(c) authToken := c.Get("Authorization") id, err := strconv.ParseUint(c.Params("id"), 10, 0) if err != nil { return err } req := new(request.ApprovalActionRequest) if err := utilVal.ParseAndValidate(c, req); err != nil { return err } // Get current user info approver := utilSvc.GetUserInfo(ctrl.log, ctrl.usersRepo, authToken) err = ctrl.approvalFlowService.ProcessApprovalStep(clientId, uint(id), "revision_requested", req.Message, approver) if err != nil { return err } return utilRes.Resp(c, utilRes.Response{ Success: true, Messages: utilRes.Messages{"Revision requested successfully"}, }) } ``` ## 4. Request/Response Models ### Request Models ```go // app/module/articles/request/approval.request.go package request type SubmitApprovalRequest struct { WorkflowId uint `json:"workflowId" validate:"required"` } type ApprovalActionRequest struct { Message *string `json:"message"` } // app/module/approval_workflows/request/approval_workflows.request.go package request import "netidhub-saas-be/app/database/entity" type ApprovalWorkflowCreateRequest struct { Name string `json:"name" validate:"required"` Description *string `json:"description"` Steps []ApprovalWorkflowStepRequest `json:"steps" validate:"required,min=1"` } type ApprovalWorkflowStepRequest struct { StepOrder int `json:"stepOrder" validate:"required,min=1"` UserLevelId uint `json:"userLevelId" validate:"required"` IsRequired bool `json:"isRequired"` CanSkip bool `json:"canSkip"` } func (req ApprovalWorkflowCreateRequest) ToEntity() *entity.ApprovalWorkflows { workflow := &entity.ApprovalWorkflows{ Name: req.Name, Description: req.Description, IsActive: &[]bool{true}[0], } for _, stepReq := range req.Steps { step := entity.ApprovalWorkflowSteps{ StepOrder: stepReq.StepOrder, UserLevelId: stepReq.UserLevelId, IsRequired: &stepReq.IsRequired, CanSkip: &stepReq.CanSkip, } workflow.Steps = append(workflow.Steps, step) } return workflow } ``` ## 5. Migration Scripts ### Database Migration ```sql -- migrations/001_create_approval_system_tables.sql -- Create approval_workflows table CREATE TABLE approval_workflows ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, description TEXT, is_active BOOLEAN DEFAULT true, client_id UUID, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); -- Create approval_workflow_steps table CREATE TABLE approval_workflow_steps ( id SERIAL PRIMARY KEY, workflow_id INT NOT NULL REFERENCES approval_workflows(id) ON DELETE CASCADE, step_order INT NOT NULL, user_level_id INT NOT NULL REFERENCES user_levels(id), is_required BOOLEAN DEFAULT true, can_skip BOOLEAN DEFAULT false, client_id UUID, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), UNIQUE(workflow_id, step_order) ); -- Create article_approval_flows table CREATE TABLE article_approval_flows ( id SERIAL PRIMARY KEY, article_id INT NOT NULL REFERENCES articles(id) ON DELETE CASCADE, workflow_id INT NOT NULL REFERENCES approval_workflows(id), current_step INT DEFAULT 1, status_id INT DEFAULT 1, -- 1=In Progress, 2=Completed, 3=Rejected, 4=Revision Requested client_id UUID, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); -- Create article_approval_step_logs table CREATE TABLE article_approval_step_logs ( id SERIAL PRIMARY KEY, article_flow_id INT NOT NULL REFERENCES article_approval_flows(id) ON DELETE CASCADE, step_order INT NOT NULL, user_level_id INT NOT NULL REFERENCES user_levels(id), approved_by INT NOT NULL REFERENCES users(id), action VARCHAR(50) NOT NULL CHECK (action IN ('approved', 'rejected', 'revision_requested')), message TEXT, approved_at TIMESTAMP, client_id UUID, created_at TIMESTAMP DEFAULT NOW() ); -- Add indexes for performance CREATE INDEX idx_approval_workflows_client_id ON approval_workflows(client_id); CREATE INDEX idx_approval_workflows_is_active ON approval_workflows(is_active); CREATE INDEX idx_approval_workflow_steps_workflow_id ON approval_workflow_steps(workflow_id); CREATE INDEX idx_article_approval_flows_article_id ON article_approval_flows(article_id); CREATE INDEX idx_article_approval_flows_status_id ON article_approval_flows(status_id); CREATE INDEX idx_article_approval_step_logs_article_flow_id ON article_approval_step_logs(article_flow_id); CREATE INDEX idx_article_approval_step_logs_approved_by ON article_approval_step_logs(approved_by); -- Modify articles table ALTER TABLE articles ADD COLUMN workflow_id INT REFERENCES approval_workflows(id); ALTER TABLE articles ADD COLUMN current_approval_step INT DEFAULT 0; -- Create default workflow for backward compatibility INSERT INTO approval_workflows (name, description, is_active) VALUES ('Legacy 3-Level Approval', 'Default 3-level approval system for backward compatibility', true); -- Get the workflow ID (assuming it's 1 for the first insert) INSERT INTO approval_workflow_steps (workflow_id, step_order, user_level_id, is_required, can_skip) VALUES (1, 1, 3, true, false), -- Level 3 approver (first step) (1, 2, 2, true, false), -- Level 2 approver (second step) (1, 3, 1, true, false); -- Level 1 approver (final step) -- Update existing articles to use default workflow UPDATE articles SET workflow_id = 1 WHERE workflow_id IS NULL; ``` ## 6. Usage Examples ### Creating a Custom Workflow ```bash # Create a 2-level approval workflow curl -X POST http://localhost:8800/api/approval-workflows \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "name": "Simple 2-Level Approval", "description": "Quick approval for simple articles", "steps": [ { "stepOrder": 1, "userLevelId": 2, "isRequired": true, "canSkip": false }, { "stepOrder": 2, "userLevelId": 1, "isRequired": true, "canSkip": false } ] }' ``` ### Submitting Article for Approval ```bash # Submit article ID 123 for approval using workflow ID 2 curl -X POST http://localhost:8800/api/articles/123/submit-approval \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "workflowId": 2 }' ``` ### Approving an Article ```bash # Approve article ID 123 curl -X POST http://localhost:8800/api/articles/123/approve \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "message": "Article looks good, approved for next level" }' ``` ### Requesting Revision ```bash # Request revision for article ID 123 curl -X POST http://localhost:8800/api/articles/123/request-revision \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "message": "Please fix the grammar issues in paragraph 2" }' ``` Dengan implementasi ini, sistem approval artikel menjadi sangat fleksibel dan dapat disesuaikan dengan kebutuhan organisasi yang berbeda-beda. Workflow dapat dibuat, dimodifikasi, dan dinonaktifkan tanpa mengubah kode aplikasi.