package service import ( "context" "errors" "fmt" "io" "log" "math/rand" "mime" "path/filepath" "strconv" "strings" "time" "web-qudo-be/app/database/entity" approvalWorkflowsRepository "web-qudo-be/app/module/approval_workflows/repository" articleApprovalFlowsRepository "web-qudo-be/app/module/article_approval_flows/repository" articleApprovalsRepository "web-qudo-be/app/module/article_approvals/repository" articleCategoriesRepository "web-qudo-be/app/module/article_categories/repository" articleCategoryDetailsRepository "web-qudo-be/app/module/article_category_details/repository" articleCategoryDetailsReq "web-qudo-be/app/module/article_category_details/request" articleFilesRepository "web-qudo-be/app/module/article_files/repository" "web-qudo-be/app/module/articles/mapper" "web-qudo-be/app/module/articles/repository" "web-qudo-be/app/module/articles/request" "web-qudo-be/app/module/articles/response" usersRepository "web-qudo-be/app/module/users/repository" config "web-qudo-be/config/config" minioStorage "web-qudo-be/config/config" "web-qudo-be/utils/paginator" utilSvc "web-qudo-be/utils/service" "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 MinioStorage *minioStorage.MinioStorage // Dynamic approval system dependencies ArticleApprovalFlowsRepo articleApprovalFlowsRepository.ArticleApprovalFlowsRepository ApprovalWorkflowsRepo approvalWorkflowsRepository.ApprovalWorkflowsRepository } // ArticlesService define interface of IArticlesService type ArticlesService interface { All(clientId *uuid.UUID, authToken string, req request.ArticlesQueryRequest) (articles []*response.ArticlesResponse, paging paginator.Pagination, err error) Show(clientId *uuid.UUID, id uint) (articles *response.ArticlesResponse, err error) ShowByOldId(clientId *uuid.UUID, oldId uint) (articles *response.ArticlesResponse, err error) ShowBySlug(clientId *uuid.UUID, slug string) (articles *response.ArticlesResponse, err error) Save(clientId *uuid.UUID, req request.ArticlesCreateRequest, authToken string) (articles *entity.Articles, err error) SaveThumbnail(clientId *uuid.UUID, c *fiber.Ctx) (err error) Update(clientId *uuid.UUID, id uint, req request.ArticlesUpdateRequest) (err error) Delete(clientId *uuid.UUID, id uint) error UpdateActivityCount(clientId *uuid.UUID, id uint, activityTypeId int) (err error) UpdateApproval(clientId *uuid.UUID, id uint, statusId int, userLevelId int, userLevelNumber int, userParentLevelId int) (err error) UpdateBanner(clientId *uuid.UUID, id uint, isBanner bool) error Viewer(clientId *uuid.UUID, c *fiber.Ctx) error SummaryStats(clientId *uuid.UUID, authToken string) (summaryStats *response.ArticleSummaryStats, err error) ArticlePerUserLevelStats(clientId *uuid.UUID, authToken string, startDate *string, endDate *string) (articlePerUserLevelStats []*response.ArticlePerUserLevelStats, err error) ArticleMonthlyStats(clientId *uuid.UUID, authToken string, year *int) (articleMonthlyStats []*response.ArticleMonthlyStats, err error) PublishScheduling(clientId *uuid.UUID, id uint, publishSchedule string) error ExecuteScheduling() error // Dynamic approval system methods SubmitForApproval(clientId *uuid.UUID, articleId uint, authToken string, workflowId *uint) error GetApprovalStatus(clientId *uuid.UUID, articleId uint) (*response.ArticleApprovalStatusResponse, error) GetArticlesWaitingForApproval(clientId *uuid.UUID, authToken string, page, limit int) ([]*response.ArticleApprovalQueueResponse, paginator.Pagination, error) GetPendingApprovals(clientId *uuid.UUID, authToken string, page, limit int, typeId *int) ([]*response.ArticleApprovalQueueResponse, paginator.Pagination, error) // Updated with typeId filter // No-approval system methods CheckApprovalRequired(clientId *uuid.UUID, articleId uint, userId uint, userLevelId uint) (bool, error) AutoApproveArticle(clientId *uuid.UUID, articleId uint, reason string) error GetClientApprovalSettings(clientId *uuid.UUID) (*response.ClientApprovalSettingsResponse, error) SetArticleApprovalExempt(clientId *uuid.UUID, articleId uint, exempt bool, reason string) error // Publish/Unpublish methods Publish(clientId *uuid.UUID, articleId uint, authToken string) error Unpublish(clientId *uuid.UUID, articleId uint, authToken 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, log zerolog.Logger, cfg *config.Config, usersRepo usersRepository.UsersRepository, minioStorage *minioStorage.MinioStorage, ) ArticlesService { return &articlesService{ Repo: repo, ArticleCategoriesRepo: articleCategoriesRepo, ArticleCategoryDetailsRepo: articleCategoryDetailsRepo, ArticleFilesRepo: articleFilesRepo, ArticleApprovalsRepo: articleApprovalsRepo, ArticleApprovalFlowsRepo: articleApprovalFlowsRepo, ApprovalWorkflowsRepo: approvalWorkflowsRepo, Log: log, UsersRepo: usersRepo, MinioStorage: minioStorage, Cfg: cfg, } } // All implement interface of ArticlesService func (_i *articlesService) All(clientId *uuid.UUID, authToken string, req request.ArticlesQueryRequest) (articless []*response.ArticlesResponse, paging paginator.Pagination, err error) { // Extract userLevelId from authToken var userLevelId *uint if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil { userLevelId = &user.UserLevelId _i.Log.Info().Interface("userLevelId", userLevelId).Msg("Extracted userLevelId 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 } results, paging, err := _i.Repo.GetAll(clientId, userLevelId, 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) articless = append(articless, articleRes) } return } func (_i *articlesService) Show(clientId *uuid.UUID, id uint) (articles *response.ArticlesResponse, err error) { 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), nil } func (_i *articlesService) ShowByOldId(clientId *uuid.UUID, oldId uint) (articles *response.ArticlesResponse, err error) { 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), nil } func (_i *articlesService) ShowBySlug(clientId *uuid.UUID, slug string) (articles *response.ArticlesResponse, err error) { result, err := _i.Repo.FindBySlug(clientId, slug) 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), nil } func (_i *articlesService) Save(clientId *uuid.UUID, req request.ArticlesCreateRequest, authToken string) (articles *entity.Articles, err error) { _i.Log.Info().Interface("data", req).Msg("") newReq := req.ToEntity() 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 { // 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 { // 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 if createdBy != nil && *createdBy.UserLevel.IsApprovalActive == true { // Get default workflow for the client defaultWorkflow, err := _i.ApprovalWorkflowsRepo.GetDefault(clientId) if err == nil && defaultWorkflow != nil { // 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") } // Create approval flow approvalFlow := &entity.ArticleApprovalFlows{ ArticleId: saveArticleRes.ID, WorkflowId: defaultWorkflow.ID, CurrentStep: 1, StatusId: 1, // In Progress SubmittedById: *newReq.CreatedById, SubmittedAt: time.Now(), ClientId: clientId, } _, err = _i.ArticleApprovalFlowsRepo.Create(clientId, approvalFlow) if err != nil { _i.Log.Error().Err(err).Msg("Failed to create approval flow") } } // 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(clientId *uuid.UUID, c *fiber.Ctx) (err error) { 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(clientId *uuid.UUID, id uint, req request.ArticlesUpdateRequest) (err error) { _i.Log.Info().Interface("data", req).Msg("") newReq := req.ToEntity() 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(clientId *uuid.UUID, id uint) error { return _i.Repo.Delete(clientId, id) } func (_i *articlesService) Viewer(clientId *uuid.UUID, c *fiber.Ctx) (err error) { 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(clientId *uuid.UUID, id uint, activityTypeId int) error { 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(clientId *uuid.UUID, authToken string) (summaryStats *response.ArticleSummaryStats, err error) { 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(clientId *uuid.UUID, authToken string, startDate *string, endDate *string) (articlePerUserLevelStats []*response.ArticlePerUserLevelStats, err error) { 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(clientId *uuid.UUID, authToken string, year *int) (articleMonthlyStats []*response.ArticleMonthlyStats, err error) { 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(clientId *uuid.UUID, id uint, statusId int, userLevelId int, userLevelNumber int, userParentLevelId int) (err error) { 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(clientId *uuid.UUID, id uint, publishSchedule string) error { result, err := _i.Repo.FindOne(clientId, id) if err != nil { return err } // Validate publish schedule format dateLayout := "2006-01-02" datetimeLayout := "2006-01-02 15:04:05" // Try parsing as datetime first (with time) _, parseErr := time.Parse(datetimeLayout, publishSchedule) if parseErr != nil { // If datetime parsing fails, try parsing as date only _, parseErr = time.Parse(dateLayout, publishSchedule) if parseErr != nil { return fmt.Errorf("invalid publish schedule format. Supported formats: '2006-01-02' or '2006-01-02 15:04:05'") } } result.PublishSchedule = &publishSchedule return _i.Repo.Update(clientId, id, result) } func (_i *articlesService) UpdateBanner(clientId *uuid.UUID, id uint, isBanner bool) error { 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 } // Support both date-only and datetime formats dateLayout := "2006-01-02" datetimeLayout := "2006-01-02 15:04:05" now := time.Now() for _, article := range articles { // Looping setiap artikel if article.PublishSchedule == nil { continue } var scheduledTime time.Time var parseErr error // Try parsing as datetime first (with time) scheduledTime, parseErr = time.Parse(datetimeLayout, *article.PublishSchedule) if parseErr != nil { // If datetime parsing fails, try parsing as date only scheduledTime, parseErr = time.Parse(dateLayout, *article.PublishSchedule) if parseErr != nil { _i.Log.Warn().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:articlesService", "Methods:ExecuteScheduling"). Str("Invalid schedule format", *article.PublishSchedule). Interface("Article ID", article.ID).Msg("") continue } // If parsed as date only, set time to start of day (00:00:00) in local timezone scheduledTime = time.Date(scheduledTime.Year(), scheduledTime.Month(), scheduledTime.Day(), 0, 0, 0, 0, now.Location()) } else { // For datetime format, parse in local timezone scheduledTime = time.Date(scheduledTime.Year(), scheduledTime.Month(), scheduledTime.Day(), scheduledTime.Hour(), scheduledTime.Minute(), scheduledTime.Second(), 0, now.Location()) } // Check if the scheduled time has passed (for datetime) or if it's today (for date only) shouldPublish := false if len(*article.PublishSchedule) > 10 { // Contains time (datetime format) // For datetime format, check if scheduled time has passed shouldPublish = now.After(scheduledTime) || now.Equal(scheduledTime) } else { // For date-only format, check if it's today today := now.Truncate(24 * time.Hour) scheduledDate := scheduledTime.Truncate(24 * time.Hour) shouldPublish = scheduledDate.Equal(today) || scheduledDate.Before(today) } if shouldPublish { 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("") } else { _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:articlesService", "Methods:ExecuteScheduling"). Interface("Successfully published 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(clientId *uuid.UUID, articleId uint, authToken string, workflowId *uint) error { // 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(clientId *uuid.UUID, articleId uint) (*response.ArticleApprovalStatusResponse, error) { // 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(clientId *uuid.UUID, authToken string, page, limit int, typeId *int) ([]*response.ArticleApprovalQueueResponse, paginator.Pagination, error) { // 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(clientId *uuid.UUID, authToken string, page, limit int) ([]*response.ArticleApprovalQueueResponse, paginator.Pagination, error) { // 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, 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(clientId *uuid.UUID, articleId uint, userId uint, userLevelId uint) (bool, error) { // 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(clientId *uuid.UUID, articleId uint, reason string) error { 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(clientId *uuid.UUID) (*response.ClientApprovalSettingsResponse, error) { // 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(clientId *uuid.UUID, articleId uint, exempt bool, reason string) error { 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 } // Publish publishes an article func (_i *articlesService) Publish(clientId *uuid.UUID, articleId uint, authToken string) error { // 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 } // Check if article is already published if article.IsPublish != nil && *article.IsPublish { return errors.New("article is already published") } // Check if user has permission to publish // For now, we'll allow any authenticated user to publish // You can add more sophisticated permission checks here // Update article to published status isPublish := true publishedAt := time.Now() isDraftFalse := false statusIdTwo := 2 // Published status article.IsPublish = &isPublish article.PublishedAt = &publishedAt article.IsDraft = &isDraftFalse article.DraftedAt = nil article.StatusId = &statusIdTwo article.PublishSchedule = nil // Clear any scheduled publish time err = _i.Repo.Update(clientId, articleId, article) if err != nil { return err } // Create approval record for audit trail articleApproval := &entity.ArticleApprovals{ ArticleId: articleId, ApprovalBy: user.ID, StatusId: statusIdTwo, Message: "Article published", ApprovalAtLevel: nil, } _, err = _i.ArticleApprovalsRepo.Create(articleApproval) if err != nil { _i.Log.Error().Err(err).Msg("Failed to create approval record for published 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("user_id", fmt.Sprintf("%d", user.ID)). Msg("Article published successfully") return nil } // Unpublish unpublishes an article func (_i *articlesService) Unpublish(clientId *uuid.UUID, articleId uint, authToken string) error { // 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 } // Check if article is already unpublished if article.IsPublish == nil || !*article.IsPublish { return errors.New("article is already unpublished") } // Check if user has permission to unpublish // For now, we'll allow any authenticated user to unpublish // You can add more sophisticated permission checks here // Update article to unpublished status isPublishFalse := false isDraftTrue := true draftedAt := time.Now() statusIdOne := 1 // Draft status article.IsPublish = &isPublishFalse article.PublishedAt = nil article.PublishSchedule = nil article.IsDraft = &isDraftTrue article.DraftedAt = &draftedAt article.StatusId = &statusIdOne err = _i.Repo.Update(clientId, articleId, article) if err != nil { return err } // Create approval record for audit trail articleApproval := &entity.ArticleApprovals{ ArticleId: articleId, ApprovalBy: user.ID, StatusId: statusIdOne, Message: "Article unpublished", ApprovalAtLevel: nil, } _, err = _i.ArticleApprovalsRepo.Create(articleApproval) if err != nil { _i.Log.Error().Err(err).Msg("Failed to create approval record for unpublished 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("user_id", fmt.Sprintf("%d", user.ID)). Msg("Article unpublished successfully") 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 } }