game list

This commit is contained in:
Yared Yemane 2025-06-19 15:04:23 +03:00
commit b1a97bc31b
64 changed files with 1227 additions and 446 deletions

View File

@ -121,7 +121,7 @@ func main() {
companySvc := company.NewService(store) companySvc := company.NewService(store)
leagueSvc := league.New(store) leagueSvc := league.New(store)
betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, logger, domain.MongoDBLogger) betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, logger, domain.MongoDBLogger)
resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc) resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc)
referalRepo := repository.NewReferralRepository(store) referalRepo := repository.NewReferralRepository(store)
vitualGameRepo := repository.NewVirtualGameRepository(store) vitualGameRepo := repository.NewVirtualGameRepository(store)
recommendationRepo := repository.NewRecommendationRepository(store) recommendationRepo := repository.NewRecommendationRepository(store)

17
db.sql Normal file
View File

@ -0,0 +1,17 @@
psql (16.8)
Type "help" for help.
gh=#
gh=#
gh=#
gh=#
gh=#
gh=#
gh=#
gh=#
gh=#
gh=#
gh=#
gh=#
gh=# 
gh=#

View File

@ -140,7 +140,7 @@ CREATE TABLE IF NOT EXISTS wallet_transfer (
cashier_id BIGINT, cashier_id BIGINT,
verified BOOLEAN DEFAULT false, verified BOOLEAN DEFAULT false,
reference_number VARCHAR(255), reference_number VARCHAR(255),
status VARCHAR(255), status VARCHAR(255),
payment_method VARCHAR(255), payment_method VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
@ -250,10 +250,12 @@ CREATE TABLE companies (
CREATE TABLE leagues ( CREATE TABLE leagues (
id BIGINT PRIMARY KEY, id BIGINT PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
img TEXT,
country_code TEXT, country_code TEXT,
bet365_id INT, bet365_id INT,
sport_id INT NOT NULL, sport_id INT NOT NULL,
is_active BOOLEAN DEFAULT true is_active BOOLEAN DEFAULT true,
is_featured BOOLEAN DEFAULT false
); );
CREATE TABLE teams ( CREATE TABLE teams (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@ -275,7 +277,6 @@ FROM companies
JOIN wallets ON wallets.id = companies.wallet_id JOIN wallets ON wallets.id = companies.wallet_id
JOIN users ON users.id = companies.admin_id; JOIN users ON users.id = companies.admin_id;
; ;
CREATE VIEW branch_details AS CREATE VIEW branch_details AS
SELECT branches.*, SELECT branches.*,
CONCAT(users.first_name, ' ', users.last_name) AS manager_name, CONCAT(users.first_name, ' ', users.last_name) AS manager_name,
@ -302,41 +303,40 @@ FROM tickets
LEFT JOIN ticket_outcomes ON tickets.id = ticket_outcomes.ticket_id LEFT JOIN ticket_outcomes ON tickets.id = ticket_outcomes.ticket_id
GROUP BY tickets.id; GROUP BY tickets.id;
-- Foreign Keys -- Foreign Keys
ALTER TABLE users ALTER TABLE users
ADD CONSTRAINT unique_email UNIQUE (email), ADD CONSTRAINT unique_email UNIQUE (email),
ADD CONSTRAINT unique_phone_number UNIQUE (phone_number); ADD CONSTRAINT unique_phone_number UNIQUE (phone_number);
ALTER TABLE refresh_tokens ALTER TABLE refresh_tokens
ADD CONSTRAINT fk_refresh_tokens_users FOREIGN KEY (user_id) REFERENCES users(id); ADD CONSTRAINT fk_refresh_tokens_users FOREIGN KEY (user_id) REFERENCES users(id);
ALTER TABLE bets ALTER TABLE bets
ADD CONSTRAINT fk_bets_users FOREIGN KEY (user_id) REFERENCES users(id), ADD CONSTRAINT fk_bets_users FOREIGN KEY (user_id) REFERENCES users(id),
ADD CONSTRAINT fk_bets_branches FOREIGN KEY (branch_id) REFERENCES branches(id); ADD CONSTRAINT fk_bets_branches FOREIGN KEY (branch_id) REFERENCES branches(id);
ALTER TABLE wallets ALTER TABLE wallets
ADD CONSTRAINT fk_wallets_users FOREIGN KEY (user_id) REFERENCES users(id), ADD CONSTRAINT fk_wallets_users FOREIGN KEY (user_id) REFERENCES users(id),
ADD COLUMN currency VARCHAR(3) NOT NULL DEFAULT 'ETB'; ADD COLUMN currency VARCHAR(3) NOT NULL DEFAULT 'ETB';
ALTER TABLE customer_wallets ALTER TABLE customer_wallets
ADD CONSTRAINT fk_customer_wallets_customers FOREIGN KEY (customer_id) REFERENCES users(id), ADD CONSTRAINT fk_customer_wallets_customers FOREIGN KEY (customer_id) REFERENCES users(id),
ADD CONSTRAINT fk_customer_wallets_regular_wallet FOREIGN KEY (regular_wallet_id) REFERENCES wallets(id), ADD CONSTRAINT fk_customer_wallets_regular_wallet FOREIGN KEY (regular_wallet_id) REFERENCES wallets(id),
ADD CONSTRAINT fk_customer_wallets_static_wallet FOREIGN KEY (static_wallet_id) REFERENCES wallets(id); ADD CONSTRAINT fk_customer_wallets_static_wallet FOREIGN KEY (static_wallet_id) REFERENCES wallets(id);
ALTER TABLE wallet_transfer ALTER TABLE wallet_transfer
ADD CONSTRAINT fk_wallet_transfer_receiver_wallet FOREIGN KEY (receiver_wallet_id) REFERENCES wallets(id), ADD CONSTRAINT fk_wallet_transfer_receiver_wallet FOREIGN KEY (receiver_wallet_id) REFERENCES wallets(id),
ADD CONSTRAINT fk_wallet_transfer_sender_wallet FOREIGN KEY (sender_wallet_id) REFERENCES wallets(id), ADD CONSTRAINT fk_wallet_transfer_sender_wallet FOREIGN KEY (sender_wallet_id) REFERENCES wallets(id),
ADD CONSTRAINT fk_wallet_transfer_cashier FOREIGN KEY (cashier_id) REFERENCES users(id); ADD CONSTRAINT fk_wallet_transfer_cashier FOREIGN KEY (cashier_id) REFERENCES users(id);
ALTER TABLE transactions ALTER TABLE transactions
ADD CONSTRAINT fk_transactions_branches FOREIGN KEY (branch_id) REFERENCES branches(id), ADD CONSTRAINT fk_transactions_branches FOREIGN KEY (branch_id) REFERENCES branches(id),
ADD CONSTRAINT fk_transactions_cashiers FOREIGN KEY (cashier_id) REFERENCES users(id), ADD CONSTRAINT fk_transactions_cashiers FOREIGN KEY (cashier_id) REFERENCES users(id),
ADD CONSTRAINT fk_transactions_bets FOREIGN KEY (bet_id) REFERENCES bets(id); ADD CONSTRAINT fk_transactions_bets FOREIGN KEY (bet_id) REFERENCES bets(id);
ALTER TABLE branches ALTER TABLE branches
ADD CONSTRAINT fk_branches_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id), ADD CONSTRAINT fk_branches_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id),
ADD CONSTRAINT fk_branches_manager FOREIGN KEY (branch_manager_id) REFERENCES users(id); ADD CONSTRAINT fk_branches_manager FOREIGN KEY (branch_manager_id) REFERENCES users(id);
ALTER TABLE branch_operations ALTER TABLE branch_operations
ADD CONSTRAINT fk_branch_operations_operations FOREIGN KEY (operation_id) REFERENCES supported_operations(id) ON DELETE CASCADE, ADD CONSTRAINT fk_branch_operations_operations FOREIGN KEY (operation_id) REFERENCES supported_operations(id) ON DELETE CASCADE,
ADD CONSTRAINT fk_branch_operations_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE; ADD CONSTRAINT fk_branch_operations_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE;
ALTER TABLE branch_cashiers ALTER TABLE branch_cashiers
ADD CONSTRAINT fk_branch_cashiers_users FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, ADD CONSTRAINT fk_branch_cashiers_users FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
ADD CONSTRAINT fk_branch_cashiers_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE; ADD CONSTRAINT fk_branch_cashiers_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE;
ALTER TABLE companies ALTER TABLE companies
ADD CONSTRAINT fk_companies_admin FOREIGN KEY (admin_id) REFERENCES users(id), ADD CONSTRAINT fk_companies_admin FOREIGN KEY (admin_id) REFERENCES users(id),
ADD CONSTRAINT fk_companies_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE; ADD CONSTRAINT fk_companies_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE;
----------------------------------------------seed data------------------------------------------------------------- ----------------------------------------------seed data-------------------------------------------------------------
-------------------------------------- DO NOT USE IN PRODUCTION------------------------------------------------- -------------------------------------- DO NOT USE IN PRODUCTION-------------------------------------------------

View File

@ -18,11 +18,11 @@ CREATE TABLE user_game_interactions (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id), user_id BIGINT NOT NULL REFERENCES users(id),
game_id BIGINT NOT NULL REFERENCES virtual_games(id), game_id BIGINT NOT NULL REFERENCES virtual_games(id),
interaction_type VARCHAR(50) NOT NULL, -- 'view', 'play', 'bet', 'favorite' interaction_type VARCHAR(50) NOT NULL,
amount DECIMAL(15,2), -- 'view', 'play', 'bet', 'favorite'
amount DECIMAL(15, 2),
duration_seconds INTEGER, duration_seconds INTEGER,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX idx_user_game_interactions_user ON user_game_interactions(user_id); CREATE INDEX idx_user_game_interactions_user ON user_game_interactions(user_id);
CREATE INDEX idx_user_game_interactions_game ON user_game_interactions(game_id); CREATE INDEX idx_user_game_interactions_game ON user_game_interactions(game_id);

View File

@ -49,16 +49,20 @@ VALUES (
SELECT * SELECT *
FROM bet_with_outcomes FROM bet_with_outcomes
wHERE ( wHERE (
branch_id = $1 branch_id = sqlc.narg('branch_id')
OR $1 IS NULL OR sqlc.narg('branch_id') IS NULL
) )
AND ( AND (
company_id = $2 company_id = sqlc.narg('company_id')
OR $2 IS NULL OR sqlc.narg('company_id') IS NULL
) )
AND ( AND (
user_id = $3 user_id = sqlc.narg('user_id')
OR $3 IS NULL OR sqlc.narg('user_id') IS NULL
)
AND (
is_shop_bet = sqlc.narg('is_shop_bet')
OR sqlc.narg('is_shop_bet') IS NULL
); );
-- name: GetBetByID :one -- name: GetBetByID :one
SELECT * SELECT *
@ -99,6 +103,11 @@ UPDATE bet_outcomes
SET status = $1 SET status = $1
WHERE id = $2 WHERE id = $2
RETURNING *; RETURNING *;
-- name: UpdateBetOutcomeStatusForEvent :many
UPDATE bet_outcomes
SEt status = $1
WHERE event_id = $2
RETURNING *;
-- name: UpdateStatus :exec -- name: UpdateStatus :exec
UPDATE bets UPDATE bets
SET status = $1, SET status = $1,

View File

@ -23,7 +23,15 @@ VALUES ($1, $2)
RETURNING *; RETURNING *;
-- name: GetAllBranches :many -- name: GetAllBranches :many
SELECT * SELECT *
FROM branch_details; FROM branch_details
WHERE (
company_id = sqlc.narg('company_id')
OR sqlc.narg('company_id') IS NULL
)
AND (
is_active = sqlc.narg('is_active')
OR sqlc.narg('is_active') IS NULL
);
-- name: GetBranchByID :one -- name: GetBranchByID :one
SELECT * SELECT *
FROM branch_details FROM branch_details

View File

@ -5,14 +5,16 @@ INSERT INTO leagues (
country_code, country_code,
bet365_id, bet365_id,
sport_id, sport_id,
is_active is_active,
is_featured
) )
VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (id) DO
UPDATE UPDATE
SET name = EXCLUDED.name, SET name = EXCLUDED.name,
country_code = EXCLUDED.country_code, country_code = EXCLUDED.country_code,
bet365_id = EXCLUDED.bet365_id, bet365_id = EXCLUDED.bet365_id,
is_active = EXCLUDED.is_active, is_active = EXCLUDED.is_active,
is_featured = EXCLUDED.is_featured,
sport_id = EXCLUDED.sport_id; sport_id = EXCLUDED.sport_id;
-- name: GetAllLeagues :many -- name: GetAllLeagues :many
SELECT id, SELECT id,
@ -20,6 +22,7 @@ SELECT id,
country_code, country_code,
bet365_id, bet365_id,
is_active, is_active,
is_featured,
sport_id sport_id
FROM leagues FROM leagues
WHERE ( WHERE (
@ -34,7 +37,21 @@ WHERE (
is_active = sqlc.narg('is_active') is_active = sqlc.narg('is_active')
OR sqlc.narg('is_active') IS NULL OR sqlc.narg('is_active') IS NULL
) )
AND (
is_featured = sqlc.narg('is_featured')
OR sqlc.narg('is_featured') IS NULL
)
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
-- name: GetFeaturedLeagues :many
SELECT id,
name,
country_code,
bet365_id,
is_active,
is_featured,
sport_id
FROM leagues
WHERE is_featured = true;
-- name: CheckLeagueSupport :one -- name: CheckLeagueSupport :one
SELECT EXISTS( SELECT EXISTS(
SELECT 1 SELECT 1
@ -48,6 +65,7 @@ SET name = COALESCE(sqlc.narg('name'), name),
country_code = COALESCE(sqlc.narg('country_code'), country_code), country_code = COALESCE(sqlc.narg('country_code'), country_code),
bet365_id = COALESCE(sqlc.narg('bet365_id'), bet365_id), bet365_id = COALESCE(sqlc.narg('bet365_id'), bet365_id),
is_active = COALESCE(sqlc.narg('is_active'), is_active), is_active = COALESCE(sqlc.narg('is_active'), is_active),
is_featured = COALESCE(sqlc.narg('is_featured'), is_featured),
sport_id = COALESCE(sqlc.narg('sport_id'), sport_id) sport_id = COALESCE(sqlc.narg('sport_id'), sport_id)
WHERE id = $1; WHERE id = $1;
-- name: UpdateLeagueByBet365ID :exec -- name: UpdateLeagueByBet365ID :exec
@ -56,6 +74,7 @@ SET name = COALESCE(sqlc.narg('name'), name),
id = COALESCE(sqlc.narg('id'), id), id = COALESCE(sqlc.narg('id'), id),
country_code = COALESCE(sqlc.narg('country_code'), country_code), country_code = COALESCE(sqlc.narg('country_code'), country_code),
is_active = COALESCE(sqlc.narg('is_active'), is_active), is_active = COALESCE(sqlc.narg('is_active'), is_active),
is_featured = COALESCE(sqlc.narg('is_featured'), is_featured),
sport_id = COALESCE(sqlc.narg('sport_id'), sport_id) sport_id = COALESCE(sqlc.narg('sport_id'), sport_id)
WHERE bet365_id = $1; WHERE bet365_id = $1;
-- name: SetLeagueActive :exec -- name: SetLeagueActive :exec

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: auth.sql // source: auth.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: bet.sql // source: bet.sql
package dbgen package dbgen
@ -129,16 +129,26 @@ wHERE (
user_id = $3 user_id = $3
OR $3 IS NULL OR $3 IS NULL
) )
AND (
is_shop_bet = $4
OR $4 IS NULL
)
` `
type GetAllBetsParams struct { type GetAllBetsParams struct {
BranchID pgtype.Int8 `json:"branch_id"` BranchID pgtype.Int8 `json:"branch_id"`
CompanyID pgtype.Int8 `json:"company_id"` CompanyID pgtype.Int8 `json:"company_id"`
UserID pgtype.Int8 `json:"user_id"` UserID pgtype.Int8 `json:"user_id"`
IsShopBet pgtype.Bool `json:"is_shop_bet"`
} }
func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWithOutcome, error) { func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWithOutcome, error) {
rows, err := q.db.Query(ctx, GetAllBets, arg.BranchID, arg.CompanyID, arg.UserID) rows, err := q.db.Query(ctx, GetAllBets,
arg.BranchID,
arg.CompanyID,
arg.UserID,
arg.IsShopBet,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -458,6 +468,54 @@ func (q *Queries) UpdateBetOutcomeStatus(ctx context.Context, arg UpdateBetOutco
return i, err return i, err
} }
const UpdateBetOutcomeStatusForEvent = `-- name: UpdateBetOutcomeStatusForEvent :many
UPDATE bet_outcomes
SEt status = $1
WHERE event_id = $2
RETURNING id, bet_id, sport_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, status, expires
`
type UpdateBetOutcomeStatusForEventParams struct {
Status int32 `json:"status"`
EventID int64 `json:"event_id"`
}
func (q *Queries) UpdateBetOutcomeStatusForEvent(ctx context.Context, arg UpdateBetOutcomeStatusForEventParams) ([]BetOutcome, error) {
rows, err := q.db.Query(ctx, UpdateBetOutcomeStatusForEvent, arg.Status, arg.EventID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []BetOutcome
for rows.Next() {
var i BetOutcome
if err := rows.Scan(
&i.ID,
&i.BetID,
&i.SportID,
&i.EventID,
&i.OddID,
&i.HomeTeamName,
&i.AwayTeamName,
&i.MarketID,
&i.MarketName,
&i.Odd,
&i.OddName,
&i.OddHeader,
&i.OddHandicap,
&i.Status,
&i.Expires,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const UpdateCashOut = `-- name: UpdateCashOut :exec const UpdateCashOut = `-- name: UpdateCashOut :exec
UPDATE bets UPDATE bets
SET cashed_out = $2, SET cashed_out = $2,

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: branch.sql // source: branch.sql
package dbgen package dbgen
@ -157,10 +157,23 @@ func (q *Queries) DeleteBranchOperation(ctx context.Context, arg DeleteBranchOpe
const GetAllBranches = `-- name: GetAllBranches :many const GetAllBranches = `-- name: GetAllBranches :many
SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance
FROM branch_details FROM branch_details
WHERE (
company_id = $1
OR $1 IS NULL
)
AND (
is_active = $2
OR $2 IS NULL
)
` `
func (q *Queries) GetAllBranches(ctx context.Context) ([]BranchDetail, error) { type GetAllBranchesParams struct {
rows, err := q.db.Query(ctx, GetAllBranches) CompanyID pgtype.Int8 `json:"company_id"`
IsActive pgtype.Bool `json:"is_active"`
}
func (q *Queries) GetAllBranches(ctx context.Context, arg GetAllBranchesParams) ([]BranchDetail, error) {
rows, err := q.db.Query(ctx, GetAllBranches, arg.CompanyID, arg.IsActive)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: cashier.sql // source: cashier.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: company.sql // source: company.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: copyfrom.go // source: copyfrom.go
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: events.sql // source: events.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: leagues.sql // source: leagues.sql
package dbgen package dbgen
@ -33,6 +33,7 @@ SELECT id,
country_code, country_code,
bet365_id, bet365_id,
is_active, is_active,
is_featured,
sport_id sport_id
FROM leagues FROM leagues
WHERE ( WHERE (
@ -47,13 +48,18 @@ WHERE (
is_active = $3 is_active = $3
OR $3 IS NULL OR $3 IS NULL
) )
LIMIT $5 OFFSET $4 AND (
is_featured = $4
OR $4 IS NULL
)
LIMIT $6 OFFSET $5
` `
type GetAllLeaguesParams struct { type GetAllLeaguesParams struct {
CountryCode pgtype.Text `json:"country_code"` CountryCode pgtype.Text `json:"country_code"`
SportID pgtype.Int4 `json:"sport_id"` SportID pgtype.Int4 `json:"sport_id"`
IsActive pgtype.Bool `json:"is_active"` IsActive pgtype.Bool `json:"is_active"`
IsFeatured pgtype.Bool `json:"is_featured"`
Offset pgtype.Int4 `json:"offset"` Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"` Limit pgtype.Int4 `json:"limit"`
} }
@ -64,6 +70,7 @@ type GetAllLeaguesRow struct {
CountryCode pgtype.Text `json:"country_code"` CountryCode pgtype.Text `json:"country_code"`
Bet365ID pgtype.Int4 `json:"bet365_id"` Bet365ID pgtype.Int4 `json:"bet365_id"`
IsActive pgtype.Bool `json:"is_active"` IsActive pgtype.Bool `json:"is_active"`
IsFeatured pgtype.Bool `json:"is_featured"`
SportID int32 `json:"sport_id"` SportID int32 `json:"sport_id"`
} }
@ -72,6 +79,7 @@ func (q *Queries) GetAllLeagues(ctx context.Context, arg GetAllLeaguesParams) ([
arg.CountryCode, arg.CountryCode,
arg.SportID, arg.SportID,
arg.IsActive, arg.IsActive,
arg.IsFeatured,
arg.Offset, arg.Offset,
arg.Limit, arg.Limit,
) )
@ -88,6 +96,57 @@ func (q *Queries) GetAllLeagues(ctx context.Context, arg GetAllLeaguesParams) ([
&i.CountryCode, &i.CountryCode,
&i.Bet365ID, &i.Bet365ID,
&i.IsActive, &i.IsActive,
&i.IsFeatured,
&i.SportID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetFeaturedLeagues = `-- name: GetFeaturedLeagues :many
SELECT id,
name,
country_code,
bet365_id,
is_active,
is_featured,
sport_id
FROM leagues
WHERE is_featured = true
`
type GetFeaturedLeaguesRow struct {
ID int64 `json:"id"`
Name string `json:"name"`
CountryCode pgtype.Text `json:"country_code"`
Bet365ID pgtype.Int4 `json:"bet365_id"`
IsActive pgtype.Bool `json:"is_active"`
IsFeatured pgtype.Bool `json:"is_featured"`
SportID int32 `json:"sport_id"`
}
func (q *Queries) GetFeaturedLeagues(ctx context.Context) ([]GetFeaturedLeaguesRow, error) {
rows, err := q.db.Query(ctx, GetFeaturedLeagues)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetFeaturedLeaguesRow
for rows.Next() {
var i GetFeaturedLeaguesRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.CountryCode,
&i.Bet365ID,
&i.IsActive,
&i.IsFeatured,
&i.SportID, &i.SportID,
); err != nil { ); err != nil {
return nil, err return nil, err
@ -107,14 +166,16 @@ INSERT INTO leagues (
country_code, country_code,
bet365_id, bet365_id,
sport_id, sport_id,
is_active is_active,
is_featured
) )
VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (id) DO
UPDATE UPDATE
SET name = EXCLUDED.name, SET name = EXCLUDED.name,
country_code = EXCLUDED.country_code, country_code = EXCLUDED.country_code,
bet365_id = EXCLUDED.bet365_id, bet365_id = EXCLUDED.bet365_id,
is_active = EXCLUDED.is_active, is_active = EXCLUDED.is_active,
is_featured = EXCLUDED.is_featured,
sport_id = EXCLUDED.sport_id sport_id = EXCLUDED.sport_id
` `
@ -125,6 +186,7 @@ type InsertLeagueParams struct {
Bet365ID pgtype.Int4 `json:"bet365_id"` Bet365ID pgtype.Int4 `json:"bet365_id"`
SportID int32 `json:"sport_id"` SportID int32 `json:"sport_id"`
IsActive pgtype.Bool `json:"is_active"` IsActive pgtype.Bool `json:"is_active"`
IsFeatured pgtype.Bool `json:"is_featured"`
} }
func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) error { func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) error {
@ -135,6 +197,7 @@ func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) erro
arg.Bet365ID, arg.Bet365ID,
arg.SportID, arg.SportID,
arg.IsActive, arg.IsActive,
arg.IsFeatured,
) )
return err return err
} }
@ -161,7 +224,8 @@ SET name = COALESCE($2, name),
country_code = COALESCE($3, country_code), country_code = COALESCE($3, country_code),
bet365_id = COALESCE($4, bet365_id), bet365_id = COALESCE($4, bet365_id),
is_active = COALESCE($5, is_active), is_active = COALESCE($5, is_active),
sport_id = COALESCE($6, sport_id) is_featured = COALESCE($6, is_featured),
sport_id = COALESCE($7, sport_id)
WHERE id = $1 WHERE id = $1
` `
@ -171,6 +235,7 @@ type UpdateLeagueParams struct {
CountryCode pgtype.Text `json:"country_code"` CountryCode pgtype.Text `json:"country_code"`
Bet365ID pgtype.Int4 `json:"bet365_id"` Bet365ID pgtype.Int4 `json:"bet365_id"`
IsActive pgtype.Bool `json:"is_active"` IsActive pgtype.Bool `json:"is_active"`
IsFeatured pgtype.Bool `json:"is_featured"`
SportID pgtype.Int4 `json:"sport_id"` SportID pgtype.Int4 `json:"sport_id"`
} }
@ -181,6 +246,7 @@ func (q *Queries) UpdateLeague(ctx context.Context, arg UpdateLeagueParams) erro
arg.CountryCode, arg.CountryCode,
arg.Bet365ID, arg.Bet365ID,
arg.IsActive, arg.IsActive,
arg.IsFeatured,
arg.SportID, arg.SportID,
) )
return err return err
@ -192,7 +258,8 @@ SET name = COALESCE($2, name),
id = COALESCE($3, id), id = COALESCE($3, id),
country_code = COALESCE($4, country_code), country_code = COALESCE($4, country_code),
is_active = COALESCE($5, is_active), is_active = COALESCE($5, is_active),
sport_id = COALESCE($6, sport_id) is_featured = COALESCE($6, is_featured),
sport_id = COALESCE($7, sport_id)
WHERE bet365_id = $1 WHERE bet365_id = $1
` `
@ -202,6 +269,7 @@ type UpdateLeagueByBet365IDParams struct {
ID pgtype.Int8 `json:"id"` ID pgtype.Int8 `json:"id"`
CountryCode pgtype.Text `json:"country_code"` CountryCode pgtype.Text `json:"country_code"`
IsActive pgtype.Bool `json:"is_active"` IsActive pgtype.Bool `json:"is_active"`
IsFeatured pgtype.Bool `json:"is_featured"`
SportID pgtype.Int4 `json:"sport_id"` SportID pgtype.Int4 `json:"sport_id"`
} }
@ -212,6 +280,7 @@ func (q *Queries) UpdateLeagueByBet365ID(ctx context.Context, arg UpdateLeagueBy
arg.ID, arg.ID,
arg.CountryCode, arg.CountryCode,
arg.IsActive, arg.IsActive,
arg.IsFeatured,
arg.SportID, arg.SportID,
) )
return err return err

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
package dbgen package dbgen
@ -218,10 +218,12 @@ type ExchangeRate struct {
type League struct { type League struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Img pgtype.Text `json:"img"`
CountryCode pgtype.Text `json:"country_code"` CountryCode pgtype.Text `json:"country_code"`
Bet365ID pgtype.Int4 `json:"bet365_id"` Bet365ID pgtype.Int4 `json:"bet365_id"`
SportID int32 `json:"sport_id"` SportID int32 `json:"sport_id"`
IsActive pgtype.Bool `json:"is_active"` IsActive pgtype.Bool `json:"is_active"`
IsFeatured pgtype.Bool `json:"is_featured"`
} }
type Notification struct { type Notification struct {

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: monitor.sql // source: monitor.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: notification.sql // source: notification.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: odds.sql // source: odds.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: otp.sql // source: otp.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: referal.sql // source: referal.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: result.sql // source: result.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: ticket.sql // source: ticket.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: transactions.sql // source: transactions.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: transfer.sql // source: transfer.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: user.sql // source: user.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: virtual_games.sql // source: virtual_games.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: wallet.sql // source: wallet.sql
package dbgen package dbgen

View File

@ -60,6 +60,7 @@ type BetFilter struct {
BranchID ValidInt64 // Can Be Nullable BranchID ValidInt64 // Can Be Nullable
CompanyID ValidInt64 // Can Be Nullable CompanyID ValidInt64 // Can Be Nullable
UserID ValidInt64 // Can Be Nullable UserID ValidInt64 // Can Be Nullable
IsShopBet ValidBool
} }
type GetBet struct { type GetBet struct {

View File

@ -11,6 +11,11 @@ type Branch struct {
IsSelfOwned bool IsSelfOwned bool
} }
type BranchFilter struct {
CompanyID ValidInt64
IsSuspended ValidBool
}
type BranchDetail struct { type BranchDetail struct {
ID int64 ID int64
Name string Name string

View File

@ -7,6 +7,7 @@ type League struct {
Bet365ID int32 `json:"bet365_id" example:"1121"` Bet365ID int32 `json:"bet365_id" example:"1121"`
IsActive bool `json:"is_active" example:"false"` IsActive bool `json:"is_active" example:"false"`
SportID int32 `json:"sport_id" example:"1"` SportID int32 `json:"sport_id" example:"1"`
IsFeatured bool `json:"is_featured" example:"false"`
} }
type UpdateLeague struct { type UpdateLeague struct {
@ -15,6 +16,7 @@ type UpdateLeague struct {
CountryCode ValidString `json:"cc" example:"uk"` CountryCode ValidString `json:"cc" example:"uk"`
Bet365ID ValidInt32 `json:"bet365_id" example:"1121"` Bet365ID ValidInt32 `json:"bet365_id" example:"1121"`
IsActive ValidBool `json:"is_active" example:"false"` IsActive ValidBool `json:"is_active" example:"false"`
IsFeatured ValidBool `json:"is_featured" example:"false"`
SportID ValidInt32 `json:"sport_id" example:"1"` SportID ValidInt32 `json:"sport_id" example:"1"`
} }
@ -22,6 +24,69 @@ type LeagueFilter struct {
CountryCode ValidString CountryCode ValidString
SportID ValidInt32 SportID ValidInt32
IsActive ValidBool IsActive ValidBool
IsFeatured ValidBool
Limit ValidInt64 Limit ValidInt64
Offset ValidInt64 Offset ValidInt64
} }
// These leagues are automatically featured when the league is created
var FeaturedLeagues = []int64{
// Football
10044469, // Ethiopian Premier League
10041282, //Premier League
10083364, //La Liga
10041095, //German Bundesliga
10041100, //Ligue 1
10041809, //UEFA Champions League
10041957, //UEFA Europa League
10079560, //UEFA Conference League
10050282, //UEFA Nations League
10044685, //FIFA Club World Cup
10050346, //UEFA Super Cup
10081269, //CONCACAF Champions Cup
10070189, //CONCACAF Gold Cup
10067913, //Europe - World Cup Qualifying
10040162, //Asia - World Cup Qualifying
10067624, //South America - World Cup Qualifying
10073057, //North & Central America - World Cup Qualifying
10037075, //International Match
10077480, //Womens International
10037109, //Europe Friendlies
10068837, //Euro U21
10041315, //Italian Serie A
10036538, //Spain Segunda
10047168, // US MLS
10043156, //England FA Cup
10042103, //France Cup
10041088, //Premier League 2
10084250, //Turkiye Super League
10041187, //Kenya Super League
10041391, //Netherlands Eredivisie
// Basketball
10041830, //NBA
10049984, //WNBA
10037165, //German Bundesliga
10036608, //Italian Lega 1
10040795, //EuroLeague
10041534, //Basketball Africa League
// Ice Hockey
10037477, //NHL
10037447, //AHL
10069385, //IIHF World Championship
// AMERICAN FOOTBALL
10037219, //NFL
// BASEBALL
10037485, // MLB
// VOLLEYBALL
10069666, //FIVB Nations League
}

View File

@ -14,19 +14,22 @@ type NotificationDeliveryStatus string
type DeliveryChannel string type DeliveryChannel string
const ( const (
NotificationTypeCashOutSuccess NotificationType = "cash_out_success" NotificationTypeCashOutSuccess NotificationType = "cash_out_success"
NotificationTypeDepositSuccess NotificationType = "deposit_success" NotificationTypeDepositSuccess NotificationType = "deposit_success"
NotificationTypeBetPlaced NotificationType = "bet_placed" NotificationTypeWithdrawSuccess NotificationType = "withdraw_success"
NotificationTypeDailyReport NotificationType = "daily_report" NotificationTypeBetPlaced NotificationType = "bet_placed"
NotificationTypeHighLossOnBet NotificationType = "high_loss_on_bet" NotificationTypeDailyReport NotificationType = "daily_report"
NotificationTypeBetOverload NotificationType = "bet_overload" NotificationTypeHighLossOnBet NotificationType = "high_loss_on_bet"
NotificationTypeSignUpWelcome NotificationType = "signup_welcome" NotificationTypeBetOverload NotificationType = "bet_overload"
NotificationTypeOTPSent NotificationType = "otp_sent" NotificationTypeSignUpWelcome NotificationType = "signup_welcome"
NOTIFICATION_TYPE_WALLET NotificationType = "wallet_threshold" NotificationTypeOTPSent NotificationType = "otp_sent"
NOTIFICATION_TYPE_TRANSFER NotificationType = "transfer_failed" NOTIFICATION_TYPE_WALLET NotificationType = "wallet_threshold"
NOTIFICATION_TYPE_ADMIN_ALERT NotificationType = "admin_alert" NOTIFICATION_TYPE_TRANSFER_FAIL NotificationType = "transfer_failed"
NOTIFICATION_RECEIVER_ADMIN NotificationRecieverSide = "admin" NOTIFICATION_TYPE_TRANSFER_SUCCESS NotificationType = "transfer_success"
NOTIFICATION_TYPE_ADMIN_ALERT NotificationType = "admin_alert"
NOTIFICATION_TYPE_BET_RESULT NotificationType = "bet_result"
NOTIFICATION_RECEIVER_ADMIN NotificationRecieverSide = "admin"
NotificationRecieverSideAdmin NotificationRecieverSide = "admin" NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
NotificationRecieverSideCustomer NotificationRecieverSide = "customer" NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
NotificationRecieverSideCashier NotificationRecieverSide = "cashier" NotificationRecieverSideCashier NotificationRecieverSide = "cashier"
@ -57,9 +60,9 @@ const (
) )
type NotificationPayload struct { type NotificationPayload struct {
Headline string `json:"headline"` Headline string `json:"headline"`
Message string `json:"message"` Message string `json:"message"`
Tags []string `json:"tags"` Tags []string `json:"tags"`
} }
type Notification struct { type Notification struct {
@ -91,3 +94,18 @@ func FromJSON(data []byte) (*Notification, error) {
} }
return &n, nil return &n, nil
} }
func ReceiverFromRole(role Role) NotificationRecieverSide {
if role == RoleAdmin {
return NotificationRecieverSideAdmin
} else if role == RoleCashier {
return NotificationRecieverSideCashier
} else if role == RoleBranchManager {
return NotificationRecieverSideBranchManager
} else if role == RoleCustomer {
return NotificationRecieverSideCustomer
} else {
return ""
}
}

View File

@ -12,7 +12,10 @@ const (
type PaymentMethod string type PaymentMethod string
// Info on why the wallet was modified
// If its internal system modification then, its always direct
const ( const (
TRANSFER_DIRECT PaymentMethod = "direct"
TRANSFER_CASH PaymentMethod = "cash" TRANSFER_CASH PaymentMethod = "cash"
TRANSFER_BANK PaymentMethod = "bank" TRANSFER_BANK PaymentMethod = "bank"
TRANSFER_CHAPA PaymentMethod = "chapa" TRANSFER_CHAPA PaymentMethod = "chapa"
@ -22,16 +25,21 @@ const (
TRANSFER_OTHER PaymentMethod = "other" TRANSFER_OTHER PaymentMethod = "other"
) )
// There is always a receiving wallet id // Info for the payment providers
// There is a sender wallet id only if wallet transfer type type PaymentDetails struct {
ReferenceNumber ValidString
BankNumber ValidString
}
// A Transfer is logged for every modification of ALL wallets and wallet types
type Transfer struct { type Transfer struct {
ID int64 ID int64
Amount Currency Amount Currency
Verified bool Verified bool
Type TransferType Type TransferType
PaymentMethod PaymentMethod PaymentMethod PaymentMethod
ReceiverWalletID int64 ReceiverWalletID ValidInt64
SenderWalletID int64 SenderWalletID ValidInt64
ReferenceNumber string ReferenceNumber string
Status string Status string
CashierID ValidInt64 CashierID ValidInt64
@ -44,8 +52,8 @@ type CreateTransfer struct {
Verified bool Verified bool
ReferenceNumber string ReferenceNumber string
Status string Status string
ReceiverWalletID int64 ReceiverWalletID ValidInt64
SenderWalletID int64 SenderWalletID ValidInt64
CashierID ValidInt64 CashierID ValidInt64
Type TransferType Type TransferType
PaymentMethod PaymentMethod PaymentMethod PaymentMethod

View File

@ -58,3 +58,11 @@ type CreateCustomerWallet struct {
RegularWalletID int64 RegularWalletID int64
StaticWalletID int64 StaticWalletID int64
} }
type WalletType string
const (
CustomerWalletType WalletType = "customer_wallet"
BranchWalletType WalletType = "branch_wallet"
CompanyWalletType WalletType = "company_wallet"
)

View File

@ -209,6 +209,10 @@ func (s *Store) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]doma
Int64: filter.UserID.Value, Int64: filter.UserID.Value,
Valid: filter.UserID.Valid, Valid: filter.UserID.Valid,
}, },
IsShopBet: pgtype.Bool{
Bool: filter.IsShopBet.Value,
Valid: filter.IsShopBet.Valid,
},
}) })
if err != nil { if err != nil {
domain.MongoDBLogger.Error("failed to get all bets", domain.MongoDBLogger.Error("failed to get all bets",
@ -360,6 +364,28 @@ func (s *Store) UpdateBetOutcomeStatus(ctx context.Context, id int64, status dom
return res, nil return res, nil
} }
func (s *Store) UpdateBetOutcomeStatusForEvent(ctx context.Context, eventID int64, status domain.OutcomeStatus) ([]domain.BetOutcome, error) {
outcomes, err := s.queries.UpdateBetOutcomeStatusForEvent(ctx, dbgen.UpdateBetOutcomeStatusForEventParams{
EventID: eventID,
Status: int32(status),
})
if err != nil {
domain.MongoDBLogger.Error("failed to update bet outcome status for event",
zap.Int64("eventID", eventID),
zap.Int32("status", int32(status)),
zap.Error(err),
)
return nil, err
}
var result []domain.BetOutcome = make([]domain.BetOutcome, 0, len(outcomes))
for _, outcome := range outcomes {
result = append(result, convertDBBetOutcomes(outcome))
}
return result, nil
}
func (s *Store) DeleteBet(ctx context.Context, id int64) error { func (s *Store) DeleteBet(ctx context.Context, id int64) error {
return s.queries.DeleteBet(ctx, id) return s.queries.DeleteBet(ctx, id)
} }

View File

@ -134,8 +134,13 @@ func (s *Store) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]do
return branches, nil return branches, nil
} }
func (s *Store) GetAllBranches(ctx context.Context) ([]domain.BranchDetail, error) { func (s *Store) GetAllBranches(ctx context.Context, filter domain.BranchFilter) ([]domain.BranchDetail, error) {
dbBranches, err := s.queries.GetAllBranches(ctx) dbBranches, err := s.queries.GetAllBranches(ctx, dbgen.GetAllBranchesParams{
CompanyID: pgtype.Int8{
Int64: filter.CompanyID.Value,
Valid: filter.CompanyID.Valid,
},
})
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -15,6 +15,7 @@ func (s *Store) SaveLeague(ctx context.Context, l domain.League) error {
CountryCode: pgtype.Text{String: l.CountryCode, Valid: true}, CountryCode: pgtype.Text{String: l.CountryCode, Valid: true},
Bet365ID: pgtype.Int4{Int32: l.Bet365ID, Valid: true}, Bet365ID: pgtype.Int4{Int32: l.Bet365ID, Valid: true},
IsActive: pgtype.Bool{Bool: l.IsActive, Valid: true}, IsActive: pgtype.Bool{Bool: l.IsActive, Valid: true},
IsFeatured: pgtype.Bool{Bool: l.IsFeatured, Valid: true},
SportID: l.SportID, SportID: l.SportID,
}) })
} }
@ -33,6 +34,10 @@ func (s *Store) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) (
Bool: filter.IsActive.Value, Bool: filter.IsActive.Value,
Valid: filter.IsActive.Valid, Valid: filter.IsActive.Valid,
}, },
IsFeatured: pgtype.Bool{
Bool: filter.IsFeatured.Value,
Valid: filter.IsFeatured.Valid,
},
Limit: pgtype.Int4{ Limit: pgtype.Int4{
Int32: int32(filter.Limit.Value), Int32: int32(filter.Limit.Value),
Valid: filter.Limit.Valid, Valid: filter.Limit.Valid,
@ -54,12 +59,35 @@ func (s *Store) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) (
CountryCode: league.CountryCode.String, CountryCode: league.CountryCode.String,
Bet365ID: league.Bet365ID.Int32, Bet365ID: league.Bet365ID.Int32,
IsActive: league.IsActive.Bool, IsActive: league.IsActive.Bool,
IsFeatured: league.IsFeatured.Bool,
SportID: league.SportID, SportID: league.SportID,
} }
} }
return leagues, nil return leagues, nil
} }
func (s *Store) GetFeaturedLeagues(ctx context.Context) ([]domain.League, error) {
l, err := s.queries.GetFeaturedLeagues(ctx)
if err != nil {
return nil, err
}
leagues := make([]domain.League, len(l))
for i, league := range l {
leagues[i] = domain.League{
ID: league.ID,
Name: league.Name,
CountryCode: league.CountryCode.String,
Bet365ID: league.Bet365ID.Int32,
IsActive: league.IsActive.Bool,
SportID: league.SportID,
}
}
return leagues, nil
}
func (s *Store) CheckLeagueSupport(ctx context.Context, leagueID int64) (bool, error) { func (s *Store) CheckLeagueSupport(ctx context.Context, leagueID int64) (bool, error) {
return s.queries.CheckLeagueSupport(ctx, leagueID) return s.queries.CheckLeagueSupport(ctx, leagueID)
} }
@ -93,6 +121,10 @@ func (s *Store) UpdateLeague(ctx context.Context, league domain.UpdateLeague) er
Bool: league.IsActive.Value, Bool: league.IsActive.Value,
Valid: league.IsActive.Valid, Valid: league.IsActive.Valid,
}, },
IsFeatured: pgtype.Bool{
Bool: league.IsFeatured.Value,
Valid: league.IsActive.Valid,
},
SportID: pgtype.Int4{ SportID: pgtype.Int4{
Int32: league.SportID.Value, Int32: league.SportID.Value,
Valid: league.SportID.Valid, Valid: league.SportID.Valid,

View File

@ -10,12 +10,18 @@ import (
func convertDBTransfer(transfer dbgen.WalletTransfer) domain.Transfer { func convertDBTransfer(transfer dbgen.WalletTransfer) domain.Transfer {
return domain.Transfer{ return domain.Transfer{
ID: transfer.ID, ID: transfer.ID,
Amount: domain.Currency(transfer.Amount.Int64), Amount: domain.Currency(transfer.Amount.Int64),
Type: domain.TransferType(transfer.Type.String), Type: domain.TransferType(transfer.Type.String),
Verified: transfer.Verified.Bool, Verified: transfer.Verified.Bool,
ReceiverWalletID: transfer.ReceiverWalletID.Int64, ReceiverWalletID: domain.ValidInt64{
SenderWalletID: transfer.SenderWalletID.Int64, Value: transfer.ReceiverWalletID.Int64,
Valid: transfer.ReceiverWalletID.Valid,
},
SenderWalletID: domain.ValidInt64{
Value: transfer.SenderWalletID.Int64,
Valid: transfer.SenderWalletID.Valid,
},
CashierID: domain.ValidInt64{ CashierID: domain.ValidInt64{
Value: transfer.CashierID.Int64, Value: transfer.CashierID.Int64,
Valid: transfer.CashierID.Valid, Valid: transfer.CashierID.Valid,
@ -29,12 +35,12 @@ func convertCreateTransfer(transfer domain.CreateTransfer) dbgen.CreateTransferP
Amount: pgtype.Int8{Int64: int64(transfer.Amount), Valid: true}, Amount: pgtype.Int8{Int64: int64(transfer.Amount), Valid: true},
Type: pgtype.Text{String: string(transfer.Type), Valid: true}, Type: pgtype.Text{String: string(transfer.Type), Valid: true},
ReceiverWalletID: pgtype.Int8{ ReceiverWalletID: pgtype.Int8{
Int64: transfer.ReceiverWalletID, Int64: transfer.ReceiverWalletID.Value,
Valid: true, Valid: transfer.ReceiverWalletID.Valid,
}, },
SenderWalletID: pgtype.Int8{ SenderWalletID: pgtype.Int8{
Int64: transfer.SenderWalletID, Int64: transfer.SenderWalletID.Value,
Valid: true, Valid: transfer.SenderWalletID.Valid,
}, },
CashierID: pgtype.Int8{ CashierID: pgtype.Int8{
Int64: transfer.CashierID.Value, Int64: transfer.CashierID.Value,

View File

@ -21,6 +21,7 @@ type BetStore interface {
UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error
UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error
UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error)
UpdateBetOutcomeStatusForEvent(ctx context.Context, eventID int64, status domain.OutcomeStatus) ([]domain.BetOutcome, error)
DeleteBet(ctx context.Context, id int64) error DeleteBet(ctx context.Context, id int64) error
GetBetSummary(ctx context.Context, filter domain.ReportFilter) ( GetBetSummary(ctx context.Context, filter domain.ReportFilter) (

View File

@ -264,7 +264,11 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
} }
deductedAmount := req.Amount / 10 deductedAmount := req.Amount / 10
err = s.walletSvc.DeductFromWallet(ctx, branch.WalletID, domain.ToCurrency(deductedAmount)) _, err = s.walletSvc.DeductFromWallet(ctx,
branch.WalletID, domain.ToCurrency(deductedAmount), domain.BranchWalletType, domain.ValidInt64{
Value: userID,
Valid: true,
}, domain.TRANSFER_DIRECT)
if err != nil { if err != nil {
s.mongoLogger.Error("failed to deduct from wallet", s.mongoLogger.Error("failed to deduct from wallet",
zap.Int64("wallet_id", branch.WalletID), zap.Int64("wallet_id", branch.WalletID),
@ -297,7 +301,10 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
} }
deductedAmount := req.Amount / 10 deductedAmount := req.Amount / 10
err = s.walletSvc.DeductFromWallet(ctx, branch.WalletID, domain.ToCurrency(deductedAmount)) _, err = s.walletSvc.DeductFromWallet(ctx, branch.WalletID, domain.ToCurrency(deductedAmount), domain.BranchWalletType, domain.ValidInt64{
Value: userID,
Valid: true,
}, domain.TRANSFER_DIRECT)
if err != nil { if err != nil {
s.mongoLogger.Error("wallet deduction failed", s.mongoLogger.Error("wallet deduction failed",
zap.Int64("wallet_id", branch.WalletID), zap.Int64("wallet_id", branch.WalletID),
@ -323,7 +330,8 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
} }
userWallet := wallets[0] userWallet := wallets[0]
err = s.walletSvc.DeductFromWallet(ctx, userWallet.ID, domain.ToCurrency(req.Amount)) _, err = s.walletSvc.DeductFromWallet(ctx, userWallet.ID,
domain.ToCurrency(req.Amount), domain.CustomerWalletType, domain.ValidInt64{}, domain.TRANSFER_DIRECT)
if err != nil { if err != nil {
s.mongoLogger.Error("wallet deduction failed for customer", s.mongoLogger.Error("wallet deduction failed for customer",
zap.Int64("wallet_id", userWallet.ID), zap.Int64("wallet_id", userWallet.ID),
@ -704,7 +712,8 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc
amount = bet.Amount amount = bet.Amount
} }
err = s.walletSvc.AddToWallet(ctx, customerWallet.RegularID, amount) _, err = s.walletSvc.AddToWallet(ctx, customerWallet.RegularID, amount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{})
if err != nil { if err != nil {
s.mongoLogger.Error("failed to add winnings to wallet", s.mongoLogger.Error("failed to add winnings to wallet",
zap.Int64("wallet_id", customerWallet.RegularID), zap.Int64("wallet_id", customerWallet.RegularID),
@ -810,6 +819,19 @@ func (s *Service) UpdateBetOutcomeStatus(ctx context.Context, id int64, status d
} }
func (s *Service) UpdateBetOutcomeStatusForEvent(ctx context.Context, eventID int64, status domain.OutcomeStatus) ([]domain.BetOutcome, error) {
outcomes, err := s.betStore.UpdateBetOutcomeStatusForEvent(ctx, eventID, status)
if err != nil {
s.mongoLogger.Error("failed to update bet outcome status",
zap.Int64("eventID", eventID),
zap.Error(err),
)
return nil, err
}
return outcomes, nil
}
func (s *Service) DeleteBet(ctx context.Context, id int64) error { func (s *Service) DeleteBet(ctx context.Context, id int64) error {
return s.betStore.DeleteBet(ctx, id) return s.betStore.DeleteBet(ctx, id)
} }

View File

@ -11,7 +11,7 @@ type BranchStore interface {
GetBranchByID(ctx context.Context, id int64) (domain.BranchDetail, error) GetBranchByID(ctx context.Context, id int64) (domain.BranchDetail, error)
GetBranchByManagerID(ctx context.Context, branchManagerID int64) ([]domain.BranchDetail, error) GetBranchByManagerID(ctx context.Context, branchManagerID int64) ([]domain.BranchDetail, error)
GetBranchByCompanyID(ctx context.Context, companyID int64) ([]domain.BranchDetail, error) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]domain.BranchDetail, error)
GetAllBranches(ctx context.Context) ([]domain.BranchDetail, error) GetAllBranches(ctx context.Context, filter domain.BranchFilter) ([]domain.BranchDetail, error)
SearchBranchByName(ctx context.Context, name string) ([]domain.BranchDetail, error) SearchBranchByName(ctx context.Context, name string) ([]domain.BranchDetail, error)
UpdateBranch(ctx context.Context, branch domain.UpdateBranch) (domain.Branch, error) UpdateBranch(ctx context.Context, branch domain.UpdateBranch) (domain.Branch, error)
DeleteBranch(ctx context.Context, id int64) error DeleteBranch(ctx context.Context, id int64) error

View File

@ -1,4 +1,4 @@
package branch package branch
import ( import (
"context" "context"
@ -42,8 +42,8 @@ func (s *Service) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]
func (s *Service) GetBranchOperations(ctx context.Context, branchID int64) ([]domain.BranchOperation, error) { func (s *Service) GetBranchOperations(ctx context.Context, branchID int64) ([]domain.BranchOperation, error) {
return s.branchStore.GetBranchOperations(ctx, branchID) return s.branchStore.GetBranchOperations(ctx, branchID)
} }
func (s *Service) GetAllBranches(ctx context.Context) ([]domain.BranchDetail, error) { func (s *Service) GetAllBranches(ctx context.Context, filter domain.BranchFilter) ([]domain.BranchDetail, error) {
return s.branchStore.GetAllBranches(ctx) return s.branchStore.GetAllBranches(ctx, filter)
} }
func (s *Service) GetBranchByCashier(ctx context.Context, userID int64) (domain.Branch, error) { func (s *Service) GetBranchByCashier(ctx context.Context, userID int64) (domain.Branch, error) {

View File

@ -83,8 +83,11 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
PaymentMethod: domain.TRANSFER_CHAPA, PaymentMethod: domain.TRANSFER_CHAPA,
ReferenceNumber: reference, ReferenceNumber: reference,
// ReceiverWalletID: 1, // ReceiverWalletID: 1,
SenderWalletID: senderWallet.ID, SenderWalletID: domain.ValidInt64{
Verified: false, Value: senderWallet.ID,
Valid: true,
},
Verified: false,
} }
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil { if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
@ -150,14 +153,16 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma
reference := uuid.New().String() reference := uuid.New().String()
createTransfer := domain.CreateTransfer{ createTransfer := domain.CreateTransfer{
Amount: domain.Currency(amount), Amount: domain.Currency(amount),
Type: domain.WITHDRAW, Type: domain.WITHDRAW,
ReceiverWalletID: 1, SenderWalletID: domain.ValidInt64{
SenderWalletID: withdrawWallet.ID, Value: withdrawWallet.ID,
Status: string(domain.PaymentStatusPending), Valid: true,
Verified: false, },
ReferenceNumber: reference, Status: string(domain.PaymentStatusPending),
PaymentMethod: domain.TRANSFER_CHAPA, Verified: false,
ReferenceNumber: reference,
PaymentMethod: domain.TRANSFER_CHAPA,
} }
transfer, err := s.transferStore.CreateTransfer(ctx, createTransfer) transfer, err := s.transferStore.CreateTransfer(ctx, createTransfer)
@ -215,6 +220,11 @@ func (s *Service) ManualVerifTransaction(ctx context.Context, txRef string) (*do
}, nil }, nil
} }
// just making sure that the sender id is valid
if !transfer.SenderWalletID.Valid {
return nil, fmt.Errorf("sender wallet id is invalid: %v \n", transfer.SenderWalletID)
}
// If not verified or not found, verify with Chapa // If not verified or not found, verify with Chapa
verification, err := s.chapaClient.VerifyPayment(ctx, txRef) verification, err := s.chapaClient.VerifyPayment(ctx, txRef)
if err != nil { if err != nil {
@ -229,7 +239,7 @@ func (s *Service) ManualVerifTransaction(ctx context.Context, txRef string) (*do
} }
// Credit user's wallet // Credit user's wallet
err = s.walletStore.UpdateBalance(ctx, transfer.SenderWalletID, transfer.Amount) err = s.walletStore.UpdateBalance(ctx, transfer.SenderWalletID.Value, transfer.Amount)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to update wallet balance: %w", err) return nil, fmt.Errorf("failed to update wallet balance: %w", err)
} }
@ -271,7 +281,11 @@ func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domai
// If payment is completed, credit user's wallet // If payment is completed, credit user's wallet
if transfer.Status == string(domain.PaymentStatusCompleted) { if transfer.Status == string(domain.PaymentStatusCompleted) {
if err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID, payment.Amount); err != nil { if _, err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID.Value, payment.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{
ReferenceNumber: domain.ValidString{
Value: transfer.Reference,
},
}); err != nil {
return fmt.Errorf("failed to credit user wallet: %w", err) return fmt.Errorf("failed to credit user wallet: %w", err)
} }
} }
@ -308,7 +322,7 @@ func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domai
// If payment is completed, credit user's wallet // If payment is completed, credit user's wallet
if payment.Status == string(domain.PaymentStatusFailed) { if payment.Status == string(domain.PaymentStatusFailed) {
if err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID, transfer.Amount); err != nil { if _, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value, transfer.Amount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil {
return fmt.Errorf("failed to credit user wallet: %w", err) return fmt.Errorf("failed to credit user wallet: %w", err)
} }
} }

View File

@ -7,6 +7,7 @@ import (
"io" "io"
"log" "log"
"net/http" "net/http"
"slices"
"strconv" "strconv"
"sync" "sync"
"time" "time"
@ -202,8 +203,10 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error {
} }
func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, source string) { func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, source string) {
sportIDs := []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91} // sportIDs := []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91}
sportIDs := []int{1}
// TODO: Add the league skipping again when we have dynamic leagues // TODO: Add the league skipping again when we have dynamic leagues
// b, err := os.OpenFile("logs/skipped_leagues.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) // b, err := os.OpenFile("logs/skipped_leagues.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
// if err != nil { // if err != nil {
// log.Printf("❌ Failed to open leagues file %v", err) // log.Printf("❌ Failed to open leagues file %v", err)
@ -212,7 +215,7 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour
for sportIndex, sportID := range sportIDs { for sportIndex, sportID := range sportIDs {
var totalPages int = 1 var totalPages int = 1
var page int = 0 var page int = 0
var limit int = 200 var limit int = 1
var count int = 0 var count int = 0
log.Printf("Sport ID %d", sportID) log.Printf("Sport ID %d", sportID)
for page <= totalPages { for page <= totalPages {
@ -252,11 +255,13 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour
// doesn't make sense to save and check back to back, but for now it can be here // doesn't make sense to save and check back to back, but for now it can be here
// no this its fine to keep it here // no this its fine to keep it here
// but change the league id to bet365 id later // but change the league id to bet365 id later
//Automatically feature the league if its in the list
err = s.store.SaveLeague(ctx, domain.League{ err = s.store.SaveLeague(ctx, domain.League{
ID: leagueID, ID: leagueID,
Name: ev.League.Name, Name: ev.League.Name,
IsActive: true, IsActive: true,
SportID: convertInt32(ev.SportID), IsFeatured: slices.Contains(domain.FeaturedLeagues, leagueID),
SportID: convertInt32(ev.SportID),
}) })
if err != nil { if err != nil {

View File

@ -9,6 +9,7 @@ import (
type Service interface { type Service interface {
SaveLeague(ctx context.Context, l domain.League) error SaveLeague(ctx context.Context, l domain.League) error
GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.League, error) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.League, error)
GetFeaturedLeagues(ctx context.Context) ([]domain.League, error)
SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error
UpdateLeague(ctx context.Context, league domain.UpdateLeague) error UpdateLeague(ctx context.Context, league domain.UpdateLeague) error
} }

View File

@ -25,6 +25,10 @@ func (s *service) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter)
return s.store.GetAllLeagues(ctx, filter) return s.store.GetAllLeagues(ctx, filter)
} }
func (s *service) GetFeaturedLeagues(ctx context.Context) ([]domain.League, error) {
return s.store.GetFeaturedLeagues(ctx)
}
func (s *service) SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error { func (s *service) SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error {
return s.store.SetLeagueActive(ctx, leagueId, isActive) return s.store.SetLeagueActive(ctx, leagueId, isActive)
} }

View File

@ -124,7 +124,7 @@ func (s *Service) ProcessReferral(ctx context.Context, referredPhone, referralCo
walletID := wallets[0].ID walletID := wallets[0].ID
currentBonus := float64(wallets[0].Balance) currentBonus := float64(wallets[0].Balance)
err = s.walletSvc.AddToWallet(ctx, walletID, domain.Currency(int64((currentBonus+referral.RewardAmount)*100))) _, err = s.walletSvc.AddToWallet(ctx, walletID, domain.ToCurrency(float32(currentBonus+referral.RewardAmount)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{})
if err != nil { if err != nil {
s.logger.Error("Failed to add referral reward to wallet", "walletID", walletID, "referrerID", referrerID, "error", err) s.logger.Error("Failed to add referral reward to wallet", "walletID", walletID, "referrerID", referrerID, "error", err)
return err return err
@ -162,7 +162,7 @@ func (s *Service) ProcessDepositBonus(ctx context.Context, userPhone string, amo
walletID := wallets[0].ID walletID := wallets[0].ID
bonus := amount * (settings.CashbackPercentage / 100) bonus := amount * (settings.CashbackPercentage / 100)
currentBonus := float64(wallets[0].Balance) currentBonus := float64(wallets[0].Balance)
err = s.walletSvc.AddToWallet(ctx, walletID, domain.Currency(int64((currentBonus+bonus)*100))) _, err = s.walletSvc.AddToWallet(ctx, walletID, domain.ToCurrency(float32(currentBonus+bonus)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{})
if err != nil { if err != nil {
s.logger.Error("Failed to add deposit bonus to wallet", "walletID", walletID, "userID", userID, "bonus", bonus, "error", err) s.logger.Error("Failed to add deposit bonus to wallet", "walletID", walletID, "userID", userID, "bonus", bonus, "error", err)
return err return err
@ -216,7 +216,7 @@ func (s *Service) ProcessBetReferral(ctx context.Context, userPhone string, betA
walletID := wallets[0].ID walletID := wallets[0].ID
currentBalance := float64(wallets[0].Balance) currentBalance := float64(wallets[0].Balance)
err = s.walletSvc.AddToWallet(ctx, walletID, domain.Currency(int64((currentBalance+bonus)*100))) _, err = s.walletSvc.AddToWallet(ctx, walletID, domain.ToCurrency(float32(currentBalance+bonus)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{})
if err != nil { if err != nil {
s.logger.Error("Failed to add bet referral bonus to wallet", "walletID", walletID, "referrerID", referrerID, "bonus", bonus, "error", err) s.logger.Error("Failed to add bet referral bonus to wallet", "walletID", walletID, "referrerID", referrerID, "bonus", bonus, "error", err)
return err return err

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"log/slog" "log/slog"
"net/http" "net/http"
"strconv" "strconv"
@ -17,30 +16,33 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
) )
type Service struct { type Service struct {
repo *repository.Store repo *repository.Store
config *config.Config config *config.Config
logger *slog.Logger logger *slog.Logger
client *http.Client client *http.Client
betSvc bet.Service betSvc bet.Service
oddSvc odds.ServiceImpl oddSvc odds.ServiceImpl
eventSvc event.Service eventSvc event.Service
leagueSvc league.Service leagueSvc league.Service
notificationSvc *notificationservice.Service
} }
func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, betSvc bet.Service, oddSvc odds.ServiceImpl, eventSvc event.Service, leagueSvc league.Service) *Service { func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, betSvc bet.Service, oddSvc odds.ServiceImpl, eventSvc event.Service, leagueSvc league.Service, notificationSvc *notificationservice.Service) *Service {
return &Service{ return &Service{
repo: repo, repo: repo,
config: cfg, config: cfg,
logger: logger, logger: logger,
client: &http.Client{Timeout: 10 * time.Second}, client: &http.Client{Timeout: 10 * time.Second},
betSvc: betSvc, betSvc: betSvc,
oddSvc: oddSvc, oddSvc: oddSvc,
eventSvc: eventSvc, eventSvc: eventSvc,
leagueSvc: leagueSvc, leagueSvc: leagueSvc,
notificationSvc: notificationSvc,
} }
} }
@ -48,6 +50,127 @@ var (
ErrEventIsNotActive = fmt.Errorf("event has been cancelled or postponed") ErrEventIsNotActive = fmt.Errorf("event has been cancelled or postponed")
) )
func (s *Service) UpdateResultForOutcomes(ctx context.Context, eventID int64, resultRes json.RawMessage, sportID int64) error {
// TODO: Optimize this since there could be many outcomes with the same event_id and market_id that could be updated the same time
outcomes, err := s.repo.GetBetOutcomeByEventID(ctx, eventID)
if err != nil {
s.logger.Error("Failed to get pending bet outcomes", "error", err)
return fmt.Errorf("failed to get pending bet outcomes for event %d: %w", eventID, err)
}
if len(outcomes) == 0 {
s.logger.Info("No bets have been placed on event", "event", eventID)
}
// if len(outcomes) == 0 {
// fmt.Printf("🕛 No bets have been placed on event %v (%d/%d) \n", eventID, i+1, len(events))
// } else {
// fmt.Printf("✅ %d bets have been placed on event %v (%d/%d) \n", len(outcomes), event.ID, i+1, len(events))
// }
for _, outcome := range outcomes {
if outcome.Expires.After(time.Now()) {
s.logger.Warn("Outcome is not expired yet", "event_id", outcome.EventID, "outcome_id", outcome.ID)
return fmt.Errorf("Outcome has not expired yet")
}
parseResult, err := s.parseResult(ctx, resultRes, outcome, sportID)
if err != nil {
s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err)
return err
}
outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, parseResult.Status)
if err != nil {
s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err)
return err
}
if outcome.Status == domain.OUTCOME_STATUS_ERROR || outcome.Status == domain.OUTCOME_STATUS_PENDING {
s.logger.Error("Outcome is pending or error", "event_id", outcome.EventID, "outcome_id", outcome.ID)
return fmt.Errorf("Error while updating outcome")
}
status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID)
if err != nil {
if err != bet.ErrOutcomesNotCompleted {
s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err)
}
return err
}
s.logger.Info("Updating bet status", "bet_id", outcome.BetID, "status", status.String())
err = s.betSvc.UpdateStatus(ctx, outcome.BetID, status)
if err != nil {
s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err)
return err
}
}
return nil
}
func (s *Service) RefundAllOutcomes(ctx context.Context, eventID int64) error {
outcomes, err := s.repo.GetBetOutcomeByEventID(ctx, eventID)
if err != nil {
s.logger.Error("Failed to get pending bet outcomes", "error", err)
return fmt.Errorf("failed to get pending bet outcomes for event %d: %w", eventID, err)
}
if len(outcomes) == 0 {
s.logger.Info("No bets have been placed on event", "event", eventID)
}
outcomes, err = s.betSvc.UpdateBetOutcomeStatusForEvent(ctx, eventID, domain.OUTCOME_STATUS_VOID)
if err != nil {
s.logger.Error("Failed to update all outcomes for event")
}
// Get all the unique bet_ids and how many outcomes have this bet_id
betIDSet := make(map[int64]int64)
for _, outcome := range outcomes {
betIDSet[outcome.BetID] += 1
}
for betID := range betIDSet {
status, err := s.betSvc.CheckBetOutcomeForBet(ctx, betID)
if err != nil {
if err != bet.ErrOutcomesNotCompleted {
s.logger.Error("Failed to check bet outcome for bet", "event_id", eventID, "error", err)
}
return err
}
err = s.betSvc.UpdateStatus(ctx, betID, status)
if err != nil {
s.logger.Error("Failed to update bet status", "event id", eventID, "error", err)
continue
}
}
return nil
// for _, outcome := range outcomes {
// outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, domain.OUTCOME_STATUS_VOID)
// if err != nil {
// s.logger.Error("Failed to refund all outcome status", "bet_outcome_id", outcome.ID, "error", err)
// return err
// }
// // Check if all the bet outcomes have been set to refund for
// status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID)
// if err != nil {
// if err != bet.ErrOutcomesNotCompleted {
// s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err)
// }
// return err
// }
// err = s.betSvc.UpdateStatus(ctx, outcome.BetID, domain.OUTCOME_STATUS_VOID)
// if err != nil {
// s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err)
// return err
// }
// }
}
func (s *Service) FetchAndProcessResults(ctx context.Context) error { func (s *Service) FetchAndProcessResults(ctx context.Context) error {
// TODO: Optimize this because there could be many bet outcomes for the same odd // TODO: Optimize this because there could be many bet outcomes for the same odd
// Take market id and match result as param and update all the bet outcomes at the same time // Take market id and match result as param and update all the bet outcomes at the same time
@ -58,29 +181,15 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error {
} }
fmt.Printf("⚠️ Expired Events: %d \n", len(events)) fmt.Printf("⚠️ Expired Events: %d \n", len(events))
removed := 0 removed := 0
errs := make([]error, 0, len(events))
for i, event := range events { for _, event := range events {
eventID, err := strconv.ParseInt(event.ID, 10, 64) eventID, err := strconv.ParseInt(event.ID, 10, 64)
if err != nil { if err != nil {
s.logger.Error("Failed to parse event id") s.logger.Error("Failed to parse", "eventID", event.ID, "err", err)
errs = append(errs, fmt.Errorf("failed to parse event id %s: %w", event.ID, err))
continue
}
outcomes, err := s.repo.GetBetOutcomeByEventID(ctx, eventID)
if err != nil {
s.logger.Error("Failed to get pending bet outcomes", "error", err)
errs = append(errs, fmt.Errorf("failed to get pending bet outcomes for event %d: %w", eventID, err))
continue continue
} }
if len(outcomes) == 0 {
fmt.Printf("🕛 No bets have been placed on event %v (%d/%d) \n", event.ID, i+1, len(events))
} else {
fmt.Printf("✅ %d bets have been placed on event %v (%d/%d) \n", len(outcomes), event.ID, i+1, len(events))
}
isDeleted := true
result, err := s.fetchResult(ctx, eventID) result, err := s.fetchResult(ctx, eventID)
if err != nil { if err != nil {
if err == ErrEventIsNotActive { if err == ErrEventIsNotActive {
@ -108,78 +217,41 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error {
} }
// TODO: Figure out what to do with the events that have been cancelled or postponed, etc... // TODO: Figure out what to do with the events that have been cancelled or postponed, etc...
if timeStatusParsed != int64(domain.TIME_STATUS_ENDED) { // if timeStatusParsed != int64(domain.TIME_STATUS_ENDED) {
s.logger.Warn("Event is not ended yet", "event_id", eventID, "time_status", commonResp.TimeStatus) // s.logger.Warn("Event is not ended yet", "event_id", eventID, "time_status", commonResp.TimeStatus)
fmt.Printf("⚠️ Event %v is not ended yet (%d/%d) \n", event.ID, i+1, len(events)) // fmt.Printf("⚠️ Event %v is not ended yet (%d/%d) \n", event.ID, i+1, len(events))
isDeleted = false // isDeleted = false
// continue
// }
// notification := &domain.Notification{
// RecipientID: recipientID,
// Type: domain.NOTIFICATION_TYPE_WALLET,
// Level: domain.NotificationLevelWarning,
// Reciever: domain.NotificationRecieverSideAdmin,
// DeliveryChannel: domain.DeliveryChannelInApp,
// Payload: domain.NotificationPayload{
// Headline: "Wallet Threshold Alert",
// Message: message,
// },
// Priority: 2, // Medium priority
// }
switch timeStatusParsed {
case int64(domain.TIME_STATUS_NOT_STARTED), int64(domain.TIME_STATUS_IN_PLAY):
continue continue
}
for j, outcome := range outcomes { case int64(domain.TIME_STATUS_TO_BE_FIXED):
fmt.Printf("⚙️ Processing 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n", s.logger.Warn("Event needs to be rescheduled or corrected", "event_id", eventID)
outcome.MarketName, // Admin users will be able to review the events
event.HomeTeam+" "+event.AwayTeam, event.ID,
j+1, len(outcomes))
if outcome.Expires.After(time.Now()) { case int64(domain.TIME_STATUS_ENDED), int64(domain.TIME_STATUS_WALKOVER), int64(domain.TIME_STATUS_DECIDED_BY_FA):
isDeleted = false err = s.UpdateResultForOutcomes(ctx, eventID, result.Results[0], sportID)
s.logger.Warn("Outcome is not expired yet", "event_id", event.ID, "outcome_id", outcome.ID)
continue
}
parseResult, err := s.parseResult(ctx, result.Results[0], outcome, sportID)
if err != nil { if err != nil {
isDeleted = false s.logger.Error("Error while updating result for event", "event_id", eventID)
s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err)
errs = append(errs, fmt.Errorf("failed to parse result for event %d: %w", outcome.EventID, err))
continue
}
outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, parseResult.Status)
if err != nil {
isDeleted = false
s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err)
continue
}
if outcome.Status == domain.OUTCOME_STATUS_ERROR || outcome.Status == domain.OUTCOME_STATUS_PENDING {
fmt.Printf("❌ Error while updating 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n",
outcome.MarketName,
event.HomeTeam+" "+event.AwayTeam, event.ID,
j+1, len(outcomes))
s.logger.Error("Outcome is pending or error", "event_id", outcome.EventID, "outcome_id", outcome.ID)
isDeleted = false
continue
} }
fmt.Printf("✅ Successfully updated 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n", s.logger.Info("Removing Event", "eventID", event.ID)
outcome.MarketName,
event.HomeTeam+" "+event.AwayTeam, event.ID,
j+1, len(outcomes))
status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID)
if err != nil {
if err != bet.ErrOutcomesNotCompleted {
s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err)
}
isDeleted = false
continue
}
fmt.Printf("🧾 Updating bet %v - event %v (%d/%d) to %v\n", outcome.BetID, event.ID, j+1, len(outcomes), status.String())
err = s.betSvc.UpdateStatus(ctx, outcome.BetID, status)
if err != nil {
s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err)
isDeleted = false
continue
}
fmt.Printf("✅ Successfully updated 🎫 Bet %v - event %v(%v) (%d/%d) \n",
outcome.BetID,
event.HomeTeam+" "+event.AwayTeam, event.ID,
j+1, len(outcomes))
}
if isDeleted {
removed += 1
fmt.Printf("⚠️ Removing Event %v \n", event.ID)
err = s.repo.DeleteEvent(ctx, event.ID) err = s.repo.DeleteEvent(ctx, event.ID)
if err != nil { if err != nil {
s.logger.Error("Failed to remove event", "event_id", event.ID, "error", err) s.logger.Error("Failed to remove event", "event_id", event.ID, "error", err)
@ -190,17 +262,91 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error {
s.logger.Error("Failed to remove odds for event", "event_id", event.ID, "error", err) s.logger.Error("Failed to remove odds for event", "event_id", event.ID, "error", err)
return err return err
} }
removed += 1
case int64(domain.TIME_STATUS_ABANDONED), int64(domain.TIME_STATUS_CANCELLED), int64(domain.TIME_STATUS_REMOVED):
s.logger.Info("Event abandoned/cancelled/removed", "event_id", eventID, "status", timeStatusParsed)
err = s.RefundAllOutcomes(ctx, eventID)
s.logger.Info("Removing Event", "eventID", event.ID)
err = s.repo.DeleteEvent(ctx, event.ID)
if err != nil {
s.logger.Error("Failed to remove event", "event_id", event.ID, "error", err)
return err
}
err = s.repo.DeleteOddsForEvent(ctx, event.ID)
if err != nil {
s.logger.Error("Failed to remove odds for event", "event_id", event.ID, "error", err)
return err
}
removed += 1
} }
// for j, outcome := range outcomes {
// fmt.Printf("⚙️ Processing 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n",
// outcome.MarketName,
// event.HomeTeam+" "+event.AwayTeam, event.ID,
// j+1, len(outcomes))
// if outcome.Expires.After(time.Now()) {
// isDeleted = false
// s.logger.Warn("Outcome is not expired yet", "event_id", event.ID, "outcome_id", outcome.ID)
// continue
// }
// parseResult, err := s.parseResult(ctx, result.Results[0], outcome, sportID)
// if err != nil {
// isDeleted = false
// s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err)
// errs = append(errs, fmt.Errorf("failed to parse result for event %d: %w", outcome.EventID, err))
// continue
// }
// outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, parseResult.Status)
// if err != nil {
// isDeleted = false
// s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err)
// continue
// }
// if outcome.Status == domain.OUTCOME_STATUS_ERROR || outcome.Status == domain.OUTCOME_STATUS_PENDING {
// fmt.Printf("❌ Error while updating 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n",
// outcome.MarketName,
// event.HomeTeam+" "+event.AwayTeam, event.ID,
// j+1, len(outcomes))
// s.logger.Error("Outcome is pending or error", "event_id", outcome.EventID, "outcome_id", outcome.ID)
// isDeleted = false
// continue
// }
// fmt.Printf("✅ Successfully updated 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n",
// outcome.MarketName,
// event.HomeTeam+" "+event.AwayTeam, event.ID,
// j+1, len(outcomes))
// status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID)
// if err != nil {
// if err != bet.ErrOutcomesNotCompleted {
// s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err)
// }
// isDeleted = false
// continue
// }
// fmt.Printf("🧾 Updating bet %v - event %v (%d/%d) to %v\n", outcome.BetID, event.ID, j+1, len(outcomes), status.String())
// err = s.betSvc.UpdateStatus(ctx, outcome.BetID, status)
// if err != nil {
// s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err)
// isDeleted = false
// continue
// }
// fmt.Printf("✅ Successfully updated 🎫 Bet %v - event %v(%v) (%d/%d) \n",
// outcome.BetID,
// event.HomeTeam+" "+event.AwayTeam, event.ID,
// j+1, len(outcomes))
// }
} }
fmt.Printf("🗑️ Removed Events: %d \n", removed) s.logger.Info("Total Number of Removed Events", "count", removed)
if len(errs) > 0 {
s.logger.Error("Errors occurred while processing results", "errors", errs)
for _, err := range errs {
fmt.Println("Error:", err)
}
return fmt.Errorf("errors occurred while processing results: %v", errs)
}
s.logger.Info("Successfully processed results", "removed_events", removed, "total_events", len(events)) s.logger.Info("Successfully processed results", "removed_events", removed, "total_events", len(events))
return nil return nil
} }
@ -211,10 +357,9 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error
s.logger.Error("Failed to fetch events") s.logger.Error("Failed to fetch events")
return 0, err return 0, err
} }
fmt.Printf("⚠️ Expired Events: %d \n", len(events))
updated := 0 updated := 0
for i, event := range events { for _, event := range events {
fmt.Printf("⚙️ Processing event %v (%d/%d) \n", event.ID, i+1, len(events)) // fmt.Printf("⚙️ Processing event %v (%d/%d) \n", event.ID, i+1, len(events))
eventID, err := strconv.ParseInt(event.ID, 10, 64) eventID, err := strconv.ParseInt(event.ID, 10, 64)
if err != nil { if err != nil {
s.logger.Error("Failed to parse event id") s.logger.Error("Failed to parse event id")
@ -232,7 +377,6 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error
} }
if result.Success != 1 || len(result.Results) == 0 { if result.Success != 1 || len(result.Results) == 0 {
s.logger.Error("Invalid API response", "event_id", eventID) s.logger.Error("Invalid API response", "event_id", eventID)
fmt.Printf("⚠️ Invalid API response for event %v \n", result)
continue continue
} }
@ -282,12 +426,13 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error
continue continue
} }
updated++ updated++
fmt.Printf("✅ Successfully updated event %v to %v (%d/%d) \n", event.ID, eventStatus, i+1, len(events)) // fmt.Printf("✅ Successfully updated event %v to %v (%d/%d) \n", event.ID, eventStatus, i+1, len(events))
s.logger.Info("Updated Event Status", "event ID", event.ID, "status", eventStatus)
// Update the league because the league country code is only found on the result response // Update the league because the league country code is only found on the result response
leagueID, err := strconv.ParseInt(commonResp.League.ID, 10, 64) leagueID, err := strconv.ParseInt(commonResp.League.ID, 10, 64)
if err != nil { if err != nil {
log.Printf("❌ Invalid league id, leagueID %v", commonResp.League.ID) // log.Printf("❌ Invalid league id, leagueID %v", commonResp.League.ID)
s.logger.Error("Invalid League ID", "leagueID", commonResp.League.ID)
continue continue
} }
@ -304,12 +449,11 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error
}) })
if err != nil { if err != nil {
log.Printf("❌ Error Updating League %v", commonResp.League.Name) s.logger.Error("Error Updating League", "League Name", commonResp.League.Name, "err", err)
log.Printf("err:%v", err)
continue continue
} }
fmt.Printf("✅ Updated League %v with country code %v \n", leagueID, commonResp.League.CC) // fmt.Printf("✅ Updated League %v with country code %v \n", leagueID, commonResp.League.CC)
s.logger.Info("Updated League with country code", "leagueID", leagueID, "code", commonResp.League.CC)
} }
if updated == 0 { if updated == 0 {

View File

@ -128,7 +128,7 @@ func (s *AleaPlayService) processTransaction(ctx context.Context, tx *domain.Vir
} }
tx.WalletID = wallets[0].ID tx.WalletID = wallets[0].ID
if err := s.walletSvc.AddToWallet(ctx, tx.WalletID, domain.Currency(tx.Amount)); err != nil { if _, err := s.walletSvc.AddToWallet(ctx, tx.WalletID, domain.Currency(tx.Amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil {
return fmt.Errorf("wallet update failed: %w", err) return fmt.Errorf("wallet update failed: %w", err)
} }

View File

@ -135,7 +135,7 @@ func (s *service) HandleCallback(ctx context.Context, callback *domain.PopOKCall
return errors.New("unknown transaction type") return errors.New("unknown transaction type")
} }
err = s.walletSvc.AddToWallet(ctx, walletID, domain.Currency(amount)) _, err = s.walletSvc.AddToWallet(ctx, walletID, domain.Currency(amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{})
if err != nil { if err != nil {
s.logger.Error("Failed to update wallet", "walletID", walletID, "userID", session.UserID, "amount", amount, "error", err) s.logger.Error("Failed to update wallet", "walletID", walletID, "userID", session.UserID, "amount", amount, "error", err)
return err return err
@ -202,7 +202,7 @@ func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) (
return &domain.PopOKBetResponse{}, fmt.Errorf("Failed to read user wallets") return &domain.PopOKBetResponse{}, fmt.Errorf("Failed to read user wallets")
} }
if err := s.walletSvc.DeductFromWallet(ctx, claims.UserID, domain.Currency(amountCents)); err != nil { if _, err := s.walletSvc.DeductFromWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.CustomerWalletType, domain.ValidInt64{}, domain.TRANSFER_DIRECT); err != nil {
return nil, fmt.Errorf("insufficient balance") return nil, fmt.Errorf("insufficient balance")
} }
@ -263,7 +263,7 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (
// 4. Credit to wallet // 4. Credit to wallet
if err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents)); err != nil { if _, err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil {
s.logger.Error("Failed to credit wallet", "userID", claims.UserID, "error", err) s.logger.Error("Failed to credit wallet", "userID", claims.UserID, "error", err)
return nil, fmt.Errorf("wallet credit failed") return nil, fmt.Errorf("wallet credit failed")
} }
@ -334,7 +334,7 @@ func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequ
// 5. Refund the bet amount (absolute value since bet amount is negative) // 5. Refund the bet amount (absolute value since bet amount is negative)
refundAmount := -originalBet.Amount refundAmount := -originalBet.Amount
if err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(refundAmount)); err != nil { if _, err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(refundAmount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil {
s.logger.Error("Failed to refund bet", "userID", claims.UserID, "error", err) s.logger.Error("Failed to refund bet", "userID", claims.UserID, "error", err)
return nil, fmt.Errorf("refund failed") return nil, fmt.Errorf("refund failed")
} }

View File

@ -14,85 +14,7 @@ var (
) )
func (s *Service) CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) { func (s *Service) CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) {
senderWallet, err := s.walletStore.GetWalletByID(ctx, transfer.SenderWalletID) // This is just a transfer log when
receiverWallet, err := s.walletStore.GetWalletByID(ctx, transfer.ReceiverWalletID)
if err != nil {
return domain.Transfer{}, fmt.Errorf("failed to get sender wallet: %w", err)
}
// Check if wallet has sufficient balance
if senderWallet.Balance < transfer.Amount || senderWallet.Balance == 0 {
// Send notification to customer
customerNotification := &domain.Notification{
RecipientID: receiverWallet.UserID,
Type: domain.NOTIFICATION_TYPE_TRANSFER,
Level: domain.NotificationLevelError,
Reciever: domain.NotificationRecieverSideCustomer,
DeliveryChannel: domain.DeliveryChannelInApp,
Payload: domain.NotificationPayload{
Headline: "Service Temporarily Unavailable",
Message: "Our payment system is currently under maintenance. Please try again later.",
},
Priority: 2,
Metadata: []byte(fmt.Sprintf(`{
"transfer_amount": %d,
"current_balance": %d,
"wallet_id": %d,
"notification_type": "customer_facing"
}`, transfer.Amount, senderWallet.Balance, transfer.SenderWalletID)),
}
// Send notification to admin team
adminNotification := &domain.Notification{
RecipientID: senderWallet.UserID,
Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT,
Level: domain.NotificationLevelError,
Reciever: domain.NotificationRecieverSideAdmin,
DeliveryChannel: domain.DeliveryChannelEmail, // Or any preferred admin channel
Payload: domain.NotificationPayload{
Headline: "CREDIT WARNING: System Running Out of Funds",
Message: fmt.Sprintf(
"Wallet ID %d has insufficient balance for transfer. Current balance: %.2f, Attempted transfer: %.2f",
transfer.SenderWalletID,
float64(senderWallet.Balance)/100,
float64(transfer.Amount)/100,
),
},
Priority: 1, // High priority for admin alerts
Metadata: fmt.Appendf(nil, `{
"wallet_id": %d,
"balance": %d,
"required_amount": %d,
"notification_type": "admin_alert"
}`, transfer.SenderWalletID, senderWallet.Balance, transfer.Amount),
}
// Send both notifications
if err := s.notificationStore.SendNotification(ctx, customerNotification); err != nil {
s.logger.Error("failed to send customer notification",
"user_id", "",
"error", err)
}
// Get admin recipients and send to all
adminRecipients, err := s.notificationStore.ListRecipientIDs(ctx, domain.NotificationRecieverSideAdmin)
if err != nil {
s.logger.Error("failed to get admin recipients", "error", err)
} else {
for _, adminID := range adminRecipients {
adminNotification.RecipientID = adminID
if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil {
s.logger.Error("failed to send admin notification",
"admin_id", adminID,
"error", err)
}
}
}
return domain.Transfer{}, ErrInsufficientBalance
}
// Proceed with transfer if balance is sufficient
return s.transferStore.CreateTransfer(ctx, transfer) return s.transferStore.CreateTransfer(ctx, transfer)
} }
@ -120,43 +42,10 @@ func (s *Service) UpdateTransferStatus(ctx context.Context, id int64, status str
return s.transferStore.UpdateTransferStatus(ctx, id, status) return s.transferStore.UpdateTransferStatus(ctx, id, status)
} }
func (s *Service) RefillWallet(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) {
receiverWallet, err := s.GetWalletByID(ctx, transfer.ReceiverWalletID)
if err != nil {
return domain.Transfer{}, err
}
// Add to receiver func (s *Service) TransferToWallet(ctx context.Context, senderID int64, receiverID int64,
senderWallet, err := s.GetWalletByID(ctx, transfer.SenderWalletID) amount domain.Currency, paymentMethod domain.PaymentMethod,
if err != nil { cashierID domain.ValidInt64) (domain.Transfer, error) {
return domain.Transfer{}, err
} else if senderWallet.Balance < transfer.Amount {
return domain.Transfer{}, ErrInsufficientBalance
}
err = s.walletStore.UpdateBalance(ctx, receiverWallet.ID, receiverWallet.Balance+transfer.Amount)
if err != nil {
return domain.Transfer{}, err
}
// Log the transfer so that if there is a mistake, it can be reverted
newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{
CashierID: transfer.CashierID,
ReceiverWalletID: receiverWallet.ID,
Amount: transfer.Amount,
Type: domain.DEPOSIT,
PaymentMethod: transfer.PaymentMethod,
Verified: true,
})
if err != nil {
return domain.Transfer{}, err
}
return newTransfer, nil
}
func (s *Service) TransferToWallet(ctx context.Context, senderID int64, receiverID int64, amount domain.Currency, paymentMethod domain.PaymentMethod, cashierID domain.ValidInt64) (domain.Transfer, error) {
senderWallet, err := s.GetWalletByID(ctx, senderID) senderWallet, err := s.GetWalletByID(ctx, senderID)
if err != nil { if err != nil {
@ -195,14 +84,20 @@ func (s *Service) TransferToWallet(ctx context.Context, senderID int64, receiver
} }
// Log the transfer so that if there is a mistake, it can be reverted // Log the transfer so that if there is a mistake, it can be reverted
transfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ transfer, err := s.CreateTransfer(ctx, domain.CreateTransfer{
SenderWalletID: senderID, SenderWalletID: domain.ValidInt64{
CashierID: cashierID, Value: senderID,
ReceiverWalletID: receiverID, Valid: true,
Amount: amount, },
Type: domain.WALLET, ReceiverWalletID: domain.ValidInt64{
PaymentMethod: paymentMethod, Value: receiverID,
Verified: true, Valid: true,
},
CashierID: cashierID,
Amount: amount,
Type: domain.WALLET,
PaymentMethod: paymentMethod,
Verified: true,
}) })
if err != nil { if err != nil {
return domain.Transfer{}, err return domain.Transfer{}, err
@ -210,3 +105,66 @@ func (s *Service) TransferToWallet(ctx context.Context, senderID int64, receiver
return transfer, nil return transfer, nil
} }
func (s *Service) SendTransferNotification(ctx context.Context, senderWallet domain.Wallet, receiverWallet domain.Wallet,
senderRole domain.Role, receiverRole domain.Role, amount domain.Currency) error {
// Send notification to sender (this could be any role) that money was transferred
senderWalletReceiverSide := domain.ReceiverFromRole(senderRole)
senderNotify := &domain.Notification{
RecipientID: senderWallet.UserID,
Type: domain.NOTIFICATION_TYPE_TRANSFER_SUCCESS,
Level: domain.NotificationLevelSuccess,
Reciever: senderWalletReceiverSide,
DeliveryChannel: domain.DeliveryChannelInApp,
Payload: domain.NotificationPayload{
Headline: "Wallet has been deducted",
Message: fmt.Sprintf(`ETB %d has been transferred from your wallet`),
},
Priority: 2,
Metadata: []byte(fmt.Sprintf(`{
"transfer_amount": %d,
"current_balance": %d,
"wallet_id": %d,
"notification_type": "customer_facing"
}`, amount, senderWallet.Balance, senderWallet.ID)),
}
// Sender notifications
if err := s.notificationStore.SendNotification(ctx, senderNotify); err != nil {
s.logger.Error("failed to send sender notification",
"user_id", "",
"error", err)
return err
}
receiverWalletReceiverSide := domain.ReceiverFromRole(receiverRole)
receiverNotify := &domain.Notification{
RecipientID: receiverWallet.UserID,
Type: domain.NOTIFICATION_TYPE_TRANSFER_SUCCESS,
Level: domain.NotificationLevelSuccess,
Reciever: receiverWalletReceiverSide,
DeliveryChannel: domain.DeliveryChannelInApp,
Payload: domain.NotificationPayload{
Headline: "Wallet has been credited",
Message: fmt.Sprintf(`ETB %d has been transferred to your wallet`),
},
Priority: 2,
Metadata: []byte(fmt.Sprintf(`{
"transfer_amount": %d,
"current_balance": %d,
"wallet_id": %d,
"notification_type": "customer_facing"
}`, amount, receiverWallet.Balance, receiverWallet.ID)),
}
// Sender notifications
if err := s.notificationStore.SendNotification(ctx, receiverNotify); err != nil {
s.logger.Error("failed to send sender notification",
"user_id", "",
"error", err)
return err
}
return nil
}

View File

@ -3,6 +3,7 @@ package wallet
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
) )
@ -68,28 +69,152 @@ func (s *Service) UpdateBalance(ctx context.Context, id int64, balance domain.Cu
return s.walletStore.UpdateBalance(ctx, id, balance) return s.walletStore.UpdateBalance(ctx, id, balance)
} }
func (s *Service) AddToWallet(ctx context.Context, id int64, amount domain.Currency) error { func (s *Service) AddToWallet(
ctx context.Context, id int64, amount domain.Currency, cashierID domain.ValidInt64, paymentMethod domain.PaymentMethod, paymentDetails domain. PaymentDetails) (domain.Transfer, error) {
wallet, err := s.GetWalletByID(ctx, id) wallet, err := s.GetWalletByID(ctx, id)
if err != nil { if err != nil {
return err return domain.Transfer{}, err
} }
return s.walletStore.UpdateBalance(ctx, id, wallet.Balance+amount) err = s.walletStore.UpdateBalance(ctx, id, wallet.Balance+amount)
if err != nil {
return domain.Transfer{}, err
}
// Log the transfer here for reference
newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{
Amount: amount,
Verified: true,
ReceiverWalletID: domain.ValidInt64{
Value: wallet.ID,
Valid: true,
},
CashierID: cashierID,
PaymentMethod: paymentMethod,
Type: domain.DEPOSIT,
ReferenceNumber: paymentDetails.ReferenceNumber.Value,
})
return newTransfer, err
} }
func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.Currency) error { func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.Currency, walletType domain.WalletType, cashierID domain.ValidInt64, paymentMethod domain.PaymentMethod) (domain.Transfer, error) {
wallet, err := s.GetWalletByID(ctx, id) wallet, err := s.GetWalletByID(ctx, id)
if err != nil { if err != nil {
return err return domain.Transfer{}, err
} }
if wallet.Balance < amount { if wallet.Balance < amount {
return ErrBalanceInsufficient // Send Wallet low to admin
if walletType == domain.CompanyWalletType || walletType == domain.BranchWalletType {
s.SendAdminWalletLowNotification(ctx, wallet, amount)
}
return domain.Transfer{}, ErrBalanceInsufficient
} }
return s.walletStore.UpdateBalance(ctx, id, wallet.Balance-amount) err = s.walletStore.UpdateBalance(ctx, id, wallet.Balance-amount)
if err != nil {
return domain.Transfer{}, nil
}
// Log the transfer here for reference
newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{
Amount: amount,
Verified: true,
SenderWalletID: domain.ValidInt64{
Value: wallet.ID,
Valid: true,
},
CashierID: cashierID,
PaymentMethod: paymentMethod,
Type: domain.WITHDRAW,
ReferenceNumber: "",
})
return newTransfer, err
} }
// Directly Refilling wallet without
// func (s *Service) RefillWallet(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) {
// receiverWallet, err := s.GetWalletByID(ctx, transfer.ReceiverWalletID)
// if err != nil {
// return domain.Transfer{}, err
// }
// // Add to receiver
// senderWallet, err := s.GetWalletByID(ctx, transfer.SenderWalletID)
// if err != nil {
// return domain.Transfer{}, err
// } else if senderWallet.Balance < transfer.Amount {
// return domain.Transfer{}, ErrInsufficientBalance
// }
// err = s.walletStore.UpdateBalance(ctx, receiverWallet.ID, receiverWallet.Balance+transfer.Amount)
// if err != nil {
// return domain.Transfer{}, err
// }
// // Log the transfer so that if there is a mistake, it can be reverted
// newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{
// CashierID: transfer.CashierID,
// ReceiverWalletID: transfer.ReceiverWalletID,
// Amount: transfer.Amount,
// Type: domain.DEPOSIT,
// PaymentMethod: transfer.PaymentMethod,
// Verified: true,
// })
// if err != nil {
// return domain.Transfer{}, err
// }
// return newTransfer, nil
// }
func (s *Service) UpdateWalletActive(ctx context.Context, id int64, isActive bool) error { func (s *Service) UpdateWalletActive(ctx context.Context, id int64, isActive bool) error {
return s.walletStore.UpdateWalletActive(ctx, id, isActive) return s.walletStore.UpdateWalletActive(ctx, id, isActive)
} }
func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWallet domain.Wallet, amount domain.Currency) error {
// Send notification to admin team
adminNotification := &domain.Notification{
RecipientID: adminWallet.UserID,
Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT,
Level: domain.NotificationLevelError,
Reciever: domain.NotificationRecieverSideAdmin,
DeliveryChannel: domain.DeliveryChannelEmail, // Or any preferred admin channel
Payload: domain.NotificationPayload{
Headline: "CREDIT WARNING: System Running Out of Funds",
Message: fmt.Sprintf(
"Wallet ID %d has insufficient balance for transfer. Current balance: %.2f, Attempted transfer: %.2f",
adminWallet.ID,
adminWallet.Balance.Float32(),
amount.Float32(),
),
},
Priority: 1, // High priority for admin alerts
Metadata: fmt.Appendf(nil, `{
"wallet_id": %d,
"balance": %d,
"required_amount": %d,
"notification_type": "admin_alert"
}`, adminWallet.ID, adminWallet.Balance, amount),
}
// Get admin recipients and send to all
adminRecipients, err := s.notificationStore.ListRecipientIDs(ctx, domain.NotificationRecieverSideAdmin)
if err != nil {
s.logger.Error("failed to get admin recipients", "error", err)
return err
} else {
for _, adminID := range adminRecipients {
adminNotification.RecipientID = adminID
if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil {
s.logger.Error("failed to send admin notification",
"admin_id", adminID,
"error", err)
}
}
}
return nil
}

