feat: referal completed

This commit is contained in:
dawitel 2025-04-12 04:12:22 +03:00
parent f796b97afe
commit 8670fba6a4
10 changed files with 155 additions and 63 deletions

View File

@ -13,6 +13,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
@ -63,13 +64,15 @@ func main() {
transactionSvc := transaction.NewService(store) transactionSvc := transaction.NewService(store)
notificationRepo := repository.NewNotificationRepository(store) notificationRepo := repository.NewNotificationRepository(store)
referalRepo := repository.NewReferralRepository(store)
notificationSvc := notificationservice.New(notificationRepo, logger, cfg) notificationSvc := notificationservice.New(notificationRepo, logger, cfg)
referalSvc := referralservice.New(referalRepo, *walletSvc, store, cfg, logger)
app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{ app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{
JwtAccessKey: cfg.JwtKey, JwtAccessKey: cfg.JwtKey,
JwtAccessExpiry: cfg.AccessExpiry, JwtAccessExpiry: cfg.AccessExpiry,
}, userSvc, ticketSvc, betSvc, walletSvc, transactionSvc, notificationSvc, }, userSvc, ticketSvc, betSvc, walletSvc, transactionSvc, notificationSvc, referalSvc)
)
logger.Info("Starting server", "port", cfg.Port) logger.Info("Starting server", "port", cfg.Port)
if err := app.Run(); err != nil { if err := app.Run(); err != nil {

View File

@ -22,6 +22,13 @@ SET
WHERE id = $1 WHERE id = $1
RETURNING *; RETURNING *;
-- name: UpdateReferralCode :exec
UPDATE users
SET
referral_code = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: GetReferralStats :one -- name: GetReferralStats :one
SELECT SELECT
COUNT(*) as total_referrals, COUNT(*) as total_referrals,

View File

@ -234,6 +234,24 @@ func (q *Queries) UpdateReferral(ctx context.Context, arg UpdateReferralParams)
return i, err return i, err
} }
const UpdateReferralCode = `-- name: UpdateReferralCode :exec
UPDATE users
SET
referral_code = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`
type UpdateReferralCodeParams struct {
ID int64
ReferralCode pgtype.Text
}
func (q *Queries) UpdateReferralCode(ctx context.Context, arg UpdateReferralCodeParams) error {
_, err := q.db.Exec(ctx, UpdateReferralCode, arg.ID, arg.ReferralCode)
return err
}
const UpdateReferralSettings = `-- name: UpdateReferralSettings :one const UpdateReferralSettings = `-- name: UpdateReferralSettings :one
UPDATE referral_settings UPDATE referral_settings
SET SET

View File

@ -50,3 +50,8 @@ type UpdateUserReq struct {
LastName ValidString LastName ValidString
Suspended ValidBool Suspended ValidBool
} }
type UpdateUserReferalCode struct {
UserID int64
Code string
}

View File

