diff --git a/cmd/main.go b/cmd/main.go index 1019bb5..2a5d805 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -142,7 +142,7 @@ func main() { ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger, *settingSvc, notificationSvc) betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, *companySvc, *settingSvc, *userSvc, notificationSvc, logger, domain.MongoDBLogger) resultSvc := result.NewService(store, cfg, logger, domain.MongoDBLogger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc, messengerSvc, *userSvc) - bonusSvc := bonus.NewService(store) + bonusSvc := bonus.NewService(store, walletSvc, settingSvc, domain.MongoDBLogger) referalRepo := repository.NewReferralRepository(store) vitualGameRepo := repository.NewVirtualGameRepository(store) recommendationRepo := repository.NewRecommendationRepository(store) diff --git a/db/data/001_initial_seed_data.sql b/db/data/001_initial_seed_data.sql index 0441221..55a3650 100644 --- a/db/data/001_initial_seed_data.sql +++ b/db/data/001_initial_seed_data.sql @@ -86,7 +86,10 @@ VALUES ('sms_provider', 'afro_message'), ('default_winning_limit', '5000000'), ('referral_reward_amount', '10000'), ('cashback_percentage', '0.2'), - ('default_max_referrals', '15') ON CONFLICT (key) DO NOTHING; + ('default_max_referrals', '15'), + ('minimum_bet_amount', '100'), + ('send_email_on_bet_finish', 'true'), + ('send_sms_on_bet_finish', 'false') ON CONFLICT (key) DO NOTHING; -- Users INSERT INTO users ( id, @@ -341,5 +344,4 @@ SET name = EXCLUDED.name, profit_percent = EXCLUDED.profit_percent, is_active = EXCLUDED.is_active, created_at = EXCLUDED.created_at, - updated_at = EXCLUDED.updated_at; - + updated_at = EXCLUDED.updated_at; \ No newline at end of file diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 13b3702..c6eefde 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -453,10 +453,17 @@ CREATE TABLE IF NOT EXISTS company_settings ( updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (company_id, key) ); -CREATE TABLE bonus ( - multiplier REAL NOT NULL, - id BIGSERIAL PRIMARY KEY, - balance_cap BIGINT NOT NULL DEFAULT 0 +CREATE TABLE user_bonuses ( + id BIGINT NOT NULL, + name TEXT NOT NULL, + description TEXT NOT NULL, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + bonus_code TEXT NOT NULL UNIQUE, + reward_amount BIGINT NOT NULL, + is_claimed BOOLEAN NOT NULL DEFAULT false, + expires_at TIMESTAMP NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE flags ( id BIGSERIAL PRIMARY KEY, @@ -723,4 +730,4 @@ ADD CONSTRAINT fk_event_settings_company FOREIGN KEY (company_id) REFERENCES com ADD CONSTRAINT fk_event_settings_event FOREIGN KEY (event_id) REFERENCES events (id) ON DELETE CASCADE; ALTER TABLE company_odd_settings ADD CONSTRAINT fk_odds_settings_company FOREIGN KEY (company_id) REFERENCES companies (id) ON DELETE CASCADE, - ADD CONSTRAINT fk_odds_settings_odds_market FOREIGN KEY (odds_market_id) REFERENCES odds_market (id) ON DELETE CASCADE; + ADD CONSTRAINT fk_odds_settings_odds_market FOREIGN KEY (odds_market_id) REFERENCES odds_market (id) ON DELETE CASCADE; \ No newline at end of file diff --git a/db/query/bonus.sql b/db/query/bonus.sql index 82b3113..4b07761 100644 --- a/db/query/bonus.sql +++ b/db/query/bonus.sql @@ -1,17 +1,52 @@ --- name: CreateBonusMultiplier :exec -INSERT INTO bonus (multiplier, balance_cap) -VALUES ($1, $2); - --- name: GetBonusMultiplier :many -SELECT id, multiplier -FROM bonus; - --- name: GetBonusBalanceCap :many -SELECT id, balance_cap -FROM bonus; - --- name: UpdateBonusMultiplier :exec -UPDATE bonus -SET multiplier = $1, - balance_cap = $2 -WHERE id = $3; \ No newline at end of file +-- name: CreateUserBonus :one +INSERT INTO user_bonuses ( + name, + description, + user_id, + bonus_code, + reward_amount, + expires_at + ) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING *; +-- name: GetAllUserBonuses :many +SELECT * +FROM user_bonuses; +-- name: GetUserBonusByID :one +SELECT * +FROM user_bonuses +WHERE id = $1; +-- name: GetBonusesByUserID :many +SELECT * +FROM user_bonuses +WHERE user_id = $1; +-- name: GetBonusStats :one +SELECT COUNT(*) AS total_bonuses, + COALESCE(SUM(reward_amount), 0)::bigint AS total_reward_earned, + COUNT( + CASE + WHEN is_claimed = true THEN 1 + END + ) AS claimed_bonuses, + COUNT( + CASE + WHEN expires_at > now() THEN 1 + END + ) AS expired_bonuses +FROM user_bonuses + JOIN users ON users.id = user_bonuses.user_id +WHERE ( + company_id = sqlc.narg('company_id') + OR sqlc.narg('company_id') IS NULL + ) + AND ( + user_id = sqlc.narg('user_id') + OR sqlc.narg('user_id') IS NULL + ); +-- name: UpdateUserBonus :exec +UPDATE user_bonuses +SET is_claimed = $2 +WHERE id = $1; +-- name: DeleteUserBonus :exec +DELETE FROM user_bonuses +WHERE id = $1; \ No newline at end of file diff --git a/db/query/transfer.sql b/db/query/transfer.sql index dc4c156..0229d0f 100644 --- a/db/query/transfer.sql +++ b/db/query/transfer.sql @@ -30,6 +30,19 @@ WHERE id = $1; SELECT * FROM wallet_transfer_details WHERE reference_number = $1; +-- name: GetTransferStats :one +SELECT COUNT(*) AS total_transfers, COUNT(*) FILTER ( + WHERE type = 'deposit' + ) AS total_deposits, + COUNT(*) FILTER ( + WHERE type = 'withdraw' + ) AS total_withdraw, + COUNT(*) FILTER ( + WHERE type = 'wallet' + ) AS total_wallet_to_wallet +FROM wallet_transfer +WHERE sender_wallet_id = $1 + OR receiver_wallet_id = $1; -- name: UpdateTransferVerification :exec UPDATE wallet_transfer SET verified = $1, diff --git a/gen/db/bonus.sql.go b/gen/db/bonus.sql.go index 12677b8..fe0b99b 100644 --- a/gen/db/bonus.sql.go +++ b/gen/db/bonus.sql.go @@ -7,43 +7,93 @@ package dbgen import ( "context" + + "github.com/jackc/pgx/v5/pgtype" ) -const CreateBonusMultiplier = `-- name: CreateBonusMultiplier :exec -INSERT INTO bonus (multiplier, balance_cap) -VALUES ($1, $2) +const CreateUserBonus = `-- name: CreateUserBonus :one +INSERT INTO user_bonuses ( + name, + description, + user_id, + bonus_code, + reward_amount, + expires_at + ) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING id, name, description, user_id, bonus_code, reward_amount, is_claimed, expires_at, created_at, updated_at ` -type CreateBonusMultiplierParams struct { - Multiplier float32 `json:"multiplier"` - BalanceCap int64 `json:"balance_cap"` +type CreateUserBonusParams struct { + Name string `json:"name"` + Description string `json:"description"` + UserID int64 `json:"user_id"` + BonusCode string `json:"bonus_code"` + RewardAmount int64 `json:"reward_amount"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` } -func (q *Queries) CreateBonusMultiplier(ctx context.Context, arg CreateBonusMultiplierParams) error { - _, err := q.db.Exec(ctx, CreateBonusMultiplier, arg.Multiplier, arg.BalanceCap) +func (q *Queries) CreateUserBonus(ctx context.Context, arg CreateUserBonusParams) (UserBonuse, error) { + row := q.db.QueryRow(ctx, CreateUserBonus, + arg.Name, + arg.Description, + arg.UserID, + arg.BonusCode, + arg.RewardAmount, + arg.ExpiresAt, + ) + var i UserBonuse + err := row.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.UserID, + &i.BonusCode, + &i.RewardAmount, + &i.IsClaimed, + &i.ExpiresAt, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const DeleteUserBonus = `-- name: DeleteUserBonus :exec +DELETE FROM user_bonuses +WHERE id = $1 +` + +func (q *Queries) DeleteUserBonus(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeleteUserBonus, id) return err } -const GetBonusBalanceCap = `-- name: GetBonusBalanceCap :many -SELECT id, balance_cap -FROM bonus +const GetAllUserBonuses = `-- name: GetAllUserBonuses :many +SELECT id, name, description, user_id, bonus_code, reward_amount, is_claimed, expires_at, created_at, updated_at +FROM user_bonuses ` -type GetBonusBalanceCapRow struct { - ID int64 `json:"id"` - BalanceCap int64 `json:"balance_cap"` -} - -func (q *Queries) GetBonusBalanceCap(ctx context.Context) ([]GetBonusBalanceCapRow, error) { - rows, err := q.db.Query(ctx, GetBonusBalanceCap) +func (q *Queries) GetAllUserBonuses(ctx context.Context) ([]UserBonuse, error) { + rows, err := q.db.Query(ctx, GetAllUserBonuses) if err != nil { return nil, err } defer rows.Close() - var items []GetBonusBalanceCapRow + var items []UserBonuse for rows.Next() { - var i GetBonusBalanceCapRow - if err := rows.Scan(&i.ID, &i.BalanceCap); err != nil { + var i UserBonuse + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.UserID, + &i.BonusCode, + &i.RewardAmount, + &i.IsClaimed, + &i.ExpiresAt, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { return nil, err } items = append(items, i) @@ -54,26 +104,82 @@ func (q *Queries) GetBonusBalanceCap(ctx context.Context) ([]GetBonusBalanceCapR return items, nil } -const GetBonusMultiplier = `-- name: GetBonusMultiplier :many -SELECT id, multiplier -FROM bonus +const GetBonusStats = `-- name: GetBonusStats :one +SELECT COUNT(*) AS total_bonuses, + COALESCE(SUM(reward_amount), 0)::bigint AS total_reward_earned, + COUNT( + CASE + WHEN is_claimed = true THEN 1 + END + ) AS claimed_bonuses, + COUNT( + CASE + WHEN expires_at > now() THEN 1 + END + ) AS expired_bonuses +FROM user_bonuses + JOIN users ON users.id = user_bonuses.user_id +WHERE ( + company_id = $1 + OR $1 IS NULL + ) + AND ( + user_id = $2 + OR $2 IS NULL + ) ` -type GetBonusMultiplierRow struct { - ID int64 `json:"id"` - Multiplier float32 `json:"multiplier"` +type GetBonusStatsParams struct { + CompanyID pgtype.Int8 `json:"company_id"` + UserID pgtype.Int8 `json:"user_id"` } -func (q *Queries) GetBonusMultiplier(ctx context.Context) ([]GetBonusMultiplierRow, error) { - rows, err := q.db.Query(ctx, GetBonusMultiplier) +type GetBonusStatsRow struct { + TotalBonuses int64 `json:"total_bonuses"` + TotalRewardEarned int64 `json:"total_reward_earned"` + ClaimedBonuses int64 `json:"claimed_bonuses"` + ExpiredBonuses int64 `json:"expired_bonuses"` +} + +func (q *Queries) GetBonusStats(ctx context.Context, arg GetBonusStatsParams) (GetBonusStatsRow, error) { + row := q.db.QueryRow(ctx, GetBonusStats, arg.CompanyID, arg.UserID) + var i GetBonusStatsRow + err := row.Scan( + &i.TotalBonuses, + &i.TotalRewardEarned, + &i.ClaimedBonuses, + &i.ExpiredBonuses, + ) + return i, err +} + +const GetBonusesByUserID = `-- name: GetBonusesByUserID :many +SELECT id, name, description, user_id, bonus_code, reward_amount, is_claimed, expires_at, created_at, updated_at +FROM user_bonuses +WHERE user_id = $1 +` + +func (q *Queries) GetBonusesByUserID(ctx context.Context, userID int64) ([]UserBonuse, error) { + rows, err := q.db.Query(ctx, GetBonusesByUserID, userID) if err != nil { return nil, err } defer rows.Close() - var items []GetBonusMultiplierRow + var items []UserBonuse for rows.Next() { - var i GetBonusMultiplierRow - if err := rows.Scan(&i.ID, &i.Multiplier); err != nil { + var i UserBonuse + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.UserID, + &i.BonusCode, + &i.RewardAmount, + &i.IsClaimed, + &i.ExpiresAt, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { return nil, err } items = append(items, i) @@ -84,20 +190,42 @@ func (q *Queries) GetBonusMultiplier(ctx context.Context) ([]GetBonusMultiplierR return items, nil } -const UpdateBonusMultiplier = `-- name: UpdateBonusMultiplier :exec -UPDATE bonus -SET multiplier = $1, - balance_cap = $2 -WHERE id = $3 +const GetUserBonusByID = `-- name: GetUserBonusByID :one +SELECT id, name, description, user_id, bonus_code, reward_amount, is_claimed, expires_at, created_at, updated_at +FROM user_bonuses +WHERE id = $1 ` -type UpdateBonusMultiplierParams struct { - Multiplier float32 `json:"multiplier"` - BalanceCap int64 `json:"balance_cap"` - ID int64 `json:"id"` +func (q *Queries) GetUserBonusByID(ctx context.Context, id int64) (UserBonuse, error) { + row := q.db.QueryRow(ctx, GetUserBonusByID, id) + var i UserBonuse + err := row.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.UserID, + &i.BonusCode, + &i.RewardAmount, + &i.IsClaimed, + &i.ExpiresAt, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err } -func (q *Queries) UpdateBonusMultiplier(ctx context.Context, arg UpdateBonusMultiplierParams) error { - _, err := q.db.Exec(ctx, UpdateBonusMultiplier, arg.Multiplier, arg.BalanceCap, arg.ID) +const UpdateUserBonus = `-- name: UpdateUserBonus :exec +UPDATE user_bonuses +SET is_claimed = $2 +WHERE id = $1 +` + +type UpdateUserBonusParams struct { + ID int64 `json:"id"` + IsClaimed bool `json:"is_claimed"` +} + +func (q *Queries) UpdateUserBonus(ctx context.Context, arg UpdateUserBonusParams) error { + _, err := q.db.Exec(ctx, UpdateUserBonus, arg.ID, arg.IsClaimed) return err } diff --git a/gen/db/models.go b/gen/db/models.go index 53adfa3..c206de5 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -79,12 +79,6 @@ type BetWithOutcome struct { Outcomes []BetOutcome `json:"outcomes"` } -type Bonu struct { - Multiplier float32 `json:"multiplier"` - ID int64 `json:"id"` - BalanceCap int64 `json:"balance_cap"` -} - type Branch struct { ID int64 `json:"id"` Name string `json:"name"` @@ -752,6 +746,19 @@ type User struct { Suspended bool `json:"suspended"` } +type UserBonuse struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + UserID int64 `json:"user_id"` + BonusCode string `json:"bonus_code"` + RewardAmount int64 `json:"reward_amount"` + IsClaimed bool `json:"is_claimed"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` +} + type UserGameInteraction struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` diff --git a/gen/db/transfer.sql.go b/gen/db/transfer.sql.go index 35e38d4..b2a1066 100644 --- a/gen/db/transfer.sql.go +++ b/gen/db/transfer.sql.go @@ -182,6 +182,40 @@ func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber st return i, err } +const GetTransferStats = `-- name: GetTransferStats :one +SELECT COUNT(*) AS total_transfers, COUNT(*) FILTER ( + WHERE type = 'deposit' + ) AS total_deposits, + COUNT(*) FILTER ( + WHERE type = 'withdraw' + ) AS total_withdraw, + COUNT(*) FILTER ( + WHERE type = 'wallet' + ) AS total_wallet_to_wallet +FROM wallet_transfer +WHERE sender_wallet_id = $1 + OR receiver_wallet_id = $1 +` + +type GetTransferStatsRow struct { + TotalTransfers int64 `json:"total_transfers"` + TotalDeposits int64 `json:"total_deposits"` + TotalWithdraw int64 `json:"total_withdraw"` + TotalWalletToWallet int64 `json:"total_wallet_to_wallet"` +} + +func (q *Queries) GetTransferStats(ctx context.Context, senderWalletID pgtype.Int8) (GetTransferStatsRow, error) { + row := q.db.QueryRow(ctx, GetTransferStats, senderWalletID) + var i GetTransferStatsRow + err := row.Scan( + &i.TotalTransfers, + &i.TotalDeposits, + &i.TotalWithdraw, + &i.TotalWalletToWallet, + ) + return i, err +} + const GetTransfersByWallet = `-- name: GetTransfersByWallet :many SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number FROM wallet_transfer_details diff --git a/internal/domain/bonus.go b/internal/domain/bonus.go new file mode 100644 index 0000000..94d88a7 --- /dev/null +++ b/internal/domain/bonus.go @@ -0,0 +1,136 @@ +package domain + +import ( + "time" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/jackc/pgx/v5/pgtype" +) + +type UserBonus struct { + ID int64 + Name string + Description string + UserID int64 + BonusCode string + RewardAmount Currency + IsClaimed bool + ExpiresAt time.Time + CreatedAt time.Time + UpdatedAt time.Time +} + +type UserBonusRes struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + UserID int64 `json:"user_id"` + BonusCode string `json:"bonus_code"` + RewardAmount float32 `json:"reward_amount"` + IsClaimed bool `json:"is_claimed"` + ExpiresAt time.Time `json:"expires_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func ConvertToBonusRes(bonus UserBonus) UserBonusRes { + return UserBonusRes{ + ID: bonus.ID, + Name: bonus.Name, + Description: bonus.Description, + UserID: bonus.UserID, + BonusCode: bonus.BonusCode, + RewardAmount: bonus.RewardAmount.Float32(), + IsClaimed: bonus.IsClaimed, + ExpiresAt: bonus.ExpiresAt, + CreatedAt: bonus.CreatedAt, + UpdatedAt: bonus.UpdatedAt, + } +} + +type CreateBonus struct { + Name string + Description string + UserID int64 + BonusCode string + RewardAmount Currency + ExpiresAt time.Time +} + +type CreateBonusReq struct { + Name string `json:"name"` + Description string `json:"description"` + UserID int64 `json:"user_id"` + BonusCode string `json:"bonus_code"` + RewardAmount float32 `json:"reward_amount"` + ExpiresAt time.Time `json:"expires_at"` +} + +func ConvertCreateBonusReq(bonus CreateBonusReq, companyID int64) CreateBonus { + return CreateBonus{ + Name: bonus.Name, + Description: bonus.Description, + UserID: bonus.UserID, + BonusCode: bonus.BonusCode, + RewardAmount: ToCurrency(bonus.RewardAmount), + ExpiresAt: bonus.ExpiresAt, + } +} + +func ConvertCreateBonus(bonus CreateBonus) dbgen.CreateUserBonusParams { + return dbgen.CreateUserBonusParams{ + Name: bonus.Name, + Description: bonus.Description, + UserID: bonus.UserID, + BonusCode: bonus.BonusCode, + RewardAmount: int64(bonus.RewardAmount), + ExpiresAt: pgtype.Timestamp{ + Time: bonus.ExpiresAt, + Valid: true, + }, + } +} + +func ConvertDBBonus(bonus dbgen.UserBonuse) UserBonus { + return UserBonus{ + ID: bonus.ID, + Name: bonus.Name, + Description: bonus.Description, + UserID: bonus.UserID, + BonusCode: bonus.BonusCode, + RewardAmount: Currency(bonus.RewardAmount), + IsClaimed: bonus.IsClaimed, + ExpiresAt: bonus.ExpiresAt.Time, + CreatedAt: bonus.CreatedAt.Time, + UpdatedAt: bonus.UpdatedAt.Time, + } +} + +func ConvertDBBonuses(bonuses []dbgen.UserBonuse) []UserBonus { + result := make([]UserBonus, len(bonuses)) + for i, bonus := range bonuses { + result[i] = ConvertDBBonus(bonus) + } + return result +} + +type BonusFilter struct { + UserID ValidInt64 + CompanyID ValidInt64 +} + +type BonusStats struct { + TotalBonus int64 + TotalRewardAmount Currency + ClaimedBonuses int64 + ExpiredBonuses int64 +} + +func ConvertDBBonusStats(stats dbgen.GetBonusStatsRow) BonusStats { + return BonusStats{ + TotalBonus: stats.TotalBonuses, + TotalRewardAmount: Currency(stats.TotalRewardEarned), + ClaimedBonuses: stats.ClaimedBonuses, + ExpiredBonuses: stats.ExpiredBonuses, + } +} diff --git a/internal/domain/setting_list.go b/internal/domain/setting_list.go index 96ffe1a..3b376ec 100644 --- a/internal/domain/setting_list.go +++ b/internal/domain/setting_list.go @@ -16,17 +16,26 @@ var ( ) type SettingList struct { - SMSProvider SMSProvider `json:"sms_provider"` - MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"` - BetAmountLimit Currency `json:"bet_amount_limit"` - DailyTicketPerIP int64 `json:"daily_ticket_limit"` - TotalWinningLimit Currency `json:"total_winning_limit"` - AmountForBetReferral Currency `json:"amount_for_bet_referral"` - CashbackAmountCap Currency `json:"cashback_amount_cap"` - DefaultWinningLimit int64 `json:"default_winning_limit"` - ReferralRewardAmount Currency `json:"referral_reward_amount"` - CashbackPercentage float32 `json:"cashback_percentage"` - DefaultMaxReferrals int64 `json:"default_max_referrals"` + SMSProvider SMSProvider `json:"sms_provider"` + MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"` + BetAmountLimit Currency `json:"bet_amount_limit"` + DailyTicketPerIP int64 `json:"daily_ticket_limit"` + TotalWinningLimit Currency `json:"total_winning_limit"` + AmountForBetReferral Currency `json:"amount_for_bet_referral"` + CashbackAmountCap Currency `json:"cashback_amount_cap"` + DefaultWinningLimit int64 `json:"default_winning_limit"` + ReferralRewardAmount Currency `json:"referral_reward_amount"` + CashbackPercentage float32 `json:"cashback_percentage"` + DefaultMaxReferrals int64 `json:"default_max_referrals"` + MinimumBetAmount Currency `json:"minimum_bet_amount"` + BetDuplicateLimit int64 `json:"bet_duplicate_limit"` + SendEmailOnBetFinish bool `json:"send_email_on_bet_finish"` + SendSMSOnBetFinish bool `json:"send_sms_on_bet_finish"` + WelcomeBonusActive bool `json:"welcome_bonus_active"` + WelcomeBonusMultiplier float32 `json:"welcome_bonus_multiplier"` + WelcomeBonusCap Currency `json:"welcome_bonus_cap"` + WelcomeBonusCount int64 `json:"welcome_bonus_count"` + WelcomeBonusExpire int64 `json:"welcome_bonus_expiry"` } type SettingListRes struct { @@ -41,6 +50,10 @@ type SettingListRes struct { ReferralRewardAmount float32 `json:"referral_reward_amount"` CashbackPercentage float32 `json:"cashback_percentage"` DefaultMaxReferrals int64 `json:"default_max_referrals"` + MinimumBetAmount float32 `json:"minimum_bet_amount"` + BetDuplicateLimit int64 `json:"bet_duplicate_limit"` + SendEmailOnBetFinish bool `json:"send_email_on_bet_finish"` + SendSMSOnBetFinish bool `json:"send_sms_on_bet_finish"` } func ConvertSettingListRes(settings SettingList) SettingListRes { @@ -56,6 +69,10 @@ func ConvertSettingListRes(settings SettingList) SettingListRes { ReferralRewardAmount: settings.ReferralRewardAmount.Float32(), CashbackPercentage: settings.CashbackPercentage, DefaultMaxReferrals: settings.DefaultMaxReferrals, + MinimumBetAmount: settings.MinimumBetAmount.Float32(), + BetDuplicateLimit: settings.BetDuplicateLimit, + SendEmailOnBetFinish: settings.SendEmailOnBetFinish, + SendSMSOnBetFinish: settings.SendSMSOnBetFinish, } } @@ -71,6 +88,10 @@ type SaveSettingListReq struct { ReferralRewardAmount *float32 `json:"referral_reward_amount"` CashbackPercentage *float32 `json:"cashback_percentage"` DefaultMaxReferrals *int64 `json:"default_max_referrals"` + MinimumBetAmount *float32 `json:"minimum_bet_amount"` + BetDuplicateLimit *int64 `json:"bet_duplicate_limit"` + SendEmailOnBetFinish *bool `json:"send_email_on_bet_finish"` + SendSMSOnBetFinish *bool `json:"send_sms_on_bet_finish"` } type ValidSettingList struct { @@ -85,6 +106,10 @@ type ValidSettingList struct { ReferralRewardAmount ValidCurrency CashbackPercentage ValidFloat32 DefaultMaxReferrals ValidInt64 + MinimumBetAmount ValidCurrency + BetDuplicateLimit ValidInt64 + SendEmailOnBetFinish ValidBool + SendSMSOnBetFinish ValidBool } func ConvertSaveSettingListReq(settings SaveSettingListReq) ValidSettingList { @@ -100,6 +125,10 @@ func ConvertSaveSettingListReq(settings SaveSettingListReq) ValidSettingList { ReferralRewardAmount: ConvertFloat32PtrToCurrency(settings.ReferralRewardAmount), CashbackPercentage: ConvertFloat32Ptr(settings.CashbackPercentage), DefaultMaxReferrals: ConvertInt64Ptr(settings.DefaultMaxReferrals), + MinimumBetAmount: ConvertFloat32PtrToCurrency(settings.MinimumBetAmount), + BetDuplicateLimit: ConvertInt64Ptr(settings.BetDuplicateLimit), + SendEmailOnBetFinish: ConvertBoolPtr(settings.SendEmailOnBetFinish), + SendSMSOnBetFinish: ConvertBoolPtr(settings.SendSMSOnBetFinish), } } @@ -117,6 +146,10 @@ func (vsl *ValidSettingList) ToSettingList() SettingList { ReferralRewardAmount: vsl.ReferralRewardAmount.Value, CashbackPercentage: vsl.CashbackPercentage.Value, DefaultMaxReferrals: vsl.DefaultMaxReferrals.Value, + MinimumBetAmount: vsl.MinimumBetAmount.Value, + BetDuplicateLimit: vsl.BetDuplicateLimit.Value, + SendEmailOnBetFinish: vsl.SendEmailOnBetFinish.Value, + SendSMSOnBetFinish: vsl.SendSMSOnBetFinish.Value, } } @@ -134,6 +167,7 @@ func (vsl *ValidSettingList) GetInt64SettingsMap() map[string]*ValidInt64 { "daily_ticket_limit": &vsl.DailyTicketPerIP, "default_winning_limit": &vsl.DefaultWinningLimit, "default_max_referrals": &vsl.DefaultMaxReferrals, + "bet_duplicate_limit": &vsl.BetDuplicateLimit, } } @@ -144,6 +178,7 @@ func (vsl *ValidSettingList) GetCurrencySettingsMap() map[string]*ValidCurrency "amount_for_bet_referral": &vsl.AmountForBetReferral, "cashback_amount_cap": &vsl.CashbackAmountCap, "referral_reward_amount": &vsl.ReferralRewardAmount, + "minimum_bet_amount": &vsl.MinimumBetAmount, } } @@ -154,7 +189,10 @@ func (vsl *ValidSettingList) GetStringSettingsMap() map[string]*ValidString { } func (vsl *ValidSettingList) GetBoolSettingsMap() map[string]*ValidBool { - return map[string]*ValidBool{} + return map[string]*ValidBool{ + "send_email_on_bet_finish": &vsl.SendEmailOnBetFinish, + "send_sms_on_bet_finish": &vsl.SendSMSOnBetFinish, + } } func (vsl *ValidSettingList) GetFloat32SettingsMap() map[string]*ValidFloat32 { @@ -167,7 +205,6 @@ func (vsl *ValidSettingList) GetTimeSettingsMap() map[string]*ValidTime { return map[string]*ValidTime{} } - // Setting Functions func (vsl *ValidSettingList) GetTotalSettings() int { diff --git a/internal/domain/transfer.go b/internal/domain/transfer.go index cf629c3..dd32c18 100644 --- a/internal/domain/transfer.go +++ b/internal/domain/transfer.go @@ -105,3 +105,10 @@ type CreateTransfer struct { Status string `json:"status"` CashierID ValidInt64 `json:"cashier_id"` } + +type TransferStats struct { + TotalTransfer int64 + TotalDeposits int64 + TotalWithdraws int64 + TotalWalletToWallet int64 +} diff --git a/internal/repository/bet.go b/internal/repository/bet.go index b1d1e52..09a667b 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -10,6 +10,7 @@ import ( dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" "go.uber.org/zap" ) @@ -220,6 +221,46 @@ func (s *Store) UpdateStatus(ctx context.Context, id int64, status domain.Outcom return err } +func (s *Store) SettleWinningBet(ctx context.Context, betID int64, userID int64, amount domain.Currency, status domain.OutcomeStatus) error { + tx, err := s.conn.BeginTx(ctx, pgx.TxOptions{}) + if err != nil { + return err + } + qtx := s.queries.WithTx(tx) + + wallet, err := qtx.GetCustomerWallet(ctx, userID) + if err != nil { + tx.Rollback(ctx) + return err + } + + // 1. Update wallet + newAmount := wallet.RegularBalance + int64(amount) + if err := qtx.UpdateBalance(ctx, dbgen.UpdateBalanceParams{ + Balance: newAmount, + ID: wallet.RegularID, + }); err != nil { + tx.Rollback(ctx) + return err + } + + // 2. Update bet + if err := qtx.UpdateStatus(ctx, dbgen.UpdateStatusParams{ + Status: int32(status), + ID: betID, + }); err != nil { + tx.Rollback(ctx) + return err + } + + // 3. Commit both together + if err := tx.Commit(ctx); err != nil { + return err + } + + return nil +} + func (s *Store) GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error) { outcomes, err := s.queries.GetBetOutcomeByEventID(ctx, dbgen.GetBetOutcomeByEventIDParams{ diff --git a/internal/repository/bonus.go b/internal/repository/bonus.go index c4f57ac..8f16e04 100644 --- a/internal/repository/bonus.go +++ b/internal/repository/bonus.go @@ -4,27 +4,75 @@ import ( "context" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) -func (s *Store) CreateBonusMultiplier(ctx context.Context, multiplier float32, balance_cap int64) error { - return s.queries.CreateBonusMultiplier(ctx, dbgen.CreateBonusMultiplierParams{ - Multiplier: multiplier, - BalanceCap: balance_cap, +func (s *Store) CreateUserBonus(ctx context.Context, bonus domain.CreateBonus) (domain.UserBonus, error) { + newBonus, err := s.queries.CreateUserBonus(ctx, domain.ConvertCreateBonus(bonus)) + + if err != nil { + return domain.UserBonus{}, err + } + + return domain.ConvertDBBonus(newBonus), nil +} + +func (s *Store) GetAllUserBonuses(ctx context.Context) ([]domain.UserBonus, error) { + bonuses, err := s.queries.GetAllUserBonuses(ctx) + + if err != nil { + return nil, err + } + + return domain.ConvertDBBonuses(bonuses), nil +} + +func (s *Store) GetBonusesByUserID(ctx context.Context, userID int64) ([]domain.UserBonus, error) { + bonuses, err := s.queries.GetBonusesByUserID(ctx, userID) + if err != nil { + return nil, err + } + + return domain.ConvertDBBonuses(bonuses), nil +} + +func (s *Store) GetBonusByID(ctx context.Context, bonusID int64) (domain.UserBonus, error) { + bonus, err := s.queries.GetUserBonusByID(ctx, bonusID) + if err != nil { + return domain.UserBonus{}, err + } + return domain.ConvertDBBonus(bonus), nil +} + + +func (s *Store) GetBonusStats(ctx context.Context, filter domain.BonusFilter) (domain.BonusStats, error) { + bonus, err := s.queries.GetBonusStats(ctx, dbgen.GetBonusStatsParams{ + CompanyID: filter.CompanyID.ToPG(), + UserID: filter.UserID.ToPG(), }) + if err != nil { + return domain.BonusStats{}, err + } + return domain.ConvertDBBonusStats(bonus), nil } -func (s *Store) GetBonusMultiplier(ctx context.Context) ([]dbgen.GetBonusMultiplierRow, error) { - return s.queries.GetBonusMultiplier(ctx) -} - -func (s *Store) GetBonusBalanceCap(ctx context.Context) ([]dbgen.GetBonusBalanceCapRow, error) { - return s.queries.GetBonusBalanceCap(ctx) -} - -func (s *Store) UpdateBonusMultiplier(ctx context.Context, id int64, mulitplier float32, balance_cap int64) error { - return s.queries.UpdateBonusMultiplier(ctx, dbgen.UpdateBonusMultiplierParams{ - ID: id, - Multiplier: mulitplier, - BalanceCap: balance_cap, +func (s *Store) UpdateUserBonus(ctx context.Context, bonusID int64, IsClaimed bool) (error) { + err := s.queries.UpdateUserBonus(ctx, dbgen.UpdateUserBonusParams{ + ID: bonusID, + IsClaimed: IsClaimed, }) + + if err != nil { + return err + } + return nil +} + + +func (s *Store) DeleteUserBonus(ctx context.Context, bonusID int64) (error) { + err := s.queries.DeleteUserBonus(ctx, bonusID) + if err != nil { + return err + } + return nil } diff --git a/internal/repository/transfer.go b/internal/repository/transfer.go index cad330e..3b5e5a9 100644 --- a/internal/repository/transfer.go +++ b/internal/repository/transfer.go @@ -148,6 +148,24 @@ func (s *Store) GetTransferByID(ctx context.Context, id int64) (domain.TransferD return convertDBTransferDetail(transfer), nil } +func (s *Store) GetTransferStats(ctx context.Context, walletID int64) (domain.TransferStats, error) { + stats, err := s.queries.GetTransferStats(ctx, pgtype.Int8{ + Int64: walletID, + Valid: true, + }) + + if err != nil { + return domain.TransferStats{}, err + } + + return domain.TransferStats{ + TotalTransfer: stats.TotalTransfers, + TotalDeposits: stats.TotalDeposits, + TotalWithdraws: stats.TotalWithdraw, + TotalWalletToWallet: stats.TotalWalletToWallet, + }, nil +} + func (s *Store) UpdateTransferVerification(ctx context.Context, id int64, verified bool) error { err := s.queries.UpdateTransferVerification(ctx, dbgen.UpdateTransferVerificationParams{ ID: id, diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index b2ef38f..65e361f 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -31,20 +31,20 @@ import ( ) var ( - ErrNoEventsAvailable = errors.New("Not enough events available with the given filters") - ErrGenerateRandomOutcome = errors.New("Failed to generate any random outcome for events") - ErrOutcomesNotCompleted = errors.New("Some bet outcomes are still pending") - ErrEventHasBeenRemoved = errors.New("Event has been removed") + ErrNoEventsAvailable = errors.New("not enough events available with the given filters") + ErrGenerateRandomOutcome = errors.New("failed to generate any random outcome for events") + ErrOutcomesNotCompleted = errors.New("some bet outcomes are still pending") + ErrEventHasBeenRemoved = errors.New("event has been removed") - ErrEventHasNotEnded = errors.New("Event has not ended yet") - ErrRawOddInvalid = errors.New("Prematch Raw Odd is Invalid") - ErrBranchIDRequired = errors.New("Branch ID required for this role") - ErrOutcomeLimit = errors.New("Too many outcomes on a single bet") - ErrTotalBalanceNotEnough = errors.New("Total Wallet balance is insufficient to create bet") + ErrEventHasNotEnded = errors.New("event has not ended yet") + ErrRawOddInvalid = errors.New("prematch Raw Odd is Invalid") + ErrBranchIDRequired = errors.New("branch ID required for this role") + ErrOutcomeLimit = errors.New("too many outcomes on a single bet") + ErrTotalBalanceNotEnough = errors.New("total Wallet balance is insufficient to create bet") - ErrInvalidAmount = errors.New("Invalid amount") - ErrBetAmountTooHigh = errors.New("Cannot create a bet with an amount above limit") - ErrBetWinningTooHigh = errors.New("Total Winnings over set limit") + ErrInvalidAmount = errors.New("invalid amount") + ErrBetAmountTooHigh = errors.New("cannot create a bet with an amount above limit") + ErrBetWinningTooHigh = errors.New("total Winnings over set limit") ) type Service struct { @@ -221,7 +221,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID if err != nil { return domain.CreateBetRes{}, err } - if req.Amount < 1 { + if req.Amount < settingsList.MinimumBetAmount.Float32() { return domain.CreateBetRes{}, ErrInvalidAmount } @@ -284,9 +284,8 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID return domain.CreateBetRes{}, err } - // TODO: Make this a setting - if role == domain.RoleCustomer && count >= 10 { - return domain.CreateBetRes{}, fmt.Errorf("max user limit for single outcome") + if role == domain.RoleCustomer && count >= settingsList.BetDuplicateLimit { + return domain.CreateBetRes{}, fmt.Errorf("max user limit for duplicate bet") } fastCode := helpers.GenerateFastCode() @@ -387,7 +386,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID zap.String("role", string(role)), zap.Int64("user_id", userID), ) - return domain.CreateBetRes{}, fmt.Errorf("Unknown Role Type") + return domain.CreateBetRes{}, fmt.Errorf("unknown role type") } bet, err := s.CreateBet(ctx, newBet) @@ -588,25 +587,21 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string, var newOdds []domain.CreateBetOutcome var totalOdds float32 = 1 + eventLogger := s.mongoLogger.With( + zap.String("eventID", eventID), + zap.Int32("sportID", sportID), + zap.String("homeTeam", HomeTeam), + zap.String("awayTeam", AwayTeam), + ) markets, err := s.prematchSvc.GetOddsByEventID(ctx, eventID, domain.OddMarketWithEventFilter{}) + if err != nil { - s.logger.Error("failed to get odds for event", "event id", eventID, "error", err) - s.mongoLogger.Error("failed to get odds for event", - zap.String("eventID", eventID), - zap.Int32("sportID", sportID), - zap.String("homeTeam", HomeTeam), - zap.String("awayTeam", AwayTeam), - zap.Error(err)) + eventLogger.Error("failed to get odds for event", zap.Error(err)) return nil, 0, err } if len(markets) == 0 { - s.logger.Error("empty odds for event", "event id", eventID) - s.mongoLogger.Warn("empty odds for event", - zap.String("eventID", eventID), - zap.Int32("sportID", sportID), - zap.String("homeTeam", HomeTeam), - zap.String("awayTeam", AwayTeam)) + eventLogger.Warn("empty odds for event") return nil, 0, fmt.Errorf("empty odds or event %v", eventID) } @@ -635,19 +630,13 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string, err = json.Unmarshal(rawBytes, &selectedOdd) if err != nil { - s.logger.Error("Failed to unmarshal raw odd", "error", err) - s.mongoLogger.Warn("Failed to unmarshal raw odd", - zap.String("eventID", eventID), - zap.Int32("sportID", sportID), - zap.Error(err)) + eventLogger.Warn("Failed to unmarshal raw odd", zap.Error(err)) continue } parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32) if err != nil { - s.logger.Error("Failed to parse odd", "error", err) - s.mongoLogger.Warn("Failed to parse odd", - zap.String("eventID", eventID), + eventLogger.Warn("Failed to parse odd", zap.String("oddValue", selectedOdd.Odds), zap.Error(err)) continue @@ -655,17 +644,13 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string, eventIDInt, err := strconv.ParseInt(eventID, 10, 64) if err != nil { - s.logger.Error("Failed to parse eventID", "error", err) - s.mongoLogger.Warn("Failed to parse eventID", - zap.String("eventID", eventID), - zap.Error(err)) + eventLogger.Warn("Failed to parse eventID", zap.Error(err)) continue } oddID, err := strconv.ParseInt(selectedOdd.ID, 10, 64) if err != nil { - s.logger.Error("Failed to parse oddID", "error", err) - s.mongoLogger.Warn("Failed to parse oddID", + eventLogger.Warn("Failed to parse oddID", zap.String("oddID", selectedOdd.ID), zap.Error(err)) continue @@ -673,8 +658,7 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string, marketID, err := strconv.ParseInt(market.MarketID, 10, 64) if err != nil { - s.logger.Error("Failed to parse marketID", "error", err) - s.mongoLogger.Warn("Failed to parse marketID", + eventLogger.Warn("Failed to parse marketID", zap.String("marketID", market.MarketID), zap.Error(err)) continue @@ -701,22 +685,12 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string, } if len(newOdds) == 0 { - s.logger.Error("Bet Outcomes is empty for market", "selectedMarkets", len(selectedMarkets)) - s.mongoLogger.Error("Bet Outcomes is empty for market", - zap.String("eventID", eventID), - zap.Int32("sportID", sportID), - zap.String("homeTeam", HomeTeam), - zap.String("awayTeam", AwayTeam), - zap.Int("selectedMarkets", len(selectedMarkets))) + eventLogger.Error("Bet Outcomes is empty for market", zap.Int("selectedMarkets", len(selectedMarkets))) return nil, 0, ErrGenerateRandomOutcome } // ✅ Final success log (optional) - s.mongoLogger.Info("Random bet outcomes generated successfully", - zap.String("eventID", eventID), - zap.Int32("sportID", sportID), - zap.Int("numOutcomes", len(newOdds)), - zap.Float32("totalOdds", totalOdds)) + eventLogger.Info("Random bet outcomes generated successfully", zap.Int("numOutcomes", len(newOdds)), zap.Float32("totalOdds", totalOdds)) return newOdds, totalOdds, nil } @@ -724,7 +698,15 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string, func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID, companyID int64, leagueID domain.ValidInt64, sportID domain.ValidInt32, firstStartTime, lastStartTime domain.ValidTime) (domain.CreateBetRes, error) { // Get a unexpired event id - + randomBetLogger := s.mongoLogger.With( + zap.Int64("userID", userID), + zap.Int64("branchID", branchID), + zap.Int64("companyID", companyID), + zap.Any("leagueID", leagueID), + zap.Any("sportID", sportID), + zap.Any("firstStartTime", firstStartTime), + zap.Any("lastStartTime", lastStartTime), + ) events, _, err := s.eventSvc.GetPaginatedUpcomingEvents(ctx, domain.EventFilter{ SportID: sportID, @@ -734,17 +716,12 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID, companyI }) if err != nil { - s.mongoLogger.Error("failed to get paginated upcoming events", - zap.Int64("userID", userID), - zap.Int64("branchID", branchID), - zap.Error(err)) + randomBetLogger.Error("failed to get paginated upcoming events", zap.Error(err)) return domain.CreateBetRes{}, err } if len(events) == 0 { - s.mongoLogger.Warn("no events available for random bet", - zap.Int64("userID", userID), - zap.Int64("branchID", branchID)) + randomBetLogger.Warn("no events available for random bet") return domain.CreateBetRes{}, ErrNoEventsAvailable } @@ -770,12 +747,7 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID, companyI newOdds, total, err := s.GenerateRandomBetOutcomes(ctx, event.ID, event.SportID, event.HomeTeam, event.AwayTeam, event.StartTime, numMarketsPerBet) if err != nil { - s.logger.Error("failed to generate random bet outcome", "event id", event.ID, "error", err) - s.mongoLogger.Error("failed to generate random bet outcome", - zap.Int64("userID", userID), - zap.Int64("branchID", branchID), - zap.String("eventID", event.ID), - zap.String("error", fmt.Sprintf("%v", err))) + s.mongoLogger.Error("failed to generate random bet outcome", zap.String("eventID", event.ID), zap.Error(err)) continue } @@ -784,10 +756,7 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID, companyI } if len(randomOdds) == 0 { - s.logger.Error("Failed to generate random any outcomes for all events") - s.mongoLogger.Error("Failed to generate random any outcomes for all events", - zap.Int64("userID", userID), - zap.Int64("branchID", branchID)) + randomBetLogger.Error("Failed to generate random any outcomes for all events") return domain.CreateBetRes{}, ErrGenerateRandomOutcome } @@ -795,20 +764,13 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID, companyI outcomesHash, err := generateOutcomeHash(randomOdds) if err != nil { - s.mongoLogger.Error("failed to generate outcome hash", - zap.Int64("user_id", userID), - zap.Error(err), - ) + randomBetLogger.Error("failed to generate outcome hash", zap.Error(err)) return domain.CreateBetRes{}, err } count, err := s.GetBetCountByUserID(ctx, userID, outcomesHash) if err != nil { - s.mongoLogger.Error("failed to get bet count", - zap.Int64("user_id", userID), - zap.String("outcome_hash", outcomesHash), - zap.Error(err), - ) + randomBetLogger.Error("failed to get bet count", zap.String("outcome_hash", outcomesHash), zap.Error(err)) return domain.CreateBetRes{}, err } @@ -830,10 +792,7 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID, companyI bet, err := s.CreateBet(ctx, newBet) if err != nil { - s.mongoLogger.Error("Failed to create a new random bet", - zap.Int64("userID", userID), - zap.Int64("branchID", branchID), - zap.String("bet", fmt.Sprintf("%+v", newBet))) + randomBetLogger.Error("Failed to create a new random bet", zap.Error(err)) return domain.CreateBetRes{}, err } @@ -843,19 +802,13 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID, companyI rows, err := s.betStore.CreateBetOutcome(ctx, randomOdds) if err != nil { - s.mongoLogger.Error("Failed to create a new random bet outcome", - zap.Int64("userID", userID), - zap.Int64("branchID", branchID), - zap.String("randomOdds", fmt.Sprintf("%+v", randomOdds))) + randomBetLogger.Error("Failed to create a new random bet outcome", zap.Any("randomOdds", randomOdds)) return domain.CreateBetRes{}, err } res := domain.ConvertCreateBetRes(bet, rows) - s.mongoLogger.Info("Random bets placed successfully", - zap.Int64("userID", userID), - zap.Int64("branchID", branchID), - zap.String("response", fmt.Sprintf("%+v", res))) + randomBetLogger.Info("Random bets placed successfully") return res, nil } @@ -902,53 +855,73 @@ func (s *Service) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) e return s.betStore.UpdateCashOut(ctx, id, cashedOut) } -func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error { - bet, err := s.GetBetByID(ctx, id) +func (s *Service) UpdateStatus(ctx context.Context, betId int64, status domain.OutcomeStatus) error { + + updateLogger := s.mongoLogger.With( + zap.Int64("bet_id", betId), + zap.String("status", status.String()), + ) + bet, err := s.GetBetByID(ctx, betId) if err != nil { - s.mongoLogger.Error("failed to update bet status: invalid bet ID", - zap.Int64("bet_id", id), - zap.Error(err), - ) + updateLogger.Error("failed to update bet status: invalid bet ID", zap.Error(err)) + return err + } + + settingsList, err := s.settingSvc.GetOverrideSettingsList(ctx, bet.CompanyID) + if err != nil { + updateLogger.Error("failed to get settings", zap.Error(err)) return err } if status == domain.OUTCOME_STATUS_ERROR || status == domain.OUTCOME_STATUS_PENDING { - s.SendAdminErrorAlertNotification(ctx, status, "") - s.SendErrorStatusNotification(ctx, status, bet.UserID, "") - s.mongoLogger.Error("Bet Status is error", - zap.Int64("bet_id", id), - zap.Error(err), - ) - return s.betStore.UpdateStatus(ctx, id, status) + if err := s.SendAdminAlertNotification(ctx, betId, status, "", bet.CompanyID); err != nil { + updateLogger.Error("failed to send admin notification", zap.Error(err)) + return err + } + + if err := s.SendErrorStatusNotification(ctx, betId, status, bet.UserID, ""); err != nil { + updateLogger.Error("failed to send error notification to user", zap.Error(err)) + return err + } + updateLogger.Error("bet entered error/pending state") + return s.betStore.UpdateStatus(ctx, betId, status) } if bet.IsShopBet { - return s.betStore.UpdateStatus(ctx, id, status) + return s.betStore.UpdateStatus(ctx, betId, status) } - customerWallet, err := s.walletSvc.GetCustomerWallet(ctx, id) + // After this point the bet is known to be a online customer bet + + customerWallet, err := s.walletSvc.GetCustomerWallet(ctx, bet.UserID) if err != nil { - s.mongoLogger.Error("failed to get customer wallet", - zap.Int64("bet_id", id), - zap.Error(err), - ) + updateLogger.Error("failed to get customer wallet", zap.Error(err)) return err } + resultNotification := SendResultNotificationParam{ + BetID: betId, + Status: status, + UserID: bet.UserID, + SendEmail: settingsList.SendEmailOnBetFinish, + SendSMS: settingsList.SendSMSOnBetFinish, + } var amount domain.Currency switch status { case domain.OUTCOME_STATUS_LOSS: - s.SendLosingStatusNotification(ctx, status, bet.UserID, "") - return s.betStore.UpdateStatus(ctx, id, status) + err := s.SendLosingStatusNotification(ctx, resultNotification) + if err != nil { + updateLogger.Error("failed to send notification", zap.Error(err)) + return err + } + return s.betStore.UpdateStatus(ctx, betId, status) case domain.OUTCOME_STATUS_WIN: amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds) - s.SendWinningStatusNotification(ctx, status, bet.UserID, amount, "") case domain.OUTCOME_STATUS_HALF: amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds) / 2 - s.SendWinningStatusNotification(ctx, status, bet.UserID, amount, "") case domain.OUTCOME_STATUS_VOID: amount = bet.Amount - s.SendWinningStatusNotification(ctx, status, bet.UserID, amount, "") default: + updateLogger.Error("invalid outcome status") return fmt.Errorf("invalid outcome status") } @@ -956,7 +929,7 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc domain.TRANSFER_DIRECT, domain.PaymentDetails{}, fmt.Sprintf("Added %v to wallet by system for winning a bet", amount.Float32())) if err != nil { - s.mongoLogger.Error("failed to add winnings to wallet", + updateLogger.Error("failed to add winnings to wallet", zap.Int64("wallet_id", customerWallet.RegularID), zap.Float32("amount", float32(amount)), zap.Error(err), @@ -964,179 +937,211 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc return err } - return s.betStore.UpdateStatus(ctx, id, status) + if err := s.betStore.UpdateStatus(ctx, betId, status); err != nil { + updateLogger.Error("failed to update bet status", + zap.String("status", status.String()), + zap.Error(err), + ) + return err + } + + resultNotification.WinningAmount = amount + if err := s.SendWinningStatusNotification(ctx, resultNotification); err != nil { + + updateLogger.Error("failed to send winning notification", + zap.Error(err), + ) + return err + } + + return nil } -func (s *Service) SendWinningStatusNotification(ctx context.Context, status domain.OutcomeStatus, userID int64, winningAmount domain.Currency, extra string) error { +func newBetResultNotification(userID int64, level domain.NotificationLevel, channel domain.DeliveryChannel, headline, message string, metadata any) *domain.Notification { + raw, _ := json.Marshal(metadata) + return &domain.Notification{ + RecipientID: userID, + DeliveryStatus: domain.DeliveryStatusPending, + IsRead: false, + Type: domain.NOTIFICATION_TYPE_BET_RESULT, + Level: level, + Reciever: domain.NotificationRecieverSideCustomer, + DeliveryChannel: channel, + Payload: domain.NotificationPayload{ + Headline: headline, + Message: message, + }, + Priority: 2, + Metadata: raw, + } +} + +type SendResultNotificationParam struct { + BetID int64 + Status domain.OutcomeStatus + UserID int64 + WinningAmount domain.Currency + Extra string + SendEmail bool + SendSMS bool +} + +func (p SendResultNotificationParam) Validate() error { + if p.BetID == 0 { + return errors.New("BetID is required") + } + if p.UserID == 0 { + return errors.New("UserID is required") + } + return nil +} + +func shouldSend(channel domain.DeliveryChannel, sendEmail, sendSMS bool) bool { + switch { + case channel == domain.DeliveryChannelEmail && sendEmail: + return true + case channel == domain.DeliveryChannelSMS && sendSMS: + return true + case channel == domain.DeliveryChannelInApp: + return true + default: + return false + } +} + +func (s *Service) SendWinningStatusNotification(ctx context.Context, param SendResultNotificationParam) error { + if err := param.Validate(); err != nil { + return err + } var headline string var message string - switch status { + switch param.Status { case domain.OUTCOME_STATUS_WIN: - headline = "You Bet Has Won!" + headline = fmt.Sprintf("Bet #%v Won!", param.BetID) message = fmt.Sprintf( - "You have been awarded %.2f", - winningAmount.Float32(), + "Congratulations! Your bet #%v has won. %.2f has been credited to your wallet.", + param.BetID, + param.WinningAmount.Float32(), ) case domain.OUTCOME_STATUS_HALF: - headline = "You have a half win" + headline = fmt.Sprintf("Bet #%v Half-Win", param.BetID) message = fmt.Sprintf( - "You have been awarded %.2f", - winningAmount.Float32(), + "Your bet #%v resulted in a half-win. %.2f has been credited to your wallet.", + param.BetID, + param.WinningAmount.Float32(), ) case domain.OUTCOME_STATUS_VOID: - headline = "Your bet has been refunded" + headline = fmt.Sprintf("Bet #%v Refunded", param.BetID) message = fmt.Sprintf( - "You have been awarded %.2f", - winningAmount.Float32(), + "Your bet #%v has been voided. %.2f has been refunded to your wallet.", + param.BetID, + param.WinningAmount.Float32(), ) + + default: + return fmt.Errorf("unsupported status: %v", param.Status) } - betNotification := &domain.Notification{ - RecipientID: userID, - DeliveryStatus: domain.DeliveryStatusPending, - IsRead: false, - Type: domain.NOTIFICATION_TYPE_BET_RESULT, - Level: domain.NotificationLevelSuccess, - Reciever: domain.NotificationRecieverSideCustomer, - DeliveryChannel: domain.DeliveryChannelInApp, - Payload: domain.NotificationPayload{ - Headline: headline, - Message: message, - }, - Priority: 2, - Metadata: fmt.Appendf(nil, `{ - "winning_amount":%.2f, - "status":%v - "more": %v - }`, winningAmount.Float32(), status, extra), - } - - if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil { - return err - } - - betNotification.DeliveryChannel = domain.DeliveryChannelEmail - if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil { - return err + for _, channel := range []domain.DeliveryChannel{ + domain.DeliveryChannelInApp, + domain.DeliveryChannelEmail, + domain.DeliveryChannelSMS, + } { + if !shouldSend(channel, param.SendEmail, param.SendSMS) { + continue + } + n := newBetResultNotification(param.UserID, domain.NotificationLevelSuccess, channel, headline, message, map[string]any{ + "winning_amount": param.WinningAmount.Float32(), + "status": param.Status, + "more": param.Extra, + }) + if err := s.notificationSvc.SendNotification(ctx, n); err != nil { + return err + } } return nil } -func (s *Service) SendLosingStatusNotification(ctx context.Context, status domain.OutcomeStatus, userID int64, extra string) error { +func (s *Service) SendLosingStatusNotification(ctx context.Context, param SendResultNotificationParam) error { + if err := param.Validate(); err != nil { + return err + } var headline string var message string - switch status { + switch param.Status { case domain.OUTCOME_STATUS_LOSS: - headline = "Your bet has lost" - message = "Better luck next time" + headline = fmt.Sprintf("Bet #%v Lost", param.BetID) + message = "Unfortunately, your bet did not win this time. Better luck next time!" + default: + return fmt.Errorf("unsupported status: %v", param.Status) } - betNotification := &domain.Notification{ - RecipientID: userID, - DeliveryStatus: domain.DeliveryStatusPending, - IsRead: false, - Type: domain.NOTIFICATION_TYPE_BET_RESULT, - Level: domain.NotificationLevelSuccess, - Reciever: domain.NotificationRecieverSideCustomer, - DeliveryChannel: domain.DeliveryChannelInApp, - Payload: domain.NotificationPayload{ - Headline: headline, - Message: message, - }, - Priority: 2, - Metadata: fmt.Appendf(nil, `{ - "status":%v - "more": %v - }`, status, extra), - } - - if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil { - return err - } - - betNotification.DeliveryChannel = domain.DeliveryChannelEmail - if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil { - return err + for _, channel := range []domain.DeliveryChannel{ + domain.DeliveryChannelInApp, + domain.DeliveryChannelEmail, + domain.DeliveryChannelSMS, + } { + if !shouldSend(channel, param.SendEmail, param.SendSMS) { + continue + } + n := newBetResultNotification(param.UserID, domain.NotificationLevelWarning, channel, headline, message, map[string]any{ + "status": param.Status, + "more": param.Extra, + }) + if err := s.notificationSvc.SendNotification(ctx, n); err != nil { + return err + } } return nil } -func (s *Service) SendErrorStatusNotification(ctx context.Context, status domain.OutcomeStatus, userID int64, extra string) error { +func (s *Service) SendErrorStatusNotification(ctx context.Context, betID int64, status domain.OutcomeStatus, userID int64, extra string) error { var headline string var message string switch status { case domain.OUTCOME_STATUS_ERROR, domain.OUTCOME_STATUS_PENDING: - headline = "There was an error with your bet" - message = "We have encounter an error with your bet. We will fix it as soon as we can" + headline = fmt.Sprintf("Bet #%v Processing Issue", betID) + message = "We encountered a problem while processing your bet. Our team is working to resolve it as soon as possible." + + default: + return fmt.Errorf("unsupported status: %v", status) } - betNotification := &domain.Notification{ - RecipientID: userID, - DeliveryStatus: domain.DeliveryStatusPending, - IsRead: false, - Type: domain.NOTIFICATION_TYPE_BET_RESULT, - Level: domain.NotificationLevelSuccess, - Reciever: domain.NotificationRecieverSideCustomer, - DeliveryChannel: domain.DeliveryChannelInApp, - Payload: domain.NotificationPayload{ - Headline: headline, - Message: message, - }, - Priority: 1, - ErrorSeverity: domain.NotificationErrorSeverityHigh, - Metadata: fmt.Appendf(nil, `{ - "status":%v - "more": %v - }`, status, extra), - } - - if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil { - return err - } - - betNotification.DeliveryChannel = domain.DeliveryChannelEmail - if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil { - return err + for _, channel := range []domain.DeliveryChannel{ + domain.DeliveryChannelInApp, + domain.DeliveryChannelEmail, + } { + n := newBetResultNotification(userID, domain.NotificationLevelError, channel, headline, message, map[string]any{ + "status": status, + "more": extra, + }) + if err := s.notificationSvc.SendNotification(ctx, n); err != nil { + return err + } } return nil } -func (s *Service) SendAdminErrorAlertNotification(ctx context.Context, status domain.OutcomeStatus, extra string) error { +func (s *Service) SendAdminAlertNotification(ctx context.Context, betID int64, status domain.OutcomeStatus, extra string, companyID int64) error { var headline string var message string switch status { case domain.OUTCOME_STATUS_ERROR, domain.OUTCOME_STATUS_PENDING: - headline = "There was an error processing bet" - message = "We have encounter an error with bet. We will fix it as soon as we can" - } + headline = fmt.Sprintf("Processing Error for Bet #%v", betID) + message = "A processing error occurred with this bet. Please review and take corrective action." - betNotification := &domain.Notification{ - ErrorSeverity: domain.NotificationErrorSeverityHigh, - DeliveryStatus: domain.DeliveryStatusPending, - IsRead: false, - Type: domain.NOTIFICATION_TYPE_BET_RESULT, - Level: domain.NotificationLevelSuccess, - Reciever: domain.NotificationRecieverSideCustomer, - DeliveryChannel: domain.DeliveryChannelEmail, - Payload: domain.NotificationPayload{ - Headline: headline, - Message: message, - }, - Priority: 2, - Metadata: fmt.Appendf(nil, `{ - "status":%v - "more": %v - }`, status, extra), + default: + return fmt.Errorf("unsupported status: %v", status) } super_admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{ @@ -1153,6 +1158,10 @@ func (s *Service) SendAdminErrorAlertNotification(ctx context.Context, status do admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{ Role: string(domain.RoleAdmin), + CompanyID: domain.ValidInt64{ + Value: companyID, + Valid: true, + }, }) if err != nil { @@ -1166,23 +1175,17 @@ func (s *Service) SendAdminErrorAlertNotification(ctx context.Context, status do users := append(super_admin_users, admin_users...) for _, user := range users { - betNotification.RecipientID = user.ID - if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil { - s.mongoLogger.Error("failed to send admin notification", - zap.Int64("admin_id", user.ID), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return err - } - betNotification.DeliveryChannel = domain.DeliveryChannelEmail - if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil { - s.mongoLogger.Error("failed to send email admin notification", - zap.Int64("admin_id", user.ID), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return err + for _, channel := range []domain.DeliveryChannel{ + domain.DeliveryChannelInApp, + domain.DeliveryChannelEmail, + } { + n := newBetResultNotification(user.ID, domain.NotificationLevelError, channel, headline, message, map[string]any{ + "status": status, + "more": extra, + }) + if err := s.notificationSvc.SendNotification(ctx, n); err != nil { + return err + } } } @@ -1366,6 +1369,14 @@ func (s *Service) ProcessBetCashback(ctx context.Context) error { } settingsList, err := s.settingSvc.GetOverrideSettingsList(ctx, bet.CompanyID) + if err != nil { + s.mongoLogger.Error("Failed to get settings", + zap.Int64("userID", bet.UserID), + zap.Error(err)) + + return err + } + cashbackAmount := math.Min(float64(settingsList.CashbackAmountCap.Float32()), float64(calculateCashbackAmount(bet.Amount.Float32(), bet.TotalOdds))) _, err = s.walletSvc.AddToWallet(ctx, wallets.StaticID, domain.ToCurrency(float32(cashbackAmount)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, diff --git a/internal/services/bonus/port.go b/internal/services/bonus/port.go index 2147b51..3dafb67 100644 --- a/internal/services/bonus/port.go +++ b/internal/services/bonus/port.go @@ -3,12 +3,15 @@ package bonus import ( "context" - dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) type BonusStore interface { - CreateBonusMultiplier(ctx context.Context, multiplier float32, balance_cap int64) error - GetBonusMultiplier(ctx context.Context) ([]dbgen.GetBonusMultiplierRow, error) - GetBonusBalanceCap(ctx context.Context) ([]dbgen.GetBonusBalanceCapRow, error) - UpdateBonusMultiplier(ctx context.Context, id int64, mulitplier float32, balance_cap int64) error + CreateUserBonus(ctx context.Context, bonus domain.CreateBonus) (domain.UserBonus, error) + GetAllUserBonuses(ctx context.Context) ([]domain.UserBonus, error) + GetBonusesByUserID(ctx context.Context, userID int64) ([]domain.UserBonus, error) + GetBonusByID(ctx context.Context, bonusID int64) (domain.UserBonus, error) + GetBonusStats(ctx context.Context, filter domain.BonusFilter) (domain.BonusStats, error) + UpdateUserBonus(ctx context.Context, bonusID int64, IsClaimed bool) error + DeleteUserBonus(ctx context.Context, bonusID int64) error } diff --git a/internal/services/bonus/service.go b/internal/services/bonus/service.go index 51e008a..089c1c7 100644 --- a/internal/services/bonus/service.go +++ b/internal/services/bonus/service.go @@ -2,32 +2,112 @@ package bonus import ( "context" + "errors" + "math" + "time" - dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" + "go.uber.org/zap" ) type Service struct { - bonusStore BonusStore + bonusStore BonusStore + walletSvc *wallet.Service + settingSvc *settings.Service + mongoLogger *zap.Logger } -func NewService(bonusStore BonusStore) *Service { +func NewService(bonusStore BonusStore, walletSvc *wallet.Service, settingSvc *settings.Service, mongoLogger *zap.Logger) *Service { return &Service{ - bonusStore: bonusStore, + bonusStore: bonusStore, + walletSvc: walletSvc, + settingSvc: settingSvc, + mongoLogger: mongoLogger, } } -func (s *Service) CreateBonusMultiplier(ctx context.Context, multiplier float32, balance_cap int64) error { - return s.bonusStore.CreateBonusMultiplier(ctx, multiplier, balance_cap) +var ( + ErrWelcomeBonusNotActive = errors.New("welcome bonus is not active") + ErrWelcomeBonusCountReached = errors.New("welcome bonus max deposit count reached") +) + +func (s *Service) ProcessWelcomeBonus(ctx context.Context, amount domain.Currency, companyID int64, userID int64) error { + settingsList, err := s.settingSvc.GetOverrideSettingsList(ctx, companyID) + if err != nil { + s.mongoLogger.Error("Failed to get settings", + zap.Int64("companyID", companyID), + zap.Error(err)) + + return err + } + + if !settingsList.WelcomeBonusActive { + return ErrWelcomeBonusNotActive + } + + wallet, err := s.walletSvc.GetCustomerWallet(ctx, userID) + if err != nil { + return err + } + + stats, err := s.walletSvc.GetTransferStats(ctx, wallet.ID) + + if err != nil { + return err + } + + if stats.TotalDeposits > settingsList.WelcomeBonusCount { + return ErrWelcomeBonusCountReached + } + + newBalance := math.Min(float64(amount)*float64(settingsList.WelcomeBonusMultiplier), float64(settingsList.WelcomeBonusCap)) + + _, err = s.CreateUserBonus(ctx, domain.CreateBonus{ + Name: "Welcome Bonus", + Description: "Awarded when the user logged in for the first time", + UserID: userID, + BonusCode: helpers.GenerateFastCode(), + RewardAmount: domain.Currency(newBalance), + ExpiresAt: time.Now().Add(time.Duration(settingsList.WelcomeBonusExpire) * 24 * time.Hour), + }) + + if err != nil { + return err + } + + // TODO: Add a claim function that adds to the static wallet when the user inputs his bonus code + // _, err = s.walletSvc.AddToWallet(ctx, wallet.StaticID, domain.ToCurrency(float32(newBalance)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, + // fmt.Sprintf("Added %v to static wallet because of deposit bonus using multiplier %v", newBalance, settingsList.WelcomeBonusMultiplier), + // ) + // if err != nil { + // return err + // } + + return nil + } -func (s *Service) GetBonusMultiplier(ctx context.Context) ([]dbgen.GetBonusMultiplierRow, error) { - return s.bonusStore.GetBonusMultiplier(ctx) +func (s *Service) CreateUserBonus(ctx context.Context, bonus domain.CreateBonus) (domain.UserBonus, error) { + return s.bonusStore.CreateUserBonus(ctx, bonus) } - -func (s *Service) GetBonusBalanceCap(ctx context.Context) ([]dbgen.GetBonusBalanceCapRow, error) { - return s.bonusStore.GetBonusBalanceCap(ctx) +func (s *Service) GetAllUserBonuses(ctx context.Context) ([]domain.UserBonus, error) { + return s.bonusStore.GetAllUserBonuses(ctx) } - -func (s *Service) UpdateBonusMultiplier(ctx context.Context, id int64, mulitplier float32, balance_cap int64) error { - return s.bonusStore.UpdateBonusMultiplier(ctx, id, mulitplier, balance_cap) +func (s *Service) GetBonusesByUserID(ctx context.Context, userID int64) ([]domain.UserBonus, error) { + return s.bonusStore.GetBonusesByUserID(ctx, userID) +} +func (s *Service) GetBonusByID(ctx context.Context, bonusID int64) (domain.UserBonus, error) { + return s.bonusStore.GetBonusByID(ctx, bonusID) +} +func (s *Service) GetBonusStats(ctx context.Context, filter domain.BonusFilter) (domain.BonusStats, error) { + return s.bonusStore.GetBonusStats(ctx, filter) +} +func (s *Service) UpdateUserBonus(ctx context.Context, bonusID int64, IsClaimed bool) error { + return s.bonusStore.UpdateUserBonus(ctx, bonusID, IsClaimed) +} +func (s *Service) DeleteUserBonus(ctx context.Context, bonusID int64) error { + return s.bonusStore.DeleteUserBonus(ctx, bonusID) } diff --git a/internal/services/wallet/port.go b/internal/services/wallet/port.go index 29e21e1..e5687d4 100644 --- a/internal/services/wallet/port.go +++ b/internal/services/wallet/port.go @@ -30,6 +30,7 @@ type TransferStore interface { GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.TransferDetail, error) GetTransferByReference(ctx context.Context, reference string) (domain.TransferDetail, error) GetTransferByID(ctx context.Context, id int64) (domain.TransferDetail, error) + GetTransferStats(ctx context.Context, walletID int64) (domain.TransferStats, error) UpdateTransferVerification(ctx context.Context, id int64, verified bool) error UpdateTransferStatus(ctx context.Context, id int64, status string) error // InitiateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) @@ -47,9 +48,9 @@ type ApprovalStore interface { } type DirectDepositStore interface { - CreateDirectDeposit(ctx context.Context, deposit domain.CreateDirectDeposit) (domain.DirectDeposit, error) - GetDirectDeposit(ctx context.Context, id int64) (domain.DirectDeposit, error) - UpdateDirectDeposit(ctx context.Context, deposit domain.UpdateDirectDeposit) (domain.DirectDeposit, error) - GetDirectDepositsByStatus(ctx context.Context, status domain.DirectDepositStatus) ([]domain.DirectDeposit, error) - GetCustomerDirectDeposits(ctx context.Context, customerID int64) ([]domain.DirectDeposit, error) + CreateDirectDeposit(ctx context.Context, deposit domain.CreateDirectDeposit) (domain.DirectDeposit, error) + GetDirectDeposit(ctx context.Context, id int64) (domain.DirectDeposit, error) + UpdateDirectDeposit(ctx context.Context, deposit domain.UpdateDirectDeposit) (domain.DirectDeposit, error) + GetDirectDepositsByStatus(ctx context.Context, status domain.DirectDepositStatus) ([]domain.DirectDeposit, error) + GetCustomerDirectDeposits(ctx context.Context, customerID int64) ([]domain.DirectDeposit, error) } diff --git a/internal/services/wallet/transfer.go b/internal/services/wallet/transfer.go index fd6bc04..461b51a 100644 --- a/internal/services/wallet/transfer.go +++ b/internal/services/wallet/transfer.go @@ -35,6 +35,9 @@ func (s *Service) GetTransfersByWallet(ctx context.Context, walletID int64) ([]d return s.transferStore.GetTransfersByWallet(ctx, walletID) } +func (s *Service) GetTransferStats(ctx context.Context, walletID int64) (domain.TransferStats, error) { + return s.transferStore.GetTransferStats(ctx, walletID) +} func (s *Service) UpdateTransferVerification(ctx context.Context, id int64, verified bool) error { return s.transferStore.UpdateTransferVerification(ctx, id, verified) } diff --git a/internal/services/wallet/wallet.go b/internal/services/wallet/wallet.go index 5865f75..7d04160 100644 --- a/internal/services/wallet/wallet.go +++ b/internal/services/wallet/wallet.go @@ -215,6 +215,9 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain. return newTransfer, err } + + + // Directly Refilling wallet without // func (s *Service) RefillWallet(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) { // receiverWallet, err := s.GetWalletByID(ctx, transfer.ReceiverWalletID) diff --git a/internal/web_server/handlers/bonus.go b/internal/web_server/handlers/bonus.go index f796827..80374cc 100644 --- a/internal/web_server/handlers/bonus.go +++ b/internal/web_server/handlers/bonus.go @@ -1,98 +1,98 @@ package handlers -import ( - "time" +// import ( +// "time" - "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" - "github.com/gofiber/fiber/v2" - "go.uber.org/zap" -) +// "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" +// "github.com/gofiber/fiber/v2" +// "go.uber.org/zap" +// ) -func (h *Handler) CreateBonusMultiplier(c *fiber.Ctx) error { - var req struct { - Multiplier float32 `json:"multiplier"` - BalanceCap int64 `json:"balance_cap"` - } +// func (h *Handler) CreateBonusMultiplier(c *fiber.Ctx) error { +// var req struct { +// Multiplier float32 `json:"multiplier"` +// BalanceCap int64 `json:"balance_cap"` +// } - if err := c.BodyParser(&req); err != nil { - h.logger.Error("failed to parse bonus multiplier request", "error", err) - h.mongoLoggerSvc.Info("failed to parse bonus multiplier", - zap.Int("status_code", fiber.StatusBadRequest), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error()) - } +// if err := c.BodyParser(&req); err != nil { +// h.logger.Error("failed to parse bonus multiplier request", "error", err) +// h.mongoLoggerSvc.Info("failed to parse bonus multiplier", +// zap.Int("status_code", fiber.StatusBadRequest), +// zap.Error(err), +// zap.Time("timestamp", time.Now()), +// ) +// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error()) +// } - // currently only one multiplier is allowed - // we can add an active bool in the db and have mulitple bonus if needed - multipliers, err := h.bonusSvc.GetBonusMultiplier(c.Context()) - if err != nil { - h.logger.Error("failed to get bonus multiplier", "error", err) - h.mongoLoggerSvc.Info("Failed to get bonus multiplier", - zap.Int("status_code", fiber.StatusBadRequest), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error()) - } +// // currently only one multiplier is allowed +// // we can add an active bool in the db and have mulitple bonus if needed +// multipliers, err := h.bonusSvc.GetBonusMultiplier(c.Context()) +// if err != nil { +// h.logger.Error("failed to get bonus multiplier", "error", err) +// h.mongoLoggerSvc.Info("Failed to get bonus multiplier", +// zap.Int("status_code", fiber.StatusBadRequest), +// zap.Error(err), +// zap.Time("timestamp", time.Now()), +// ) +// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error()) +// } - if len(multipliers) > 0 { - return fiber.NewError(fiber.StatusBadRequest, "only one multiplier is allowed") - } +// if len(multipliers) > 0 { +// return fiber.NewError(fiber.StatusBadRequest, "only one multiplier is allowed") +// } - if err := h.bonusSvc.CreateBonusMultiplier(c.Context(), req.Multiplier, req.BalanceCap); err != nil { - h.mongoLoggerSvc.Error("failed to create bonus multiplier", - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusInternalServerError, "failed to create bonus multiplier"+err.Error()) - } +// if err := h.bonusSvc.CreateBonusMultiplier(c.Context(), req.Multiplier, req.BalanceCap); err != nil { +// h.mongoLoggerSvc.Error("failed to create bonus multiplier", +// zap.Int("status_code", fiber.StatusInternalServerError), +// zap.Error(err), +// zap.Time("timestamp", time.Now()), +// ) +// return fiber.NewError(fiber.StatusInternalServerError, "failed to create bonus multiplier"+err.Error()) +// } - return response.WriteJSON(c, fiber.StatusOK, "Create bonus multiplier successfully", nil, nil) -} +// return response.WriteJSON(c, fiber.StatusOK, "Create bonus multiplier successfully", nil, nil) +// } -func (h *Handler) GetBonusMultiplier(c *fiber.Ctx) error { - multipliers, err := h.bonusSvc.GetBonusMultiplier(c.Context()) - if err != nil { - h.mongoLoggerSvc.Info("failed to get bonus multiplier", - zap.Int("status_code", fiber.StatusBadRequest), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body"+err.Error()) - } +// func (h *Handler) GetBonusMultiplier(c *fiber.Ctx) error { +// multipliers, err := h.bonusSvc.GetBonusMultiplier(c.Context()) +// if err != nil { +// h.mongoLoggerSvc.Info("failed to get bonus multiplier", +// zap.Int("status_code", fiber.StatusBadRequest), +// zap.Error(err), +// zap.Time("timestamp", time.Now()), +// ) +// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body"+err.Error()) +// } - return response.WriteJSON(c, fiber.StatusOK, "Fetched bonus multiplier successfully", multipliers, nil) -} +// return response.WriteJSON(c, fiber.StatusOK, "Fetched bonus multiplier successfully", multipliers, nil) +// } -func (h *Handler) UpdateBonusMultiplier(c *fiber.Ctx) error { - var req struct { - ID int64 `json:"id"` - Multiplier float32 `json:"multiplier"` - BalanceCap int64 `json:"balance_cap"` - } +// func (h *Handler) UpdateBonusMultiplier(c *fiber.Ctx) error { +// var req struct { +// ID int64 `json:"id"` +// Multiplier float32 `json:"multiplier"` +// BalanceCap int64 `json:"balance_cap"` +// } - if err := c.BodyParser(&req); err != nil { - h.mongoLoggerSvc.Info("failed to parse bonus multiplier", - zap.Int("status_code", fiber.StatusBadRequest), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error()) - } +// if err := c.BodyParser(&req); err != nil { +// h.mongoLoggerSvc.Info("failed to parse bonus multiplier", +// zap.Int("status_code", fiber.StatusBadRequest), +// zap.Error(err), +// zap.Time("timestamp", time.Now()), +// ) +// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error()) +// } - if err := h.bonusSvc.UpdateBonusMultiplier(c.Context(), req.ID, req.Multiplier, req.BalanceCap); err != nil { - h.logger.Error("failed to update bonus multiplier", "error", err) - h.mongoLoggerSvc.Error("failed to update bonus multiplier", - zap.Int64("id", req.ID), - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusInternalServerError, "failed to update bonus multiplier:"+err.Error()) - } +// if err := h.bonusSvc.UpdateBonusMultiplier(c.Context(), req.ID, req.Multiplier, req.BalanceCap); err != nil { +// h.logger.Error("failed to update bonus multiplier", "error", err) +// h.mongoLoggerSvc.Error("failed to update bonus multiplier", +// zap.Int64("id", req.ID), +// zap.Int("status_code", fiber.StatusInternalServerError), +// zap.Error(err), +// zap.Time("timestamp", time.Now()), +// ) +// return fiber.NewError(fiber.StatusInternalServerError, "failed to update bonus multiplier:"+err.Error()) +// } - return response.WriteJSON(c, fiber.StatusOK, "Updated bonus multiplier successfully", nil, nil) -} +// return response.WriteJSON(c, fiber.StatusOK, "Updated bonus multiplier successfully", nil, nil) +// } diff --git a/internal/web_server/handlers/chapa.go b/internal/web_server/handlers/chapa.go index 00b771a..9a2a7c2 100644 --- a/internal/web_server/handlers/chapa.go +++ b/internal/web_server/handlers/chapa.go @@ -2,7 +2,6 @@ package handlers import ( "fmt" - "math" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/gofiber/fiber/v2" @@ -53,36 +52,39 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error { } // get static wallet of user - wallet, err := h.walletSvc.GetCustomerWallet(c.Context(), userID) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ - Error: err.Error(), - Message: "Failed to initiate Chapa deposit", - }) - } + // wallet, err := h.walletSvc.GetCustomerWallet(c.Context(), userID) + // if err != nil { + // return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + // Error: err.Error(), + // Message: "Failed to initiate Chapa deposit", + // }) + // } - var multiplier float32 = 1 - bonusMultiplier, err := h.bonusSvc.GetBonusMultiplier(c.Context()) - if err == nil { - multiplier = bonusMultiplier[0].Multiplier - } + // var multiplier float32 = 1 + // bonusMultiplier, err := h.bonusSvc.GetBonusMultiplier(c.Context()) + // if err == nil { + // multiplier = bonusMultiplier[0].Multiplier + // } - var balanceCap int64 = 0 - bonusBalanceCap, err := h.bonusSvc.GetBonusBalanceCap(c.Context()) - if err == nil { - balanceCap = bonusBalanceCap[0].BalanceCap - } + // var balanceCap int64 = 0 + // bonusBalanceCap, err := h.bonusSvc.GetBonusBalanceCap(c.Context()) + // if err == nil { + // balanceCap = bonusBalanceCap[0].BalanceCap + // } - capedBalanceAmount := domain.Currency((math.Min(req.Amount, float64(balanceCap)) * float64(multiplier)) * 100) + // capedBalanceAmount := domain.Currency((math.Min(req.Amount, float64(balanceCap)) * float64(multiplier)) * 100) - _, err = h.walletSvc.AddToWallet(c.Context(), wallet.StaticID, capedBalanceAmount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, - fmt.Sprintf("Added %v to static wallet because of deposit bonus using multiplier %v", capedBalanceAmount, multiplier), - ) - if err != nil { - h.logger.Error("Failed to add bonus to static wallet", "walletID", wallet.StaticID, "user id", userID, "error", err) - return err - } + // _, err = h.walletSvc.AddToWallet(c.Context(), wallet.StaticID, capedBalanceAmount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, + // fmt.Sprintf("Added %v to static wallet because of deposit bonus using multiplier %v", capedBalanceAmount, multiplier), + // ) + // if err != nil { + // h.logger.Error("Failed to add bonus to static wallet", "walletID", wallet.StaticID, "user id", userID, "error", err) + // return err + // } + // if err := h.bonusSvc.ProcessWelcomeBonus(c.Context(), domain.ToCurrency(float32(req.Amount)), 0, userID); err != nil { + // return err + // } return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Chapa deposit process initiated successfully", Data: checkoutURL, diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 4a7f4b7..28b31e4 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -206,9 +206,9 @@ func (a *App) initAppRoutes() { a.fiber.Get("/raffle-ticket/unsuspend/:id", a.authMiddleware, h.UnSuspendRaffleTicket) // Bonus Routes - groupV1.Get("/bonus", a.authMiddleware, h.GetBonusMultiplier) - groupV1.Post("/bonus/create", a.authMiddleware, h.CreateBonusMultiplier) - groupV1.Put("/bonus/update", a.authMiddleware, h.UpdateBonusMultiplier) + // groupV1.Get("/bonus", a.authMiddleware, h.GetBonusMultiplier) + // groupV1.Post("/bonus/create", a.authMiddleware, h.CreateBonusMultiplier) + // groupV1.Put("/bonus/update", a.authMiddleware, h.UpdateBonusMultiplier) groupV1.Get("/cashiers", a.authMiddleware, h.GetAllCashiers) groupV1.Get("/cashiers/:id", a.authMiddleware, h.GetCashierByID)