qudoco-be/app/module/articles/service/articles.service.go

1369 lines
44 KiB
Go

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 = &currentApprovalStep
}
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 = &currentApprovalStep
}
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
}
}