fix: refactor bonus and bonus settings; added welcome bonus

This commit is contained in:
Samuel Tariku 2025-09-12 16:28:39 +03:00
parent 215eb5a1d8
commit e5f42f1928
22 changed files with 875 additions and 994 deletions

View File

@ -142,7 +142,7 @@ func main() {
ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger, *settingSvc, notificationSvc) 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) 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) resultSvc := result.NewService(store, cfg, logger, domain.MongoDBLogger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc, messengerSvc, *userSvc)
bonusSvc := bonus.NewService(store, walletSvc, settingSvc, domain.MongoDBLogger) bonusSvc := bonus.NewService(store, walletSvc, settingSvc, notificationSvc, domain.MongoDBLogger)
referalRepo := repository.NewReferralRepository(store) referalRepo := repository.NewReferralRepository(store)
vitualGameRepo := repository.NewVirtualGameRepository(store) vitualGameRepo := repository.NewVirtualGameRepository(store)
recommendationRepo := repository.NewRecommendationRepository(store) recommendationRepo := repository.NewRecommendationRepository(store)

View File

@ -88,8 +88,14 @@ VALUES ('sms_provider', 'afro_message'),
('cashback_percentage', '0.2'), ('cashback_percentage', '0.2'),
('default_max_referrals', '15'), ('default_max_referrals', '15'),
('minimum_bet_amount', '100'), ('minimum_bet_amount', '100'),
('bet_duplicate_limit', '5'),
('send_email_on_bet_finish', 'true'), ('send_email_on_bet_finish', 'true'),
('send_sms_on_bet_finish', 'false') ON CONFLICT (key) DO NOTHING; ('send_sms_on_bet_finish', 'false'),
('welcome_bonus_active', 'false'),
('welcome_bonus_multiplier', '1.5'),
('welcome_bonus_multiplier', '100000'),
('welcome_bonus_count', '3'),
('welcome_bonus_expiry', '10') ON CONFLICT (key) DO NOTHING;
-- Users -- Users
INSERT INTO users ( INSERT INTO users (
id, id,

View File

@ -457,11 +457,12 @@ CREATE TABLE user_bonuses (
id BIGINT NOT NULL, id BIGINT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
description TEXT NOT NULL, description TEXT NOT NULL,
type TEXT NOT NULL,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
bonus_code TEXT NOT NULL UNIQUE,
reward_amount BIGINT NOT NULL, reward_amount BIGINT NOT NULL,
is_claimed BOOLEAN NOT NULL DEFAULT false, is_claimed BOOLEAN NOT NULL DEFAULT false,
expires_at TIMESTAMP NOT NULL, expires_at TIMESTAMP NOT NULL,
claimed_at TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
@ -500,7 +501,23 @@ CREATE TABLE direct_deposits (
CREATE INDEX idx_direct_deposits_status ON direct_deposits (status); CREATE INDEX idx_direct_deposits_status ON direct_deposits (status);
CREATE INDEX idx_direct_deposits_customer ON direct_deposits (customer_id); CREATE INDEX idx_direct_deposits_customer ON direct_deposits (customer_id);
CREATE INDEX idx_direct_deposits_reference ON direct_deposits (bank_reference); CREATE INDEX idx_direct_deposits_reference ON direct_deposits (bank_reference);
-- Views CREATE TABLE IF NOT EXISTS raffles (
id SERIAL PRIMARY KEY,
company_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
expires_at TIMESTAMP NOT NULL,
type VARCHAR(50) NOT NULL CHECK (type IN ('virtual', 'sport')),
status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'completed'))
);
CREATE TABLE IF NOT EXISTS raffle_tickets (
id SERIAL PRIMARY KEY,
raffle_id INT NOT NULL REFERENCES raffles(id) ON DELETE CASCADE,
user_id INT NOT NULL,
is_active BOOL DEFAULT true,
UNIQUE (raffle_id, user_id)
);
------ Views
CREATE VIEW companies_details AS CREATE VIEW companies_details AS
SELECT companies.*, SELECT companies.*,
wallets.balance, wallets.balance,
@ -526,22 +543,6 @@ CREATE TABLE IF NOT EXISTS supported_operations (
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
description VARCHAR(255) NOT NULL description VARCHAR(255) NOT NULL
); );
CREATE TABLE IF NOT EXISTS raffles (
id SERIAL PRIMARY KEY,
company_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
expires_at TIMESTAMP NOT NULL,
type VARCHAR(50) NOT NULL CHECK (type IN ('virtual', 'sport')),
status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'completed'))
);
CREATE TABLE IF NOT EXISTS raffle_tickets (
id SERIAL PRIMARY KEY,
raffle_id INT NOT NULL REFERENCES raffles(id) ON DELETE CASCADE,
user_id INT NOT NULL,
is_active BOOL DEFAULT true,
UNIQUE (raffle_id, user_id)
);
CREATE VIEW bet_with_outcomes AS CREATE VIEW bet_with_outcomes AS
SELECT bets.*, SELECT bets.*,
CONCAT (users.first_name, ' ', users.last_name) AS full_name, CONCAT (users.first_name, ' ', users.last_name) AS full_name,

View File

@ -18,7 +18,8 @@ CREATE TABLE IF NOT EXISTS notifications (
'admin_alert', 'admin_alert',
'bet_result', 'bet_result',
'transfer_rejected', 'transfer_rejected',
'approval_required' 'approval_required',
'bonus_awarded'
) )
), ),
level TEXT NOT NULL CHECK (level IN ('info', 'error', 'warning', 'success')), level TEXT NOT NULL CHECK (level IN ('info', 'error', 'warning', 'success')),

View File

@ -2,8 +2,8 @@
INSERT INTO user_bonuses ( INSERT INTO user_bonuses (
name, name,
description, description,
type,
user_id, user_id,
bonus_code,
reward_amount, reward_amount,
expires_at expires_at
) )
@ -11,15 +11,24 @@ VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *; RETURNING *;
-- name: GetAllUserBonuses :many -- name: GetAllUserBonuses :many
SELECT * SELECT *
FROM user_bonuses; FROM user_bonuses
WHERE (
user_id = sqlc.narg('user_id')
OR sqlc.narg('user_id') IS NULL
)
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
-- name: GetUserBonusByID :one -- name: GetUserBonusByID :one
SELECT * SELECT *
FROM user_bonuses FROM user_bonuses
WHERE id = $1; WHERE id = $1;
-- name: GetBonusesByUserID :many
SELECT * -- name: GetBonusCount :one
SELECT COUNT(*)
FROM user_bonuses FROM user_bonuses
WHERE user_id = $1; WHERE (
user_id = sqlc.narg('user_id')
OR sqlc.narg('user_id') IS NULL
);
-- name: GetBonusStats :one -- name: GetBonusStats :one
SELECT COUNT(*) AS total_bonuses, SELECT COUNT(*) AS total_bonuses,
COALESCE(SUM(reward_amount), 0)::bigint AS total_reward_earned, COALESCE(SUM(reward_amount), 0)::bigint AS total_reward_earned,
@ -30,7 +39,7 @@ SELECT COUNT(*) AS total_bonuses,
) AS claimed_bonuses, ) AS claimed_bonuses,
COUNT( COUNT(
CASE CASE
WHEN expires_at > now() THEN 1 WHEN expires_at < now() THEN 1
END END
) AS expired_bonuses ) AS expired_bonuses
FROM user_bonuses FROM user_bonuses

View File

