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)
|
ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger, *settingSvc, notificationSvc)
|
||||||
betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, *companySvc, *settingSvc, *userSvc, notificationSvc, logger, domain.MongoDBLogger)
|
betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, *companySvc, *settingSvc, *userSvc, notificationSvc, logger, domain.MongoDBLogger)
|
||||||
resultSvc := result.NewService(store, cfg, logger, domain.MongoDBLogger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc, messengerSvc, *userSvc)
|
resultSvc := result.NewService(store, cfg, logger, domain.MongoDBLogger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc, messengerSvc, *userSvc)
|
||||||
bonusSvc := bonus.NewService(store, walletSvc, settingSvc, domain.MongoDBLogger)
|
bonusSvc := bonus.NewService(store, walletSvc, settingSvc, notificationSvc, domain.MongoDBLogger)
|
||||||
referalRepo := repository.NewReferralRepository(store)
|
referalRepo := repository.NewReferralRepository(store)
|
||||||
vitualGameRepo := repository.NewVirtualGameRepository(store)
|
vitualGameRepo := repository.NewVirtualGameRepository(store)
|
||||||
recommendationRepo := repository.NewRecommendationRepository(store)
|
recommendationRepo := repository.NewRecommendationRepository(store)
|
||||||
|
|
|
||||||
|
|
@ -88,8 +88,14 @@ VALUES ('sms_provider', 'afro_message'),
|
||||||
('cashback_percentage', '0.2'),
|
('cashback_percentage', '0.2'),
|
||||||
('default_max_referrals', '15'),
|
('default_max_referrals', '15'),
|
||||||
('minimum_bet_amount', '100'),
|
('minimum_bet_amount', '100'),
|
||||||
|
('bet_duplicate_limit', '5'),
|
||||||
('send_email_on_bet_finish', 'true'),
|
('send_email_on_bet_finish', 'true'),
|
||||||
('send_sms_on_bet_finish', 'false') ON CONFLICT (key) DO NOTHING;
|
('send_sms_on_bet_finish', 'false'),
|
||||||
|
('welcome_bonus_active', 'false'),
|
||||||
|
('welcome_bonus_multiplier', '1.5'),
|
||||||
|
('welcome_bonus_multiplier', '100000'),
|
||||||
|
('welcome_bonus_count', '3'),
|
||||||
|
('welcome_bonus_expiry', '10') ON CONFLICT (key) DO NOTHING;
|
||||||
-- Users
|
-- Users
|
||||||
INSERT INTO users (
|
INSERT INTO users (
|
||||||
id,
|
id,
|
||||||
|
|
|
||||||
|
|
@ -457,11 +457,12 @@ CREATE TABLE user_bonuses (
|
||||||
id BIGINT NOT NULL,
|
id BIGINT NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
description TEXT NOT NULL,
|
description TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL,
|
||||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
bonus_code TEXT NOT NULL UNIQUE,
|
|
||||||
reward_amount BIGINT NOT NULL,
|
reward_amount BIGINT NOT NULL,
|
||||||
is_claimed BOOLEAN NOT NULL DEFAULT false,
|
is_claimed BOOLEAN NOT NULL DEFAULT false,
|
||||||
expires_at TIMESTAMP NOT NULL,
|
expires_at TIMESTAMP NOT NULL,
|
||||||
|
claimed_at TIMESTAMP NOT NULL,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
@ -500,7 +501,23 @@ CREATE TABLE direct_deposits (
|
||||||
CREATE INDEX idx_direct_deposits_status ON direct_deposits (status);
|
CREATE INDEX idx_direct_deposits_status ON direct_deposits (status);
|
||||||
CREATE INDEX idx_direct_deposits_customer ON direct_deposits (customer_id);
|
CREATE INDEX idx_direct_deposits_customer ON direct_deposits (customer_id);
|
||||||
CREATE INDEX idx_direct_deposits_reference ON direct_deposits (bank_reference);
|
CREATE INDEX idx_direct_deposits_reference ON direct_deposits (bank_reference);
|
||||||
-- Views
|
CREATE TABLE IF NOT EXISTS raffles (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
company_id INT NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
expires_at TIMESTAMP NOT NULL,
|
||||||
|
type VARCHAR(50) NOT NULL CHECK (type IN ('virtual', 'sport')),
|
||||||
|
status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'completed'))
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS raffle_tickets (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
raffle_id INT NOT NULL REFERENCES raffles(id) ON DELETE CASCADE,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
is_active BOOL DEFAULT true,
|
||||||
|
UNIQUE (raffle_id, user_id)
|
||||||
|
);
|
||||||
|
------ Views
|
||||||
CREATE VIEW companies_details AS
|
CREATE VIEW companies_details AS
|
||||||
SELECT companies.*,
|
SELECT companies.*,
|
||||||
wallets.balance,
|
wallets.balance,
|
||||||
|
|
@ -526,22 +543,6 @@ CREATE TABLE IF NOT EXISTS supported_operations (
|
||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
description VARCHAR(255) NOT NULL
|
description VARCHAR(255) NOT NULL
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS raffles (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
company_id INT NOT NULL,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
||||||
expires_at TIMESTAMP NOT NULL,
|
|
||||||
type VARCHAR(50) NOT NULL CHECK (type IN ('virtual', 'sport')),
|
|
||||||
status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'completed'))
|
|
||||||
);
|
|
||||||
CREATE TABLE IF NOT EXISTS raffle_tickets (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
raffle_id INT NOT NULL REFERENCES raffles(id) ON DELETE CASCADE,
|
|
||||||
user_id INT NOT NULL,
|
|
||||||
is_active BOOL DEFAULT true,
|
|
||||||
UNIQUE (raffle_id, user_id)
|
|
||||||
);
|
|
||||||
CREATE VIEW bet_with_outcomes AS
|
CREATE VIEW bet_with_outcomes AS
|
||||||
SELECT bets.*,
|
SELECT bets.*,
|
||||||
CONCAT (users.first_name, ' ', users.last_name) AS full_name,
|
CONCAT (users.first_name, ' ', users.last_name) AS full_name,
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ CREATE TABLE IF NOT EXISTS notifications (
|
||||||
'admin_alert',
|
'admin_alert',
|
||||||
'bet_result',
|
'bet_result',
|
||||||
'transfer_rejected',
|
'transfer_rejected',
|
||||||
'approval_required'
|
'approval_required',
|
||||||
|
'bonus_awarded'
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
level TEXT NOT NULL CHECK (level IN ('info', 'error', 'warning', 'success')),
|
level TEXT NOT NULL CHECK (level IN ('info', 'error', 'warning', 'success')),
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
INSERT INTO user_bonuses (
|
INSERT INTO user_bonuses (
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
|
type,
|
||||||
user_id,
|
user_id,
|
||||||
bonus_code,
|
|
||||||
reward_amount,
|
reward_amount,
|
||||||
expires_at
|
expires_at
|
||||||
)
|
)
|
||||||
|
|
@ -11,15 +11,24 @@ VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
-- name: GetAllUserBonuses :many
|
-- name: GetAllUserBonuses :many
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM user_bonuses;
|
FROM user_bonuses
|
||||||
|
WHERE (
|
||||||
|
user_id = sqlc.narg('user_id')
|
||||||
|
OR sqlc.narg('user_id') IS NULL
|
||||||
|
)
|
||||||
|
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
|
||||||
-- name: GetUserBonusByID :one
|
-- name: GetUserBonusByID :one
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM user_bonuses
|
FROM user_bonuses
|
||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
-- name: GetBonusesByUserID :many
|
|
||||||
SELECT *
|
-- name: GetBonusCount :one
|
||||||
|
SELECT COUNT(*)
|
||||||
FROM user_bonuses
|
FROM user_bonuses
|
||||||
WHERE user_id = $1;
|
WHERE (
|
||||||
|
user_id = sqlc.narg('user_id')
|
||||||
|
OR sqlc.narg('user_id') IS NULL
|
||||||
|
);
|
||||||
-- name: GetBonusStats :one
|
-- name: GetBonusStats :one
|
||||||
SELECT COUNT(*) AS total_bonuses,
|
SELECT COUNT(*) AS total_bonuses,
|
||||||
COALESCE(SUM(reward_amount), 0)::bigint AS total_reward_earned,
|
COALESCE(SUM(reward_amount), 0)::bigint AS total_reward_earned,
|
||||||
|
|
@ -30,7 +39,7 @@ SELECT COUNT(*) AS total_bonuses,
|
||||||
) AS claimed_bonuses,
|
) AS claimed_bonuses,
|
||||||
COUNT(
|
COUNT(
|
||||||
CASE
|
CASE
|
||||||
WHEN expires_at > now() THEN 1
|
WHEN expires_at < now() THEN 1
|
||||||
END
|
END
|
||||||
) AS expired_bonuses
|
) AS expired_bonuses
|
||||||
FROM user_bonuses
|
FROM user_bonuses
|
||||||
|
|
|
||||||
|
|
@ -15,20 +15,20 @@ const CreateUserBonus = `-- name: CreateUserBonus :one
|
||||||
INSERT INTO user_bonuses (
|
INSERT INTO user_bonuses (
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
|
type,
|
||||||
user_id,
|
user_id,
|
||||||
bonus_code,
|
|
||||||
reward_amount,
|
reward_amount,
|
||||||
expires_at
|
expires_at
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
RETURNING id, name, description, user_id, bonus_code, reward_amount, is_claimed, expires_at, created_at, updated_at
|
RETURNING id, name, description, type, user_id, reward_amount, is_claimed, expires_at, claimed_at, created_at, updated_at
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateUserBonusParams struct {
|
type CreateUserBonusParams struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
Type string `json:"type"`
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
BonusCode string `json:"bonus_code"`
|
|
||||||
RewardAmount int64 `json:"reward_amount"`
|
RewardAmount int64 `json:"reward_amount"`
|
||||||
ExpiresAt pgtype.Timestamp `json:"expires_at"`
|
ExpiresAt pgtype.Timestamp `json:"expires_at"`
|
||||||
}
|
}
|
||||||
|
|
@ -37,8 +37,8 @@ func (q *Queries) CreateUserBonus(ctx context.Context, arg CreateUserBonusParams
|
||||||
row := q.db.QueryRow(ctx, CreateUserBonus,
|
row := q.db.QueryRow(ctx, CreateUserBonus,
|
||||||
arg.Name,
|
arg.Name,
|
||||||
arg.Description,
|
arg.Description,
|
||||||
|
arg.Type,
|
||||||
arg.UserID,
|
arg.UserID,
|
||||||
arg.BonusCode,
|
|
||||||
arg.RewardAmount,
|
arg.RewardAmount,
|
||||||
arg.ExpiresAt,
|
arg.ExpiresAt,
|
||||||
)
|
)
|
||||||
|
|
@ -47,11 +47,12 @@ func (q *Queries) CreateUserBonus(ctx context.Context, arg CreateUserBonusParams
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Description,
|
&i.Description,
|
||||||
|
&i.Type,
|
||||||
&i.UserID,
|
&i.UserID,
|
||||||
&i.BonusCode,
|
|
||||||
&i.RewardAmount,
|
&i.RewardAmount,
|
||||||
&i.IsClaimed,
|
&i.IsClaimed,
|
||||||
&i.ExpiresAt,
|
&i.ExpiresAt,
|
||||||
|
&i.ClaimedAt,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
)
|
)
|
||||||
|
|
@ -69,12 +70,23 @@ func (q *Queries) DeleteUserBonus(ctx context.Context, id int64) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetAllUserBonuses = `-- name: GetAllUserBonuses :many
|
const GetAllUserBonuses = `-- name: GetAllUserBonuses :many
|
||||||
SELECT id, name, description, user_id, bonus_code, reward_amount, is_claimed, expires_at, created_at, updated_at
|
SELECT id, name, description, type, user_id, reward_amount, is_claimed, expires_at, claimed_at, created_at, updated_at
|
||||||
FROM user_bonuses
|
FROM user_bonuses
|
||||||
|
WHERE (
|
||||||
|
user_id = $1
|
||||||
|
OR $1 IS NULL
|
||||||
|
)
|
||||||
|
LIMIT $3 OFFSET $2
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetAllUserBonuses(ctx context.Context) ([]UserBonuse, error) {
|
type GetAllUserBonusesParams struct {
|
||||||
rows, err := q.db.Query(ctx, GetAllUserBonuses)
|
UserID pgtype.Int8 `json:"user_id"`
|
||||||
|
Offset pgtype.Int4 `json:"offset"`
|
||||||
|
Limit pgtype.Int4 `json:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetAllUserBonuses(ctx context.Context, arg GetAllUserBonusesParams) ([]UserBonuse, error) {
|
||||||
|
rows, err := q.db.Query(ctx, GetAllUserBonuses, arg.UserID, arg.Offset, arg.Limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -86,11 +98,12 @@ func (q *Queries) GetAllUserBonuses(ctx context.Context) ([]UserBonuse, error) {
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Description,
|
&i.Description,
|
||||||
|
&i.Type,
|
||||||
&i.UserID,
|
&i.UserID,
|
||||||
&i.BonusCode,
|
|
||||||
&i.RewardAmount,
|
&i.RewardAmount,
|
||||||
&i.IsClaimed,
|
&i.IsClaimed,
|
||||||
&i.ExpiresAt,
|
&i.ExpiresAt,
|
||||||
|
&i.ClaimedAt,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
@ -104,6 +117,22 @@ func (q *Queries) GetAllUserBonuses(ctx context.Context) ([]UserBonuse, error) {
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GetBonusCount = `-- name: GetBonusCount :one
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM user_bonuses
|
||||||
|
WHERE (
|
||||||
|
user_id = $1
|
||||||
|
OR $1 IS NULL
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetBonusCount(ctx context.Context, userID pgtype.Int8) (int64, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetBonusCount, userID)
|
||||||
|
var count int64
|
||||||
|
err := row.Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
const GetBonusStats = `-- name: GetBonusStats :one
|
const GetBonusStats = `-- name: GetBonusStats :one
|
||||||
SELECT COUNT(*) AS total_bonuses,
|
SELECT COUNT(*) AS total_bonuses,
|
||||||
COALESCE(SUM(reward_amount), 0)::bigint AS total_reward_earned,
|
COALESCE(SUM(reward_amount), 0)::bigint AS total_reward_earned,
|
||||||
|
|
@ -114,7 +143,7 @@ SELECT COUNT(*) AS total_bonuses,
|
||||||
) AS claimed_bonuses,
|
) AS claimed_bonuses,
|
||||||
COUNT(
|
COUNT(
|
||||||
CASE
|
CASE
|
||||||
WHEN expires_at > now() THEN 1
|
WHEN expires_at < now() THEN 1
|
||||||
END
|
END
|
||||||
) AS expired_bonuses
|
) AS expired_bonuses
|
||||||
FROM user_bonuses
|
FROM user_bonuses
|
||||||
|
|
@ -153,45 +182,8 @@ func (q *Queries) GetBonusStats(ctx context.Context, arg GetBonusStatsParams) (G
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetBonusesByUserID = `-- name: GetBonusesByUserID :many
|
|
||||||
SELECT id, name, description, user_id, bonus_code, reward_amount, is_claimed, expires_at, created_at, updated_at
|
|
||||||
FROM user_bonuses
|
|
||||||
WHERE user_id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetBonusesByUserID(ctx context.Context, userID int64) ([]UserBonuse, error) {
|
|
||||||
rows, err := q.db.Query(ctx, GetBonusesByUserID, userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []UserBonuse
|
|
||||||
for rows.Next() {
|
|
||||||
var i UserBonuse
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Description,
|
|
||||||
&i.UserID,
|
|
||||||
&i.BonusCode,
|
|
||||||
&i.RewardAmount,
|
|
||||||
&i.IsClaimed,
|
|
||||||
&i.ExpiresAt,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetUserBonusByID = `-- name: GetUserBonusByID :one
|
const GetUserBonusByID = `-- name: GetUserBonusByID :one
|
||||||
SELECT id, name, description, user_id, bonus_code, reward_amount, is_claimed, expires_at, created_at, updated_at
|
SELECT id, name, description, type, user_id, reward_amount, is_claimed, expires_at, claimed_at, created_at, updated_at
|
||||||
FROM user_bonuses
|
FROM user_bonuses
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
@ -203,11 +195,12 @@ func (q *Queries) GetUserBonusByID(ctx context.Context, id int64) (UserBonuse, e
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Description,
|
&i.Description,
|
||||||
|
&i.Type,
|
||||||
&i.UserID,
|
&i.UserID,
|
||||||
&i.BonusCode,
|
|
||||||
&i.RewardAmount,
|
&i.RewardAmount,
|
||||||
&i.IsClaimed,
|
&i.IsClaimed,
|
||||||
&i.ExpiresAt,
|
&i.ExpiresAt,
|
||||||
|
&i.ClaimedAt,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -750,11 +750,12 @@ type UserBonuse struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
Type string `json:"type"`
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
BonusCode string `json:"bonus_code"`
|
|
||||||
RewardAmount int64 `json:"reward_amount"`
|
RewardAmount int64 `json:"reward_amount"`
|
||||||
IsClaimed bool `json:"is_claimed"`
|
IsClaimed bool `json:"is_claimed"`
|
||||||
ExpiresAt pgtype.Timestamp `json:"expires_at"`
|
ExpiresAt pgtype.Timestamp `json:"expires_at"`
|
||||||
|
ClaimedAt pgtype.Timestamp `json:"claimed_at"`
|
||||||
CreatedAt pgtype.Timestamp `json:"created_at"`
|
CreatedAt pgtype.Timestamp `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamp `json:"updated_at"`
|
UpdatedAt pgtype.Timestamp `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,19 @@ import (
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type BonusType string
|
||||||
|
|
||||||
|
var (
|
||||||
|
WelcomeBonus BonusType = "welcome_bonus"
|
||||||
|
DepositBonus BonusType = "deposit_bonus"
|
||||||
|
)
|
||||||
|
|
||||||
type UserBonus struct {
|
type UserBonus struct {
|
||||||
ID int64
|
ID int64
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
UserID int64
|
UserID int64
|
||||||
BonusCode string
|
Type BonusType
|
||||||
RewardAmount Currency
|
RewardAmount Currency
|
||||||
IsClaimed bool
|
IsClaimed bool
|
||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
|
|
@ -25,7 +32,7 @@ type UserBonusRes struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
BonusCode string `json:"bonus_code"`
|
Type BonusType `json:"type"`
|
||||||
RewardAmount float32 `json:"reward_amount"`
|
RewardAmount float32 `json:"reward_amount"`
|
||||||
IsClaimed bool `json:"is_claimed"`
|
IsClaimed bool `json:"is_claimed"`
|
||||||
ExpiresAt time.Time `json:"expires_at"`
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
|
@ -38,8 +45,8 @@ func ConvertToBonusRes(bonus UserBonus) UserBonusRes {
|
||||||
ID: bonus.ID,
|
ID: bonus.ID,
|
||||||
Name: bonus.Name,
|
Name: bonus.Name,
|
||||||
Description: bonus.Description,
|
Description: bonus.Description,
|
||||||
|
Type: bonus.Type,
|
||||||
UserID: bonus.UserID,
|
UserID: bonus.UserID,
|
||||||
BonusCode: bonus.BonusCode,
|
|
||||||
RewardAmount: bonus.RewardAmount.Float32(),
|
RewardAmount: bonus.RewardAmount.Float32(),
|
||||||
IsClaimed: bonus.IsClaimed,
|
IsClaimed: bonus.IsClaimed,
|
||||||
ExpiresAt: bonus.ExpiresAt,
|
ExpiresAt: bonus.ExpiresAt,
|
||||||
|
|
@ -48,41 +55,51 @@ func ConvertToBonusRes(bonus UserBonus) UserBonusRes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ConvertToBonusResList(bonuses []UserBonus) []UserBonusRes {
|
||||||
|
result := make([]UserBonusRes, len(bonuses))
|
||||||
|
|
||||||
|
for i, bonus := range bonuses {
|
||||||
|
result[i] = ConvertToBonusRes(bonus)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
type CreateBonus struct {
|
type CreateBonus struct {
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
|
Type BonusType
|
||||||
UserID int64
|
UserID int64
|
||||||
BonusCode string
|
|
||||||
RewardAmount Currency
|
RewardAmount Currency
|
||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateBonusReq struct {
|
// type CreateBonusReq struct {
|
||||||
Name string `json:"name"`
|
// Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
// Description string `json:"description"`
|
||||||
UserID int64 `json:"user_id"`
|
// Type BonusType `json:"type"`
|
||||||
BonusCode string `json:"bonus_code"`
|
// UserID int64 `json:"user_id"`
|
||||||
RewardAmount float32 `json:"reward_amount"`
|
// RewardAmount float32 `json:"reward_amount"`
|
||||||
ExpiresAt time.Time `json:"expires_at"`
|
// ExpiresAt time.Time `json:"expires_at"`
|
||||||
}
|
// }
|
||||||
|
|
||||||
func ConvertCreateBonusReq(bonus CreateBonusReq, companyID int64) CreateBonus {
|
// func ConvertCreateBonusReq(bonus CreateBonusReq, companyID int64) CreateBonus {
|
||||||
return CreateBonus{
|
// return CreateBonus{
|
||||||
Name: bonus.Name,
|
// Name: bonus.Name,
|
||||||
Description: bonus.Description,
|
// Description: bonus.Description,
|
||||||
UserID: bonus.UserID,
|
// Type: bonus.Type,
|
||||||
BonusCode: bonus.BonusCode,
|
// UserID: bonus.UserID,
|
||||||
RewardAmount: ToCurrency(bonus.RewardAmount),
|
// RewardAmount: ToCurrency(bonus.RewardAmount),
|
||||||
ExpiresAt: bonus.ExpiresAt,
|
// ExpiresAt: bonus.ExpiresAt,
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
func ConvertCreateBonus(bonus CreateBonus) dbgen.CreateUserBonusParams {
|
func ConvertCreateBonus(bonus CreateBonus) dbgen.CreateUserBonusParams {
|
||||||
return dbgen.CreateUserBonusParams{
|
return dbgen.CreateUserBonusParams{
|
||||||
Name: bonus.Name,
|
Name: bonus.Name,
|
||||||
Description: bonus.Description,
|
Description: bonus.Description,
|
||||||
|
Type: string(bonus.Type),
|
||||||
UserID: bonus.UserID,
|
UserID: bonus.UserID,
|
||||||
BonusCode: bonus.BonusCode,
|
|
||||||
RewardAmount: int64(bonus.RewardAmount),
|
RewardAmount: int64(bonus.RewardAmount),
|
||||||
ExpiresAt: pgtype.Timestamp{
|
ExpiresAt: pgtype.Timestamp{
|
||||||
Time: bonus.ExpiresAt,
|
Time: bonus.ExpiresAt,
|
||||||
|
|
@ -93,11 +110,12 @@ func ConvertCreateBonus(bonus CreateBonus) dbgen.CreateUserBonusParams {
|
||||||
|
|
||||||
func ConvertDBBonus(bonus dbgen.UserBonuse) UserBonus {
|
func ConvertDBBonus(bonus dbgen.UserBonuse) UserBonus {
|
||||||
return UserBonus{
|
return UserBonus{
|
||||||
ID: bonus.ID,
|
ID: bonus.ID,
|
||||||
Name: bonus.Name,
|
Name: bonus.Name,
|
||||||
Description: bonus.Description,
|
Description: bonus.Description,
|
||||||
UserID: bonus.UserID,
|
Type: BonusType(bonus.Type),
|
||||||
BonusCode: bonus.BonusCode,
|
UserID: bonus.UserID,
|
||||||
|
|
||||||
RewardAmount: Currency(bonus.RewardAmount),
|
RewardAmount: Currency(bonus.RewardAmount),
|
||||||
IsClaimed: bonus.IsClaimed,
|
IsClaimed: bonus.IsClaimed,
|
||||||
ExpiresAt: bonus.ExpiresAt.Time,
|
ExpiresAt: bonus.ExpiresAt.Time,
|
||||||
|
|
@ -117,6 +135,8 @@ func ConvertDBBonuses(bonuses []dbgen.UserBonuse) []UserBonus {
|
||||||
type BonusFilter struct {
|
type BonusFilter struct {
|
||||||
UserID ValidInt64
|
UserID ValidInt64
|
||||||
CompanyID ValidInt64
|
CompanyID ValidInt64
|
||||||
|
Limit ValidInt
|
||||||
|
Offset ValidInt
|
||||||
}
|
}
|
||||||
|
|
||||||
type BonusStats struct {
|
type BonusStats struct {
|
||||||
|
|
@ -126,6 +146,22 @@ type BonusStats struct {
|
||||||
ExpiredBonuses int64
|
ExpiredBonuses int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BonusStatsRes struct {
|
||||||
|
TotalBonus int64 `json:"total_bonus"`
|
||||||
|
TotalRewardAmount float32 `json:"total_reward_amount"`
|
||||||
|
ClaimedBonuses int64 `json:"claimed_bonuses"`
|
||||||
|
ExpiredBonuses int64 `json:"expired_bonuses"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConvertToBonusStatsRes(bonus BonusStats) BonusStatsRes {
|
||||||
|
return BonusStatsRes{
|
||||||
|
TotalBonus: bonus.TotalBonus,
|
||||||
|
TotalRewardAmount: bonus.TotalRewardAmount.Float32(),
|
||||||
|
ClaimedBonuses: bonus.ClaimedBonuses,
|
||||||
|
ExpiredBonuses: bonus.ExpiredBonuses,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func ConvertDBBonusStats(stats dbgen.GetBonusStatsRow) BonusStats {
|
func ConvertDBBonusStats(stats dbgen.GetBonusStatsRow) BonusStats {
|
||||||
return BonusStats{
|
return BonusStats{
|
||||||
TotalBonus: stats.TotalBonuses,
|
TotalBonus: stats.TotalBonuses,
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ const (
|
||||||
NOTIFICATION_TYPE_BET_RESULT NotificationType = "bet_result"
|
NOTIFICATION_TYPE_BET_RESULT NotificationType = "bet_result"
|
||||||
NOTIFICATION_TYPE_TRANSFER_REJECTED NotificationType = "transfer_rejected"
|
NOTIFICATION_TYPE_TRANSFER_REJECTED NotificationType = "transfer_rejected"
|
||||||
NOTIFICATION_TYPE_APPROVAL_REQUIRED NotificationType = "approval_required"
|
NOTIFICATION_TYPE_APPROVAL_REQUIRED NotificationType = "approval_required"
|
||||||
|
NOTIFICATION_TYPE_BONUS_AWARDED NotificationType = "bonus_awarded"
|
||||||
|
|
||||||
NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
|
NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
|
||||||
NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
|
NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
|
||||||
|
|
@ -73,7 +74,7 @@ type Notification struct {
|
||||||
RecipientID int64 `json:"recipient_id"`
|
RecipientID int64 `json:"recipient_id"`
|
||||||
Type NotificationType `json:"type"`
|
Type NotificationType `json:"type"`
|
||||||
Level NotificationLevel `json:"level"`
|
Level NotificationLevel `json:"level"`
|
||||||
ErrorSeverity NotificationErrorSeverity `json:"error_severity"`
|
ErrorSeverity NotificationErrorSeverity `json:"error_severity"`
|
||||||
Reciever NotificationRecieverSide `json:"reciever"`
|
Reciever NotificationRecieverSide `json:"reciever"`
|
||||||
IsRead bool `json:"is_read"`
|
IsRead bool `json:"is_read"`
|
||||||
DeliveryStatus NotificationDeliveryStatus `json:"delivery_status,omitempty"`
|
DeliveryStatus NotificationDeliveryStatus `json:"delivery_status,omitempty"`
|
||||||
|
|
|
||||||
|
|
@ -39,117 +39,147 @@ type SettingList struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SettingListRes struct {
|
type SettingListRes struct {
|
||||||
SMSProvider SMSProvider `json:"sms_provider"`
|
SMSProvider SMSProvider `json:"sms_provider"`
|
||||||
MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"`
|
MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"`
|
||||||
BetAmountLimit float32 `json:"bet_amount_limit"`
|
BetAmountLimit float32 `json:"bet_amount_limit"`
|
||||||
DailyTicketPerIP int64 `json:"daily_ticket_limit"`
|
DailyTicketPerIP int64 `json:"daily_ticket_limit"`
|
||||||
TotalWinningLimit float32 `json:"total_winning_limit"`
|
TotalWinningLimit float32 `json:"total_winning_limit"`
|
||||||
AmountForBetReferral float32 `json:"amount_for_bet_referral"`
|
AmountForBetReferral float32 `json:"amount_for_bet_referral"`
|
||||||
CashbackAmountCap float32 `json:"cashback_amount_cap"`
|
CashbackAmountCap float32 `json:"cashback_amount_cap"`
|
||||||
DefaultWinningLimit int64 `json:"default_winning_limit"`
|
DefaultWinningLimit int64 `json:"default_winning_limit"`
|
||||||
ReferralRewardAmount float32 `json:"referral_reward_amount"`
|
ReferralRewardAmount float32 `json:"referral_reward_amount"`
|
||||||
CashbackPercentage float32 `json:"cashback_percentage"`
|
CashbackPercentage float32 `json:"cashback_percentage"`
|
||||||
DefaultMaxReferrals int64 `json:"default_max_referrals"`
|
DefaultMaxReferrals int64 `json:"default_max_referrals"`
|
||||||
MinimumBetAmount float32 `json:"minimum_bet_amount"`
|
MinimumBetAmount float32 `json:"minimum_bet_amount"`
|
||||||
BetDuplicateLimit int64 `json:"bet_duplicate_limit"`
|
BetDuplicateLimit int64 `json:"bet_duplicate_limit"`
|
||||||
SendEmailOnBetFinish bool `json:"send_email_on_bet_finish"`
|
SendEmailOnBetFinish bool `json:"send_email_on_bet_finish"`
|
||||||
SendSMSOnBetFinish bool `json:"send_sms_on_bet_finish"`
|
SendSMSOnBetFinish bool `json:"send_sms_on_bet_finish"`
|
||||||
|
WelcomeBonusActive bool `json:"welcome_bonus_active"`
|
||||||
|
WelcomeBonusMultiplier float32 `json:"welcome_bonus_multiplier"`
|
||||||
|
WelcomeBonusCap float32 `json:"welcome_bonus_cap"`
|
||||||
|
WelcomeBonusCount int64 `json:"welcome_bonus_count"`
|
||||||
|
WelcomeBonusExpire int64 `json:"welcome_bonus_expiry"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConvertSettingListRes(settings SettingList) SettingListRes {
|
func ConvertSettingListRes(settings SettingList) SettingListRes {
|
||||||
return SettingListRes{
|
return SettingListRes{
|
||||||
SMSProvider: settings.SMSProvider,
|
SMSProvider: settings.SMSProvider,
|
||||||
MaxNumberOfOutcomes: settings.MaxNumberOfOutcomes,
|
MaxNumberOfOutcomes: settings.MaxNumberOfOutcomes,
|
||||||
BetAmountLimit: settings.BetAmountLimit.Float32(),
|
BetAmountLimit: settings.BetAmountLimit.Float32(),
|
||||||
DailyTicketPerIP: settings.DailyTicketPerIP,
|
DailyTicketPerIP: settings.DailyTicketPerIP,
|
||||||
TotalWinningLimit: settings.TotalWinningLimit.Float32(),
|
TotalWinningLimit: settings.TotalWinningLimit.Float32(),
|
||||||
AmountForBetReferral: settings.AmountForBetReferral.Float32(),
|
AmountForBetReferral: settings.AmountForBetReferral.Float32(),
|
||||||
CashbackAmountCap: settings.CashbackAmountCap.Float32(),
|
CashbackAmountCap: settings.CashbackAmountCap.Float32(),
|
||||||
DefaultWinningLimit: settings.DefaultWinningLimit,
|
DefaultWinningLimit: settings.DefaultWinningLimit,
|
||||||
ReferralRewardAmount: settings.ReferralRewardAmount.Float32(),
|
ReferralRewardAmount: settings.ReferralRewardAmount.Float32(),
|
||||||
CashbackPercentage: settings.CashbackPercentage,
|
CashbackPercentage: settings.CashbackPercentage,
|
||||||
DefaultMaxReferrals: settings.DefaultMaxReferrals,
|
DefaultMaxReferrals: settings.DefaultMaxReferrals,
|
||||||
MinimumBetAmount: settings.MinimumBetAmount.Float32(),
|
MinimumBetAmount: settings.MinimumBetAmount.Float32(),
|
||||||
BetDuplicateLimit: settings.BetDuplicateLimit,
|
BetDuplicateLimit: settings.BetDuplicateLimit,
|
||||||
SendEmailOnBetFinish: settings.SendEmailOnBetFinish,
|
SendEmailOnBetFinish: settings.SendEmailOnBetFinish,
|
||||||
SendSMSOnBetFinish: settings.SendSMSOnBetFinish,
|
SendSMSOnBetFinish: settings.SendSMSOnBetFinish,
|
||||||
|
WelcomeBonusActive: settings.WelcomeBonusActive,
|
||||||
|
WelcomeBonusMultiplier: settings.WelcomeBonusMultiplier,
|
||||||
|
WelcomeBonusCap: settings.WelcomeBonusCap.Float32(),
|
||||||
|
WelcomeBonusCount: settings.WelcomeBonusCount,
|
||||||
|
WelcomeBonusExpire: settings.WelcomeBonusExpire,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type SaveSettingListReq struct {
|
type SaveSettingListReq struct {
|
||||||
SMSProvider *string `json:"sms_provider,omitempty"`
|
SMSProvider *string `json:"sms_provider,omitempty"`
|
||||||
MaxNumberOfOutcomes *int64 `json:"max_number_of_outcomes,omitempty"`
|
MaxNumberOfOutcomes *int64 `json:"max_number_of_outcomes,omitempty"`
|
||||||
BetAmountLimit *float32 `json:"bet_amount_limit,omitempty"`
|
BetAmountLimit *float32 `json:"bet_amount_limit,omitempty"`
|
||||||
DailyTicketPerIP *int64 `json:"daily_ticket_limit,omitempty"`
|
DailyTicketPerIP *int64 `json:"daily_ticket_limit,omitempty"`
|
||||||
TotalWinningLimit *float32 `json:"total_winning_limit,omitempty"`
|
TotalWinningLimit *float32 `json:"total_winning_limit,omitempty"`
|
||||||
AmountForBetReferral *float32 `json:"amount_for_bet_referral,omitempty"`
|
AmountForBetReferral *float32 `json:"amount_for_bet_referral,omitempty"`
|
||||||
CashbackAmountCap *float32 `json:"cashback_amount_cap,omitempty"`
|
CashbackAmountCap *float32 `json:"cashback_amount_cap,omitempty"`
|
||||||
DefaultWinningLimit *int64 `json:"default_winning_limit,omitempty"`
|
DefaultWinningLimit *int64 `json:"default_winning_limit,omitempty"`
|
||||||
ReferralRewardAmount *float32 `json:"referral_reward_amount"`
|
ReferralRewardAmount *float32 `json:"referral_reward_amount"`
|
||||||
CashbackPercentage *float32 `json:"cashback_percentage"`
|
CashbackPercentage *float32 `json:"cashback_percentage"`
|
||||||
DefaultMaxReferrals *int64 `json:"default_max_referrals"`
|
DefaultMaxReferrals *int64 `json:"default_max_referrals"`
|
||||||
MinimumBetAmount *float32 `json:"minimum_bet_amount"`
|
MinimumBetAmount *float32 `json:"minimum_bet_amount"`
|
||||||
BetDuplicateLimit *int64 `json:"bet_duplicate_limit"`
|
BetDuplicateLimit *int64 `json:"bet_duplicate_limit"`
|
||||||
SendEmailOnBetFinish *bool `json:"send_email_on_bet_finish"`
|
SendEmailOnBetFinish *bool `json:"send_email_on_bet_finish"`
|
||||||
SendSMSOnBetFinish *bool `json:"send_sms_on_bet_finish"`
|
SendSMSOnBetFinish *bool `json:"send_sms_on_bet_finish"`
|
||||||
|
WelcomeBonusActive *bool `json:"welcome_bonus_active"`
|
||||||
|
WelcomeBonusMultiplier *float32 `json:"welcome_bonus_multiplier"`
|
||||||
|
WelcomeBonusCap *float32 `json:"welcome_bonus_cap"`
|
||||||
|
WelcomeBonusCount *int64 `json:"welcome_bonus_count"`
|
||||||
|
WelcomeBonusExpire *int64 `json:"welcome_bonus_expiry"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ValidSettingList struct {
|
type ValidSettingList struct {
|
||||||
SMSProvider ValidString
|
SMSProvider ValidString
|
||||||
MaxNumberOfOutcomes ValidInt64
|
MaxNumberOfOutcomes ValidInt64
|
||||||
BetAmountLimit ValidCurrency
|
BetAmountLimit ValidCurrency
|
||||||
DailyTicketPerIP ValidInt64
|
DailyTicketPerIP ValidInt64
|
||||||
TotalWinningLimit ValidCurrency
|
TotalWinningLimit ValidCurrency
|
||||||
AmountForBetReferral ValidCurrency
|
AmountForBetReferral ValidCurrency
|
||||||
CashbackAmountCap ValidCurrency
|
CashbackAmountCap ValidCurrency
|
||||||
DefaultWinningLimit ValidInt64
|
DefaultWinningLimit ValidInt64
|
||||||
ReferralRewardAmount ValidCurrency
|
ReferralRewardAmount ValidCurrency
|
||||||
CashbackPercentage ValidFloat32
|
CashbackPercentage ValidFloat32
|
||||||
DefaultMaxReferrals ValidInt64
|
DefaultMaxReferrals ValidInt64
|
||||||
MinimumBetAmount ValidCurrency
|
MinimumBetAmount ValidCurrency
|
||||||
BetDuplicateLimit ValidInt64
|
BetDuplicateLimit ValidInt64
|
||||||
SendEmailOnBetFinish ValidBool
|
SendEmailOnBetFinish ValidBool
|
||||||
SendSMSOnBetFinish ValidBool
|
SendSMSOnBetFinish ValidBool
|
||||||
|
WelcomeBonusActive ValidBool
|
||||||
|
WelcomeBonusMultiplier ValidFloat32
|
||||||
|
WelcomeBonusCap ValidCurrency
|
||||||
|
WelcomeBonusCount ValidInt64
|
||||||
|
WelcomeBonusExpire ValidInt64
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConvertSaveSettingListReq(settings SaveSettingListReq) ValidSettingList {
|
func ConvertSaveSettingListReq(settings SaveSettingListReq) ValidSettingList {
|
||||||
return ValidSettingList{
|
return ValidSettingList{
|
||||||
SMSProvider: ConvertStringPtr(settings.SMSProvider),
|
SMSProvider: ConvertStringPtr(settings.SMSProvider),
|
||||||
MaxNumberOfOutcomes: ConvertInt64Ptr(settings.MaxNumberOfOutcomes),
|
MaxNumberOfOutcomes: ConvertInt64Ptr(settings.MaxNumberOfOutcomes),
|
||||||
BetAmountLimit: ConvertFloat32PtrToCurrency(settings.BetAmountLimit),
|
BetAmountLimit: ConvertFloat32PtrToCurrency(settings.BetAmountLimit),
|
||||||
DailyTicketPerIP: ConvertInt64Ptr(settings.DailyTicketPerIP),
|
DailyTicketPerIP: ConvertInt64Ptr(settings.DailyTicketPerIP),
|
||||||
TotalWinningLimit: ConvertFloat32PtrToCurrency(settings.TotalWinningLimit),
|
TotalWinningLimit: ConvertFloat32PtrToCurrency(settings.TotalWinningLimit),
|
||||||
AmountForBetReferral: ConvertFloat32PtrToCurrency(settings.AmountForBetReferral),
|
AmountForBetReferral: ConvertFloat32PtrToCurrency(settings.AmountForBetReferral),
|
||||||
CashbackAmountCap: ConvertFloat32PtrToCurrency(settings.CashbackAmountCap),
|
CashbackAmountCap: ConvertFloat32PtrToCurrency(settings.CashbackAmountCap),
|
||||||
DefaultWinningLimit: ConvertInt64Ptr(settings.DefaultWinningLimit),
|
DefaultWinningLimit: ConvertInt64Ptr(settings.DefaultWinningLimit),
|
||||||
ReferralRewardAmount: ConvertFloat32PtrToCurrency(settings.ReferralRewardAmount),
|
ReferralRewardAmount: ConvertFloat32PtrToCurrency(settings.ReferralRewardAmount),
|
||||||
CashbackPercentage: ConvertFloat32Ptr(settings.CashbackPercentage),
|
CashbackPercentage: ConvertFloat32Ptr(settings.CashbackPercentage),
|
||||||
DefaultMaxReferrals: ConvertInt64Ptr(settings.DefaultMaxReferrals),
|
DefaultMaxReferrals: ConvertInt64Ptr(settings.DefaultMaxReferrals),
|
||||||
MinimumBetAmount: ConvertFloat32PtrToCurrency(settings.MinimumBetAmount),
|
MinimumBetAmount: ConvertFloat32PtrToCurrency(settings.MinimumBetAmount),
|
||||||
BetDuplicateLimit: ConvertInt64Ptr(settings.BetDuplicateLimit),
|
BetDuplicateLimit: ConvertInt64Ptr(settings.BetDuplicateLimit),
|
||||||
SendEmailOnBetFinish: ConvertBoolPtr(settings.SendEmailOnBetFinish),
|
SendEmailOnBetFinish: ConvertBoolPtr(settings.SendEmailOnBetFinish),
|
||||||
SendSMSOnBetFinish: ConvertBoolPtr(settings.SendSMSOnBetFinish),
|
SendSMSOnBetFinish: ConvertBoolPtr(settings.SendSMSOnBetFinish),
|
||||||
|
WelcomeBonusActive: ConvertBoolPtr(settings.WelcomeBonusActive),
|
||||||
|
WelcomeBonusMultiplier: ConvertFloat32Ptr(settings.WelcomeBonusMultiplier),
|
||||||
|
WelcomeBonusCap: ConvertFloat32PtrToCurrency(settings.WelcomeBonusCap),
|
||||||
|
WelcomeBonusCount: ConvertInt64Ptr(settings.WelcomeBonusCount),
|
||||||
|
WelcomeBonusExpire: ConvertInt64Ptr(settings.WelcomeBonusExpire),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always make sure to run the validation before converting this
|
// Always make sure to run the validation before converting this
|
||||||
func (vsl *ValidSettingList) ToSettingList() SettingList {
|
func (vsl *ValidSettingList) ToSettingList() SettingList {
|
||||||
return SettingList{
|
return SettingList{
|
||||||
SMSProvider: SMSProvider(vsl.SMSProvider.Value),
|
SMSProvider: SMSProvider(vsl.SMSProvider.Value),
|
||||||
MaxNumberOfOutcomes: vsl.MaxNumberOfOutcomes.Value,
|
MaxNumberOfOutcomes: vsl.MaxNumberOfOutcomes.Value,
|
||||||
BetAmountLimit: vsl.BetAmountLimit.Value,
|
BetAmountLimit: vsl.BetAmountLimit.Value,
|
||||||
DailyTicketPerIP: vsl.DailyTicketPerIP.Value,
|
DailyTicketPerIP: vsl.DailyTicketPerIP.Value,
|
||||||
TotalWinningLimit: vsl.TotalWinningLimit.Value,
|
TotalWinningLimit: vsl.TotalWinningLimit.Value,
|
||||||
AmountForBetReferral: vsl.AmountForBetReferral.Value,
|
AmountForBetReferral: vsl.AmountForBetReferral.Value,
|
||||||
CashbackAmountCap: vsl.CashbackAmountCap.Value,
|
CashbackAmountCap: vsl.CashbackAmountCap.Value,
|
||||||
DefaultWinningLimit: vsl.DefaultWinningLimit.Value,
|
DefaultWinningLimit: vsl.DefaultWinningLimit.Value,
|
||||||
ReferralRewardAmount: vsl.ReferralRewardAmount.Value,
|
ReferralRewardAmount: vsl.ReferralRewardAmount.Value,
|
||||||
CashbackPercentage: vsl.CashbackPercentage.Value,
|
CashbackPercentage: vsl.CashbackPercentage.Value,
|
||||||
DefaultMaxReferrals: vsl.DefaultMaxReferrals.Value,
|
DefaultMaxReferrals: vsl.DefaultMaxReferrals.Value,
|
||||||
MinimumBetAmount: vsl.MinimumBetAmount.Value,
|
MinimumBetAmount: vsl.MinimumBetAmount.Value,
|
||||||
BetDuplicateLimit: vsl.BetDuplicateLimit.Value,
|
BetDuplicateLimit: vsl.BetDuplicateLimit.Value,
|
||||||
SendEmailOnBetFinish: vsl.SendEmailOnBetFinish.Value,
|
SendEmailOnBetFinish: vsl.SendEmailOnBetFinish.Value,
|
||||||
SendSMSOnBetFinish: vsl.SendSMSOnBetFinish.Value,
|
SendSMSOnBetFinish: vsl.SendSMSOnBetFinish.Value,
|
||||||
|
WelcomeBonusActive: vsl.WelcomeBonusActive.Value,
|
||||||
|
WelcomeBonusMultiplier: vsl.WelcomeBonusMultiplier.Value,
|
||||||
|
WelcomeBonusCap: vsl.WelcomeBonusCap.Value,
|
||||||
|
WelcomeBonusCount: vsl.WelcomeBonusCount.Value,
|
||||||
|
WelcomeBonusExpire: vsl.WelcomeBonusExpire.Value,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,6 +198,8 @@ func (vsl *ValidSettingList) GetInt64SettingsMap() map[string]*ValidInt64 {
|
||||||
"default_winning_limit": &vsl.DefaultWinningLimit,
|
"default_winning_limit": &vsl.DefaultWinningLimit,
|
||||||
"default_max_referrals": &vsl.DefaultMaxReferrals,
|
"default_max_referrals": &vsl.DefaultMaxReferrals,
|
||||||
"bet_duplicate_limit": &vsl.BetDuplicateLimit,
|
"bet_duplicate_limit": &vsl.BetDuplicateLimit,
|
||||||
|
"welcome_bonus_count": &vsl.WelcomeBonusCount,
|
||||||
|
"welcome_bonus_expiry": &vsl.WelcomeBonusExpire,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,6 +211,7 @@ func (vsl *ValidSettingList) GetCurrencySettingsMap() map[string]*ValidCurrency
|
||||||
"cashback_amount_cap": &vsl.CashbackAmountCap,
|
"cashback_amount_cap": &vsl.CashbackAmountCap,
|
||||||
"referral_reward_amount": &vsl.ReferralRewardAmount,
|
"referral_reward_amount": &vsl.ReferralRewardAmount,
|
||||||
"minimum_bet_amount": &vsl.MinimumBetAmount,
|
"minimum_bet_amount": &vsl.MinimumBetAmount,
|
||||||
|
"welcome_bonus_cap": &vsl.WelcomeBonusCap,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,12 +225,14 @@ func (vsl *ValidSettingList) GetBoolSettingsMap() map[string]*ValidBool {
|
||||||
return map[string]*ValidBool{
|
return map[string]*ValidBool{
|
||||||
"send_email_on_bet_finish": &vsl.SendEmailOnBetFinish,
|
"send_email_on_bet_finish": &vsl.SendEmailOnBetFinish,
|
||||||
"send_sms_on_bet_finish": &vsl.SendSMSOnBetFinish,
|
"send_sms_on_bet_finish": &vsl.SendSMSOnBetFinish,
|
||||||
|
"welcome_bonus_active": &vsl.WelcomeBonusActive,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (vsl *ValidSettingList) GetFloat32SettingsMap() map[string]*ValidFloat32 {
|
func (vsl *ValidSettingList) GetFloat32SettingsMap() map[string]*ValidFloat32 {
|
||||||
return map[string]*ValidFloat32{
|
return map[string]*ValidFloat32{
|
||||||
"cashback_percentage": &vsl.CashbackPercentage,
|
"cashback_percentage": &vsl.CashbackPercentage,
|
||||||
|
"welcome_bonus_multiplier": &vsl.WelcomeBonusMultiplier,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
package helpers
|
package helpers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
random "crypto/rand"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand/v2"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"math/big"
|
||||||
|
"math/rand/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GenerateID() string {
|
func GenerateID() string {
|
||||||
|
|
@ -24,3 +25,20 @@ func GenerateFastCode() string {
|
||||||
}
|
}
|
||||||
return code
|
return code
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenerateCashoutID() (string, error) {
|
||||||
|
const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
const length int = 13
|
||||||
|
charLen := big.NewInt(int64(len(chars)))
|
||||||
|
result := make([]byte, length)
|
||||||
|
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
index, err := random.Int(random.Reader, charLen)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
result[i] = chars[index.Int64()]
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(result), nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,12 @@ func (s *Store) CreateUserBonus(ctx context.Context, bonus domain.CreateBonus) (
|
||||||
return domain.ConvertDBBonus(newBonus), nil
|
return domain.ConvertDBBonus(newBonus), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetAllUserBonuses(ctx context.Context) ([]domain.UserBonus, error) {
|
func (s *Store) GetAllUserBonuses(ctx context.Context, filter domain.BonusFilter) ([]domain.UserBonus, error) {
|
||||||
bonuses, err := s.queries.GetAllUserBonuses(ctx)
|
bonuses, err := s.queries.GetAllUserBonuses(ctx, dbgen.GetAllUserBonusesParams{
|
||||||
|
UserID: filter.UserID.ToPG(),
|
||||||
|
Offset: filter.Offset.ToPG(),
|
||||||
|
Limit: filter.Limit.ToPG(),
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -27,13 +31,12 @@ func (s *Store) GetAllUserBonuses(ctx context.Context) ([]domain.UserBonus, erro
|
||||||
return domain.ConvertDBBonuses(bonuses), nil
|
return domain.ConvertDBBonuses(bonuses), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetBonusesByUserID(ctx context.Context, userID int64) ([]domain.UserBonus, error) {
|
func (s *Store) GetBonusCount(ctx context.Context, filter domain.BonusFilter) (int64, error) {
|
||||||
bonuses, err := s.queries.GetBonusesByUserID(ctx, userID)
|
count, err := s.queries.GetBonusCount(ctx, filter.UserID.ToPG())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
return count, nil
|
||||||
return domain.ConvertDBBonuses(bonuses), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetBonusByID(ctx context.Context, bonusID int64) (domain.UserBonus, error) {
|
func (s *Store) GetBonusByID(ctx context.Context, bonusID int64) (domain.UserBonus, error) {
|
||||||
|
|
@ -45,6 +48,8 @@ func (s *Store) GetBonusByID(ctx context.Context, bonusID int64) (domain.UserBon
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func (s *Store) GetBonusStats(ctx context.Context, filter domain.BonusFilter) (domain.BonusStats, error) {
|
func (s *Store) GetBonusStats(ctx context.Context, filter domain.BonusFilter) (domain.BonusStats, error) {
|
||||||
bonus, err := s.queries.GetBonusStats(ctx, dbgen.GetBonusStatsParams{
|
bonus, err := s.queries.GetBonusStats(ctx, dbgen.GetBonusStatsParams{
|
||||||
CompanyID: filter.CompanyID.ToPG(),
|
CompanyID: filter.CompanyID.ToPG(),
|
||||||
|
|
|
||||||
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++ {
|
for i := 0; i < length; i++ {
|
||||||
index, err := rand.Int(rand.Reader, charLen)
|
index, err := rand.Int(rand.Reader, charLen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.mongoLogger.Error("failed to generate random index for cashout ID",
|
|
||||||
zap.Int("position", i),
|
|
||||||
zap.Error(err),
|
|
||||||
)
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
result[i] = chars[index.Int64()]
|
result[i] = chars[index.Int64()]
|
||||||
|
|
@ -957,241 +953,6 @@ func (s *Service) UpdateStatus(ctx context.Context, betId int64, status domain.O
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newBetResultNotification(userID int64, level domain.NotificationLevel, channel domain.DeliveryChannel, headline, message string, metadata any) *domain.Notification {
|
|
||||||
raw, _ := json.Marshal(metadata)
|
|
||||||
return &domain.Notification{
|
|
||||||
RecipientID: userID,
|
|
||||||
DeliveryStatus: domain.DeliveryStatusPending,
|
|
||||||
IsRead: false,
|
|
||||||
Type: domain.NOTIFICATION_TYPE_BET_RESULT,
|
|
||||||
Level: level,
|
|
||||||
Reciever: domain.NotificationRecieverSideCustomer,
|
|
||||||
DeliveryChannel: channel,
|
|
||||||
Payload: domain.NotificationPayload{
|
|
||||||
Headline: headline,
|
|
||||||
Message: message,
|
|
||||||
},
|
|
||||||
Priority: 2,
|
|
||||||
Metadata: raw,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type SendResultNotificationParam struct {
|
|
||||||
BetID int64
|
|
||||||
Status domain.OutcomeStatus
|
|
||||||
UserID int64
|
|
||||||
WinningAmount domain.Currency
|
|
||||||
Extra string
|
|
||||||
SendEmail bool
|
|
||||||
SendSMS bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p SendResultNotificationParam) Validate() error {
|
|
||||||
if p.BetID == 0 {
|
|
||||||
return errors.New("BetID is required")
|
|
||||||
}
|
|
||||||
if p.UserID == 0 {
|
|
||||||
return errors.New("UserID is required")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldSend(channel domain.DeliveryChannel, sendEmail, sendSMS bool) bool {
|
|
||||||
switch {
|
|
||||||
case channel == domain.DeliveryChannelEmail && sendEmail:
|
|
||||||
return true
|
|
||||||
case channel == domain.DeliveryChannelSMS && sendSMS:
|
|
||||||
return true
|
|
||||||
case channel == domain.DeliveryChannelInApp:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) SendWinningStatusNotification(ctx context.Context, param SendResultNotificationParam) error {
|
|
||||||
if err := param.Validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var headline string
|
|
||||||
var message string
|
|
||||||
|
|
||||||
switch param.Status {
|
|
||||||
case domain.OUTCOME_STATUS_WIN:
|
|
||||||
headline = fmt.Sprintf("Bet #%v Won!", param.BetID)
|
|
||||||
message = fmt.Sprintf(
|
|
||||||
"Congratulations! Your bet #%v has won. %.2f has been credited to your wallet.",
|
|
||||||
param.BetID,
|
|
||||||
param.WinningAmount.Float32(),
|
|
||||||
)
|
|
||||||
case domain.OUTCOME_STATUS_HALF:
|
|
||||||
headline = fmt.Sprintf("Bet #%v Half-Win", param.BetID)
|
|
||||||
message = fmt.Sprintf(
|
|
||||||
"Your bet #%v resulted in a half-win. %.2f has been credited to your wallet.",
|
|
||||||
param.BetID,
|
|
||||||
param.WinningAmount.Float32(),
|
|
||||||
)
|
|
||||||
case domain.OUTCOME_STATUS_VOID:
|
|
||||||
headline = fmt.Sprintf("Bet #%v Refunded", param.BetID)
|
|
||||||
message = fmt.Sprintf(
|
|
||||||
"Your bet #%v has been voided. %.2f has been refunded to your wallet.",
|
|
||||||
param.BetID,
|
|
||||||
param.WinningAmount.Float32(),
|
|
||||||
)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported status: %v", param.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, channel := range []domain.DeliveryChannel{
|
|
||||||
domain.DeliveryChannelInApp,
|
|
||||||
domain.DeliveryChannelEmail,
|
|
||||||
domain.DeliveryChannelSMS,
|
|
||||||
} {
|
|
||||||
if !shouldSend(channel, param.SendEmail, param.SendSMS) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
n := newBetResultNotification(param.UserID, domain.NotificationLevelSuccess, channel, headline, message, map[string]any{
|
|
||||||
"winning_amount": param.WinningAmount.Float32(),
|
|
||||||
"status": param.Status,
|
|
||||||
"more": param.Extra,
|
|
||||||
})
|
|
||||||
if err := s.notificationSvc.SendNotification(ctx, n); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) SendLosingStatusNotification(ctx context.Context, param SendResultNotificationParam) error {
|
|
||||||
if err := param.Validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var headline string
|
|
||||||
var message string
|
|
||||||
|
|
||||||
switch param.Status {
|
|
||||||
case domain.OUTCOME_STATUS_LOSS:
|
|
||||||
headline = fmt.Sprintf("Bet #%v Lost", param.BetID)
|
|
||||||
message = "Unfortunately, your bet did not win this time. Better luck next time!"
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported status: %v", param.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, channel := range []domain.DeliveryChannel{
|
|
||||||
domain.DeliveryChannelInApp,
|
|
||||||
domain.DeliveryChannelEmail,
|
|
||||||
domain.DeliveryChannelSMS,
|
|
||||||
} {
|
|
||||||
if !shouldSend(channel, param.SendEmail, param.SendSMS) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
n := newBetResultNotification(param.UserID, domain.NotificationLevelWarning, channel, headline, message, map[string]any{
|
|
||||||
"status": param.Status,
|
|
||||||
"more": param.Extra,
|
|
||||||
})
|
|
||||||
if err := s.notificationSvc.SendNotification(ctx, n); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) SendErrorStatusNotification(ctx context.Context, betID int64, status domain.OutcomeStatus, userID int64, extra string) error {
|
|
||||||
|
|
||||||
var headline string
|
|
||||||
var message string
|
|
||||||
|
|
||||||
switch status {
|
|
||||||
case domain.OUTCOME_STATUS_ERROR, domain.OUTCOME_STATUS_PENDING:
|
|
||||||
headline = fmt.Sprintf("Bet #%v Processing Issue", betID)
|
|
||||||
message = "We encountered a problem while processing your bet. Our team is working to resolve it as soon as possible."
|
|
||||||
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported status: %v", status)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, channel := range []domain.DeliveryChannel{
|
|
||||||
domain.DeliveryChannelInApp,
|
|
||||||
domain.DeliveryChannelEmail,
|
|
||||||
} {
|
|
||||||
n := newBetResultNotification(userID, domain.NotificationLevelError, channel, headline, message, map[string]any{
|
|
||||||
"status": status,
|
|
||||||
"more": extra,
|
|
||||||
})
|
|
||||||
if err := s.notificationSvc.SendNotification(ctx, n); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) SendAdminAlertNotification(ctx context.Context, betID int64, status domain.OutcomeStatus, extra string, companyID int64) error {
|
|
||||||
|
|
||||||
var headline string
|
|
||||||
var message string
|
|
||||||
|
|
||||||
switch status {
|
|
||||||
case domain.OUTCOME_STATUS_ERROR, domain.OUTCOME_STATUS_PENDING:
|
|
||||||
headline = fmt.Sprintf("Processing Error for Bet #%v", betID)
|
|
||||||
message = "A processing error occurred with this bet. Please review and take corrective action."
|
|
||||||
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported status: %v", status)
|
|
||||||
}
|
|
||||||
|
|
||||||
super_admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{
|
|
||||||
Role: string(domain.RoleSuperAdmin),
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
s.mongoLogger.Error("failed to get super_admin recipients",
|
|
||||||
zap.Error(err),
|
|
||||||
zap.Time("timestamp", time.Now()),
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{
|
|
||||||
Role: string(domain.RoleAdmin),
|
|
||||||
CompanyID: domain.ValidInt64{
|
|
||||||
Value: companyID,
|
|
||||||
Valid: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
s.mongoLogger.Error("failed to get admin recipients",
|
|
||||||
zap.Error(err),
|
|
||||||
zap.Time("timestamp", time.Now()),
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
users := append(super_admin_users, admin_users...)
|
|
||||||
|
|
||||||
for _, user := range users {
|
|
||||||
for _, channel := range []domain.DeliveryChannel{
|
|
||||||
domain.DeliveryChannelInApp,
|
|
||||||
domain.DeliveryChannelEmail,
|
|
||||||
} {
|
|
||||||
n := newBetResultNotification(user.ID, domain.NotificationLevelError, channel, headline, message, map[string]any{
|
|
||||||
"status": status,
|
|
||||||
"more": extra,
|
|
||||||
})
|
|
||||||
if err := s.notificationSvc.SendNotification(ctx, n); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) CheckBetOutcomeForBet(ctx context.Context, betID int64) (domain.OutcomeStatus, error) {
|
func (s *Service) CheckBetOutcomeForBet(ctx context.Context, betID int64) (domain.OutcomeStatus, error) {
|
||||||
betOutcomes, err := s.betStore.GetBetOutcomeByBetID(ctx, betID)
|
betOutcomes, err := s.betStore.GetBetOutcomeByBetID(ctx, betID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
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 {
|
type BonusStore interface {
|
||||||
CreateUserBonus(ctx context.Context, bonus domain.CreateBonus) (domain.UserBonus, error)
|
CreateUserBonus(ctx context.Context, bonus domain.CreateBonus) (domain.UserBonus, error)
|
||||||
GetAllUserBonuses(ctx context.Context) ([]domain.UserBonus, error)
|
GetAllUserBonuses(ctx context.Context, filter domain.BonusFilter) ([]domain.UserBonus, error)
|
||||||
GetBonusesByUserID(ctx context.Context, userID int64) ([]domain.UserBonus, error)
|
GetBonusCount(ctx context.Context, filter domain.BonusFilter) (int64, error)
|
||||||
GetBonusByID(ctx context.Context, bonusID int64) (domain.UserBonus, error)
|
GetBonusByID(ctx context.Context, bonusID int64) (domain.UserBonus, error)
|
||||||
GetBonusStats(ctx context.Context, filter domain.BonusFilter) (domain.BonusStats, error)
|
GetBonusStats(ctx context.Context, filter domain.BonusFilter) (domain.BonusStats, error)
|
||||||
UpdateUserBonus(ctx context.Context, bonusID int64, IsClaimed bool) error
|
UpdateUserBonus(ctx context.Context, bonusID int64, IsClaimed bool) error
|
||||||
|
|
|
||||||
|
|
@ -3,29 +3,32 @@ package bonus
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers"
|
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
bonusStore BonusStore
|
bonusStore BonusStore
|
||||||
walletSvc *wallet.Service
|
walletSvc *wallet.Service
|
||||||
settingSvc *settings.Service
|
settingSvc *settings.Service
|
||||||
mongoLogger *zap.Logger
|
notificationSvc *notificationservice.Service
|
||||||
|
mongoLogger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(bonusStore BonusStore, walletSvc *wallet.Service, settingSvc *settings.Service, mongoLogger *zap.Logger) *Service {
|
func NewService(bonusStore BonusStore, walletSvc *wallet.Service, settingSvc *settings.Service, notificationSvc *notificationservice.Service, mongoLogger *zap.Logger) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
bonusStore: bonusStore,
|
bonusStore: bonusStore,
|
||||||
walletSvc: walletSvc,
|
walletSvc: walletSvc,
|
||||||
settingSvc: settingSvc,
|
settingSvc: settingSvc,
|
||||||
mongoLogger: mongoLogger,
|
notificationSvc: notificationSvc,
|
||||||
|
mongoLogger: mongoLogger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,7 +37,7 @@ var (
|
||||||
ErrWelcomeBonusCountReached = errors.New("welcome bonus max deposit count reached")
|
ErrWelcomeBonusCountReached = errors.New("welcome bonus max deposit count reached")
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) ProcessWelcomeBonus(ctx context.Context, amount domain.Currency, companyID int64, userID int64) error {
|
func (s *Service) CreateWelcomeBonus(ctx context.Context, amount domain.Currency, companyID int64, userID int64) error {
|
||||||
settingsList, err := s.settingSvc.GetOverrideSettingsList(ctx, companyID)
|
settingsList, err := s.settingSvc.GetOverrideSettingsList(ctx, companyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.mongoLogger.Error("Failed to get settings",
|
s.mongoLogger.Error("Failed to get settings",
|
||||||
|
|
@ -65,11 +68,10 @@ func (s *Service) ProcessWelcomeBonus(ctx context.Context, amount domain.Currenc
|
||||||
|
|
||||||
newBalance := math.Min(float64(amount)*float64(settingsList.WelcomeBonusMultiplier), float64(settingsList.WelcomeBonusCap))
|
newBalance := math.Min(float64(amount)*float64(settingsList.WelcomeBonusMultiplier), float64(settingsList.WelcomeBonusCap))
|
||||||
|
|
||||||
_, err = s.CreateUserBonus(ctx, domain.CreateBonus{
|
bonus, err := s.CreateUserBonus(ctx, domain.CreateBonus{
|
||||||
Name: "Welcome Bonus",
|
Name: "Welcome Bonus",
|
||||||
Description: "Awarded when the user logged in for the first time",
|
Description: fmt.Sprintf("Awarded for deposit number (%v / %v)", stats.TotalDeposits, settingsList.WelcomeBonusCount),
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
BonusCode: helpers.GenerateFastCode(),
|
|
||||||
RewardAmount: domain.Currency(newBalance),
|
RewardAmount: domain.Currency(newBalance),
|
||||||
ExpiresAt: time.Now().Add(time.Duration(settingsList.WelcomeBonusExpire) * 24 * time.Hour),
|
ExpiresAt: time.Now().Add(time.Duration(settingsList.WelcomeBonusExpire) * 24 * time.Hour),
|
||||||
})
|
})
|
||||||
|
|
@ -78,26 +80,73 @@ func (s *Service) ProcessWelcomeBonus(ctx context.Context, amount domain.Currenc
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add a claim function that adds to the static wallet when the user inputs his bonus code
|
err = s.SendBonusNotification(ctx, SendBonusNotificationParam{
|
||||||
// _, err = s.walletSvc.AddToWallet(ctx, wallet.StaticID, domain.ToCurrency(float32(newBalance)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{},
|
BonusID: bonus.ID,
|
||||||
// fmt.Sprintf("Added %v to static wallet because of deposit bonus using multiplier %v", newBalance, settingsList.WelcomeBonusMultiplier),
|
UserID: userID,
|
||||||
// )
|
Type: domain.DepositBonus,
|
||||||
// if err != nil {
|
Amount: domain.Currency(newBalance),
|
||||||
// return err
|
SendEmail: true,
|
||||||
// }
|
SendSMS: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrBonusIsAlreadyClaimed = errors.New("bonus is already claimed")
|
||||||
|
ErrBonusUserIDNotMatch = errors.New("bonus user id is not a match")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) ProcessBonusClaim(ctx context.Context, bonusID, userID int64) error {
|
||||||
|
|
||||||
|
bonus, err := s.GetBonusByID(ctx, bonusID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if bonus.UserID != userID {
|
||||||
|
|
||||||
|
}
|
||||||
|
if bonus.IsClaimed {
|
||||||
|
return ErrBonusIsAlreadyClaimed
|
||||||
|
}
|
||||||
|
|
||||||
|
wallet, err := s.walletSvc.GetCustomerWallet(ctx, bonus.UserID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.walletSvc.AddToWallet(
|
||||||
|
ctx, wallet.StaticID, bonus.RewardAmount,
|
||||||
|
domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{},
|
||||||
|
fmt.Sprintf("Added %v to bonus wallet due to %v", bonus.RewardAmount, bonus.Type),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.UpdateUserBonus(ctx, bonusID, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CreateUserBonus(ctx context.Context, bonus domain.CreateBonus) (domain.UserBonus, error) {
|
func (s *Service) CreateUserBonus(ctx context.Context, bonus domain.CreateBonus) (domain.UserBonus, error) {
|
||||||
return s.bonusStore.CreateUserBonus(ctx, bonus)
|
return s.bonusStore.CreateUserBonus(ctx, bonus)
|
||||||
}
|
}
|
||||||
func (s *Service) GetAllUserBonuses(ctx context.Context) ([]domain.UserBonus, error) {
|
func (s *Service) GetAllUserBonuses(ctx context.Context, filter domain.BonusFilter) ([]domain.UserBonus, error) {
|
||||||
return s.bonusStore.GetAllUserBonuses(ctx)
|
return s.bonusStore.GetAllUserBonuses(ctx, filter)
|
||||||
}
|
}
|
||||||
func (s *Service) GetBonusesByUserID(ctx context.Context, userID int64) ([]domain.UserBonus, error) {
|
|
||||||
return s.bonusStore.GetBonusesByUserID(ctx, userID)
|
func (s *Service) GetBonusCount(ctx context.Context, filter domain.BonusFilter) (int64, error) {
|
||||||
|
return s.bonusStore.GetBonusCount(ctx, filter)
|
||||||
}
|
}
|
||||||
func (s *Service) GetBonusByID(ctx context.Context, bonusID int64) (domain.UserBonus, error) {
|
func (s *Service) GetBonusByID(ctx context.Context, bonusID int64) (domain.UserBonus, error) {
|
||||||
return s.bonusStore.GetBonusByID(ctx, bonusID)
|
return s.bonusStore.GetBonusByID(ctx, bonusID)
|
||||||
|
|
|
||||||
|
|
@ -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,21 +22,21 @@ var (
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
virtualGameSvc virtualgameservice.VirtualGameService
|
virtualGameSvc virtualgameservice.VirtualGameService
|
||||||
repo repository.VirtualGameRepository
|
repo repository.VirtualGameRepository
|
||||||
client *Client
|
client *Client
|
||||||
walletSvc *wallet.Service
|
walletSvc *wallet.Service
|
||||||
transfetStore wallet.TransferStore
|
transfetStore wallet.TransferStore
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(virtualGameSvc virtualgameservice.VirtualGameService,repo repository.VirtualGameRepository,client *Client, walletSvc *wallet.Service, transferStore wallet.TransferStore, cfg *config.Config) *Service {
|
func New(virtualGameSvc virtualgameservice.VirtualGameService, repo repository.VirtualGameRepository, client *Client, walletSvc *wallet.Service, transferStore wallet.TransferStore, cfg *config.Config) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
virtualGameSvc: virtualGameSvc,
|
virtualGameSvc: virtualGameSvc,
|
||||||
repo: repo,
|
repo: repo,
|
||||||
client: client,
|
client: client,
|
||||||
walletSvc: walletSvc,
|
walletSvc: walletSvc,
|
||||||
transfetStore: transferStore,
|
transfetStore: transferStore,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,6 +80,8 @@ func (s *Service) GetGames(ctx context.Context, req domain.GameListRequest) ([]d
|
||||||
sigParams := map[string]any{
|
sigParams := map[string]any{
|
||||||
"brandId": req.BrandID,
|
"brandId": req.BrandID,
|
||||||
"providerId": req.ProviderID,
|
"providerId": req.ProviderID,
|
||||||
|
"size": req.Size,
|
||||||
|
"page": req.Page,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Call external API
|
// 3. Call external API
|
||||||
|
|
@ -128,7 +130,6 @@ func (s *Service) StartGame(ctx context.Context, req domain.GameStartRequest) (*
|
||||||
return &res, nil
|
return &res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (s *Service) StartDemoGame(ctx context.Context, req domain.DemoGameRequest) (*domain.GameStartResponse, error) {
|
func (s *Service) StartDemoGame(ctx context.Context, req domain.DemoGameRequest) (*domain.GameStartResponse, error) {
|
||||||
// 1. Check if provider is enabled in DB
|
// 1. Check if provider is enabled in DB
|
||||||
// provider, err := s.repo.GetVirtualGameProviderByID(ctx, req.ProviderID)
|
// provider, err := s.repo.GetVirtualGameProviderByID(ctx, req.ProviderID)
|
||||||
|
|
@ -160,7 +161,6 @@ func (s *Service) StartDemoGame(ctx context.Context, req domain.DemoGameRequest)
|
||||||
return &res, nil
|
return &res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (s *Service) GetBalance(ctx context.Context, req domain.BalanceRequest) (*domain.BalanceResponse, error) {
|
func (s *Service) GetBalance(ctx context.Context, req domain.BalanceRequest) (*domain.BalanceResponse, error) {
|
||||||
// Retrieve player's real balance from wallet Service
|
// Retrieve player's real balance from wallet Service
|
||||||
playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64)
|
playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64)
|
||||||
|
|
|
||||||
|
|
@ -26,32 +26,32 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
|
||||||
spec string
|
spec string
|
||||||
task func()
|
task func()
|
||||||
}{
|
}{
|
||||||
// {
|
{
|
||||||
// spec: "0 0 * * * *", // Every 1 hour
|
spec: "0 0 * * * *", // Every 1 hour
|
||||||
// task: func() {
|
task: func() {
|
||||||
// mongoLogger.Info("Began fetching upcoming events cron task")
|
mongoLogger.Info("Began fetching upcoming events cron task")
|
||||||
// if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
|
if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
|
||||||
// mongoLogger.Error("Failed to fetch upcoming events",
|
mongoLogger.Error("Failed to fetch upcoming events",
|
||||||
// zap.Error(err),
|
zap.Error(err),
|
||||||
// )
|
)
|
||||||
// } else {
|
} else {
|
||||||
// mongoLogger.Info("Completed fetching upcoming events without errors")
|
mongoLogger.Info("Completed fetching upcoming events without errors")
|
||||||
// }
|
}
|
||||||
// },
|
},
|
||||||
// },
|
},
|
||||||
// {
|
{
|
||||||
// spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events)
|
spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events)
|
||||||
// task: func() {
|
task: func() {
|
||||||
// mongoLogger.Info("Began fetching non live odds cron task")
|
mongoLogger.Info("Began fetching non live odds cron task")
|
||||||
// if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
|
if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
|
||||||
// mongoLogger.Error("Failed to fetch non live odds",
|
mongoLogger.Error("Failed to fetch non live odds",
|
||||||
// zap.Error(err),
|
zap.Error(err),
|
||||||
// )
|
)
|
||||||
// } else {
|
} else {
|
||||||
// mongoLogger.Info("Completed fetching non live odds without errors")
|
mongoLogger.Info("Completed fetching non live odds without errors")
|
||||||
// }
|
}
|
||||||
// },
|
},
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
spec: "0 */5 * * * *", // Every 5 Minutes
|
spec: "0 */5 * * * *", // Every 5 Minutes
|
||||||
task: func() {
|
task: func() {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
// import (
|
import (
|
||||||
// "time"
|
"strconv"
|
||||||
|
|
||||||
// "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
// "github.com/gofiber/fiber/v2"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
|
||||||
// "go.uber.org/zap"
|
"github.com/gofiber/fiber/v2"
|
||||||
// )
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
// func (h *Handler) CreateBonusMultiplier(c *fiber.Ctx) error {
|
// func (h *Handler) CreateBonusMultiplier(c *fiber.Ctx) error {
|
||||||
// var req struct {
|
// var req struct {
|
||||||
|
|
@ -96,3 +97,110 @@ package handlers
|
||||||
|
|
||||||
// return response.WriteJSON(c, fiber.StatusOK, "Updated bonus multiplier successfully", nil, nil)
|
// return response.WriteJSON(c, fiber.StatusOK, "Updated bonus multiplier successfully", nil, nil)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
func (h *Handler) GetBonusesByUserID(c *fiber.Ctx) error {
|
||||||
|
userID, ok := c.Locals("user_id").(int64)
|
||||||
|
if !ok || userID == 0 {
|
||||||
|
h.InternalServerErrorLogger().Error("Invalid user ID in context",
|
||||||
|
zap.Int64("userID", userID),
|
||||||
|
)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification")
|
||||||
|
}
|
||||||
|
|
||||||
|
page := c.QueryInt("page", 1)
|
||||||
|
pageSize := c.QueryInt("page_size", 10)
|
||||||
|
limit := domain.ValidInt{
|
||||||
|
Value: pageSize,
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
offset := domain.ValidInt{
|
||||||
|
Value: page - 1,
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := domain.BonusFilter{
|
||||||
|
UserID: domain.ValidInt64{
|
||||||
|
Value: userID,
|
||||||
|
Valid: true,
|
||||||
|
},
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
}
|
||||||
|
|
||||||
|
bonuses, err := h.bonusSvc.GetAllUserBonuses(c.Context(), filter)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
h.InternalServerErrorLogger().Error("Failed to bonus by userID", zap.Int64("userId", userID))
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to get bonus by user ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := h.bonusSvc.GetBonusCount(c.Context(), filter)
|
||||||
|
if err != nil {
|
||||||
|
h.InternalServerErrorLogger().Error("Failed to get bonus count", zap.Int64("userId", userID))
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to get bonus count by user ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
res := domain.ConvertToBonusResList(bonuses)
|
||||||
|
|
||||||
|
return response.WritePaginatedJSON(c, fiber.StatusOK, "Fetched User Bonuses", res, nil, page, int(count))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GetBonusStats(c *fiber.Ctx) error {
|
||||||
|
userID, ok := c.Locals("user_id").(int64)
|
||||||
|
if !ok || userID == 0 {
|
||||||
|
h.InternalServerErrorLogger().Error("Invalid user ID in context",
|
||||||
|
zap.Int64("userID", userID),
|
||||||
|
)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification")
|
||||||
|
}
|
||||||
|
|
||||||
|
stats, err := h.bonusSvc.GetBonusStats(c.Context(), domain.BonusFilter{
|
||||||
|
UserID: domain.ValidInt64{
|
||||||
|
Value: userID,
|
||||||
|
Valid: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
h.InternalServerErrorLogger().Error("Failed to get bonus stats",
|
||||||
|
zap.Int64("userID", userID),
|
||||||
|
)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get bonus stats")
|
||||||
|
}
|
||||||
|
|
||||||
|
res := domain.ConvertToBonusStatsRes(stats)
|
||||||
|
|
||||||
|
return response.WriteJSON(c, fiber.StatusOK, "Get Bonus Stats", res, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// bonus/:id/claim
|
||||||
|
func (h *Handler) ClaimBonus(c *fiber.Ctx) error {
|
||||||
|
bonusIDParam := c.Params("id")
|
||||||
|
bonusID, err := strconv.ParseInt(bonusIDParam, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
h.BadRequestLogger().Error("Invalid bonus ID",
|
||||||
|
zap.Int64("bonusID", bonusID),
|
||||||
|
)
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid bonus id")
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, ok := c.Locals("user_id").(int64)
|
||||||
|
if !ok || userID == 0 {
|
||||||
|
h.InternalServerErrorLogger().Error("Invalid user ID in context",
|
||||||
|
zap.Int64("userID", userID),
|
||||||
|
)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.bonusSvc.ProcessBonusClaim(c.Context(), bonusID, userID); err != nil {
|
||||||
|
h.InternalServerErrorLogger().Error("Failed to update bonus claim",
|
||||||
|
zap.Int64("userID", userID),
|
||||||
|
zap.Int64("bonusID", bonusID),
|
||||||
|
)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update bonus claim")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.WriteJSON(c, fiber.StatusOK, "Bonus has successfully been claimed", nil, nil)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -206,7 +206,9 @@ func (a *App) initAppRoutes() {
|
||||||
a.fiber.Get("/raffle-ticket/unsuspend/:id", a.authMiddleware, h.UnSuspendRaffleTicket)
|
a.fiber.Get("/raffle-ticket/unsuspend/:id", a.authMiddleware, h.UnSuspendRaffleTicket)
|
||||||
|
|
||||||
// Bonus Routes
|
// Bonus Routes
|
||||||
// groupV1.Get("/bonus", a.authMiddleware, h.GetBonusMultiplier)
|
groupV1.Get("/bonus", a.authMiddleware, h.GetBonusesByUserID)
|
||||||
|
groupV1.Get("/bonus/stats", a.authMiddleware, h.GetBonusStats)
|
||||||
|
groupV1.Post("/bonus/claim/:id", a.authMiddleware, h.ClaimBonus)
|
||||||
// groupV1.Post("/bonus/create", a.authMiddleware, h.CreateBonusMultiplier)
|
// groupV1.Post("/bonus/create", a.authMiddleware, h.CreateBonusMultiplier)
|
||||||
// groupV1.Put("/bonus/update", a.authMiddleware, h.UpdateBonusMultiplier)
|
// groupV1.Put("/bonus/update", a.authMiddleware, h.UpdateBonusMultiplier)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user