View File

@ -46,22 +46,22 @@ 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() {
if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
log.Printf("FetchUpcomingEvents error: %v", err) // log.Printf("FetchUpcomingEvents error: %v", err)
} // }
}, // },
}, // },
{ // {
spec: "0 0 * * * *", // Every 15 minutes // spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events)
task: func() { // task: func() {
if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
log.Printf("FetchNonLiveOdds error: %v", err) // log.Printf("FetchNonLiveOdds error: %v", err)
} // }
}, // },
}, // },
{ {
spec: "0 */5 * * * *", // Every 5 Minutes spec: "0 */5 * * * *", // Every 5 Minutes
task: func() { task: func() {
@ -89,7 +89,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
} }
for _, job := range schedule { for _, job := range schedule {
// job.task() job.task()
if _, err := c.AddFunc(job.spec, job.task); err != nil { if _, err := c.AddFunc(job.spec, job.task); err != nil {
log.Fatalf("Failed to schedule cron job: %v", err) log.Fatalf("Failed to schedule cron job: %v", err)
} }

View File

@ -36,15 +36,14 @@ type loginCustomerRes struct {
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /auth/login [post] // @Router /auth/login [post]
func (h *Handler) LoginCustomer(c *fiber.Ctx) error { func (h *Handler) LoginCustomer(c *fiber.Ctx) error {
var req loginCustomerReq var req loginCustomerReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
h.logger.Error("Failed to parse LoginCustomer request", "error", err) h.logger.Error("Failed to parse LoginCustomer request", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
} }
if valErrs, ok := h.validator.Validate(c, req); !ok { if _, ok := h.validator.Validate(c, req); !ok {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) return fiber.NewError(fiber.StatusBadRequest, "Invalid Request")
} }
successRes, err := h.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password) successRes, err := h.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password)

View File

@ -1,7 +1,6 @@
package handlers package handlers
import ( import (
"fmt"
"strconv" "strconv"
"time" "time"
@ -24,7 +23,6 @@ import (
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet [post] // @Router /bet [post]
func (h *Handler) CreateBet(c *fiber.Ctx) error { func (h *Handler) CreateBet(c *fiber.Ctx) error {
fmt.Printf("Calling leagues")
// Get user_id from middleware // Get user_id from middleware
userID := c.Locals("user_id").(int64) userID := c.Locals("user_id").(int64)
role := c.Locals("role").(domain.Role) role := c.Locals("role").(domain.Role)
@ -160,12 +158,29 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error {
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet [get] // @Router /bet [get]
func (h *Handler) GetAllBet(c *fiber.Ctx) error { func (h *Handler) GetAllBet(c *fiber.Ctx) error {
// role := c.Locals("role").(domain.Role)
companyID := c.Locals("company_id").(domain.ValidInt64) companyID := c.Locals("company_id").(domain.ValidInt64)
branchID := c.Locals("branch_id").(domain.ValidInt64) branchID := c.Locals("branch_id").(domain.ValidInt64)
var isShopBet domain.ValidBool
isShopBetQuery := c.Query("is_shop")
if isShopBetQuery != "" {
isShopBetParse, err := strconv.ParseBool(isShopBetQuery)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Failed to parse is_shop_bet")
}
isShopBet = domain.ValidBool{
Value: isShopBetParse,
Valid: true,
}
}
bets, err := h.betSvc.GetAllBets(c.Context(), domain.BetFilter{ bets, err := h.betSvc.GetAllBets(c.Context(), domain.BetFilter{
BranchID: branchID, BranchID: branchID,
CompanyID: companyID, CompanyID: companyID,
IsShopBet: isShopBet,
}) })
if err != nil { if err != nil {
h.logger.Error("Failed to get bets", "error", err) h.logger.Error("Failed to get bets", "error", err)

View File

@ -382,8 +382,23 @@ func (h *Handler) GetBranchByCompanyID(c *fiber.Ctx) error {
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /branch [get] // @Router /branch [get]
func (h *Handler) GetAllBranches(c *fiber.Ctx) error { func (h *Handler) GetAllBranches(c *fiber.Ctx) error {
// TODO: Limit the get all branches to only the companies for branch manager and cashiers companyID := c.Locals("company_id").(domain.ValidInt64)
branches, err := h.branchSvc.GetAllBranches(c.Context()) isActiveParam := c.Params("is_active")
isActiveValid := isActiveParam != ""
isActive, err := strconv.ParseBool(isActiveParam)
if isActiveValid && err != nil {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid is_active param", err, nil)
}
branches, err := h.branchSvc.GetAllBranches(c.Context(),
domain.BranchFilter{
CompanyID: companyID,
IsSuspended: domain.ValidBool{
Value: isActive,
Valid: isActiveValid,
},
})
if err != nil { if err != nil {
h.logger.Error("Failed to get branches", "error", err) h.logger.Error("Failed to get branches", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get branches", err, nil) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get branches", err, nil)

View File

@ -106,3 +106,32 @@ func (h *Handler) SetLeagueActive(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil) return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil)
} }
func (h *Handler) SetLeagueAsFeatured(c *fiber.Ctx) error {
fmt.Printf("Set Active Leagues")
leagueIdStr := c.Params("id")
if leagueIdStr == "" {
response.WriteJSON(c, fiber.StatusBadRequest, "Missing league id", nil, nil)
}
leagueId, err := strconv.Atoi(leagueIdStr)
if err != nil {
response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, nil)
}
var req SetLeagueActiveReq
if err := c.BodyParser(&req); err != nil {
h.logger.Error("SetLeagueReq failed", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Failed to parse request", err, nil)
}
valErrs, ok := h.validator.Validate(c, req)
if !ok {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
}
if err := h.leagueSvc.SetLeagueActive(c.Context(), int64(leagueId), req.IsActive); err != nil {
response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update league", err, nil)
}
return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil)
}

View File

@ -111,7 +111,8 @@ type ManagersRes struct {
func (h *Handler) GetAllManagers(c *fiber.Ctx) error { func (h *Handler) GetAllManagers(c *fiber.Ctx) error {
role := c.Locals("role").(domain.Role) role := c.Locals("role").(domain.Role)
companyId := c.Locals("company_id").(domain.ValidInt64) companyId := c.Locals("company_id").(domain.ValidInt64)
// Checking to make sure that admin user has a company id in the token
if role != domain.RoleSuperAdmin && !companyId.Valid { if role != domain.RoleSuperAdmin && !companyId.Valid {
return fiber.NewError(fiber.StatusInternalServerError, "Cannot get company ID") return fiber.NewError(fiber.StatusInternalServerError, "Cannot get company ID")
} }
@ -182,32 +183,38 @@ func (h *Handler) GetAllManagers(c *fiber.Ctx) error {
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /managers/{id} [get] // @Router /managers/{id} [get]
func (h *Handler) GetManagerByID(c *fiber.Ctx) error { func (h *Handler) GetManagerByID(c *fiber.Ctx) error {
// branchId := int64(12) //c.Locals("branch_id").(int64) role := c.Locals("role").(domain.Role)
// filter := user.Filter{ companyId := c.Locals("company_id").(domain.ValidInt64)
// Role: string(domain.RoleUser), requestUserID := c.Locals("user_id").(int64)
// BranchId: user.ValidBranchId{
// Value: branchId, // Only Super Admin / Admin / Branch Manager can view this route
// Valid: true, if role != domain.RoleSuperAdmin && role != domain.RoleAdmin && role != domain.RoleBranchManager {
// }, return fiber.NewError(fiber.StatusUnauthorized, "Role Unauthorized")
// Page: c.QueryInt("page", 1), }
// PageSize: c.QueryInt("page_size", 10),
// } if role != domain.RoleSuperAdmin && !companyId.Valid {
// valErrs, ok := validator.Validate(c, filter) return fiber.NewError(fiber.StatusInternalServerError, "Cannot get company ID")
// if !ok { }
// return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
// }
userIDstr := c.Params("id") userIDstr := c.Params("id")
userID, err := strconv.ParseInt(userIDstr, 10, 64) userID, err := strconv.ParseInt(userIDstr, 10, 64)
if err != nil { if err != nil {
h.logger.Error("failed to fetch user using UserID", "error", err) return fiber.NewError(fiber.StatusBadRequest, "Invalid managers ID")
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid managers ID", nil, nil)
} }
user, err := h.userSvc.GetUserByID(c.Context(), userID) user, err := h.userSvc.GetUserByID(c.Context(), userID)
if err != nil { if err != nil {
h.logger.Error("Get User By ID failed", "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to get managers")
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get managers", nil, nil) }
// A Branch Manager can only fetch his own branch info
if role == domain.RoleBranchManager && user.ID != requestUserID {
return fiber.NewError(fiber.StatusBadRequest, "User Access Not Allowed")
}
// Check that only admin from company can view this route
if role != domain.RoleSuperAdmin && user.CompanyID.Value != companyId.Value {
return fiber.NewError(fiber.StatusBadRequest, "Only company user can view manager info")
} }
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID) lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
@ -259,7 +266,9 @@ type updateManagerReq struct {
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /managers/{id} [put] // @Router /managers/{id} [put]
func (h *Handler) UpdateManagers(c *fiber.Ctx) error { func (h *Handler) UpdateManagers(c *fiber.Ctx) error {
var req updateManagerReq var req updateManagerReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
h.logger.Error("UpdateManagers failed", "error", err) h.logger.Error("UpdateManagers failed", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil)

View File

@ -56,8 +56,8 @@ func (h *Handler) GetRawOddsByMarketID(c *fiber.Ctx) error {
rawOdds, err := h.prematchSvc.GetRawOddsByMarketID(c.Context(), marketID, upcomingID) rawOdds, err := h.prematchSvc.GetRawOddsByMarketID(c.Context(), marketID, upcomingID)
if err != nil { if err != nil {
fmt.Printf("Failed to fetch raw odds: %v market_id:%v upcomingID:%v\n", err, marketID, upcomingID) // fmt.Printf("Failed to fetch raw odds: %v market_id:%v upcomingID:%v\n", err, marketID, upcomingID)
h.logger.Error("failed to fetch raw odds", "error", err) h.logger.Error("Failed to get raw odds by market ID", "marketID", marketID, "upcomingID", upcomingID, "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve raw odds", err, nil) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve raw odds", err, nil)
} }
@ -172,6 +172,62 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error {
} }
type TopLeaguesRes struct {
Leagues []TopLeague `json:"leagues"`
}
type TopLeague struct {
LeagueID int64 `json:"league_id"`
LeagueName string `json:"league_name"`
Events []domain.UpcomingEvent `json:"events"`
// Total int64 `json:"total"`
}
// @Summary Retrieve all top leagues
// @Description Retrieve all top leagues
// @Tags prematch
// @Accept json
// @Produce json
// @Success 200 {array} domain.UpcomingEvent
// @Failure 500 {object} response.APIResponse
// @Router /top-leagues [get]
func (h *Handler) GetTopLeagues(c *fiber.Ctx) error {
leagues, err := h.leagueSvc.GetFeaturedLeagues(c.Context())
if err != nil {
h.logger.Error("Error while fetching top leagues", "err", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get featured leagues", nil, nil)
}
var topLeague []TopLeague = make([]TopLeague, 0, len(leagues))
for _, league := range leagues {
events, _, err := h.eventSvc.GetPaginatedUpcomingEvents(
c.Context(), domain.EventFilter{
LeagueID: domain.ValidInt32{
Value: int32(league.ID),
Valid: true,
},
})
if err != nil {
fmt.Printf("Error while fetching events for top league %v \n", league.ID)
h.logger.Error("Error while fetching events for top league", "League ID", league.ID)
}
topLeague = append(topLeague, TopLeague{
LeagueID: league.ID,
LeagueName: league.Name,
Events: events,
})
}
res := TopLeaguesRes{
Leagues: topLeague,
}
return response.WriteJSON(c, fiber.StatusOK, "All top leagues events retrieved successfully", res, nil)
}
// @Summary Retrieve an upcoming by ID // @Summary Retrieve an upcoming by ID
// @Description Retrieve an upcoming event by ID // @Description Retrieve an upcoming event by ID
// @Tags prematch // @Tags prematch

View File

@ -15,7 +15,7 @@ type TransferWalletRes struct {
Verified bool `json:"verified" example:"true"` Verified bool `json:"verified" example:"true"`
Type string `json:"type" example:"transfer"` Type string `json:"type" example:"transfer"`
PaymentMethod string `json:"payment_method" example:"bank"` PaymentMethod string `json:"payment_method" example:"bank"`
ReceiverWalletID int64 `json:"receiver_wallet_id" example:"1"` ReceiverWalletID *int64 `json:"receiver_wallet_id" example:"1"`
SenderWalletID *int64 `json:"sender_wallet_id" example:"1"` SenderWalletID *int64 `json:"sender_wallet_id" example:"1"`
CashierID *int64 `json:"cashier_id" example:"789"` CashierID *int64 `json:"cashier_id" example:"789"`
CreatedAt time.Time `json:"created_at" example:"2025-04-08T12:00:00Z"` CreatedAt time.Time `json:"created_at" example:"2025-04-08T12:00:00Z"`
@ -27,7 +27,7 @@ type RefillRes struct {
Verified bool `json:"verified" example:"true"` Verified bool `json:"verified" example:"true"`
Type string `json:"type" example:"transfer"` Type string `json:"type" example:"transfer"`
PaymentMethod string `json:"payment_method" example:"bank"` PaymentMethod string `json:"payment_method" example:"bank"`
ReceiverWalletID int64 `json:"receiver_wallet_id" example:"1"` ReceiverWalletID *int64 `json:"receiver_wallet_id" example:"1"`
SenderWalletID *int64 `json:"sender_wallet_id" example:"1"` SenderWalletID *int64 `json:"sender_wallet_id" example:"1"`
CashierID *int64 `json:"cashier_id" example:"789"` CashierID *int64 `json:"cashier_id" example:"789"`
CreatedAt time.Time `json:"created_at" example:"2025-04-08T12:00:00Z"` CreatedAt time.Time `json:"created_at" example:"2025-04-08T12:00:00Z"`
@ -35,13 +35,20 @@ type RefillRes struct {
} }
func convertTransfer(transfer domain.Transfer) TransferWalletRes { func convertTransfer(transfer domain.Transfer) TransferWalletRes {
var senderWalletID *int64
senderWalletID = &transfer.SenderWalletID
var cashierID *int64 var cashierID *int64
if transfer.CashierID.Valid { if transfer.CashierID.Valid {
cashierID = &transfer.CashierID.Value cashierID = &transfer.CashierID.Value
} }
var receiverID *int64
if transfer.ReceiverWalletID.Valid {
receiverID = &transfer.ReceiverWalletID.Value
}
var senderId *int64
if transfer.SenderWalletID.Valid {
senderId = &transfer.SenderWalletID.Value
}
return TransferWalletRes{ return TransferWalletRes{
ID: transfer.ID, ID: transfer.ID,
@ -49,8 +56,8 @@ func convertTransfer(transfer domain.Transfer) TransferWalletRes {
Verified: transfer.Verified, Verified: transfer.Verified,
Type: string(transfer.Type), Type: string(transfer.Type),
PaymentMethod: string(transfer.PaymentMethod), PaymentMethod: string(transfer.PaymentMethod),
ReceiverWalletID: transfer.ReceiverWalletID, ReceiverWalletID: receiverID,
SenderWalletID: senderWalletID, SenderWalletID: senderId,
CashierID: cashierID, CashierID: cashierID,
CreatedAt: transfer.CreatedAt, CreatedAt: transfer.CreatedAt,
UpdatedAt: transfer.UpdatedAt, UpdatedAt: transfer.UpdatedAt,
@ -126,16 +133,16 @@ func (h *Handler) TransferToWallet(c *fiber.Ctx) error {
} }
// Get sender ID from the cashier // Get sender ID from the cashier
userID := c.Locals("user_id").(int64) userID := c.Locals("user_id").(int64)
role := string(c.Locals("role").(domain.Role)) role := c.Locals("role").(domain.Role)
companyID := c.Locals("company_id").(int64) companyID := c.Locals("company_id").(int64)
var senderID int64 var senderID int64
//TODO: check to make sure that the cashiers aren't transferring TO branch wallet //TODO: check to make sure that the cashiers aren't transferring TO branch wallet
if role == string(domain.RoleCustomer) { if role == domain.RoleCustomer {
h.logger.Error("Unauthorized access", "userID", userID, "role", role) h.logger.Error("Unauthorized access", "userID", userID, "role", role)
return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil)
} else if role == string(domain.RoleBranchManager) || role == string(domain.RoleAdmin) || role == string(domain.RoleSuperAdmin) { } else if role == domain.RoleBranchManager || role == domain.RoleAdmin || role == domain.RoleSuperAdmin {
company, err := h.companySvc.GetCompanyByID(c.Context(), companyID) company, err := h.companySvc.GetCompanyByID(c.Context(), companyID)
if err != nil { if err != nil {
return response.WriteJSON(c, fiber.StatusInternalServerError, "Error fetching company", err, nil) return response.WriteJSON(c, fiber.StatusInternalServerError, "Error fetching company", err, nil)
@ -190,6 +197,7 @@ func (h *Handler) RefillWallet(c *fiber.Ctx) error {
receiverIDString := c.Params("id") receiverIDString := c.Params("id")
userID := c.Locals("user_id").(int64)
receiverID, err := strconv.ParseInt(receiverIDString, 10, 64) receiverID, err := strconv.ParseInt(receiverIDString, 10, 64)
if err != nil { if err != nil {
@ -197,13 +205,6 @@ func (h *Handler) RefillWallet(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid wallet ID", err, nil) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid wallet ID", err, nil)
} }
// Get sender ID from the cashier // Get sender ID from the cashier
userID := c.Locals("user_id").(int64)
role := string(c.Locals("role").(domain.Role))
if role != string(domain.RoleSuperAdmin) {
h.logger.Error("Unauthorized access", "userID", userID, "role", role)
return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil)
}
var req CreateRefillReq var req CreateRefillReq
@ -217,16 +218,11 @@ func (h *Handler) RefillWallet(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
} }
transfer, err := h.walletSvc.RefillWallet(c.Context(), domain.CreateTransfer{ transfer, err := h.walletSvc.AddToWallet(
Amount: domain.ToCurrency(req.Amount), c.Context(), receiverID, domain.ToCurrency(req.Amount), domain.ValidInt64{
PaymentMethod: domain.TRANSFER_BANK,
ReceiverWalletID: receiverID,
CashierID: domain.ValidInt64{
Value: userID, Value: userID,
Valid: true, Valid: true,
}, }, domain.TRANSFER_BANK, domain.PaymentDetails{})
Type: domain.TransferType("deposit"),
})
if !ok { if !ok {
return response.WriteJSON(c, fiber.StatusInternalServerError, "Creating Transfer Failed", err, nil) return response.WriteJSON(c, fiber.StatusInternalServerError, "Creating Transfer Failed", err, nil)

View File

@ -194,7 +194,8 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error {
} }
// TODO: Remove later // TODO: Remove later
err = h.walletSvc.AddToWallet(c.Context(), newWallet.ID, domain.ToCurrency(100.0)) _, err = h.walletSvc.AddToWallet(
c.Context(), newWallet.ID, domain.ToCurrency(100.0), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{})
if err != nil { if err != nil {
h.logger.Error("Failed to update wallet for user", "userID", newUser.ID, "error", err) h.logger.Error("Failed to update wallet for user", "userID", newUser.ID, "error", err)
@ -416,6 +417,7 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error {
return nil return nil
} }
companyID := c.Locals("company_id").(domain.ValidInt64) companyID := c.Locals("company_id").(domain.ValidInt64)
users, err := h.userSvc.SearchUserByNameOrPhone(c.Context(), req.SearchString, req.Role, companyID) users, err := h.userSvc.SearchUserByNameOrPhone(c.Context(), req.SearchString, req.Role, companyID)
if err != nil { if err != nil {
h.logger.Error("SearchUserByNameOrPhone failed", "error", err) h.logger.Error("SearchUserByNameOrPhone failed", "error", err)

View File

@ -87,6 +87,22 @@ func (a *App) CompanyOnly(c *fiber.Ctx) error {
return c.Next() return c.Next()
} }
func (a *App) OnlyAdminAndAbove(c *fiber.Ctx) error {
userRole := c.Locals("role").(domain.Role)
if userRole != domain.RoleSuperAdmin && userRole != domain.RoleAdmin {
return fiber.NewError(fiber.StatusUnauthorized, "Invalid access token")
}
return c.Next()
}
func (a *App) OnlyBranchManagerAndAbove(c *fiber.Ctx) error {
userRole := c.Locals("role").(domain.Role)
if userRole != domain.RoleSuperAdmin && userRole != domain.RoleAdmin && userRole != domain.RoleBranchManager {
return fiber.NewError(fiber.StatusUnauthorized, "Invalid access token")
}
return c.Next()
}
func (a *App) WebsocketAuthMiddleware(c *fiber.Ctx) error { func (a *App) WebsocketAuthMiddleware(c *fiber.Ctx) error {
tokenStr := c.Query("token") tokenStr := c.Query("token")
if tokenStr == "" { if tokenStr == "" {

View File

@ -130,6 +130,7 @@ func (a *App) initAppRoutes() {
a.fiber.Get("/events", h.GetAllUpcomingEvents) a.fiber.Get("/events", h.GetAllUpcomingEvents)
a.fiber.Get("/events/:id", h.GetUpcomingEventByID) a.fiber.Get("/events/:id", h.GetUpcomingEventByID)
a.fiber.Delete("/events/:id", a.authMiddleware, a.SuperAdminOnly, h.SetEventStatusToRemoved) a.fiber.Delete("/events/:id", a.authMiddleware, a.SuperAdminOnly, h.SetEventStatusToRemoved)
a.fiber.Get("/top-leagues", h.GetTopLeagues)
// Leagues // Leagues
a.fiber.Get("/leagues", h.GetAllLeagues) a.fiber.Get("/leagues", h.GetAllLeagues)