@ -19,6 +19,7 @@ type ReferralRepository interface {
UpdateSettings(ctx context.Context, settings *domain.ReferralSettings) error UpdateSettings(ctx context.Context, settings *domain.ReferralSettings) error
CreateSettings(ctx context.Context, settings *domain.ReferralSettings) error CreateSettings(ctx context.Context, settings *domain.ReferralSettings) error
GetReferralByReferredID(ctx context.Context, referredID string) (*domain.Referral, error) // New method GetReferralByReferredID(ctx context.Context, referredID string) (*domain.Referral, error) // New method
UpdateUserReferalCode(ctx context.Context, codedata domain.UpdateUserReferalCode) error
} }
type ReferralRepo struct { type ReferralRepo struct {
@ -29,6 +30,18 @@ func NewReferralRepository(store *Store) ReferralRepository {
return &ReferralRepo{store: store} return &ReferralRepo{store: store}
} }
func (r *ReferralRepo) UpdateUserReferalCode(ctx context.Context, codedata domain.UpdateUserReferalCode) error {
params := dbgen.UpdateReferralCodeParams{
ID: codedata.UserID,
ReferralCode: pgtype.Text{
String: codedata.Code,
Valid: true,
},
}
return r.store.queries.UpdateReferralCode(ctx, params)
}
func (r *ReferralRepo) CreateReferral(ctx context.Context, referral *domain.Referral) error { func (r *ReferralRepo) CreateReferral(ctx context.Context, referral *domain.Referral) error {
rewardAmount := pgtype.Numeric{} rewardAmount := pgtype.Numeric{}
if err := rewardAmount.Scan(referral.RewardAmount); err != nil { if err := rewardAmount.Scan(referral.RewardAmount); err != nil {

View File

@ -17,11 +17,11 @@ import (
type Service struct { type Service struct {
repo repository.NotificationRepository repo repository.NotificationRepository
logger *slog.Logger
connections sync.Map connections sync.Map
notificationCh chan *domain.Notification notificationCh chan *domain.Notification
stopCh chan struct{} stopCh chan struct{}
config *config.Config config *config.Config
logger *slog.Logger
} }
func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *config.Config) NotificationStore { func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *config.Config) NotificationStore {

View File

@ -8,7 +8,7 @@ import (
type ReferralStore interface { type ReferralStore interface {
GenerateReferralCode() (string, error) GenerateReferralCode() (string, error)
CreateReferral(ctx context.Context, userID string) (*domain.Referral, error) CreateReferral(ctx context.Context, userID int64) error
ProcessReferral(ctx context.Context, referredID, referralCode string) error ProcessReferral(ctx context.Context, referredID, referralCode string) error
ProcessDepositBonus(ctx context.Context, userID string, amount float64) error ProcessDepositBonus(ctx context.Context, userID string, amount float64) error
GetReferralStats(ctx context.Context, userID string) (*domain.ReferralStats, error) GetReferralStats(ctx context.Context, userID string) (*domain.ReferralStats, error)

View File

@ -5,9 +5,11 @@ import (
"crypto/rand" "crypto/rand"
"encoding/base32" "encoding/base32"
"errors" "errors"
"log/slog"
"strconv" "strconv"
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
@ -17,13 +19,17 @@ type Service struct {
repo repository.ReferralRepository repo repository.ReferralRepository
walletSvc wallet.Service walletSvc wallet.Service
store *repository.Store store *repository.Store
config *config.Config
logger *slog.Logger
} }
func NewService(repo repository.ReferralRepository, walletSvc wallet.Service, store *repository.Store) *Service { func New(repo repository.ReferralRepository, walletSvc wallet.Service, store *repository.Store, cfg *config.Config, logger *slog.Logger) *Service {
return &Service{ return &Service{
repo: repo, repo: repo,
walletSvc: walletSvc, walletSvc: walletSvc,
store: store, store: store,
config: cfg,
logger: logger,
} }
} }
@ -37,77 +43,57 @@ var (
func (s *Service) GenerateReferralCode() (string, error) { func (s *Service) GenerateReferralCode() (string, error) {
b := make([]byte, 8) b := make([]byte, 8)
if _, err := rand.Read(b); err != nil { if _, err := rand.Read(b); err != nil {
s.logger.Error("Failed to generate random bytes for referral code", "error", err)
return "", err return "", err
} }
return base32.StdEncoding.EncodeToString(b)[:10], nil code := base32.StdEncoding.EncodeToString(b)[:10]
} s.logger.Debug("Generated referral code", "code", code)
return code, nil
func (s *Service) CreateReferral(ctx context.Context, userPhone string) (*domain.Referral, error) {
settings, err := s.repo.GetSettings(ctx)
if err != nil {
return nil, err
} }
func (s *Service) CreateReferral(ctx context.Context, userID int64) error {
s.logger.Info("Creating referral code for user", "userID", userID)
code, err := s.GenerateReferralCode() code, err := s.GenerateReferralCode()
if err != nil { if err != nil {
return nil, err s.logger.Error("Failed to generate referral code", "error", err)
return err
} }
userID, err := strconv.ParseInt(userPhone, 10, 64) if err := s.repo.UpdateUserReferalCode(ctx, domain.UpdateUserReferalCode{
if err != nil {
return nil, errors.New("invalid phone number format")
}
wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID)
if err != nil {
return nil, err
}
if len(wallets) == 0 {
_, err = s.walletSvc.CreateWallet(ctx, domain.CreateWallet{
IsWithdraw: true,
IsBettable: true,
UserID: userID, UserID: userID,
}) Code: code,
if err != nil { }); err != nil {
return nil, err return err
}
} }
referral := &domain.Referral{ return nil
ReferrerID: userPhone,
ReferralCode: code,
Status: domain.ReferralPending,
RewardAmount: settings.ReferralRewardAmount,
ExpiresAt: time.Now().Add(time.Duration(settings.ExpiresAfterDays) * 24 * time.Hour),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.repo.CreateReferral(ctx, referral); err != nil {
return nil, err
}
return referral, nil
} }
func (s *Service) ProcessReferral(ctx context.Context, referredPhone, referralCode string) error { func (s *Service) ProcessReferral(ctx context.Context, referredPhone, referralCode string) error {
s.logger.Info("Processing referral", "referredPhone", referredPhone, "referralCode", referralCode)
referral, err := s.repo.GetReferralByCode(ctx, referralCode) referral, err := s.repo.GetReferralByCode(ctx, referralCode)
if err != nil { if err != nil {
s.logger.Error("Failed to get referral by code", "referralCode", referralCode, "error", err)
return err return err
} }
if referral == nil || referral.Status != domain.ReferralPending || referral.ExpiresAt.Before(time.Now()) { if referral == nil || referral.Status != domain.ReferralPending || referral.ExpiresAt.Before(time.Now()) {
s.logger.Warn("Invalid or expired referral", "referralCode", referralCode, "status", referral.Status)
return ErrInvalidReferral return ErrInvalidReferral
} }
user, err := s.store.GetUserByPhone(ctx, referredPhone) user, err := s.store.GetUserByPhone(ctx, referredPhone)
if err != nil { if err != nil {
if errors.Is(err, domain.ErrUserNotFound) { if errors.Is(err, domain.ErrUserNotFound) {
s.logger.Warn("User not found for referral", "referredPhone", referredPhone)
return ErrUserNotFound return ErrUserNotFound
} }
s.logger.Error("Failed to get user by phone", "referredPhone", referredPhone, "error", err)
return err return err
} }
if !user.PhoneVerified { if !user.PhoneVerified {
s.logger.Warn("Phone not verified for referral", "referredPhone", referredPhone)
return ErrInvalidReferralSignup return ErrInvalidReferralSignup
} }
@ -116,99 +102,166 @@ func (s *Service) ProcessReferral(ctx context.Context, referredPhone, referralCo
referral.UpdatedAt = time.Now() referral.UpdatedAt = time.Now()
if err := s.repo.UpdateReferral(ctx, referral); err != nil { if err := s.repo.UpdateReferral(ctx, referral); err != nil {
s.logger.Error("Failed to update referral", "referralCode", referralCode, "error", err)
return err return err
} }
referrerID, err := strconv.ParseInt(referral.ReferrerID, 10, 64) referrerID, err := strconv.ParseInt(referral.ReferrerID, 10, 64)
if err != nil { if err != nil {
s.logger.Error("Invalid referrer phone number format", "referrerID", referral.ReferrerID, "error", err)
return errors.New("invalid referrer phone number format") return errors.New("invalid referrer phone number format")
} }
wallets, err := s.walletSvc.GetWalletsByUser(ctx, referrerID) wallets, err := s.walletSvc.GetWalletsByUser(ctx, referrerID)
if err != nil { if err != nil {
s.logger.Error("Failed to get wallets for referrer", "referrerID", referrerID, "error", err)
return err return err
} }
if len(wallets) == 0 { if len(wallets) == 0 {
s.logger.Error("Referrer has no wallet", "referrerID", referrerID)
return errors.New("referrer has no wallet") return errors.New("referrer has no wallet")
} }
walletID := wallets[0].ID walletID := wallets[0].ID
currentBonus := float64(wallets[0].Balance) currentBonus := float64(wallets[0].Balance)
return s.walletSvc.Add(ctx, walletID, domain.Currency(int64((currentBonus+referral.RewardAmount)*100))) err = s.walletSvc.Add(ctx, walletID, domain.Currency(int64((currentBonus+referral.RewardAmount)*100)))
if err != nil {
s.logger.Error("Failed to add referral reward to wallet", "walletID", walletID, "referrerID", referrerID, "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 { 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) settings, err := s.repo.GetSettings(ctx)
if err != nil { if err != nil {
s.logger.Error("Failed to get referral settings", "error", err)
return err return err
} }
userID, err := strconv.ParseInt(userPhone, 10, 64) userID, err := strconv.ParseInt(userPhone, 10, 64)
if err != nil { if err != nil {
s.logger.Error("Invalid phone number format", "userPhone", userPhone, "error", err)
return errors.New("invalid phone number format") return errors.New("invalid phone number format")
} }
wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID) wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID)
if err != nil { if err != nil {
s.logger.Error("Failed to get wallets for user", "userID", userID, "error", err)
return err return err
} }
if len(wallets) == 0 { if len(wallets) == 0 {
s.logger.Error("User has no wallet", "userID", userID)
return errors.New("user has no wallet") return errors.New("user has no wallet")
} }
walletID := wallets[0].ID walletID := wallets[0].ID
bonus := amount * (settings.CashbackPercentage / 100) bonus := amount * (settings.CashbackPercentage / 100)
currentBonus := float64(wallets[0].Balance) currentBonus := float64(wallets[0].Balance)
return s.walletSvc.Add(ctx, walletID, domain.Currency(int64((currentBonus+bonus)*100))) err = s.walletSvc.Add(ctx, walletID, domain.Currency(int64((currentBonus+bonus)*100)))
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 { 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) settings, err := s.repo.GetSettings(ctx)
if err != nil { if err != nil {
s.logger.Error("Failed to get referral settings", "error", err)
return err return err
} }
referral, err := s.repo.GetReferralByReferredID(ctx, userPhone) referral, err := s.repo.GetReferralByReferredID(ctx, userPhone)
if err != nil { if err != nil {
s.logger.Error("Failed to get referral by referred ID", "userPhone", userPhone, "error", err)
return err return err
} }
if referral == nil || referral.Status != domain.ReferralCompleted { if referral == nil || referral.Status != domain.ReferralCompleted {
s.logger.Warn("No valid referral found", "userPhone", userPhone, "status", referral.Status)
return ErrNoReferralFound return ErrNoReferralFound
} }
referrerID, err := strconv.ParseInt(referral.ReferrerID, 10, 64) referrerID, err := strconv.ParseInt(referral.ReferrerID, 10, 64)
if err != nil { if err != nil {
s.logger.Error("Invalid referrer phone number format", "referrerID", referral.ReferrerID, "error", err)
return errors.New("invalid referrer phone number format") return errors.New("invalid referrer phone number format")
} }
wallets, err := s.walletSvc.GetWalletsByUser(ctx, referrerID) wallets, err := s.walletSvc.GetWalletsByUser(ctx, referrerID)
if err != nil { if err != nil {
s.logger.Error("Failed to get wallets for referrer", "referrerID", referrerID, "error", err)
return err return err
} }
if len(wallets) == 0 { if len(wallets) == 0 {
s.logger.Error("Referrer has no wallet", "referrerID", referrerID)
return errors.New("referrer has no wallet") return errors.New("referrer has no wallet")
} }
bonusPercentage := settings.BetReferralBonusPercentage bonusPercentage := settings.BetReferralBonusPercentage
if bonusPercentage == 0 { if bonusPercentage == 0 {
bonusPercentage = 5.0 bonusPercentage = 5.0
s.logger.Debug("Using default bet referral bonus percentage", "percentage", bonusPercentage)
} }
bonus := betAmount * (bonusPercentage / 100) bonus := betAmount * (bonusPercentage / 100)
walletID := wallets[0].ID walletID := wallets[0].ID
currentBalance := float64(wallets[0].Balance) currentBalance := float64(wallets[0].Balance)
return s.walletSvc.Add(ctx, walletID, domain.Currency(int64((currentBalance+bonus)*100))) err = s.walletSvc.Add(ctx, walletID, domain.Currency(int64((currentBalance+bonus)*100)))
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) { func (s *Service) GetReferralStats(ctx context.Context, userPhone string) (*domain.ReferralStats, error) {
return s.repo.GetReferralStats(ctx, userPhone) 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) UpdateReferralSettings(ctx context.Context, settings *domain.ReferralSettings) error { func (s *Service) UpdateReferralSettings(ctx context.Context, settings *domain.ReferralSettings) error {
s.logger.Info("Updating referral settings", "settingsID", settings.ID)
settings.UpdatedAt = time.Now() settings.UpdatedAt = time.Now()
return s.repo.UpdateSettings(ctx, settings) 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) { func (s *Service) GetReferralSettings(ctx context.Context) (*domain.ReferralSettings, error) {
return s.repo.GetSettings(ctx) 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", "settingsID", settings.ID)
return settings, nil
} }

View File

@ -6,26 +6,19 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
func (h *Handler) CreateReferral(c *fiber.Ctx) error { func (h *Handler) CreateReferralCode(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(int64) userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 { if !ok || userID == 0 {
h.logger.Error("Invalid user ID in context") h.logger.Error("Invalid user ID in context")
return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification") return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification")
} }
user, err := h.userSvc.GetUserByID(c.Context(), userID) if err := h.referralSvc.CreateReferral(c.Context(), userID); err != nil {
if err != nil {
h.logger.Error("Failed to get user", "userID", userID, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user")
}
referral, err := h.referralSvc.CreateReferral(c.Context(), user.PhoneNumber)
if err != nil {
h.logger.Error("Failed to create referral", "userID", userID, "error", err) h.logger.Error("Failed to create referral", "userID", userID, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create referral") return fiber.NewError(fiber.StatusInternalServerError, "Failed to create referral")
} }
return response.WriteJSON(c, fiber.StatusOK, "Referral created successfully", referral, nil) return response.WriteJSON(c, fiber.StatusOK, "Referral created successfully", nil, nil)
} }
// GetReferralStats godoc // GetReferralStats godoc

View File

@ -71,7 +71,7 @@ func (a *App) initAppRoutes() {
a.fiber.Patch("/wallet/:id", h.UpdateWalletActive) a.fiber.Patch("/wallet/:id", h.UpdateWalletActive)
// Referral Routes // Referral Routes
a.fiber.Post("/referral/create", a.authMiddleware, h.CreateReferral) a.fiber.Post("/referral/create", a.authMiddleware, h.CreateReferralCode)
a.fiber.Get("/referral/stats", a.authMiddleware, h.GetReferralStats) a.fiber.Get("/referral/stats", a.authMiddleware, h.GetReferralStats)
a.fiber.Get("/referral/settings", h.GetReferralSettings) a.fiber.Get("/referral/settings", h.GetReferralSettings)
a.fiber.Patch("/referral/settings", a.authMiddleware, h.UpdateReferralSettings) a.fiber.Patch("/referral/settings", a.authMiddleware, h.UpdateReferralSettings)