@ -15,20 +15,20 @@ const CreateUserBonus = `-- name: CreateUserBonus :one
INSERT INTO user_bonuses ( INSERT INTO user_bonuses (
name, name,
description, description,
type,
user_id, user_id,
bonus_code,
reward_amount, reward_amount,
expires_at expires_at
) )
VALUES ($1, $2, $3, $4, $5, $6) 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 RETURNING id, name, description, type, user_id, reward_amount, is_claimed, expires_at, claimed_at, created_at, updated_at
` `
type CreateUserBonusParams struct { type CreateUserBonusParams struct {
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Type string `json:"type"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
BonusCode string `json:"bonus_code"`
RewardAmount int64 `json:"reward_amount"` RewardAmount int64 `json:"reward_amount"`
ExpiresAt pgtype.Timestamp `json:"expires_at"` ExpiresAt pgtype.Timestamp `json:"expires_at"`
} }
@ -37,8 +37,8 @@ func (q *Queries) CreateUserBonus(ctx context.Context, arg CreateUserBonusParams
row := q.db.QueryRow(ctx, CreateUserBonus, row := q.db.QueryRow(ctx, CreateUserBonus,
arg.Name, arg.Name,
arg.Description, arg.Description,
arg.Type,
arg.UserID, arg.UserID,
arg.BonusCode,
arg.RewardAmount, arg.RewardAmount,
arg.ExpiresAt, arg.ExpiresAt,
) )
@ -47,11 +47,12 @@ func (q *Queries) CreateUserBonus(ctx context.Context, arg CreateUserBonusParams
&i.ID, &i.ID,
&i.Name, &i.Name,
&i.Description, &i.Description,
&i.Type,
&i.UserID, &i.UserID,
&i.BonusCode,
&i.RewardAmount, &i.RewardAmount,
&i.IsClaimed, &i.IsClaimed,
&i.ExpiresAt, &i.ExpiresAt,
&i.ClaimedAt,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
) )
@ -69,12 +70,23 @@ func (q *Queries) DeleteUserBonus(ctx context.Context, id int64) error {
} }
const GetAllUserBonuses = `-- name: GetAllUserBonuses :many const GetAllUserBonuses = `-- name: GetAllUserBonuses :many
SELECT id, name, description, user_id, bonus_code, reward_amount, is_claimed, expires_at, created_at, updated_at SELECT id, name, description, type, user_id, reward_amount, is_claimed, expires_at, claimed_at, created_at, updated_at
FROM user_bonuses FROM user_bonuses
WHERE (
user_id = $1
OR $1 IS NULL
)
LIMIT $3 OFFSET $2
` `
func (q *Queries) GetAllUserBonuses(ctx context.Context) ([]UserBonuse, error) { type GetAllUserBonusesParams struct {
rows, err := q.db.Query(ctx, GetAllUserBonuses) UserID pgtype.Int8 `json:"user_id"`
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
}
func (q *Queries) GetAllUserBonuses(ctx context.Context, arg GetAllUserBonusesParams) ([]UserBonuse, error) {
rows, err := q.db.Query(ctx, GetAllUserBonuses, arg.UserID, arg.Offset, arg.Limit)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -86,11 +98,12 @@ func (q *Queries) GetAllUserBonuses(ctx context.Context) ([]UserBonuse, error) {
&i.ID, &i.ID,
&i.Name, &i.Name,
&i.Description, &i.Description,
&i.Type,
&i.UserID, &i.UserID,
&i.BonusCode,
&i.RewardAmount, &i.RewardAmount,
&i.IsClaimed, &i.IsClaimed,
&i.ExpiresAt, &i.ExpiresAt,
&i.ClaimedAt,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
); err != nil { ); err != nil {
@ -104,6 +117,22 @@ func (q *Queries) GetAllUserBonuses(ctx context.Context) ([]UserBonuse, error) {
return items, nil return items, nil
} }
const GetBonusCount = `-- name: GetBonusCount :one
SELECT COUNT(*)
FROM user_bonuses
WHERE (
user_id = $1
OR $1 IS NULL
)
`
func (q *Queries) GetBonusCount(ctx context.Context, userID pgtype.Int8) (int64, error) {
row := q.db.QueryRow(ctx, GetBonusCount, userID)
var count int64
err := row.Scan(&count)
return count, err
}
const GetBonusStats = `-- name: GetBonusStats :one const GetBonusStats = `-- name: GetBonusStats :one
SELECT COUNT(*) AS total_bonuses, SELECT COUNT(*) AS total_bonuses,
COALESCE(SUM(reward_amount), 0)::bigint AS total_reward_earned, COALESCE(SUM(reward_amount), 0)::bigint AS total_reward_earned,
@ -114,7 +143,7 @@ SELECT COUNT(*) AS total_bonuses,
) AS claimed_bonuses, ) AS claimed_bonuses,
COUNT( COUNT(
CASE CASE
WHEN expires_at > now() THEN 1 WHEN expires_at < now() THEN 1
END END
) AS expired_bonuses ) AS expired_bonuses
FROM user_bonuses FROM user_bonuses
@ -153,45 +182,8 @@ func (q *Queries) GetBonusStats(ctx context.Context, arg GetBonusStatsParams) (G
return i, err 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 []UserBonuse
for rows.Next() {
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)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetUserBonusByID = `-- name: GetUserBonusByID :one const GetUserBonusByID = `-- name: GetUserBonusByID :one
SELECT id, name, description, user_id, bonus_code, reward_amount, is_claimed, expires_at, created_at, updated_at SELECT id, name, description, type, user_id, reward_amount, is_claimed, expires_at, claimed_at, created_at, updated_at
FROM user_bonuses FROM user_bonuses
WHERE id = $1 WHERE id = $1
` `
@ -203,11 +195,12 @@ func (q *Queries) GetUserBonusByID(ctx context.Context, id int64) (UserBonuse, e
&i.ID, &i.ID,
&i.Name, &i.Name,
&i.Description, &i.Description,
&i.Type,
&i.UserID, &i.UserID,
&i.BonusCode,
&i.RewardAmount, &i.RewardAmount,
&i.IsClaimed, &i.IsClaimed,
&i.ExpiresAt, &i.ExpiresAt,
&i.ClaimedAt,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
) )

View File

@ -750,11 +750,12 @@ type UserBonuse struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Type string `json:"type"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
BonusCode string `json:"bonus_code"`
RewardAmount int64 `json:"reward_amount"` RewardAmount int64 `json:"reward_amount"`
IsClaimed bool `json:"is_claimed"` IsClaimed bool `json:"is_claimed"`
ExpiresAt pgtype.Timestamp `json:"expires_at"` ExpiresAt pgtype.Timestamp `json:"expires_at"`
ClaimedAt pgtype.Timestamp `json:"claimed_at"`
CreatedAt pgtype.Timestamp `json:"created_at"` CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"`
} }

View File

