package service import ( "context" "crypto/rand" "encoding/json" "errors" "fmt" "sno/internal/models" "sno/internal/repository" "strconv" "time" "github.com/redis/go-redis/v9" ) // QRService defines the interface for QR code processing logic. type QRService interface { ValidatePayload(ctx context.Context, userID int64, payload string) (*models.QRValidateResponse, error) GenerateUniqueToken(ctx context.Context, qrType string, value string) (string, error) ValidateUniqueToken(ctx context.Context, token string) (*models.QRTokenData, error) } // qrService implements the QRService interface. type qrService struct { qrScanRepo repository.QRScanRepository adminSvc AdminService quizSvc QuizService redis *redis.Client } // NewQRService creates a new instance of a QR service. func NewQRService(qrRepo repository.QRScanRepository, adminSvc AdminService, quizSvc QuizService, redisClient *redis.Client) QRService { return &qrService{ qrScanRepo: qrRepo, adminSvc: adminSvc, quizSvc: quizSvc, redis: redisClient, } } // generateRandomToken generates a secure random token func (s *qrService) generateRandomToken() (string, error) { b := make([]byte, 16) // 128 bits _, err := rand.Read(b) if err != nil { return "", err } return fmt.Sprintf("%x", b), nil } // GenerateUniqueToken creates a new unique QR token and stores it in Redis func (s *qrService) GenerateUniqueToken(ctx context.Context, qrType string, value string) (string, error) { token, err := s.generateRandomToken() if err != nil { return "", fmt.Errorf("failed to generate token: %w", err) } tokenData := &models.QRTokenData{ Type: qrType, Value: value, Used: false, } // Store in Redis with 30 day expiration data, err := json.Marshal(tokenData) if err != nil { return "", fmt.Errorf("failed to marshal token data: %w", err) } key := fmt.Sprintf("qr_token:%s", token) err = s.redis.Set(ctx, key, data, 30*24*time.Hour).Err() if err != nil { return "", fmt.Errorf("failed to store token in Redis: %w", err) } return token, nil } // ValidateUniqueToken validates a QR token and marks it as used func (s *qrService) ValidateUniqueToken(ctx context.Context, token string) (*models.QRTokenData, error) { key := fmt.Sprintf("qr_token:%s", token) // Get token data from Redis data, err := s.redis.Get(ctx, key).Bytes() if err != nil { if err == redis.Nil { return nil, errors.New("invalid or expired token") } return nil, fmt.Errorf("failed to get token from Redis: %w", err) } var tokenData models.QRTokenData if err := json.Unmarshal(data, &tokenData); err != nil { return nil, fmt.Errorf("failed to unmarshal token data: %w", err) } // Check if token is already used if tokenData.Used { return nil, errors.New("token has already been used") } // Mark token as used tokenData.Used = true updatedData, err := json.Marshal(tokenData) if err != nil { return nil, fmt.Errorf("failed to marshal updated token data: %w", err) } err = s.redis.Set(ctx, key, updatedData, 30*24*time.Hour).Err() if err != nil { return nil, fmt.Errorf("failed to update token in Redis: %w", err) } return &tokenData, nil } // ValidatePayload validates a QR token and performs the corresponding action. func (s *qrService) ValidatePayload(ctx context.Context, userID int64, payload string) (*models.QRValidateResponse, error) { // Validate as unique token tokenData, err := s.ValidateUniqueToken(ctx, payload) if err != nil { return nil, err } // Process the validated token data return s.processTokenData(ctx, userID, tokenData, payload) } // processTokenData processes validated token data and returns response func (s *qrService) processTokenData(ctx context.Context, userID int64, tokenData *models.QRTokenData, payload string) (*models.QRValidateResponse, error) { scanLog := &models.QRScan{ UserID: userID, Source: models.InApp, Value: &payload, } switch tokenData.Type { case "reward": scanLog.Type = models.QRReward amount, err := strconv.Atoi(tokenData.Value) if err != nil { return nil, fmt.Errorf("invalid reward amount: %s", tokenData.Value) } if err := s.adminSvc.GrantStars(ctx, userID, amount); err != nil { return nil, fmt.Errorf("failed to grant reward: %w", err) } // Log the successful scan _, _ = s.qrScanRepo.Create(ctx, scanLog) return &models.QRValidateResponse{ Type: "REWARD", Data: map[string]int{"amount": amount}, }, nil case "quiz": scanLog.Type = models.QRQuiz quizID, err := strconv.Atoi(tokenData.Value) if err != nil { return nil, fmt.Errorf("invalid quiz ID: %s", tokenData.Value) } quiz, err := s.quizSvc.GetQuizByID(ctx, quizID) if err != nil { return nil, fmt.Errorf("failed to get quiz: %w", err) } // Log the successful scan _, _ = s.qrScanRepo.Create(ctx, scanLog) return &models.QRValidateResponse{ Type: "OPEN_QUIZ", Data: quiz, }, nil case "shop": scanLog.Type = models.QRShop // Shop QR codes can be used for various shop-related actions // For now, just log the scan and return success _, _ = s.qrScanRepo.Create(ctx, scanLog) return &models.QRValidateResponse{ Type: "SHOP_ACTION", Data: map[string]string{"action": tokenData.Value}, }, nil default: return nil, fmt.Errorf("unknown token type: %s", tokenData.Type) } }