fix: refactoring bonus

This commit is contained in:
Samuel Tariku 2025-09-10 23:27:11 +03:00
parent 5595600ede
commit 215eb5a1d8
23 changed files with 1116 additions and 500 deletions

View File

@ -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)

View File

@ -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,
@ -342,4 +345,3 @@ SET name = EXCLUDED.name,
is_active = EXCLUDED.is_active,
created_at = EXCLUDED.created_at,
updated_at = EXCLUDED.updated_at;

View File

@ -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,

View File

@ -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;
-- 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;

View File

@ -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,

View File

@ -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
}

View File

@ -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"`

View File

@ -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

136
internal/domain/bonus.go Normal file
View File

@ -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,
}
}

View File

@ -27,6 +27,15 @@ type SettingList struct {
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 {

View File

@ -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
}

View File

@ -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{

View File

@ -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
}

View File

@ -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,

View File

@ -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
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",
eventLogger := s.mongoLogger.With(
zap.String("eventID", eventID),
zap.Int32("sportID", sportID),
zap.String("homeTeam", HomeTeam),
zap.String("awayTeam", AwayTeam),
zap.Error(err))
)
markets, err := s.prematchSvc.GetOddsByEventID(ctx, eventID, domain.OddMarketWithEventFilter{})
if err != nil {
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)
if err != nil {
s.mongoLogger.Error("failed to update bet status: invalid bet ID",
zap.Int64("bet_id", id),
zap.Error(err),
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 {
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 bet.IsShopBet {
return s.betStore.UpdateStatus(ctx, id, status)
}
customerWallet, err := s.walletSvc.GetCustomerWallet(ctx, id)
if err != nil {
s.mongoLogger.Error("failed to get customer wallet",
zap.Int64("bet_id", id),
zap.Error(err),
)
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, betId, status)
}
// After this point the bet is known to be a online customer bet
customerWallet, err := s.walletSvc.GetCustomerWallet(ctx, bet.UserID)
if err != nil {
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
}
func (s *Service) SendWinningStatusNotification(ctx context.Context, status domain.OutcomeStatus, userID int64, winningAmount domain.Currency, extra string) error {
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 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),
for _, channel := range []domain.DeliveryChannel{
domain.DeliveryChannelInApp,
domain.DeliveryChannelEmail,
domain.DeliveryChannelSMS,
} {
if !shouldSend(channel, param.SendEmail, param.SendSMS) {
continue
}
if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil {
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
}
betNotification.DeliveryChannel = domain.DeliveryChannelEmail
if err := s.notificationSvc.SendNotification(ctx, betNotification); 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),
for _, channel := range []domain.DeliveryChannel{
domain.DeliveryChannelInApp,
domain.DeliveryChannelEmail,
domain.DeliveryChannelSMS,
} {
if !shouldSend(channel, param.SendEmail, param.SendSMS) {
continue
}
if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil {
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
}
betNotification.DeliveryChannel = domain.DeliveryChannelEmail
if err := s.notificationSvc.SendNotification(ctx, betNotification); 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 {
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
}
betNotification.DeliveryChannel = domain.DeliveryChannelEmail
if err := s.notificationSvc.SendNotification(ctx, betNotification); 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()),
)
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
}
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
}
}
@ -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,

View File

@ -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
}

View File

@ -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
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,
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
}
func (s *Service) GetBonusMultiplier(ctx context.Context) ([]dbgen.GetBonusMultiplierRow, error) {
return s.bonusStore.GetBonusMultiplier(ctx)
if !settingsList.WelcomeBonusActive {
return ErrWelcomeBonusNotActive
}
func (s *Service) GetBonusBalanceCap(ctx context.Context) ([]dbgen.GetBonusBalanceCapRow, error) {
return s.bonusStore.GetBonusBalanceCap(ctx)
wallet, err := s.walletSvc.GetCustomerWallet(ctx, userID)
if err != nil {
return err
}
func (s *Service) UpdateBonusMultiplier(ctx context.Context, id int64, mulitplier float32, balance_cap int64) error {
return s.bonusStore.UpdateBonusMultiplier(ctx, id, mulitplier, balance_cap)
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) CreateUserBonus(ctx context.Context, bonus domain.CreateBonus) (domain.UserBonus, error) {
return s.bonusStore.CreateUserBonus(ctx, bonus)
}
func (s *Service) GetAllUserBonuses(ctx context.Context) ([]domain.UserBonus, error) {
return s.bonusStore.GetAllUserBonuses(ctx)
}
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)
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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)

View File

@ -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)
// }

View File

@ -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,

View File

@ -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)