fix: refactor bonus and bonus settings; added welcome bonus
This commit is contained in:
parent
215eb5a1d8
commit
e5f42f1928
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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')),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -93,11 +110,12 @@ func ConvertCreateBonus(bonus CreateBonus) dbgen.CreateUserBonusParams {
|
|||
|
||||
func ConvertDBBonus(bonus dbgen.UserBonuse) UserBonus {
|
||||
return UserBonus{
|
||||
ID: bonus.ID,
|
||||
Name: bonus.Name,
|
||||
Description: bonus.Description,
|
||||
UserID: bonus.UserID,
|
||||
BonusCode: bonus.BonusCode,
|
||||
ID: bonus.ID,
|
||||
Name: bonus.Name,
|
||||
Description: bonus.Description,
|
||||
Type: BonusType(bonus.Type),
|
||||
UserID: bonus.UserID,
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -73,7 +74,7 @@ type Notification struct {
|
|||
RecipientID int64 `json:"recipient_id"`
|
||||
Type NotificationType `json:"type"`
|
||||
Level NotificationLevel `json:"level"`
|
||||
ErrorSeverity NotificationErrorSeverity `json:"error_severity"`
|
||||
ErrorSeverity NotificationErrorSeverity `json:"error_severity"`
|
||||
Reciever NotificationRecieverSide `json:"reciever"`
|
||||
IsRead bool `json:"is_read"`
|
||||
DeliveryStatus NotificationDeliveryStatus `json:"delivery_status,omitempty"`
|
||||
|
|
|
|||
|
|
@ -39,117 +39,147 @@ type SettingList struct {
|
|||
}
|
||||
|
||||
type SettingListRes struct {
|
||||
SMSProvider SMSProvider `json:"sms_provider"`
|
||||
MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"`
|
||||
BetAmountLimit float32 `json:"bet_amount_limit"`
|
||||
DailyTicketPerIP int64 `json:"daily_ticket_limit"`
|
||||
TotalWinningLimit float32 `json:"total_winning_limit"`
|
||||
AmountForBetReferral float32 `json:"amount_for_bet_referral"`
|
||||
CashbackAmountCap float32 `json:"cashback_amount_cap"`
|
||||
DefaultWinningLimit int64 `json:"default_winning_limit"`
|
||||
ReferralRewardAmount float32 `json:"referral_reward_amount"`
|
||||
CashbackPercentage float32 `json:"cashback_percentage"`
|
||||
DefaultMaxReferrals int64 `json:"default_max_referrals"`
|
||||
MinimumBetAmount float32 `json:"minimum_bet_amount"`
|
||||
BetDuplicateLimit int64 `json:"bet_duplicate_limit"`
|
||||
SendEmailOnBetFinish bool `json:"send_email_on_bet_finish"`
|
||||
SendSMSOnBetFinish bool `json:"send_sms_on_bet_finish"`
|
||||
SMSProvider SMSProvider `json:"sms_provider"`
|
||||
MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"`
|
||||
BetAmountLimit float32 `json:"bet_amount_limit"`
|
||||
DailyTicketPerIP int64 `json:"daily_ticket_limit"`
|
||||
TotalWinningLimit float32 `json:"total_winning_limit"`
|
||||
AmountForBetReferral float32 `json:"amount_for_bet_referral"`
|
||||
CashbackAmountCap float32 `json:"cashback_amount_cap"`
|
||||
DefaultWinningLimit int64 `json:"default_winning_limit"`
|
||||
ReferralRewardAmount float32 `json:"referral_reward_amount"`
|
||||
CashbackPercentage float32 `json:"cashback_percentage"`
|
||||
DefaultMaxReferrals int64 `json:"default_max_referrals"`
|
||||
MinimumBetAmount float32 `json:"minimum_bet_amount"`
|
||||
BetDuplicateLimit int64 `json:"bet_duplicate_limit"`
|
||||
SendEmailOnBetFinish bool `json:"send_email_on_bet_finish"`
|
||||
SendSMSOnBetFinish bool `json:"send_sms_on_bet_finish"`
|
||||
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 {
|
||||
return SettingListRes{
|
||||
SMSProvider: settings.SMSProvider,
|
||||
MaxNumberOfOutcomes: settings.MaxNumberOfOutcomes,
|
||||
BetAmountLimit: settings.BetAmountLimit.Float32(),
|
||||
DailyTicketPerIP: settings.DailyTicketPerIP,
|
||||
TotalWinningLimit: settings.TotalWinningLimit.Float32(),
|
||||
AmountForBetReferral: settings.AmountForBetReferral.Float32(),
|
||||
CashbackAmountCap: settings.CashbackAmountCap.Float32(),
|
||||
DefaultWinningLimit: settings.DefaultWinningLimit,
|
||||
ReferralRewardAmount: settings.ReferralRewardAmount.Float32(),
|
||||
CashbackPercentage: settings.CashbackPercentage,
|
||||
DefaultMaxReferrals: settings.DefaultMaxReferrals,
|
||||
MinimumBetAmount: settings.MinimumBetAmount.Float32(),
|
||||
BetDuplicateLimit: settings.BetDuplicateLimit,
|
||||
SendEmailOnBetFinish: settings.SendEmailOnBetFinish,
|
||||
SendSMSOnBetFinish: settings.SendSMSOnBetFinish,
|
||||
SMSProvider: settings.SMSProvider,
|
||||
MaxNumberOfOutcomes: settings.MaxNumberOfOutcomes,
|
||||
BetAmountLimit: settings.BetAmountLimit.Float32(),
|
||||
DailyTicketPerIP: settings.DailyTicketPerIP,
|
||||
TotalWinningLimit: settings.TotalWinningLimit.Float32(),
|
||||
AmountForBetReferral: settings.AmountForBetReferral.Float32(),
|
||||
CashbackAmountCap: settings.CashbackAmountCap.Float32(),
|
||||
DefaultWinningLimit: settings.DefaultWinningLimit,
|
||||
ReferralRewardAmount: settings.ReferralRewardAmount.Float32(),
|
||||
CashbackPercentage: settings.CashbackPercentage,
|
||||
DefaultMaxReferrals: settings.DefaultMaxReferrals,
|
||||
MinimumBetAmount: settings.MinimumBetAmount.Float32(),
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
type SaveSettingListReq struct {
|
||||
SMSProvider *string `json:"sms_provider,omitempty"`
|
||||
MaxNumberOfOutcomes *int64 `json:"max_number_of_outcomes,omitempty"`
|
||||
BetAmountLimit *float32 `json:"bet_amount_limit,omitempty"`
|
||||
DailyTicketPerIP *int64 `json:"daily_ticket_limit,omitempty"`
|
||||
TotalWinningLimit *float32 `json:"total_winning_limit,omitempty"`
|
||||
AmountForBetReferral *float32 `json:"amount_for_bet_referral,omitempty"`
|
||||
CashbackAmountCap *float32 `json:"cashback_amount_cap,omitempty"`
|
||||
DefaultWinningLimit *int64 `json:"default_winning_limit,omitempty"`
|
||||
ReferralRewardAmount *float32 `json:"referral_reward_amount"`
|
||||
CashbackPercentage *float32 `json:"cashback_percentage"`
|
||||
DefaultMaxReferrals *int64 `json:"default_max_referrals"`
|
||||
MinimumBetAmount *float32 `json:"minimum_bet_amount"`
|
||||
BetDuplicateLimit *int64 `json:"bet_duplicate_limit"`
|
||||
SendEmailOnBetFinish *bool `json:"send_email_on_bet_finish"`
|
||||
SendSMSOnBetFinish *bool `json:"send_sms_on_bet_finish"`
|
||||
SMSProvider *string `json:"sms_provider,omitempty"`
|
||||
MaxNumberOfOutcomes *int64 `json:"max_number_of_outcomes,omitempty"`
|
||||
BetAmountLimit *float32 `json:"bet_amount_limit,omitempty"`
|
||||
DailyTicketPerIP *int64 `json:"daily_ticket_limit,omitempty"`
|
||||
TotalWinningLimit *float32 `json:"total_winning_limit,omitempty"`
|
||||
AmountForBetReferral *float32 `json:"amount_for_bet_referral,omitempty"`
|
||||
CashbackAmountCap *float32 `json:"cashback_amount_cap,omitempty"`
|
||||
DefaultWinningLimit *int64 `json:"default_winning_limit,omitempty"`
|
||||
ReferralRewardAmount *float32 `json:"referral_reward_amount"`
|
||||
CashbackPercentage *float32 `json:"cashback_percentage"`
|
||||
DefaultMaxReferrals *int64 `json:"default_max_referrals"`
|
||||
MinimumBetAmount *float32 `json:"minimum_bet_amount"`
|
||||
BetDuplicateLimit *int64 `json:"bet_duplicate_limit"`
|
||||
SendEmailOnBetFinish *bool `json:"send_email_on_bet_finish"`
|
||||
SendSMSOnBetFinish *bool `json:"send_sms_on_bet_finish"`
|
||||
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 {
|
||||
SMSProvider ValidString
|
||||
MaxNumberOfOutcomes ValidInt64
|
||||
BetAmountLimit ValidCurrency
|
||||
DailyTicketPerIP ValidInt64
|
||||
TotalWinningLimit ValidCurrency
|
||||
AmountForBetReferral ValidCurrency
|
||||
CashbackAmountCap ValidCurrency
|
||||
DefaultWinningLimit ValidInt64
|
||||
ReferralRewardAmount ValidCurrency
|
||||
CashbackPercentage ValidFloat32
|
||||
DefaultMaxReferrals ValidInt64
|
||||
MinimumBetAmount ValidCurrency
|
||||
BetDuplicateLimit ValidInt64
|
||||
SendEmailOnBetFinish ValidBool
|
||||
SendSMSOnBetFinish ValidBool
|
||||
SMSProvider ValidString
|
||||
MaxNumberOfOutcomes ValidInt64
|
||||
BetAmountLimit ValidCurrency
|
||||
DailyTicketPerIP ValidInt64
|
||||
TotalWinningLimit ValidCurrency
|
||||
AmountForBetReferral ValidCurrency
|
||||
CashbackAmountCap ValidCurrency
|
||||
DefaultWinningLimit ValidInt64
|
||||
ReferralRewardAmount ValidCurrency
|
||||
CashbackPercentage ValidFloat32
|
||||
DefaultMaxReferrals ValidInt64
|
||||
MinimumBetAmount ValidCurrency
|
||||
BetDuplicateLimit ValidInt64
|
||||
SendEmailOnBetFinish ValidBool
|
||||
SendSMSOnBetFinish ValidBool
|
||||
WelcomeBonusActive ValidBool
|
||||
WelcomeBonusMultiplier ValidFloat32
|
||||
WelcomeBonusCap ValidCurrency
|
||||
WelcomeBonusCount ValidInt64
|
||||
WelcomeBonusExpire ValidInt64
|
||||
}
|
||||
|
||||
func ConvertSaveSettingListReq(settings SaveSettingListReq) ValidSettingList {
|
||||
return ValidSettingList{
|
||||
SMSProvider: ConvertStringPtr(settings.SMSProvider),
|
||||
MaxNumberOfOutcomes: ConvertInt64Ptr(settings.MaxNumberOfOutcomes),
|
||||
BetAmountLimit: ConvertFloat32PtrToCurrency(settings.BetAmountLimit),
|
||||
DailyTicketPerIP: ConvertInt64Ptr(settings.DailyTicketPerIP),
|
||||
TotalWinningLimit: ConvertFloat32PtrToCurrency(settings.TotalWinningLimit),
|
||||
AmountForBetReferral: ConvertFloat32PtrToCurrency(settings.AmountForBetReferral),
|
||||
CashbackAmountCap: ConvertFloat32PtrToCurrency(settings.CashbackAmountCap),
|
||||
DefaultWinningLimit: ConvertInt64Ptr(settings.DefaultWinningLimit),
|
||||
ReferralRewardAmount: ConvertFloat32PtrToCurrency(settings.ReferralRewardAmount),
|
||||
CashbackPercentage: ConvertFloat32Ptr(settings.CashbackPercentage),
|
||||
DefaultMaxReferrals: ConvertInt64Ptr(settings.DefaultMaxReferrals),
|
||||
MinimumBetAmount: ConvertFloat32PtrToCurrency(settings.MinimumBetAmount),
|
||||
BetDuplicateLimit: ConvertInt64Ptr(settings.BetDuplicateLimit),
|
||||
SendEmailOnBetFinish: ConvertBoolPtr(settings.SendEmailOnBetFinish),
|
||||
SendSMSOnBetFinish: ConvertBoolPtr(settings.SendSMSOnBetFinish),
|
||||
SMSProvider: ConvertStringPtr(settings.SMSProvider),
|
||||
MaxNumberOfOutcomes: ConvertInt64Ptr(settings.MaxNumberOfOutcomes),
|
||||
BetAmountLimit: ConvertFloat32PtrToCurrency(settings.BetAmountLimit),
|
||||
DailyTicketPerIP: ConvertInt64Ptr(settings.DailyTicketPerIP),
|
||||
TotalWinningLimit: ConvertFloat32PtrToCurrency(settings.TotalWinningLimit),
|
||||
AmountForBetReferral: ConvertFloat32PtrToCurrency(settings.AmountForBetReferral),
|
||||
CashbackAmountCap: ConvertFloat32PtrToCurrency(settings.CashbackAmountCap),
|
||||
DefaultWinningLimit: ConvertInt64Ptr(settings.DefaultWinningLimit),
|
||||
ReferralRewardAmount: ConvertFloat32PtrToCurrency(settings.ReferralRewardAmount),
|
||||
CashbackPercentage: ConvertFloat32Ptr(settings.CashbackPercentage),
|
||||
DefaultMaxReferrals: ConvertInt64Ptr(settings.DefaultMaxReferrals),
|
||||
MinimumBetAmount: ConvertFloat32PtrToCurrency(settings.MinimumBetAmount),
|
||||
BetDuplicateLimit: ConvertInt64Ptr(settings.BetDuplicateLimit),
|
||||
SendEmailOnBetFinish: ConvertBoolPtr(settings.SendEmailOnBetFinish),
|
||||
SendSMSOnBetFinish: ConvertBoolPtr(settings.SendSMSOnBetFinish),
|
||||
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
|
||||
func (vsl *ValidSettingList) ToSettingList() SettingList {
|
||||
return SettingList{
|
||||
SMSProvider: SMSProvider(vsl.SMSProvider.Value),
|
||||
MaxNumberOfOutcomes: vsl.MaxNumberOfOutcomes.Value,
|
||||
BetAmountLimit: vsl.BetAmountLimit.Value,
|
||||
DailyTicketPerIP: vsl.DailyTicketPerIP.Value,
|
||||
TotalWinningLimit: vsl.TotalWinningLimit.Value,
|
||||
AmountForBetReferral: vsl.AmountForBetReferral.Value,
|
||||
CashbackAmountCap: vsl.CashbackAmountCap.Value,
|
||||
DefaultWinningLimit: vsl.DefaultWinningLimit.Value,
|
||||
ReferralRewardAmount: vsl.ReferralRewardAmount.Value,
|
||||
CashbackPercentage: vsl.CashbackPercentage.Value,
|
||||
DefaultMaxReferrals: vsl.DefaultMaxReferrals.Value,
|
||||
MinimumBetAmount: vsl.MinimumBetAmount.Value,
|
||||
BetDuplicateLimit: vsl.BetDuplicateLimit.Value,
|
||||
SendEmailOnBetFinish: vsl.SendEmailOnBetFinish.Value,
|
||||
SendSMSOnBetFinish: vsl.SendSMSOnBetFinish.Value,
|
||||
SMSProvider: SMSProvider(vsl.SMSProvider.Value),
|
||||
MaxNumberOfOutcomes: vsl.MaxNumberOfOutcomes.Value,
|
||||
BetAmountLimit: vsl.BetAmountLimit.Value,
|
||||
DailyTicketPerIP: vsl.DailyTicketPerIP.Value,
|
||||
TotalWinningLimit: vsl.TotalWinningLimit.Value,
|
||||
AmountForBetReferral: vsl.AmountForBetReferral.Value,
|
||||
CashbackAmountCap: vsl.CashbackAmountCap.Value,
|
||||
DefaultWinningLimit: vsl.DefaultWinningLimit.Value,
|
||||
ReferralRewardAmount: vsl.ReferralRewardAmount.Value,
|
||||
CashbackPercentage: vsl.CashbackPercentage.Value,
|
||||
DefaultMaxReferrals: vsl.DefaultMaxReferrals.Value,
|
||||
MinimumBetAmount: vsl.MinimumBetAmount.Value,
|
||||
BetDuplicateLimit: vsl.BetDuplicateLimit.Value,
|
||||
SendEmailOnBetFinish: vsl.SendEmailOnBetFinish.Value,
|
||||
SendSMSOnBetFinish: vsl.SendSMSOnBetFinish.Value,
|
||||
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,
|
||||
"cashback_percentage": &vsl.CashbackPercentage,
|
||||
"welcome_bonus_multiplier": &vsl.WelcomeBonusMultiplier,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
247
internal/services/bet/notification.go
Normal file
247
internal/services/bet/notification.go
Normal 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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
83
internal/services/bonus/notification.go
Normal file
83
internal/services/bonus/notification.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -3,29 +3,32 @@ 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"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
bonusStore BonusStore
|
||||
walletSvc *wallet.Service
|
||||
settingSvc *settings.Service
|
||||
mongoLogger *zap.Logger
|
||||
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,
|
||||
mongoLogger: mongoLogger,
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -22,23 +22,23 @@ var (
|
|||
|
||||
type Service struct {
|
||||
virtualGameSvc virtualgameservice.VirtualGameService
|
||||
repo repository.VirtualGameRepository
|
||||
client *Client
|
||||
walletSvc *wallet.Service
|
||||
transfetStore wallet.TransferStore
|
||||
cfg *config.Config
|
||||
repo repository.VirtualGameRepository
|
||||
client *Client
|
||||
walletSvc *wallet.Service
|
||||
transfetStore wallet.TransferStore
|
||||
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,
|
||||
client: client,
|
||||
walletSvc: walletSvc,
|
||||
transfetStore: transferStore,
|
||||
cfg: cfg,
|
||||
repo: repo,
|
||||
client: client,
|
||||
walletSvc: walletSvc,
|
||||
transfetStore: transferStore,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) GetProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) {
|
||||
// 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{
|
||||
"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)
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user