jaecoo-be/app/module/products/service/products.service.go

573 lines
14 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"context"
"errors"
"fmt"
"io"
approvalHistoriesService "jaecoo-be/app/module/approval_histories/service"
"jaecoo-be/app/module/products/mapper"
"jaecoo-be/app/module/products/repository"
"jaecoo-be/app/module/products/request"
"jaecoo-be/app/module/products/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"
"mime/multipart"
"path/filepath"
"strconv"
"strings"
"time"
"encoding/json"
"github.com/gofiber/fiber/v2"
"github.com/minio/minio-go/v7"
"github.com/rs/zerolog"
)
type productsService struct {
Repo repository.ProductsRepository
Log zerolog.Logger
Cfg *config.Config
MinioStorage *minioStorage.MinioStorage
UsersRepo usersRepository.UsersRepository
ApprovalHistoriesService approvalHistoriesService.ApprovalHistoriesService
}
type ProductsService interface {
GetAll(req request.ProductsQueryRequest) (products []*response.ProductsResponse, paging paginator.Pagination, err error)
GetOne(id uint) (product *response.ProductsResponse, err error)
Create(c *fiber.Ctx, req request.ProductsCreateRequest) (product *response.ProductsResponse, err error)
Update(c *fiber.Ctx, id uint, req request.ProductsUpdateRequest) (product *response.ProductsResponse, err error)
Delete(id uint) (err error)
Approve(id uint, authToken string) (product *response.ProductsResponse, err error)
Reject(id uint, authToken string, message *string) (product *response.ProductsResponse, err error)
Comment(id uint, authToken string, message *string) (product *response.ProductsResponse, err error)
UploadFileToMinio(c *fiber.Ctx, fileKey string) (filePath *string, err error)
Viewer(c *fiber.Ctx) (err error)
}
func NewProductsService(repo repository.ProductsRepository, log zerolog.Logger, cfg *config.Config, minioStorage *minioStorage.MinioStorage, usersRepo usersRepository.UsersRepository, approvalHistoriesService approvalHistoriesService.ApprovalHistoriesService) ProductsService {
return &productsService{
Repo: repo,
Log: log,
Cfg: cfg,
MinioStorage: minioStorage,
UsersRepo: usersRepo,
ApprovalHistoriesService: approvalHistoriesService,
}
}
func (_i *productsService) GetAll(req request.ProductsQueryRequest) (products []*response.ProductsResponse, paging paginator.Pagination, err error) {
productsEntity, paging, err := _i.Repo.GetAll(req)
if err != nil {
return
}
host := _i.Cfg.App.Domain
for _, product := range productsEntity {
products = append(products, mapper.ProductsResponseMapper(product, host))
}
return
}
func (_i *productsService) GetOne(id uint) (product *response.ProductsResponse, err error) {
productEntity, err := _i.Repo.FindOne(id)
if err != nil {
return
}
if productEntity == nil {
err = errors.New("product not found")
return
}
host := _i.Cfg.App.Domain
product = mapper.ProductsResponseMapper(productEntity, host)
return
}
func (_i *productsService) uploadColorFile(
fileHeader *multipart.FileHeader,
) (*string, error) {
minioClient, err := _i.MinioStorage.ConnectMinio()
if err != nil {
return nil, err
}
src, err := fileHeader.Open()
if err != nil {
return nil, err
}
defer src.Close()
now := time.Now()
ext := filepath.Ext(fileHeader.Filename)
name := strings.TrimSuffix(fileHeader.Filename, ext)
name = strings.ReplaceAll(name, " ", "")
filename := fmt.Sprintf(
"%s_%d%s",
name,
rand.Intn(999999),
ext,
)
objectName := fmt.Sprintf(
"products/colors/%d/%d/%s",
now.Year(),
now.Month(),
filename,
)
_, err = minioClient.PutObject(
context.Background(),
_i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName,
objectName,
src,
fileHeader.Size,
minio.PutObjectOptions{},
)
if err != nil {
return nil, err
}
return &objectName, nil
}
func (_i *productsService) Create(c *fiber.Ctx, req request.ProductsCreateRequest) (product *response.ProductsResponse, err error) {
// Handle file upload if exists
if filePath, uploadErr := _i.UploadFileToMinio(c, "file"); uploadErr == nil && filePath != nil {
req.ThumbnailPath = filePath
}
// 🔥 CONVERT REQUEST KE ENTITY
productEntity := req.ToEntity()
// ===============================
// 3⃣ 🔥 HANDLE COLORS + IMAGE
// ===============================
form, _ := c.MultipartForm()
colorFiles := form.File["color_images"]
var colorEntities []struct {
Name string `json:"name"`
ImagePath *string `json:"image_path"`
}
for i, cReq := range req.Colors {
if cReq.Name == "" {
continue
}
color := struct {
Name string `json:"name"`
ImagePath *string `json:"image_path"`
}{
Name: cReq.Name,
}
// hubungkan file berdasarkan index
if len(colorFiles) > i {
fileHeader := colorFiles[i]
path, err := _i.uploadColorFile(fileHeader)
if err == nil && path != nil {
color.ImagePath = path
}
}
colorEntities = append(colorEntities, color)
}
// simpan ke kolom products.colors (JSON string)
if len(colorEntities) > 0 {
bytes, _ := json.Marshal(colorEntities)
str := string(bytes)
productEntity.Colors = &str
}
// 4⃣ DEFAULT ACTIVE
isActive := true
productEntity.IsActive = &isActive
// 5⃣ SIMPAN KE DB
productEntity, err = _i.Repo.Create(productEntity)
if err != nil {
return
}
// 6⃣ RESPONSE
host := _i.Cfg.App.Domain
product = mapper.ProductsResponseMapper(productEntity, host)
return
}
func (_i *productsService) 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("products/upload/%d/%d/%s", now.Year(), now.Month(), newFilename)
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Products: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", "Products:UploadFileToMinio").
Interface("Error uploading file", err).Msg("")
return nil, err
}
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Products:UploadFileToMinio").
Interface("Successfully uploaded", objectName).Msg("")
return &objectName, nil
}
func (_i *productsService) Update(c *fiber.Ctx, id uint, req request.ProductsUpdateRequest) (product *response.ProductsResponse, err error) {
// Handle file upload if exists
if filePath, uploadErr := _i.UploadFileToMinio(c, "file"); uploadErr == nil && filePath != nil {
req.ThumbnailPath = filePath
}
productEntity := req.ToEntity()
err = _i.Repo.Update(id, productEntity)
if err != nil {
return
}
productEntity, err = _i.Repo.FindOne(id)
if err != nil {
return
}
host := _i.Cfg.App.Domain
product = mapper.ProductsResponseMapper(productEntity, host)
return
}
func (_i *productsService) Delete(id uint) (err error) {
err = _i.Repo.Delete(id)
return
}
func (_i *productsService) Viewer(c *fiber.Ctx) (err error) {
filename := c.Params("filename")
var objectName string
var found bool
// First, try to find by ThumbnailPath (for main product images)
result, err := _i.Repo.FindByThumbnailPath(filename)
if err == nil && result != nil && result.ThumbnailPath != nil && *result.ThumbnailPath != "" {
// Check if the filename matches the thumbnail
if strings.Contains(*result.ThumbnailPath, filename) {
objectName = *result.ThumbnailPath
found = true
}
}
// If not found in ThumbnailPath, search in Colors JSON field
if !found {
// Create a query request with large limit to search all products
queryReq := request.ProductsQueryRequest{
Pagination: &paginator.Pagination{
Page: 1,
Limit: 1000, // Large limit to search all products
},
}
allProducts, _, err := _i.Repo.GetAll(queryReq)
if err == nil {
for _, product := range allProducts {
if product.Colors != nil && *product.Colors != "" {
var rawColors []struct {
Name string `json:"name"`
ImagePath *string `json:"image_path"`
}
if err := json.Unmarshal([]byte(*product.Colors), &rawColors); err == nil {
for _, color := range rawColors {
if color.ImagePath != nil && strings.Contains(*color.ImagePath, filename) {
objectName = *color.ImagePath
found = true
break
}
}
}
if found {
break
}
}
}
}
}
if !found || objectName == "" {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": true,
"msg": "Product file not found",
})
}
ctx := context.Background()
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Products: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", "Products: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]
}
func (_i *productsService) Approve(id uint, authToken string) (product *response.ProductsResponse, 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 product (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(
"products",
id,
&statusApprove, // ✅ pointer
"approve",
&userID,
nil,
)
if err != nil {
_i.Log.Error().Err(err).Msg("Failed to save approval history")
}
// Get updated product data
productEntity, err := _i.Repo.FindOne(id)
if err != nil {
return
}
if productEntity == nil {
err = errors.New("product not found")
return
}
host := _i.Cfg.App.Domain
product = mapper.ProductsResponseMapper(productEntity, host)
return
}
func (_i *productsService) Reject(id uint, authToken string, message *string) (product *response.ProductsResponse, 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 product (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(
"productss",
id,
&statusReject, // ✅ pointer
"reject",
&userID,
message,
)
if err != nil {
_i.Log.Error().Err(err).Msg("Failed to save rejection history")
}
// Get updated product data
productEntity, err := _i.Repo.FindOne(id)
if err != nil {
return
}
if productEntity == nil {
err = errors.New("product not found")
return
}
host := _i.Cfg.App.Domain
product = mapper.ProductsResponseMapper(productEntity, host)
return
}
func (_i *productsService) Comment(
id uint,
authToken string,
message *string,
) (banner *response.ProductsResponse, 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.ProductsResponseMapper(bannerEntity, host)
return
}