@ -7,12 +7,19 @@ import (
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
type BonusType string
var (
WelcomeBonus BonusType = "welcome_bonus"
DepositBonus BonusType = "deposit_bonus"
)
type UserBonus struct { type UserBonus struct {
ID int64 ID int64
Name string Name string
Description string Description string
UserID int64 UserID int64
BonusCode string Type BonusType
RewardAmount Currency RewardAmount Currency
IsClaimed bool IsClaimed bool
ExpiresAt time.Time ExpiresAt time.Time
@ -25,7 +32,7 @@ type UserBonusRes struct {
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
BonusCode string `json:"bonus_code"` Type BonusType `json:"type"`
RewardAmount float32 `json:"reward_amount"` RewardAmount float32 `json:"reward_amount"`
IsClaimed bool `json:"is_claimed"` IsClaimed bool `json:"is_claimed"`
ExpiresAt time.Time `json:"expires_at"` ExpiresAt time.Time `json:"expires_at"`
@ -38,8 +45,8 @@ func ConvertToBonusRes(bonus UserBonus) UserBonusRes {
ID: bonus.ID, ID: bonus.ID,
Name: bonus.Name, Name: bonus.Name,
Description: bonus.Description, Description: bonus.Description,
Type: bonus.Type,
UserID: bonus.UserID, UserID: bonus.UserID,
BonusCode: bonus.BonusCode,
RewardAmount: bonus.RewardAmount.Float32(), RewardAmount: bonus.RewardAmount.Float32(),
IsClaimed: bonus.IsClaimed, IsClaimed: bonus.IsClaimed,
ExpiresAt: bonus.ExpiresAt, ExpiresAt: bonus.ExpiresAt,
@ -48,41 +55,51 @@ func ConvertToBonusRes(bonus UserBonus) UserBonusRes {
} }
} }
func ConvertToBonusResList(bonuses []UserBonus) []UserBonusRes {
result := make([]UserBonusRes, len(bonuses))
for i, bonus := range bonuses {
result[i] = ConvertToBonusRes(bonus)
}
return result
}
type CreateBonus struct { type CreateBonus struct {
Name string Name string
Description string Description string
Type BonusType
UserID int64 UserID int64
BonusCode string
RewardAmount Currency RewardAmount Currency
ExpiresAt time.Time ExpiresAt time.Time
} }
type CreateBonusReq struct { // type CreateBonusReq struct {
Name string `json:"name"` // Name string `json:"name"`
Description string `json:"description"` // Description string `json:"description"`
UserID int64 `json:"user_id"` // Type BonusType `json:"type"`
BonusCode string `json:"bonus_code"` // UserID int64 `json:"user_id"`
RewardAmount float32 `json:"reward_amount"` // RewardAmount float32 `json:"reward_amount"`
ExpiresAt time.Time `json:"expires_at"` // ExpiresAt time.Time `json:"expires_at"`
} // }
func ConvertCreateBonusReq(bonus CreateBonusReq, companyID int64) CreateBonus { // func ConvertCreateBonusReq(bonus CreateBonusReq, companyID int64) CreateBonus {
return CreateBonus{ // return CreateBonus{
Name: bonus.Name, // Name: bonus.Name,
Description: bonus.Description, // Description: bonus.Description,
UserID: bonus.UserID, // Type: bonus.Type,
BonusCode: bonus.BonusCode, // UserID: bonus.UserID,
RewardAmount: ToCurrency(bonus.RewardAmount), // RewardAmount: ToCurrency(bonus.RewardAmount),
ExpiresAt: bonus.ExpiresAt, // ExpiresAt: bonus.ExpiresAt,
} // }
} // }
func ConvertCreateBonus(bonus CreateBonus) dbgen.CreateUserBonusParams { func ConvertCreateBonus(bonus CreateBonus) dbgen.CreateUserBonusParams {
return dbgen.CreateUserBonusParams{ return dbgen.CreateUserBonusParams{
Name: bonus.Name, Name: bonus.Name,
Description: bonus.Description, Description: bonus.Description,
Type: string(bonus.Type),
UserID: bonus.UserID, UserID: bonus.UserID,
BonusCode: bonus.BonusCode,
RewardAmount: int64(bonus.RewardAmount), RewardAmount: int64(bonus.RewardAmount),
ExpiresAt: pgtype.Timestamp{ ExpiresAt: pgtype.Timestamp{
Time: bonus.ExpiresAt, Time: bonus.ExpiresAt,
@ -93,11 +110,12 @@ func ConvertCreateBonus(bonus CreateBonus) dbgen.CreateUserBonusParams {
func ConvertDBBonus(bonus dbgen.UserBonuse) UserBonus { func ConvertDBBonus(bonus dbgen.UserBonuse) UserBonus {
return UserBonus{ return UserBonus{
ID: bonus.ID, ID: bonus.ID,
Name: bonus.Name, Name: bonus.Name,
Description: bonus.Description, Description: bonus.Description,
UserID: bonus.UserID, Type: BonusType(bonus.Type),
BonusCode: bonus.BonusCode, UserID: bonus.UserID,
RewardAmount: Currency(bonus.RewardAmount), RewardAmount: Currency(bonus.RewardAmount),
IsClaimed: bonus.IsClaimed, IsClaimed: bonus.IsClaimed,
ExpiresAt: bonus.ExpiresAt.Time, ExpiresAt: bonus.ExpiresAt.Time,
@ -117,6 +135,8 @@ func ConvertDBBonuses(bonuses []dbgen.UserBonuse) []UserBonus {
type BonusFilter struct { type BonusFilter struct {
UserID ValidInt64 UserID ValidInt64
CompanyID ValidInt64 CompanyID ValidInt64
Limit ValidInt
Offset ValidInt
} }
type BonusStats struct { type BonusStats struct {
@ -126,6 +146,22 @@ type BonusStats struct {
ExpiredBonuses int64 ExpiredBonuses int64
} }
type BonusStatsRes struct {
TotalBonus int64 `json:"total_bonus"`
TotalRewardAmount float32 `json:"total_reward_amount"`
ClaimedBonuses int64 `json:"claimed_bonuses"`
ExpiredBonuses int64 `json:"expired_bonuses"`
}
func ConvertToBonusStatsRes(bonus BonusStats) BonusStatsRes {
return BonusStatsRes{
TotalBonus: bonus.TotalBonus,
TotalRewardAmount: bonus.TotalRewardAmount.Float32(),
ClaimedBonuses: bonus.ClaimedBonuses,
ExpiredBonuses: bonus.ExpiredBonuses,
}
}
func ConvertDBBonusStats(stats dbgen.GetBonusStatsRow) BonusStats { func ConvertDBBonusStats(stats dbgen.GetBonusStatsRow) BonusStats {
return BonusStats{ return BonusStats{
TotalBonus: stats.TotalBonuses, TotalBonus: stats.TotalBonuses,

View File

@ -32,6 +32,7 @@ const (
NOTIFICATION_TYPE_BET_RESULT NotificationType = "bet_result" NOTIFICATION_TYPE_BET_RESULT NotificationType = "bet_result"
NOTIFICATION_TYPE_TRANSFER_REJECTED NotificationType = "transfer_rejected" NOTIFICATION_TYPE_TRANSFER_REJECTED NotificationType = "transfer_rejected"
NOTIFICATION_TYPE_APPROVAL_REQUIRED NotificationType = "approval_required" NOTIFICATION_TYPE_APPROVAL_REQUIRED NotificationType = "approval_required"
NOTIFICATION_TYPE_BONUS_AWARDED NotificationType = "bonus_awarded"
NotificationRecieverSideAdmin NotificationRecieverSide = "admin" NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
NotificationRecieverSideCustomer NotificationRecieverSide = "customer" NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
@ -73,7 +74,7 @@ type Notification struct {
RecipientID int64 `json:"recipient_id"` RecipientID int64 `json:"recipient_id"`
Type NotificationType `json:"type"` Type NotificationType `json:"type"`
Level NotificationLevel `json:"level"` Level NotificationLevel `json:"level"`
ErrorSeverity NotificationErrorSeverity `json:"error_severity"` ErrorSeverity NotificationErrorSeverity `json:"error_severity"`
Reciever NotificationRecieverSide `json:"reciever"` Reciever NotificationRecieverSide `json:"reciever"`
IsRead bool `json:"is_read"` IsRead bool `json:"is_read"`
DeliveryStatus NotificationDeliveryStatus `json:"delivery_status,omitempty"` DeliveryStatus NotificationDeliveryStatus `json:"delivery_status,omitempty"`

View File

@ -39,117 +39,147 @@ type SettingList struct {
} }
type SettingListRes struct { type SettingListRes struct {
SMSProvider SMSProvider `json:"sms_provider"` SMSProvider SMSProvider `json:"sms_provider"`
MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"` MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"`
BetAmountLimit float32 `json:"bet_amount_limit"` BetAmountLimit float32 `json:"bet_amount_limit"`
DailyTicketPerIP int64 `json:"daily_ticket_limit"` DailyTicketPerIP int64 `json:"daily_ticket_limit"`
TotalWinningLimit float32 `json:"total_winning_limit"` TotalWinningLimit float32 `json:"total_winning_limit"`
AmountForBetReferral float32 `json:"amount_for_bet_referral"` AmountForBetReferral float32 `json:"amount_for_bet_referral"`
CashbackAmountCap float32 `json:"cashback_amount_cap"` CashbackAmountCap float32 `json:"cashback_amount_cap"`
DefaultWinningLimit int64 `json:"default_winning_limit"` DefaultWinningLimit int64 `json:"default_winning_limit"`
ReferralRewardAmount float32 `json:"referral_reward_amount"` ReferralRewardAmount float32 `json:"referral_reward_amount"`
CashbackPercentage float32 `json:"cashback_percentage"` CashbackPercentage float32 `json:"cashback_percentage"`
DefaultMaxReferrals int64 `json:"default_max_referrals"` DefaultMaxReferrals int64 `json:"default_max_referrals"`
MinimumBetAmount float32 `json:"minimum_bet_amount"` MinimumBetAmount float32 `json:"minimum_bet_amount"`
BetDuplicateLimit int64 `json:"bet_duplicate_limit"` BetDuplicateLimit int64 `json:"bet_duplicate_limit"`
SendEmailOnBetFinish bool `json:"send_email_on_bet_finish"` SendEmailOnBetFinish bool `json:"send_email_on_bet_finish"`
SendSMSOnBetFinish bool `json:"send_sms_on_bet_finish"` SendSMSOnBetFinish bool `json:"send_sms_on_bet_finish"`
WelcomeBonusActive bool `json:"welcome_bonus_active"`
WelcomeBonusMultiplier float32 `json:"welcome_bonus_multiplier"`
WelcomeBonusCap float32 `json:"welcome_bonus_cap"`
WelcomeBonusCount int64 `json:"welcome_bonus_count"`
WelcomeBonusExpire int64 `json:"welcome_bonus_expiry"`
} }
func ConvertSettingListRes(settings SettingList) SettingListRes { func ConvertSettingListRes(settings SettingList) SettingListRes {
return SettingListRes{ return SettingListRes{
SMSProvider: settings.SMSProvider, SMSProvider: settings.SMSProvider,
MaxNumberOfOutcomes: settings.MaxNumberOfOutcomes, MaxNumberOfOutcomes: settings.MaxNumberOfOutcomes,
BetAmountLimit: settings.BetAmountLimit.Float32(), BetAmountLimit: settings.BetAmountLimit.Float32(),
DailyTicketPerIP: settings.DailyTicketPerIP, DailyTicketPerIP: settings.DailyTicketPerIP,
TotalWinningLimit: settings.TotalWinningLimit.Float32(), TotalWinningLimit: settings.TotalWinningLimit.Float32(),
AmountForBetReferral: settings.AmountForBetReferral.Float32(), AmountForBetReferral: settings.AmountForBetReferral.Float32(),
CashbackAmountCap: settings.CashbackAmountCap.Float32(), CashbackAmountCap: settings.CashbackAmountCap.Float32(),
DefaultWinningLimit: settings.DefaultWinningLimit, DefaultWinningLimit: settings.DefaultWinningLimit,
ReferralRewardAmount: settings.ReferralRewardAmount.Float32(), ReferralRewardAmount: settings.ReferralRewardAmount.Float32(),
CashbackPercentage: settings.CashbackPercentage, CashbackPercentage: settings.CashbackPercentage,
DefaultMaxReferrals: settings.DefaultMaxReferrals, DefaultMaxReferrals: settings.DefaultMaxReferrals,
MinimumBetAmount: settings.MinimumBetAmount.Float32(), MinimumBetAmount: settings.MinimumBetAmount.Float32(),
BetDuplicateLimit: settings.BetDuplicateLimit, BetDuplicateLimit: settings.BetDuplicateLimit,
SendEmailOnBetFinish: settings.SendEmailOnBetFinish, SendEmailOnBetFinish: settings.SendEmailOnBetFinish,
SendSMSOnBetFinish: settings.SendSMSOnBetFinish, SendSMSOnBetFinish: settings.SendSMSOnBetFinish,
WelcomeBonusActive: settings.WelcomeBonusActive,
WelcomeBonusMultiplier: settings.WelcomeBonusMultiplier,
WelcomeBonusCap: settings.WelcomeBonusCap.Float32(),
WelcomeBonusCount: settings.WelcomeBonusCount,
WelcomeBonusExpire: settings.WelcomeBonusExpire,
} }
} }
type SaveSettingListReq struct { type SaveSettingListReq struct {
SMSProvider *string `json:"sms_provider,omitempty"` SMSProvider *string `json:"sms_provider,omitempty"`
MaxNumberOfOutcomes *int64 `json:"max_number_of_outcomes,omitempty"` MaxNumberOfOutcomes *int64 `json:"max_number_of_outcomes,omitempty"`
BetAmountLimit *float32 `json:"bet_amount_limit,omitempty"` BetAmountLimit *float32 `json:"bet_amount_limit,omitempty"`
DailyTicketPerIP *int64 `json:"daily_ticket_limit,omitempty"` DailyTicketPerIP *int64 `json:"daily_ticket_limit,omitempty"`
TotalWinningLimit *float32 `json:"total_winning_limit,omitempty"` TotalWinningLimit *float32 `json:"total_winning_limit,omitempty"`
AmountForBetReferral *float32 `json:"amount_for_bet_referral,omitempty"` AmountForBetReferral *float32 `json:"amount_for_bet_referral,omitempty"`
CashbackAmountCap *float32 `json:"cashback_amount_cap,omitempty"` CashbackAmountCap *float32 `json:"cashback_amount_cap,omitempty"`
DefaultWinningLimit *int64 `json:"default_winning_limit,omitempty"` DefaultWinningLimit *int64 `json:"default_winning_limit,omitempty"`
ReferralRewardAmount *float32 `json:"referral_reward_amount"` ReferralRewardAmount *float32 `json:"referral_reward_amount"`
CashbackPercentage *float32 `json:"cashback_percentage"` CashbackPercentage *float32 `json:"cashback_percentage"`
DefaultMaxReferrals *int64 `json:"default_max_referrals"` DefaultMaxReferrals *int64 `json:"default_max_referrals"`
MinimumBetAmount *float32 `json:"minimum_bet_amount"` MinimumBetAmount *float32 `json:"minimum_bet_amount"`
BetDuplicateLimit *int64 `json:"bet_duplicate_limit"` BetDuplicateLimit *int64 `json:"bet_duplicate_limit"`
SendEmailOnBetFinish *bool `json:"send_email_on_bet_finish"` SendEmailOnBetFinish *bool `json:"send_email_on_bet_finish"`
SendSMSOnBetFinish *bool `json:"send_sms_on_bet_finish"` SendSMSOnBetFinish *bool `json:"send_sms_on_bet_finish"`
WelcomeBonusActive *bool `json:"welcome_bonus_active"`
WelcomeBonusMultiplier *float32 `json:"welcome_bonus_multiplier"`
WelcomeBonusCap *float32 `json:"welcome_bonus_cap"`
WelcomeBonusCount *int64 `json:"welcome_bonus_count"`
WelcomeBonusExpire *int64 `json:"welcome_bonus_expiry"`
} }
type ValidSettingList struct { type ValidSettingList struct {
SMSProvider ValidString SMSProvider ValidString
MaxNumberOfOutcomes ValidInt64 MaxNumberOfOutcomes ValidInt64
BetAmountLimit ValidCurrency BetAmountLimit ValidCurrency
DailyTicketPerIP ValidInt64 DailyTicketPerIP ValidInt64
TotalWinningLimit ValidCurrency TotalWinningLimit ValidCurrency
AmountForBetReferral ValidCurrency AmountForBetReferral ValidCurrency
CashbackAmountCap ValidCurrency CashbackAmountCap ValidCurrency
DefaultWinningLimit ValidInt64 DefaultWinningLimit ValidInt64
ReferralRewardAmount ValidCurrency ReferralRewardAmount ValidCurrency
CashbackPercentage ValidFloat32 CashbackPercentage ValidFloat32
DefaultMaxReferrals ValidInt64 DefaultMaxReferrals ValidInt64
MinimumBetAmount ValidCurrency MinimumBetAmount ValidCurrency
BetDuplicateLimit ValidInt64 BetDuplicateLimit ValidInt64
SendEmailOnBetFinish ValidBool SendEmailOnBetFinish ValidBool
SendSMSOnBetFinish ValidBool SendSMSOnBetFinish ValidBool
WelcomeBonusActive ValidBool
WelcomeBonusMultiplier ValidFloat32
WelcomeBonusCap ValidCurrency
WelcomeBonusCount ValidInt64
WelcomeBonusExpire ValidInt64
} }
func ConvertSaveSettingListReq(settings SaveSettingListReq) ValidSettingList { func ConvertSaveSettingListReq(settings SaveSettingListReq) ValidSettingList {
return ValidSettingList{ return ValidSettingList{
SMSProvider: ConvertStringPtr(settings.SMSProvider), SMSProvider: ConvertStringPtr(settings.SMSProvider),
MaxNumberOfOutcomes: ConvertInt64Ptr(settings.MaxNumberOfOutcomes), MaxNumberOfOutcomes: ConvertInt64Ptr(settings.MaxNumberOfOutcomes),
BetAmountLimit: ConvertFloat32PtrToCurrency(settings.BetAmountLimit), BetAmountLimit: ConvertFloat32PtrToCurrency(settings.BetAmountLimit),
DailyTicketPerIP: ConvertInt64Ptr(settings.DailyTicketPerIP), DailyTicketPerIP: ConvertInt64Ptr(settings.DailyTicketPerIP),
TotalWinningLimit: ConvertFloat32PtrToCurrency(settings.TotalWinningLimit), TotalWinningLimit: ConvertFloat32PtrToCurrency(settings.TotalWinningLimit),
AmountForBetReferral: ConvertFloat32PtrToCurrency(settings.AmountForBetReferral), AmountForBetReferral: ConvertFloat32PtrToCurrency(settings.AmountForBetReferral),
CashbackAmountCap: ConvertFloat32PtrToCurrency(settings.CashbackAmountCap), CashbackAmountCap: ConvertFloat32PtrToCurrency(settings.CashbackAmountCap),
DefaultWinningLimit: ConvertInt64Ptr(settings.DefaultWinningLimit), DefaultWinningLimit: ConvertInt64Ptr(settings.DefaultWinningLimit),
ReferralRewardAmount: ConvertFloat32PtrToCurrency(settings.ReferralRewardAmount), ReferralRewardAmount: ConvertFloat32PtrToCurrency(settings.ReferralRewardAmount),
CashbackPercentage: ConvertFloat32Ptr(settings.CashbackPercentage), CashbackPercentage: ConvertFloat32Ptr(settings.CashbackPercentage),
DefaultMaxReferrals: ConvertInt64Ptr(settings.DefaultMaxReferrals), DefaultMaxReferrals: ConvertInt64Ptr(settings.DefaultMaxReferrals),
MinimumBetAmount: ConvertFloat32PtrToCurrency(settings.MinimumBetAmount), MinimumBetAmount: ConvertFloat32PtrToCurrency(settings.MinimumBetAmount),
BetDuplicateLimit: ConvertInt64Ptr(settings.BetDuplicateLimit), BetDuplicateLimit: ConvertInt64Ptr(settings.BetDuplicateLimit),
SendEmailOnBetFinish: ConvertBoolPtr(settings.SendEmailOnBetFinish), SendEmailOnBetFinish: ConvertBoolPtr(settings.SendEmailOnBetFinish),
SendSMSOnBetFinish: ConvertBoolPtr(settings.SendSMSOnBetFinish), SendSMSOnBetFinish: ConvertBoolPtr(settings.SendSMSOnBetFinish),
WelcomeBonusActive: ConvertBoolPtr(settings.WelcomeBonusActive),
WelcomeBonusMultiplier: ConvertFloat32Ptr(settings.WelcomeBonusMultiplier),
WelcomeBonusCap: ConvertFloat32PtrToCurrency(settings.WelcomeBonusCap),
WelcomeBonusCount: ConvertInt64Ptr(settings.WelcomeBonusCount),
WelcomeBonusExpire: ConvertInt64Ptr(settings.WelcomeBonusExpire),
} }
} }
// Always make sure to run the validation before converting this // Always make sure to run the validation before converting this
func (vsl *ValidSettingList) ToSettingList() SettingList { func (vsl *ValidSettingList) ToSettingList() SettingList {
return SettingList{ return SettingList{
SMSProvider: SMSProvider(vsl.SMSProvider.Value), SMSProvider: SMSProvider(vsl.SMSProvider.Value),
MaxNumberOfOutcomes: vsl.MaxNumberOfOutcomes.Value, MaxNumberOfOutcomes: vsl.MaxNumberOfOutcomes.Value,
BetAmountLimit: vsl.BetAmountLimit.Value, BetAmountLimit: vsl.BetAmountLimit.Value,
DailyTicketPerIP: vsl.DailyTicketPerIP.Value, DailyTicketPerIP: vsl.DailyTicketPerIP.Value,
TotalWinningLimit: vsl.TotalWinningLimit.Value, TotalWinningLimit: vsl.TotalWinningLimit.Value,
AmountForBetReferral: vsl.AmountForBetReferral.Value, AmountForBetReferral: vsl.AmountForBetReferral.Value,
CashbackAmountCap: vsl.CashbackAmountCap.Value, CashbackAmountCap: vsl.CashbackAmountCap.Value,
DefaultWinningLimit: vsl.DefaultWinningLimit.Value, DefaultWinningLimit: vsl.DefaultWinningLimit.Value,
ReferralRewardAmount: vsl.ReferralRewardAmount.Value, ReferralRewardAmount: vsl.ReferralRewardAmount.Value,
CashbackPercentage: vsl.CashbackPercentage.Value, CashbackPercentage: vsl.CashbackPercentage.Value,
DefaultMaxReferrals: vsl.DefaultMaxReferrals.Value, DefaultMaxReferrals: vsl.DefaultMaxReferrals.Value,
MinimumBetAmount: vsl.MinimumBetAmount.Value, MinimumBetAmount: vsl.MinimumBetAmount.Value,
BetDuplicateLimit: vsl.BetDuplicateLimit.Value, BetDuplicateLimit: vsl.BetDuplicateLimit.Value,
SendEmailOnBetFinish: vsl.SendEmailOnBetFinish.Value, SendEmailOnBetFinish: vsl.SendEmailOnBetFinish.Value,
SendSMSOnBetFinish: vsl.SendSMSOnBetFinish.Value, SendSMSOnBetFinish: vsl.SendSMSOnBetFinish.Value,
WelcomeBonusActive: vsl.WelcomeBonusActive.Value,
WelcomeBonusMultiplier: vsl.WelcomeBonusMultiplier.Value,
WelcomeBonusCap: vsl.WelcomeBonusCap.Value,
WelcomeBonusCount: vsl.WelcomeBonusCount.Value,
WelcomeBonusExpire: vsl.WelcomeBonusExpire.Value,
} }
} }
@ -168,6 +198,8 @@ func (vsl *ValidSettingList) GetInt64SettingsMap() map[string]*ValidInt64 {
"default_winning_limit": &vsl.DefaultWinningLimit, "default_winning_limit": &vsl.DefaultWinningLimit,
"default_max_referrals": &vsl.DefaultMaxReferrals, "default_max_referrals": &vsl.DefaultMaxReferrals,
"bet_duplicate_limit": &vsl.BetDuplicateLimit, "bet_duplicate_limit": &vsl.BetDuplicateLimit,
"welcome_bonus_count": &vsl.WelcomeBonusCount,
"welcome_bonus_expiry": &vsl.WelcomeBonusExpire,
} }
} }
@ -179,6 +211,7 @@ func (vsl *ValidSettingList) GetCurrencySettingsMap() map[string]*ValidCurrency
"cashback_amount_cap": &vsl.CashbackAmountCap, "cashback_amount_cap": &vsl.CashbackAmountCap,
"referral_reward_amount": &vsl.ReferralRewardAmount, "referral_reward_amount": &vsl.ReferralRewardAmount,
"minimum_bet_amount": &vsl.MinimumBetAmount, "minimum_bet_amount": &vsl.MinimumBetAmount,
"welcome_bonus_cap": &vsl.WelcomeBonusCap,
} }
} }
@ -192,12 +225,14 @@ func (vsl *ValidSettingList) GetBoolSettingsMap() map[string]*ValidBool {
return map[string]*ValidBool{ return map[string]*ValidBool{
"send_email_on_bet_finish": &vsl.SendEmailOnBetFinish, "send_email_on_bet_finish": &vsl.SendEmailOnBetFinish,
"send_sms_on_bet_finish": &vsl.SendSMSOnBetFinish, "send_sms_on_bet_finish": &vsl.SendSMSOnBetFinish,
"welcome_bonus_active": &vsl.WelcomeBonusActive,
} }
} }
func (vsl *ValidSettingList) GetFloat32SettingsMap() map[string]*ValidFloat32 { func (vsl *ValidSettingList) GetFloat32SettingsMap() map[string]*ValidFloat32 {
return map[string]*ValidFloat32{ return map[string]*ValidFloat32{
"cashback_percentage": &vsl.CashbackPercentage, "cashback_percentage": &vsl.CashbackPercentage,
"welcome_bonus_multiplier": &vsl.WelcomeBonusMultiplier,
} }
} }

View File

@ -1,10 +1,11 @@
package helpers package helpers
import ( import (
random "crypto/rand"
"fmt" "fmt"
"math/rand/v2"
"github.com/google/uuid" "github.com/google/uuid"
"math/big"
"math/rand/v2"
) )
func GenerateID() string { func GenerateID() string {
@ -24,3 +25,20 @@ func GenerateFastCode() string {
} }
return code return code
} }
func GenerateCashoutID() (string, error) {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
const length int = 13
charLen := big.NewInt(int64(len(chars)))
result := make([]byte, length)
for i := 0; i < length; i++ {
index, err := random.Int(random.Reader, charLen)
if err != nil {
return "", err
}
result[i] = chars[index.Int64()]
}
return string(result), nil
}

View File

@ -17,8 +17,12 @@ func (s *Store) CreateUserBonus(ctx context.Context, bonus domain.CreateBonus) (
return domain.ConvertDBBonus(newBonus), nil return domain.ConvertDBBonus(newBonus), nil
} }
func (s *Store) GetAllUserBonuses(ctx context.Context) ([]domain.UserBonus, error) { func (s *Store) GetAllUserBonuses(ctx context.Context, filter domain.BonusFilter) ([]domain.UserBonus, error) {
bonuses, err := s.queries.GetAllUserBonuses(ctx) bonuses, err := s.queries.GetAllUserBonuses(ctx, dbgen.GetAllUserBonusesParams{
UserID: filter.UserID.ToPG(),
Offset: filter.Offset.ToPG(),
Limit: filter.Limit.ToPG(),
})
if err != nil { if err != nil {
return nil, err return nil, err
@ -27,13 +31,12 @@ func (s *Store) GetAllUserBonuses(ctx context.Context) ([]domain.UserBonus, erro
return domain.ConvertDBBonuses(bonuses), nil return domain.ConvertDBBonuses(bonuses), nil
} }
func (s *Store) GetBonusesByUserID(ctx context.Context, userID int64) ([]domain.UserBonus, error) { func (s *Store) GetBonusCount(ctx context.Context, filter domain.BonusFilter) (int64, error) {
bonuses, err := s.queries.GetBonusesByUserID(ctx, userID) count, err := s.queries.GetBonusCount(ctx, filter.UserID.ToPG())
if err != nil { if err != nil {
return nil, err return 0, err
} }
return count, nil
return domain.ConvertDBBonuses(bonuses), nil
} }
func (s *Store) GetBonusByID(ctx context.Context, bonusID int64) (domain.UserBonus, error) { func (s *Store) GetBonusByID(ctx context.Context, bonusID int64) (domain.UserBonus, error) {
@ -45,6 +48,8 @@ func (s *Store) GetBonusByID(ctx context.Context, bonusID int64) (domain.UserBon
} }
func (s *Store) GetBonusStats(ctx context.Context, filter domain.BonusFilter) (domain.BonusStats, error) { func (s *Store) GetBonusStats(ctx context.Context, filter domain.BonusFilter) (domain.BonusStats, error) {
bonus, err := s.queries.GetBonusStats(ctx, dbgen.GetBonusStatsParams{ bonus, err := s.queries.GetBonusStats(ctx, dbgen.GetBonusStatsParams{
CompanyID: filter.CompanyID.ToPG(), CompanyID: filter.CompanyID.ToPG(),

View File

@ -0,0 +1,247 @@
package bet
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"go.uber.org/zap"
)
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 param.Status {
case domain.OUTCOME_STATUS_WIN:
headline = fmt.Sprintf("Bet #%v Won!", param.BetID)
message = fmt.Sprintf(
"Congratulations! Your bet #%v has won. %.2f has been credited to your wallet.",
param.BetID,
param.WinningAmount.Float32(),
)
case domain.OUTCOME_STATUS_HALF:
headline = fmt.Sprintf("Bet #%v Half-Win", param.BetID)
message = fmt.Sprintf(
"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 = fmt.Sprintf("Bet #%v Refunded", param.BetID)
message = fmt.Sprintf(
"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)
}
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, param SendResultNotificationParam) error {
if err := param.Validate(); err != nil {
return err
}
var headline string
var message string
switch param.Status {
case domain.OUTCOME_STATUS_LOSS:
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)
}
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, 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 = 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)
}
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) 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 = fmt.Sprintf("Processing Error for Bet #%v", betID)
message = "A processing error occurred with this bet. Please review and take corrective action."
default:
return fmt.Errorf("unsupported status: %v", status)
}
super_admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{
Role: string(domain.RoleSuperAdmin),
})
if err != nil {
s.mongoLogger.Error("failed to get super_admin recipients",
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{
Role: string(domain.RoleAdmin),
CompanyID: domain.ValidInt64{
Value: companyID,
Valid: true,
},
})
if err != nil {
s.mongoLogger.Error("failed to get admin recipients",
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
users := append(super_admin_users, admin_users...)
for _, user := range users {
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
}
}
}
return nil
}

View File

@ -97,10 +97,6 @@ func (s *Service) GenerateCashoutID() (string, error) {
for i := 0; i < length; i++ { for i := 0; i < length; i++ {
index, err := rand.Int(rand.Reader, charLen) index, err := rand.Int(rand.Reader, charLen)
if err != nil { if err != nil {
s.mongoLogger.Error("failed to generate random index for cashout ID",
zap.Int("position", i),
zap.Error(err),
)
return "", err return "", err
} }
result[i] = chars[index.Int64()] result[i] = chars[index.Int64()]
@ -957,241 +953,6 @@ func (s *Service) UpdateStatus(ctx context.Context, betId int64, status domain.O
return nil 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 param.Status {
case domain.OUTCOME_STATUS_WIN:
headline = fmt.Sprintf("Bet #%v Won!", param.BetID)
message = fmt.Sprintf(
"Congratulations! Your bet #%v has won. %.2f has been credited to your wallet.",
param.BetID,
param.WinningAmount.Float32(),
)
case domain.OUTCOME_STATUS_HALF:
headline = fmt.Sprintf("Bet #%v Half-Win", param.BetID)
message = fmt.Sprintf(
"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 = fmt.Sprintf("Bet #%v Refunded", param.BetID)
message = fmt.Sprintf(
"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)
}
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, param SendResultNotificationParam) error {
if err := param.Validate(); err != nil {
return err
}
var headline string
var message string
switch param.Status {
case domain.OUTCOME_STATUS_LOSS:
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)
}
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, 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 = 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)
}
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) 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 = fmt.Sprintf("Processing Error for Bet #%v", betID)
message = "A processing error occurred with this bet. Please review and take corrective action."
default:
return fmt.Errorf("unsupported status: %v", status)
}
super_admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{
Role: string(domain.RoleSuperAdmin),
})
if err != nil {
s.mongoLogger.Error("failed to get super_admin recipients",
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{
Role: string(domain.RoleAdmin),
CompanyID: domain.ValidInt64{
Value: companyID,
Valid: true,
},
})
if err != nil {
s.mongoLogger.Error("failed to get admin recipients",
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
users := append(super_admin_users, admin_users...)
for _, user := range users {
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
}
}
}
return nil
}
func (s *Service) CheckBetOutcomeForBet(ctx context.Context, betID int64) (domain.OutcomeStatus, error) { func (s *Service) CheckBetOutcomeForBet(ctx context.Context, betID int64) (domain.OutcomeStatus, error) {
betOutcomes, err := s.betStore.GetBetOutcomeByBetID(ctx, betID) betOutcomes, err := s.betStore.GetBetOutcomeByBetID(ctx, betID)
if err != nil { if err != nil {

View File

@ -0,0 +1,83 @@
package bonus
import (
"context"
"encoding/json"
"fmt"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type SendBonusNotificationParam struct {
BonusID int64
UserID int64
Type domain.BonusType
Amount domain.Currency
SendEmail bool
SendSMS bool
}
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) SendBonusNotification(ctx context.Context, param SendBonusNotificationParam) error {
var headline string
var message string
switch param.Type {
case domain.WelcomeBonus:
headline = "You've been awarded a welcome bonus!"
message = fmt.Sprintf(
"Congratulations! A you've been given %.2f as a welcome bonus for you to bet on.",
param.Amount,
)
default:
return fmt.Errorf("unsupported bonus type: %v", param.Type)
}
for _, channel := range []domain.DeliveryChannel{
domain.DeliveryChannelInApp,
domain.DeliveryChannelEmail,
domain.DeliveryChannelSMS,
} {
if !shouldSend(channel, param.SendEmail, param.SendSMS) {
continue
}
raw, _ := json.Marshal(map[string]any{
"bonus_id": param.BonusID,
"type": param.Type,
})
n := &domain.Notification{
RecipientID: param.UserID,
DeliveryStatus: domain.DeliveryStatusPending,
IsRead: false,
Type: domain.NOTIFICATION_TYPE_BONUS_AWARDED,
Level: domain.NotificationLevelSuccess,
Reciever: domain.NotificationRecieverSideCustomer,
DeliveryChannel: channel,
Payload: domain.NotificationPayload{
Headline: headline,
Message: message,
},
Priority: 2,
Metadata: raw,
}
if err := s.notificationSvc.SendNotification(ctx, n); err != nil {
return err
}
}
return nil
}

View File

@ -8,8 +8,8 @@ import (
type BonusStore interface { type BonusStore interface {
CreateUserBonus(ctx context.Context, bonus domain.CreateBonus) (domain.UserBonus, error) CreateUserBonus(ctx context.Context, bonus domain.CreateBonus) (domain.UserBonus, error)
GetAllUserBonuses(ctx context.Context) ([]domain.UserBonus, error) GetAllUserBonuses(ctx context.Context, filter domain.BonusFilter) ([]domain.UserBonus, error)
GetBonusesByUserID(ctx context.Context, userID int64) ([]domain.UserBonus, error) GetBonusCount(ctx context.Context, filter domain.BonusFilter) (int64, error)
GetBonusByID(ctx context.Context, bonusID int64) (domain.UserBonus, error) GetBonusByID(ctx context.Context, bonusID int64) (domain.UserBonus, error)
GetBonusStats(ctx context.Context, filter domain.BonusFilter) (domain.BonusStats, error) GetBonusStats(ctx context.Context, filter domain.BonusFilter) (domain.BonusStats, error)
UpdateUserBonus(ctx context.Context, bonusID int64, IsClaimed bool) error UpdateUserBonus(ctx context.Context, bonusID int64, IsClaimed bool) error

View File

@ -3,29 +3,32 @@ package bonus
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"math" "math"
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"go.uber.org/zap" "go.uber.org/zap"
) )
type Service struct { type Service struct {
bonusStore BonusStore bonusStore BonusStore
walletSvc *wallet.Service walletSvc *wallet.Service
settingSvc *settings.Service settingSvc *settings.Service
mongoLogger *zap.Logger notificationSvc *notificationservice.Service
mongoLogger *zap.Logger
} }
func NewService(bonusStore BonusStore, walletSvc *wallet.Service, settingSvc *settings.Service, mongoLogger *zap.Logger) *Service { func NewService(bonusStore BonusStore, walletSvc *wallet.Service, settingSvc *settings.Service, notificationSvc *notificationservice.Service, mongoLogger *zap.Logger) *Service {
return &Service{ return &Service{
bonusStore: bonusStore, bonusStore: bonusStore,
walletSvc: walletSvc, walletSvc: walletSvc,
settingSvc: settingSvc, settingSvc: settingSvc,
mongoLogger: mongoLogger, notificationSvc: notificationSvc,
mongoLogger: mongoLogger,
} }
} }
@ -34,7 +37,7 @@ var (
ErrWelcomeBonusCountReached = errors.New("welcome bonus max deposit count reached") ErrWelcomeBonusCountReached = errors.New("welcome bonus max deposit count reached")
) )
func (s *Service) ProcessWelcomeBonus(ctx context.Context, amount domain.Currency, companyID int64, userID int64) error { func (s *Service) CreateWelcomeBonus(ctx context.Context, amount domain.Currency, companyID int64, userID int64) error {
settingsList, err := s.settingSvc.GetOverrideSettingsList(ctx, companyID) settingsList, err := s.settingSvc.GetOverrideSettingsList(ctx, companyID)
if err != nil { if err != nil {
s.mongoLogger.Error("Failed to get settings", s.mongoLogger.Error("Failed to get settings",
@ -65,11 +68,10 @@ func (s *Service) ProcessWelcomeBonus(ctx context.Context, amount domain.Currenc
newBalance := math.Min(float64(amount)*float64(settingsList.WelcomeBonusMultiplier), float64(settingsList.WelcomeBonusCap)) newBalance := math.Min(float64(amount)*float64(settingsList.WelcomeBonusMultiplier), float64(settingsList.WelcomeBonusCap))
_, err = s.CreateUserBonus(ctx, domain.CreateBonus{ bonus, err := s.CreateUserBonus(ctx, domain.CreateBonus{
Name: "Welcome Bonus", Name: "Welcome Bonus",
Description: "Awarded when the user logged in for the first time", Description: fmt.Sprintf("Awarded for deposit number (%v / %v)", stats.TotalDeposits, settingsList.WelcomeBonusCount),
UserID: userID, UserID: userID,
BonusCode: helpers.GenerateFastCode(),
RewardAmount: domain.Currency(newBalance), RewardAmount: domain.Currency(newBalance),
ExpiresAt: time.Now().Add(time.Duration(settingsList.WelcomeBonusExpire) * 24 * time.Hour), ExpiresAt: time.Now().Add(time.Duration(settingsList.WelcomeBonusExpire) * 24 * time.Hour),
}) })
@ -78,26 +80,73 @@ func (s *Service) ProcessWelcomeBonus(ctx context.Context, amount domain.Currenc
return err return err
} }
// TODO: Add a claim function that adds to the static wallet when the user inputs his bonus code err = s.SendBonusNotification(ctx, SendBonusNotificationParam{
// _, err = s.walletSvc.AddToWallet(ctx, wallet.StaticID, domain.ToCurrency(float32(newBalance)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, BonusID: bonus.ID,
// fmt.Sprintf("Added %v to static wallet because of deposit bonus using multiplier %v", newBalance, settingsList.WelcomeBonusMultiplier), UserID: userID,
// ) Type: domain.DepositBonus,
// if err != nil { Amount: domain.Currency(newBalance),
// return err SendEmail: true,
// } SendSMS: false,
})
if err != nil {
return err
}
return nil return nil
}
var (
ErrBonusIsAlreadyClaimed = errors.New("bonus is already claimed")
ErrBonusUserIDNotMatch = errors.New("bonus user id is not a match")
)
func (s *Service) ProcessBonusClaim(ctx context.Context, bonusID, userID int64) error {
bonus, err := s.GetBonusByID(ctx, bonusID)
if err != nil {
return err
}
if bonus.UserID != userID {
}
if bonus.IsClaimed {
return ErrBonusIsAlreadyClaimed
}
wallet, err := s.walletSvc.GetCustomerWallet(ctx, bonus.UserID)
if err != nil {
return err
}
_, err = s.walletSvc.AddToWallet(
ctx, wallet.StaticID, bonus.RewardAmount,
domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{},
fmt.Sprintf("Added %v to bonus wallet due to %v", bonus.RewardAmount, bonus.Type),
)
if err != nil {
return err
}
if err := s.UpdateUserBonus(ctx, bonusID, true); err != nil {
return err
}
return nil
} }
func (s *Service) CreateUserBonus(ctx context.Context, bonus domain.CreateBonus) (domain.UserBonus, error) { func (s *Service) CreateUserBonus(ctx context.Context, bonus domain.CreateBonus) (domain.UserBonus, error) {
return s.bonusStore.CreateUserBonus(ctx, bonus) return s.bonusStore.CreateUserBonus(ctx, bonus)
} }
func (s *Service) GetAllUserBonuses(ctx context.Context) ([]domain.UserBonus, error) { func (s *Service) GetAllUserBonuses(ctx context.Context, filter domain.BonusFilter) ([]domain.UserBonus, error) {
return s.bonusStore.GetAllUserBonuses(ctx) return s.bonusStore.GetAllUserBonuses(ctx, filter)
} }
func (s *Service) GetBonusesByUserID(ctx context.Context, userID int64) ([]domain.UserBonus, error) {
return s.bonusStore.GetBonusesByUserID(ctx, userID) func (s *Service) GetBonusCount(ctx context.Context, filter domain.BonusFilter) (int64, error) {
return s.bonusStore.GetBonusCount(ctx, filter)
} }
func (s *Service) GetBonusByID(ctx context.Context, bonusID int64) (domain.UserBonus, error) { func (s *Service) GetBonusByID(ctx context.Context, bonusID int64) (domain.UserBonus, error) {
return s.bonusStore.GetBonusByID(ctx, bonusID) return s.bonusStore.GetBonusByID(ctx, bonusID)

View File

@ -1,475 +0,0 @@
package notificationservice
import (
"context"
"encoding/json"
"errors"
"log/slog"
"sync"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/ws"
afro "github.com/amanuelabay/afrosms-go"
"github.com/gorilla/websocket"
"github.com/redis/go-redis/v9"
)
type Service struct {
repo repository.NotificationRepository
Hub *ws.NotificationHub
// notificationStore
connections sync.Map
notificationCh chan *domain.Notification
stopCh chan struct{}
config *config.Config
logger *slog.Logger
redisClient *redis.Client
}
func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *config.Config) *Service {
hub := ws.NewNotificationHub()
rdb := redis.NewClient(&redis.Options{
Addr: cfg.RedisAddr, // e.g., "redis:6379"
})
svc := &Service{
repo: repo,
Hub: hub,
logger: logger,
connections: sync.Map{},
notificationCh: make(chan *domain.Notification, 1000),
stopCh: make(chan struct{}),
config: cfg,
redisClient: rdb,
}
go hub.Run()
go svc.startWorker()
go svc.startRetryWorker()
go svc.RunRedisSubscriber(context.Background())
return svc
}
func (s *Service) addConnection(recipientID int64, c *websocket.Conn) {
if c == nil {
s.logger.Warn("[NotificationSvc.AddConnection] Attempted to add nil WebSocket connection", "recipientID", recipientID)
return
}
s.connections.Store(recipientID, c)
s.logger.Info("[NotificationSvc.AddConnection] Added WebSocket connection", "recipientID", recipientID)
}
func (s *Service) SendNotification(ctx context.Context, notification *domain.Notification) error {
notification.ID = helpers.GenerateID()
notification.Timestamp = time.Now()
notification.DeliveryStatus = domain.DeliveryStatusPending
created, err := s.repo.CreateNotification(ctx, notification)
if err != nil {
s.logger.Error("[NotificationSvc.SendNotification] Failed to create notification", "id", notification.ID, "error", err)
return err
}
notification = created
if notification.DeliveryChannel == domain.DeliveryChannelInApp {
s.Hub.Broadcast <- map[string]interface{}{
"type": "CREATED_NOTIFICATION",
"recipient_id": notification.RecipientID,
"payload": notification,
}
}
select {
case s.notificationCh <- notification:
default:
s.logger.Warn("[NotificationSvc.SendNotification] Notification channel full, dropping notification", "id", notification.ID)
}
return nil
}
func (s *Service) MarkAsRead(ctx context.Context, notificationIDs []string, recipientID int64) error {
for _, notificationID := range notificationIDs {
_, err := s.repo.UpdateNotificationStatus(ctx, notificationID, string(domain.DeliveryStatusSent), true, nil)
if err != nil {
s.logger.Error("[NotificationSvc.MarkAsRead] Failed to mark notification as read", "notificationID", notificationID, "recipientID", recipientID, "error", err)
return err
}
// count, err := s.repo.CountUnreadNotifications(ctx, recipientID)
// if err != nil {
// s.logger.Error("[NotificationSvc.MarkAsRead] Failed to count unread notifications", "recipientID", recipientID, "error", err)
// return err
// }
// s.Hub.Broadcast <- map[string]interface{}{
// "type": "COUNT_NOT_OPENED_NOTIFICATION",
// "recipient_id": recipientID,
// "payload": map[string]int{
// "not_opened_notifications_count": int(count),
// },
// }
s.logger.Info("[NotificationSvc.MarkAsRead] Notification marked as read", "notificationID", notificationID, "recipientID", recipientID)
}
return nil
}
func (s *Service) ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error) {
notifications, err := s.repo.ListNotifications(ctx, recipientID, limit, offset)
if err != nil {
s.logger.Error("[NotificationSvc.ListNotifications] Failed to list notifications", "recipientID", recipientID, "limit", limit, "offset", offset, "error", err)
return nil, err
}
s.logger.Info("[NotificationSvc.ListNotifications] Successfully listed notifications", "recipientID", recipientID, "count", len(notifications))
return notifications, nil
}
func (s *Service) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) {
notifications, err := s.repo.GetAllNotifications(ctx, limit, offset)
if err != nil {
s.logger.Error("[NotificationSvc.ListNotifications] Failed to get all notifications")
return nil, err
}
s.logger.Info("[NotificationSvc.ListNotifications] Successfully retrieved all notifications", "count", len(notifications))
return notifications, nil
}
func (s *Service) ConnectWebSocket(ctx context.Context, recipientID int64, c *websocket.Conn) error {
s.addConnection(recipientID, c)
s.logger.Info("[NotificationSvc.ConnectWebSocket] WebSocket connection established", "recipientID", recipientID)
return nil
}
func (s *Service) DisconnectWebSocket(recipientID int64) {
if conn, loaded := s.connections.LoadAndDelete(recipientID); loaded {
conn.(*websocket.Conn).Close()
s.logger.Info("[NotificationSvc.DisconnectWebSocket] Disconnected WebSocket", "recipientID", recipientID)
}
}
func (s *Service) SendSMS(ctx context.Context, recipientID int64, message string) error {
s.logger.Info("[NotificationSvc.SendSMS] SMS notification requested", "recipientID", recipientID, "message", message)
apiKey := s.config.AFRO_SMS_API_KEY
senderName := s.config.AFRO_SMS_SENDER_NAME
receiverPhone := s.config.AFRO_SMS_RECEIVER_PHONE_NUMBER
hostURL := s.config.ADRO_SMS_HOST_URL
endpoint := "/api/send"
request := afro.GetRequest(apiKey, endpoint, hostURL)
request.Method = "GET"
request.Sender(senderName)
request.To(receiverPhone, message)
response, err := afro.MakeRequestWithContext(ctx, request)
if err != nil {
s.logger.Error("[NotificationSvc.SendSMS] Failed to send SMS", "recipientID", recipientID, "error", err)
return err
}
if response["acknowledge"] == "success" {
s.logger.Info("[NotificationSvc.SendSMS] SMS sent successfully", "recipientID", recipientID)
} else {
s.logger.Error("[NotificationSvc.SendSMS] Failed to send SMS", "recipientID", recipientID, "response", response["response"])
return errors.New("SMS delivery failed: " + response["response"].(string))
}
return nil
}
func (s *Service) SendEmail(ctx context.Context, recipientID int64, subject, message string) error {
s.logger.Info("[NotificationSvc.SendEmail] Email notification requested", "recipientID", recipientID, "subject", subject)
return nil
}
func (s *Service) startWorker() {
for {
select {
case notification := <-s.notificationCh:
s.handleNotification(notification)
case <-s.stopCh:
s.logger.Info("[NotificationSvc.StartWorker] Worker stopped")
return
}
}
}
func (s *Service) ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) {
return s.repo.ListRecipientIDs(ctx, receiver)
}
func (s *Service) handleNotification(notification *domain.Notification) {
ctx := context.Background()
switch notification.DeliveryChannel {
case domain.DeliveryChannelSMS:
err := s.SendSMS(ctx, notification.RecipientID, notification.Payload.Message)
if err != nil {
notification.DeliveryStatus = domain.DeliveryStatusFailed
} else {
notification.DeliveryStatus = domain.DeliveryStatusSent
}
case domain.DeliveryChannelEmail:
err := s.SendEmail(ctx, notification.RecipientID, notification.Payload.Headline, notification.Payload.Message)
if err != nil {
notification.DeliveryStatus = domain.DeliveryStatusFailed
} else {
notification.DeliveryStatus = domain.DeliveryStatusSent
}
default:
if notification.DeliveryChannel != domain.DeliveryChannelInApp {
s.logger.Warn("[NotificationSvc.HandleNotification] Unsupported delivery channel", "channel", notification.DeliveryChannel)
notification.DeliveryStatus = domain.DeliveryStatusFailed
}
}
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil {
s.logger.Error("[NotificationSvc.HandleNotification] Failed to update notification status", "id", notification.ID, "error", err)
}
}
func (s *Service) startRetryWorker() {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
s.retryFailedNotifications()
case <-s.stopCh:
s.logger.Info("[NotificationSvc.StartRetryWorker] Retry worker stopped")
return
}
}
}
func (s *Service) retryFailedNotifications() {
ctx := context.Background()
failedNotifications, err := s.repo.ListFailedNotifications(ctx, 100)
if err != nil {
s.logger.Error("[NotificationSvc.RetryFailedNotifications] Failed to list failed notifications", "error", err)
return
}
for _, n := range failedNotifications {
notification := &n
go func(notification *domain.Notification) {
for attempt := 0; attempt < 3; attempt++ {
time.Sleep(time.Duration(attempt) * time.Second)
switch notification.DeliveryChannel {
case domain.DeliveryChannelSMS:
if err := s.SendSMS(ctx, notification.RecipientID, notification.Payload.Message); err == nil {
notification.DeliveryStatus = domain.DeliveryStatusSent
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil {
s.logger.Error("[NotificationSvc.RetryFailedNotifications] Failed to update after retry", "id", notification.ID, "error", err)
}
s.logger.Info("[NotificationSvc.RetryFailedNotifications] Successfully retried notification", "id", notification.ID)
return
}
case domain.DeliveryChannelEmail:
if err := s.SendEmail(ctx, notification.RecipientID, notification.Payload.Headline, notification.Payload.Message); err == nil {
notification.DeliveryStatus = domain.DeliveryStatusSent
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil {
s.logger.Error("[NotificationSvc.RetryFailedNotifications] Failed to update after retry", "id", notification.ID, "error", err)
}
s.logger.Info("[NotificationSvc.RetryFailedNotifications] Successfully retried notification", "id", notification.ID)
return
}
}
}
s.logger.Error("[NotificationSvc.RetryFailedNotifications] Max retries reached for notification", "id", notification.ID)
}(notification)
}
}
func (s *Service) CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) {
return s.repo.CountUnreadNotifications(ctx, recipient_id)
}
// func (s *Service) GetNotificationCounts(ctx context.Context, filter domain.ReportFilter) (total, read, unread int64, err error){
// return s.repo.Get(ctx, filter)
// }
func (s *Service) RunRedisSubscriber(ctx context.Context) {
pubsub := s.redisClient.Subscribe(ctx, "live_metrics")
defer pubsub.Close()
ch := pubsub.Channel()
for msg := range ch {
var parsed map[string]interface{}
if err := json.Unmarshal([]byte(msg.Payload), &parsed); err != nil {
s.logger.Error("invalid Redis message format", "payload", msg.Payload, "error", err)
continue
}
eventType, _ := parsed["type"].(string)
payload := parsed["payload"]
recipientID, hasRecipient := parsed["recipient_id"]
recipientType, _ := parsed["recipient_type"].(string)
message := map[string]interface{}{
"type": eventType,
"payload": payload,
}
if hasRecipient {
message["recipient_id"] = recipientID
message["recipient_type"] = recipientType
}
s.Hub.Broadcast <- message
}
}
func (s *Service) UpdateLiveWalletMetrics(ctx context.Context, companies []domain.GetCompany, branches []domain.BranchWallet) error {
const key = "live_metrics"
companyBalances := make([]domain.CompanyWalletBalance, 0, len(companies))
for _, c := range companies {
companyBalances = append(companyBalances, domain.CompanyWalletBalance{
CompanyID: c.ID,
CompanyName: c.Name,
Balance: float64(c.WalletBalance.Float32()),
})
}
branchBalances := make([]domain.BranchWalletBalance, 0, len(branches))
for _, b := range branches {
branchBalances = append(branchBalances, domain.BranchWalletBalance{
BranchID: b.ID,
BranchName: b.Name,
CompanyID: b.CompanyID,
Balance: float64(b.Balance.Float32()),
})
}
payload := domain.LiveWalletMetrics{
Timestamp: time.Now(),
CompanyBalances: companyBalances,
BranchBalances: branchBalances,
}
updatedData, err := json.Marshal(payload)
if err != nil {
return err
}
if err := s.redisClient.Set(ctx, key, updatedData, 0).Err(); err != nil {
return err
}
if err := s.redisClient.Publish(ctx, key, updatedData).Err(); err != nil {
return err
}
return nil
}
func (s *Service) GetLiveMetrics(ctx context.Context) (domain.LiveMetric, error) {
const key = "live_metrics"
var metric domain.LiveMetric
val, err := s.redisClient.Get(ctx, key).Result()
if err == redis.Nil {
// Key does not exist yet, return zero-valued struct
return domain.LiveMetric{}, nil
} else if err != nil {
return domain.LiveMetric{}, err
}
if err := json.Unmarshal([]byte(val), &metric); err != nil {
return domain.LiveMetric{}, err
}
return metric, nil
}
func (s *Service) UpdateLiveMetricForWallet(ctx context.Context, wallet domain.Wallet) {
var (
payload domain.LiveWalletMetrics
event map[string]interface{}
key = "live_metrics"
)
// Try company first
company, companyErr := s.GetCompanyByWalletID(ctx, wallet.ID)
if companyErr == nil {
payload = domain.LiveWalletMetrics{
Timestamp: time.Now(),
CompanyBalances: []domain.CompanyWalletBalance{{
CompanyID: company.ID,
CompanyName: company.Name,
Balance: float64(wallet.Balance),
}},
BranchBalances: []domain.BranchWalletBalance{},
}
event = map[string]interface{}{
"type": "LIVE_WALLET_METRICS_UPDATE",
"recipient_id": company.ID,
"recipient_type": "company",
"payload": payload,
}
} else {
// Try branch next
branch, branchErr := s.GetBranchByWalletID(ctx, wallet.ID)
if branchErr == nil {
payload = domain.LiveWalletMetrics{
Timestamp: time.Now(),
CompanyBalances: []domain.CompanyWalletBalance{},
BranchBalances: []domain.BranchWalletBalance{{
BranchID: branch.ID,
BranchName: branch.Name,
CompanyID: branch.CompanyID,
Balance: float64(wallet.Balance),
}},
}
event = map[string]interface{}{
"type": "LIVE_WALLET_METRICS_UPDATE",
"recipient_id": branch.ID,
"recipient_type": "branch",
"payload": payload,
}
} else {
// Neither company nor branch matched this wallet
s.logger.Warn("wallet not linked to any company or branch", "walletID", wallet.ID)
return
}
}
// Save latest metric to Redis
if jsonBytes, err := json.Marshal(payload); err == nil {
s.redisClient.Set(ctx, key, jsonBytes, 0)
} else {
s.logger.Error("failed to marshal wallet metrics payload", "walletID", wallet.ID, "err", err)
}
// Publish via Redis
if jsonEvent, err := json.Marshal(event); err == nil {
s.redisClient.Publish(ctx, key, jsonEvent)
} else {
s.logger.Error("failed to marshal event payload", "walletID", wallet.ID, "err", err)
}
// Broadcast over WebSocket
s.Hub.Broadcast <- event
}
func (s *Service) GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error) {
return s.GetCompanyByWalletID(ctx, walletID)
}
func (s *Service) GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error) {
return s.GetBranchByWalletID(ctx, walletID)
}

View File

@ -22,23 +22,23 @@ var (
type Service struct { type Service struct {
virtualGameSvc virtualgameservice.VirtualGameService virtualGameSvc virtualgameservice.VirtualGameService
repo repository.VirtualGameRepository repo repository.VirtualGameRepository
client *Client client *Client
walletSvc *wallet.Service walletSvc *wallet.Service
transfetStore wallet.TransferStore transfetStore wallet.TransferStore
cfg *config.Config cfg *config.Config
} }
func New(virtualGameSvc virtualgameservice.VirtualGameService,repo repository.VirtualGameRepository,client *Client, walletSvc *wallet.Service, transferStore wallet.TransferStore, cfg *config.Config) *Service { func New(virtualGameSvc virtualgameservice.VirtualGameService, repo repository.VirtualGameRepository, client *Client, walletSvc *wallet.Service, transferStore wallet.TransferStore, cfg *config.Config) *Service {
return &Service{ return &Service{
virtualGameSvc: virtualGameSvc, virtualGameSvc: virtualGameSvc,
repo: repo, repo: repo,
client: client, client: client,
walletSvc: walletSvc, walletSvc: walletSvc,
transfetStore: transferStore, transfetStore: transferStore,
cfg: cfg, cfg: cfg,
} }
} }
func (s *Service) GetProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) { func (s *Service) GetProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) {
// Always mirror request body fields into sigParams // Always mirror request body fields into sigParams
@ -80,6 +80,8 @@ func (s *Service) GetGames(ctx context.Context, req domain.GameListRequest) ([]d
sigParams := map[string]any{ sigParams := map[string]any{
"brandId": req.BrandID, "brandId": req.BrandID,
"providerId": req.ProviderID, "providerId": req.ProviderID,
"size": req.Size,
"page": req.Page,
} }
// 3. Call external API // 3. Call external API
@ -128,7 +130,6 @@ func (s *Service) StartGame(ctx context.Context, req domain.GameStartRequest) (*
return &res, nil return &res, nil
} }
func (s *Service) StartDemoGame(ctx context.Context, req domain.DemoGameRequest) (*domain.GameStartResponse, error) { func (s *Service) StartDemoGame(ctx context.Context, req domain.DemoGameRequest) (*domain.GameStartResponse, error) {
// 1. Check if provider is enabled in DB // 1. Check if provider is enabled in DB
// provider, err := s.repo.GetVirtualGameProviderByID(ctx, req.ProviderID) // provider, err := s.repo.GetVirtualGameProviderByID(ctx, req.ProviderID)
@ -160,7 +161,6 @@ func (s *Service) StartDemoGame(ctx context.Context, req domain.DemoGameRequest)
return &res, nil return &res, nil
} }
func (s *Service) GetBalance(ctx context.Context, req domain.BalanceRequest) (*domain.BalanceResponse, error) { func (s *Service) GetBalance(ctx context.Context, req domain.BalanceRequest) (*domain.BalanceResponse, error) {
// Retrieve player's real balance from wallet Service // Retrieve player's real balance from wallet Service
playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64) playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64)

View File

@ -26,32 +26,32 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
spec string spec string
task func() task func()
}{ }{
// { {
// spec: "0 0 * * * *", // Every 1 hour spec: "0 0 * * * *", // Every 1 hour
// task: func() { task: func() {
// mongoLogger.Info("Began fetching upcoming events cron task") mongoLogger.Info("Began fetching upcoming events cron task")
// if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
// mongoLogger.Error("Failed to fetch upcoming events", mongoLogger.Error("Failed to fetch upcoming events",
// zap.Error(err), zap.Error(err),
// ) )
// } else { } else {
// mongoLogger.Info("Completed fetching upcoming events without errors") mongoLogger.Info("Completed fetching upcoming events without errors")
// } }
// }, },
// }, },
// { {
// spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events) spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events)
// task: func() { task: func() {
// mongoLogger.Info("Began fetching non live odds cron task") mongoLogger.Info("Began fetching non live odds cron task")
// if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
// mongoLogger.Error("Failed to fetch non live odds", mongoLogger.Error("Failed to fetch non live odds",
// zap.Error(err), zap.Error(err),
// ) )
// } else { } else {
// mongoLogger.Info("Completed fetching non live odds without errors") mongoLogger.Info("Completed fetching non live odds without errors")
// } }
// }, },
// }, },
{ {
spec: "0 */5 * * * *", // Every 5 Minutes spec: "0 */5 * * * *", // Every 5 Minutes
task: func() { task: func() {

View File

@ -1,12 +1,13 @@
package handlers package handlers
// import ( import (
// "time" "strconv"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
// "github.com/gofiber/fiber/v2" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
// "go.uber.org/zap" "github.com/gofiber/fiber/v2"
// ) "go.uber.org/zap"
)
// func (h *Handler) CreateBonusMultiplier(c *fiber.Ctx) error { // func (h *Handler) CreateBonusMultiplier(c *fiber.Ctx) error {
// var req struct { // var req struct {
@ -96,3 +97,110 @@ package handlers
// return response.WriteJSON(c, fiber.StatusOK, "Updated bonus multiplier successfully", nil, nil) // return response.WriteJSON(c, fiber.StatusOK, "Updated bonus multiplier successfully", nil, nil)
// } // }
func (h *Handler) GetBonusesByUserID(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 {
h.InternalServerErrorLogger().Error("Invalid user ID in context",
zap.Int64("userID", userID),
)
return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification")
}
page := c.QueryInt("page", 1)
pageSize := c.QueryInt("page_size", 10)
limit := domain.ValidInt{
Value: pageSize,
Valid: true,
}
offset := domain.ValidInt{
Value: page - 1,
Valid: true,
}
filter := domain.BonusFilter{
UserID: domain.ValidInt64{
Value: userID,
Valid: true,
},
Limit: limit,
Offset: offset,
}
bonuses, err := h.bonusSvc.GetAllUserBonuses(c.Context(), filter)
if err != nil {
h.InternalServerErrorLogger().Error("Failed to bonus by userID", zap.Int64("userId", userID))
return fiber.NewError(fiber.StatusInternalServerError, "failed to get bonus by user ID")
}
count, err := h.bonusSvc.GetBonusCount(c.Context(), filter)
if err != nil {
h.InternalServerErrorLogger().Error("Failed to get bonus count", zap.Int64("userId", userID))
return fiber.NewError(fiber.StatusInternalServerError, "failed to get bonus count by user ID")
}
res := domain.ConvertToBonusResList(bonuses)
return response.WritePaginatedJSON(c, fiber.StatusOK, "Fetched User Bonuses", res, nil, page, int(count))
}
func (h *Handler) GetBonusStats(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 {
h.InternalServerErrorLogger().Error("Invalid user ID in context",
zap.Int64("userID", userID),
)
return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification")
}
stats, err := h.bonusSvc.GetBonusStats(c.Context(), domain.BonusFilter{
UserID: domain.ValidInt64{
Value: userID,
Valid: true,
},
})
if err != nil {
h.InternalServerErrorLogger().Error("Failed to get bonus stats",
zap.Int64("userID", userID),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get bonus stats")
}
res := domain.ConvertToBonusStatsRes(stats)
return response.WriteJSON(c, fiber.StatusOK, "Get Bonus Stats", res, nil)
}
// bonus/:id/claim
func (h *Handler) ClaimBonus(c *fiber.Ctx) error {
bonusIDParam := c.Params("id")
bonusID, err := strconv.ParseInt(bonusIDParam, 10, 64)
if err != nil {
h.BadRequestLogger().Error("Invalid bonus ID",
zap.Int64("bonusID", bonusID),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid bonus id")
}
userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 {
h.InternalServerErrorLogger().Error("Invalid user ID in context",
zap.Int64("userID", userID),
)
return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification")
}
if err := h.bonusSvc.ProcessBonusClaim(c.Context(), bonusID, userID); err != nil {
h.InternalServerErrorLogger().Error("Failed to update bonus claim",
zap.Int64("userID", userID),
zap.Int64("bonusID", bonusID),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update bonus claim")
}
return response.WriteJSON(c, fiber.StatusOK, "Bonus has successfully been claimed", nil, nil)
}

View File

@ -206,7 +206,9 @@ func (a *App) initAppRoutes() {
a.fiber.Get("/raffle-ticket/unsuspend/:id", a.authMiddleware, h.UnSuspendRaffleTicket) a.fiber.Get("/raffle-ticket/unsuspend/:id", a.authMiddleware, h.UnSuspendRaffleTicket)
// Bonus Routes // Bonus Routes
// groupV1.Get("/bonus", a.authMiddleware, h.GetBonusMultiplier) groupV1.Get("/bonus", a.authMiddleware, h.GetBonusesByUserID)
groupV1.Get("/bonus/stats", a.authMiddleware, h.GetBonusStats)
groupV1.Post("/bonus/claim/:id", a.authMiddleware, h.ClaimBonus)
// groupV1.Post("/bonus/create", a.authMiddleware, h.CreateBonusMultiplier) // groupV1.Post("/bonus/create", a.authMiddleware, h.CreateBonusMultiplier)
// groupV1.Put("/bonus/update", a.authMiddleware, h.UpdateBonusMultiplier) // groupV1.Put("/bonus/update", a.authMiddleware, h.UpdateBonusMultiplier)