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)
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, walletSvc, settingSvc, domain.MongoDBLogger)
bonusSvc := bonus.NewService(store, walletSvc, settingSvc, notificationSvc, domain.MongoDBLogger)
referalRepo := repository.NewReferralRepository(store)
vitualGameRepo := repository.NewVirtualGameRepository(store)
recommendationRepo := repository.NewRecommendationRepository(store)

View File

@ -88,8 +88,14 @@ VALUES ('sms_provider', 'afro_message'),
('cashback_percentage', '0.2'),
('default_max_referrals', '15'),
('minimum_bet_amount', '100'),
('bet_duplicate_limit', '5'),
('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
INSERT INTO users (
id,

View File

@ -457,11 +457,12 @@ CREATE TABLE user_bonuses (
id BIGINT NOT NULL,
name TEXT NOT NULL,
description TEXT NOT NULL,
type 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,
claimed_at TIMESTAMP NOT NULL,
created_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_customer ON direct_deposits (customer_id);
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
SELECT companies.*,
wallets.balance,
@ -526,22 +543,6 @@ CREATE TABLE IF NOT EXISTS supported_operations (
name 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
SELECT bets.*,
CONCAT (users.first_name, ' ', users.last_name) AS full_name,

View File

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

View File

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

View File

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

View File

@ -750,11 +750,12 @@ type UserBonuse struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Type string `json:"type"`
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"`
ClaimedAt pgtype.Timestamp `json:"claimed_at"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
}

View File

@ -7,12 +7,19 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
type BonusType string
var (
WelcomeBonus BonusType = "welcome_bonus"
DepositBonus BonusType = "deposit_bonus"
)
type UserBonus struct {
ID int64
Name string
Description string
UserID int64
BonusCode string
Type BonusType
RewardAmount Currency
IsClaimed bool
ExpiresAt time.Time
@ -25,7 +32,7 @@ type UserBonusRes struct {
Name string `json:"name"`
Description string `json:"description"`
UserID int64 `json:"user_id"`
BonusCode string `json:"bonus_code"`
Type BonusType `json:"type"`
RewardAmount float32 `json:"reward_amount"`
IsClaimed bool `json:"is_claimed"`
ExpiresAt time.Time `json:"expires_at"`
@ -38,8 +45,8 @@ func ConvertToBonusRes(bonus UserBonus) UserBonusRes {
ID: bonus.ID,
Name: bonus.Name,
Description: bonus.Description,
Type: bonus.Type,
UserID: bonus.UserID,
BonusCode: bonus.BonusCode,
RewardAmount: bonus.RewardAmount.Float32(),
IsClaimed: bonus.IsClaimed,
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 {
Name string
Description string
Type BonusType
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"`
}
// type CreateBonusReq struct {
// Name string `json:"name"`
// Description string `json:"description"`
// Type BonusType `json:"type"`
// UserID int64 `json:"user_id"`
// 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 ConvertCreateBonusReq(bonus CreateBonusReq, companyID int64) CreateBonus {
// return CreateBonus{
// Name: bonus.Name,
// Description: bonus.Description,
// Type: bonus.Type,
// UserID: bonus.UserID,
// RewardAmount: ToCurrency(bonus.RewardAmount),
// ExpiresAt: bonus.ExpiresAt,
// }
// }
func ConvertCreateBonus(bonus CreateBonus) dbgen.CreateUserBonusParams {
return dbgen.CreateUserBonusParams{
Name: bonus.Name,
Description: bonus.Description,
Type: string(bonus.Type),
UserID: bonus.UserID,
BonusCode: bonus.BonusCode,
RewardAmount: int64(bonus.RewardAmount),
ExpiresAt: pgtype.Timestamp{
Time: bonus.ExpiresAt,
@ -96,8 +113,9 @@ func ConvertDBBonus(bonus dbgen.UserBonuse) UserBonus {
ID: bonus.ID,
Name: bonus.Name,
Description: bonus.Description,
Type: BonusType(bonus.Type),
UserID: bonus.UserID,
BonusCode: bonus.BonusCode,
RewardAmount: Currency(bonus.RewardAmount),
IsClaimed: bonus.IsClaimed,
ExpiresAt: bonus.ExpiresAt.Time,
@ -117,6 +135,8 @@ func ConvertDBBonuses(bonuses []dbgen.UserBonuse) []UserBonus {
type BonusFilter struct {
UserID ValidInt64
CompanyID ValidInt64
Limit ValidInt
Offset ValidInt
}
type BonusStats struct {
@ -126,6 +146,22 @@ type BonusStats struct {
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 {
return BonusStats{
TotalBonus: stats.TotalBonuses,

View File

@ -32,6 +32,7 @@ const (
NOTIFICATION_TYPE_BET_RESULT NotificationType = "bet_result"
NOTIFICATION_TYPE_TRANSFER_REJECTED NotificationType = "transfer_rejected"
NOTIFICATION_TYPE_APPROVAL_REQUIRED NotificationType = "approval_required"
NOTIFICATION_TYPE_BONUS_AWARDED NotificationType = "bonus_awarded"
NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
NotificationRecieverSideCustomer NotificationRecieverSide = "customer"

View File

@ -54,6 +54,11 @@ type SettingListRes struct {
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 float32 `json:"welcome_bonus_cap"`
WelcomeBonusCount int64 `json:"welcome_bonus_count"`
WelcomeBonusExpire int64 `json:"welcome_bonus_expiry"`
}
func ConvertSettingListRes(settings SettingList) SettingListRes {
@ -73,6 +78,11 @@ func ConvertSettingListRes(settings SettingList) SettingListRes {
BetDuplicateLimit: settings.BetDuplicateLimit,
SendEmailOnBetFinish: settings.SendEmailOnBetFinish,
SendSMSOnBetFinish: settings.SendSMSOnBetFinish,
WelcomeBonusActive: settings.WelcomeBonusActive,
WelcomeBonusMultiplier: settings.WelcomeBonusMultiplier,
WelcomeBonusCap: settings.WelcomeBonusCap.Float32(),
WelcomeBonusCount: settings.WelcomeBonusCount,
WelcomeBonusExpire: settings.WelcomeBonusExpire,
}
}
@ -92,6 +102,11 @@ type SaveSettingListReq struct {
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 *float32 `json:"welcome_bonus_cap"`
WelcomeBonusCount *int64 `json:"welcome_bonus_count"`
WelcomeBonusExpire *int64 `json:"welcome_bonus_expiry"`
}
type ValidSettingList struct {
@ -110,6 +125,11 @@ type ValidSettingList struct {
BetDuplicateLimit ValidInt64
SendEmailOnBetFinish ValidBool
SendSMSOnBetFinish ValidBool
WelcomeBonusActive ValidBool
WelcomeBonusMultiplier ValidFloat32
WelcomeBonusCap ValidCurrency
WelcomeBonusCount ValidInt64
WelcomeBonusExpire ValidInt64
}
func ConvertSaveSettingListReq(settings SaveSettingListReq) ValidSettingList {
@ -129,6 +149,11 @@ func ConvertSaveSettingListReq(settings SaveSettingListReq) ValidSettingList {
BetDuplicateLimit: ConvertInt64Ptr(settings.BetDuplicateLimit),
SendEmailOnBetFinish: ConvertBoolPtr(settings.SendEmailOnBetFinish),
SendSMSOnBetFinish: ConvertBoolPtr(settings.SendSMSOnBetFinish),
WelcomeBonusActive: ConvertBoolPtr(settings.WelcomeBonusActive),
WelcomeBonusMultiplier: ConvertFloat32Ptr(settings.WelcomeBonusMultiplier),
WelcomeBonusCap: ConvertFloat32PtrToCurrency(settings.WelcomeBonusCap),
WelcomeBonusCount: ConvertInt64Ptr(settings.WelcomeBonusCount),
WelcomeBonusExpire: ConvertInt64Ptr(settings.WelcomeBonusExpire),
}
}
@ -150,6 +175,11 @@ func (vsl *ValidSettingList) ToSettingList() SettingList {
BetDuplicateLimit: vsl.BetDuplicateLimit.Value,
SendEmailOnBetFinish: vsl.SendEmailOnBetFinish.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_max_referrals": &vsl.DefaultMaxReferrals,
"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,
"referral_reward_amount": &vsl.ReferralRewardAmount,
"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{
"send_email_on_bet_finish": &vsl.SendEmailOnBetFinish,
"send_sms_on_bet_finish": &vsl.SendSMSOnBetFinish,
"welcome_bonus_active": &vsl.WelcomeBonusActive,
}
}
func (vsl *ValidSettingList) GetFloat32SettingsMap() map[string]*ValidFloat32 {
return map[string]*ValidFloat32{
"cashback_percentage": &vsl.CashbackPercentage,
"welcome_bonus_multiplier": &vsl.WelcomeBonusMultiplier,
}
}

View File

@ -1,10 +1,11 @@
package helpers
import (
random "crypto/rand"
"fmt"
"math/rand/v2"
"github.com/google/uuid"
"math/big"
"math/rand/v2"
)
func GenerateID() string {
@ -24,3 +25,20 @@ func GenerateFastCode() string {
}
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
}
func (s *Store) GetAllUserBonuses(ctx context.Context) ([]domain.UserBonus, error) {
bonuses, err := s.queries.GetAllUserBonuses(ctx)
func (s *Store) GetAllUserBonuses(ctx context.Context, filter domain.BonusFilter) ([]domain.UserBonus, error) {
bonuses, err := s.queries.GetAllUserBonuses(ctx, dbgen.GetAllUserBonusesParams{
UserID: filter.UserID.ToPG(),
Offset: filter.Offset.ToPG(),
Limit: filter.Limit.ToPG(),
})
if err != nil {
return nil, err
@ -27,13 +31,12 @@ func (s *Store) GetAllUserBonuses(ctx context.Context) ([]domain.UserBonus, erro
return domain.ConvertDBBonuses(bonuses), nil
}
func (s *Store) GetBonusesByUserID(ctx context.Context, userID int64) ([]domain.UserBonus, error) {
bonuses, err := s.queries.GetBonusesByUserID(ctx, userID)
func (s *Store) GetBonusCount(ctx context.Context, filter domain.BonusFilter) (int64, error) {
count, err := s.queries.GetBonusCount(ctx, filter.UserID.ToPG())
if err != nil {
return nil, err
return 0, err
}
return domain.ConvertDBBonuses(bonuses), nil
return count, nil
}
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) {
bonus, err := s.queries.GetBonusStats(ctx, dbgen.GetBonusStatsParams{
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++ {
index, err := rand.Int(rand.Reader, charLen)
if err != nil {
s.mongoLogger.Error("failed to generate random index for cashout ID",
zap.Int("position", i),
zap.Error(err),
)
return "", err
}
result[i] = chars[index.Int64()]
@ -957,241 +953,6 @@ func (s *Service) UpdateStatus(ctx context.Context, betId int64, status domain.O
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) {
betOutcomes, err := s.betStore.GetBetOutcomeByBetID(ctx, betID)
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 {
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)
GetAllUserBonuses(ctx context.Context, filter domain.BonusFilter) ([]domain.UserBonus, error)
GetBonusCount(ctx context.Context, filter domain.BonusFilter) (int64, 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

View File

@ -3,11 +3,12 @@ package bonus
import (
"context"
"errors"
"fmt"
"math"
"time"
"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/wallet"
"go.uber.org/zap"
@ -17,14 +18,16 @@ type Service struct {
bonusStore BonusStore
walletSvc *wallet.Service
settingSvc *settings.Service
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{
bonusStore: bonusStore,
walletSvc: walletSvc,
settingSvc: settingSvc,
notificationSvc: notificationSvc,
mongoLogger: mongoLogger,
}
}
@ -34,7 +37,7 @@ var (
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)
if err != nil {
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))
_, err = s.CreateUserBonus(ctx, domain.CreateBonus{
bonus, err := s.CreateUserBonus(ctx, domain.CreateBonus{
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,
BonusCode: helpers.GenerateFastCode(),
RewardAmount: domain.Currency(newBalance),
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
}
// 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
// }
err = s.SendBonusNotification(ctx, SendBonusNotificationParam{
BonusID: bonus.ID,
UserID: userID,
Type: domain.DepositBonus,
Amount: domain.Currency(newBalance),
SendEmail: true,
SendSMS: false,
})
if err != nil {
return err
}
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) {
return s.bonusStore.CreateUserBonus(ctx, bonus)
}
func (s *Service) GetAllUserBonuses(ctx context.Context) ([]domain.UserBonus, error) {
return s.bonusStore.GetAllUserBonuses(ctx)
func (s *Service) GetAllUserBonuses(ctx context.Context, filter domain.BonusFilter) ([]domain.UserBonus, error) {
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) {
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

@ -29,7 +29,7 @@ type Service struct {
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{
virtualGameSvc: virtualGameSvc,
repo: repo,
@ -80,6 +80,8 @@ func (s *Service) GetGames(ctx context.Context, req domain.GameListRequest) ([]d
sigParams := map[string]any{
"brandId": req.BrandID,
"providerId": req.ProviderID,
"size": req.Size,
"page": req.Page,
}
// 3. Call external API
@ -128,7 +130,6 @@ func (s *Service) StartGame(ctx context.Context, req domain.GameStartRequest) (*
return &res, nil
}
func (s *Service) StartDemoGame(ctx context.Context, req domain.DemoGameRequest) (*domain.GameStartResponse, error) {
// 1. Check if provider is enabled in DB
// 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
}
func (s *Service) GetBalance(ctx context.Context, req domain.BalanceRequest) (*domain.BalanceResponse, error) {
// Retrieve player's real balance from wallet Service
playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64)

View File

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

View File

@ -1,12 +1,13 @@
package handlers
// import (
// "time"
import (
"strconv"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
// "github.com/gofiber/fiber/v2"
// "go.uber.org/zap"
// )
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"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 {
@ -96,3 +97,110 @@ package handlers
// 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)
// 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.Put("/bonus/update", a.authMiddleware, h.UpdateBonusMultiplier)