test wheels

This commit is contained in:
Aditya Siregar
2025-09-18 12:01:20 +07:00
parent f64fec1fe2
commit be92ec8b23
21 changed files with 420 additions and 85 deletions
+76 -72
View File
@@ -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: &registrationSession.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
}
+2 -2
View File
@@ -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,