Revert "voucher"

This reverts commit cfe690a40f569e492e16605cbd3321f9ad142247.
This commit is contained in:
Aditya Siregar 2025-09-16 19:31:39 +07:00
parent 36c2352cb2
commit 12ee54390f
15 changed files with 2 additions and 1199 deletions

View File

@ -94,8 +94,6 @@ func (a *App) Initialize(cfg *config.Config) error {
validators.accountValidator,
*services.orderIngredientTransactionService,
validators.orderIngredientTransactionValidator,
services.voucherService,
validators.voucherValidator,
)
return nil
@ -169,7 +167,6 @@ type repositories struct {
chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl
accountRepo *repository.AccountRepositoryImpl
orderIngredientTransactionRepo *repository.OrderIngredientTransactionRepositoryImpl
voucherRepo *repository.VoucherRepository
txManager *repository.TxManager
}
@ -203,8 +200,7 @@ func (a *App) initRepositories() *repositories {
chartOfAccountRepo: repository.NewChartOfAccountRepositoryImpl(a.db),
accountRepo: repository.NewAccountRepositoryImpl(a.db),
orderIngredientTransactionRepo: repository.NewOrderIngredientTransactionRepositoryImpl(a.db).(*repository.OrderIngredientTransactionRepositoryImpl),
voucherRepo: repository.NewVoucherRepository(a.db),
txManager: repository.NewTxManager(a.db),
txManager: repository.NewTxManager(a.db),
}
}
@ -233,7 +229,6 @@ type processors struct {
chartOfAccountProcessor *processor.ChartOfAccountProcessorImpl
accountProcessor *processor.AccountProcessorImpl
orderIngredientTransactionProcessor *processor.OrderIngredientTransactionProcessorImpl
voucherProcessor *processor.VoucherProcessor
fileClient processor.FileClient
inventoryMovementService service.InventoryMovementService
}
@ -267,7 +262,6 @@ 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,
}
@ -300,7 +294,6 @@ 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 {
@ -331,7 +324,6 @@ 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)
@ -363,7 +355,6 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
chartOfAccountService: chartOfAccountService,
accountService: accountService,
orderIngredientTransactionService: orderIngredientTransactionService,
voucherService: voucherService,
}
}
@ -397,7 +388,6 @@ type validators struct {
chartOfAccountValidator *validator.ChartOfAccountValidatorImpl
accountValidator *validator.AccountValidatorImpl
orderIngredientTransactionValidator *validator.OrderIngredientTransactionValidatorImpl
voucherValidator validator.VoucherValidator
}
func (a *App) initValidators() *validators {
@ -421,6 +411,5 @@ func (a *App) initValidators() *validators {
chartOfAccountValidator: validator.NewChartOfAccountValidator().(*validator.ChartOfAccountValidatorImpl),
accountValidator: validator.NewAccountValidator().(*validator.AccountValidatorImpl),
orderIngredientTransactionValidator: validator.NewOrderIngredientTransactionValidator().(*validator.OrderIngredientTransactionValidatorImpl),
voucherValidator: validator.NewVoucherValidator(),
}
}

View File

@ -1,51 +0,0 @@
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"`
}

View File

@ -23,7 +23,6 @@ func GetAllEntities() []interface{} {
&PurchaseOrderItem{},
&PurchaseOrderAttachment{},
&IngredientUnitConverter{},
&Voucher{},
// Analytics entities are not database tables, they are query results
}
}

View File

@ -1,15 +0,0 @@
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"
}

View File

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

View File

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

View File

@ -1,51 +0,0 @@
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"`
}

View File

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

View File

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

View File

@ -40,7 +40,6 @@ type Router struct {
chartOfAccountHandler *handler.ChartOfAccountHandler
accountHandler *handler.AccountHandler
orderIngredientTransactionHandler *handler.OrderIngredientTransactionHandler
voucherHandler *handler.VoucherHandler
authMiddleware *middleware.AuthMiddleware
}
@ -91,9 +90,7 @@ func NewRouter(cfg *config.Config,
accountService service.AccountService,
accountValidator validator.AccountValidator,
orderIngredientTransactionService service.OrderIngredientTransactionService,
orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator,
voucherService service.VoucherService,
voucherValidator validator.VoucherValidator) *Router {
orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator) *Router {
return &Router{
config: cfg,
@ -123,7 +120,6 @@ 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,
}
}
@ -430,16 +426,6 @@ 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())
{

View File

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

View File

@ -1,41 +0,0 @@
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, ""
}

View File

@ -1,17 +0,0 @@
-- 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);

BIN
server

Binary file not shown.

View File

@ -1,222 +0,0 @@
# 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"
```