package service import ( "context" "encoding/json" "errors" "fmt" "io" "log" "math/rand" "mime" "netidhub-saas-be/app/database/entity" approvalWorkflowsRepository "netidhub-saas-be/app/module/approval_workflows/repository" articleApprovalFlowsRepository "netidhub-saas-be/app/module/article_approval_flows/repository" articleApprovalFlowsService "netidhub-saas-be/app/module/article_approval_flows/service" articleApprovalsRepository "netidhub-saas-be/app/module/article_approvals/repository" articleCategoriesRepository "netidhub-saas-be/app/module/article_categories/repository" articleCategoryDetailsRepository "netidhub-saas-be/app/module/article_category_details/repository" articleCategoryDetailsReq "netidhub-saas-be/app/module/article_category_details/request" articleFilesRepository "netidhub-saas-be/app/module/article_files/repository" "netidhub-saas-be/app/module/articles/mapper" "netidhub-saas-be/app/module/articles/repository" "netidhub-saas-be/app/module/articles/request" "netidhub-saas-be/app/module/articles/response" clientsRepository "netidhub-saas-be/app/module/clients/repository" usersRepository "netidhub-saas-be/app/module/users/repository" config "netidhub-saas-be/config/config" minioStorage "netidhub-saas-be/config/config" "netidhub-saas-be/utils/paginator" utilSvc "netidhub-saas-be/utils/service" "path/filepath" "strconv" "strings" "time" "github.com/gofiber/fiber/v2" "github.com/google/uuid" "github.com/minio/minio-go/v7" "github.com/rs/zerolog" ) // ArticlesService type articlesService struct { Repo repository.ArticlesRepository ArticleCategoriesRepo articleCategoriesRepository.ArticleCategoriesRepository ArticleFilesRepo articleFilesRepository.ArticleFilesRepository ArticleApprovalsRepo articleApprovalsRepository.ArticleApprovalsRepository ArticleCategoryDetailsRepo articleCategoryDetailsRepository.ArticleCategoryDetailsRepository Log zerolog.Logger Cfg *config.Config UsersRepo usersRepository.UsersRepository ClientsRepo clientsRepository.ClientsRepository MinioStorage *minioStorage.MinioStorage // Dynamic approval system dependencies ArticleApprovalFlowsRepo articleApprovalFlowsRepository.ArticleApprovalFlowsRepository ApprovalWorkflowsRepo approvalWorkflowsRepository.ApprovalWorkflowsRepository ArticleApprovalFlowsSvc articleApprovalFlowsService.ArticleApprovalFlowsService } // ArticlesService define interface of IArticlesService type ArticlesService interface { All(authToken string, req request.ArticlesQueryRequest) (articles []*response.ArticlesResponse, paging paginator.Pagination, err error) Show(authToken string, id uint) (articles *response.ArticlesResponse, err error) ShowByOldId(authToken string, oldId uint) (articles *response.ArticlesResponse, err error) Save(authToken string, req request.ArticlesCreateRequest) (articles *entity.Articles, err error) SaveThumbnail(authToken string, c *fiber.Ctx) (err error) Update(authToken string, id uint, req request.ArticlesUpdateRequest) (err error) Delete(authToken string, id uint) error UpdateActivityCount(authToken string, id uint, activityTypeId int) (err error) UpdateApproval(authToken string, id uint, statusId int, userLevelId int, userLevelNumber int, userParentLevelId int) (err error) UpdateBanner(authToken string, id uint, isBanner bool) error Viewer(authToken string, c *fiber.Ctx) error SummaryStats(authToken string) (summaryStats *response.ArticleSummaryStats, err error) ArticlePerUserLevelStats(authToken string, startDate *string, endDate *string) (articlePerUserLevelStats []*response.ArticlePerUserLevelStats, err error) ArticleMonthlyStats(authToken string, year *int) (articleMonthlyStats []*response.ArticleMonthlyStats, err error) PublishScheduling(authToken string, id uint, publishSchedule string) error ExecuteScheduling() error // Dynamic approval system methods SubmitForApproval(authToken string, articleId uint, workflowId *uint) error GetApprovalStatus(authToken string, articleId uint) (*response.ArticleApprovalStatusResponse, error) GetArticlesWaitingForApproval(authToken string, page, limit int) ([]*response.ArticleApprovalQueueResponse, paginator.Pagination, error) GetPendingApprovals(authToken string, page, limit int, typeId *int) ([]*response.ArticleApprovalQueueResponse, paginator.Pagination, error) // Updated with typeId filter // No-approval system methods CheckApprovalRequired(authToken string, articleId uint, userId uint, userLevelId uint) (bool, error) AutoApproveArticle(authToken string, articleId uint, reason string) error GetClientApprovalSettings(authToken string) (*response.ClientApprovalSettingsResponse, error) SetArticleApprovalExempt(authToken string, articleId uint, exempt bool, reason string) error } // NewArticlesService init ArticlesService func NewArticlesService( repo repository.ArticlesRepository, articleCategoriesRepo articleCategoriesRepository.ArticleCategoriesRepository, articleCategoryDetailsRepo articleCategoryDetailsRepository.ArticleCategoryDetailsRepository, articleFilesRepo articleFilesRepository.ArticleFilesRepository, articleApprovalsRepo articleApprovalsRepository.ArticleApprovalsRepository, articleApprovalFlowsRepo articleApprovalFlowsRepository.ArticleApprovalFlowsRepository, approvalWorkflowsRepo approvalWorkflowsRepository.ApprovalWorkflowsRepository, articleApprovalFlowsSvc articleApprovalFlowsService.ArticleApprovalFlowsService, log zerolog.Logger, cfg *config.Config, usersRepo usersRepository.UsersRepository, clientsRepo clientsRepository.ClientsRepository, minioStorage *minioStorage.MinioStorage, ) ArticlesService { return &articlesService{ Repo: repo, ArticleCategoriesRepo: articleCategoriesRepo, ArticleCategoryDetailsRepo: articleCategoryDetailsRepo, ArticleFilesRepo: articleFilesRepo, ArticleApprovalsRepo: articleApprovalsRepo, ArticleApprovalFlowsRepo: articleApprovalFlowsRepo, ApprovalWorkflowsRepo: approvalWorkflowsRepo, ArticleApprovalFlowsSvc: articleApprovalFlowsSvc, Log: log, UsersRepo: usersRepo, ClientsRepo: clientsRepo, MinioStorage: minioStorage, Cfg: cfg, } } // All implement interface of ArticlesService func (_i *articlesService) All(authToken string, req request.ArticlesQueryRequest) (articless []*response.ArticlesResponse, paging paginator.Pagination, err error) { // Extract clientId, userLevelId, and userId from authToken var clientId *uuid.UUID var userLevelId *uint var userId *uint if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil { if user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } userLevelId = &user.UserLevelId userId = &user.ID _i.Log.Info().Interface("userLevelId", userLevelId).Msg("Extracted userLevelId from auth token") _i.Log.Info().Interface("userId", userId).Msg("Extracted userId from auth token") } } if req.Category != nil { findCategory, err := _i.ArticleCategoriesRepo.FindOneBySlug(clientId, *req.Category) if err != nil { return nil, paging, err } req.CategoryId = &findCategory.ID } // Handle clientSlug filter - find client by slug and set clientId if req.ClientSlug != nil { findClient, err := _i.ClientsRepo.FindBySlug(*req.ClientSlug) if err != nil { _i.Log.Error().Err(err).Str("clientSlug", *req.ClientSlug).Msg("Failed to find client by slug") return nil, paging, err } if findClient != nil { clientId = &findClient.ID _i.Log.Info().Str("clientSlug", *req.ClientSlug).Str("clientId", findClient.ID.String()).Msg("Found client by slug") } } results, paging, err := _i.Repo.GetAll(clientId, userLevelId, userId, req) if err != nil { return } _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:articlesService", "Methods:All"). Interface("results", results).Msg("") host := _i.Cfg.App.Domain for _, result := range results { articleRes := mapper.ArticlesResponseMapper(_i.Log, host, clientId, result, _i.ArticleCategoriesRepo, _i.ArticleCategoryDetailsRepo, _i.ArticleFilesRepo, _i.UsersRepo, _i.ClientsRepo) articless = append(articless, articleRes) } return } func (_i *articlesService) Show(authToken string, id uint) (articles *response.ArticlesResponse, err error) { // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } result, err := _i.Repo.FindOne(clientId, id) if err != nil { return nil, err } host := _i.Cfg.App.Domain return mapper.ArticlesResponseMapper(_i.Log, host, clientId, result, _i.ArticleCategoriesRepo, _i.ArticleCategoryDetailsRepo, _i.ArticleFilesRepo, _i.UsersRepo, _i.ClientsRepo), nil } func (_i *articlesService) ShowByOldId(authToken string, oldId uint) (articles *response.ArticlesResponse, err error) { // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } result, err := _i.Repo.FindByOldId(clientId, oldId) if err != nil { return nil, err } host := _i.Cfg.App.Domain return mapper.ArticlesResponseMapper(_i.Log, host, clientId, result, _i.ArticleCategoriesRepo, _i.ArticleCategoryDetailsRepo, _i.ArticleFilesRepo, _i.UsersRepo, _i.ClientsRepo), nil } func (_i *articlesService) Save(authToken string, req request.ArticlesCreateRequest) (articles *entity.Articles, err error) { _i.Log.Info().Interface("data", req).Msg("") newReq := req.ToEntity() // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } var userLevelNumber int var approvalLevelId int if req.CreatedById != nil { createdBy, err := _i.UsersRepo.FindOne(clientId, *req.CreatedById) if err != nil { return nil, fmt.Errorf("User not found") } newReq.CreatedById = &createdBy.ID userLevelNumber = createdBy.UserLevel.LevelNumber // Find the next higher level for approval (level_number should be smaller) approvalLevelId = _i.findNextApprovalLevel(clientId, userLevelNumber) } else { createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) newReq.CreatedById = &createdBy.ID userLevelNumber = createdBy.UserLevel.LevelNumber // Find the next higher level for approval (level_number should be smaller) approvalLevelId = _i.findNextApprovalLevel(clientId, userLevelNumber) } isDraft := true if req.IsDraft == &isDraft { draftedAt := time.Now() newReq.IsDraft = &isDraft newReq.DraftedAt = &draftedAt isPublishFalse := false newReq.IsPublish = &isPublishFalse newReq.PublishedAt = nil } isPublish := true if req.IsPublish == &isPublish { publishedAt := time.Now() newReq.IsPublish = &isPublish newReq.PublishedAt = &publishedAt isDraftFalse := false newReq.IsDraft = &isDraftFalse newReq.DraftedAt = nil } if req.CreatedAt != nil { layout := "2006-01-02 15:04:05" parsedTime, err := time.Parse(layout, *req.CreatedAt) if err != nil { return nil, fmt.Errorf("Error parsing time:", err) } newReq.CreatedAt = parsedTime } // Dynamic Approval Workflow System statusIdOne := 1 statusIdTwo := 2 isPublishFalse := false // Get user info for approval logic createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) // Check if user level requires approval if createdBy != nil && *createdBy.UserLevel.IsApprovalActive == false { _i.Log.Info(). Uint("userId", createdBy.ID). Uint("userLevelId", createdBy.UserLevel.ID). Str("userLevelName", createdBy.UserLevel.Name). Bool("isApprovalActive", *createdBy.UserLevel.IsApprovalActive). Msg("User level does not require approval - auto publishing") // User level doesn't require approval - auto publish newReq.NeedApprovalFrom = nil newReq.StatusId = &statusIdTwo newReq.IsPublish = &isPublishFalse newReq.PublishedAt = nil newReq.BypassApproval = &[]bool{true}[0] } else { _i.Log.Info(). Uint("userId", createdBy.ID). Uint("userLevelId", createdBy.UserLevel.ID). Str("userLevelName", createdBy.UserLevel.Name). Bool("isApprovalActive", func() bool { if createdBy != nil && createdBy.UserLevel.IsApprovalActive != nil { return *createdBy.UserLevel.IsApprovalActive } return false }()). Msg("User level requires approval - setting to pending") // User level requires approval - set to pending newReq.NeedApprovalFrom = &approvalLevelId newReq.StatusId = &statusIdOne newReq.IsPublish = &isPublishFalse newReq.PublishedAt = nil newReq.BypassApproval = &[]bool{false}[0] } saveArticleRes, err := _i.Repo.Create(clientId, newReq) if err != nil { return nil, err } // Dynamic Approval Workflow Assignment with Multi-Branch Support if createdBy != nil && *createdBy.UserLevel.IsApprovalActive == true { _i.Log.Info(). Uint("userId", createdBy.ID). Uint("userLevelId", createdBy.UserLevel.ID). Str("userLevelName", createdBy.UserLevel.Name). Bool("isApprovalActive", *createdBy.UserLevel.IsApprovalActive). Msg("User level requires approval - proceeding with workflow assignment") // Get default workflow for the client defaultWorkflow, err := _i.ApprovalWorkflowsRepo.GetDefault(clientId) if err != nil { _i.Log.Error().Err(err).Msg("Failed to get default workflow") } else if defaultWorkflow == nil { _i.Log.Warn().Msg("No default workflow found for client") } else { _i.Log.Info(). Uint("workflowId", defaultWorkflow.ID). Str("workflowName", defaultWorkflow.Name). Bool("isActive", *defaultWorkflow.IsActive). Bool("isDefault", *defaultWorkflow.IsDefault). Msg("Found default workflow") // Assign workflow to article saveArticleRes.WorkflowId = &defaultWorkflow.ID saveArticleRes.CurrentApprovalStep = &[]int{1}[0] // Start at step 1 // Update article with workflow info err = _i.Repo.Update(clientId, saveArticleRes.ID, saveArticleRes) if err != nil { _i.Log.Error().Err(err).Msg("Failed to update article with workflow") } else { _i.Log.Info(). Uint("articleId", saveArticleRes.ID). Uint("workflowId", defaultWorkflow.ID). Msg("Article updated with workflow successfully") } // Create approval flow with multi-branch support approvalFlow := &entity.ArticleApprovalFlows{ ArticleId: saveArticleRes.ID, WorkflowId: defaultWorkflow.ID, CurrentStep: 1, StatusId: 1, // In Progress SubmittedById: *newReq.CreatedById, SubmittedAt: time.Now(), ClientId: clientId, // Multi-branch fields will be set by the service CurrentBranch: nil, // Will be determined by first applicable step BranchPath: nil, // Will be populated as flow progresses IsParallelFlow: &[]bool{false}[0], // Default to sequential } createdFlow, err := _i.ArticleApprovalFlowsRepo.Create(clientId, approvalFlow) if err != nil { _i.Log.Error().Err(err).Msg("Failed to create approval flow") } else { _i.Log.Info(). Uint("flowId", createdFlow.ID). Uint("articleId", saveArticleRes.ID). Uint("workflowId", defaultWorkflow.ID). Msg("Approval flow created successfully") // Initialize the multi-branch flow by determining the first applicable step err = _i.initializeMultiBranchFlow(authToken, createdFlow.ID, createdBy.UserLevel.ID) if err != nil { _i.Log.Error().Err(err).Msg("Failed to initialize multi-branch flow") } else { _i.Log.Info(). Uint("flowId", createdFlow.ID). Uint("userLevelId", createdBy.UserLevel.ID). Msg("Multi-branch flow initialized successfully") } } } // Create legacy approval record for backward compatibility articleApproval := &entity.ArticleApprovals{ ArticleId: saveArticleRes.ID, ApprovalBy: *newReq.CreatedById, StatusId: statusIdOne, Message: "Need Approval", ApprovalAtLevel: &approvalLevelId, } _, err = _i.ArticleApprovalsRepo.Create(articleApproval) if err != nil { _i.Log.Error().Err(err).Msg("Failed to create legacy approval record") } } else { // Auto-publish for users who don't require approval articleApproval := &entity.ArticleApprovals{ ArticleId: saveArticleRes.ID, ApprovalBy: *newReq.CreatedById, StatusId: statusIdTwo, Message: "Publish Otomatis", ApprovalAtLevel: nil, } _, err = _i.ArticleApprovalsRepo.Create(articleApproval) if err != nil { _i.Log.Error().Err(err).Msg("Failed to create auto-approval record") } } var categoryIds []string if req.CategoryIds != "" { categoryIds = strings.Split(req.CategoryIds, ",") } _i.Log.Info().Interface("categoryIds", categoryIds).Msg("") for _, categoryId := range categoryIds { categoryIdInt, _ := strconv.Atoi(categoryId) _i.Log.Info().Interface("categoryIdUint", uint(categoryIdInt)).Msg("") findCategory, err := _i.ArticleCategoriesRepo.FindOne(clientId, uint(categoryIdInt)) _i.Log.Info().Interface("findCategory", findCategory).Msg("") if err != nil { return nil, err } if findCategory == nil { return nil, errors.New("category not found") } categoryReq := articleCategoryDetailsReq.ArticleCategoryDetailsCreateRequest{ ArticleId: saveArticleRes.ID, CategoryId: categoryIdInt, IsActive: true, } newCategoryReq := categoryReq.ToEntity() err = _i.ArticleCategoryDetailsRepo.Create(newCategoryReq) if err != nil { return nil, err } } return saveArticleRes, nil } func (_i *articlesService) SaveThumbnail(authToken string, c *fiber.Ctx) (err error) { // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } id, err := strconv.ParseUint(c.Params("id"), 10, 0) if err != nil { return err } _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:articlesService", "Methods:SaveThumbnail"). Interface("id", id).Msg("") bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName form, err := c.MultipartForm() if err != nil { return err } files := form.File["files"] // Create minio connection. minioClient, err := _i.MinioStorage.ConnectMinio() if err != nil { // Return status 500 and minio connection error. return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": true, "msg": err.Error(), }) } // Iterasi semua file yang diunggah for _, file := range files { _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "Uploader:: loop1"). Interface("data", file).Msg("") src, err := file.Open() if err != nil { return err } defer src.Close() filename := filepath.Base(file.Filename) filename = strings.ReplaceAll(filename, " ", "") filenameWithoutExt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))]) extension := filepath.Ext(file.Filename)[1:] now := time.Now() rand.New(rand.NewSource(now.UnixNano())) randUniqueId := rand.Intn(1000000) newFilenameWithoutExt := filenameWithoutExt + "_" + strconv.Itoa(randUniqueId) newFilename := newFilenameWithoutExt + "." + extension objectName := fmt.Sprintf("articles/thumbnail/%d/%d/%s", now.Year(), now.Month(), newFilename) findCategory, err := _i.Repo.FindOne(clientId, uint(id)) findCategory.ThumbnailName = &newFilename findCategory.ThumbnailPath = &objectName err = _i.Repo.Update(clientId, uint(id), findCategory) if err != nil { return err } // Upload file ke MinIO _, err = minioClient.PutObject(context.Background(), bucketName, objectName, src, file.Size, minio.PutObjectOptions{}) if err != nil { return err } } return } func (_i *articlesService) Update(authToken string, id uint, req request.ArticlesUpdateRequest) (err error) { _i.Log.Info().Interface("data", req).Msg("") newReq := req.ToEntity() // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } if req.CreatedAt != nil { layout := "2006-01-02 15:04:05" parsedTime, err := time.Parse(layout, *req.CreatedAt) if err != nil { return fmt.Errorf("Error parsing time:", err) } newReq.CreatedAt = parsedTime } return _i.Repo.UpdateSkipNull(clientId, id, newReq) } func (_i *articlesService) Delete(authToken string, id uint) error { // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } return _i.Repo.Delete(clientId, id) } func (_i *articlesService) Viewer(authToken string, c *fiber.Ctx) (err error) { // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } thumbnailName := c.Params("thumbnailName") emptyImage := "empty-image.jpg" searchThumbnail := emptyImage if thumbnailName != emptyImage { result, err := _i.Repo.FindByFilename(clientId, thumbnailName) if err != nil { return err } _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "articlesService:Viewer"). Interface("resultThumbnail", result.ThumbnailPath).Msg("") if result.ThumbnailPath != nil { searchThumbnail = *result.ThumbnailPath } else { searchThumbnail = "articles/thumbnail/" + emptyImage } } else { searchThumbnail = "articles/thumbnail/" + emptyImage } _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "articlesService:Viewer"). Interface("searchThumbnail", searchThumbnail).Msg("") ctx := context.Background() bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName objectName := searchThumbnail // Create minio connection. minioClient, err := _i.MinioStorage.ConnectMinio() if err != nil { // Return status 500 and minio connection error. return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": true, "msg": err.Error(), }) } fileContent, err := minioClient.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{}) if err != nil { log.Fatalln(err) } defer fileContent.Close() contentType := mime.TypeByExtension("." + getFileExtension(objectName)) if contentType == "" { contentType = "application/octet-stream" } c.Set("Content-Type", contentType) if _, err := io.Copy(c.Response().BodyWriter(), fileContent); err != nil { return err } return } func (_i *articlesService) UpdateActivityCount(authToken string, id uint, activityTypeId int) error { // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } result, err := _i.Repo.FindOne(clientId, id) if err != nil { return err } viewCount := 0 if result.ViewCount != nil { viewCount = *result.ViewCount } shareCount := 0 if result.ShareCount != nil { shareCount = *result.ShareCount } commentCount := 0 if result.CommentCount != nil { commentCount = *result.CommentCount } if activityTypeId == 2 { viewCount++ } else if activityTypeId == 3 { shareCount++ } else if activityTypeId == 4 { commentCount++ } result.ViewCount = &viewCount result.ShareCount = &shareCount result.CommentCount = &commentCount return _i.Repo.Update(clientId, id, result) } func (_i *articlesService) SummaryStats(authToken string) (summaryStats *response.ArticleSummaryStats, err error) { // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) result, err := _i.Repo.SummaryStats(clientId, user.ID) if err != nil { return nil, err } return result, nil } func (_i *articlesService) ArticlePerUserLevelStats(authToken string, startDate *string, endDate *string) (articlePerUserLevelStats []*response.ArticlePerUserLevelStats, err error) { // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "articlesService:ArticlePerUserLevelStats"). Interface("startDate", startDate).Msg("") _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "articlesService:ArticlePerUserLevelStats"). Interface("endDate", endDate).Msg("") var userLevelId *uint var userLevelNumber *int if user != nil { userLevelId = &user.UserLevelId userLevelNumber = &user.UserLevel.LevelNumber } _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "articlesService:ArticlePerUserLevelStats"). Interface("userLevelId", userLevelId).Msg("") _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "articlesService:ArticlePerUserLevelStats"). Interface("userLevelNumber", userLevelNumber).Msg("") result, err := _i.Repo.ArticlePerUserLevelStats(clientId, userLevelId, userLevelNumber, nil, nil) if err != nil { return nil, err } return result, nil } func (_i *articlesService) ArticleMonthlyStats(authToken string, year *int) (articleMonthlyStats []*response.ArticleMonthlyStats, err error) { // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) var userLevelId *uint var userLevelNumber *int if user != nil { userLevelId = &user.UserLevelId userLevelNumber = &user.UserLevel.LevelNumber } result, err := _i.Repo.ArticleMonthlyStats(clientId, userLevelId, userLevelNumber, *year) if err != nil { return nil, err } return result, nil } func (_i *articlesService) UpdateApproval(authToken string, id uint, statusId int, userLevelId int, userLevelNumber int, userParentLevelId int) (err error) { // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } result, err := _i.Repo.FindOne(clientId, id) if err != nil { return err } _i.Log.Info().Interface("statusId", statusId).Msg("") statusIdOne := 1 statusIdTwo := 2 statusIdThree := 3 isPublish := true isDraftFalse := false if statusId == 2 { if userLevelNumber == 2 || userLevelNumber == 3 { result.NeedApprovalFrom = &userParentLevelId result.StatusId = &statusIdOne } else { result.NeedApprovalFrom = nil result.StatusId = &statusIdTwo result.IsPublish = &isPublish publishedAt := time.Now() result.PublishedAt = &publishedAt result.IsDraft = &isDraftFalse result.DraftedAt = nil } userLevelIdStr := strconv.Itoa(userLevelId) if result.HasApprovedBy == nil { result.HasApprovedBy = &userLevelIdStr } else { hasApprovedBySlice := strings.Split(*result.HasApprovedBy, ",") hasApprovedBySlice = append(hasApprovedBySlice, userLevelIdStr) hasApprovedByJoin := strings.Join(hasApprovedBySlice, ",") result.HasApprovedBy = &hasApprovedByJoin } } else if statusId == 3 { result.StatusId = &statusIdThree result.NeedApprovalFrom = nil result.HasApprovedBy = nil } err = _i.Repo.Update(clientId, id, result) if err != nil { return err } return } func (_i *articlesService) PublishScheduling(authToken string, id uint, publishSchedule string) error { // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } result, err := _i.Repo.FindOne(clientId, id) if err != nil { return err } result.PublishSchedule = &publishSchedule return _i.Repo.Update(clientId, id, result) } func (_i *articlesService) UpdateBanner(authToken string, id uint, isBanner bool) error { // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } result, err := _i.Repo.FindOne(clientId, id) if err != nil { return err } result.IsBanner = &isBanner return _i.Repo.Update(clientId, id, result) } func (_i *articlesService) ExecuteScheduling() error { // For background jobs, we don't have context, so pass nil for clientId articles, err := _i.Repo.GetAllPublishSchedule(nil) if err != nil { return err } layout := "2006-01-02" now := time.Now() today := now.Truncate(24 * time.Hour) for _, article := range articles { // Looping setiap artikel if article.PublishSchedule == nil { continue } parsedDate, err := time.Parse(layout, *article.PublishSchedule) if err != nil { continue } if parsedDate.Equal(today) { isPublish := true statusIdTwo := 2 article.PublishSchedule = nil article.IsPublish = &isPublish article.PublishedAt = &now article.StatusId = &statusIdTwo // For background jobs, we don't have context, so pass nil for clientId if err := _i.Repo.UpdateSkipNull(nil, article.ID, article); err != nil { _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:articlesService", "Methods:ExecuteScheduling"). Interface("Failed to publish Article ID : ", article.ID).Msg("") } } } return err } func getFileExtension(filename string) string { // split file name parts := strings.Split(filename, ".") // jika tidak ada ekstensi, kembalikan string kosong if len(parts) == 1 || (len(parts) == 2 && parts[0] == "") { return "" } // ambil ekstensi terakhir return parts[len(parts)-1] } // SubmitForApproval submits an article for approval using the dynamic workflow system func (_i *articlesService) SubmitForApproval(authToken string, articleId uint, workflowId *uint) error { // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } // Extract user info from auth token user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user == nil { return errors.New("user not found from auth token") } // Check if article exists article, err := _i.Repo.FindOne(clientId, articleId) if err != nil { return err } // If no workflow specified, get the default workflow if workflowId == nil { defaultWorkflow, err := _i.ApprovalWorkflowsRepo.GetDefault(clientId) if err != nil { return err } workflowId = &defaultWorkflow.ID } // Validate workflow exists and is active _, err = _i.ApprovalWorkflowsRepo.FindOne(clientId, *workflowId) if err != nil { return err } // Create approval flow approvalFlow := &entity.ArticleApprovalFlows{ ArticleId: articleId, WorkflowId: *workflowId, CurrentStep: 1, StatusId: 1, // 1 = In Progress ClientId: clientId, SubmittedById: user.ID, } _, err = _i.ArticleApprovalFlowsRepo.Create(clientId, approvalFlow) if err != nil { return err } // Update article status to pending approval statusId := 1 // Pending approval article.StatusId = &statusId article.WorkflowId = workflowId err = _i.Repo.Update(clientId, articleId, article) if err != nil { return err } _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:articlesService", "Methods:SubmitForApproval"). Interface("Article submitted for approval", articleId).Msg("") return nil } // GetApprovalStatus gets the current approval status of an article func (_i *articlesService) GetApprovalStatus(authToken string, articleId uint) (*response.ArticleApprovalStatusResponse, error) { // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } // Check if article exists _, err := _i.Repo.FindOne(clientId, articleId) if err != nil { return nil, err } // Get approval flow approvalFlow, err := _i.ArticleApprovalFlowsRepo.FindByArticleId(clientId, articleId) if err != nil { // Article might not be in approval process return &response.ArticleApprovalStatusResponse{ ArticleId: articleId, Status: "not_submitted", CurrentStep: 0, TotalSteps: 0, Progress: 0, }, nil } // Get workflow details workflow, err := _i.ApprovalWorkflowsRepo.FindOne(clientId, approvalFlow.WorkflowId) if err != nil { return nil, err } // Get workflow steps workflowSteps, err := _i.ApprovalWorkflowsRepo.GetWorkflowSteps(clientId, approvalFlow.WorkflowId) if err != nil { return nil, err } totalSteps := len(workflowSteps) progress := 0.0 if totalSteps > 0 { progress = float64(approvalFlow.CurrentStep-1) / float64(totalSteps) * 100 } // Determine status status := "in_progress" if approvalFlow.StatusId == 2 { status = "approved" } else if approvalFlow.StatusId == 3 { status = "rejected" } else if approvalFlow.StatusId == 4 { status = "revision_requested" } // Get current approver info var currentApprover *string var nextStep *string if approvalFlow.CurrentStep <= totalSteps && approvalFlow.StatusId == 1 { if approvalFlow.CurrentStep < totalSteps { // Array indexing starts from 0, so subtract 1 from CurrentStep nextStepIndex := approvalFlow.CurrentStep - 1 if nextStepIndex >= 0 && nextStepIndex < len(workflowSteps) { nextStepInfo := workflowSteps[nextStepIndex] nextStep = &nextStepInfo.RequiredUserLevel.Name } } } return &response.ArticleApprovalStatusResponse{ ArticleId: articleId, WorkflowId: &workflow.ID, WorkflowName: &workflow.Name, CurrentStep: approvalFlow.CurrentStep, TotalSteps: totalSteps, Status: status, CurrentApprover: currentApprover, SubmittedAt: &approvalFlow.CreatedAt, LastActionAt: &approvalFlow.UpdatedAt, Progress: progress, CanApprove: false, // TODO: Implement based on user permissions NextStep: nextStep, }, nil } // GetPendingApprovals gets articles pending approval for a specific user level func (_i *articlesService) GetPendingApprovals(authToken string, page, limit int, typeId *int) ([]*response.ArticleApprovalQueueResponse, paginator.Pagination, error) { // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } // Extract user info from auth token user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user == nil { return nil, paginator.Pagination{}, errors.New("user not found from auth token") } // Prepare filters filters := make(map[string]interface{}) if typeId != nil { filters["type_id"] = *typeId } // Get pending approvals for the user level approvalFlows, paging, err := _i.ArticleApprovalFlowsRepo.GetPendingApprovals(clientId, user.UserLevelId, page, limit, filters) if err != nil { return nil, paging, err } var responses []*response.ArticleApprovalQueueResponse for _, flow := range approvalFlows { // Get article details article, err := _i.Repo.FindOne(clientId, flow.ArticleId) if err != nil { continue } // Get workflow details workflow, err := _i.ApprovalWorkflowsRepo.FindOne(clientId, flow.WorkflowId) if err != nil { continue } // Get workflow steps workflowSteps, err := _i.ApprovalWorkflowsRepo.GetWorkflowSteps(clientId, flow.WorkflowId) if err != nil { continue } // Calculate days in queue daysInQueue := int(time.Since(flow.CreatedAt).Hours() / 24) // Determine priority based on days in queue priority := "low" if daysInQueue > 7 { priority = "urgent" } else if daysInQueue > 3 { priority = "high" } else if daysInQueue > 1 { priority = "medium" } // Get author name var authorName string if article.CreatedById != nil { user, err := _i.UsersRepo.FindOne(clientId, *article.CreatedById) if err == nil && user != nil { authorName = user.Fullname } } // Get category name var categoryName string if article.CategoryId != 0 { category, err := _i.ArticleCategoriesRepo.FindOne(clientId, uint(article.CategoryId)) if err == nil && category != nil { categoryName = category.Title } } response := &response.ArticleApprovalQueueResponse{ ID: article.ID, Title: article.Title, Slug: article.Slug, Description: article.Description, CategoryName: categoryName, AuthorName: authorName, SubmittedAt: flow.CreatedAt, CurrentStep: flow.CurrentStep, TotalSteps: len(workflowSteps), Priority: priority, DaysInQueue: daysInQueue, WorkflowName: workflow.Name, CanApprove: true, // TODO: Implement based on user permissions EstimatedTime: "2-3 days", // TODO: Calculate based on historical data } responses = append(responses, response) } return responses, paging, nil } // GetArticlesWaitingForApproval gets articles that are waiting for approval by a specific user level func (_i *articlesService) GetArticlesWaitingForApproval(authToken string, page, limit int) ([]*response.ArticleApprovalQueueResponse, paginator.Pagination, error) { // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } // Extract user info from auth token user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user == nil { return nil, paginator.Pagination{}, errors.New("user not found from auth token") } // Use the existing repository method with proper filtering pagination := paginator.Pagination{ Page: page, Limit: limit, } req := request.ArticlesQueryRequest{ Pagination: &pagination, } articles, paging, err := _i.Repo.GetAll(clientId, &user.UserLevelId, &user.ID, req) if err != nil { return nil, paging, err } // Build response var responses []*response.ArticleApprovalQueueResponse for _, article := range articles { response := &response.ArticleApprovalQueueResponse{ ID: article.ID, Title: article.Title, Slug: article.Slug, Description: article.Description, SubmittedAt: article.CreatedAt, CurrentStep: 1, // Will be updated with actual step CanApprove: true, } responses = append(responses, response) } return responses, paging, nil } // CheckApprovalRequired checks if an article requires approval based on client settings func (_i *articlesService) CheckApprovalRequired(authToken string, articleId uint, userId uint, userLevelId uint) (bool, error) { // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } // Get article to check category and other properties article, err := _i.Repo.FindOne(clientId, articleId) if err != nil { return true, err // Default to requiring approval on error } // Check if article is already exempt if article.ApprovalExempt != nil && *article.ApprovalExempt { return false, nil } // Check if article should bypass approval if article.BypassApproval != nil && *article.BypassApproval { return false, nil } // Check client-level settings (this would require the client approval settings service) // For now, we'll use a simple check // TODO: Integrate with ClientApprovalSettingsService // Check if workflow is set to no approval if article.WorkflowId != nil { workflow, err := _i.ApprovalWorkflowsRepo.FindOne(clientId, *article.WorkflowId) if err == nil && workflow != nil { if workflow.RequiresApproval != nil && !*workflow.RequiresApproval { return false, nil } if workflow.AutoPublish != nil && *workflow.AutoPublish { return false, nil } } } // Default to requiring approval return true, nil } // AutoApproveArticle automatically approves an article (for no-approval scenarios) func (_i *articlesService) AutoApproveArticle(authToken string, articleId uint, reason string) error { // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } article, err := _i.Repo.FindOne(clientId, articleId) if err != nil { return err } // Update article status to approved updates := map[string]interface{}{ "status_id": 2, // Assuming 2 = approved "is_publish": true, "published_at": time.Now(), "current_approval_step": 0, // Reset approval step } // Convert updates map to article entity articleUpdate := &entity.Articles{} if isPublish, ok := updates["is_publish"].(bool); ok { articleUpdate.IsPublish = &isPublish } if publishedAt, ok := updates["published_at"].(time.Time); ok { articleUpdate.PublishedAt = &publishedAt } if currentApprovalStep, ok := updates["current_approval_step"].(int); ok { articleUpdate.CurrentApprovalStep = ¤tApprovalStep } err = _i.Repo.Update(clientId, articleId, articleUpdate) if err != nil { return err } // Create approval flow record for audit trail approvalFlow := &entity.ArticleApprovalFlows{ ArticleId: articleId, WorkflowId: *article.WorkflowId, CurrentStep: 0, StatusId: 2, // approved SubmittedById: *article.CreatedById, SubmittedAt: time.Now(), CompletedAt: &[]time.Time{time.Now()}[0], ClientId: clientId, } _, err = _i.ArticleApprovalFlowsRepo.Create(clientId, approvalFlow) if err != nil { _i.Log.Error().Err(err).Msg("Failed to create approval flow for auto-approved article") // Don't return error as article was already updated } _i.Log.Info(). Str("article_id", fmt.Sprintf("%d", articleId)). Str("client_id", clientId.String()). Str("reason", reason). Msg("Article auto-approved") return nil } // GetClientApprovalSettings gets the approval settings for a client func (_i *articlesService) GetClientApprovalSettings(authToken string) (*response.ClientApprovalSettingsResponse, error) { // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } // This would require the ClientApprovalSettingsService // For now, return default settings return &response.ClientApprovalSettingsResponse{ ClientId: clientId.String(), RequiresApproval: true, // Default to requiring approval AutoPublishArticles: false, IsActive: true, }, nil } // SetArticleApprovalExempt sets whether an article is exempt from approval func (_i *articlesService) SetArticleApprovalExempt(authToken string, articleId uint, exempt bool, reason string) error { // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } updates := map[string]interface{}{ "approval_exempt": &exempt, } if exempt { // If exempt, also set bypass approval bypass := true updates["bypass_approval"] = &bypass updates["current_approval_step"] = 0 } // Convert updates map to article entity articleUpdate := &entity.Articles{} if approvalExempt, ok := updates["approval_exempt"].(*bool); ok { articleUpdate.ApprovalExempt = approvalExempt } if bypassApproval, ok := updates["bypass_approval"].(*bool); ok { articleUpdate.BypassApproval = bypassApproval } if currentApprovalStep, ok := updates["current_approval_step"].(int); ok { articleUpdate.CurrentApprovalStep = ¤tApprovalStep } err := _i.Repo.Update(clientId, articleId, articleUpdate) if err != nil { return err } _i.Log.Info(). Str("article_id", fmt.Sprintf("%d", articleId)). Str("client_id", clientId.String()). Bool("exempt", exempt). Str("reason", reason). Msg("Article approval exemption updated") return nil } // findNextApprovalLevel finds the next higher level for approval func (_i *articlesService) findNextApprovalLevel(clientId *uuid.UUID, currentLevelNumber int) int { // For now, we'll use a simple logic based on level numbers // Level 3 (POLRES) -> Level 2 (POLDAS) -> Level 1 (POLDAS) switch currentLevelNumber { case 3: // POLRES return 2 // Should be approved by POLDAS (Level 2) case 2: // POLDAS return 1 // Should be approved by Level 1 case 1: // Highest level return 0 // No approval needed, can publish directly default: _i.Log.Warn().Int("currentLevel", currentLevelNumber).Msg("Unknown level, no approval needed") 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 }