test wheels
This commit is contained in:
parent
f64fec1fe2
commit
be92ec8b23
@ -177,7 +177,6 @@ type repositories struct {
|
||||
chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl
|
||||
accountRepo *repository.AccountRepositoryImpl
|
||||
orderIngredientTransactionRepo *repository.OrderIngredientTransactionRepositoryImpl
|
||||
customerPointsRepo *repository.CustomerPointsRepository
|
||||
customerTokensRepo *repository.CustomerTokensRepository
|
||||
tierRepo *repository.TierRepository
|
||||
gameRepo *repository.GameRepository
|
||||
@ -222,7 +221,6 @@ func (a *App) initRepositories() *repositories {
|
||||
chartOfAccountRepo: repository.NewChartOfAccountRepositoryImpl(a.db),
|
||||
accountRepo: repository.NewAccountRepositoryImpl(a.db),
|
||||
orderIngredientTransactionRepo: repository.NewOrderIngredientTransactionRepositoryImpl(a.db).(*repository.OrderIngredientTransactionRepositoryImpl),
|
||||
customerPointsRepo: repository.NewCustomerPointsRepository(a.db),
|
||||
customerTokensRepo: repository.NewCustomerTokensRepository(a.db),
|
||||
tierRepo: repository.NewTierRepository(a.db),
|
||||
gameRepo: repository.NewGameRepository(a.db),
|
||||
@ -263,7 +261,6 @@ type processors struct {
|
||||
chartOfAccountProcessor *processor.ChartOfAccountProcessorImpl
|
||||
accountProcessor *processor.AccountProcessorImpl
|
||||
orderIngredientTransactionProcessor *processor.OrderIngredientTransactionProcessorImpl
|
||||
customerPointsProcessor *processor.CustomerPointsProcessor
|
||||
customerTokensProcessor *processor.CustomerTokensProcessor
|
||||
tierProcessor *processor.TierProcessor
|
||||
gameProcessor *processor.GameProcessor
|
||||
@ -310,7 +307,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),
|
||||
customerPointsProcessor: processor.NewCustomerPointsProcessor(repos.customerPointsRepo),
|
||||
customerTokensProcessor: processor.NewCustomerTokensProcessor(repos.customerTokensRepo),
|
||||
tierProcessor: processor.NewTierProcessor(repos.tierRepo),
|
||||
gameProcessor: processor.NewGameProcessor(repos.gameRepo),
|
||||
@ -320,7 +316,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
||||
rewardProcessor: processor.NewRewardProcessor(repos.rewardRepo),
|
||||
campaignProcessor: processor.NewCampaignProcessor(repos.campaignRepo),
|
||||
customerAuthProcessor: processor.NewCustomerAuthProcessor(repos.customerAuthRepo, otpProcessor, repos.otpRepo, cfg.GetCustomerJWTSecret(), cfg.GetCustomerJWTExpiresTTL()),
|
||||
customerPointsProcessor: processor.NewCustomerPointsProcessor(repos.customerPointsRepo),
|
||||
customerPointsProcessor: processor.NewCustomerPointsProcessor(repos.customerPointsRepo, repos.gameRepo),
|
||||
otpProcessor: otpProcessor,
|
||||
fileClient: fileClient,
|
||||
inventoryMovementService: inventoryMovementService,
|
||||
|
||||
60
internal/contract/customer_game_contract.go
Normal file
60
internal/contract/customer_game_contract.go
Normal file
@ -0,0 +1,60 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Request Contracts
|
||||
type GetCustomerGamesRequest struct {
|
||||
// No additional fields needed - customer ID comes from JWT token
|
||||
}
|
||||
|
||||
// Response Contracts
|
||||
type GetCustomerGamesResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Data *GetCustomerGamesResponseData `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type GetCustomerGamesResponseData struct {
|
||||
Games []CustomerGameResponse `json:"games"`
|
||||
}
|
||||
|
||||
type CustomerGameResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
Prizes []CustomerGamePrizeResponse `json:"prizes,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CustomerGamePrizeResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
GameID uuid.UUID `json:"game_id"`
|
||||
Name string `json:"name"`
|
||||
Image *string `json:"image,omitempty"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Ferris Wheel Game Contracts
|
||||
type GetFerrisWheelGameRequest struct {
|
||||
// No additional fields needed - customer ID comes from JWT token
|
||||
}
|
||||
|
||||
type GetFerrisWheelGameResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Data *GetFerrisWheelGameResponseData `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type GetFerrisWheelGameResponseData struct {
|
||||
Game CustomerGameResponse `json:"game"`
|
||||
Prizes []CustomerGamePrizeResponse `json:"prizes"`
|
||||
}
|
||||
@ -14,6 +14,7 @@ type CreateGamePrizeRequest struct {
|
||||
MaxStock *int `json:"max_stock,omitempty"`
|
||||
Threshold *int64 `json:"threshold,omitempty"`
|
||||
FallbackPrizeID *uuid.UUID `json:"fallback_prize_id,omitempty"`
|
||||
Image *string `json:"image,omitempty" validate:"omitempty,max=500"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
@ -24,6 +25,7 @@ type UpdateGamePrizeRequest struct {
|
||||
MaxStock *int `json:"max_stock,omitempty"`
|
||||
Threshold *int64 `json:"threshold,omitempty"`
|
||||
FallbackPrizeID *uuid.UUID `json:"fallback_prize_id,omitempty"`
|
||||
Image *string `json:"image,omitempty" validate:"omitempty,max=500"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
@ -36,6 +38,7 @@ type GamePrizeResponse struct {
|
||||
MaxStock *int `json:"max_stock,omitempty"`
|
||||
Threshold *int64 `json:"threshold,omitempty"`
|
||||
FallbackPrizeID *uuid.UUID `json:"fallback_prize_id,omitempty"`
|
||||
Image *string `json:"image,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
Game *GameResponse `json:"game,omitempty"`
|
||||
FallbackPrize *GamePrizeResponse `json:"fallback_prize,omitempty"`
|
||||
|
||||
@ -16,6 +16,7 @@ type GamePrize struct {
|
||||
MaxStock *int `gorm:"" json:"max_stock,omitempty"`
|
||||
Threshold *int64 `gorm:"" json:"threshold,omitempty"`
|
||||
FallbackPrizeID *uuid.UUID `gorm:"type:uuid" json:"fallback_prize_id,omitempty"`
|
||||
Image *string `gorm:"type:varchar(500)" json:"image,omitempty"`
|
||||
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
@ -17,6 +17,7 @@ type OtpSession struct {
|
||||
IsUsed bool `gorm:"default:false;index" json:"is_used"`
|
||||
AttemptsCount int `gorm:"default:0" json:"attempts_count"`
|
||||
MaxAttempts int `gorm:"default:3" json:"max_attempts"`
|
||||
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
}
|
||||
|
||||
@ -121,3 +121,33 @@ func (h *CustomerPointsHandler) GetCustomerWallet(c *gin.Context) {
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CustomerPointsHandler::GetCustomerWallet")
|
||||
}
|
||||
|
||||
func (h *CustomerPointsHandler) GetCustomerGames(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
response, err := h.customerPointsService.GetCustomerGames(ctx)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("CustomerPointsHandler::GetCustomerGames -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()),
|
||||
}), "CustomerPointsHandler::GetCustomerGames")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CustomerPointsHandler::GetCustomerGames")
|
||||
}
|
||||
|
||||
func (h *CustomerPointsHandler) GetFerrisWheelGame(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
response, err := h.customerPointsService.GetFerrisWheelGame(ctx)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("CustomerPointsHandler::GetFerrisWheelGame -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()),
|
||||
}), "CustomerPointsHandler::GetFerrisWheelGame")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CustomerPointsHandler::GetFerrisWheelGame")
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ func ToGamePrizeResponse(gamePrize *entities.GamePrize) *models.GamePrizeRespons
|
||||
MaxStock: gamePrize.MaxStock,
|
||||
Threshold: gamePrize.Threshold,
|
||||
FallbackPrizeID: gamePrize.FallbackPrizeID,
|
||||
Image: gamePrize.Image,
|
||||
Metadata: gamePrize.Metadata,
|
||||
Game: ToGameResponse(&gamePrize.Game),
|
||||
FallbackPrize: ToGamePrizeResponse(gamePrize.FallbackPrize),
|
||||
@ -47,6 +48,7 @@ func ToGamePrizeEntity(req *models.CreateGamePrizeRequest) *entities.GamePrize {
|
||||
MaxStock: req.MaxStock,
|
||||
Threshold: req.Threshold,
|
||||
FallbackPrizeID: req.FallbackPrizeID,
|
||||
Image: req.Image,
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
}
|
||||
@ -71,6 +73,9 @@ func UpdateGamePrizeEntity(gamePrize *entities.GamePrize, req *models.UpdateGame
|
||||
if req.FallbackPrizeID != nil {
|
||||
gamePrize.FallbackPrizeID = req.FallbackPrizeID
|
||||
}
|
||||
if req.Image != nil {
|
||||
gamePrize.Image = req.Image
|
||||
}
|
||||
if req.Metadata != nil {
|
||||
gamePrize.Metadata = req.Metadata
|
||||
}
|
||||
|
||||
60
internal/models/customer_game.go
Normal file
60
internal/models/customer_game.go
Normal file
@ -0,0 +1,60 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Request Models
|
||||
type GetCustomerGamesRequest struct {
|
||||
// No additional fields needed - customer ID comes from JWT token
|
||||
}
|
||||
|
||||
// Response Models
|
||||
type GetCustomerGamesResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Data *GetCustomerGamesResponseData `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type GetCustomerGamesResponseData struct {
|
||||
Games []CustomerGameResponse `json:"games"`
|
||||
}
|
||||
|
||||
type CustomerGameResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
Prizes []CustomerGamePrizeResponse `json:"prizes,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CustomerGamePrizeResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
GameID uuid.UUID `json:"game_id"`
|
||||
Name string `json:"name"`
|
||||
Image *string `json:"image,omitempty"`
|
||||
Metadata *map[string]interface{} `json:"metadata,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Ferris Wheel Game Models
|
||||
type GetFerrisWheelGameRequest struct {
|
||||
// No additional fields needed - customer ID comes from JWT token
|
||||
}
|
||||
|
||||
type GetFerrisWheelGameResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Data *GetFerrisWheelGameResponseData `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type GetFerrisWheelGameResponseData struct {
|
||||
Game CustomerGameResponse `json:"game"`
|
||||
Prizes []CustomerGamePrizeResponse `json:"prizes"`
|
||||
}
|
||||
@ -14,6 +14,7 @@ type CreateGamePrizeRequest struct {
|
||||
MaxStock *int `json:"max_stock,omitempty"`
|
||||
Threshold *int64 `json:"threshold,omitempty"`
|
||||
FallbackPrizeID *uuid.UUID `json:"fallback_prize_id,omitempty"`
|
||||
Image *string `json:"image,omitempty" validate:"omitempty,max=500"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
@ -24,6 +25,7 @@ type UpdateGamePrizeRequest struct {
|
||||
MaxStock *int `json:"max_stock,omitempty"`
|
||||
Threshold *int64 `json:"threshold,omitempty"`
|
||||
FallbackPrizeID *uuid.UUID `json:"fallback_prize_id,omitempty"`
|
||||
Image *string `json:"image,omitempty" validate:"omitempty,max=500"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
@ -36,6 +38,7 @@ type GamePrizeResponse struct {
|
||||
MaxStock *int `json:"max_stock,omitempty"`
|
||||
Threshold *int64 `json:"threshold,omitempty"`
|
||||
FallbackPrizeID *uuid.UUID `json:"fallback_prize_id,omitempty"`
|
||||
Image *string `json:"image,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
Game *GameResponse `json:"game,omitempty"`
|
||||
FallbackPrize *GamePrizeResponse `json:"fallback_prize,omitempty"`
|
||||
|
||||
@ -25,24 +25,20 @@ type CustomerAuthProcessor interface {
|
||||
}
|
||||
|
||||
type customerAuthProcessor struct {
|
||||
customerAuthRepo repository.CustomerAuthRepository
|
||||
otpProcessor OtpProcessor
|
||||
otpRepo repository.OtpRepository
|
||||
jwtSecret string
|
||||
tokenTTLMinutes int
|
||||
otpStorage map[string]*models.OtpSession // In-memory storage for OTP sessions
|
||||
registrationStorage map[string]*models.RegistrationSession // In-memory storage for registration sessions
|
||||
customerAuthRepo repository.CustomerAuthRepository
|
||||
otpProcessor OtpProcessor
|
||||
otpRepo repository.OtpRepository
|
||||
jwtSecret string
|
||||
tokenTTLMinutes int
|
||||
}
|
||||
|
||||
func NewCustomerAuthProcessor(customerAuthRepo repository.CustomerAuthRepository, otpProcessor OtpProcessor, otpRepo repository.OtpRepository, jwtSecret string, tokenTTLMinutes int) CustomerAuthProcessor {
|
||||
return &customerAuthProcessor{
|
||||
customerAuthRepo: customerAuthRepo,
|
||||
otpProcessor: otpProcessor,
|
||||
otpRepo: otpRepo,
|
||||
jwtSecret: jwtSecret,
|
||||
tokenTTLMinutes: tokenTTLMinutes,
|
||||
otpStorage: make(map[string]*models.OtpSession),
|
||||
registrationStorage: make(map[string]*models.RegistrationSession),
|
||||
customerAuthRepo: customerAuthRepo,
|
||||
otpProcessor: otpProcessor,
|
||||
otpRepo: otpRepo,
|
||||
jwtSecret: jwtSecret,
|
||||
tokenTTLMinutes: tokenTTLMinutes,
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,17 +151,19 @@ func (p *customerAuthProcessor) StartRegistration(ctx context.Context, req *cont
|
||||
return nil, fmt.Errorf("failed to create OTP session: %w", err)
|
||||
}
|
||||
|
||||
// Store registration session
|
||||
registrationSession := &models.RegistrationSession{
|
||||
Token: registrationToken,
|
||||
PhoneNumber: req.PhoneNumber,
|
||||
Name: req.Name,
|
||||
BirthDate: req.BirthDate,
|
||||
ExpiresAt: time.Now().Add(10 * time.Minute),
|
||||
Step: "otp_sent",
|
||||
// Store registration data in OTP session metadata
|
||||
registrationData := map[string]interface{}{
|
||||
"registration_token": registrationToken,
|
||||
"name": req.Name,
|
||||
"birth_date": req.BirthDate,
|
||||
"step": "otp_sent",
|
||||
}
|
||||
|
||||
p.registrationStorage[registrationToken] = registrationSession
|
||||
// Update OTP session with registration metadata
|
||||
otpSession.Metadata = registrationData
|
||||
if err := p.otpRepo.UpdateOtpSession(ctx, otpSession); err != nil {
|
||||
return nil, fmt.Errorf("failed to update OTP session with registration data: %w", err)
|
||||
}
|
||||
|
||||
// Send OTP via WhatsApp
|
||||
if err := p.otpProcessor.SendOtpViaWhatsApp(req.PhoneNumber, otpSession.Code, "registration"); err != nil {
|
||||
@ -184,18 +182,19 @@ func (p *customerAuthProcessor) StartRegistration(ctx context.Context, req *cont
|
||||
}
|
||||
|
||||
func (p *customerAuthProcessor) VerifyOtp(ctx context.Context, req *contract.RegisterVerifyOtpRequest) (*models.RegisterVerifyOtpResponse, error) {
|
||||
// Get registration session
|
||||
registrationSession, exists := p.registrationStorage[req.RegistrationToken]
|
||||
if !exists {
|
||||
otpSession, err := p.otpRepo.GetOtpSessionByRegistrationToken(ctx, req.RegistrationToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get OTP session: %w", err)
|
||||
}
|
||||
|
||||
if otpSession == nil {
|
||||
return nil, fmt.Errorf("invalid or expired registration token")
|
||||
}
|
||||
|
||||
if time.Now().After(registrationSession.ExpiresAt) {
|
||||
delete(p.registrationStorage, req.RegistrationToken)
|
||||
if otpSession.IsExpired() {
|
||||
return nil, fmt.Errorf("registration token expired")
|
||||
}
|
||||
|
||||
// Validate OTP format
|
||||
if !p.otpProcessor.ValidateOtpCode(req.OtpCode) {
|
||||
return &models.RegisterVerifyOtpResponse{
|
||||
Status: "FAILED",
|
||||
@ -203,34 +202,33 @@ func (p *customerAuthProcessor) VerifyOtp(ctx context.Context, req *contract.Reg
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get the OTP session for this phone number and purpose
|
||||
otpSession, err := p.otpRepo.GetOtpSessionByPhoneAndPurpose(ctx, registrationSession.PhoneNumber, "registration")
|
||||
if err != nil {
|
||||
return &models.RegisterVerifyOtpResponse{
|
||||
Status: "FAILED",
|
||||
Message: "Failed to validate OTP session.",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if otpSession == nil {
|
||||
return &models.RegisterVerifyOtpResponse{
|
||||
Status: "FAILED",
|
||||
Message: "No active OTP session found.",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Verify OTP code and mark as used
|
||||
if otpSession.Code != req.OtpCode {
|
||||
otpSession.IncrementAttempts()
|
||||
p.otpRepo.UpdateOtpSession(ctx, otpSession)
|
||||
if err := p.otpRepo.UpdateOtpSession(ctx, otpSession); err != nil {
|
||||
fmt.Printf("Warning: failed to update OTP session attempts: %v\n", err)
|
||||
}
|
||||
return &models.RegisterVerifyOtpResponse{
|
||||
Status: "FAILED",
|
||||
Message: "Invalid OTP code.",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if otpSession.IsUsed || otpSession.IsMaxAttemptsReached() {
|
||||
return &models.RegisterVerifyOtpResponse{
|
||||
Status: "FAILED",
|
||||
Message: "OTP code already used or max attempts reached.",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Mark OTP as used
|
||||
otpSession.MarkAsUsed()
|
||||
|
||||
// Update registration step in metadata
|
||||
if otpSession.Metadata == nil {
|
||||
otpSession.Metadata = make(map[string]interface{})
|
||||
}
|
||||
otpSession.Metadata["step"] = "otp_verified"
|
||||
|
||||
if err := p.otpRepo.UpdateOtpSession(ctx, otpSession); err != nil {
|
||||
return &models.RegisterVerifyOtpResponse{
|
||||
Status: "FAILED",
|
||||
@ -238,10 +236,6 @@ func (p *customerAuthProcessor) VerifyOtp(ctx context.Context, req *contract.Reg
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Update registration session
|
||||
registrationSession.Step = "otp_verified"
|
||||
p.registrationStorage[req.RegistrationToken] = registrationSession
|
||||
|
||||
return &models.RegisterVerifyOtpResponse{
|
||||
Status: "OTP_VERIFIED",
|
||||
Message: "OTP verified, continue to set password.",
|
||||
@ -252,23 +246,26 @@ func (p *customerAuthProcessor) VerifyOtp(ctx context.Context, req *contract.Reg
|
||||
}
|
||||
|
||||
func (p *customerAuthProcessor) SetPassword(ctx context.Context, req *contract.RegisterSetPasswordRequest) (*models.RegisterSetPasswordResponse, error) {
|
||||
// Validate passwords match
|
||||
if req.Password != req.ConfirmPassword {
|
||||
return nil, fmt.Errorf("passwords do not match")
|
||||
}
|
||||
|
||||
// Get registration session
|
||||
registrationSession, exists := p.registrationStorage[req.RegistrationToken]
|
||||
if !exists {
|
||||
// Get OTP session by registration token from metadata
|
||||
otpSession, err := p.otpRepo.GetOtpSessionByRegistrationToken(ctx, req.RegistrationToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get OTP session: %w", err)
|
||||
}
|
||||
|
||||
if otpSession == nil {
|
||||
return nil, fmt.Errorf("invalid or expired registration token")
|
||||
}
|
||||
|
||||
if time.Now().After(registrationSession.ExpiresAt) {
|
||||
delete(p.registrationStorage, req.RegistrationToken)
|
||||
if otpSession.IsExpired() {
|
||||
return nil, fmt.Errorf("registration token expired")
|
||||
}
|
||||
|
||||
if registrationSession.Step != "otp_verified" {
|
||||
step, ok := otpSession.Metadata["step"].(string)
|
||||
if !ok || step != "otp_verified" {
|
||||
return nil, fmt.Errorf("OTP verification required before setting password")
|
||||
}
|
||||
|
||||
@ -280,31 +277,38 @@ func (p *customerAuthProcessor) SetPassword(ctx context.Context, req *contract.R
|
||||
|
||||
passwordHashStr := string(passwordHash)
|
||||
|
||||
// Extract registration data from OTP session metadata
|
||||
name, ok := otpSession.Metadata["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid registration data: name not found")
|
||||
}
|
||||
|
||||
birthDateStr, ok := otpSession.Metadata["birth_date"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid registration data: birth_date not found")
|
||||
}
|
||||
|
||||
// Parse birth date
|
||||
birthDate, err := time.Parse("2006-01-02", registrationSession.BirthDate)
|
||||
birthDate, err := time.Parse("2006-01-02", birthDateStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid birth date format: %w", err)
|
||||
}
|
||||
|
||||
// Create customer
|
||||
defaultOrgID := uuid.MustParse("87bec7c1-e274-4f66-bac5-84e632208470") // This should be configurable
|
||||
|
||||
customer := &entities.Customer{
|
||||
Name: registrationSession.Name,
|
||||
PhoneNumber: ®istrationSession.PhoneNumber,
|
||||
BirthDate: &birthDate,
|
||||
PasswordHash: &passwordHashStr,
|
||||
IsActive: true,
|
||||
// Note: OrganizationID should be set based on your business logic
|
||||
// For now, we'll use a default organization or require it in the request
|
||||
OrganizationID: defaultOrgID,
|
||||
Name: name,
|
||||
PhoneNumber: &otpSession.PhoneNumber,
|
||||
BirthDate: &birthDate,
|
||||
PasswordHash: &passwordHashStr,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := p.customerAuthRepo.CreateCustomer(ctx, customer); err != nil {
|
||||
return nil, fmt.Errorf("failed to create customer: %w", err)
|
||||
}
|
||||
|
||||
// Clean up registration session
|
||||
delete(p.registrationStorage, req.RegistrationToken)
|
||||
|
||||
// Generate JWT tokens using customer JWT util
|
||||
accessToken, refreshToken, _, err := util.GenerateCustomerTokens(customer, p.jwtSecret, p.tokenTTLMinutes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate tokens: %w", err)
|
||||
@ -320,7 +324,7 @@ func (p *customerAuthProcessor) SetPassword(ctx context.Context, req *contract.R
|
||||
ID: customer.ID,
|
||||
Name: customer.Name,
|
||||
PhoneNumber: *customer.PhoneNumber,
|
||||
BirthDate: registrationSession.BirthDate,
|
||||
BirthDate: birthDate.Format("2006-01-02"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
|
||||
@ -13,11 +13,13 @@ import (
|
||||
|
||||
type CustomerPointsProcessor struct {
|
||||
customerPointsRepo repository.CustomerPointsRepository
|
||||
gameRepo *repository.GameRepository
|
||||
}
|
||||
|
||||
func NewCustomerPointsProcessor(customerPointsRepo repository.CustomerPointsRepository) *CustomerPointsProcessor {
|
||||
func NewCustomerPointsProcessor(customerPointsRepo repository.CustomerPointsRepository, gameRepo *repository.GameRepository) *CustomerPointsProcessor {
|
||||
return &CustomerPointsProcessor{
|
||||
customerPointsRepo: customerPointsRepo,
|
||||
gameRepo: gameRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,3 +222,92 @@ func (p *CustomerPointsProcessor) GetCustomerWalletAPI(ctx context.Context, cust
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetCustomerGamesAPI gets active SPIN games for customers
|
||||
func (p *CustomerPointsProcessor) GetCustomerGamesAPI(ctx context.Context) (*models.GetCustomerGamesResponse, error) {
|
||||
// Get active SPIN games
|
||||
games, err := p.gameRepo.GetActiveSpinGames(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get active SPIN games: %w", err)
|
||||
}
|
||||
|
||||
// Convert to response format
|
||||
var gameResponses []models.CustomerGameResponse
|
||||
for _, game := range games {
|
||||
var prizeResponses []models.CustomerGamePrizeResponse
|
||||
for _, prize := range game.Prizes {
|
||||
prizeResponses = append(prizeResponses, models.CustomerGamePrizeResponse{
|
||||
ID: prize.ID,
|
||||
GameID: prize.GameID,
|
||||
Name: prize.Name,
|
||||
Image: prize.Image,
|
||||
Metadata: (*map[string]interface{})(&prize.Metadata),
|
||||
CreatedAt: prize.CreatedAt,
|
||||
UpdatedAt: prize.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
gameResponses = append(gameResponses, models.CustomerGameResponse{
|
||||
ID: game.ID,
|
||||
Name: game.Name,
|
||||
Type: string(game.Type),
|
||||
IsActive: game.IsActive,
|
||||
Metadata: (*map[string]interface{})(&game.Metadata),
|
||||
Prizes: prizeResponses,
|
||||
CreatedAt: game.CreatedAt,
|
||||
UpdatedAt: game.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return &models.GetCustomerGamesResponse{
|
||||
Status: "SUCCESS",
|
||||
Message: "Customer games retrieved successfully.",
|
||||
Data: &models.GetCustomerGamesResponseData{
|
||||
Games: gameResponses,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetFerrisWheelGameAPI gets the Ferris Wheel game for customers
|
||||
func (p *CustomerPointsProcessor) GetFerrisWheelGameAPI(ctx context.Context) (*models.GetFerrisWheelGameResponse, error) {
|
||||
// Get Ferris Wheel game
|
||||
game, err := p.gameRepo.GetFerrisWheelGame(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get Ferris Wheel game: %w", err)
|
||||
}
|
||||
|
||||
// Convert prizes to response format
|
||||
var prizeResponses []models.CustomerGamePrizeResponse
|
||||
for _, prize := range game.Prizes {
|
||||
prizeResponses = append(prizeResponses, models.CustomerGamePrizeResponse{
|
||||
ID: prize.ID,
|
||||
GameID: prize.GameID,
|
||||
Name: prize.Name,
|
||||
Image: prize.Image,
|
||||
Metadata: (*map[string]interface{})(&prize.Metadata),
|
||||
CreatedAt: prize.CreatedAt,
|
||||
UpdatedAt: prize.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// Convert game to response format
|
||||
gameResponse := models.CustomerGameResponse{
|
||||
ID: game.ID,
|
||||
Name: game.Name,
|
||||
Type: string(game.Type),
|
||||
IsActive: game.IsActive,
|
||||
Metadata: (*map[string]interface{})(&game.Metadata),
|
||||
Prizes: prizeResponses,
|
||||
CreatedAt: game.CreatedAt,
|
||||
UpdatedAt: game.UpdatedAt,
|
||||
}
|
||||
|
||||
return &models.GetFerrisWheelGameResponse{
|
||||
Status: "SUCCESS",
|
||||
Message: "Ferris Wheel game retrieved successfully.",
|
||||
Data: &models.GetFerrisWheelGameResponseData{
|
||||
Game: gameResponse,
|
||||
Prizes: prizeResponses,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ type GamePlayProcessor struct {
|
||||
gameRepo *repository.GameRepository
|
||||
gamePrizeRepo *repository.GamePrizeRepository
|
||||
customerTokensRepo *repository.CustomerTokensRepository
|
||||
customerPointsRepo *repository.CustomerPointsRepository
|
||||
customerPointsRepo repository.CustomerPointsRepository
|
||||
}
|
||||
|
||||
func NewGamePlayProcessor(
|
||||
@ -27,7 +27,7 @@ func NewGamePlayProcessor(
|
||||
gameRepo *repository.GameRepository,
|
||||
gamePrizeRepo *repository.GamePrizeRepository,
|
||||
customerTokensRepo *repository.CustomerTokensRepository,
|
||||
customerPointsRepo *repository.CustomerPointsRepository,
|
||||
customerPointsRepo repository.CustomerPointsRepository,
|
||||
) *GamePlayProcessor {
|
||||
return &GamePlayProcessor{
|
||||
gamePlayRepo: gamePlayRepo,
|
||||
|
||||
@ -86,3 +86,27 @@ func (r *GameRepository) GetActiveGames(ctx context.Context) ([]entities.Game, e
|
||||
}
|
||||
return games, nil
|
||||
}
|
||||
|
||||
func (r *GameRepository) GetActiveSpinGames(ctx context.Context) ([]entities.Game, error) {
|
||||
var games []entities.Game
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("Prizes").
|
||||
Where("is_active = ? AND type = ?", true, entities.GameTypeSpin).
|
||||
Find(&games).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return games, nil
|
||||
}
|
||||
|
||||
func (r *GameRepository) GetFerrisWheelGame(ctx context.Context) (*entities.Game, error) {
|
||||
var game entities.Game
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("Prizes").
|
||||
Where("is_active = ? AND LOWER(name) LIKE ?", true, "%ferris wheel%").
|
||||
First(&game).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &game, nil
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ type OtpRepository interface {
|
||||
GetOtpSessionByToken(ctx context.Context, token string) (*entities.OtpSession, error)
|
||||
GetOtpSessionByPhoneAndPurpose(ctx context.Context, phoneNumber string, purpose string) (*entities.OtpSession, error)
|
||||
GetLastOtpSessionByPhoneAndPurpose(ctx context.Context, phoneNumber string, purpose string) (*entities.OtpSession, error)
|
||||
GetOtpSessionByRegistrationToken(ctx context.Context, registrationToken string) (*entities.OtpSession, error)
|
||||
UpdateOtpSession(ctx context.Context, otpSession *entities.OtpSession) error
|
||||
DeleteOtpSession(ctx context.Context, token string) error
|
||||
DeleteExpiredOtpSessions(ctx context.Context) error
|
||||
@ -73,6 +74,18 @@ func (r *otpRepository) GetLastOtpSessionByPhoneAndPurpose(ctx context.Context,
|
||||
return &otpSession, nil
|
||||
}
|
||||
|
||||
func (r *otpRepository) GetOtpSessionByRegistrationToken(ctx context.Context, registrationToken string) (*entities.OtpSession, error) {
|
||||
var otpSession entities.OtpSession
|
||||
if err := r.db.WithContext(ctx).Where("metadata->>'registration_token' = ? AND purpose = ?",
|
||||
registrationToken, "registration").First(&otpSession).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil // OTP session not found
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get OTP session by registration token: %w", err)
|
||||
}
|
||||
return &otpSession, nil
|
||||
}
|
||||
|
||||
func (r *otpRepository) UpdateOtpSession(ctx context.Context, otpSession *entities.OtpSession) error {
|
||||
if err := r.db.WithContext(ctx).Save(otpSession).Error; err != nil {
|
||||
return fmt.Errorf("failed to update OTP session: %w", err)
|
||||
|
||||
@ -194,6 +194,8 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
||||
customer.GET("/points", r.customerPointsHandler.GetCustomerPoints)
|
||||
customer.GET("/tokens", r.customerPointsHandler.GetCustomerTokens)
|
||||
customer.GET("/wallet", r.customerPointsHandler.GetCustomerWallet)
|
||||
customer.GET("/games", r.customerPointsHandler.GetCustomerGames)
|
||||
customer.GET("/ferris-wheel", r.customerPointsHandler.GetFerrisWheelGame)
|
||||
}
|
||||
|
||||
organizations := v1.Group("/organizations")
|
||||
|
||||
@ -12,6 +12,8 @@ type CustomerPointsService interface {
|
||||
GetCustomerPoints(ctx context.Context, customerID string) (*models.GetCustomerPointsResponse, error)
|
||||
GetCustomerTokens(ctx context.Context, customerID string) (*models.GetCustomerTokensResponse, error)
|
||||
GetCustomerWallet(ctx context.Context, customerID string) (*models.GetCustomerWalletResponse, error)
|
||||
GetCustomerGames(ctx context.Context) (*models.GetCustomerGamesResponse, error)
|
||||
GetFerrisWheelGame(ctx context.Context) (*models.GetFerrisWheelGameResponse, error)
|
||||
}
|
||||
|
||||
type customerPointsService struct {
|
||||
@ -62,3 +64,21 @@ func (s *customerPointsService) GetCustomerWallet(ctx context.Context, customerI
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *customerPointsService) GetCustomerGames(ctx context.Context) (*models.GetCustomerGamesResponse, error) {
|
||||
response, err := s.customerPointsProcessor.GetCustomerGamesAPI(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get customer games: %w", err)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *customerPointsService) GetFerrisWheelGame(ctx context.Context) (*models.GetFerrisWheelGameResponse, error) {
|
||||
response, err := s.customerPointsProcessor.GetFerrisWheelGameAPI(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get Ferris Wheel game: %w", err)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
@ -49,7 +49,11 @@ func CustomerPointsModelToResponse(model *models.CustomerPointsResponse) *contra
|
||||
}
|
||||
}
|
||||
|
||||
func PaginatedCustomerPointsResponseToContract(model *models.PaginatedResponse[models.CustomerPointsResponse]) *contract.PaginatedCustomerPointsResponse {
|
||||
func PaginatedCustomerPointsResponseToContract(model *models.PaginatedCustomerPointsResponse) *contract.PaginatedCustomerPointsResponse {
|
||||
if model == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
responses := make([]contract.CustomerPointsResponse, len(model.Data))
|
||||
for i, item := range model.Data {
|
||||
responses[i] = *CustomerPointsModelToResponse(&item)
|
||||
@ -57,10 +61,10 @@ func PaginatedCustomerPointsResponseToContract(model *models.PaginatedResponse[m
|
||||
|
||||
return &contract.PaginatedCustomerPointsResponse{
|
||||
Data: responses,
|
||||
TotalCount: int(model.Pagination.Total),
|
||||
Page: model.Pagination.Page,
|
||||
Limit: model.Pagination.Limit,
|
||||
TotalPages: model.Pagination.TotalPages,
|
||||
TotalCount: model.TotalCount,
|
||||
Page: model.Page,
|
||||
Limit: model.Limit,
|
||||
TotalPages: model.TotalPages,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
3
migrations/000059_add_image_to_game_prizes.down.sql
Normal file
3
migrations/000059_add_image_to_game_prizes.down.sql
Normal file
@ -0,0 +1,3 @@
|
||||
-- Remove image field from game_prizes table
|
||||
ALTER TABLE game_prizes
|
||||
DROP COLUMN image;
|
||||
6
migrations/000059_add_image_to_game_prizes.up.sql
Normal file
6
migrations/000059_add_image_to_game_prizes.up.sql
Normal file
@ -0,0 +1,6 @@
|
||||
-- Add image field to game_prizes table
|
||||
ALTER TABLE game_prizes
|
||||
ADD COLUMN image VARCHAR(500);
|
||||
|
||||
-- Add comment for the new column
|
||||
COMMENT ON COLUMN game_prizes.image IS 'URL or path to the prize image';
|
||||
3
migrations/000060_add_metadata_to_otp_sessions.down.sql
Normal file
3
migrations/000060_add_metadata_to_otp_sessions.down.sql
Normal file
@ -0,0 +1,3 @@
|
||||
-- Remove metadata field from otp_sessions table
|
||||
ALTER TABLE otp_sessions
|
||||
DROP COLUMN metadata;
|
||||
6
migrations/000060_add_metadata_to_otp_sessions.up.sql
Normal file
6
migrations/000060_add_metadata_to_otp_sessions.up.sql
Normal file
@ -0,0 +1,6 @@
|
||||
-- Add metadata field to otp_sessions table
|
||||
ALTER TABLE otp_sessions
|
||||
ADD COLUMN metadata JSONB DEFAULT '{}';
|
||||
|
||||
-- Add comment for the new column
|
||||
COMMENT ON COLUMN otp_sessions.metadata IS 'Additional data stored as JSON for the OTP session';
|
||||
Loading…
x
Reference in New Issue
Block a user