package service import ( "encoding/json" "errors" "fmt" "netidhub-saas-be/app/database/entity" approvalWorkflowStepsRepo "netidhub-saas-be/app/module/approval_workflow_steps/repository" approvalWorkflowsRepo "netidhub-saas-be/app/module/approval_workflows/repository" "netidhub-saas-be/app/module/article_approval_flows/repository" "netidhub-saas-be/app/module/article_approval_flows/request" approvalStepLogsRepo "netidhub-saas-be/app/module/article_approval_step_logs/repository" articlesRepo "netidhub-saas-be/app/module/articles/repository" usersRepo "netidhub-saas-be/app/module/users/repository" "netidhub-saas-be/utils/paginator" utilSvc "netidhub-saas-be/utils/service" "time" "github.com/google/uuid" "github.com/rs/zerolog" ) type articleApprovalFlowsService struct { ArticleApprovalFlowsRepository repository.ArticleApprovalFlowsRepository ApprovalWorkflowsRepository approvalWorkflowsRepo.ApprovalWorkflowsRepository ApprovalWorkflowStepsRepository approvalWorkflowStepsRepo.ApprovalWorkflowStepsRepository ArticleApprovalStepLogsRepository approvalStepLogsRepo.ArticleApprovalStepLogsRepository ArticlesRepository articlesRepo.ArticlesRepository UsersRepository usersRepo.UsersRepository Log zerolog.Logger } // ArticleApprovalFlowsService define interface of IArticleApprovalFlowsService type ArticleApprovalFlowsService interface { // Basic CRUD GetAll(authToken string, req request.ArticleApprovalFlowsQueryRequest) (flows []*entity.ArticleApprovalFlows, paging paginator.Pagination, err error) FindOne(authToken string, id uint) (flow *entity.ArticleApprovalFlows, err error) Create(authToken string, flow *entity.ArticleApprovalFlows) (flowReturn *entity.ArticleApprovalFlows, err error) Update(id uint, flow *entity.ArticleApprovalFlows) (err error) Delete(authToken string, id uint) (err error) // Article submission and approval workflow SubmitArticleForApproval(authToken string, articleId uint, submittedById uint, workflowId *uint) (flow *entity.ArticleApprovalFlows, err error) ApproveStep(authToken string, flowId uint, approvedById uint, message string) (err error) ResubmitAfterRevision(authToken string, flowId uint, resubmittedById uint) (err error) // Dashboard and queue methods GetPendingApprovals(authToken string, userLevelId uint, page, limit int, filters map[string]interface{}) (flows []*entity.ArticleApprovalFlows, paging paginator.Pagination, err error) GetMyApprovalQueue(authToken string, userLevelId uint, page, limit int, includePreview bool, urgentOnly bool) (flows []*entity.ArticleApprovalFlows, paging paginator.Pagination, err error) GetApprovalHistory(authToken string, articleId uint, page, limit int) (logs []*entity.ArticleApprovalStepLogs, paging paginator.Pagination, err error) // Statistics and analytics GetPendingCountByLevel(authToken string, userLevelId uint) (count int64, err error) GetOverdueCountByLevel(authToken string, userLevelId uint) (count int64, err error) GetApprovalStatistics(authToken string, userLevelId uint, startDate, endDate time.Time) (stats map[string]interface{}, err error) GetWorkloadAnalytics(authToken string, userLevelId uint) (analytics map[string]interface{}, err error) // Workflow management CanUserApproveStep(authToken string, flowId uint, userId uint, userLevelId uint) (canApprove bool, reason string, err error) GetCurrentStepInfo(authToken string, flowId uint) (stepInfo map[string]interface{}, err error) GetNextStepPreview(authToken string, flowId uint) (nextStep *entity.ApprovalWorkflowSteps, err error) // Multi-branch support methods ProcessMultiBranchApproval(authToken string, flowId uint, approvedById uint, message string) (err error) ProcessApprovalAction(authToken string, flowId uint, action string, actionById 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) FindActiveByArticleId(articleId uint) (flow *entity.ArticleApprovalFlows, err error) } func NewArticleApprovalFlowsService( articleApprovalFlowsRepository repository.ArticleApprovalFlowsRepository, approvalWorkflowsRepository approvalWorkflowsRepo.ApprovalWorkflowsRepository, approvalWorkflowStepsRepository approvalWorkflowStepsRepo.ApprovalWorkflowStepsRepository, articleApprovalStepLogsRepository approvalStepLogsRepo.ArticleApprovalStepLogsRepository, articlesRepository articlesRepo.ArticlesRepository, usersRepository usersRepo.UsersRepository, log zerolog.Logger, ) ArticleApprovalFlowsService { return &articleApprovalFlowsService{ ArticleApprovalFlowsRepository: articleApprovalFlowsRepository, ApprovalWorkflowsRepository: approvalWorkflowsRepository, ApprovalWorkflowStepsRepository: approvalWorkflowStepsRepository, ArticleApprovalStepLogsRepository: articleApprovalStepLogsRepository, ArticlesRepository: articlesRepository, UsersRepository: usersRepository, Log: log, } } // Basic CRUD implementations func (_i *articleApprovalFlowsService) GetAll(authToken string, req request.ArticleApprovalFlowsQueryRequest) (flows []*entity.ArticleApprovalFlows, paging paginator.Pagination, 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 nil, paginator.Pagination{}, errors.New("clientId not found in auth token") } return _i.ArticleApprovalFlowsRepository.GetAll(clientId, req) } func (_i *articleApprovalFlowsService) FindOne(authToken string, id uint) (flow *entity.ArticleApprovalFlows, 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 nil, errors.New("clientId not found in auth token") } return _i.ArticleApprovalFlowsRepository.FindOne(clientId, id) } func (_i *articleApprovalFlowsService) Create(authToken string, flow *entity.ArticleApprovalFlows) (flowReturn *entity.ArticleApprovalFlows, 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 nil, errors.New("clientId not found in auth token") } return _i.ArticleApprovalFlowsRepository.Create(clientId, flow) } func (_i *articleApprovalFlowsService) Update(id uint, flow *entity.ArticleApprovalFlows) (err error) { return _i.ArticleApprovalFlowsRepository.Update(id, flow) } func (_i *articleApprovalFlowsService) Delete(authToken string, id uint) (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") } return _i.ArticleApprovalFlowsRepository.Delete(clientId, id) } // Article submission and approval workflow func (_i *articleApprovalFlowsService) SubmitArticleForApproval(authToken string, articleId uint, submittedById uint, workflowId *uint) (flow *entity.ArticleApprovalFlows, 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 nil, errors.New("clientId not found in auth token") } // Check if article already has an active approval flow existingFlow, err := _i.ArticleApprovalFlowsRepository.FindActiveByArticleId(articleId) if err == nil && existingFlow != nil { return nil, errors.New("article already has an active approval flow") } // Get workflow (use default if not specified) var workflow *entity.ApprovalWorkflows if workflowId != nil { workflow, err = _i.ApprovalWorkflowsRepository.FindOne(clientId, *workflowId) } else { workflow, err = _i.ApprovalWorkflowsRepository.FindDefault(clientId) } if err != nil { return nil, err } if workflow == nil { return nil, errors.New("no workflow found") } if workflow.IsActive != nil && !*workflow.IsActive { return nil, errors.New("workflow is not active") } // Get first step of workflow firstStep, err := _i.ApprovalWorkflowStepsRepository.FindByWorkflowAndStep(clientId, workflow.ID, 1) if err != nil { return nil, err } if firstStep == nil { return nil, errors.New("workflow has no steps") } // Create approval flow flow = &entity.ArticleApprovalFlows{ ArticleId: articleId, WorkflowId: workflow.ID, CurrentStep: 1, StatusId: 1, // pending SubmittedById: submittedById, SubmittedAt: time.Now(), } flow, err = _i.ArticleApprovalFlowsRepository.Create(clientId, flow) if err != nil { return nil, err } // Get current article data first currentArticle, err := _i.ArticlesRepository.FindOne(clientId, articleId) if err != nil { return nil, err } // Update only the necessary fields currentArticle.WorkflowId = &workflow.ID currentArticle.CurrentApprovalStep = &flow.CurrentStep currentArticle.StatusId = &[]int{1}[0] // pending approval err = _i.ArticlesRepository.UpdateSkipNull(clientId, articleId, currentArticle) if err != nil { return nil, err } // Process auto-skip logic based on user level err = _i.processAutoSkipSteps(authToken, flow, submittedById) if err != nil { return nil, err } return flow, nil } // processAutoSkipSteps handles automatic step skipping based on user level func (_i *articleApprovalFlowsService) processAutoSkipSteps(authToken string, flow *entity.ArticleApprovalFlows, submittedById uint) 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 user level of the submitter userLevelId, err := _i.getUserLevelId(authToken, submittedById) if err != nil { return err } // Get all workflow steps steps, err := _i.ApprovalWorkflowStepsRepository.GetByWorkflowId(clientId, flow.WorkflowId) if err != nil { return err } // Sort steps by step order sortStepsByOrder(steps) // Process each step to determine if it should be auto-skipped for _, step := range steps { shouldSkip := _i.shouldSkipStep(userLevelId, step.RequiredUserLevelId) if shouldSkip { // Create skip log stepLog := &entity.ArticleApprovalStepLogs{ ApprovalFlowId: flow.ID, StepOrder: step.StepOrder, StepName: step.StepName, ApprovedById: &submittedById, Action: "auto_skip", Message: &[]string{"Step auto-skipped due to user level"}[0], ProcessedAt: time.Now(), UserLevelId: step.RequiredUserLevelId, } _, err = _i.ArticleApprovalStepLogsRepository.Create(clientId, stepLog) if err != nil { return err } // Update flow to next step (handle step order starting from 0) nextStepOrder := step.StepOrder + 1 flow.CurrentStep = nextStepOrder } else { // Stop at first step that cannot be skipped break } } // Update flow with final current step err = _i.ArticleApprovalFlowsRepository.Update(flow.ID, flow) if err != nil { return err } // Get current article data first currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId) if err != nil { return err } // Update only the necessary fields currentArticle.CurrentApprovalStep = &flow.CurrentStep err = _i.ArticlesRepository.UpdateSkipNull(clientId, flow.ArticleId, currentArticle) if err != nil { return err } // Check if all steps were skipped (workflow complete) // Find the highest step order maxStepOrder := 0 for _, step := range steps { if step.StepOrder > maxStepOrder { maxStepOrder = step.StepOrder } } if flow.CurrentStep > maxStepOrder { // All steps completed, mark as approved flow.StatusId = 2 // approved flow.CurrentStep = 0 // Set to 0 to indicate completion flow.CompletedAt = &[]time.Time{time.Now()}[0] err = _i.ArticleApprovalFlowsRepository.Update(flow.ID, flow) if err != nil { return err } // Get current article data first currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId) if err != nil { return err } // Update only the necessary fields currentArticle.StatusId = &[]int{2}[0] // approved currentArticle.CurrentApprovalStep = &[]int{0}[0] // Set to 0 to indicate completion 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 func (_i *articleApprovalFlowsService) getUserLevelId(authToken string, userId uint) (uint, 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 0, errors.New("clientId not found in auth token") } // Get user from database to retrieve user level user, err := _i.UsersRepository.FindOne(clientId, userId) if err != nil { _i.Log.Error().Err(err).Uint("userId", userId).Msg("Failed to find user") return 0, err } if user.UserLevel == nil { _i.Log.Error().Uint("userId", userId).Msg("User has no user level") return 0, errors.New("user has no user level") } _i.Log.Info(). Uint("userId", userId). Uint("userLevelId", user.UserLevel.ID). Str("userLevelName", user.UserLevel.Name). Msg("Retrieved user level from database") return user.UserLevel.ID, nil } // shouldSkipStep determines if a step should be auto-skipped based on user level func (_i *articleApprovalFlowsService) shouldSkipStep(userLevelId, requiredLevelId uint) bool { // Get user level details to compare level numbers // User level with lower level_number (higher authority) can skip steps requiring higher level_number // For now, we'll use a simple comparison based on IDs // In production, this should compare level_number fields // Simple logic: if user level ID is less than required level ID, they can skip // This assumes level 1 (ID=1) has higher authority than level 2 (ID=2), etc. return userLevelId < requiredLevelId } // sortStepsByOrder sorts workflow steps by their step order func sortStepsByOrder(steps []*entity.ApprovalWorkflowSteps) { // Simple bubble sort for step order n := len(steps) for i := 0; i < n-1; i++ { for j := 0; j < n-i-1; j++ { if steps[j].StepOrder > steps[j+1].StepOrder { steps[j], steps[j+1] = steps[j+1], steps[j] } } } } func (_i *articleApprovalFlowsService) ApproveStep(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 approval 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 } // Check if there's a next step nextStep, err := _i.ApprovalWorkflowStepsRepository.GetNextStep(clientId, flow.WorkflowId, flow.CurrentStep) if err != nil && err.Error() != "record not found" { return err } if nextStep == nil || nextStep.ID == 0 { // No next step - approval complete flowUpdate := &entity.ArticleApprovalFlows{ StatusId: 2, // approved CurrentStep: 0, // Set to 0 to indicate completion CompletedAt: &[]time.Time{time.Now()}[0], } // Debug logging _i.Log.Info(). Interface("flowUpdate :: ", flowUpdate). Msg("Retrieved next step from database") err = _i.ArticleApprovalFlowsRepository.Update(flowId, flowUpdate) if err != nil { return err } // Get current article data first currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId) if err != nil { return err } // Update only the necessary fields 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 } } else { // Move to next step flowUpdate := &entity.ArticleApprovalFlows{ CurrentStep: nextStep.StepOrder, StatusId: 1, // pending } err = _i.ArticleApprovalFlowsRepository.Update(flowId, flowUpdate) if err != nil { return err } // Get current article data first currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId) if err != nil { return err } // Update only the necessary fields currentArticle.CurrentApprovalStep = &nextStep.StepOrder err = _i.ArticlesRepository.UpdateSkipNull(clientId, flow.ArticleId, currentArticle) if err != nil { return err } } return nil } func (_i *articleApprovalFlowsService) ResubmitAfterRevision(authToken string, flowId uint, resubmittedById uint) (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 approval 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 != 4 { // not revision_requested return errors.New("approval flow is not in revision requested state") } // Reset approval flow to pending flowUpdate := &entity.ArticleApprovalFlows{ StatusId: 1, // pending RevisionRequested: &[]bool{false}[0], RevisionMessage: nil, CurrentStep: 1, // restart from first step } err = _i.ArticleApprovalFlowsRepository.Update(flowId, flowUpdate) if err != nil { return err } // Get current article data first currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId) if err != nil { return err } // Update only the necessary fields currentArticle.StatusId = &[]int{1}[0] // pending approval currentArticle.CurrentApprovalStep = &[]int{1}[0] err = _i.ArticlesRepository.UpdateSkipNull(clientId, flow.ArticleId, currentArticle) if err != nil { return err } // Create resubmission log stepLog := &entity.ArticleApprovalStepLogs{ ApprovalFlowId: flow.ID, StepOrder: 1, StepName: "Resubmission", ApprovedById: &resubmittedById, Action: "resubmit", Message: &[]string{"Article resubmitted after revision"}[0], ProcessedAt: time.Now(), } _, err = _i.ArticleApprovalStepLogsRepository.Create(clientId, stepLog) if err != nil { return err } return nil } // Dashboard and queue methods func (_i *articleApprovalFlowsService) GetPendingApprovals(authToken string, userLevelId uint, page, limit int, filters map[string]interface{}) (flows []*entity.ArticleApprovalFlows, paging paginator.Pagination, 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 nil, paginator.Pagination{}, errors.New("clientId not found in auth token") } return _i.ArticleApprovalFlowsRepository.GetPendingApprovals(clientId, userLevelId, page, limit, filters) } func (_i *articleApprovalFlowsService) GetMyApprovalQueue(authToken string, userLevelId uint, page, limit int, includePreview bool, urgentOnly bool) (flows []*entity.ArticleApprovalFlows, paging paginator.Pagination, 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 nil, paginator.Pagination{}, errors.New("clientId not found in auth token") } return _i.ArticleApprovalFlowsRepository.GetMyApprovalQueue(clientId, userLevelId, page, limit, includePreview, urgentOnly) } func (_i *articleApprovalFlowsService) GetApprovalHistory(authToken string, articleId uint, page, limit int) (logs []*entity.ArticleApprovalStepLogs, paging paginator.Pagination, 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 nil, paginator.Pagination{}, errors.New("clientId not found in auth token") } return _i.ArticleApprovalStepLogsRepository.GetApprovalHistory(clientId, articleId, page, limit) } // Statistics and analytics func (_i *articleApprovalFlowsService) GetPendingCountByLevel(authToken string, userLevelId uint) (count int64, 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 0, errors.New("clientId not found in auth token") } return _i.ArticleApprovalFlowsRepository.GetPendingCountByLevel(clientId, userLevelId) } func (_i *articleApprovalFlowsService) GetOverdueCountByLevel(authToken string, userLevelId uint) (count int64, 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 0, errors.New("clientId not found in auth token") } return _i.ArticleApprovalFlowsRepository.GetOverdueCountByLevel(clientId, userLevelId) } func (_i *articleApprovalFlowsService) GetApprovalStatistics(authToken string, userLevelId uint, startDate, endDate time.Time) (stats map[string]interface{}, 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 nil, errors.New("clientId not found in auth token") } stats = make(map[string]interface{}) // Get approved count approvedCount, err := _i.ArticleApprovalFlowsRepository.GetApprovedCountByPeriod(clientId, userLevelId, startDate, endDate) if err != nil { return nil, err } // Get rejected count rejectedCount, err := _i.ArticleApprovalFlowsRepository.GetRejectedCountByPeriod(clientId, userLevelId, startDate, endDate) if err != nil { return nil, err } // Get revision request count revisionCount, err := _i.ArticleApprovalFlowsRepository.GetRevisionRequestCountByPeriod(clientId, userLevelId, startDate, endDate) if err != nil { return nil, err } stats["approved_count"] = approvedCount stats["rejected_count"] = rejectedCount stats["revision_requested_count"] = revisionCount stats["total_processed"] = approvedCount + rejectedCount + revisionCount stats["period_start"] = startDate stats["period_end"] = endDate return stats, nil } func (_i *articleApprovalFlowsService) GetWorkloadAnalytics(authToken string, userLevelId uint) (analytics map[string]interface{}, 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 nil, errors.New("clientId not found in auth token") } return _i.ArticleApprovalFlowsRepository.GetLevelWorkload(clientId, userLevelId) } // Workflow management func (_i *articleApprovalFlowsService) CanUserApproveStep(authToken string, flowId uint, userId uint, userLevelId uint) (canApprove bool, reason 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 false, "clientId not found in auth token", errors.New("clientId not found in auth token") } // Get approval flow flow, err := _i.ArticleApprovalFlowsRepository.FindOne(clientId, flowId) if err != nil { return false, "", err } if flow == nil { return false, "approval flow not found", nil } if flow.StatusId != 1 && flow.StatusId != 4 { // not pending or revision_requested return false, "approval flow is not in pending state", nil } // Get current step currentStep, err := _i.ApprovalWorkflowStepsRepository.FindByWorkflowAndStep(clientId, flow.WorkflowId, flow.CurrentStep) if err != nil { return false, "", err } if currentStep == nil { return false, "current step not found", nil } // Check if user level matches required level if currentStep.RequiredUserLevelId != userLevelId { return false, "user level does not match required level for this step", nil } // Check if user submitted the article (cannot approve own submission) if flow.SubmittedById == userId { return false, "cannot approve own submission", nil } return true, "", nil } func (_i *articleApprovalFlowsService) GetCurrentStepInfo(authToken string, flowId uint) (stepInfo map[string]interface{}, 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 nil, errors.New("clientId not found in auth token") } stepInfo = make(map[string]interface{}) // Get approval flow flow, err := _i.ArticleApprovalFlowsRepository.FindOne(clientId, flowId) if err != nil { return nil, err } if flow == nil { return nil, errors.New("approval flow not found") } // Get current step currentStep, err := _i.ApprovalWorkflowStepsRepository.FindByWorkflowAndStep(clientId, flow.WorkflowId, flow.CurrentStep) if err != nil { return nil, err } stepInfo["current_step"] = flow.CurrentStep stepInfo["step_name"] = currentStep.StepName stepInfo["required_user_level_id"] = currentStep.RequiredUserLevelId stepInfo["can_skip"] = currentStep.CanSkip stepInfo["auto_approve_after_hours"] = currentStep.AutoApproveAfterHours stepInfo["status"] = flow.StatusId return stepInfo, nil } func (_i *articleApprovalFlowsService) GetNextStepPreview(authToken string, flowId uint) (nextStep *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 _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } if clientId == nil { return nil, errors.New("clientId not found in auth token") } // Get approval flow flow, err := _i.ArticleApprovalFlowsRepository.FindOne(clientId, flowId) if err != nil { return nil, err } if flow == nil { return nil, errors.New("approval flow not found") } // Get next step nextStep, err = _i.ApprovalWorkflowStepsRepository.GetNextStep(clientId, flow.WorkflowId, flow.CurrentStep) if err != nil { return nil, err } 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) } func (_i *articleApprovalFlowsService) FindActiveByArticleId(articleId uint) (flow *entity.ArticleApprovalFlows, err error) { return _i.ArticleApprovalFlowsRepository.FindActiveByArticleId(articleId) } // ProcessApprovalAction processes different approval actions (approve, request_update, reject) func (_i *articleApprovalFlowsService) ProcessApprovalAction(authToken string, flowId uint, action string, actionById uint, message string) (err error) { _i.Log.Info(). Uint("flowId", flowId). Str("action", action). Uint("actionById", actionById). Str("message", message). Msg("Processing approval action") // 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 } } // Get the approval flow flow, err := _i.ArticleApprovalFlowsRepository.FindOne(clientId, flowId) if err != nil { return fmt.Errorf("failed to find approval flow: %w", err) } // Get the article article, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId) if err != nil { return fmt.Errorf("failed to find article: %w", err) } switch action { case "approve": return _i.processApproveAction(authToken, flow, article, actionById, message) case "revision": return _i.processRequestUpdateAction(authToken, flow, article, actionById, message) case "reject": return _i.processRejectAction(authToken, flow, article, actionById, message) default: return fmt.Errorf("invalid action: %s", action) } } // processApproveAction handles approve action - moves to next step func (_i *articleApprovalFlowsService) processApproveAction(authToken string, flow *entity.ArticleApprovalFlows, article *entity.Articles, actionById uint, message string) error { _i.Log.Info().Uint("flowId", flow.ID).Msg("Processing approve action") // Use existing multi-branch approval logic return _i.ProcessMultiBranchApproval(authToken, flow.ID, actionById, message) } // processRequestUpdateAction handles request update action - moves back 1 step func (_i *articleApprovalFlowsService) processRequestUpdateAction(authToken string, flow *entity.ArticleApprovalFlows, article *entity.Articles, actionById uint, message string) error { _i.Log.Info().Uint("flowId", flow.ID).Msg("Processing request update action") var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepository, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId } } // Reset to initial step (step 1) and status pending flow.CurrentStep = 1 flow.StatusId = 1 // pending - seperti belum berjalan flow approval flow.RevisionRequested = &[]bool{true}[0] flow.RevisionMessage = &message err := _i.ArticleApprovalFlowsRepository.Update(flow.ID, flow) if err != nil { return fmt.Errorf("failed to update flow for request update: %w", err) } // Update article status to draft for revision article.StatusId = &[]int{1}[0] // 1 = draft article.IsDraft = &[]bool{true}[0] article.WorkflowId = &flow.WorkflowId // Keep workflow ID for restart article.CurrentApprovalStep = &[]int{1}[0] // Reset to step 1 err = _i.ArticlesRepository.Update(clientId, article.ID, article) if err != nil { return fmt.Errorf("failed to update article for revision: %w", err) } // Create step log stepLog := &entity.ArticleApprovalStepLogs{ ApprovalFlowId: flow.ID, StepOrder: flow.CurrentStep, Action: "revision", ApprovedById: &actionById, Message: &message, CreatedAt: time.Now(), } _, err = _i.ArticleApprovalStepLogsRepository.Create(clientId, stepLog) if err != nil { _i.Log.Error().Err(err).Msg("Failed to create step log for request update") } _i.Log.Info(). Uint("flowId", flow.ID). Int("resetStep", 1). Msg("Revision request processed - flow reset to step 1") return nil } // processRejectAction handles reject action - returns to draft status func (_i *articleApprovalFlowsService) processRejectAction(authToken string, flow *entity.ArticleApprovalFlows, article *entity.Articles, actionById uint, message string) error { _i.Log.Info().Uint("flowId", flow.ID).Msg("Processing reject action") var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepository, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId } } // Reset to initial step (step 1) and status pending flow.CurrentStep = 1 flow.StatusId = 1 // pending - seperti belum berjalan flow approval flow.RevisionMessage = &message // Use same field for consistency // Don't set completedAt - flow is not completed, just reset err := _i.ArticleApprovalFlowsRepository.Update(flow.ID, flow) if err != nil { return fmt.Errorf("failed to update flow for rejection: %w", err) } // Update article status to draft article.StatusId = &[]int{1}[0] // 1 = draft article.IsDraft = &[]bool{true}[0] article.WorkflowId = &flow.WorkflowId // Keep workflow ID for restart article.CurrentApprovalStep = &[]int{1}[0] // Reset to step 1 err = _i.ArticlesRepository.Update(clientId, article.ID, article) if err != nil { return fmt.Errorf("failed to update article for rejection: %w", err) } // Create step log stepLog := &entity.ArticleApprovalStepLogs{ ApprovalFlowId: flow.ID, StepOrder: flow.CurrentStep, Action: "reject", ApprovedById: &actionById, Message: &message, CreatedAt: time.Now(), } _, err = _i.ArticleApprovalStepLogsRepository.Create(clientId, stepLog) if err != nil { _i.Log.Error().Err(err).Msg("Failed to create step log for rejection") } _i.Log.Info(). Uint("flowId", flow.ID). Uint("articleId", article.ID). Msg("Rejection processed - flow reset to step 1, article returned to draft") return nil }