package service import ( "context" "errors" "fmt" "io" approvalHistoriesService "jaecoo-be/app/module/approval_histories/service" "jaecoo-be/app/module/banners/mapper" "jaecoo-be/app/module/banners/repository" "jaecoo-be/app/module/banners/request" "jaecoo-be/app/module/banners/response" usersRepository "jaecoo-be/app/module/users/repository" "jaecoo-be/config/config" minioStorage "jaecoo-be/config/config" "jaecoo-be/utils/paginator" utilSvc "jaecoo-be/utils/service" "math/rand" "mime" "path/filepath" "strconv" "strings" "time" "github.com/gofiber/fiber/v2" "github.com/google/uuid" "github.com/minio/minio-go/v7" "github.com/rs/zerolog" ) type bannersService struct { Repo repository.BannersRepository Log zerolog.Logger Cfg *config.Config MinioStorage *minioStorage.MinioStorage UsersRepo usersRepository.UsersRepository ApprovalHistoriesService approvalHistoriesService.ApprovalHistoriesService } type BannersService interface { GetAll(clientId *uuid.UUID,req request.BannersQueryRequest) (banners []*response.BannersResponse, paging paginator.Pagination, err error) GetOne(id uint) (banner *response.BannersResponse, err error) Create(c *fiber.Ctx, req request.BannersCreateRequest) (banner *response.BannersResponse, err error) Update(c *fiber.Ctx, id uint, req request.BannersUpdateRequest) (banner *response.BannersResponse, err error) Delete(id uint) (err error) Approve(id uint, authToken string) (banner *response.BannersResponse, err error) Reject(id uint, authToken string, message *string) (banner *response.BannersResponse, err error) Comment(id uint, authToken string, message *string) (banner *response.BannersResponse, err error) UploadFileToMinio(c *fiber.Ctx, fileKey string) (filePath *string, err error) Viewer(c *fiber.Ctx) (err error) } func NewBannersService(repo repository.BannersRepository, log zerolog.Logger, cfg *config.Config, minioStorage *minioStorage.MinioStorage, usersRepo usersRepository.UsersRepository, approvalHistoriesService approvalHistoriesService.ApprovalHistoriesService) BannersService { return &bannersService{ Repo: repo, Log: log, Cfg: cfg, MinioStorage: minioStorage, UsersRepo: usersRepo, ApprovalHistoriesService: approvalHistoriesService, } } func (_i *bannersService) GetAll( clientId *uuid.UUID, req request.BannersQueryRequest, ) (banners []*response.BannersResponse, paging paginator.Pagination, err error) { bannersEntity, paging, err := _i.Repo.GetAll(clientId, req) if err != nil { return } host := _i.Cfg.App.Domain for _, banner := range bannersEntity { banners = append(banners, mapper.BannersResponseMapper(banner, host)) } return } func (_i *bannersService) GetOne(id uint) (banner *response.BannersResponse, err error) { bannerEntity, err := _i.Repo.FindOne(id) if err != nil { return } if bannerEntity == nil { err = errors.New("banner not found") return } host := _i.Cfg.App.Domain banner = mapper.BannersResponseMapper(bannerEntity, host) return } func (_i *bannersService) Create(c *fiber.Ctx, req request.BannersCreateRequest) (banner *response.BannersResponse, err error) { // Handle file upload if exists if filePath, uploadErr := _i.UploadFileToMinio(c, "file"); uploadErr == nil && filePath != nil { req.ThumbnailPath = filePath } bannerEntity := req.ToEntity() isActive := true bannerEntity.IsActive = &isActive bannerEntity, err = _i.Repo.Create(bannerEntity) if err != nil { return } host := _i.Cfg.App.Domain banner = mapper.BannersResponseMapper(bannerEntity, host) return } func (_i *bannersService) UploadFileToMinio(c *fiber.Ctx, fileKey string) (filePath *string, err error) { form, err := c.MultipartForm() if err != nil { return nil, err } files := form.File[fileKey] if len(files) == 0 { return nil, nil // No file uploaded, return nil without error } fileHeader := files[0] // Create minio connection minioClient, err := _i.MinioStorage.ConnectMinio() if err != nil { return nil, err } bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName // Open file src, err := fileHeader.Open() if err != nil { return nil, err } defer src.Close() // Process filename filename := filepath.Base(fileHeader.Filename) filename = strings.ReplaceAll(filename, " ", "") filenameWithoutExt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))]) extension := filepath.Ext(fileHeader.Filename)[1:] // Generate unique filename now := time.Now() rand.New(rand.NewSource(now.UnixNano())) randUniqueId := rand.Intn(1000000) newFilenameWithoutExt := filenameWithoutExt + "_" + strconv.Itoa(randUniqueId) newFilename := newFilenameWithoutExt + "." + extension // Create object name with path structure objectName := fmt.Sprintf("banners/upload/%d/%d/%s", now.Year(), now.Month(), newFilename) _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "Banners:UploadFileToMinio"). Interface("Uploading file", objectName).Msg("") // Upload file to MinIO _, err = minioClient.PutObject(context.Background(), bucketName, objectName, src, fileHeader.Size, minio.PutObjectOptions{}) if err != nil { _i.Log.Error().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "Banners:UploadFileToMinio"). Interface("Error uploading file", err).Msg("") return nil, err } _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "Banners:UploadFileToMinio"). Interface("Successfully uploaded", objectName).Msg("") return &objectName, nil } func (_i *bannersService) Update(c *fiber.Ctx, id uint, req request.BannersUpdateRequest) (banner *response.BannersResponse, err error) { // Handle file upload if exists if filePath, uploadErr := _i.UploadFileToMinio(c, "file"); uploadErr == nil && filePath != nil { req.ThumbnailPath = filePath } bannerEntity := req.ToEntity() err = _i.Repo.Update(id, bannerEntity) if err != nil { return } bannerEntity, err = _i.Repo.FindOne(id) if err != nil { return } host := _i.Cfg.App.Domain banner = mapper.BannersResponseMapper(bannerEntity, host) return } func (_i *bannersService) Delete(id uint) (err error) { err = _i.Repo.Delete(id) return } func (_i *bannersService) Approve(id uint, authToken string) (banner *response.BannersResponse, err error) { // Get user from token user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user == nil { err = errors.New("unauthorized: user not found") return } // Check if user has admin role (roleId = 1) if user.UserRoleId != 1 { err = errors.New("unauthorized: only admin can approve") return } // Approve banner (update status_id to 2) err = _i.Repo.Approve(id) if err != nil { return } // Save approval history userID := user.ID statusApprove := 2 err = _i.ApprovalHistoriesService.CreateHistory( "banners", id, &statusApprove, // ✅ pointer "approve", &userID, nil, ) if err != nil { _i.Log.Error().Err(err).Msg("Failed to save approval history") // Don't return error, just log it } // Get updated banner data bannerEntity, err := _i.Repo.FindOne(id) if err != nil { return } if bannerEntity == nil { err = errors.New("banner not found") return } host := _i.Cfg.App.Domain banner = mapper.BannersResponseMapper(bannerEntity, host) return } func (_i *bannersService) Reject(id uint, authToken string, message *string) (banner *response.BannersResponse, err error) { // Get user from token user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user == nil { err = errors.New("unauthorized: user not found") return } // Check if user has admin role (roleId = 1) if user.UserRoleId != 1 { err = errors.New("unauthorized: only admin can reject") return } // Reject banner (update status_id to 3) err = _i.Repo.Reject(id) if err != nil { return } // Save rejection history userID := user.ID statusReject := 3 err = _i.ApprovalHistoriesService.CreateHistory( "banners", id, &statusReject, // ✅ pointer "reject", &userID, message, ) if err != nil { _i.Log.Error().Err(err).Msg("Failed to save rejection history") // Don't return error, just log it } // Get updated banner data bannerEntity, err := _i.Repo.FindOne(id) if err != nil { return } if bannerEntity == nil { err = errors.New("banner not found") return } host := _i.Cfg.App.Domain banner = mapper.BannersResponseMapper(bannerEntity, host) return } func (_i *bannersService) Comment( id uint, authToken string, message *string, ) (banner *response.BannersResponse, err error) { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user == nil { err = errors.New("unauthorized") return } if user.UserRoleId != 1 { err = errors.New("only admin can comment") return } // SIMPAN COMMENT KE HISTORY (INTI FITURNYA) userID := user.ID err = _i.ApprovalHistoriesService.CreateHistory( "banners", id, nil, // status_id NULL "comment", &userID, message, ) if err != nil { return } // Ambil banner terbaru bannerEntity, err := _i.Repo.FindOne(id) if err != nil { return } if bannerEntity == nil { err = errors.New("banner not found") return } host := _i.Cfg.App.Domain banner = mapper.BannersResponseMapper(bannerEntity, host) return } func (_i *bannersService) Viewer(c *fiber.Ctx) (err error) { filename := c.Params("filename") // Find banner by filename (repository will search using LIKE pattern) result, err := _i.Repo.FindByThumbnailPath(filename) if err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ "error": true, "msg": "Banner file not found", }) } if result.ThumbnailPath == nil || *result.ThumbnailPath == "" { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ "error": true, "msg": "Banner thumbnail path not found", }) } ctx := context.Background() bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName objectName := *result.ThumbnailPath _i.Log.Info().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "Banners:Viewer"). Interface("data", objectName).Msg("") // Create minio connection minioClient, err := _i.MinioStorage.ConnectMinio() if err != nil { 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 { _i.Log.Error().Str("timestamp", time.Now(). Format(time.RFC3339)).Str("Service:Resource", "Banners:Viewer"). Interface("Error getting file", err).Msg("") return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": true, "msg": "Failed to retrieve file", }) } defer fileContent.Close() // Determine Content-Type based on file extension contentType := mime.TypeByExtension("." + getFileExtension(objectName)) if contentType == "" { contentType = "application/octet-stream" // fallback if no MIME type matches } c.Set("Content-Type", contentType) if _, err := io.Copy(c.Response().BodyWriter(), fileContent); err != nil { return err } return } 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] }