diff --git a/internal/app/app.go b/internal/app/app.go index 3dbbcda..6b224ab 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -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(), } } diff --git a/internal/contract/voucher_contract.go b/internal/contract/voucher_contract.go deleted file mode 100644 index 6f797e8..0000000 --- a/internal/contract/voucher_contract.go +++ /dev/null @@ -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"` -} diff --git a/internal/entities/entities.go b/internal/entities/entities.go index b79c634..1805974 100644 --- a/internal/entities/entities.go +++ b/internal/entities/entities.go @@ -23,7 +23,6 @@ func GetAllEntities() []interface{} { &PurchaseOrderItem{}, &PurchaseOrderAttachment{}, &IngredientUnitConverter{}, - &Voucher{}, // Analytics entities are not database tables, they are query results } } diff --git a/internal/entities/voucher.go b/internal/entities/voucher.go deleted file mode 100644 index 0866f3a..0000000 --- a/internal/entities/voucher.go +++ /dev/null @@ -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" -} diff --git a/internal/handler/voucher_handler.go b/internal/handler/voucher_handler.go deleted file mode 100644 index 4c44e25..0000000 --- a/internal/handler/voucher_handler.go +++ /dev/null @@ -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") -} diff --git a/internal/mappers/voucher_mapper.go b/internal/mappers/voucher_mapper.go deleted file mode 100644 index fdf85fc..0000000 --- a/internal/mappers/voucher_mapper.go +++ /dev/null @@ -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 -} diff --git a/internal/models/voucher.go b/internal/models/voucher.go deleted file mode 100644 index 4b34f4e..0000000 --- a/internal/models/voucher.go +++ /dev/null @@ -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"` -} diff --git a/internal/processor/voucher_processor.go b/internal/processor/voucher_processor.go deleted file mode 100644 index e9651f3..0000000 --- a/internal/processor/voucher_processor.go +++ /dev/null @@ -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 -} diff --git a/internal/repository/voucher_repository.go b/internal/repository/voucher_repository.go deleted file mode 100644 index 58d336e..0000000 --- a/internal/repository/voucher_repository.go +++ /dev/null @@ -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 -} diff --git a/internal/router/router.go b/internal/router/router.go index 67d6b79..17a67c1 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -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()) { diff --git a/internal/service/voucher_service.go b/internal/service/voucher_service.go deleted file mode 100644 index a70643b..0000000 --- a/internal/service/voucher_service.go +++ /dev/null @@ -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 -} diff --git a/internal/validator/voucher_validator.go b/internal/validator/voucher_validator.go deleted file mode 100644 index be3509f..0000000 --- a/internal/validator/voucher_validator.go +++ /dev/null @@ -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, "" -} diff --git a/migrations/001_create_vouchers_table.sql b/migrations/001_create_vouchers_table.sql deleted file mode 100644 index 59961b5..0000000 --- a/migrations/001_create_vouchers_table.sql +++ /dev/null @@ -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); diff --git a/server b/server index 0553262..3ed1e65 100755 Binary files a/server and b/server differ diff --git a/test_voucher_api.md b/test_voucher_api.md deleted file mode 100644 index 8980a1b..0000000 --- a/test_voucher_api.md +++ /dev/null @@ -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" -```