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

620 lines
18 KiB
Go

package service
import (
"context"
"errors"
"fmt"
"io"
"log"
"math/rand"
"mime"
"narasi-ahli-be/app/database/entity"
articleApprovalsRepository "narasi-ahli-be/app/module/article_approvals/repository"
articleCategoriesRepository "narasi-ahli-be/app/module/article_categories/repository"
articleCategoryDetailsRepository "narasi-ahli-be/app/module/article_category_details/repository"
articleCategoryDetailsReq "narasi-ahli-be/app/module/article_category_details/request"
articleFilesRepository "narasi-ahli-be/app/module/article_files/repository"
"narasi-ahli-be/app/module/articles/mapper"
"narasi-ahli-be/app/module/articles/repository"
"narasi-ahli-be/app/module/articles/request"
"narasi-ahli-be/app/module/articles/response"
usersRepository "narasi-ahli-be/app/module/users/repository"
config "narasi-ahli-be/config/config"
minioStorage "narasi-ahli-be/config/config"
"narasi-ahli-be/utils/paginator"
utilSvc "narasi-ahli-be/utils/service"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"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
}
// ArticlesService define interface of IArticlesService
type ArticlesService interface {
All(req request.ArticlesQueryRequest) (articles []*response.ArticlesResponse, paging paginator.Pagination, err error)
Show(id uint) (articles *response.ArticlesResponse, err error)
ShowByOldId(oldId uint) (articles *response.ArticlesResponse, err error)
Save(req request.ArticlesCreateRequest, authToken string) (articles *entity.Articles, err error)
SaveThumbnail(c *fiber.Ctx) (err error)
Update(id uint, req request.ArticlesUpdateRequest) (err error)
Delete(id uint) error
UpdateActivityCount(id uint, activityTypeId int) (err error)
UpdateApproval(id uint, statusId int, userLevelId int, userLevelNumber int, userParentLevelId int) (err error)
UpdateBanner(id uint, isBanner bool) error
Viewer(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(id uint, publishSchedule string) error
}
// NewArticlesService init ArticlesService
func NewArticlesService(
repo repository.ArticlesRepository,
articleCategoriesRepo articleCategoriesRepository.ArticleCategoriesRepository,
articleCategoryDetailsRepo articleCategoryDetailsRepository.ArticleCategoryDetailsRepository,
articleFilesRepo articleFilesRepository.ArticleFilesRepository,
articleApprovalsRepo articleApprovalsRepository.ArticleApprovalsRepository,
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,
Log: log,
UsersRepo: usersRepo,
MinioStorage: minioStorage,
Cfg: cfg,
}
}
// All implement interface of ArticlesService
func (_i *articlesService) All(req request.ArticlesQueryRequest) (articless []*response.ArticlesResponse, paging paginator.Pagination, err error) {
if req.Category != nil {
findCategory, err := _i.ArticleCategoriesRepo.FindOneBySlug(*req.Category)
if err != nil {
return nil, paging, err
}
req.CategoryId = &findCategory.ID
}
results, paging, err := _i.Repo.GetAll(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, result, _i.ArticleCategoriesRepo, _i.ArticleCategoryDetailsRepo, _i.ArticleFilesRepo, _i.UsersRepo)
articless = append(articless, articleRes)
}
return
}
func (_i *articlesService) Show(id uint) (articles *response.ArticlesResponse, err error) {
result, err := _i.Repo.FindOne(id)
if err != nil {
return nil, err
}
host := _i.Cfg.App.Domain
return mapper.ArticlesResponseMapper(_i.Log, host, result, _i.ArticleCategoriesRepo, _i.ArticleCategoryDetailsRepo, _i.ArticleFilesRepo, _i.UsersRepo), nil
}
func (_i *articlesService) ShowByOldId(oldId uint) (articles *response.ArticlesResponse, err error) {
result, err := _i.Repo.FindByOldId(oldId)
if err != nil {
return nil, err
}
host := _i.Cfg.App.Domain
return mapper.ArticlesResponseMapper(_i.Log, host, result, _i.ArticleCategoriesRepo, _i.ArticleCategoryDetailsRepo, _i.ArticleFilesRepo, _i.UsersRepo), nil
}
func (_i *articlesService) Save(req request.ArticlesCreateRequest, authToken string) (articles *entity.Articles, err error) {
_i.Log.Info().Interface("data", req).Msg("")
newReq := req.ToEntity()
var userLevelNumber int
var userParentLevelId int
if req.CreatedById != nil {
createdBy, err := _i.UsersRepo.FindOne(*req.CreatedById)
if err != nil {
return nil, fmt.Errorf("User not found")
}
newReq.CreatedById = &createdBy.ID
userLevelNumber = createdBy.UserLevel.LevelNumber
if createdBy.UserLevel.ParentLevelId != nil {
userParentLevelId = *createdBy.UserLevel.ParentLevelId
}
} else {
createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
newReq.CreatedById = &createdBy.ID
userLevelNumber = createdBy.UserLevel.LevelNumber
if createdBy.UserLevel.ParentLevelId != nil {
userParentLevelId = *createdBy.UserLevel.ParentLevelId
}
}
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
}
// Approval
statusIdOne := 1
statusIdTwo := 2
isPublishFalse := false
createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
if createdBy != nil && *createdBy.UserLevel.IsApprovalActive == false {
newReq.NeedApprovalFrom = nil
newReq.StatusId = &statusIdTwo
} else {
newReq.NeedApprovalFrom = &userParentLevelId
newReq.StatusId = &statusIdOne
newReq.IsPublish = &isPublishFalse
newReq.PublishedAt = nil
}
saveArticleRes, err := _i.Repo.Create(newReq)
if err != nil {
return nil, err
}
// Approval
var articleApproval *entity.ArticleApprovals
if createdBy != nil && *createdBy.UserLevel.IsApprovalActive == true {
articleApproval = &entity.ArticleApprovals{
ArticleId: saveArticleRes.ID,
ApprovalBy: *newReq.CreatedById,
StatusId: statusIdOne,
Message: "Need Approval",
ApprovalAtLevel: &userLevelNumber,
}
} else {
articleApproval = &entity.ArticleApprovals{
ArticleId: saveArticleRes.ID,
ApprovalBy: *newReq.CreatedById,
StatusId: statusIdTwo,
Message: "Publish Otomatis",
ApprovalAtLevel: nil,
}
}
_, err = _i.ArticleApprovalsRepo.Create(articleApproval)
if err != nil {
return nil, err
}
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(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(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(uint(id))
findCategory.ThumbnailName = &newFilename
findCategory.ThumbnailPath = &objectName
err = _i.Repo.Update(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(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(id, newReq)
}
func (_i *articlesService) Delete(id uint) error {
result, err := _i.Repo.FindOne(id)
if err != nil {
return err
}
isActive := false
result.IsActive = &isActive
return _i.Repo.Update(id, result)
}
func (_i *articlesService) Viewer(c *fiber.Ctx) (err error) {
thumbnailName := c.Params("thumbnailName")
emptyImage := "empty-image.jpg"
searchThumbnail := emptyImage
if thumbnailName != emptyImage {
result, err := _i.Repo.FindByFilename(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(id uint, activityTypeId int) error {
result, err := _i.Repo.FindOne(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(id, result)
}
func (_i *articlesService) SummaryStats(authToken string) (summaryStats *response.ArticleSummaryStats, err error) {
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
result, err := _i.Repo.SummaryStats(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) {
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(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) {
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(userLevelId, userLevelNumber, *year)
if err != nil {
return nil, err
}
return result, nil
}
func (_i *articlesService) UpdateApproval(id uint, statusId int, userLevelId int, userLevelNumber int, userParentLevelId int) (err error) {
result, err := _i.Repo.FindOne(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(id, result)
if err != nil {
return err
}
return
}
func (_i *articlesService) PublishScheduling(id uint, publishSchedule string) error {
result, err := _i.Repo.FindOne(id)
if err != nil {
return err
}
result.PublishSchedule = &publishSchedule
return _i.Repo.Update(id, result)
}
func (_i *articlesService) UpdateBanner(id uint, isBanner bool) error {
result, err := _i.Repo.FindOne(id)
if err != nil {
return err
}
result.IsBanner = &isBanner
return _i.Repo.Update(id, result)
}
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]
}