package referralservice import ( "context" "crypto/rand" "encoding/base32" "errors" "fmt" "log/slog" "strconv" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" ) type Service struct { repo repository.ReferralRepository walletSvc wallet.Service store *repository.Store config *config.Config logger *slog.Logger } func New(repo repository.ReferralRepository, walletSvc wallet.Service, store *repository.Store, cfg *config.Config, logger *slog.Logger) *Service { return &Service{ repo: repo, walletSvc: walletSvc, store: store, config: cfg, logger: logger, } } var ( ErrInvalidReferral = errors.New("invalid or expired referral") ErrInvalidReferralSignup = errors.New("referral requires phone signup") ErrUserNotFound = errors.New("user not found") ErrNoReferralFound = errors.New("no referral found for this user") ) func (s *Service) GenerateReferralCode() (string, error) { b := make([]byte, 8) if _, err := rand.Read(b); err != nil { s.logger.Error("Failed to generate random bytes for referral code", "error", err) return "", err } code := base32.StdEncoding.EncodeToString(b)[:10] s.logger.Debug("Generated referral code", "code", code) return code, nil } func (s *Service) CreateReferral(ctx context.Context, userID int64) error { s.logger.Info("Creating referral code for user", "userID", userID) // check if user already has an active referral code referral, err := s.repo.GetActiveReferralByReferrerID(ctx, fmt.Sprintf("%d", userID)) if err != nil { s.logger.Error("Failed to check if user alredy has active referral code", "error", err) return err } if referral != nil && referral.Status == domain.ReferralPending && referral.ExpiresAt.After(time.Now()) { s.logger.Error("user already has an active referral code", "error", err) return err } settings, err := s.GetReferralSettings(ctx) if err != nil || settings == nil { s.logger.Error("Failed to fetch referral settings", "error", err) return err } // check referral count limit referralCount, err := s.GetReferralCountByID(ctx, fmt.Sprintf("%d", userID)) if err != nil { s.logger.Error("Failed to get referral count", "userID", userID, "error", err) return err } fmt.Println("referralCount: ", referralCount) if referralCount == int64(settings.MaxReferrals) { s.logger.Error("referral count limit has been reached", "referralCount", referralCount, "error", err) return err } code, err := s.GenerateReferralCode() if err != nil { s.logger.Error("Failed to generate referral code", "error", err) return err } var rewardAmount float64 = settings.ReferralRewardAmount var expireDuration time.Time = time.Now().Add(time.Duration((24 * settings.ExpiresAfterDays)) * time.Hour) if err := s.repo.CreateReferral(ctx, &domain.Referral{ ReferralCode: code, ReferrerID: fmt.Sprintf("%d", userID), Status: domain.ReferralPending, RewardAmount: rewardAmount, ExpiresAt: expireDuration, }); err != nil { return err } return nil } func (s *Service) ProcessReferral(ctx context.Context, referredPhone, referralCode string, companyID int64) error { s.logger.Info("Processing referral", "referredPhone", referredPhone, "referralCode", referralCode) referral, err := s.repo.GetReferralByCode(ctx, referralCode) if err != nil || referral == nil { s.logger.Error("Failed to get referral by code", "referralCode", referralCode, "error", err) return err } if referral.Status != domain.ReferralPending || referral.ExpiresAt.Before(time.Now()) { s.logger.Warn("Invalid or expired referral", "referralCode", referralCode, "status", referral.Status) return ErrInvalidReferral } user, err := s.store.GetUserByPhone(ctx, referredPhone, domain.ValidInt64{ Value: companyID, Valid: true, }) if err != nil { if errors.Is(err, domain.ErrUserNotFound) { s.logger.Warn("User not found for referral", "referredPhone", referredPhone) return ErrUserNotFound } s.logger.Error("Failed to get user by phone", "referredPhone", referredPhone, "error", err) return err } if !user.PhoneVerified { s.logger.Warn("Phone not verified for referral", "referredPhone", referredPhone) return ErrInvalidReferralSignup } referral.ReferredID = &referredPhone referral.Status = domain.ReferralCompleted referral.UpdatedAt = time.Now() if err := s.repo.UpdateReferral(ctx, referral); err != nil { s.logger.Error("Failed to update referral", "referralCode", referralCode, "error", err) return err } referrerId, err := strconv.Atoi(referral.ReferrerID) if err != nil { s.logger.Error("Failed to convert referrer id", "referrerId", referral.ReferrerID, "error", err) return err } wallets, err := s.store.GetCustomerWallet(ctx, int64(referrerId)) if err != nil { s.logger.Error("Failed to get referrer wallets", "referrerId", referral.ReferrerID, "error", err) return err } _, err = s.walletSvc.AddToWallet(ctx, wallets.StaticID, domain.ToCurrency(float32(referral.RewardAmount)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, fmt.Sprintf("Added %v to static wallet because of referral ID %v", referral.RewardAmount, referrerId), ) if err != nil { s.logger.Error("Failed to add referral reward to static wallet", "walletID", wallets.StaticID, "referrer phone number", referredPhone, "error", err) return err } s.logger.Info("Referral processed successfully", "referredPhone", referredPhone, "referralCode", referralCode, "rewardAmount", referral.RewardAmount) return nil } func (s *Service) ProcessDepositBonus(ctx context.Context, userPhone string, amount float64) error { s.logger.Info("Processing deposit bonus", "userPhone", userPhone, "amount", amount) settings, err := s.repo.GetSettings(ctx) if err != nil { s.logger.Error("Failed to get referral settings", "error", err) return err } userID, err := strconv.ParseInt(userPhone, 10, 64) if err != nil { s.logger.Error("Invalid phone number format", "userPhone", userPhone, "error", err) return errors.New("invalid phone number format") } wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID) if err != nil { s.logger.Error("Failed to get wallets for user", "userID", userID, "error", err) return err } if len(wallets) == 0 { s.logger.Error("User has no wallet", "userID", userID) return errors.New("user has no wallet") } walletID := wallets[0].ID bonus := amount * (settings.CashbackPercentage / 100) currentBonus := float64(wallets[0].Balance) _, err = s.walletSvc.AddToWallet(ctx, walletID, domain.ToCurrency(float32(currentBonus+bonus)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, fmt.Sprintf("Added %v to static wallet because of Deposit Cashback Bonus %d", currentBonus+bonus, bonus)) if err != nil { s.logger.Error("Failed to add deposit bonus to wallet", "walletID", walletID, "userID", userID, "bonus", bonus, "error", err) return err } s.logger.Info("Deposit bonus processed successfully", "userPhone", userPhone, "bonus", bonus) return nil } func (s *Service) ProcessBetReferral(ctx context.Context, userPhone string, betAmount float64) error { s.logger.Info("Processing bet referral", "userPhone", userPhone, "betAmount", betAmount) settings, err := s.repo.GetSettings(ctx) if err != nil { s.logger.Error("Failed to get referral settings", "error", err) return err } referral, err := s.repo.GetReferralByReferredID(ctx, userPhone) if err != nil { s.logger.Error("Failed to get referral by referred ID", "userPhone", userPhone, "error", err) return err } if referral == nil || referral.Status != domain.ReferralCompleted { s.logger.Warn("No valid referral found", "userPhone", userPhone, "status", referral.Status) return ErrNoReferralFound } referrerID, err := strconv.ParseInt(referral.ReferrerID, 10, 64) if err != nil { s.logger.Error("Invalid referrer phone number format", "referrerID", referral.ReferrerID, "error", err) return errors.New("invalid referrer phone number format") } wallets, err := s.walletSvc.GetWalletsByUser(ctx, referrerID) if err != nil { s.logger.Error("Failed to get wallets for referrer", "referrerID", referrerID, "error", err) return err } if len(wallets) == 0 { s.logger.Error("Referrer has no wallet", "referrerID", referrerID) return errors.New("referrer has no wallet") } bonusPercentage := settings.BetReferralBonusPercentage if bonusPercentage == 0 { bonusPercentage = 5.0 s.logger.Debug("Using default bet referral bonus percentage", "percentage", bonusPercentage) } bonus := betAmount * (bonusPercentage / 100) walletID := wallets[0].ID currentBalance := float64(wallets[0].Balance) _, err = s.walletSvc.AddToWallet(ctx, walletID, domain.ToCurrency(float32(currentBalance+bonus)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, fmt.Sprintf("Added %v to static wallet because of bet referral", referral.RewardAmount)) if err != nil { s.logger.Error("Failed to add bet referral bonus to wallet", "walletID", walletID, "referrerID", referrerID, "bonus", bonus, "error", err) return err } s.logger.Info("Bet referral processed successfully", "userPhone", userPhone, "referrerID", referrerID, "bonus", bonus) return nil } func (s *Service) GetReferralStats(ctx context.Context, userPhone string) (*domain.ReferralStats, error) { s.logger.Info("Fetching referral stats", "userPhone", userPhone) stats, err := s.repo.GetReferralStats(ctx, userPhone) if err != nil { s.logger.Error("Failed to get referral stats", "userPhone", userPhone, "error", err) return nil, err } s.logger.Info("Referral stats retrieved successfully", "userPhone", userPhone, "totalReferrals", stats.TotalReferrals) return stats, nil } func (s *Service) CreateReferralSettings(ctx context.Context, req domain.ReferralSettingsReq) error { s.logger.Info("Creating referral setting") if err := s.repo.CreateSettings(ctx, &domain.ReferralSettings{ ReferralRewardAmount: req.ReferralRewardAmount, CashbackPercentage: req.CashbackPercentage, MaxReferrals: req.MaxReferrals, ExpiresAfterDays: req.ExpiresAfterDays, UpdatedBy: req.UpdatedBy, CreatedAt: time.Now(), UpdatedAt: time.Now(), }); err != nil { s.logger.Error("Failed to create referral setting", "error", err) return err } s.logger.Info("Referral setting created succesfully") return nil } func (s *Service) UpdateReferralSettings(ctx context.Context, settings *domain.ReferralSettings) error { s.logger.Info("Updating referral settings", "settingsID", settings.ID) settings.UpdatedAt = time.Now() err := s.repo.UpdateSettings(ctx, settings) if err != nil { s.logger.Error("Failed to update referral settings", "settingsID", settings.ID, "error", err) return err } s.logger.Info("Referral settings updated successfully", "settingsID", settings.ID) return nil } func (s *Service) GetReferralSettings(ctx context.Context) (*domain.ReferralSettings, error) { s.logger.Info("Fetching referral settings") settings, err := s.repo.GetSettings(ctx) if err != nil { s.logger.Error("Failed to get referral settings", "error", err) return nil, err } s.logger.Info("Referral settings retrieved successfully", "settings", settings) return settings, nil } func (s *Service) GetReferralCountByID(ctx context.Context, referrerID string) (int64, error) { count, err := s.repo.GetReferralCountByID(ctx, referrerID) if err != nil { s.logger.Error("Failed to get referral count", "userID", referrerID, "error", err) return 0, err } return count, nil }