voucher
This commit is contained in:
parent
4f6208e479
commit
cfe690a40f
@ -94,6 +94,8 @@ func (a *App) Initialize(cfg *config.Config) error {
|
||||
validators.accountValidator,
|
||||
*services.orderIngredientTransactionService,
|
||||
validators.orderIngredientTransactionValidator,
|
||||
services.voucherService,
|
||||
validators.voucherValidator,
|
||||
)
|
||||
|
||||
return nil
|
||||
@ -167,6 +169,7 @@ type repositories struct {
|
||||
chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl
|
||||
accountRepo *repository.AccountRepositoryImpl
|
||||
orderIngredientTransactionRepo *repository.OrderIngredientTransactionRepositoryImpl
|
||||
voucherRepo *repository.VoucherRepository
|
||||
txManager *repository.TxManager
|
||||
}
|
||||
|
||||
@ -200,7 +203,8 @@ func (a *App) initRepositories() *repositories {
|
||||
chartOfAccountRepo: repository.NewChartOfAccountRepositoryImpl(a.db),
|
||||
accountRepo: repository.NewAccountRepositoryImpl(a.db),
|
||||
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
|
||||
accountProcessor *processor.AccountProcessorImpl
|
||||
orderIngredientTransactionProcessor *processor.OrderIngredientTransactionProcessorImpl
|
||||
voucherProcessor *processor.VoucherProcessor
|
||||
fileClient processor.FileClient
|
||||
inventoryMovementService service.InventoryMovementService
|
||||
}
|
||||
@ -262,6 +267,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
||||
chartOfAccountProcessor: processor.NewChartOfAccountProcessorImpl(repos.chartOfAccountRepo, repos.chartOfAccountTypeRepo),
|
||||
accountProcessor: processor.NewAccountProcessorImpl(repos.accountRepo, repos.chartOfAccountRepo),
|
||||
orderIngredientTransactionProcessor: processor.NewOrderIngredientTransactionProcessorImpl(repos.orderIngredientTransactionRepo, repos.productRecipeRepo, repos.ingredientRepo, repos.unitRepo).(*processor.OrderIngredientTransactionProcessorImpl),
|
||||
voucherProcessor: processor.NewVoucherProcessor(repos.voucherRepo),
|
||||
fileClient: fileClient,
|
||||
inventoryMovementService: inventoryMovementService,
|
||||
}
|
||||
@ -294,6 +300,7 @@ type services struct {
|
||||
chartOfAccountService service.ChartOfAccountService
|
||||
accountService service.AccountService
|
||||
orderIngredientTransactionService *service.OrderIngredientTransactionService
|
||||
voucherService service.VoucherService
|
||||
}
|
||||
|
||||
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)
|
||||
accountService := service.NewAccountService(processors.accountProcessor)
|
||||
orderIngredientTransactionService := service.NewOrderIngredientTransactionService(processors.orderIngredientTransactionProcessor, repos.txManager)
|
||||
voucherService := service.NewVoucherService(processors.voucherProcessor)
|
||||
|
||||
// Update order service with order ingredient transaction service
|
||||
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,
|
||||
accountService: accountService,
|
||||
orderIngredientTransactionService: orderIngredientTransactionService,
|
||||
voucherService: voucherService,
|
||||
}
|
||||
}
|
||||
|
||||
@ -388,6 +397,7 @@ type validators struct {
|
||||
chartOfAccountValidator *validator.ChartOfAccountValidatorImpl
|
||||
accountValidator *validator.AccountValidatorImpl
|
||||
orderIngredientTransactionValidator *validator.OrderIngredientTransactionValidatorImpl
|
||||
voucherValidator validator.VoucherValidator
|
||||
}
|
||||
|
||||
func (a *App) initValidators() *validators {
|
||||
@ -411,5 +421,6 @@ func (a *App) initValidators() *validators {
|
||||
chartOfAccountValidator: validator.NewChartOfAccountValidator().(*validator.ChartOfAccountValidatorImpl),
|
||||
accountValidator: validator.NewAccountValidator().(*validator.AccountValidatorImpl),
|
||||
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{},
|
||||
&PurchaseOrderAttachment{},
|
||||
&IngredientUnitConverter{},
|
||||
&Voucher{},
|
||||
// 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
|
||||
accountHandler *handler.AccountHandler
|
||||
orderIngredientTransactionHandler *handler.OrderIngredientTransactionHandler
|
||||
voucherHandler *handler.VoucherHandler
|
||||
authMiddleware *middleware.AuthMiddleware
|
||||
}
|
||||
|
||||
@ -90,7 +91,9 @@ func NewRouter(cfg *config.Config,
|
||||
accountService service.AccountService,
|
||||
accountValidator validator.AccountValidator,
|
||||
orderIngredientTransactionService service.OrderIngredientTransactionService,
|
||||
orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator) *Router {
|
||||
orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator,
|
||||
voucherService service.VoucherService,
|
||||
voucherValidator validator.VoucherValidator) *Router {
|
||||
|
||||
return &Router{
|
||||
config: cfg,
|
||||
@ -120,6 +123,7 @@ func NewRouter(cfg *config.Config,
|
||||
chartOfAccountHandler: handler.NewChartOfAccountHandler(chartOfAccountService, chartOfAccountValidator),
|
||||
accountHandler: handler.NewAccountHandler(accountService, accountValidator),
|
||||
orderIngredientTransactionHandler: handler.NewOrderIngredientTransactionHandler(&orderIngredientTransactionService, orderIngredientTransactionValidator),
|
||||
voucherHandler: handler.NewVoucherHandler(voucherService, voucherValidator),
|
||||
authMiddleware: authMiddleware,
|
||||
}
|
||||
}
|
||||
@ -426,6 +430,16 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
||||
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.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