voucher
This commit is contained in:
parent
4f6208e479
commit
cfe690a40f
@ -94,6 +94,8 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
validators.accountValidator,
|
validators.accountValidator,
|
||||||
*services.orderIngredientTransactionService,
|
*services.orderIngredientTransactionService,
|
||||||
validators.orderIngredientTransactionValidator,
|
validators.orderIngredientTransactionValidator,
|
||||||
|
services.voucherService,
|
||||||
|
validators.voucherValidator,
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -167,6 +169,7 @@ type repositories struct {
|
|||||||
chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl
|
chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl
|
||||||
accountRepo *repository.AccountRepositoryImpl
|
accountRepo *repository.AccountRepositoryImpl
|
||||||
orderIngredientTransactionRepo *repository.OrderIngredientTransactionRepositoryImpl
|
orderIngredientTransactionRepo *repository.OrderIngredientTransactionRepositoryImpl
|
||||||
|
voucherRepo *repository.VoucherRepository
|
||||||
txManager *repository.TxManager
|
txManager *repository.TxManager
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,7 +203,8 @@ func (a *App) initRepositories() *repositories {
|
|||||||
chartOfAccountRepo: repository.NewChartOfAccountRepositoryImpl(a.db),
|
chartOfAccountRepo: repository.NewChartOfAccountRepositoryImpl(a.db),
|
||||||
accountRepo: repository.NewAccountRepositoryImpl(a.db),
|
accountRepo: repository.NewAccountRepositoryImpl(a.db),
|
||||||
orderIngredientTransactionRepo: repository.NewOrderIngredientTransactionRepositoryImpl(a.db).(*repository.OrderIngredientTransactionRepositoryImpl),
|
orderIngredientTransactionRepo: repository.NewOrderIngredientTransactionRepositoryImpl(a.db).(*repository.OrderIngredientTransactionRepositoryImpl),
|
||||||
txManager: repository.NewTxManager(a.db),
|
voucherRepo: repository.NewVoucherRepository(a.db),
|
||||||
|
txManager: repository.NewTxManager(a.db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,6 +233,7 @@ type processors struct {
|
|||||||
chartOfAccountProcessor *processor.ChartOfAccountProcessorImpl
|
chartOfAccountProcessor *processor.ChartOfAccountProcessorImpl
|
||||||
accountProcessor *processor.AccountProcessorImpl
|
accountProcessor *processor.AccountProcessorImpl
|
||||||
orderIngredientTransactionProcessor *processor.OrderIngredientTransactionProcessorImpl
|
orderIngredientTransactionProcessor *processor.OrderIngredientTransactionProcessorImpl
|
||||||
|
voucherProcessor *processor.VoucherProcessor
|
||||||
fileClient processor.FileClient
|
fileClient processor.FileClient
|
||||||
inventoryMovementService service.InventoryMovementService
|
inventoryMovementService service.InventoryMovementService
|
||||||
}
|
}
|
||||||
@ -262,6 +267,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
|||||||
chartOfAccountProcessor: processor.NewChartOfAccountProcessorImpl(repos.chartOfAccountRepo, repos.chartOfAccountTypeRepo),
|
chartOfAccountProcessor: processor.NewChartOfAccountProcessorImpl(repos.chartOfAccountRepo, repos.chartOfAccountTypeRepo),
|
||||||
accountProcessor: processor.NewAccountProcessorImpl(repos.accountRepo, repos.chartOfAccountRepo),
|
accountProcessor: processor.NewAccountProcessorImpl(repos.accountRepo, repos.chartOfAccountRepo),
|
||||||
orderIngredientTransactionProcessor: processor.NewOrderIngredientTransactionProcessorImpl(repos.orderIngredientTransactionRepo, repos.productRecipeRepo, repos.ingredientRepo, repos.unitRepo).(*processor.OrderIngredientTransactionProcessorImpl),
|
orderIngredientTransactionProcessor: processor.NewOrderIngredientTransactionProcessorImpl(repos.orderIngredientTransactionRepo, repos.productRecipeRepo, repos.ingredientRepo, repos.unitRepo).(*processor.OrderIngredientTransactionProcessorImpl),
|
||||||
|
voucherProcessor: processor.NewVoucherProcessor(repos.voucherRepo),
|
||||||
fileClient: fileClient,
|
fileClient: fileClient,
|
||||||
inventoryMovementService: inventoryMovementService,
|
inventoryMovementService: inventoryMovementService,
|
||||||
}
|
}
|
||||||
@ -294,6 +300,7 @@ type services struct {
|
|||||||
chartOfAccountService service.ChartOfAccountService
|
chartOfAccountService service.ChartOfAccountService
|
||||||
accountService service.AccountService
|
accountService service.AccountService
|
||||||
orderIngredientTransactionService *service.OrderIngredientTransactionService
|
orderIngredientTransactionService *service.OrderIngredientTransactionService
|
||||||
|
voucherService service.VoucherService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
||||||
@ -324,6 +331,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
chartOfAccountService := service.NewChartOfAccountService(processors.chartOfAccountProcessor)
|
chartOfAccountService := service.NewChartOfAccountService(processors.chartOfAccountProcessor)
|
||||||
accountService := service.NewAccountService(processors.accountProcessor)
|
accountService := service.NewAccountService(processors.accountProcessor)
|
||||||
orderIngredientTransactionService := service.NewOrderIngredientTransactionService(processors.orderIngredientTransactionProcessor, repos.txManager)
|
orderIngredientTransactionService := service.NewOrderIngredientTransactionService(processors.orderIngredientTransactionProcessor, repos.txManager)
|
||||||
|
voucherService := service.NewVoucherService(processors.voucherProcessor)
|
||||||
|
|
||||||
// Update order service with order ingredient transaction service
|
// Update order service with order ingredient transaction service
|
||||||
orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager)
|
orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager)
|
||||||
@ -355,6 +363,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
chartOfAccountService: chartOfAccountService,
|
chartOfAccountService: chartOfAccountService,
|
||||||
accountService: accountService,
|
accountService: accountService,
|
||||||
orderIngredientTransactionService: orderIngredientTransactionService,
|
orderIngredientTransactionService: orderIngredientTransactionService,
|
||||||
|
voucherService: voucherService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -388,6 +397,7 @@ type validators struct {
|
|||||||
chartOfAccountValidator *validator.ChartOfAccountValidatorImpl
|
chartOfAccountValidator *validator.ChartOfAccountValidatorImpl
|
||||||
accountValidator *validator.AccountValidatorImpl
|
accountValidator *validator.AccountValidatorImpl
|
||||||
orderIngredientTransactionValidator *validator.OrderIngredientTransactionValidatorImpl
|
orderIngredientTransactionValidator *validator.OrderIngredientTransactionValidatorImpl
|
||||||
|
voucherValidator validator.VoucherValidator
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initValidators() *validators {
|
func (a *App) initValidators() *validators {
|
||||||
@ -411,5 +421,6 @@ func (a *App) initValidators() *validators {
|
|||||||
chartOfAccountValidator: validator.NewChartOfAccountValidator().(*validator.ChartOfAccountValidatorImpl),
|
chartOfAccountValidator: validator.NewChartOfAccountValidator().(*validator.ChartOfAccountValidatorImpl),
|
||||||
accountValidator: validator.NewAccountValidator().(*validator.AccountValidatorImpl),
|
accountValidator: validator.NewAccountValidator().(*validator.AccountValidatorImpl),
|
||||||
orderIngredientTransactionValidator: validator.NewOrderIngredientTransactionValidator().(*validator.OrderIngredientTransactionValidatorImpl),
|
orderIngredientTransactionValidator: validator.NewOrderIngredientTransactionValidator().(*validator.OrderIngredientTransactionValidatorImpl),
|
||||||
|
voucherValidator: validator.NewVoucherValidator(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
51
internal/contract/voucher_contract.go
Normal file
51
internal/contract/voucher_contract.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package contract
|
||||||
|
|
||||||
|
type VoucherResponse struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
PhoneNumber string `json:"phone_number"`
|
||||||
|
VoucherCode string `json:"voucher_code"`
|
||||||
|
WinnerNumber int `json:"winner_number"`
|
||||||
|
IsWinner bool `json:"is_winner"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VoucherSpinResponse struct {
|
||||||
|
VoucherCode string `json:"voucher_code"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
PhoneNumber string `json:"phone_number"` // This will be masked
|
||||||
|
IsWinner bool `json:"is_winner"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListVouchersForSpinRequest struct {
|
||||||
|
Limit int `json:"limit" validate:"min=1,max=50"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListVouchersForSpinResponse struct {
|
||||||
|
Vouchers []VoucherSpinResponse `json:"vouchers"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginatedVoucherResponse struct {
|
||||||
|
Data []VoucherResponse `json:"data"`
|
||||||
|
TotalCount int `json:"total_count"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VoucherRow struct {
|
||||||
|
RowNumber int `json:"row_number"`
|
||||||
|
Vouchers []VoucherSpinResponse `json:"vouchers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListVouchersByRowsRequest struct {
|
||||||
|
Rows int `json:"rows" validate:"min=1,max=10"`
|
||||||
|
WinnerNumber *int `json:"winner_number,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListVouchersByRowsResponse struct {
|
||||||
|
Rows []VoucherRow `json:"rows"`
|
||||||
|
TotalRows int `json:"total_rows"`
|
||||||
|
TotalVouchers int `json:"total_vouchers"`
|
||||||
|
}
|
||||||
@ -23,6 +23,7 @@ func GetAllEntities() []interface{} {
|
|||||||
&PurchaseOrderItem{},
|
&PurchaseOrderItem{},
|
||||||
&PurchaseOrderAttachment{},
|
&PurchaseOrderAttachment{},
|
||||||
&IngredientUnitConverter{},
|
&IngredientUnitConverter{},
|
||||||
|
&Voucher{},
|
||||||
// Analytics entities are not database tables, they are query results
|
// Analytics entities are not database tables, they are query results
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
internal/entities/voucher.go
Normal file
15
internal/entities/voucher.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
type Voucher struct {
|
||||||
|
ID int64 `gorm:"type:bigserial;primary_key;autoIncrement" json:"id"`
|
||||||
|
Name string `gorm:"not null;size:100" json:"name" validate:"required"`
|
||||||
|
Email string `gorm:"not null;size:255" json:"email" validate:"required"`
|
||||||
|
PhoneNumber string `gorm:"not null;size:20" json:"phone_number" validate:"required"`
|
||||||
|
VoucherCode string `gorm:"not null;size:50" json:"voucher_code" validate:"required"`
|
||||||
|
WinnerNumber int `gorm:"not null;default:0" json:"winner_number" validate:"required"`
|
||||||
|
IsWinner bool `gorm:"not null;default:false" json:"is_winner"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Voucher) TableName() string {
|
||||||
|
return "vouchers"
|
||||||
|
}
|
||||||
177
internal/handler/voucher_handler.go
Normal file
177
internal/handler/voucher_handler.go
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/logger"
|
||||||
|
"apskel-pos-be/internal/service"
|
||||||
|
"apskel-pos-be/internal/util"
|
||||||
|
"apskel-pos-be/internal/validator"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VoucherHandler struct {
|
||||||
|
voucherService service.VoucherService
|
||||||
|
voucherValidator validator.VoucherValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVoucherHandler(
|
||||||
|
voucherService service.VoucherService,
|
||||||
|
voucherValidator validator.VoucherValidator,
|
||||||
|
) *VoucherHandler {
|
||||||
|
return &VoucherHandler{
|
||||||
|
voucherService: voucherService,
|
||||||
|
voucherValidator: voucherValidator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *VoucherHandler) GetRandomVouchersForSpin(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
req := &contract.ListVouchersForSpinRequest{
|
||||||
|
Limit: 10, // Default limit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse query parameters
|
||||||
|
if limitStr := c.Query("limit"); limitStr != "" {
|
||||||
|
if limit, err := strconv.Atoi(limitStr); err == nil {
|
||||||
|
req.Limit = limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validationError, validationErrorCode := h.voucherValidator.ValidateListVouchersForSpinRequest(req)
|
||||||
|
if validationError != nil {
|
||||||
|
logger.FromContext(ctx).WithError(validationError).Error("VoucherHandler::GetRandomVouchersForSpin -> request validation failed")
|
||||||
|
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "VoucherHandler::GetRandomVouchersForSpin")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vouchersResponse, err := h.voucherService.GetRandomVouchersForSpin(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("VoucherHandler::GetRandomVouchersForSpin -> Failed to get random vouchers from service")
|
||||||
|
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{errorResp}), "VoucherHandler::GetRandomVouchersForSpin")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.FromContext(ctx).Infof("VoucherHandler::GetRandomVouchersForSpin -> Successfully retrieved %d vouchers for spin", vouchersResponse.Count)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(vouchersResponse), "VoucherHandler::GetRandomVouchersForSpin")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *VoucherHandler) GetRandomVouchersByRows(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
req := &contract.ListVouchersByRowsRequest{
|
||||||
|
Rows: 4, // Default 4 rows
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse query parameters
|
||||||
|
if rowsStr := c.Query("rows"); rowsStr != "" {
|
||||||
|
if rows, err := strconv.Atoi(rowsStr); err == nil {
|
||||||
|
req.Rows = rows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse winner_number parameter (optional)
|
||||||
|
if winnerNumberStr := c.Query("winner_number"); winnerNumberStr != "" {
|
||||||
|
if wn, err := strconv.Atoi(winnerNumberStr); err == nil {
|
||||||
|
req.WinnerNumber = &wn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validationError, validationErrorCode := h.voucherValidator.ValidateListVouchersByRowsRequest(req)
|
||||||
|
if validationError != nil {
|
||||||
|
logger.FromContext(ctx).WithError(validationError).Error("VoucherHandler::GetRandomVouchersByRows -> request validation failed")
|
||||||
|
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "VoucherHandler::GetRandomVouchersByRows")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vouchersResponse, err := h.voucherService.GetRandomVouchersByRows(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("VoucherHandler::GetRandomVouchersByRows -> Failed to get random vouchers by rows from service")
|
||||||
|
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{errorResp}), "VoucherHandler::GetRandomVouchersByRows")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.FromContext(ctx).Infof("VoucherHandler::GetRandomVouchersByRows -> Successfully retrieved %d rows with %d total vouchers", vouchersResponse.TotalRows, vouchersResponse.TotalVouchers)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(vouchersResponse), "VoucherHandler::GetRandomVouchersByRows")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *VoucherHandler) GetVoucherByID(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
voucherID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("VoucherHandler::GetVoucherByID -> Invalid voucher ID")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid voucher ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "VoucherHandler::GetVoucherByID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
voucherResponse, err := h.voucherService.GetVoucherByID(c.Request.Context(), voucherID)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("VoucherHandler::GetVoucherByID -> Failed to get voucher from service")
|
||||||
|
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{errorResp}), "VoucherHandler::GetVoucherByID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(voucherResponse), "VoucherHandler::GetVoucherByID")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *VoucherHandler) GetVoucherByCode(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
voucherCode := c.Param("code")
|
||||||
|
if voucherCode == "" {
|
||||||
|
logger.FromContext(ctx).Error("VoucherHandler::GetVoucherByCode -> Missing voucher code")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Voucher code is required")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "VoucherHandler::GetVoucherByCode")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
voucherResponse, err := h.voucherService.GetVoucherByCode(c.Request.Context(), voucherCode)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("VoucherHandler::GetVoucherByCode -> Failed to get voucher from service")
|
||||||
|
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{errorResp}), "VoucherHandler::GetVoucherByCode")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(voucherResponse), "VoucherHandler::GetVoucherByCode")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *VoucherHandler) ListVouchers(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
page := 1
|
||||||
|
limit := 10
|
||||||
|
|
||||||
|
// Parse query parameters
|
||||||
|
if pageStr := c.Query("page"); pageStr != "" {
|
||||||
|
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
|
||||||
|
page = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if limitStr := c.Query("limit"); limitStr != "" {
|
||||||
|
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
|
||||||
|
limit = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vouchersResponse, err := h.voucherService.ListVouchers(c.Request.Context(), page, limit)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("VoucherHandler::ListVouchers -> Failed to list vouchers from service")
|
||||||
|
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{errorResp}), "VoucherHandler::ListVouchers")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(vouchersResponse), "VoucherHandler::ListVouchers")
|
||||||
|
}
|
||||||
120
internal/mappers/voucher_mapper.go
Normal file
120
internal/mappers/voucher_mapper.go
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
package mappers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func VoucherEntityToResponse(voucher *entities.Voucher) *models.VoucherResponse {
|
||||||
|
return &models.VoucherResponse{
|
||||||
|
ID: voucher.ID,
|
||||||
|
Name: voucher.Name,
|
||||||
|
Email: voucher.Email,
|
||||||
|
PhoneNumber: voucher.PhoneNumber,
|
||||||
|
VoucherCode: voucher.VoucherCode,
|
||||||
|
WinnerNumber: voucher.WinnerNumber,
|
||||||
|
IsWinner: voucher.IsWinner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func VoucherModelToContract(voucher *models.VoucherResponse) *contract.VoucherResponse {
|
||||||
|
return &contract.VoucherResponse{
|
||||||
|
ID: voucher.ID,
|
||||||
|
Name: voucher.Name,
|
||||||
|
Email: voucher.Email,
|
||||||
|
PhoneNumber: voucher.PhoneNumber,
|
||||||
|
VoucherCode: voucher.VoucherCode,
|
||||||
|
WinnerNumber: voucher.WinnerNumber,
|
||||||
|
IsWinner: voucher.IsWinner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func VoucherEntityToSpinResponse(voucher *entities.Voucher) *models.VoucherSpinResponse {
|
||||||
|
maskedPhone := maskPhoneNumber(&voucher.PhoneNumber)
|
||||||
|
return &models.VoucherSpinResponse{
|
||||||
|
VoucherCode: voucher.VoucherCode,
|
||||||
|
Name: voucher.Name,
|
||||||
|
PhoneNumber: maskedPhone,
|
||||||
|
IsWinner: voucher.IsWinner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func VoucherSpinModelToContract(voucher *models.VoucherSpinResponse) *contract.VoucherSpinResponse {
|
||||||
|
return &contract.VoucherSpinResponse{
|
||||||
|
VoucherCode: voucher.VoucherCode,
|
||||||
|
Name: voucher.Name,
|
||||||
|
PhoneNumber: voucher.PhoneNumber,
|
||||||
|
IsWinner: voucher.IsWinner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListVouchersForSpinRequestToModel(req *contract.ListVouchersForSpinRequest) *models.ListVouchersForSpinRequest {
|
||||||
|
return &models.ListVouchersForSpinRequest{
|
||||||
|
Limit: req.Limit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListVouchersForSpinResponseToContract(resp *models.ListVouchersForSpinResponse) *contract.ListVouchersForSpinResponse {
|
||||||
|
vouchers := make([]contract.VoucherSpinResponse, len(resp.Vouchers))
|
||||||
|
for i, voucher := range resp.Vouchers {
|
||||||
|
vouchers[i] = *VoucherSpinModelToContract(&voucher)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.ListVouchersForSpinResponse{
|
||||||
|
Vouchers: vouchers,
|
||||||
|
Count: resp.Count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func VoucherRowModelToContract(row *models.VoucherRow) *contract.VoucherRow {
|
||||||
|
vouchers := make([]contract.VoucherSpinResponse, len(row.Vouchers))
|
||||||
|
for i, voucher := range row.Vouchers {
|
||||||
|
vouchers[i] = *VoucherSpinModelToContract(&voucher)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.VoucherRow{
|
||||||
|
RowNumber: row.RowNumber,
|
||||||
|
Vouchers: vouchers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListVouchersByRowsRequestToModel(req *contract.ListVouchersByRowsRequest) *models.ListVouchersByRowsRequest {
|
||||||
|
return &models.ListVouchersByRowsRequest{
|
||||||
|
Rows: req.Rows,
|
||||||
|
WinnerNumber: req.WinnerNumber,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListVouchersByRowsResponseToContract(resp *models.ListVouchersByRowsResponse) *contract.ListVouchersByRowsResponse {
|
||||||
|
rows := make([]contract.VoucherRow, len(resp.Rows))
|
||||||
|
for i, row := range resp.Rows {
|
||||||
|
rows[i] = *VoucherRowModelToContract(&row)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.ListVouchersByRowsResponse{
|
||||||
|
Rows: rows,
|
||||||
|
TotalRows: resp.TotalRows,
|
||||||
|
TotalVouchers: resp.TotalVouchers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// maskPhoneNumber masks phone number for privacy
|
||||||
|
func maskPhoneNumber(phone *string) string {
|
||||||
|
if phone == nil || *phone == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
phoneStr := *phone
|
||||||
|
if len(phoneStr) <= 4 {
|
||||||
|
return strings.Repeat("*", len(phoneStr))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show first 2 and last 2 characters, mask the middle
|
||||||
|
start := phoneStr[:2]
|
||||||
|
end := phoneStr[len(phoneStr)-2:]
|
||||||
|
middle := strings.Repeat("*", len(phoneStr)-4)
|
||||||
|
|
||||||
|
return start + middle + end
|
||||||
|
}
|
||||||
51
internal/models/voucher.go
Normal file
51
internal/models/voucher.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type VoucherResponse struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
PhoneNumber string `json:"phone_number"`
|
||||||
|
VoucherCode string `json:"voucher_code"`
|
||||||
|
WinnerNumber int `json:"winner_number"`
|
||||||
|
IsWinner bool `json:"is_winner"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VoucherSpinResponse struct {
|
||||||
|
VoucherCode string `json:"voucher_code"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
PhoneNumber string `json:"phone_number"` // This will be masked
|
||||||
|
IsWinner bool `json:"is_winner"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListVouchersForSpinRequest struct {
|
||||||
|
Limit int `json:"limit" validate:"min=1,max=50"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListVouchersForSpinResponse struct {
|
||||||
|
Vouchers []VoucherSpinResponse `json:"vouchers"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginatedVoucherResponse struct {
|
||||||
|
Data []VoucherResponse `json:"data"`
|
||||||
|
TotalCount int `json:"total_count"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VoucherRow struct {
|
||||||
|
RowNumber int `json:"row_number"`
|
||||||
|
Vouchers []VoucherSpinResponse `json:"vouchers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListVouchersByRowsRequest struct {
|
||||||
|
Rows int `json:"rows" validate:"min=1,max=10"`
|
||||||
|
WinnerNumber *int `json:"winner_number,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListVouchersByRowsResponse struct {
|
||||||
|
Rows []VoucherRow `json:"rows"`
|
||||||
|
TotalRows int `json:"total_rows"`
|
||||||
|
TotalVouchers int `json:"total_vouchers"`
|
||||||
|
}
|
||||||
156
internal/processor/voucher_processor.go
Normal file
156
internal/processor/voucher_processor.go
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/mappers"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
"apskel-pos-be/internal/repository"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VoucherProcessor struct {
|
||||||
|
voucherRepo *repository.VoucherRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVoucherProcessor(voucherRepo *repository.VoucherRepository) *VoucherProcessor {
|
||||||
|
return &VoucherProcessor{
|
||||||
|
voucherRepo: voucherRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRandomVouchersForSpin retrieves random vouchers for spin feature
|
||||||
|
func (p *VoucherProcessor) GetRandomVouchersForSpin(ctx context.Context, req *models.ListVouchersForSpinRequest) (*models.ListVouchersForSpinResponse, error) {
|
||||||
|
// Set default limit if not provided
|
||||||
|
limit := req.Limit
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 10 // Default limit
|
||||||
|
}
|
||||||
|
if limit > 50 {
|
||||||
|
limit = 50 // Max limit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get random vouchers from repository
|
||||||
|
vouchers, err := p.voucherRepo.GetRandomVouchers(ctx, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get random vouchers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to spin response format
|
||||||
|
voucherResponses := make([]models.VoucherSpinResponse, len(vouchers))
|
||||||
|
for i, voucher := range vouchers {
|
||||||
|
voucherResponses[i] = *mappers.VoucherEntityToSpinResponse(&voucher)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.ListVouchersForSpinResponse{
|
||||||
|
Vouchers: voucherResponses,
|
||||||
|
Count: len(voucherResponses),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVoucherByID retrieves a voucher by ID
|
||||||
|
func (p *VoucherProcessor) GetVoucherByID(ctx context.Context, voucherID int64) (*models.VoucherResponse, error) {
|
||||||
|
voucher, err := p.voucherRepo.GetByID(ctx, voucherID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("voucher not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappers.VoucherEntityToResponse(voucher), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVoucherByCode retrieves a voucher by voucher code
|
||||||
|
func (p *VoucherProcessor) GetVoucherByCode(ctx context.Context, voucherCode string) (*models.VoucherResponse, error) {
|
||||||
|
voucher, err := p.voucherRepo.GetByVoucherCode(ctx, voucherCode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("voucher not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappers.VoucherEntityToResponse(voucher), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListVouchers retrieves vouchers with pagination
|
||||||
|
func (p *VoucherProcessor) ListVouchers(ctx context.Context, page, limit int) (*models.PaginatedVoucherResponse, error) {
|
||||||
|
offset := (page - 1) * limit
|
||||||
|
|
||||||
|
vouchers, total, err := p.voucherRepo.List(ctx, offset, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to list vouchers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to response format
|
||||||
|
voucherResponses := make([]models.VoucherResponse, len(vouchers))
|
||||||
|
for i, voucher := range vouchers {
|
||||||
|
voucherResponses[i] = *mappers.VoucherEntityToResponse(&voucher)
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPages := int((total + int64(limit) - 1) / int64(limit))
|
||||||
|
|
||||||
|
return &models.PaginatedVoucherResponse{
|
||||||
|
Data: voucherResponses,
|
||||||
|
TotalCount: int(total),
|
||||||
|
Page: page,
|
||||||
|
Limit: limit,
|
||||||
|
TotalPages: totalPages,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRandomVouchersByRows retrieves random vouchers organized into rows
|
||||||
|
func (p *VoucherProcessor) GetRandomVouchersByRows(ctx context.Context, req *models.ListVouchersByRowsRequest) (*models.ListVouchersByRowsResponse, error) {
|
||||||
|
// Set default values if not provided
|
||||||
|
rows := req.Rows
|
||||||
|
if rows <= 0 {
|
||||||
|
rows = 4 // Default to 4 rows
|
||||||
|
}
|
||||||
|
if rows > 10 {
|
||||||
|
rows = 10 // Max 10 rows
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get random vouchers organized by rows
|
||||||
|
voucherRows, err := p.voucherRepo.GetRandomVouchersByRows(ctx, rows, req.WinnerNumber)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get random vouchers by rows: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to response format and select winners
|
||||||
|
responseRows := make([]models.VoucherRow, len(voucherRows))
|
||||||
|
totalVouchers := 0
|
||||||
|
|
||||||
|
for i, row := range voucherRows {
|
||||||
|
vouchers := make([]models.VoucherSpinResponse, len(row))
|
||||||
|
|
||||||
|
// Select a random winner from this row if there are vouchers
|
||||||
|
if len(row) > 0 {
|
||||||
|
// Select random winner
|
||||||
|
rand.Seed(time.Now().UnixNano() + int64(i)) // Add row index for different seed
|
||||||
|
winnerIndex := rand.Intn(len(row))
|
||||||
|
|
||||||
|
// Mark as winner in database
|
||||||
|
err := p.voucherRepo.MarkAsWinner(ctx, row[winnerIndex].ID)
|
||||||
|
if err != nil {
|
||||||
|
// Log error but continue - don't fail the entire request
|
||||||
|
fmt.Printf("Failed to mark voucher %d as winner: %v\n", row[winnerIndex].ID, err)
|
||||||
|
} else {
|
||||||
|
// Update the voucher object to reflect the change
|
||||||
|
row[winnerIndex].IsWinner = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert all vouchers to response format
|
||||||
|
for j, voucher := range row {
|
||||||
|
vouchers[j] = *mappers.VoucherEntityToSpinResponse(&voucher)
|
||||||
|
}
|
||||||
|
|
||||||
|
responseRows[i] = models.VoucherRow{
|
||||||
|
RowNumber: i + 1, // Row numbers start from 1
|
||||||
|
Vouchers: vouchers,
|
||||||
|
}
|
||||||
|
totalVouchers += len(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.ListVouchersByRowsResponse{
|
||||||
|
Rows: responseRows,
|
||||||
|
TotalRows: len(responseRows),
|
||||||
|
TotalVouchers: totalVouchers,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
228
internal/repository/voucher_repository.go
Normal file
228
internal/repository/voucher_repository.go
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"context"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VoucherRepository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVoucherRepository(db *gorm.DB) *VoucherRepository {
|
||||||
|
return &VoucherRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VoucherRepository) Create(ctx context.Context, voucher *entities.Voucher) error {
|
||||||
|
return r.db.WithContext(ctx).Create(voucher).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VoucherRepository) GetByID(ctx context.Context, id int64) (*entities.Voucher, error) {
|
||||||
|
var voucher entities.Voucher
|
||||||
|
err := r.db.WithContext(ctx).Where("id = ?", id).First(&voucher).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &voucher, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VoucherRepository) GetByVoucherCode(ctx context.Context, voucherCode string) (*entities.Voucher, error) {
|
||||||
|
var voucher entities.Voucher
|
||||||
|
err := r.db.WithContext(ctx).Where("voucher_code = ?", voucherCode).First(&voucher).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &voucher, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VoucherRepository) List(ctx context.Context, offset, limit int) ([]entities.Voucher, int64, error) {
|
||||||
|
var vouchers []entities.Voucher
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
query := r.db.WithContext(ctx)
|
||||||
|
|
||||||
|
if err := query.Model(&entities.Voucher{}).Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := query.Offset(offset).Limit(limit).Find(&vouchers).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return vouchers, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VoucherRepository) GetRandomVouchers(ctx context.Context, limit int) ([]entities.Voucher, error) {
|
||||||
|
var vouchers []entities.Voucher
|
||||||
|
|
||||||
|
// First, get the total count
|
||||||
|
var total int64
|
||||||
|
if err := r.db.WithContext(ctx).Model(&entities.Voucher{}).Count(&total).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if total == 0 {
|
||||||
|
return vouchers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have fewer vouchers than requested, return all
|
||||||
|
if int(total) <= limit {
|
||||||
|
err := r.db.WithContext(ctx).Find(&vouchers).Error
|
||||||
|
return vouchers, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate random offsets to get random vouchers
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
usedOffsets := make(map[int]bool)
|
||||||
|
|
||||||
|
for len(vouchers) < limit && len(usedOffsets) < int(total) {
|
||||||
|
offset := rand.Intn(int(total))
|
||||||
|
if usedOffsets[offset] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
usedOffsets[offset] = true
|
||||||
|
|
||||||
|
var voucher entities.Voucher
|
||||||
|
err := r.db.WithContext(ctx).Offset(offset).Limit(1).First(&voucher).Error
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
vouchers = append(vouchers, voucher)
|
||||||
|
}
|
||||||
|
|
||||||
|
return vouchers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VoucherRepository) GetRandomVouchersByRows(ctx context.Context, rows int, winnerNumber *int) ([][]entities.Voucher, error) {
|
||||||
|
var allVouchers []entities.Voucher
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// First, try to get vouchers based on winner_number parameter
|
||||||
|
if winnerNumber != nil {
|
||||||
|
// If winner_number is provided, filter by it and exclude already won vouchers
|
||||||
|
query := r.db.WithContext(ctx).Where("winner_number = ? AND is_winner = ?", *winnerNumber, false)
|
||||||
|
err = query.Find(&allVouchers).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no vouchers found for the specified winner_number, fallback to winner_number = 0
|
||||||
|
if len(allVouchers) == 0 {
|
||||||
|
fallbackQuery := r.db.WithContext(ctx).Where("winner_number = ? AND is_winner = ?", 0, false)
|
||||||
|
err = fallbackQuery.Find(&allVouchers).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still no vouchers found, try without is_winner filter for winner_number = 0
|
||||||
|
if len(allVouchers) == 0 {
|
||||||
|
fallbackQuery2 := r.db.WithContext(ctx).Where("winner_number = ?", 0)
|
||||||
|
err = fallbackQuery2.Find(&allVouchers).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still no vouchers found, get any vouchers available
|
||||||
|
if len(allVouchers) == 0 {
|
||||||
|
err = r.db.WithContext(ctx).Find(&allVouchers).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If winner_number is not provided, use default winner_number = 0 and exclude already won vouchers
|
||||||
|
query := r.db.WithContext(ctx).Where("winner_number = ? AND is_winner = ?", 0, false)
|
||||||
|
err = query.Find(&allVouchers).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no vouchers found, try without is_winner filter for winner_number = 0
|
||||||
|
if len(allVouchers) == 0 {
|
||||||
|
fallbackQuery := r.db.WithContext(ctx).Where("winner_number = ?", 0)
|
||||||
|
err = fallbackQuery.Find(&allVouchers).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still no vouchers found, get any vouchers available
|
||||||
|
if len(allVouchers) == 0 {
|
||||||
|
err = r.db.WithContext(ctx).Find(&allVouchers).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allVouchers) == 0 {
|
||||||
|
// Return empty rows if no vouchers available
|
||||||
|
return make([][]entities.Voucher, rows), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shuffle the vouchers for randomness
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
rand.Shuffle(len(allVouchers), func(i, j int) {
|
||||||
|
allVouchers[i], allVouchers[j] = allVouchers[j], allVouchers[i]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Calculate vouchers per row (distribute evenly)
|
||||||
|
vouchersPerRow := len(allVouchers) / rows
|
||||||
|
if vouchersPerRow == 0 {
|
||||||
|
vouchersPerRow = 1 // At least 1 voucher per row
|
||||||
|
}
|
||||||
|
|
||||||
|
// Organize vouchers into rows
|
||||||
|
voucherRows := make([][]entities.Voucher, rows)
|
||||||
|
voucherIndex := 0
|
||||||
|
|
||||||
|
for i := 0; i < rows; i++ {
|
||||||
|
// Calculate how many vouchers this row should have
|
||||||
|
remainingRows := rows - i
|
||||||
|
remainingVouchers := len(allVouchers) - voucherIndex
|
||||||
|
|
||||||
|
// Distribute remaining vouchers evenly among remaining rows
|
||||||
|
rowSize := remainingVouchers / remainingRows
|
||||||
|
if remainingVouchers%remainingRows > 0 {
|
||||||
|
rowSize++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we don't exceed available vouchers
|
||||||
|
if voucherIndex+rowSize > len(allVouchers) {
|
||||||
|
rowSize = len(allVouchers) - voucherIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the row
|
||||||
|
if voucherIndex < len(allVouchers) {
|
||||||
|
endIdx := voucherIndex + rowSize
|
||||||
|
if endIdx > len(allVouchers) {
|
||||||
|
endIdx = len(allVouchers)
|
||||||
|
}
|
||||||
|
voucherRows[i] = allVouchers[voucherIndex:endIdx]
|
||||||
|
voucherIndex = endIdx
|
||||||
|
} else {
|
||||||
|
voucherRows[i] = []entities.Voucher{} // Empty row
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return voucherRows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VoucherRepository) Update(ctx context.Context, voucher *entities.Voucher) error {
|
||||||
|
return r.db.WithContext(ctx).Save(voucher).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VoucherRepository) Delete(ctx context.Context, id int64) error {
|
||||||
|
return r.db.WithContext(ctx).Delete(&entities.Voucher{}, id).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VoucherRepository) MarkAsWinner(ctx context.Context, id int64) error {
|
||||||
|
return r.db.WithContext(ctx).Model(&entities.Voucher{}).Where("id = ?", id).Update("is_winner", true).Error
|
||||||
|
}
|
||||||
@ -40,6 +40,7 @@ type Router struct {
|
|||||||
chartOfAccountHandler *handler.ChartOfAccountHandler
|
chartOfAccountHandler *handler.ChartOfAccountHandler
|
||||||
accountHandler *handler.AccountHandler
|
accountHandler *handler.AccountHandler
|
||||||
orderIngredientTransactionHandler *handler.OrderIngredientTransactionHandler
|
orderIngredientTransactionHandler *handler.OrderIngredientTransactionHandler
|
||||||
|
voucherHandler *handler.VoucherHandler
|
||||||
authMiddleware *middleware.AuthMiddleware
|
authMiddleware *middleware.AuthMiddleware
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +91,9 @@ func NewRouter(cfg *config.Config,
|
|||||||
accountService service.AccountService,
|
accountService service.AccountService,
|
||||||
accountValidator validator.AccountValidator,
|
accountValidator validator.AccountValidator,
|
||||||
orderIngredientTransactionService service.OrderIngredientTransactionService,
|
orderIngredientTransactionService service.OrderIngredientTransactionService,
|
||||||
orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator) *Router {
|
orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator,
|
||||||
|
voucherService service.VoucherService,
|
||||||
|
voucherValidator validator.VoucherValidator) *Router {
|
||||||
|
|
||||||
return &Router{
|
return &Router{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
@ -120,6 +123,7 @@ func NewRouter(cfg *config.Config,
|
|||||||
chartOfAccountHandler: handler.NewChartOfAccountHandler(chartOfAccountService, chartOfAccountValidator),
|
chartOfAccountHandler: handler.NewChartOfAccountHandler(chartOfAccountService, chartOfAccountValidator),
|
||||||
accountHandler: handler.NewAccountHandler(accountService, accountValidator),
|
accountHandler: handler.NewAccountHandler(accountService, accountValidator),
|
||||||
orderIngredientTransactionHandler: handler.NewOrderIngredientTransactionHandler(&orderIngredientTransactionService, orderIngredientTransactionValidator),
|
orderIngredientTransactionHandler: handler.NewOrderIngredientTransactionHandler(&orderIngredientTransactionService, orderIngredientTransactionValidator),
|
||||||
|
voucherHandler: handler.NewVoucherHandler(voucherService, voucherValidator),
|
||||||
authMiddleware: authMiddleware,
|
authMiddleware: authMiddleware,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -426,6 +430,16 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
orderIngredientTransactions.POST("/bulk", r.orderIngredientTransactionHandler.BulkCreateOrderIngredientTransactions)
|
orderIngredientTransactions.POST("/bulk", r.orderIngredientTransactionHandler.BulkCreateOrderIngredientTransactions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vouchers := protected.Group("/vouchers")
|
||||||
|
vouchers.Use(r.authMiddleware.RequireAdminOrManager())
|
||||||
|
{
|
||||||
|
vouchers.GET("/spin", r.voucherHandler.GetRandomVouchersForSpin)
|
||||||
|
vouchers.GET("/rows", r.voucherHandler.GetRandomVouchersByRows)
|
||||||
|
vouchers.GET("", r.voucherHandler.ListVouchers)
|
||||||
|
vouchers.GET("/:id", r.voucherHandler.GetVoucherByID)
|
||||||
|
vouchers.GET("/code/:code", r.voucherHandler.GetVoucherByCode)
|
||||||
|
}
|
||||||
|
|
||||||
outlets := protected.Group("/outlets")
|
outlets := protected.Group("/outlets")
|
||||||
outlets.Use(r.authMiddleware.RequireAdminOrManager())
|
outlets.Use(r.authMiddleware.RequireAdminOrManager())
|
||||||
{
|
{
|
||||||
|
|||||||
93
internal/service/voucher_service.go
Normal file
93
internal/service/voucher_service.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/mappers"
|
||||||
|
"apskel-pos-be/internal/processor"
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VoucherService interface {
|
||||||
|
GetRandomVouchersForSpin(ctx context.Context, req *contract.ListVouchersForSpinRequest) (*contract.ListVouchersForSpinResponse, error)
|
||||||
|
GetRandomVouchersByRows(ctx context.Context, req *contract.ListVouchersByRowsRequest) (*contract.ListVouchersByRowsResponse, error)
|
||||||
|
GetVoucherByID(ctx context.Context, voucherID int64) (*contract.VoucherResponse, error)
|
||||||
|
GetVoucherByCode(ctx context.Context, voucherCode string) (*contract.VoucherResponse, error)
|
||||||
|
ListVouchers(ctx context.Context, page, limit int) (*contract.PaginatedVoucherResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type VoucherServiceImpl struct {
|
||||||
|
voucherProcessor *processor.VoucherProcessor
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVoucherService(voucherProcessor *processor.VoucherProcessor) *VoucherServiceImpl {
|
||||||
|
return &VoucherServiceImpl{
|
||||||
|
voucherProcessor: voucherProcessor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *VoucherServiceImpl) GetRandomVouchersForSpin(ctx context.Context, req *contract.ListVouchersForSpinRequest) (*contract.ListVouchersForSpinResponse, error) {
|
||||||
|
modelReq := mappers.ListVouchersForSpinRequestToModel(req)
|
||||||
|
|
||||||
|
response, err := s.voucherProcessor.GetRandomVouchersForSpin(ctx, modelReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
contractResponse := mappers.ListVouchersForSpinResponseToContract(response)
|
||||||
|
return contractResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *VoucherServiceImpl) GetRandomVouchersByRows(ctx context.Context, req *contract.ListVouchersByRowsRequest) (*contract.ListVouchersByRowsResponse, error) {
|
||||||
|
modelReq := mappers.ListVouchersByRowsRequestToModel(req)
|
||||||
|
|
||||||
|
response, err := s.voucherProcessor.GetRandomVouchersByRows(ctx, modelReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
contractResponse := mappers.ListVouchersByRowsResponseToContract(response)
|
||||||
|
return contractResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *VoucherServiceImpl) GetVoucherByID(ctx context.Context, voucherID int64) (*contract.VoucherResponse, error) {
|
||||||
|
response, err := s.voucherProcessor.GetVoucherByID(ctx, voucherID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
contractResponse := mappers.VoucherModelToContract(response)
|
||||||
|
return contractResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *VoucherServiceImpl) GetVoucherByCode(ctx context.Context, voucherCode string) (*contract.VoucherResponse, error) {
|
||||||
|
response, err := s.voucherProcessor.GetVoucherByCode(ctx, voucherCode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
contractResponse := mappers.VoucherModelToContract(response)
|
||||||
|
return contractResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *VoucherServiceImpl) ListVouchers(ctx context.Context, page, limit int) (*contract.PaginatedVoucherResponse, error) {
|
||||||
|
response, err := s.voucherProcessor.ListVouchers(ctx, page, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to contract response
|
||||||
|
vouchers := make([]contract.VoucherResponse, len(response.Data))
|
||||||
|
for i, voucher := range response.Data {
|
||||||
|
vouchers[i] = *mappers.VoucherModelToContract(&voucher)
|
||||||
|
}
|
||||||
|
|
||||||
|
contractResponse := &contract.PaginatedVoucherResponse{
|
||||||
|
Data: vouchers,
|
||||||
|
TotalCount: response.TotalCount,
|
||||||
|
Page: response.Page,
|
||||||
|
Limit: response.Limit,
|
||||||
|
TotalPages: response.TotalPages,
|
||||||
|
}
|
||||||
|
|
||||||
|
return contractResponse, nil
|
||||||
|
}
|
||||||
41
internal/validator/voucher_validator.go
Normal file
41
internal/validator/voucher_validator.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VoucherValidator interface {
|
||||||
|
ValidateListVouchersForSpinRequest(req *contract.ListVouchersForSpinRequest) (error, string)
|
||||||
|
ValidateListVouchersByRowsRequest(req *contract.ListVouchersByRowsRequest) (error, string)
|
||||||
|
}
|
||||||
|
|
||||||
|
type VoucherValidatorImpl struct{}
|
||||||
|
|
||||||
|
func NewVoucherValidator() VoucherValidator {
|
||||||
|
return &VoucherValidatorImpl{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoucherValidatorImpl) ValidateListVouchersForSpinRequest(req *contract.ListVouchersForSpinRequest) (error, string) {
|
||||||
|
if req.Limit < 1 {
|
||||||
|
return errors.New("limit must be at least 1"), "INVALID_LIMIT"
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Limit > 50 {
|
||||||
|
return errors.New("limit cannot exceed 50"), "INVALID_LIMIT"
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VoucherValidatorImpl) ValidateListVouchersByRowsRequest(req *contract.ListVouchersByRowsRequest) (error, string) {
|
||||||
|
if req.Rows < 1 {
|
||||||
|
return errors.New("rows must be at least 1"), "INVALID_ROWS"
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Rows > 10 {
|
||||||
|
return errors.New("rows cannot exceed 10"), "INVALID_ROWS"
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
17
migrations/001_create_vouchers_table.sql
Normal file
17
migrations/001_create_vouchers_table.sql
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
-- Create vouchers table
|
||||||
|
CREATE TABLE IF NOT EXISTS vouchers (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
email VARCHAR(255),
|
||||||
|
phone_number VARCHAR(20),
|
||||||
|
voucher_code VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
winner_number INTEGER NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create index on voucher_code for faster lookups
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vouchers_voucher_code ON vouchers(voucher_code);
|
||||||
|
|
||||||
|
-- Create index on winner_number for sorting
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vouchers_winner_number ON vouchers(winner_number);
|
||||||
222
test_voucher_api.md
Normal file
222
test_voucher_api.md
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
# Voucher API Test Guide
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### 1. Get Random Vouchers for Spin
|
||||||
|
**GET** `/api/v1/vouchers/spin`
|
||||||
|
|
||||||
|
Query Parameters:
|
||||||
|
- `limit` (optional): Number of vouchers to return (1-50, default: 10)
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Success",
|
||||||
|
"data": {
|
||||||
|
"vouchers": [
|
||||||
|
{
|
||||||
|
"voucher_code": "VOUCHER001",
|
||||||
|
"name": "John Doe",
|
||||||
|
"phone_number": "08**1234",
|
||||||
|
"is_winner": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"voucher_code": "VOUCHER002",
|
||||||
|
"name": "Jane Smith",
|
||||||
|
"phone_number": "09**5678",
|
||||||
|
"is_winner": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"count": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Get Random Vouchers by Rows (UPDATED!)
|
||||||
|
**GET** `/api/v1/vouchers/rows`
|
||||||
|
|
||||||
|
Query Parameters:
|
||||||
|
- `rows` (optional): Number of rows to return (1-10, default: 4)
|
||||||
|
- `winner_number` (optional): Winner number to filter by (if not provided, defaults to 0)
|
||||||
|
|
||||||
|
**Logic:**
|
||||||
|
- If `winner_number` is provided: Randomly select from vouchers where `winner_number = provided_value` AND `is_winner = false`
|
||||||
|
- If no vouchers found for the specified `winner_number`: Fallback to `winner_number = 0` AND `is_winner = false`
|
||||||
|
- If `winner_number` is not provided: Randomly select from vouchers where `winner_number = 0` AND `is_winner = false`
|
||||||
|
- Excludes vouchers already marked as winners
|
||||||
|
- Distributes vouchers evenly across the specified number of rows
|
||||||
|
- **Automatically selects one random winner from each row and sets their `is_winner` to `true`**
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Success",
|
||||||
|
"data": {
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row_number": 1,
|
||||||
|
"vouchers": [
|
||||||
|
{
|
||||||
|
"voucher_code": "VOUCHER001",
|
||||||
|
"name": "John Doe",
|
||||||
|
"phone_number": "08**1234",
|
||||||
|
"is_winner": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"voucher_code": "VOUCHER002",
|
||||||
|
"name": "Jane Smith",
|
||||||
|
"phone_number": "09**5678",
|
||||||
|
"is_winner": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"voucher_code": "VOUCHER003",
|
||||||
|
"name": "Bob Johnson",
|
||||||
|
"phone_number": "08**9012",
|
||||||
|
"is_winner": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_number": 2,
|
||||||
|
"vouchers": [
|
||||||
|
{
|
||||||
|
"voucher_code": "VOUCHER004",
|
||||||
|
"name": "Alice Brown",
|
||||||
|
"phone_number": "09**3456",
|
||||||
|
"is_winner": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"voucher_code": "VOUCHER005",
|
||||||
|
"name": "Charlie Wilson",
|
||||||
|
"phone_number": "08**7890",
|
||||||
|
"is_winner": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total_rows": 2,
|
||||||
|
"total_vouchers": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. List All Vouchers
|
||||||
|
**GET** `/api/v1/vouchers`
|
||||||
|
|
||||||
|
Query Parameters:
|
||||||
|
- `page` (optional): Page number (default: 1)
|
||||||
|
- `limit` (optional): Items per page (default: 10, max: 100)
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Success",
|
||||||
|
"data": {
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"phone_number": "08123456789",
|
||||||
|
"voucher_code": "VOUCHER001",
|
||||||
|
"winner_number": 1,
|
||||||
|
"created_at": "2024-01-01T00:00:00Z",
|
||||||
|
"updated_at": "2024-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total_count": 1,
|
||||||
|
"page": 1,
|
||||||
|
"limit": 10,
|
||||||
|
"total_pages": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Get Voucher by ID
|
||||||
|
**GET** `/api/v1/vouchers/{id}`
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Success",
|
||||||
|
"data": {
|
||||||
|
"id": "uuid",
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"phone_number": "08123456789",
|
||||||
|
"voucher_code": "VOUCHER001",
|
||||||
|
"winner_number": 1,
|
||||||
|
"created_at": "2024-01-01T00:00:00Z",
|
||||||
|
"updated_at": "2024-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Get Voucher by Code
|
||||||
|
**GET** `/api/v1/vouchers/code/{code}`
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Success",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"phone_number": "08123456789",
|
||||||
|
"voucher_code": "VOUCHER001",
|
||||||
|
"winner_number": 1,
|
||||||
|
"is_winner": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
1. **Random Selection**: The `/spin` endpoint returns random vouchers from the database
|
||||||
|
2. **Phone Number Masking**: Phone numbers are masked for privacy (shows first 2 and last 2 digits)
|
||||||
|
3. **Pagination**: List endpoint supports pagination
|
||||||
|
4. **Authentication**: All endpoints require admin or manager role
|
||||||
|
5. **Validation**: Input validation for limit parameters
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE vouchers (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
email VARCHAR(255) NOT NULL,
|
||||||
|
phone_number VARCHAR(20) NOT NULL,
|
||||||
|
voucher_code VARCHAR(50) NOT NULL,
|
||||||
|
winner_number INT NOT NULL DEFAULT 0,
|
||||||
|
is_winner BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get 5 random vouchers for spin
|
||||||
|
curl -X GET "http://localhost:8080/api/v1/vouchers/spin?limit=5" \
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||||
|
|
||||||
|
# Get 4 rows (default) - vouchers distributed evenly across rows
|
||||||
|
curl -X GET "http://localhost:8080/api/v1/vouchers/rows" \
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||||
|
|
||||||
|
# Get 3 rows - vouchers distributed evenly across 3 rows (from winner_number = 0)
|
||||||
|
curl -X GET "http://localhost:8080/api/v1/vouchers/rows?rows=3" \
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||||
|
|
||||||
|
# Get 3 rows - vouchers from winner_number = 1 (fallback to winner_number = 0 if none found)
|
||||||
|
curl -X GET "http://localhost:8080/api/v1/vouchers/rows?rows=3&winner_number=1" \
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||||
|
|
||||||
|
# Get 3 rows - vouchers from winner_number = 5 (will fallback to winner_number = 0 if none found)
|
||||||
|
curl -X GET "http://localhost:8080/api/v1/vouchers/rows?rows=3&winner_number=5" \
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||||
|
|
||||||
|
# List all vouchers with pagination
|
||||||
|
curl -X GET "http://localhost:8080/api/v1/vouchers?page=1&limit=20" \
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||||
|
```
|
||||||
Loading…
x
Reference in New Issue
Block a user