package service import ( "context" "errors" "fmt" "io" "narasi-ahli-be/app/database/entity" "narasi-ahli-be/app/module/ebooks/mapper" "narasi-ahli-be/app/module/ebooks/repository" "narasi-ahli-be/app/module/ebooks/request" "narasi-ahli-be/app/module/ebooks/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" ) // EbooksService type ebooksService struct { Repo repository.EbooksRepository WishlistRepo repository.EbookWishlistsRepository PurchaseRepo repository.EbookPurchasesRepository Log zerolog.Logger Cfg *config.Config UsersRepo usersRepository.UsersRepository MinioStorage *minioStorage.MinioStorage } // EbooksService define interface of IEbooksService type EbooksService interface { All(req request.EbooksQueryRequest) (ebooks []*response.EbooksResponse, paging paginator.Pagination, err error) Show(id uint) (ebook *response.EbooksResponse, err error) ShowBySlug(slug string) (ebook *response.EbooksResponse, err error) Save(req request.EbooksCreateRequest, authToken string) (ebook *entity.Ebooks, err error) SavePdfFile(c *fiber.Ctx, ebookId uint) (err error) SaveThumbnail(c *fiber.Ctx, ebookId uint) (err error) Update(id uint, req request.EbooksUpdateRequest) (err error) Delete(id uint) error SummaryStats(authToken string) (summaryStats *response.EbookSummaryStats, err error) DownloadPdf(c *fiber.Ctx, ebookId uint) error } // NewEbooksService init EbooksService func NewEbooksService( repo repository.EbooksRepository, wishlistRepo repository.EbookWishlistsRepository, purchaseRepo repository.EbookPurchasesRepository, log zerolog.Logger, cfg *config.Config, usersRepo usersRepository.UsersRepository, minioStorage *minioStorage.MinioStorage) EbooksService { return &ebooksService{ Repo: repo, WishlistRepo: wishlistRepo, PurchaseRepo: purchaseRepo, Log: log, UsersRepo: usersRepo, MinioStorage: minioStorage, Cfg: cfg, } } // All implement interface of EbooksService func (_i *ebooksService) All(req request.EbooksQueryRequest) (ebooks []*response.EbooksResponse, paging paginator.Pagination, err error) { ebooksData, paging, err := _i.Repo.GetAll(req) if err != nil { return nil, paging, err } ebooks = mapper.ToEbooksResponseList(ebooksData) return ebooks, paging, nil } // Show implement interface of EbooksService func (_i *ebooksService) Show(id uint) (ebook *response.EbooksResponse, err error) { ebookData, err := _i.Repo.FindOne(id) if err != nil { return nil, err } ebook = mapper.ToEbooksResponse(ebookData) return ebook, nil } // ShowBySlug implement interface of EbooksService func (_i *ebooksService) ShowBySlug(slug string) (ebook *response.EbooksResponse, err error) { ebookData, err := _i.Repo.FindBySlug(slug) if err != nil { return nil, err } ebook = mapper.ToEbooksResponse(ebookData) return ebook, nil } // Save implement interface of EbooksService func (_i *ebooksService) Save(req request.EbooksCreateRequest, authToken string) (ebook *entity.Ebooks, err error) { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user == nil { return nil, errors.New("user not found") } ebookEntity := req.ToEntity() ebookEntity.AuthorId = user.ID ebookEntity.CreatedById = &user.ID ebookData, err := _i.Repo.Create(ebookEntity) if err != nil { return nil, err } return ebookData, nil } // SavePdfFile implement interface of EbooksService func (_i *ebooksService) SavePdfFile(c *fiber.Ctx, ebookId uint) (err error) { // Get the uploaded file file, err := c.FormFile("file") if err != nil { return errors.New("file is required") } // Validate file type contentType := file.Header.Get("Content-Type") if contentType != "application/pdf" { return errors.New("only PDF files are allowed") } // Validate file size (max 50MB) if file.Size > 50*1024*1024 { return errors.New("file size must be less than 50MB") } // Generate unique filename ext := filepath.Ext(file.Filename) filename := fmt.Sprintf("ebook_%d_%d%s", ebookId, time.Now().Unix(), ext) // Upload to MinIO fileReader, err := file.Open() if err != nil { return err } defer fileReader.Close() bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName minioClient, err := _i.MinioStorage.ConnectMinio() if err != nil { return err } objectName := fmt.Sprintf("ebooks/pdfs/%s", filename) _, err = minioClient.PutObject( context.Background(), bucketName, objectName, fileReader, file.Size, minio.PutObjectOptions{ ContentType: contentType, }, ) if err != nil { return err } // Update ebook record with file info ebookUpdate := &entity.Ebooks{ PdfFilePath: &objectName, PdfFileName: &filename, PdfFileSize: &file.Size, } err = _i.Repo.UpdateSkipNull(ebookId, ebookUpdate) if err != nil { return err } return nil } // SaveThumbnail implement interface of EbooksService func (_i *ebooksService) SaveThumbnail(c *fiber.Ctx, ebookId uint) (err error) { // Get the uploaded file file, err := c.FormFile("file") if err != nil { return errors.New("file is required") } // Validate file type contentType := file.Header.Get("Content-Type") if !strings.HasPrefix(contentType, "image/") { return errors.New("only image files are allowed") } // Validate file size (max 5MB) if file.Size > 5*1024*1024 { return errors.New("file size must be less than 5MB") } // Generate unique filename ext := filepath.Ext(file.Filename) filename := fmt.Sprintf("ebook_thumbnail_%d_%d%s", ebookId, time.Now().Unix(), ext) // Upload to MinIO fileReader, err := file.Open() if err != nil { return err } defer fileReader.Close() bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName minioClient, err := _i.MinioStorage.ConnectMinio() if err != nil { return err } objectName := fmt.Sprintf("ebooks/thumbnails/%s", filename) _, err = minioClient.PutObject( context.Background(), bucketName, objectName, fileReader, file.Size, minio.PutObjectOptions{ ContentType: contentType, }, ) if err != nil { return err } // Update ebook record with thumbnail info ebookUpdate := &entity.Ebooks{ ThumbnailPath: &objectName, ThumbnailName: &filename, } err = _i.Repo.UpdateSkipNull(ebookId, ebookUpdate) if err != nil { return err } return nil } // Update implement interface of EbooksService func (_i *ebooksService) Update(id uint, req request.EbooksUpdateRequest) (err error) { ebookEntity := req.ToEntity() err = _i.Repo.Update(id, ebookEntity) if err != nil { return err } return nil } // Delete implement interface of EbooksService func (_i *ebooksService) Delete(id uint) error { err := _i.Repo.Delete(id) if err != nil { return err } return nil } // SummaryStats implement interface of EbooksService func (_i *ebooksService) SummaryStats(authToken string) (summaryStats *response.EbookSummaryStats, err error) { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user == nil { return nil, errors.New("user not found") } summaryStats, err = _i.Repo.SummaryStats(user.ID) if err != nil { return nil, err } return summaryStats, nil } // DownloadPdf implement interface of EbooksService func (_i *ebooksService) DownloadPdf(c *fiber.Ctx, ebookId uint) error { // Get ebook data ebook, err := _i.Repo.FindOne(ebookId) if err != nil { return err } if ebook.PdfFilePath == nil { return errors.New("PDF file not found") } // Check if user has purchased this ebook authToken := c.Get("Authorization") user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user == nil { return errors.New("user not found") } purchase, err := _i.PurchaseRepo.FindByBuyerAndEbook(user.ID, ebookId) if err != nil { return errors.New("you must purchase this ebook before downloading") } if purchase.PaymentStatus == nil || *purchase.PaymentStatus != "paid" { return errors.New("payment not completed") } // Get file from MinIO bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName minioClient, err := _i.MinioStorage.ConnectMinio() if err != nil { return err } object, err := minioClient.GetObject( context.Background(), bucketName, *ebook.PdfFilePath, minio.GetObjectOptions{}, ) if err != nil { return err } defer object.Close() // Read file content fileContent, err := io.ReadAll(object) if err != nil { return err } // Update download count err = _i.Repo.UpdateDownloadCount(ebookId) if err != nil { _i.Log.Error().Err(err).Msg("Failed to update download count") } err = _i.PurchaseRepo.UpdateDownloadCount(purchase.ID) if err != nil { _i.Log.Error().Err(err).Msg("Failed to update purchase download count") } // Set response headers c.Set("Content-Type", "application/pdf") c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", *ebook.PdfFileName)) c.Set("Content-Length", strconv.Itoa(len(fileContent))) return c.Send(fileContent) }