feat: add ebook features

This commit is contained in:
hanif salafi 2025-09-21 23:10:08 +07:00
parent 9788d5c2f9
commit 21c68096c3
29 changed files with 8775 additions and 0 deletions

View File

@ -0,0 +1,26 @@
package entity
import (
users "narasi-ahli-be/app/database/entity/users"
"time"
)
type EbookPurchases struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
BuyerId uint `json:"buyer_id" gorm:"type:int4"`
Buyer *users.Users `json:"buyer" gorm:"foreignKey:BuyerId;references:ID"`
EbookId uint `json:"ebook_id" gorm:"type:int4"`
Ebook *Ebooks `json:"ebook" gorm:"foreignKey:EbookId;references:ID"`
PurchasePrice float64 `json:"purchase_price" gorm:"type:decimal(10,2)"`
PaymentMethod *string `json:"payment_method" gorm:"type:varchar"`
PaymentStatus *string `json:"payment_status" gorm:"type:varchar;default:'pending'"`
TransactionId *string `json:"transaction_id" gorm:"type:varchar"`
PaymentProof *string `json:"payment_proof" gorm:"type:varchar"`
PaymentDate *time.Time `json:"payment_date" gorm:"type:timestamp"`
DownloadCount *int `json:"download_count" gorm:"type:int4;default:0"`
LastDownloadAt *time.Time `json:"last_download_at" gorm:"type:timestamp"`
StatusId *int `json:"status_id" gorm:"type:int4;default:1"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,24 @@
package entity
import (
users "narasi-ahli-be/app/database/entity/users"
"time"
)
type EbookRatings struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
UserId uint `json:"user_id" gorm:"type:int4"`
User *users.Users `json:"user" gorm:"foreignKey:UserId;references:ID"`
EbookId uint `json:"ebook_id" gorm:"type:int4"`
Ebook *Ebooks `json:"ebook" gorm:"foreignKey:EbookId;references:ID"`
PurchaseId uint `json:"purchase_id" gorm:"type:int4"`
Purchase *EbookPurchases `json:"purchase" gorm:"foreignKey:PurchaseId;references:ID"`
Rating int `json:"rating" gorm:"type:int4;check:rating >= 1 AND rating <= 5"`
Review *string `json:"review" gorm:"type:text"`
IsAnonymous *bool `json:"is_anonymous" gorm:"type:bool;default:false"`
IsVerified *bool `json:"is_verified" gorm:"type:bool;default:true"`
StatusId *int `json:"status_id" gorm:"type:int4;default:1"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,16 @@
package entity
import (
users "narasi-ahli-be/app/database/entity/users"
"time"
)
type EbookWishlists struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
UserId uint `json:"user_id" gorm:"type:int4"`
User *users.Users `json:"user" gorm:"foreignKey:UserId;references:ID"`
EbookId uint `json:"ebook_id" gorm:"type:int4"`
Ebook *Ebooks `json:"ebook" gorm:"foreignKey:EbookId;references:ID"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,40 @@
package entity
import (
users "narasi-ahli-be/app/database/entity/users"
"time"
)
type Ebooks struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Title string `json:"title" gorm:"type:varchar"`
Slug string `json:"slug" gorm:"type:varchar"`
Description string `json:"description" gorm:"type:varchar"`
Price float64 `json:"price" gorm:"type:decimal(10,2)"`
PdfFilePath *string `json:"pdf_file_path" gorm:"type:varchar"`
PdfFileName *string `json:"pdf_file_name" gorm:"type:varchar"`
PdfFileSize *int64 `json:"pdf_file_size" gorm:"type:int8"`
ThumbnailPath *string `json:"thumbnail_path" gorm:"type:varchar"`
ThumbnailName *string `json:"thumbnail_name" gorm:"type:varchar"`
AuthorId uint `json:"author_id" gorm:"type:int4"`
Author *users.Users `json:"author" gorm:"foreignKey:AuthorId;references:ID"`
Category *string `json:"category" gorm:"type:varchar"`
Tags *string `json:"tags" gorm:"type:varchar"`
PageCount *int `json:"page_count" gorm:"type:int4"`
Language *string `json:"language" gorm:"type:varchar;default:'id'"`
Isbn *string `json:"isbn" gorm:"type:varchar"`
Publisher *string `json:"publisher" gorm:"type:varchar"`
PublishedYear *int `json:"published_year" gorm:"type:int4"`
DownloadCount *int `json:"download_count" gorm:"type:int4;default:0"`
PurchaseCount *int `json:"purchase_count" gorm:"type:int4;default:0"`
WishlistCount *int `json:"wishlist_count" gorm:"type:int4;default:0"`
Rating *float64 `json:"rating" gorm:"type:decimal(3,2);default:0"`
ReviewCount *int `json:"review_count" gorm:"type:int4;default:0"`
StatusId *int `json:"status_id" gorm:"type:int4;default:1"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
IsPublished *bool `json:"is_published" gorm:"type:bool;default:false"`
PublishedAt *time.Time `json:"published_at" gorm:"type:timestamp"`
CreatedById *uint `json:"created_by_id" gorm:"type:int4"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -125,6 +125,12 @@ func Models() []interface{} {
entity.AIChatSessions{},
entity.AIChatMessages{},
entity.AIChatLogs{},
// Ebook entities
entity.Ebooks{},
entity.EbookWishlists{},
entity.EbookPurchases{},
entity.EbookRatings{},
}
}

View File

@ -0,0 +1,210 @@
package controller
import (
"narasi-ahli-be/app/module/ebooks/service"
usersRepository "narasi-ahli-be/app/module/users/repository"
"narasi-ahli-be/utils/paginator"
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog"
utilRes "narasi-ahli-be/utils/response"
utilVal "narasi-ahli-be/utils/validator"
)
type ebookPurchasesController struct {
purchaseService service.EbookPurchasesService
usersRepo usersRepository.UsersRepository
Log zerolog.Logger
}
type EbookPurchasesController interface {
GetByBuyerId(c *fiber.Ctx) error
PurchaseEbook(c *fiber.Ctx) error
UpdatePaymentStatus(c *fiber.Ctx) error
GetPurchaseById(c *fiber.Ctx) error
ConfirmPayment(c *fiber.Ctx) error
}
func NewEbookPurchasesController(purchaseService service.EbookPurchasesService, usersRepo usersRepository.UsersRepository, log zerolog.Logger) EbookPurchasesController {
return &ebookPurchasesController{
purchaseService: purchaseService,
usersRepo: usersRepo,
Log: log,
}
}
// GetByBuyerId Purchase
// @Summary Get user purchases
// @Description API for getting user purchases
// @Tags Ebook Purchase
// @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebooks/purchases [get]
func (_i *ebookPurchasesController) GetByBuyerId(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
authToken := c.Get("Authorization")
purchasesData, paging, err := _i.purchaseService.GetByBuyerId(authToken, paginate)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Purchases successfully retrieved"},
Data: purchasesData,
Meta: paging,
})
}
// PurchaseEbook Purchase
// @Summary Purchase ebook
// @Description API for purchasing ebook
// @Tags Ebook Purchase
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param ebookId path int true "Ebook ID"
// @Param paymentMethod query string true "Payment Method"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebooks/purchase/{ebookId} [post]
func (_i *ebookPurchasesController) PurchaseEbook(c *fiber.Ctx) error {
ebookId, err := strconv.ParseUint(c.Params("ebookId"), 10, 0)
if err != nil {
return err
}
paymentMethod := c.Query("paymentMethod")
if paymentMethod == "" {
return utilRes.Resp(c, utilRes.Response{
Success: false,
Messages: utilRes.Messages{"Payment method is required"},
})
}
authToken := c.Get("Authorization")
purchase, err := _i.purchaseService.PurchaseEbook(authToken, uint(ebookId), paymentMethod)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Ebook purchase successfully created"},
Data: purchase,
})
}
// UpdatePaymentStatus Purchase
// @Summary Update payment status
// @Description API for updating payment status
// @Tags Ebook Purchase
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param id path int true "Purchase ID"
// @Param payload body PaymentStatusUpdateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebooks/purchases/{id}/payment [put]
func (_i *ebookPurchasesController) UpdatePaymentStatus(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
req := new(PaymentStatusUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err = _i.purchaseService.UpdatePaymentStatus(uint(id), req.PaymentStatus, req.TransactionId, req.PaymentProof)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Payment status successfully updated"},
})
}
// GetPurchaseById Purchase
// @Summary Get purchase by ID
// @Description API for getting purchase by ID
// @Tags Ebook Purchase
// @Security Bearer
// @Param id path int true "Purchase ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebooks/purchases/{id} [get]
func (_i *ebookPurchasesController) GetPurchaseById(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
purchaseData, err := _i.purchaseService.GetPurchaseById(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Purchase successfully retrieved"},
Data: purchaseData,
})
}
// ConfirmPayment Purchase
// @Summary Confirm payment
// @Description API for confirming payment
// @Tags Ebook Purchase
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param id path int true "Purchase ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebooks/purchases/{id}/confirm [put]
func (_i *ebookPurchasesController) ConfirmPayment(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.purchaseService.ConfirmPayment(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Payment successfully confirmed"},
})
}
// PaymentStatusUpdateRequest struct for payment status update
type PaymentStatusUpdateRequest struct {
PaymentStatus string `json:"paymentStatus" validate:"required"`
TransactionId string `json:"transactionId"`
PaymentProof string `json:"paymentProof"`
}

View File

@ -0,0 +1,313 @@
package controller
import (
"narasi-ahli-be/app/module/ebooks/request"
"narasi-ahli-be/app/module/ebooks/service"
"narasi-ahli-be/utils/paginator"
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog"
utilRes "narasi-ahli-be/utils/response"
utilVal "narasi-ahli-be/utils/validator"
)
type ebookRatingsController struct {
ratingsService service.EbookRatingsService
Log zerolog.Logger
}
type EbookRatingsController interface {
GetAll(c *fiber.Ctx) error
GetByEbookId(c *fiber.Ctx) error
GetByUserId(c *fiber.Ctx) error
GetEbookRatingSummary(c *fiber.Ctx) error
Create(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
GetRatingStats(c *fiber.Ctx) error
}
func NewEbookRatingsController(ratingsService service.EbookRatingsService, log zerolog.Logger) EbookRatingsController {
return &ebookRatingsController{
ratingsService: ratingsService,
Log: log,
}
}
// GetAll Ratings
// @Summary Get all Ebook Ratings
// @Description API for getting all Ebook Ratings
// @Tags Ebook Ratings
// @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param req query request.EbookRatingsQueryRequest false "query parameters"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebook-ratings [get]
func (_i *ebookRatingsController) GetAll(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
reqContext := request.EbookRatingsQueryRequestContext{
EbookId: c.Query("ebookId"),
UserId: c.Query("userId"),
Rating: c.Query("rating"),
IsVerified: c.Query("isVerified"),
StatusId: c.Query("statusId"),
}
req := reqContext.ToParamRequest()
req.Pagination = paginate
ratingsData, paging, err := _i.ratingsService.GetAll(req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Ebook ratings list successfully retrieved"},
Data: ratingsData,
Meta: paging,
})
}
// GetByEbookId Ratings
// @Summary Get ratings by ebook ID
// @Description API for getting ratings by ebook ID
// @Tags Ebook Ratings
// @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param id path int true "Ebook ID"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebook-ratings/ebook/{id} [get]
func (_i *ebookRatingsController) GetByEbookId(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
ratingsData, paging, err := _i.ratingsService.GetByEbookId(uint(id), paginate)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Ebook ratings successfully retrieved"},
Data: ratingsData,
Meta: paging,
})
}
// GetByUserId Ratings
// @Summary Get user ratings
// @Description API for getting user ratings
// @Tags Ebook Ratings
// @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebook-ratings/user [get]
func (_i *ebookRatingsController) GetByUserId(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
authToken := c.Get("Authorization")
ratingsData, paging, err := _i.ratingsService.GetByUserId(authToken, paginate)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"User ratings successfully retrieved"},
Data: ratingsData,
Meta: paging,
})
}
// GetEbookRatingSummary Ratings
// @Summary Get ebook rating summary
// @Description API for getting ebook rating summary with stats and recent reviews
// @Tags Ebook Ratings
// @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param id path int true "Ebook ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebook-ratings/summary/{id} [get]
func (_i *ebookRatingsController) GetEbookRatingSummary(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
summaryData, err := _i.ratingsService.GetEbookRatingSummary(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Ebook rating summary successfully retrieved"},
Data: summaryData,
})
}
// Create Rating
// @Summary Create ebook rating
// @Description API for creating ebook rating
// @Tags Ebook Ratings
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param payload body request.EbookRatingsCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebook-ratings [post]
func (_i *ebookRatingsController) Create(c *fiber.Ctx) error {
req := new(request.EbookRatingsCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
authToken := c.Get("Authorization")
dataResult, err := _i.ratingsService.Create(*req, authToken)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Ebook rating successfully created"},
Data: dataResult,
})
}
// Update Rating
// @Summary Update ebook rating
// @Description API for updating ebook rating
// @Tags Ebook Ratings
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param payload body request.EbookRatingsUpdateRequest true "Required payload"
// @Param id path int true "Rating ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebook-ratings/{id} [put]
func (_i *ebookRatingsController) Update(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
req := new(request.EbookRatingsUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
authToken := c.Get("Authorization")
err = _i.ratingsService.Update(uint(id), *req, authToken)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Ebook rating successfully updated"},
})
}
// Delete Rating
// @Summary Delete ebook rating
// @Description API for deleting ebook rating
// @Tags Ebook Ratings
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param id path int true "Rating ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebook-ratings/{id} [delete]
func (_i *ebookRatingsController) Delete(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
authToken := c.Get("Authorization")
err = _i.ratingsService.Delete(uint(id), authToken)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Ebook rating successfully deleted"},
})
}
// GetRatingStats Ratings
// @Summary Get ebook rating statistics
// @Description API for getting ebook rating statistics
// @Tags Ebook Ratings
// @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param id path int true "Ebook ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebook-ratings/stats/{id} [get]
func (_i *ebookRatingsController) GetRatingStats(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
statsData, err := _i.ratingsService.GetRatingStats(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Ebook rating statistics successfully retrieved"},
Data: statsData,
})
}

View File

@ -0,0 +1,160 @@
package controller
import (
"narasi-ahli-be/app/module/ebooks/service"
usersRepository "narasi-ahli-be/app/module/users/repository"
"narasi-ahli-be/utils/paginator"
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog"
utilRes "narasi-ahli-be/utils/response"
)
type ebookWishlistsController struct {
wishlistService service.EbookWishlistsService
usersRepo usersRepository.UsersRepository
Log zerolog.Logger
}
type EbookWishlistsController interface {
GetByUserId(c *fiber.Ctx) error
AddToWishlist(c *fiber.Ctx) error
RemoveFromWishlist(c *fiber.Ctx) error
IsInWishlist(c *fiber.Ctx) error
}
func NewEbookWishlistsController(wishlistService service.EbookWishlistsService, usersRepo usersRepository.UsersRepository, log zerolog.Logger) EbookWishlistsController {
return &ebookWishlistsController{
wishlistService: wishlistService,
usersRepo: usersRepo,
Log: log,
}
}
// GetByUserId Wishlist
// @Summary Get user wishlist
// @Description API for getting user wishlist
// @Tags Ebook Wishlist
// @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebooks/wishlist [get]
func (_i *ebookWishlistsController) GetByUserId(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
authToken := c.Get("Authorization")
wishlistsData, paging, err := _i.wishlistService.GetByUserId(authToken, paginate)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Wishlist successfully retrieved"},
Data: wishlistsData,
Meta: paging,
})
}
// AddToWishlist Wishlist
// @Summary Add ebook to wishlist
// @Description API for adding ebook to wishlist
// @Tags Ebook Wishlist
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param ebookId path int true "Ebook ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebooks/wishlist/{ebookId} [post]
func (_i *ebookWishlistsController) AddToWishlist(c *fiber.Ctx) error {
ebookId, err := strconv.ParseUint(c.Params("ebookId"), 10, 0)
if err != nil {
return err
}
authToken := c.Get("Authorization")
err = _i.wishlistService.AddToWishlist(authToken, uint(ebookId))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Ebook successfully added to wishlist"},
})
}
// RemoveFromWishlist Wishlist
// @Summary Remove ebook from wishlist
// @Description API for removing ebook from wishlist
// @Tags Ebook Wishlist
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param ebookId path int true "Ebook ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebooks/wishlist/{ebookId} [delete]
func (_i *ebookWishlistsController) RemoveFromWishlist(c *fiber.Ctx) error {
ebookId, err := strconv.ParseUint(c.Params("ebookId"), 10, 0)
if err != nil {
return err
}
authToken := c.Get("Authorization")
err = _i.wishlistService.RemoveFromWishlist(authToken, uint(ebookId))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Ebook successfully removed from wishlist"},
})
}
// IsInWishlist Wishlist
// @Summary Check if ebook is in wishlist
// @Description API for checking if ebook is in wishlist
// @Tags Ebook Wishlist
// @Security Bearer
// @Param ebookId path int true "Ebook ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebooks/wishlist/check/{ebookId} [get]
func (_i *ebookWishlistsController) IsInWishlist(c *fiber.Ctx) error {
ebookId, err := strconv.ParseUint(c.Params("ebookId"), 10, 0)
if err != nil {
return err
}
authToken := c.Get("Authorization")
isInWishlist, err := _i.wishlistService.IsInWishlist(authToken, uint(ebookId))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Wishlist status successfully retrieved"},
Data: map[string]bool{"isInWishlist": isInWishlist},
})
}

View File

@ -0,0 +1,350 @@
package controller
import (
"narasi-ahli-be/app/module/ebooks/request"
"narasi-ahli-be/app/module/ebooks/service"
"narasi-ahli-be/utils/paginator"
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog"
utilRes "narasi-ahli-be/utils/response"
utilVal "narasi-ahli-be/utils/validator"
)
type ebooksController struct {
ebooksService service.EbooksService
Log zerolog.Logger
}
type EbooksController interface {
All(c *fiber.Ctx) error
Show(c *fiber.Ctx) error
ShowBySlug(c *fiber.Ctx) error
Save(c *fiber.Ctx) error
SavePdfFile(c *fiber.Ctx) error
SaveThumbnail(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
SummaryStats(c *fiber.Ctx) error
DownloadPdf(c *fiber.Ctx) error
}
func NewEbooksController(ebooksService service.EbooksService, log zerolog.Logger) EbooksController {
return &ebooksController{
ebooksService: ebooksService,
Log: log,
}
}
// All Ebooks
// @Summary Get all Ebooks
// @Description API for getting all Ebooks
// @Tags Ebooks
// @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param req query request.EbooksQueryRequest false "query parameters"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebooks [get]
func (_i *ebooksController) All(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
reqContext := request.EbooksQueryRequestContext{
Title: c.Query("title"),
Description: c.Query("description"),
AuthorId: c.Query("authorId"),
Category: c.Query("category"),
Tags: c.Query("tags"),
MinPrice: c.Query("minPrice"),
MaxPrice: c.Query("maxPrice"),
StatusId: c.Query("statusId"),
IsPublished: c.Query("isPublished"),
}
req := reqContext.ToParamRequest()
req.Pagination = paginate
ebooksData, paging, err := _i.ebooksService.All(req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Ebooks list successfully retrieved"},
Data: ebooksData,
Meta: paging,
})
}
// Show Ebook
// @Summary Get one Ebook
// @Description API for getting one Ebook
// @Tags Ebooks
// @Security Bearer
// @Param id path int true "Ebook ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebooks/{id} [get]
func (_i *ebooksController) Show(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
ebookData, err := _i.ebooksService.Show(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Ebook successfully retrieved"},
Data: ebookData,
})
}
// ShowBySlug Ebook
// @Summary Get one Ebook by slug
// @Description API for getting one Ebook by slug
// @Tags Ebooks
// @Security Bearer
// @Param slug path string true "Ebook Slug"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebooks/slug/{slug} [get]
func (_i *ebooksController) ShowBySlug(c *fiber.Ctx) error {
slug := c.Params("slug")
ebookData, err := _i.ebooksService.ShowBySlug(slug)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Ebook successfully retrieved"},
Data: ebookData,
})
}
// Save Ebook
// @Summary Create Ebook
// @Description API for create Ebook
// @Tags Ebooks
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param payload body request.EbooksCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebooks [post]
func (_i *ebooksController) Save(c *fiber.Ctx) error {
req := new(request.EbooksCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
authToken := c.Get("Authorization")
dataResult, err := _i.ebooksService.Save(*req, authToken)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Ebook successfully created"},
Data: dataResult,
})
}
// SavePdfFile Ebook
// @Summary Save PDF File Ebook
// @Description API for Save PDF File of Ebook
// @Tags Ebooks
// @Security Bearer
// @Produce json
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param file formData file true "Upload PDF file"
// @Param id path int true "Ebook ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebooks/pdf/{id} [post]
func (_i *ebooksController) SavePdfFile(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.ebooksService.SavePdfFile(c, uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"PDF file of Ebook successfully uploaded"},
})
}
// SaveThumbnail Ebook
// @Summary Save Thumbnail Ebook
// @Description API for Save Thumbnail of Ebook
// @Tags Ebooks
// @Security Bearer
// @Produce json
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param files formData file true "Upload thumbnail"
// @Param id path int true "Ebook ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebooks/thumbnail/{id} [post]
func (_i *ebooksController) SaveThumbnail(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.ebooksService.SaveThumbnail(c, uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Thumbnail of Ebook successfully uploaded"},
})
}
// Update Ebook
// @Summary Update Ebook
// @Description API for update Ebook
// @Tags Ebooks
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param payload body request.EbooksUpdateRequest true "Required payload"
// @Param id path int true "Ebook ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebooks/{id} [put]
func (_i *ebooksController) Update(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
req := new(request.EbooksUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err = _i.ebooksService.Update(uint(id), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Ebook successfully updated"},
})
}
// Delete Ebook
// @Summary Delete Ebook
// @Description API for delete Ebook
// @Tags Ebooks
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param id path int true "Ebook ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebooks/{id} [delete]
func (_i *ebooksController) Delete(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.ebooksService.Delete(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Ebook successfully deleted"},
})
}
// SummaryStats Ebook
// @Summary SummaryStats Ebook
// @Description API for Summary Stats of Ebook
// @Tags Ebooks
// @Security Bearer
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebooks/statistic/summary [get]
func (_i *ebooksController) SummaryStats(c *fiber.Ctx) error {
authToken := c.Get("Authorization")
response, err := _i.ebooksService.SummaryStats(authToken)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Summary Stats of Ebooks successfully retrieved"},
Data: response,
})
}
// DownloadPdf Ebook
// @Summary Download PDF Ebook
// @Description API for Download PDF of Ebook
// @Tags Ebooks
// @Security Bearer
// @Param id path int true "Ebook ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ebooks/download/{id} [get]
func (_i *ebooksController) DownloadPdf(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.ebooksService.DownloadPdf(c, uint(id))
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,125 @@
package ebooks
import (
"narasi-ahli-be/app/module/ebooks/controller"
"narasi-ahli-be/app/module/ebooks/repository"
"narasi-ahli-be/app/module/ebooks/service"
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
)
// EbooksRouter struct of EbooksRouter
type EbooksRouter struct {
App fiber.Router
Controller *Controller
}
// Controller struct for all controllers
type Controller struct {
Ebooks controller.EbooksController
Wishlists controller.EbookWishlistsController
Purchases controller.EbookPurchasesController
Ratings controller.EbookRatingsController
}
// NewEbooksModule register bulky of Ebooks module
var NewEbooksModule = fx.Options(
// register repository of Ebooks module
fx.Provide(repository.NewEbooksRepository),
fx.Provide(repository.NewEbookWishlistsRepository),
fx.Provide(repository.NewEbookPurchasesRepository),
fx.Provide(repository.NewEbookRatingsRepository),
// register service of Ebooks module
fx.Provide(service.NewEbooksService),
fx.Provide(service.NewEbookWishlistsService),
fx.Provide(service.NewEbookPurchasesService),
fx.Provide(service.NewEbookRatingsService),
// register controller of Ebooks module
fx.Provide(controller.NewEbooksController),
fx.Provide(controller.NewEbookWishlistsController),
fx.Provide(controller.NewEbookPurchasesController),
fx.Provide(controller.NewEbookRatingsController),
fx.Provide(NewController),
// register router of Ebooks module
fx.Provide(NewEbooksRouter),
)
// NewController init Controller
func NewController(
ebooksController controller.EbooksController,
wishlistsController controller.EbookWishlistsController,
purchasesController controller.EbookPurchasesController,
ratingsController controller.EbookRatingsController,
) *Controller {
return &Controller{
Ebooks: ebooksController,
Wishlists: wishlistsController,
Purchases: purchasesController,
Ratings: ratingsController,
}
}
// NewEbooksRouter init EbooksRouter
func NewEbooksRouter(fiber *fiber.App, controller *Controller) *EbooksRouter {
return &EbooksRouter{
App: fiber,
Controller: controller,
}
}
// RegisterEbooksRoutes register routes of Ebooks module
func (_i *EbooksRouter) RegisterEbooksRoutes() {
// define controllers
ebooksController := _i.Controller.Ebooks
wishlistsController := _i.Controller.Wishlists
purchasesController := _i.Controller.Purchases
ratingsController := _i.Controller.Ratings
// define routes
_i.App.Route("/ebooks", func(router fiber.Router) {
// Ebook CRUD routes
router.Get("/", ebooksController.All)
router.Get("/slug/:slug", ebooksController.ShowBySlug)
router.Get("/:id", ebooksController.Show)
router.Post("/", ebooksController.Save)
router.Put("/:id", ebooksController.Update)
router.Delete("/:id", ebooksController.Delete)
// File upload routes
router.Post("/pdf/:id", ebooksController.SavePdfFile)
router.Post("/thumbnail/:id", ebooksController.SaveThumbnail)
// Download route
router.Get("/download/:id", ebooksController.DownloadPdf)
// Statistics route
router.Get("/statistic/summary", ebooksController.SummaryStats)
// Wishlist routes
router.Get("/wishlist", wishlistsController.GetByUserId)
router.Post("/wishlist/:ebookId", wishlistsController.AddToWishlist)
router.Delete("/wishlist/:ebookId", wishlistsController.RemoveFromWishlist)
router.Get("/wishlist/check/:ebookId", wishlistsController.IsInWishlist)
// Purchase routes
router.Get("/purchases", purchasesController.GetByBuyerId)
router.Post("/purchase/:ebookId", purchasesController.PurchaseEbook)
router.Get("/purchases/:id", purchasesController.GetPurchaseById)
router.Put("/purchases/:id/payment", purchasesController.UpdatePaymentStatus)
router.Put("/purchases/:id/confirm", purchasesController.ConfirmPayment)
// Rating routes
router.Get("/ratings", ratingsController.GetAll)
router.Post("/ratings", ratingsController.Create)
router.Put("/ratings/:id", ratingsController.Update)
router.Delete("/ratings/:id", ratingsController.Delete)
router.Get("/ratings/ebook/:id", ratingsController.GetByEbookId)
router.Get("/ratings/user", ratingsController.GetByUserId)
router.Get("/ratings/summary/:id", ratingsController.GetEbookRatingSummary)
router.Get("/ratings/stats/:id", ratingsController.GetRatingStats)
})
}

View File

@ -0,0 +1,94 @@
package mapper
import (
"narasi-ahli-be/app/database/entity"
ebookRatingsResponse "narasi-ahli-be/app/module/ebooks/response"
)
func ToEbookRatingsResponse(rating *entity.EbookRatings) *ebookRatingsResponse.EbookRatingsResponse {
if rating == nil {
return nil
}
var userName, userEmail *string
if rating.User != nil {
if rating.IsAnonymous != nil && *rating.IsAnonymous {
userName = stringPtr("Anonymous")
userEmail = nil
} else {
userName = &rating.User.Fullname
userEmail = &rating.User.Email
}
}
var ebookTitle *string
if rating.Ebook != nil {
ebookTitle = &rating.Ebook.Title
}
return &ebookRatingsResponse.EbookRatingsResponse{
ID: rating.ID,
UserId: rating.UserId,
UserName: userName,
UserEmail: userEmail,
EbookId: rating.EbookId,
EbookTitle: ebookTitle,
PurchaseId: rating.PurchaseId,
Rating: rating.Rating,
Review: rating.Review,
IsAnonymous: rating.IsAnonymous,
IsVerified: rating.IsVerified,
StatusId: rating.StatusId,
IsActive: rating.IsActive,
CreatedAt: rating.CreatedAt,
UpdatedAt: rating.UpdatedAt,
}
}
func ToEbookRatingsResponseList(ratings []*entity.EbookRatings) []*ebookRatingsResponse.EbookRatingsResponse {
if ratings == nil {
return nil
}
var responses []*ebookRatingsResponse.EbookRatingsResponse
for _, rating := range ratings {
responses = append(responses, ToEbookRatingsResponse(rating))
}
return responses
}
func ToEbookRatingSummaryResponse(ebookId uint, ebookTitle string, averageRating float64, totalRatings int, ratingCounts map[int]int, recentReviews []*entity.EbookRatings) *ebookRatingsResponse.EbookRatingSummaryResponse {
reviews := ToEbookRatingsResponseList(recentReviews)
var reviewsList []ebookRatingsResponse.EbookRatingsResponse
for _, review := range reviews {
reviewsList = append(reviewsList, *review)
}
return &ebookRatingsResponse.EbookRatingSummaryResponse{
EbookId: ebookId,
EbookTitle: ebookTitle,
AverageRating: averageRating,
TotalRatings: totalRatings,
RatingCounts: ratingCounts,
RecentReviews: reviewsList,
}
}
func ToEbookRatingStatsResponse(totalRatings int, averageRating float64, ratingCounts map[int]int) *ebookRatingsResponse.EbookRatingStatsResponse {
return &ebookRatingsResponse.EbookRatingStatsResponse{
TotalRatings: totalRatings,
AverageRating: averageRating,
RatingCounts: ratingCounts,
FiveStarCount: ratingCounts[5],
FourStarCount: ratingCounts[4],
ThreeStarCount: ratingCounts[3],
TwoStarCount: ratingCounts[2],
OneStarCount: ratingCounts[1],
}
}
// Helper function
func stringPtr(s string) *string {
return &s
}

View File

@ -0,0 +1,140 @@
package mapper
import (
"narasi-ahli-be/app/database/entity"
ebooksResponse "narasi-ahli-be/app/module/ebooks/response"
)
func ToEbooksResponse(ebook *entity.Ebooks) *ebooksResponse.EbooksResponse {
if ebook == nil {
return nil
}
var authorName, authorEmail *string
if ebook.Author != nil {
authorName = &ebook.Author.Fullname
authorEmail = &ebook.Author.Email
}
return &ebooksResponse.EbooksResponse{
ID: ebook.ID,
Title: ebook.Title,
Slug: ebook.Slug,
Description: ebook.Description,
Price: ebook.Price,
PdfFilePath: ebook.PdfFilePath,
PdfFileName: ebook.PdfFileName,
PdfFileSize: ebook.PdfFileSize,
ThumbnailPath: ebook.ThumbnailPath,
ThumbnailName: ebook.ThumbnailName,
AuthorId: ebook.AuthorId,
AuthorName: authorName,
AuthorEmail: authorEmail,
Category: ebook.Category,
Tags: ebook.Tags,
PageCount: ebook.PageCount,
Language: ebook.Language,
Isbn: ebook.Isbn,
Publisher: ebook.Publisher,
PublishedYear: ebook.PublishedYear,
DownloadCount: ebook.DownloadCount,
PurchaseCount: ebook.PurchaseCount,
WishlistCount: ebook.WishlistCount,
Rating: ebook.Rating,
ReviewCount: ebook.ReviewCount,
StatusId: ebook.StatusId,
IsActive: ebook.IsActive,
IsPublished: ebook.IsPublished,
PublishedAt: ebook.PublishedAt,
CreatedById: ebook.CreatedById,
CreatedAt: ebook.CreatedAt,
UpdatedAt: ebook.UpdatedAt,
}
}
func ToEbooksResponseList(ebooks []*entity.Ebooks) []*ebooksResponse.EbooksResponse {
if ebooks == nil {
return nil
}
var responses []*ebooksResponse.EbooksResponse
for _, ebook := range ebooks {
responses = append(responses, ToEbooksResponse(ebook))
}
return responses
}
func ToEbookWishlistResponse(wishlist *entity.EbookWishlists) *ebooksResponse.EbookWishlistResponse {
if wishlist == nil {
return nil
}
return &ebooksResponse.EbookWishlistResponse{
ID: wishlist.ID,
UserId: wishlist.UserId,
EbookId: wishlist.EbookId,
Ebook: ToEbooksResponse(wishlist.Ebook),
CreatedAt: wishlist.CreatedAt,
UpdatedAt: wishlist.UpdatedAt,
}
}
func ToEbookWishlistResponseList(wishlists []*entity.EbookWishlists) []*ebooksResponse.EbookWishlistResponse {
if wishlists == nil {
return nil
}
var responses []*ebooksResponse.EbookWishlistResponse
for _, wishlist := range wishlists {
responses = append(responses, ToEbookWishlistResponse(wishlist))
}
return responses
}
func ToEbookPurchaseResponse(purchase *entity.EbookPurchases) *ebooksResponse.EbookPurchaseResponse {
if purchase == nil {
return nil
}
var buyerName, buyerEmail *string
if purchase.Buyer != nil {
buyerName = &purchase.Buyer.Fullname
buyerEmail = &purchase.Buyer.Email
}
return &ebooksResponse.EbookPurchaseResponse{
ID: purchase.ID,
BuyerId: purchase.BuyerId,
BuyerName: buyerName,
BuyerEmail: buyerEmail,
EbookId: purchase.EbookId,
Ebook: ToEbooksResponse(purchase.Ebook),
PurchasePrice: purchase.PurchasePrice,
PaymentMethod: purchase.PaymentMethod,
PaymentStatus: purchase.PaymentStatus,
TransactionId: purchase.TransactionId,
PaymentProof: purchase.PaymentProof,
PaymentDate: purchase.PaymentDate,
DownloadCount: purchase.DownloadCount,
LastDownloadAt: purchase.LastDownloadAt,
StatusId: purchase.StatusId,
IsActive: purchase.IsActive,
CreatedAt: purchase.CreatedAt,
UpdatedAt: purchase.UpdatedAt,
}
}
func ToEbookPurchaseResponseList(purchases []*entity.EbookPurchases) []*ebooksResponse.EbookPurchaseResponse {
if purchases == nil {
return nil
}
var responses []*ebooksResponse.EbookPurchaseResponse
for _, purchase := range purchases {
responses = append(responses, ToEbookPurchaseResponse(purchase))
}
return responses
}

View File

@ -0,0 +1,98 @@
package repository
import (
"narasi-ahli-be/app/database"
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/utils/paginator"
"github.com/rs/zerolog"
)
type ebookPurchasesRepository struct {
DB *database.Database
Log zerolog.Logger
}
// EbookPurchasesRepository define interface of IEbookPurchasesRepository
type EbookPurchasesRepository interface {
GetByBuyerId(buyerId uint, pagination *paginator.Pagination) (purchases []*entity.EbookPurchases, paging paginator.Pagination, err error)
FindByBuyerAndEbook(buyerId uint, ebookId uint) (purchase *entity.EbookPurchases, err error)
FindOne(id uint) (purchase *entity.EbookPurchases, err error)
Create(purchase *entity.EbookPurchases) (purchaseReturn *entity.EbookPurchases, err error)
Update(id uint, purchase *entity.EbookPurchases) (err error)
UpdateDownloadCount(id uint) (err error)
Delete(id uint) (err error)
}
func NewEbookPurchasesRepository(db *database.Database, log zerolog.Logger) EbookPurchasesRepository {
return &ebookPurchasesRepository{
DB: db,
Log: log,
}
}
func (_i *ebookPurchasesRepository) GetByBuyerId(buyerId uint, pagination *paginator.Pagination) (purchases []*entity.EbookPurchases, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&entity.EbookPurchases{}).
Preload("Ebook").
Preload("Ebook.Author").
Preload("Buyer").
Where("buyer_id = ?", buyerId)
query.Count(&count)
pagination.Count = count
pagination = paginator.Paging(pagination)
err = query.Offset(pagination.Offset).Limit(pagination.Limit).
Order("created_at DESC").
Find(&purchases).Error
if err != nil {
return
}
paging = *pagination
return
}
func (_i *ebookPurchasesRepository) FindByBuyerAndEbook(buyerId uint, ebookId uint) (purchase *entity.EbookPurchases, err error) {
query := _i.DB.DB.Where("buyer_id = ? AND ebook_id = ?", buyerId, ebookId)
if err := query.First(&purchase).Error; err != nil {
return nil, err
}
return purchase, nil
}
func (_i *ebookPurchasesRepository) FindOne(id uint) (purchase *entity.EbookPurchases, err error) {
query := _i.DB.DB.Preload("Ebook").Preload("Buyer")
if err := query.First(&purchase, id).Error; err != nil {
return nil, err
}
return purchase, nil
}
func (_i *ebookPurchasesRepository) Create(purchase *entity.EbookPurchases) (purchaseReturn *entity.EbookPurchases, err error) {
result := _i.DB.DB.Create(purchase)
return purchase, result.Error
}
func (_i *ebookPurchasesRepository) Update(id uint, purchase *entity.EbookPurchases) (err error) {
return _i.DB.DB.Model(&entity.EbookPurchases{}).
Where(&entity.EbookPurchases{ID: id}).
Updates(purchase).Error
}
func (_i *ebookPurchasesRepository) UpdateDownloadCount(id uint) (err error) {
return _i.DB.DB.Model(&entity.EbookPurchases{}).
Where("id = ?", id).
Update("download_count", "download_count + 1").Error
}
func (_i *ebookPurchasesRepository) Delete(id uint) error {
return _i.DB.DB.Delete(&entity.EbookPurchases{}, id).Error
}

View File

@ -0,0 +1,243 @@
package repository
import (
"narasi-ahli-be/app/database"
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/app/module/ebooks/request"
"narasi-ahli-be/utils/paginator"
"github.com/rs/zerolog"
)
type ebookRatingsRepository struct {
DB *database.Database
Log zerolog.Logger
}
// EbookRatingsRepository define interface of IEbookRatingsRepository
type EbookRatingsRepository interface {
GetAll(req request.EbookRatingsQueryRequest) (ratings []*entity.EbookRatings, paging paginator.Pagination, err error)
GetByEbookId(ebookId uint, pagination *paginator.Pagination) (ratings []*entity.EbookRatings, paging paginator.Pagination, err error)
GetByUserId(userId uint, pagination *paginator.Pagination) (ratings []*entity.EbookRatings, paging paginator.Pagination, err error)
FindOne(id uint) (rating *entity.EbookRatings, err error)
FindByUserAndEbook(userId uint, ebookId uint) (rating *entity.EbookRatings, err error)
FindByPurchaseId(purchaseId uint) (rating *entity.EbookRatings, err error)
Create(rating *entity.EbookRatings) (ratingReturn *entity.EbookRatings, err error)
Update(id uint, rating *entity.EbookRatings) (err error)
Delete(id uint) (err error)
GetEbookRatingStats(ebookId uint) (totalRatings int, averageRating float64, ratingCounts map[int]int, err error)
GetRecentReviews(ebookId uint, limit int) (reviews []*entity.EbookRatings, err error)
}
func NewEbookRatingsRepository(db *database.Database, log zerolog.Logger) EbookRatingsRepository {
return &ebookRatingsRepository{
DB: db,
Log: log,
}
}
// implement interface of IEbookRatingsRepository
func (_i *ebookRatingsRepository) GetAll(req request.EbookRatingsQueryRequest) (ratings []*entity.EbookRatings, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&entity.EbookRatings{}).
Preload("User").
Preload("Ebook").
Preload("Purchase").
Where("ebook_ratings.is_active = ?", true)
if req.EbookId != nil {
query = query.Where("ebook_ratings.ebook_id = ?", req.EbookId)
}
if req.UserId != nil {
query = query.Where("ebook_ratings.user_id = ?", req.UserId)
}
if req.Rating != nil {
query = query.Where("ebook_ratings.rating = ?", req.Rating)
}
if req.IsVerified != nil {
query = query.Where("ebook_ratings.is_verified = ?", req.IsVerified)
}
if req.StatusId != nil {
query = query.Where("ebook_ratings.status_id = ?", req.StatusId)
}
query.Count(&count)
if req.Pagination.SortBy != "" {
direction := "ASC"
if req.Pagination.Sort == "desc" {
direction = "DESC"
}
query.Order("ebook_ratings." + req.Pagination.SortBy + " " + direction)
} else {
query.Order("ebook_ratings.created_at DESC")
}
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&ratings).Error
if err != nil {
return
}
paging = *req.Pagination
return
}
func (_i *ebookRatingsRepository) GetByEbookId(ebookId uint, pagination *paginator.Pagination) (ratings []*entity.EbookRatings, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&entity.EbookRatings{}).
Preload("User").
Preload("Ebook").
Preload("Purchase").
Where("ebook_ratings.ebook_id = ? AND ebook_ratings.is_active = ?", ebookId, true)
query.Count(&count)
pagination.Count = count
pagination = paginator.Paging(pagination)
err = query.Offset(pagination.Offset).Limit(pagination.Limit).
Order("ebook_ratings.created_at DESC").
Find(&ratings).Error
if err != nil {
return
}
paging = *pagination
return
}
func (_i *ebookRatingsRepository) GetByUserId(userId uint, pagination *paginator.Pagination) (ratings []*entity.EbookRatings, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&entity.EbookRatings{}).
Preload("User").
Preload("Ebook").
Preload("Purchase").
Where("ebook_ratings.user_id = ? AND ebook_ratings.is_active = ?", userId, true)
query.Count(&count)
pagination.Count = count
pagination = paginator.Paging(pagination)
err = query.Offset(pagination.Offset).Limit(pagination.Limit).
Order("ebook_ratings.created_at DESC").
Find(&ratings).Error
if err != nil {
return
}
paging = *pagination
return
}
func (_i *ebookRatingsRepository) FindOne(id uint) (rating *entity.EbookRatings, err error) {
query := _i.DB.DB.Preload("User").Preload("Ebook").Preload("Purchase")
if err := query.First(&rating, id).Error; err != nil {
return nil, err
}
return rating, nil
}
func (_i *ebookRatingsRepository) FindByUserAndEbook(userId uint, ebookId uint) (rating *entity.EbookRatings, err error) {
query := _i.DB.DB.Where("user_id = ? AND ebook_id = ?", userId, ebookId)
if err := query.First(&rating).Error; err != nil {
return nil, err
}
return rating, nil
}
func (_i *ebookRatingsRepository) FindByPurchaseId(purchaseId uint) (rating *entity.EbookRatings, err error) {
query := _i.DB.DB.Where("purchase_id = ?", purchaseId)
if err := query.First(&rating).Error; err != nil {
return nil, err
}
return rating, nil
}
func (_i *ebookRatingsRepository) Create(rating *entity.EbookRatings) (ratingReturn *entity.EbookRatings, err error) {
result := _i.DB.DB.Create(rating)
return rating, result.Error
}
func (_i *ebookRatingsRepository) Update(id uint, rating *entity.EbookRatings) (err error) {
return _i.DB.DB.Model(&entity.EbookRatings{}).
Where(&entity.EbookRatings{ID: id}).
Updates(rating).Error
}
func (_i *ebookRatingsRepository) Delete(id uint) error {
return _i.DB.DB.Delete(&entity.EbookRatings{}, id).Error
}
func (_i *ebookRatingsRepository) GetEbookRatingStats(ebookId uint) (totalRatings int, averageRating float64, ratingCounts map[int]int, err error) {
var stats struct {
TotalRatings int `json:"totalRatings"`
AverageRating float64 `json:"averageRating"`
}
// Get total ratings and average
err = _i.DB.DB.Model(&entity.EbookRatings{}).
Select("COUNT(*) as total_ratings, COALESCE(AVG(rating), 0) as average_rating").
Where("ebook_id = ? AND is_active = ? AND is_verified = ?", ebookId, true, true).
Scan(&stats).Error
if err != nil {
return 0, 0, nil, err
}
// Get rating counts
var ratingCountsData []struct {
Rating int `json:"rating"`
Count int `json:"count"`
}
err = _i.DB.DB.Model(&entity.EbookRatings{}).
Select("rating, COUNT(*) as count").
Where("ebook_id = ? AND is_active = ? AND is_verified = ?", ebookId, true, true).
Group("rating").
Scan(&ratingCountsData).Error
if err != nil {
return 0, 0, nil, err
}
// Convert to map
ratingCounts = make(map[int]int)
for _, rc := range ratingCountsData {
ratingCounts[rc.Rating] = rc.Count
}
// Ensure all ratings 1-5 are present
for i := 1; i <= 5; i++ {
if _, exists := ratingCounts[i]; !exists {
ratingCounts[i] = 0
}
}
return stats.TotalRatings, stats.AverageRating, ratingCounts, nil
}
func (_i *ebookRatingsRepository) GetRecentReviews(ebookId uint, limit int) (reviews []*entity.EbookRatings, err error) {
query := _i.DB.DB.Model(&entity.EbookRatings{}).
Preload("User").
Preload("Ebook").
Preload("Purchase").
Where("ebook_id = ? AND is_active = ? AND is_verified = ? AND review IS NOT NULL AND review != ''", ebookId, true, true).
Order("created_at DESC").
Limit(limit)
err = query.Find(&reviews).Error
return reviews, err
}

View File

@ -0,0 +1,79 @@
package repository
import (
"narasi-ahli-be/app/database"
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/utils/paginator"
"github.com/rs/zerolog"
)
type ebookWishlistsRepository struct {
DB *database.Database
Log zerolog.Logger
}
// EbookWishlistsRepository define interface of IEbookWishlistsRepository
type EbookWishlistsRepository interface {
GetByUserId(userId uint, pagination *paginator.Pagination) (wishlists []*entity.EbookWishlists, paging paginator.Pagination, err error)
FindByUserAndEbook(userId uint, ebookId uint) (wishlist *entity.EbookWishlists, err error)
Create(wishlist *entity.EbookWishlists) (wishlistReturn *entity.EbookWishlists, err error)
Delete(id uint) (err error)
DeleteByUserAndEbook(userId uint, ebookId uint) (err error)
}
func NewEbookWishlistsRepository(db *database.Database, log zerolog.Logger) EbookWishlistsRepository {
return &ebookWishlistsRepository{
DB: db,
Log: log,
}
}
func (_i *ebookWishlistsRepository) GetByUserId(userId uint, pagination *paginator.Pagination) (wishlists []*entity.EbookWishlists, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&entity.EbookWishlists{}).
Preload("Ebook").
Preload("Ebook.Author").
Where("user_id = ?", userId)
query.Count(&count)
pagination.Count = count
pagination = paginator.Paging(pagination)
err = query.Offset(pagination.Offset).Limit(pagination.Limit).
Order("created_at DESC").
Find(&wishlists).Error
if err != nil {
return
}
paging = *pagination
return
}
func (_i *ebookWishlistsRepository) FindByUserAndEbook(userId uint, ebookId uint) (wishlist *entity.EbookWishlists, err error) {
query := _i.DB.DB.Where("user_id = ? AND ebook_id = ?", userId, ebookId)
if err := query.First(&wishlist).Error; err != nil {
return nil, err
}
return wishlist, nil
}
func (_i *ebookWishlistsRepository) Create(wishlist *entity.EbookWishlists) (wishlistReturn *entity.EbookWishlists, err error) {
result := _i.DB.DB.Create(wishlist)
return wishlist, result.Error
}
func (_i *ebookWishlistsRepository) Delete(id uint) error {
return _i.DB.DB.Delete(&entity.EbookWishlists{}, id).Error
}
func (_i *ebookWishlistsRepository) DeleteByUserAndEbook(userId uint, ebookId uint) error {
return _i.DB.DB.Where("user_id = ? AND ebook_id = ?", userId, ebookId).
Delete(&entity.EbookWishlists{}).Error
}

View File

@ -0,0 +1,186 @@
package repository
import (
"fmt"
"narasi-ahli-be/app/database"
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/app/module/ebooks/request"
"narasi-ahli-be/app/module/ebooks/response"
"narasi-ahli-be/utils/paginator"
utilSvc "narasi-ahli-be/utils/service"
"strings"
"github.com/rs/zerolog"
)
type ebooksRepository struct {
DB *database.Database
Log zerolog.Logger
}
// EbooksRepository define interface of IEbooksRepository
type EbooksRepository interface {
GetAll(req request.EbooksQueryRequest) (ebooks []*entity.Ebooks, paging paginator.Pagination, err error)
FindOne(id uint) (ebook *entity.Ebooks, err error)
FindBySlug(slug string) (ebook *entity.Ebooks, err error)
Create(ebook *entity.Ebooks) (ebookReturn *entity.Ebooks, err error)
Update(id uint, ebook *entity.Ebooks) (err error)
UpdateSkipNull(id uint, ebook *entity.Ebooks) (err error)
Delete(id uint) (err error)
UpdateDownloadCount(id uint) (err error)
UpdatePurchaseCount(id uint) (err error)
UpdateWishlistCount(id uint) (err error)
SummaryStats(authorId uint) (ebookSummaryStats *response.EbookSummaryStats, err error)
}
func NewEbooksRepository(db *database.Database, log zerolog.Logger) EbooksRepository {
return &ebooksRepository{
DB: db,
Log: log,
}
}
// implement interface of IEbooksRepository
func (_i *ebooksRepository) GetAll(req request.EbooksQueryRequest) (ebooks []*entity.Ebooks, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&entity.Ebooks{}).
Preload("Author").
Where("ebooks.is_active = ?", true)
if req.Title != nil && *req.Title != "" {
title := strings.ToLower(*req.Title)
query = query.Where("LOWER(ebooks.title) LIKE ?", "%"+strings.ToLower(title)+"%")
}
if req.Description != nil && *req.Description != "" {
description := strings.ToLower(*req.Description)
query = query.Where("LOWER(ebooks.description) LIKE ?", "%"+strings.ToLower(description)+"%")
}
if req.AuthorId != nil {
query = query.Where("ebooks.author_id = ?", req.AuthorId)
}
if req.Category != nil && *req.Category != "" {
category := strings.ToLower(*req.Category)
query = query.Where("LOWER(ebooks.category) LIKE ?", "%"+strings.ToLower(category)+"%")
}
if req.Tags != nil && *req.Tags != "" {
tags := strings.ToLower(*req.Tags)
query = query.Where("LOWER(ebooks.tags) LIKE ?", "%"+strings.ToLower(tags)+"%")
}
if req.MinPrice != nil {
query = query.Where("ebooks.price >= ?", req.MinPrice)
}
if req.MaxPrice != nil {
query = query.Where("ebooks.price <= ?", req.MaxPrice)
}
if req.IsPublished != nil {
query = query.Where("ebooks.is_published = ?", req.IsPublished)
}
if req.StatusId != nil {
query = query.Where("ebooks.status_id = ?", req.StatusId)
}
query.Count(&count)
if req.Pagination.SortBy != "" {
direction := "ASC"
if req.Pagination.Sort == "desc" {
direction = "DESC"
}
query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction))
} else {
direction := "DESC"
sortBy := "ebooks.created_at"
query.Order(fmt.Sprintf("%s %s", sortBy, direction))
}
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&ebooks).Error
if err != nil {
return
}
paging = *req.Pagination
return
}
func (_i *ebooksRepository) FindOne(id uint) (ebook *entity.Ebooks, err error) {
query := _i.DB.DB.Preload("Author")
if err := query.First(&ebook, id).Error; err != nil {
return nil, err
}
return ebook, nil
}
func (_i *ebooksRepository) FindBySlug(slug string) (ebook *entity.Ebooks, err error) {
query := _i.DB.DB.Preload("Author").Where("slug = ?", slug)
if err := query.First(&ebook).Error; err != nil {
return nil, err
}
return ebook, nil
}
func (_i *ebooksRepository) Create(ebook *entity.Ebooks) (ebookReturn *entity.Ebooks, err error) {
result := _i.DB.DB.Create(ebook)
return ebook, result.Error
}
func (_i *ebooksRepository) Update(id uint, ebook *entity.Ebooks) (err error) {
ebookMap, err := utilSvc.StructToMap(ebook)
if err != nil {
return err
}
return _i.DB.DB.Model(&entity.Ebooks{}).
Where(&entity.Ebooks{ID: id}).
Updates(ebookMap).Error
}
func (_i *ebooksRepository) UpdateSkipNull(id uint, ebook *entity.Ebooks) (err error) {
return _i.DB.DB.Model(&entity.Ebooks{}).
Where(&entity.Ebooks{ID: id}).
Updates(ebook).Error
}
func (_i *ebooksRepository) Delete(id uint) error {
return _i.DB.DB.Delete(&entity.Ebooks{}, id).Error
}
func (_i *ebooksRepository) UpdateDownloadCount(id uint) (err error) {
return _i.DB.DB.Model(&entity.Ebooks{}).
Where("id = ?", id).
Update("download_count", "download_count + 1").Error
}
func (_i *ebooksRepository) UpdatePurchaseCount(id uint) (err error) {
return _i.DB.DB.Model(&entity.Ebooks{}).
Where("id = ?", id).
Update("purchase_count", "purchase_count + 1").Error
}
func (_i *ebooksRepository) UpdateWishlistCount(id uint) (err error) {
return _i.DB.DB.Model(&entity.Ebooks{}).
Where("id = ?", id).
Update("wishlist_count", "wishlist_count + 1").Error
}
func (_i *ebooksRepository) SummaryStats(authorId uint) (ebookSummaryStats *response.EbookSummaryStats, err error) {
query := _i.DB.DB.Model(&entity.Ebooks{}).
Select(
"COUNT(*) AS total_ebooks, "+
"COALESCE(SUM(purchase_count), 0) AS total_sales, "+
"COALESCE(SUM(price * purchase_count), 0) AS total_revenue, "+
"COALESCE(SUM(download_count), 0) AS total_downloads, "+
"COALESCE(SUM(wishlist_count), 0) AS total_wishlists, "+
"COALESCE(AVG(rating), 0) AS average_rating").
Where("author_id = ?", authorId)
err = query.Scan(&ebookSummaryStats).Error
return ebookSummaryStats, err
}

View File

@ -0,0 +1,113 @@
package request
import (
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/utils/paginator"
"strconv"
"time"
)
type EbookRatingsGeneric interface {
ToEntity()
}
type EbookRatingsQueryRequest struct {
EbookId *uint `json:"ebookId"`
UserId *uint `json:"userId"`
Rating *int `json:"rating"`
IsVerified *bool `json:"isVerified"`
StatusId *int `json:"statusId"`
Pagination *paginator.Pagination `json:"pagination"`
}
type EbookRatingsCreateRequest struct {
EbookId uint `json:"ebookId" validate:"required"`
PurchaseId uint `json:"purchaseId" validate:"required"`
Rating int `json:"rating" validate:"required,min=1,max=5"`
Review *string `json:"review"`
IsAnonymous *bool `json:"isAnonymous"`
}
func (req EbookRatingsCreateRequest) ToEntity() *entity.EbookRatings {
return &entity.EbookRatings{
EbookId: req.EbookId,
PurchaseId: req.PurchaseId,
Rating: req.Rating,
Review: req.Review,
IsAnonymous: req.IsAnonymous,
IsVerified: boolPtr(true),
StatusId: intPtr(1),
IsActive: boolPtr(true),
}
}
type EbookRatingsUpdateRequest struct {
Rating int `json:"rating" validate:"required,min=1,max=5"`
Review *string `json:"review"`
IsAnonymous *bool `json:"isAnonymous"`
}
func (req EbookRatingsUpdateRequest) ToEntity() *entity.EbookRatings {
return &entity.EbookRatings{
Rating: req.Rating,
Review: req.Review,
IsAnonymous: req.IsAnonymous,
UpdatedAt: time.Now(),
}
}
type EbookRatingsQueryRequestContext struct {
EbookId string `json:"ebookId"`
UserId string `json:"userId"`
Rating string `json:"rating"`
IsVerified string `json:"isVerified"`
StatusId string `json:"statusId"`
}
func (req EbookRatingsQueryRequestContext) ToParamRequest() EbookRatingsQueryRequest {
var request EbookRatingsQueryRequest
if ebookIdStr := req.EbookId; ebookIdStr != "" {
ebookId, err := strconv.Atoi(ebookIdStr)
if err == nil {
ebookIdUint := uint(ebookId)
request.EbookId = &ebookIdUint
}
}
if userIdStr := req.UserId; userIdStr != "" {
userId, err := strconv.Atoi(userIdStr)
if err == nil {
userIdUint := uint(userId)
request.UserId = &userIdUint
}
}
if ratingStr := req.Rating; ratingStr != "" {
rating, err := strconv.Atoi(ratingStr)
if err == nil {
request.Rating = &rating
}
}
if isVerifiedStr := req.IsVerified; isVerifiedStr != "" {
isVerified, err := strconv.ParseBool(isVerifiedStr)
if err == nil {
request.IsVerified = &isVerified
}
}
if statusIdStr := req.StatusId; statusIdStr != "" {
statusId, err := strconv.Atoi(statusIdStr)
if err == nil {
request.StatusId = &statusId
}
}
return request
}
// Helper functions
func boolPtr(b bool) *bool {
return &b
}
func intPtr(i int) *int {
return &i
}

View File

@ -0,0 +1,178 @@
package request
import (
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/utils/paginator"
"strconv"
"time"
)
type EbooksGeneric interface {
ToEntity()
}
type EbooksQueryRequest struct {
Title *string `json:"title"`
Description *string `json:"description"`
AuthorId *uint `json:"authorId"`
Category *string `json:"category"`
Tags *string `json:"tags"`
MinPrice *float64 `json:"minPrice"`
MaxPrice *float64 `json:"maxPrice"`
StatusId *int `json:"statusId"`
IsPublished *bool `json:"isPublished"`
Pagination *paginator.Pagination `json:"pagination"`
}
type EbooksCreateRequest struct {
Title string `json:"title" validate:"required"`
Slug string `json:"slug" validate:"required"`
Description string `json:"description" validate:"required"`
Price float64 `json:"price" validate:"required,min=0"`
Category *string `json:"category"`
Tags *string `json:"tags"`
PageCount *int `json:"pageCount"`
Language *string `json:"language"`
Isbn *string `json:"isbn"`
Publisher *string `json:"publisher"`
PublishedYear *int `json:"publishedYear"`
IsPublished *bool `json:"isPublished"`
CreatedAt *string `json:"createdAt"`
CreatedById *uint `json:"createdById"`
}
func (req EbooksCreateRequest) ToEntity() *entity.Ebooks {
return &entity.Ebooks{
Title: req.Title,
Slug: req.Slug,
Description: req.Description,
Price: req.Price,
Category: req.Category,
Tags: req.Tags,
PageCount: req.PageCount,
Language: req.Language,
Isbn: req.Isbn,
Publisher: req.Publisher,
PublishedYear: req.PublishedYear,
IsPublished: req.IsPublished,
}
}
type EbooksUpdateRequest struct {
Title string `json:"title" validate:"required"`
Slug string `json:"slug" validate:"required"`
Description string `json:"description" validate:"required"`
Price float64 `json:"price" validate:"required,min=0"`
Category *string `json:"category"`
Tags *string `json:"tags"`
PageCount *int `json:"pageCount"`
Language *string `json:"language"`
Isbn *string `json:"isbn"`
Publisher *string `json:"publisher"`
PublishedYear *int `json:"publishedYear"`
IsPublished *bool `json:"isPublished"`
StatusId *int `json:"statusId"`
CreatedAt *string `json:"createdAt"`
CreatedById *uint `json:"createdById"`
}
func (req EbooksUpdateRequest) ToEntity() *entity.Ebooks {
if req.CreatedById == nil {
return &entity.Ebooks{
Title: req.Title,
Slug: req.Slug,
Description: req.Description,
Price: req.Price,
Category: req.Category,
Tags: req.Tags,
PageCount: req.PageCount,
Language: req.Language,
Isbn: req.Isbn,
Publisher: req.Publisher,
PublishedYear: req.PublishedYear,
IsPublished: req.IsPublished,
StatusId: req.StatusId,
UpdatedAt: time.Now(),
}
} else {
return &entity.Ebooks{
Title: req.Title,
Slug: req.Slug,
Description: req.Description,
Price: req.Price,
Category: req.Category,
Tags: req.Tags,
PageCount: req.PageCount,
Language: req.Language,
Isbn: req.Isbn,
Publisher: req.Publisher,
PublishedYear: req.PublishedYear,
IsPublished: req.IsPublished,
StatusId: req.StatusId,
CreatedById: req.CreatedById,
UpdatedAt: time.Now(),
}
}
}
type EbooksQueryRequestContext struct {
Title string `json:"title"`
Description string `json:"description"`
AuthorId string `json:"authorId"`
Category string `json:"category"`
Tags string `json:"tags"`
MinPrice string `json:"minPrice"`
MaxPrice string `json:"maxPrice"`
StatusId string `json:"statusId"`
IsPublished string `json:"isPublished"`
}
func (req EbooksQueryRequestContext) ToParamRequest() EbooksQueryRequest {
var request EbooksQueryRequest
if title := req.Title; title != "" {
request.Title = &title
}
if description := req.Description; description != "" {
request.Description = &description
}
if category := req.Category; category != "" {
request.Category = &category
}
if tags := req.Tags; tags != "" {
request.Tags = &tags
}
if authorIdStr := req.AuthorId; authorIdStr != "" {
authorId, err := strconv.Atoi(authorIdStr)
if err == nil {
authorIdUint := uint(authorId)
request.AuthorId = &authorIdUint
}
}
if minPriceStr := req.MinPrice; minPriceStr != "" {
minPrice, err := strconv.ParseFloat(minPriceStr, 64)
if err == nil {
request.MinPrice = &minPrice
}
}
if maxPriceStr := req.MaxPrice; maxPriceStr != "" {
maxPrice, err := strconv.ParseFloat(maxPriceStr, 64)
if err == nil {
request.MaxPrice = &maxPrice
}
}
if isPublishedStr := req.IsPublished; isPublishedStr != "" {
isPublished, err := strconv.ParseBool(isPublishedStr)
if err == nil {
request.IsPublished = &isPublished
}
}
if statusIdStr := req.StatusId; statusIdStr != "" {
statusId, err := strconv.Atoi(statusIdStr)
if err == nil {
request.StatusId = &statusId
}
}
return request
}

View File

@ -0,0 +1,43 @@
package response
import (
"time"
)
type EbookRatingsResponse struct {
ID uint `json:"id"`
UserId uint `json:"userId"`
UserName *string `json:"userName"`
UserEmail *string `json:"userEmail"`
EbookId uint `json:"ebookId"`
EbookTitle *string `json:"ebookTitle"`
PurchaseId uint `json:"purchaseId"`
Rating int `json:"rating"`
Review *string `json:"review"`
IsAnonymous *bool `json:"isAnonymous"`
IsVerified *bool `json:"isVerified"`
StatusId *int `json:"statusId"`
IsActive *bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type EbookRatingSummaryResponse struct {
EbookId uint `json:"ebookId"`
EbookTitle string `json:"ebookTitle"`
AverageRating float64 `json:"averageRating"`
TotalRatings int `json:"totalRatings"`
RatingCounts map[int]int `json:"ratingCounts"`
RecentReviews []EbookRatingsResponse `json:"recentReviews"`
}
type EbookRatingStatsResponse struct {
TotalRatings int `json:"totalRatings"`
AverageRating float64 `json:"averageRating"`
RatingCounts map[int]int `json:"ratingCounts"`
FiveStarCount int `json:"fiveStarCount"`
FourStarCount int `json:"fourStarCount"`
ThreeStarCount int `json:"threeStarCount"`
TwoStarCount int `json:"twoStarCount"`
OneStarCount int `json:"oneStarCount"`
}

View File

@ -0,0 +1,79 @@
package response
import (
"time"
)
type EbooksResponse struct {
ID uint `json:"id"`
Title string `json:"title"`
Slug string `json:"slug"`
Description string `json:"description"`
Price float64 `json:"price"`
PdfFilePath *string `json:"pdfFilePath"`
PdfFileName *string `json:"pdfFileName"`
PdfFileSize *int64 `json:"pdfFileSize"`
ThumbnailPath *string `json:"thumbnailPath"`
ThumbnailName *string `json:"thumbnailName"`
AuthorId uint `json:"authorId"`
AuthorName *string `json:"authorName"`
AuthorEmail *string `json:"authorEmail"`
Category *string `json:"category"`
Tags *string `json:"tags"`
PageCount *int `json:"pageCount"`
Language *string `json:"language"`
Isbn *string `json:"isbn"`
Publisher *string `json:"publisher"`
PublishedYear *int `json:"publishedYear"`
DownloadCount *int `json:"downloadCount"`
PurchaseCount *int `json:"purchaseCount"`
WishlistCount *int `json:"wishlistCount"`
Rating *float64 `json:"rating"`
ReviewCount *int `json:"reviewCount"`
StatusId *int `json:"statusId"`
IsActive *bool `json:"isActive"`
IsPublished *bool `json:"isPublished"`
PublishedAt *time.Time `json:"publishedAt"`
CreatedById *uint `json:"createdById"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type EbookWishlistResponse struct {
ID uint `json:"id"`
UserId uint `json:"userId"`
EbookId uint `json:"ebookId"`
Ebook *EbooksResponse `json:"ebook"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type EbookPurchaseResponse struct {
ID uint `json:"id"`
BuyerId uint `json:"buyerId"`
BuyerName *string `json:"buyerName"`
BuyerEmail *string `json:"buyerEmail"`
EbookId uint `json:"ebookId"`
Ebook *EbooksResponse `json:"ebook"`
PurchasePrice float64 `json:"purchasePrice"`
PaymentMethod *string `json:"paymentMethod"`
PaymentStatus *string `json:"paymentStatus"`
TransactionId *string `json:"transactionId"`
PaymentProof *string `json:"paymentProof"`
PaymentDate *time.Time `json:"paymentDate"`
DownloadCount *int `json:"downloadCount"`
LastDownloadAt *time.Time `json:"lastDownloadAt"`
StatusId *int `json:"statusId"`
IsActive *bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type EbookSummaryStats struct {
TotalEbooks int `json:"totalEbooks"`
TotalSales int `json:"totalSales"`
TotalRevenue float64 `json:"totalRevenue"`
TotalDownloads int `json:"totalDownloads"`
TotalWishlists int `json:"totalWishlists"`
AverageRating float64 `json:"averageRating"`
}

View File

@ -0,0 +1,185 @@
package service
import (
"errors"
"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/response"
usersRepository "narasi-ahli-be/app/module/users/repository"
"narasi-ahli-be/utils/paginator"
utilSvc "narasi-ahli-be/utils/service"
"time"
"github.com/rs/zerolog"
)
// EbookPurchasesService
type ebookPurchasesService struct {
Repo repository.EbookPurchasesRepository
EbooksRepo repository.EbooksRepository
Log zerolog.Logger
UsersRepo usersRepository.UsersRepository
}
// EbookPurchasesService define interface of IEbookPurchasesService
type EbookPurchasesService interface {
GetByBuyerId(authToken string, pagination *paginator.Pagination) (purchases []*response.EbookPurchaseResponse, paging paginator.Pagination, err error)
PurchaseEbook(authToken string, ebookId uint, paymentMethod string) (purchase *response.EbookPurchaseResponse, err error)
UpdatePaymentStatus(purchaseId uint, paymentStatus string, transactionId string, paymentProof string) (err error)
GetPurchaseById(purchaseId uint) (purchase *response.EbookPurchaseResponse, err error)
ConfirmPayment(purchaseId uint) (err error)
}
// NewEbookPurchasesService init EbookPurchasesService
func NewEbookPurchasesService(
repo repository.EbookPurchasesRepository,
ebooksRepo repository.EbooksRepository,
log zerolog.Logger,
usersRepo usersRepository.UsersRepository) EbookPurchasesService {
return &ebookPurchasesService{
Repo: repo,
EbooksRepo: ebooksRepo,
Log: log,
UsersRepo: usersRepo,
}
}
// GetByBuyerId implement interface of EbookPurchasesService
func (_i *ebookPurchasesService) GetByBuyerId(authToken string, pagination *paginator.Pagination) (purchases []*response.EbookPurchaseResponse, paging paginator.Pagination, err error) {
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
if user == nil {
return nil, paginator.Pagination{}, errors.New("user not found")
}
purchasesData, paging, err := _i.Repo.GetByBuyerId(user.ID, pagination)
if err != nil {
return nil, paging, err
}
purchases = mapper.ToEbookPurchaseResponseList(purchasesData)
return purchases, paging, nil
}
// PurchaseEbook implement interface of EbookPurchasesService
func (_i *ebookPurchasesService) PurchaseEbook(authToken string, ebookId uint, paymentMethod string) (purchase *response.EbookPurchaseResponse, err error) {
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
if user == nil {
return nil, errors.New("user not found")
}
// Check if ebook exists
ebook, err := _i.EbooksRepo.FindOne(ebookId)
if err != nil {
return nil, errors.New("ebook not found")
}
// Check if already purchased
existingPurchase, err := _i.Repo.FindByBuyerAndEbook(user.ID, ebookId)
if err == nil && existingPurchase != nil {
return nil, errors.New("ebook already purchased")
}
// Create purchase record
purchaseEntity := &entity.EbookPurchases{
BuyerId: user.ID,
EbookId: ebookId,
PurchasePrice: ebook.Price,
PaymentMethod: &paymentMethod,
PaymentStatus: stringPtr("pending"),
StatusId: intPtr(1),
IsActive: boolPtr(true),
}
purchaseData, err := _i.Repo.Create(purchaseEntity)
if err != nil {
return nil, err
}
// Update purchase count
err = _i.EbooksRepo.UpdatePurchaseCount(ebookId)
if err != nil {
_i.Log.Error().Err(err).Msg("Failed to update purchase count")
}
purchase = mapper.ToEbookPurchaseResponse(purchaseData)
return purchase, nil
}
// UpdatePaymentStatus implement interface of EbookPurchasesService
func (_i *ebookPurchasesService) UpdatePaymentStatus(purchaseId uint, paymentStatus string, transactionId string, paymentProof string) (err error) {
_, err = _i.Repo.FindOne(purchaseId)
if err != nil {
return errors.New("purchase not found")
}
updateData := &entity.EbookPurchases{
PaymentStatus: &paymentStatus,
TransactionId: &transactionId,
PaymentProof: &paymentProof,
}
if paymentStatus == "paid" {
now := time.Now()
updateData.PaymentDate = &now
}
err = _i.Repo.Update(purchaseId, updateData)
if err != nil {
return err
}
return nil
}
// GetPurchaseById implement interface of EbookPurchasesService
func (_i *ebookPurchasesService) GetPurchaseById(purchaseId uint) (purchase *response.EbookPurchaseResponse, err error) {
purchaseData, err := _i.Repo.FindOne(purchaseId)
if err != nil {
return nil, err
}
purchase = mapper.ToEbookPurchaseResponse(purchaseData)
return purchase, nil
}
// ConfirmPayment implement interface of EbookPurchasesService
func (_i *ebookPurchasesService) ConfirmPayment(purchaseId uint) (err error) {
purchase, err := _i.Repo.FindOne(purchaseId)
if err != nil {
return errors.New("purchase not found")
}
if purchase.PaymentStatus == nil || *purchase.PaymentStatus != "paid" {
return errors.New("payment not completed")
}
// Update status to confirmed
updateData := &entity.EbookPurchases{
StatusId: intPtr(2), // Assuming 2 is confirmed status
}
err = _i.Repo.Update(purchaseId, updateData)
if err != nil {
return err
}
return nil
}
// Helper functions
func stringPtr(s string) *string {
return &s
}
func intPtr(i int) *int {
return &i
}
func boolPtr(b bool) *bool {
return &b
}

View File

@ -0,0 +1,280 @@
package service
import (
"errors"
"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"
"narasi-ahli-be/utils/paginator"
utilSvc "narasi-ahli-be/utils/service"
"github.com/rs/zerolog"
)
// EbookRatingsService
type ebookRatingsService struct {
Repo repository.EbookRatingsRepository
EbooksRepo repository.EbooksRepository
EbookPurchasesRepo repository.EbookPurchasesRepository
Log zerolog.Logger
UsersRepo usersRepository.UsersRepository
}
// EbookRatingsService define interface of IEbookRatingsService
type EbookRatingsService interface {
GetAll(req request.EbookRatingsQueryRequest) (ratings []*response.EbookRatingsResponse, paging paginator.Pagination, err error)
GetByEbookId(ebookId uint, pagination *paginator.Pagination) (ratings []*response.EbookRatingsResponse, paging paginator.Pagination, err error)
GetByUserId(authToken string, pagination *paginator.Pagination) (ratings []*response.EbookRatingsResponse, paging paginator.Pagination, err error)
GetEbookRatingSummary(ebookId uint) (summary *response.EbookRatingSummaryResponse, err error)
Create(req request.EbookRatingsCreateRequest, authToken string) (rating *response.EbookRatingsResponse, err error)
Update(id uint, req request.EbookRatingsUpdateRequest, authToken string) (err error)
Delete(id uint, authToken string) error
GetRatingStats(ebookId uint) (stats *response.EbookRatingStatsResponse, err error)
}
// NewEbookRatingsService init EbookRatingsService
func NewEbookRatingsService(
repo repository.EbookRatingsRepository,
ebooksRepo repository.EbooksRepository,
ebookPurchasesRepo repository.EbookPurchasesRepository,
log zerolog.Logger,
usersRepo usersRepository.UsersRepository) EbookRatingsService {
return &ebookRatingsService{
Repo: repo,
EbooksRepo: ebooksRepo,
EbookPurchasesRepo: ebookPurchasesRepo,
Log: log,
UsersRepo: usersRepo,
}
}
// GetAll implement interface of EbookRatingsService
func (_i *ebookRatingsService) GetAll(req request.EbookRatingsQueryRequest) (ratings []*response.EbookRatingsResponse, paging paginator.Pagination, err error) {
ratingsData, paging, err := _i.Repo.GetAll(req)
if err != nil {
return nil, paging, err
}
ratings = mapper.ToEbookRatingsResponseList(ratingsData)
return ratings, paging, nil
}
// GetByEbookId implement interface of EbookRatingsService
func (_i *ebookRatingsService) GetByEbookId(ebookId uint, pagination *paginator.Pagination) (ratings []*response.EbookRatingsResponse, paging paginator.Pagination, err error) {
ratingsData, paging, err := _i.Repo.GetByEbookId(ebookId, pagination)
if err != nil {
return nil, paging, err
}
ratings = mapper.ToEbookRatingsResponseList(ratingsData)
return ratings, paging, nil
}
// GetByUserId implement interface of EbookRatingsService
func (_i *ebookRatingsService) GetByUserId(authToken string, pagination *paginator.Pagination) (ratings []*response.EbookRatingsResponse, paging paginator.Pagination, err error) {
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
if user == nil {
return nil, paginator.Pagination{}, errors.New("user not found")
}
ratingsData, paging, err := _i.Repo.GetByUserId(user.ID, pagination)
if err != nil {
return nil, paging, err
}
ratings = mapper.ToEbookRatingsResponseList(ratingsData)
return ratings, paging, nil
}
// GetEbookRatingSummary implement interface of EbookRatingsService
func (_i *ebookRatingsService) GetEbookRatingSummary(ebookId uint) (summary *response.EbookRatingSummaryResponse, err error) {
// Get ebook info
ebook, err := _i.EbooksRepo.FindOne(ebookId)
if err != nil {
return nil, errors.New("ebook not found")
}
// Get rating stats
totalRatings, averageRating, ratingCounts, err := _i.Repo.GetEbookRatingStats(ebookId)
if err != nil {
return nil, err
}
// Get recent reviews
recentReviews, err := _i.Repo.GetRecentReviews(ebookId, 5)
if err != nil {
return nil, err
}
summary = mapper.ToEbookRatingSummaryResponse(ebookId, ebook.Title, averageRating, totalRatings, ratingCounts, recentReviews)
return summary, nil
}
// Create implement interface of EbookRatingsService
func (_i *ebookRatingsService) Create(req request.EbookRatingsCreateRequest, authToken string) (rating *response.EbookRatingsResponse, err error) {
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
if user == nil {
return nil, errors.New("user not found")
}
// Check if ebook exists
_, err = _i.EbooksRepo.FindOne(req.EbookId)
if err != nil {
return nil, errors.New("ebook not found")
}
// Check if purchase exists and belongs to user
purchase, err := _i.EbookPurchasesRepo.FindOne(req.PurchaseId)
if err != nil {
return nil, errors.New("purchase not found")
}
if purchase.BuyerId != user.ID {
return nil, errors.New("purchase does not belong to user")
}
if purchase.EbookId != req.EbookId {
return nil, errors.New("purchase does not match ebook")
}
if purchase.PaymentStatus == nil || *purchase.PaymentStatus != "paid" {
return nil, errors.New("payment not completed")
}
// Check if user already rated this ebook
existingRating, err := _i.Repo.FindByUserAndEbook(user.ID, req.EbookId)
if err == nil && existingRating != nil {
return nil, errors.New("user already rated this ebook")
}
// Check if user already rated this purchase
existingPurchaseRating, err := _i.Repo.FindByPurchaseId(req.PurchaseId)
if err == nil && existingPurchaseRating != nil {
return nil, errors.New("user already rated this purchase")
}
// Create rating
ratingEntity := req.ToEntity()
ratingEntity.UserId = user.ID
ratingData, err := _i.Repo.Create(ratingEntity)
if err != nil {
return nil, err
}
// Update ebook rating stats
err = _i.updateEbookRatingStats(req.EbookId)
if err != nil {
_i.Log.Error().Err(err).Msg("Failed to update ebook rating stats")
}
rating = mapper.ToEbookRatingsResponse(ratingData)
return rating, nil
}
// Update implement interface of EbookRatingsService
func (_i *ebookRatingsService) Update(id uint, req request.EbookRatingsUpdateRequest, authToken string) (err error) {
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
if user == nil {
return errors.New("user not found")
}
// Check if rating exists and belongs to user
existingRating, err := _i.Repo.FindOne(id)
if err != nil {
return errors.New("rating not found")
}
if existingRating.UserId != user.ID {
return errors.New("rating does not belong to user")
}
// Update rating
ratingEntity := req.ToEntity()
err = _i.Repo.Update(id, ratingEntity)
if err != nil {
return err
}
// Update ebook rating stats
err = _i.updateEbookRatingStats(existingRating.EbookId)
if err != nil {
_i.Log.Error().Err(err).Msg("Failed to update ebook rating stats")
}
return nil
}
// Delete implement interface of EbookRatingsService
func (_i *ebookRatingsService) Delete(id uint, authToken string) error {
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
if user == nil {
return errors.New("user not found")
}
// Check if rating exists and belongs to user
existingRating, err := _i.Repo.FindOne(id)
if err != nil {
return errors.New("rating not found")
}
if existingRating.UserId != user.ID {
return errors.New("rating does not belong to user")
}
// Delete rating
err = _i.Repo.Delete(id)
if err != nil {
return err
}
// Update ebook rating stats
err = _i.updateEbookRatingStats(existingRating.EbookId)
if err != nil {
_i.Log.Error().Err(err).Msg("Failed to update ebook rating stats")
}
return nil
}
// GetRatingStats implement interface of EbookRatingsService
func (_i *ebookRatingsService) GetRatingStats(ebookId uint) (stats *response.EbookRatingStatsResponse, err error) {
totalRatings, averageRating, ratingCounts, err := _i.Repo.GetEbookRatingStats(ebookId)
if err != nil {
return nil, err
}
stats = mapper.ToEbookRatingStatsResponse(totalRatings, averageRating, ratingCounts)
return stats, nil
}
// updateEbookRatingStats updates the rating statistics in the ebook table
func (_i *ebookRatingsService) updateEbookRatingStats(ebookId uint) error {
totalRatings, averageRating, _, err := _i.Repo.GetEbookRatingStats(ebookId)
if err != nil {
return err
}
// Update ebook with new rating stats
ebookUpdate := &entity.Ebooks{
Rating: &averageRating,
ReviewCount: &totalRatings,
}
err = _i.EbooksRepo.UpdateSkipNull(ebookId, ebookUpdate)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,138 @@
package service
import (
"errors"
"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/response"
usersRepository "narasi-ahli-be/app/module/users/repository"
"narasi-ahli-be/utils/paginator"
utilSvc "narasi-ahli-be/utils/service"
"github.com/rs/zerolog"
)
// EbookWishlistsService
type ebookWishlistsService struct {
Repo repository.EbookWishlistsRepository
EbooksRepo repository.EbooksRepository
Log zerolog.Logger
UsersRepo usersRepository.UsersRepository
}
// EbookWishlistsService define interface of IEbookWishlistsService
type EbookWishlistsService interface {
GetByUserId(authToken string, pagination *paginator.Pagination) (wishlists []*response.EbookWishlistResponse, paging paginator.Pagination, err error)
AddToWishlist(authToken string, ebookId uint) (err error)
RemoveFromWishlist(authToken string, ebookId uint) (err error)
IsInWishlist(authToken string, ebookId uint) (isInWishlist bool, err error)
}
// NewEbookWishlistsService init EbookWishlistsService
func NewEbookWishlistsService(
repo repository.EbookWishlistsRepository,
ebooksRepo repository.EbooksRepository,
log zerolog.Logger,
usersRepo usersRepository.UsersRepository) EbookWishlistsService {
return &ebookWishlistsService{
Repo: repo,
EbooksRepo: ebooksRepo,
Log: log,
UsersRepo: usersRepo,
}
}
// GetByUserId implement interface of EbookWishlistsService
func (_i *ebookWishlistsService) GetByUserId(authToken string, pagination *paginator.Pagination) (wishlists []*response.EbookWishlistResponse, paging paginator.Pagination, err error) {
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
if user == nil {
return nil, paginator.Pagination{}, errors.New("user not found")
}
wishlistsData, paging, err := _i.Repo.GetByUserId(user.ID, pagination)
if err != nil {
return nil, paging, err
}
wishlists = mapper.ToEbookWishlistResponseList(wishlistsData)
return wishlists, paging, nil
}
// AddToWishlist implement interface of EbookWishlistsService
func (_i *ebookWishlistsService) AddToWishlist(authToken string, ebookId uint) (err error) {
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
if user == nil {
return errors.New("user not found")
}
// Check if ebook exists
_, err = _i.EbooksRepo.FindOne(ebookId)
if err != nil {
return errors.New("ebook not found")
}
// Check if already in wishlist
existingWishlist, err := _i.Repo.FindByUserAndEbook(user.ID, ebookId)
if err == nil && existingWishlist != nil {
return errors.New("ebook already in wishlist")
}
// Add to wishlist
wishlist := &entity.EbookWishlists{
UserId: user.ID,
EbookId: ebookId,
}
_, err = _i.Repo.Create(wishlist)
if err != nil {
return err
}
// Update wishlist count
err = _i.EbooksRepo.UpdateWishlistCount(ebookId)
if err != nil {
_i.Log.Error().Err(err).Msg("Failed to update wishlist count")
}
return nil
}
// RemoveFromWishlist implement interface of EbookWishlistsService
func (_i *ebookWishlistsService) RemoveFromWishlist(authToken string, ebookId uint) (err error) {
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
if user == nil {
return errors.New("user not found")
}
// Check if in wishlist
existingWishlist, err := _i.Repo.FindByUserAndEbook(user.ID, ebookId)
if err != nil || existingWishlist == nil {
return errors.New("ebook not in wishlist")
}
// Remove from wishlist
err = _i.Repo.Delete(existingWishlist.ID)
if err != nil {
return err
}
return nil
}
// IsInWishlist implement interface of EbookWishlistsService
func (_i *ebookWishlistsService) IsInWishlist(authToken string, ebookId uint) (isInWishlist bool, err error) {
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
if user == nil {
return false, errors.New("user not found")
}
existingWishlist, err := _i.Repo.FindByUserAndEbook(user.ID, ebookId)
if err != nil {
return false, nil // Not in wishlist
}
return existingWishlist != nil, nil
}

View File

@ -0,0 +1,365 @@
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)
}

View File

@ -13,6 +13,7 @@ import (
"narasi-ahli-be/app/module/cities"
"narasi-ahli-be/app/module/custom_static_pages"
"narasi-ahli-be/app/module/districts"
"narasi-ahli-be/app/module/ebooks"
"narasi-ahli-be/app/module/education_history"
"narasi-ahli-be/app/module/feedbacks"
"narasi-ahli-be/app/module/magazine_files"
@ -50,6 +51,7 @@ type Router struct {
CitiesRouter *cities.CitiesRouter
CustomStaticPagesRouter *custom_static_pages.CustomStaticPagesRouter
DistrictsRouter *districts.DistrictsRouter
EbooksRouter *ebooks.EbooksRouter
FeedbacksRouter *feedbacks.FeedbacksRouter
MagazineFilesRouter *magazine_files.MagazineFilesRouter
MagazinesRouter *magazines.MagazinesRouter
@ -82,6 +84,7 @@ func NewRouter(
citiesRouter *cities.CitiesRouter,
customStaticPagesRouter *custom_static_pages.CustomStaticPagesRouter,
districtsRouter *districts.DistrictsRouter,
ebooksRouter *ebooks.EbooksRouter,
feedbacksRouter *feedbacks.FeedbacksRouter,
magazineFilesRouter *magazine_files.MagazineFilesRouter,
magazinesRouter *magazines.MagazinesRouter,
@ -112,6 +115,7 @@ func NewRouter(
CitiesRouter: citiesRouter,
CustomStaticPagesRouter: customStaticPagesRouter,
DistrictsRouter: districtsRouter,
EbooksRouter: ebooksRouter,
FeedbacksRouter: feedbacksRouter,
MagazineFilesRouter: magazineFilesRouter,
MagazinesRouter: magazinesRouter,
@ -152,6 +156,7 @@ func (r *Router) Register() {
r.CitiesRouter.RegisterCitiesRoutes()
r.CustomStaticPagesRouter.RegisterCustomStaticPagesRoutes()
r.DistrictsRouter.RegisterDistrictsRoutes()
r.EbooksRouter.RegisterEbooksRoutes()
r.FeedbacksRouter.RegisterFeedbacksRoutes()
r.MagazinesRouter.RegisterMagazinesRoutes()
r.MagazineFilesRouter.RegisterMagazineFilesRoutes()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,7 @@ import (
"narasi-ahli-be/app/module/cities"
"narasi-ahli-be/app/module/custom_static_pages"
"narasi-ahli-be/app/module/districts"
"narasi-ahli-be/app/module/ebooks"
"narasi-ahli-be/app/module/education_history"
"narasi-ahli-be/app/module/feedbacks"
"narasi-ahli-be/app/module/magazine_files"
@ -76,6 +77,7 @@ func main() {
cities.NewCitiesModule,
custom_static_pages.NewCustomStaticPagesModule,
districts.NewDistrictsModule,
ebooks.NewEbooksModule,
feedbacks.NewFeedbacksModule,
magazines.NewMagazinesModule,
magazine_files.NewMagazineFilesModule,