5 min report fix + arifpay integration

This commit is contained in:
Yared Yemane 2025-07-23 14:03:38 +03:00
commit a35d4b37d3
86 changed files with 2772 additions and 560 deletions

2
.gitignore vendored
View File

@ -8,4 +8,4 @@ build
logs/ logs/
app_logs/ app_logs/
backup/ backup/
reports/

View File

@ -7,5 +7,12 @@
], ],
"cSpell.enabledFileTypes": { "cSpell.enabledFileTypes": {
"sql": false "sql": false
} },
"workbench.editor.customLabels.enabled": true,
"workbench.editor.customLabels.patterns": {
"**/internal/services/**/service.go": "${dirname}.service",
"**/internal/services/**/*.go": "${filename}.${dirname}.service",
"**/internal/domain/**/*.go": "${filename}.${dirname}",
"**/internal/repository/**/*.go": "${filename}.repo",
},
} }

View File

@ -42,7 +42,8 @@ import (
issuereporting "github.com/SamuelTariku/FortuneBet-Backend/internal/services/issue_reporting" issuereporting "github.com/SamuelTariku/FortuneBet-Backend/internal/services/issue_reporting"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/kafka" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/kafka"
"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/messenger"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation"
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
@ -104,13 +105,15 @@ func main() {
// Initialize services // Initialize services
settingSvc := settings.NewService(store) settingSvc := settings.NewService(store)
messengerSvc := messenger.NewService(settingSvc, cfg)
authSvc := authentication.NewService(store, store, cfg.RefreshExpiry) authSvc := authentication.NewService(store, store, cfg.RefreshExpiry)
userSvc := user.NewService(store, store, cfg) userSvc := user.NewService(store, store, messengerSvc, cfg)
eventSvc := event.New(cfg.Bet365Token, store) eventSvc := event.New(cfg.Bet365Token, store)
oddsSvc := odds.New(store, cfg, logger) oddsSvc := odds.New(store, cfg, logger)
notificationRepo := repository.NewNotificationRepository(store) notificationRepo := repository.NewNotificationRepository(store)
virtuaGamesRepo := repository.NewVirtualGameRepository(store) virtuaGamesRepo := repository.NewVirtualGameRepository(store)
notificationSvc := notificationservice.New(notificationRepo, logger, cfg) notificationSvc := notificationservice.New(notificationRepo, domain.MongoDBLogger, logger, cfg, messengerSvc, userSvc)
var notificatioStore notificationservice.NotificationStore var notificatioStore notificationservice.NotificationStore
// var userStore user.UserStore // var userStore user.UserStore
@ -121,6 +124,8 @@ func main() {
notificatioStore, notificatioStore,
// userStore, // userStore,
notificationSvc, notificationSvc,
userSvc,
domain.MongoDBLogger,
logger, logger,
kafka.NewProducer([]string{"localhost:9092"}, "wallet-events"), kafka.NewProducer([]string{"localhost:9092"}, "wallet-events"),
) )
@ -129,7 +134,7 @@ func main() {
companySvc := company.NewService(store) companySvc := company.NewService(store)
leagueSvc := league.New(store) leagueSvc := league.New(store)
ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger, *settingSvc, notificationSvc) ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger, *settingSvc, notificationSvc)
betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, *companySvc, *settingSvc, notificationSvc, logger, domain.MongoDBLogger) betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, *companySvc, *settingSvc, *userSvc, notificationSvc, logger, domain.MongoDBLogger)
resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc) resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc)
bonusSvc := bonus.NewService(store) bonusSvc := bonus.NewService(store)
referalRepo := repository.NewReferralRepository(store) referalRepo := repository.NewReferralRepository(store)

View File

@ -79,4 +79,6 @@ DROP TABLE IF EXISTS events;
DROP TABLE IF EXISTS leagues; DROP TABLE IF EXISTS leagues;
DROP TABLE IF EXISTS teams; DROP TABLE IF EXISTS teams;
DROP TABLE IF EXISTS settings; DROP TABLE IF EXISTS settings;
DROP TABLE IF EXISTS bonus;
DROP TABLE IF EXISTS flags;
-- DELETE FROM wallet_transfer; -- DELETE FROM wallet_transfer;

View File

@ -133,6 +133,7 @@ CREATE TABLE IF NOT EXISTS wallets (
is_bettable BOOLEAN NOT NULL, is_bettable BOOLEAN NOT NULL,
is_transferable BOOLEAN NOT NULL, is_transferable BOOLEAN NOT NULL,
user_id BIGINT NOT NULL, user_id BIGINT NOT NULL,
type VARCHAR(255) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT true, is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
@ -208,7 +209,7 @@ CREATE TABLE IF NOT EXISTS branches (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
location TEXT NOT NULL, location TEXT NOT NULL,
profit_percent REAL NOt NULL, profit_percent REAL NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT false, is_active BOOLEAN NOT NULL DEFAULT false,
wallet_id BIGINT NOT NULL, wallet_id BIGINT NOT NULL,
branch_manager_id BIGINT NOT NULL, branch_manager_id BIGINT NOT NULL,
@ -216,7 +217,11 @@ CREATE TABLE IF NOT EXISTS branches (
is_self_owned BOOLEAN NOT NULL DEFAULT false, is_self_owned BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(wallet_id) UNIQUE(wallet_id),
CONSTRAINT profit_percentage_check CHECK (
profit_percent >= 0
AND profit_percent < 1
)
); );
CREATE TABLE IF NOT EXISTS branch_operations ( CREATE TABLE IF NOT EXISTS branch_operations (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
@ -258,7 +263,8 @@ CREATE TABLE events (
status TEXT, status TEXT,
fetched_at TIMESTAMP DEFAULT now(), fetched_at TIMESTAMP DEFAULT now(),
source TEXT DEFAULT 'b365api', source TEXT DEFAULT 'b365api',
flagged BOOLEAN NOT NULL DEFAULT false is_featured BOOLEAN NOT NULL DEFAULT FALSE,
is_active BOOLEAN NOT NULL DEFAULT TRUE
); );
CREATE TABLE odds ( CREATE TABLE odds (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
@ -289,7 +295,11 @@ CREATE TABLE companies (
deducted_percentage REAL NOT NULL, deducted_percentage REAL NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT false, is_active BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT deducted_percentage_check CHECK (
deducted_percentage >= 0
AND deducted_percentage < 1
)
); );
CREATE TABLE leagues ( CREATE TABLE leagues (
id BIGINT PRIMARY KEY, id BIGINT PRIMARY KEY,
@ -319,6 +329,25 @@ CREATE TABLE bonus (
multiplier REAL NOT NULL, multiplier REAL NOT NULL,
balance_cap BIGINT NOT NULL DEFAULT 0 balance_cap BIGINT NOT NULL DEFAULT 0
); );
CREATE TABLE flags (
id BIGSERIAL PRIMARY KEY,
bet_id BIGINT REFERENCES bets(id) ON DELETE CASCADE,
odd_id BIGINT REFERENCES odds(id),
reason TEXT,
flagged_at TIMESTAMP DEFAULT NOW(),
resolved BOOLEAN DEFAULT FALSE,
-- either bet or odd is flagged (not at the same time)
CHECK (
(
bet_id IS NOT NULL
AND odd_id IS NULL
)
OR (
bet_id IS NULL
AND odd_id IS NOT NULL
)
)
);
-- Views -- Views
CREATE VIEW companies_details AS CREATE VIEW companies_details AS
SELECT companies.*, SELECT companies.*,

View File

@ -1,11 +1,22 @@
-- Settings Initial Data -- Settings Initial Data
INSERT INTO settings (key, value) INSERT INTO settings (key, value)
<<<<<<< HEAD
VALUES VALUES
('max_number_of_outcomes', '30'), ('max_number_of_outcomes', '30'),
=======
VALUES ('sms_provider', '30'),
('max_number_of_outcomes', '30'),
>>>>>>> d43b12c589d32e4b6147cfb54a3b939c476bae6f
('bet_amount_limit', '100000'), ('bet_amount_limit', '100000'),
('daily_ticket_limit', '50'), ('daily_ticket_limit', '50'),
('total_winnings_limit', '1000000'), ('total_winnings_limit', '1000000'),
('amount_for_bet_referral', '1000000'), ('amount_for_bet_referral', '1000000'),
<<<<<<< HEAD
('cashback_amount_cap', '1000') ('cashback_amount_cap', '1000')
ON CONFLICT (key) ON CONFLICT (key)
DO UPDATE SET value = EXCLUDED.value; DO UPDATE SET value = EXCLUDED.value;
=======
('cashback_amount_cap', '1000') ON CONFLICT (key) DO
UPDATE
SET value = EXCLUDED.value;
>>>>>>> d43b12c589d32e4b6147cfb54a3b939c476bae6f

View File

@ -45,8 +45,7 @@ VALUES ('addis_ababa', 'Addis Ababa'),
('meki', 'Meki'), ('meki', 'Meki'),
('negele_borana', 'Negele Borana'), ('negele_borana', 'Negele Borana'),
('alaba_kulito', 'Alaba Kulito'), ('alaba_kulito', 'Alaba Kulito'),
('alamata 14,', 'Alamata 14,'), ('alamata,', 'Alamata,'),
('030', '030'),
('chiro', 'Chiro'), ('chiro', 'Chiro'),
('tepi', 'Tepi'), ('tepi', 'Tepi'),
('durame', 'Durame'), ('durame', 'Durame'),

View File

@ -101,11 +101,19 @@ WHERE (event_id = $1)
SELECT * SELECT *
FROM bet_outcomes FROM bet_outcomes
WHERE bet_id = $1; WHERE bet_id = $1;
-- name: GetBetCount :one -- name: GetBetOutcomeCountByOddID :one
SELECT COUNT(*)
FROM bet_outcomes
WHERE odd_id = $1;
-- name: GetBetCountByUserID :one
SELECT COUNT(*) SELECT COUNT(*)
FROM bets FROM bets
WHERE user_id = $1 WHERE user_id = $1
AND outcomes_hash = $2; AND outcomes_hash = $2;
-- name: GetBetCountByOutcomesHash :one
SELECT COUNT(*)
FROM bets
WHERE outcomes_hash = $1;
-- name: UpdateCashOut :exec -- name: UpdateCashOut :exec
UPDATE bets UPDATE bets
SET cashed_out = $2, SET cashed_out = $2,

View File

@ -5,9 +5,10 @@ INSERT INTO branches (
wallet_id, wallet_id,
branch_manager_id, branch_manager_id,
company_id, company_id,
is_self_owned is_self_owned,
profit_percent
) )
VALUES ($1, $2, $3, $4, $5, $6) VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *; RETURNING *;
-- name: CreateSupportedOperation :one -- name: CreateSupportedOperation :one
INSERT INTO supported_operations (name, description) INSERT INTO supported_operations (name, description)
@ -88,6 +89,7 @@ SET name = COALESCE(sqlc.narg(name), name),
company_id = COALESCE(sqlc.narg(company_id), company_id), company_id = COALESCE(sqlc.narg(company_id), company_id),
is_self_owned = COALESCE(sqlc.narg(is_self_owned), is_self_owned), is_self_owned = COALESCE(sqlc.narg(is_self_owned), is_self_owned),
is_active = COALESCE(sqlc.narg(is_active), is_active), is_active = COALESCE(sqlc.narg(is_active), is_active),
profit_percent = COALESCE(sqlc.narg(profit_percent), profit_percent),
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $1 WHERE id = $1
RETURNING *; RETURNING *;

View File

@ -157,6 +157,11 @@ WHERE is_live = false
events.sport_id = sqlc.narg('sport_id') events.sport_id = sqlc.narg('sport_id')
OR sqlc.narg('sport_id') IS NULL OR sqlc.narg('sport_id') IS NULL
) )
AND (
match_name ILIKE '%' || sqlc.narg('query') || '%'
OR league_name ILIKE '%' || sqlc.narg('query') || '%'
OR sqlc.narg('query') IS NULL
)
AND ( AND (
start_time < sqlc.narg('last_start_time') start_time < sqlc.narg('last_start_time')
OR sqlc.narg('last_start_time') IS NULL OR sqlc.narg('last_start_time') IS NULL
@ -170,8 +175,8 @@ WHERE is_live = false
OR sqlc.narg('country_code') IS NULL OR sqlc.narg('country_code') IS NULL
) )
AND ( AND (
flagged = sqlc.narg('flagged') events.is_featured = sqlc.narg('is_featured')
OR sqlc.narg('flagged') IS NULL OR sqlc.narg('is_featured') IS NULL
); );
-- name: GetPaginatedUpcomingEvents :many -- name: GetPaginatedUpcomingEvents :many
SELECT events.*, SELECT events.*,
@ -189,6 +194,11 @@ WHERE start_time > now()
events.sport_id = sqlc.narg('sport_id') events.sport_id = sqlc.narg('sport_id')
OR sqlc.narg('sport_id') IS NULL OR sqlc.narg('sport_id') IS NULL
) )
AND (
match_name ILIKE '%' || sqlc.narg('query') || '%'
OR league_name ILIKE '%' || sqlc.narg('query') || '%'
OR sqlc.narg('query') IS NULL
)
AND ( AND (
start_time < sqlc.narg('last_start_time') start_time < sqlc.narg('last_start_time')
OR sqlc.narg('last_start_time') IS NULL OR sqlc.narg('last_start_time') IS NULL
@ -202,8 +212,8 @@ WHERE start_time > now()
OR sqlc.narg('country_code') IS NULL OR sqlc.narg('country_code') IS NULL
) )
AND ( AND (
flagged = sqlc.narg('flagged') events.is_featured = sqlc.narg('is_featured')
OR sqlc.narg('flagged') IS NULL OR sqlc.narg('is_featured') IS NULL
) )
ORDER BY start_time ASC ORDER BY start_time ASC
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
@ -219,9 +229,9 @@ UPDATE events
SET score = $1, SET score = $1,
status = $2 status = $2
WHERE id = $3; WHERE id = $3;
-- name: UpdateFlagged :exec -- name: UpdateFeatured :exec
UPDATE events UPDATE events
SET flagged = $1 SET is_featured = $1
WHERE id = $2; WHERE id = $2;
-- name: DeleteEvent :exec -- name: DeleteEvent :exec
DELETE FROM events DELETE FROM events

8
db/query/flags.sql Normal file
View File

@ -0,0 +1,8 @@
-- name: CreateFlag :one
INSERT INTO flags (
bet_id,
odd_id,
reason
) VALUES (
$1, $2, $3
) RETURNING *;

View File

@ -41,6 +41,8 @@ WHERE (
is_featured = sqlc.narg('is_featured') is_featured = sqlc.narg('is_featured')
OR sqlc.narg('is_featured') IS NULL OR sqlc.narg('is_featured') IS NULL
) )
ORDER BY is_featured DESC,
name ASC
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
-- name: GetFeaturedLeagues :many -- name: GetFeaturedLeagues :many
SELECT id, SELECT id,

7
db/query/location.sql Normal file
View File

@ -0,0 +1,7 @@
-- name: GetAllBranchLocations :many
SELECT *
FROM branch_locations
WHERE (
value ILIKE '%' || sqlc.narg('query') || '%'
OR sqlc.narg('query') IS NULL
);

View File

@ -10,26 +10,4 @@ INSERT INTO settings (key, value, updated_at)
VALUES ($1, $2, CURRENT_TIMESTAMP) ON CONFLICT (key) DO VALUES ($1, $2, CURRENT_TIMESTAMP) ON CONFLICT (key) DO
UPDATE UPDATE
SET value = EXCLUDED.value SET value = EXCLUDED.value
RETURNING *; RETURNING *;
-- name: SetInitialData :exec
INSERT INTO settings (key, value)
VALUES ('max_number_of_outcomes', '30') ON CONFLICT (key) DO
UPDATE
SET value = EXCLUDED.value;
INSERT INTO settings (key, value)
VALUES ('bet_amount_limit', '100000') ON CONFLICT (key) DO
UPDATE
SET value = EXCLUDED.value;
INSERT INTO settings (key, value)
VALUES ('daily_ticket_limit', '50') ON CONFLICT (key) DO
UPDATE
SET value = EXCLUDED.value;
INSERT INTO settings (key, value)
VALUES ('total_winnings_limit', '1000000') ON CONFLICT (key) DO
UPDATE
SET value = EXCLUDED.value;
INSERT INTO settings (key, value)
VALUES ('amount_for_bet_referral', '1000000') ON CONFLICT (key) DO
UPDATE
SET value = EXCLUDED.value;

View File

@ -192,4 +192,9 @@ SET password = $1,
WHERE ( WHERE (
email = $2 email = $2
OR phone_number = $3 OR phone_number = $3
); );
-- name: GetAdminByCompanyID :one
SELECT users.*
FROM companies
JOIN users ON companies.admin_id = users.id
where companies.id = $1;

View File

@ -3,9 +3,10 @@ INSERT INTO wallets (
is_withdraw, is_withdraw,
is_bettable, is_bettable,
is_transferable, is_transferable,
user_id user_id,
type
) )
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4, $5)
RETURNING *; RETURNING *;
-- name: CreateCustomerWallet :one -- name: CreateCustomerWallet :one
INSERT INTO customer_wallets ( INSERT INTO customer_wallets (

View File

@ -282,20 +282,33 @@ func (q *Queries) GetBetByUserID(ctx context.Context, userID int64) ([]BetWithOu
return items, nil return items, nil
} }
const GetBetCount = `-- name: GetBetCount :one const GetBetCountByOutcomesHash = `-- name: GetBetCountByOutcomesHash :one
SELECT COUNT(*)
FROM bets
WHERE outcomes_hash = $1
`
func (q *Queries) GetBetCountByOutcomesHash(ctx context.Context, outcomesHash string) (int64, error) {
row := q.db.QueryRow(ctx, GetBetCountByOutcomesHash, outcomesHash)
var count int64
err := row.Scan(&count)
return count, err
}
const GetBetCountByUserID = `-- name: GetBetCountByUserID :one
SELECT COUNT(*) SELECT COUNT(*)
FROM bets FROM bets
WHERE user_id = $1 WHERE user_id = $1
AND outcomes_hash = $2 AND outcomes_hash = $2
` `
type GetBetCountParams struct { type GetBetCountByUserIDParams struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
OutcomesHash string `json:"outcomes_hash"` OutcomesHash string `json:"outcomes_hash"`
} }
func (q *Queries) GetBetCount(ctx context.Context, arg GetBetCountParams) (int64, error) { func (q *Queries) GetBetCountByUserID(ctx context.Context, arg GetBetCountByUserIDParams) (int64, error) {
row := q.db.QueryRow(ctx, GetBetCount, arg.UserID, arg.OutcomesHash) row := q.db.QueryRow(ctx, GetBetCountByUserID, arg.UserID, arg.OutcomesHash)
var count int64 var count int64
err := row.Scan(&count) err := row.Scan(&count)
return count, err return count, err
@ -397,6 +410,19 @@ func (q *Queries) GetBetOutcomeByEventID(ctx context.Context, arg GetBetOutcomeB
return items, nil return items, nil
} }
const GetBetOutcomeCountByOddID = `-- name: GetBetOutcomeCountByOddID :one
SELECT COUNT(*)
FROM bet_outcomes
WHERE odd_id = $1
`
func (q *Queries) GetBetOutcomeCountByOddID(ctx context.Context, oddID int64) (int64, error) {
row := q.db.QueryRow(ctx, GetBetOutcomeCountByOddID, oddID)
var count int64
err := row.Scan(&count)
return count, err
}
const GetBetsForCashback = `-- name: GetBetsForCashback :many const GetBetsForCashback = `-- name: GetBetsForCashback :many
SELECT id, amount, total_odds, status, user_id, is_shop_bet, cashed_out, outcomes_hash, fast_code, processed, created_at, updated_at, full_name, phone_number, outcomes SELECT id, amount, total_odds, status, user_id, is_shop_bet, cashed_out, outcomes_hash, fast_code, processed, created_at, updated_at, full_name, phone_number, outcomes
FROM bet_with_outcomes FROM bet_with_outcomes

View File

@ -18,19 +18,21 @@ INSERT INTO branches (
wallet_id, wallet_id,
branch_manager_id, branch_manager_id,
company_id, company_id,
is_self_owned is_self_owned,
profit_percent
) )
VALUES ($1, $2, $3, $4, $5, $6) VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at RETURNING id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at
` `
type CreateBranchParams struct { type CreateBranchParams struct {
Name string `json:"name"` Name string `json:"name"`
Location string `json:"location"` Location string `json:"location"`
WalletID int64 `json:"wallet_id"` WalletID int64 `json:"wallet_id"`
BranchManagerID int64 `json:"branch_manager_id"` BranchManagerID int64 `json:"branch_manager_id"`
CompanyID int64 `json:"company_id"` CompanyID int64 `json:"company_id"`
IsSelfOwned bool `json:"is_self_owned"` IsSelfOwned bool `json:"is_self_owned"`
ProfitPercent float32 `json:"profit_percent"`
} }
func (q *Queries) CreateBranch(ctx context.Context, arg CreateBranchParams) (Branch, error) { func (q *Queries) CreateBranch(ctx context.Context, arg CreateBranchParams) (Branch, error) {
@ -41,6 +43,7 @@ func (q *Queries) CreateBranch(ctx context.Context, arg CreateBranchParams) (Bra
arg.BranchManagerID, arg.BranchManagerID,
arg.CompanyID, arg.CompanyID,
arg.IsSelfOwned, arg.IsSelfOwned,
arg.ProfitPercent,
) )
var i Branch var i Branch
err := row.Scan( err := row.Scan(
@ -498,19 +501,21 @@ SET name = COALESCE($2, name),
company_id = COALESCE($5, company_id), company_id = COALESCE($5, company_id),
is_self_owned = COALESCE($6, is_self_owned), is_self_owned = COALESCE($6, is_self_owned),
is_active = COALESCE($7, is_active), is_active = COALESCE($7, is_active),
profit_percent = COALESCE($8, profit_percent),
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $1 WHERE id = $1
RETURNING id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at RETURNING id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at
` `
type UpdateBranchParams struct { type UpdateBranchParams struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name pgtype.Text `json:"name"` Name pgtype.Text `json:"name"`
Location pgtype.Text `json:"location"` Location pgtype.Text `json:"location"`
BranchManagerID pgtype.Int8 `json:"branch_manager_id"` BranchManagerID pgtype.Int8 `json:"branch_manager_id"`
CompanyID pgtype.Int8 `json:"company_id"` CompanyID pgtype.Int8 `json:"company_id"`
IsSelfOwned pgtype.Bool `json:"is_self_owned"` IsSelfOwned pgtype.Bool `json:"is_self_owned"`
IsActive pgtype.Bool `json:"is_active"` IsActive pgtype.Bool `json:"is_active"`
ProfitPercent pgtype.Float4 `json:"profit_percent"`
} }
func (q *Queries) UpdateBranch(ctx context.Context, arg UpdateBranchParams) (Branch, error) { func (q *Queries) UpdateBranch(ctx context.Context, arg UpdateBranchParams) (Branch, error) {
@ -522,6 +527,7 @@ func (q *Queries) UpdateBranch(ctx context.Context, arg UpdateBranchParams) (Bra
arg.CompanyID, arg.CompanyID,
arg.IsSelfOwned, arg.IsSelfOwned,
arg.IsActive, arg.IsActive,
arg.ProfitPercent,
) )
var i Branch var i Branch
err := row.Scan( err := row.Scan(

View File

@ -22,7 +22,7 @@ func (q *Queries) DeleteEvent(ctx context.Context, id string) error {
} }
const GetAllUpcomingEvents = `-- name: GetAllUpcomingEvents :many const GetAllUpcomingEvents = `-- name: GetAllUpcomingEvents :many
SELECT id, sport_id, match_name, home_team, away_team, home_team_id, away_team_id, home_kit_image, away_kit_image, league_id, league_name, league_cc, start_time, score, match_minute, timer_status, added_time, match_period, is_live, status, fetched_at, source, flagged SELECT id, sport_id, match_name, home_team, away_team, home_team_id, away_team_id, home_kit_image, away_kit_image, league_id, league_name, league_cc, start_time, score, match_minute, timer_status, added_time, match_period, is_live, status, fetched_at, source, is_featured, is_active
FROM events FROM events
WHERE start_time > now() WHERE start_time > now()
AND is_live = false AND is_live = false
@ -62,7 +62,8 @@ func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]Event, error) {
&i.Status, &i.Status,
&i.FetchedAt, &i.FetchedAt,
&i.Source, &i.Source,
&i.Flagged, &i.IsFeatured,
&i.IsActive,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -75,7 +76,7 @@ func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]Event, error) {
} }
const GetExpiredUpcomingEvents = `-- name: GetExpiredUpcomingEvents :many const GetExpiredUpcomingEvents = `-- name: GetExpiredUpcomingEvents :many
SELECT events.id, events.sport_id, events.match_name, events.home_team, events.away_team, events.home_team_id, events.away_team_id, events.home_kit_image, events.away_kit_image, events.league_id, events.league_name, events.league_cc, events.start_time, events.score, events.match_minute, events.timer_status, events.added_time, events.match_period, events.is_live, events.status, events.fetched_at, events.source, events.flagged, SELECT events.id, events.sport_id, events.match_name, events.home_team, events.away_team, events.home_team_id, events.away_team_id, events.home_kit_image, events.away_kit_image, events.league_id, events.league_name, events.league_cc, events.start_time, events.score, events.match_minute, events.timer_status, events.added_time, events.match_period, events.is_live, events.status, events.fetched_at, events.source, events.is_featured, events.is_active,
leagues.country_code as league_cc leagues.country_code as league_cc
FROM events FROM events
LEFT JOIN leagues ON leagues.id = league_id LEFT JOIN leagues ON leagues.id = league_id
@ -110,7 +111,8 @@ type GetExpiredUpcomingEventsRow struct {
Status pgtype.Text `json:"status"` Status pgtype.Text `json:"status"`
FetchedAt pgtype.Timestamp `json:"fetched_at"` FetchedAt pgtype.Timestamp `json:"fetched_at"`
Source pgtype.Text `json:"source"` Source pgtype.Text `json:"source"`
Flagged bool `json:"flagged"` IsFeatured bool `json:"is_featured"`
IsActive bool `json:"is_active"`
LeagueCc_2 pgtype.Text `json:"league_cc_2"` LeagueCc_2 pgtype.Text `json:"league_cc_2"`
} }
@ -146,7 +148,8 @@ func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context, status pgtype.Te
&i.Status, &i.Status,
&i.FetchedAt, &i.FetchedAt,
&i.Source, &i.Source,
&i.Flagged, &i.IsFeatured,
&i.IsActive,
&i.LeagueCc_2, &i.LeagueCc_2,
); err != nil { ); err != nil {
return nil, err return nil, err
@ -160,7 +163,7 @@ func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context, status pgtype.Te
} }
const GetPaginatedUpcomingEvents = `-- name: GetPaginatedUpcomingEvents :many const GetPaginatedUpcomingEvents = `-- name: GetPaginatedUpcomingEvents :many
SELECT events.id, events.sport_id, events.match_name, events.home_team, events.away_team, events.home_team_id, events.away_team_id, events.home_kit_image, events.away_kit_image, events.league_id, events.league_name, events.league_cc, events.start_time, events.score, events.match_minute, events.timer_status, events.added_time, events.match_period, events.is_live, events.status, events.fetched_at, events.source, events.flagged, SELECT events.id, events.sport_id, events.match_name, events.home_team, events.away_team, events.home_team_id, events.away_team_id, events.home_kit_image, events.away_kit_image, events.league_id, events.league_name, events.league_cc, events.start_time, events.score, events.match_minute, events.timer_status, events.added_time, events.match_period, events.is_live, events.status, events.fetched_at, events.source, events.is_featured, events.is_active,
leagues.country_code as league_cc leagues.country_code as league_cc
FROM events FROM events
LEFT JOIN leagues ON leagues.id = league_id LEFT JOIN leagues ON leagues.id = league_id
@ -176,32 +179,38 @@ WHERE start_time > now()
OR $2 IS NULL OR $2 IS NULL
) )
AND ( AND (
start_time < $3 match_name ILIKE '%' || $3 || '%'
OR league_name ILIKE '%' || $3 || '%'
OR $3 IS NULL OR $3 IS NULL
) )
AND ( AND (
start_time > $4 start_time < $4
OR $4 IS NULL OR $4 IS NULL
) )
AND ( AND (
leagues.country_code = $5 start_time > $5
OR $5 IS NULL OR $5 IS NULL
) )
AND ( AND (
flagged = $6 leagues.country_code = $6
OR $6 IS NULL OR $6 IS NULL
) )
AND (
events.is_featured = $7
OR $7 IS NULL
)
ORDER BY start_time ASC ORDER BY start_time ASC
LIMIT $8 OFFSET $7 LIMIT $9 OFFSET $8
` `
type GetPaginatedUpcomingEventsParams struct { type GetPaginatedUpcomingEventsParams struct {
LeagueID pgtype.Int4 `json:"league_id"` LeagueID pgtype.Int4 `json:"league_id"`
SportID pgtype.Int4 `json:"sport_id"` SportID pgtype.Int4 `json:"sport_id"`
Query pgtype.Text `json:"query"`
LastStartTime pgtype.Timestamp `json:"last_start_time"` LastStartTime pgtype.Timestamp `json:"last_start_time"`
FirstStartTime pgtype.Timestamp `json:"first_start_time"` FirstStartTime pgtype.Timestamp `json:"first_start_time"`
CountryCode pgtype.Text `json:"country_code"` CountryCode pgtype.Text `json:"country_code"`
Flagged pgtype.Bool `json:"flagged"` 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"`
} }
@ -229,7 +238,8 @@ type GetPaginatedUpcomingEventsRow struct {
Status pgtype.Text `json:"status"` Status pgtype.Text `json:"status"`
FetchedAt pgtype.Timestamp `json:"fetched_at"` FetchedAt pgtype.Timestamp `json:"fetched_at"`
Source pgtype.Text `json:"source"` Source pgtype.Text `json:"source"`
Flagged bool `json:"flagged"` IsFeatured bool `json:"is_featured"`
IsActive bool `json:"is_active"`
LeagueCc_2 pgtype.Text `json:"league_cc_2"` LeagueCc_2 pgtype.Text `json:"league_cc_2"`
} }
@ -237,10 +247,11 @@ func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginat
rows, err := q.db.Query(ctx, GetPaginatedUpcomingEvents, rows, err := q.db.Query(ctx, GetPaginatedUpcomingEvents,
arg.LeagueID, arg.LeagueID,
arg.SportID, arg.SportID,
arg.Query,
arg.LastStartTime, arg.LastStartTime,
arg.FirstStartTime, arg.FirstStartTime,
arg.CountryCode, arg.CountryCode,
arg.Flagged, arg.IsFeatured,
arg.Offset, arg.Offset,
arg.Limit, arg.Limit,
) )
@ -274,7 +285,8 @@ func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginat
&i.Status, &i.Status,
&i.FetchedAt, &i.FetchedAt,
&i.Source, &i.Source,
&i.Flagged, &i.IsFeatured,
&i.IsActive,
&i.LeagueCc_2, &i.LeagueCc_2,
); err != nil { ); err != nil {
return nil, err return nil, err
@ -302,40 +314,47 @@ WHERE is_live = false
OR $2 IS NULL OR $2 IS NULL
) )
AND ( AND (
start_time < $3 match_name ILIKE '%' || $3 || '%'
OR league_name ILIKE '%' || $3 || '%'
OR $3 IS NULL OR $3 IS NULL
) )
AND ( AND (
start_time > $4 start_time < $4
OR $4 IS NULL OR $4 IS NULL
) )
AND ( AND (
leagues.country_code = $5 start_time > $5
OR $5 IS NULL OR $5 IS NULL
) )
AND ( AND (
flagged = $6 leagues.country_code = $6
OR $6 IS NULL OR $6 IS NULL
) )
AND (
events.is_featured = $7
OR $7 IS NULL
)
` `
type GetTotalEventsParams struct { type GetTotalEventsParams struct {
LeagueID pgtype.Int4 `json:"league_id"` LeagueID pgtype.Int4 `json:"league_id"`
SportID pgtype.Int4 `json:"sport_id"` SportID pgtype.Int4 `json:"sport_id"`
Query pgtype.Text `json:"query"`
LastStartTime pgtype.Timestamp `json:"last_start_time"` LastStartTime pgtype.Timestamp `json:"last_start_time"`
FirstStartTime pgtype.Timestamp `json:"first_start_time"` FirstStartTime pgtype.Timestamp `json:"first_start_time"`
CountryCode pgtype.Text `json:"country_code"` CountryCode pgtype.Text `json:"country_code"`
Flagged pgtype.Bool `json:"flagged"` IsFeatured pgtype.Bool `json:"is_featured"`
} }
func (q *Queries) GetTotalEvents(ctx context.Context, arg GetTotalEventsParams) (int64, error) { func (q *Queries) GetTotalEvents(ctx context.Context, arg GetTotalEventsParams) (int64, error) {
row := q.db.QueryRow(ctx, GetTotalEvents, row := q.db.QueryRow(ctx, GetTotalEvents,
arg.LeagueID, arg.LeagueID,
arg.SportID, arg.SportID,
arg.Query,
arg.LastStartTime, arg.LastStartTime,
arg.FirstStartTime, arg.FirstStartTime,
arg.CountryCode, arg.CountryCode,
arg.Flagged, arg.IsFeatured,
) )
var count int64 var count int64
err := row.Scan(&count) err := row.Scan(&count)
@ -343,7 +362,7 @@ func (q *Queries) GetTotalEvents(ctx context.Context, arg GetTotalEventsParams)
} }
const GetUpcomingByID = `-- name: GetUpcomingByID :one const GetUpcomingByID = `-- name: GetUpcomingByID :one
SELECT id, sport_id, match_name, home_team, away_team, home_team_id, away_team_id, home_kit_image, away_kit_image, league_id, league_name, league_cc, start_time, score, match_minute, timer_status, added_time, match_period, is_live, status, fetched_at, source, flagged SELECT id, sport_id, match_name, home_team, away_team, home_team_id, away_team_id, home_kit_image, away_kit_image, league_id, league_name, league_cc, start_time, score, match_minute, timer_status, added_time, match_period, is_live, status, fetched_at, source, is_featured, is_active
FROM events FROM events
WHERE id = $1 WHERE id = $1
AND is_live = false AND is_live = false
@ -377,7 +396,8 @@ func (q *Queries) GetUpcomingByID(ctx context.Context, id string) (Event, error)
&i.Status, &i.Status,
&i.FetchedAt, &i.FetchedAt,
&i.Source, &i.Source,
&i.Flagged, &i.IsFeatured,
&i.IsActive,
) )
return i, err return i, err
} }
@ -623,19 +643,19 @@ func (q *Queries) ListLiveEvents(ctx context.Context) ([]string, error) {
return items, nil return items, nil
} }
const UpdateFlagged = `-- name: UpdateFlagged :exec const UpdateFeatured = `-- name: UpdateFeatured :exec
UPDATE events UPDATE events
SET flagged = $1 SET is_featured = $1
WHERE id = $2 WHERE id = $2
` `
type UpdateFlaggedParams struct { type UpdateFeaturedParams struct {
Flagged bool `json:"flagged"` IsFeatured bool `json:"is_featured"`
ID string `json:"id"` ID string `json:"id"`
} }
func (q *Queries) UpdateFlagged(ctx context.Context, arg UpdateFlaggedParams) error { func (q *Queries) UpdateFeatured(ctx context.Context, arg UpdateFeaturedParams) error {
_, err := q.db.Exec(ctx, UpdateFlagged, arg.Flagged, arg.ID) _, err := q.db.Exec(ctx, UpdateFeatured, arg.IsFeatured, arg.ID)
return err return err
} }

42
gen/db/flags.sql.go Normal file
View File

@ -0,0 +1,42 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// source: flags.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const CreateFlag = `-- name: CreateFlag :one
INSERT INTO flags (
bet_id,
odd_id,
reason
) VALUES (
$1, $2, $3
) RETURNING id, bet_id, odd_id, reason, flagged_at, resolved
`
type CreateFlagParams struct {
BetID pgtype.Int8 `json:"bet_id"`
OddID pgtype.Int8 `json:"odd_id"`
Reason pgtype.Text `json:"reason"`
}
func (q *Queries) CreateFlag(ctx context.Context, arg CreateFlagParams) (Flag, error) {
row := q.db.QueryRow(ctx, CreateFlag, arg.BetID, arg.OddID, arg.Reason)
var i Flag
err := row.Scan(
&i.ID,
&i.BetID,
&i.OddID,
&i.Reason,
&i.FlaggedAt,
&i.Resolved,
)
return i, err
}

View File

@ -52,6 +52,8 @@ WHERE (
is_featured = $4 is_featured = $4
OR $4 IS NULL OR $4 IS NULL
) )
ORDER BY is_featured DESC,
name ASC
LIMIT $6 OFFSET $5 LIMIT $6 OFFSET $5
` `

41
gen/db/location.sql.go Normal file
View File

@ -0,0 +1,41 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// source: location.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const GetAllBranchLocations = `-- name: GetAllBranchLocations :many
SELECT key, value
FROM branch_locations
WHERE (
value ILIKE '%' || $1 || '%'
OR $1 IS NULL
)
`
func (q *Queries) GetAllBranchLocations(ctx context.Context, query pgtype.Text) ([]BranchLocation, error) {
rows, err := q.db.Query(ctx, GetAllBranchLocations, query)
if err != nil {
return nil, err
}
defer rows.Close()
var items []BranchLocation
for rows.Next() {
var i BranchLocation
if err := rows.Scan(&i.Key, &i.Value); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@ -257,7 +257,8 @@ type Event struct {
Status pgtype.Text `json:"status"` Status pgtype.Text `json:"status"`
FetchedAt pgtype.Timestamp `json:"fetched_at"` FetchedAt pgtype.Timestamp `json:"fetched_at"`
Source pgtype.Text `json:"source"` Source pgtype.Text `json:"source"`
Flagged bool `json:"flagged"` IsFeatured bool `json:"is_featured"`
IsActive bool `json:"is_active"`
} }
type ExchangeRate struct { type ExchangeRate struct {
@ -276,6 +277,15 @@ type FavoriteGame struct {
CreatedAt pgtype.Timestamp `json:"created_at"` CreatedAt pgtype.Timestamp `json:"created_at"`
} }
type Flag struct {
ID int64 `json:"id"`
BetID pgtype.Int8 `json:"bet_id"`
OddID pgtype.Int8 `json:"odd_id"`
Reason pgtype.Text `json:"reason"`
FlaggedAt pgtype.Timestamp `json:"flagged_at"`
Resolved pgtype.Bool `json:"resolved"`
}
type League struct { type League struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@ -664,6 +674,7 @@ type Wallet struct {
IsBettable bool `json:"is_bettable"` IsBettable bool `json:"is_bettable"`
IsTransferable bool `json:"is_transferable"` IsTransferable bool `json:"is_transferable"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Type string `json:"type"`
IsActive bool `json:"is_active"` IsActive bool `json:"is_active"`
CreatedAt pgtype.Timestamp `json:"created_at"` CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"`

View File

@ -81,15 +81,3 @@ func (q *Queries) SaveSetting(ctx context.Context, arg SaveSettingParams) (Setti
) )
return i, err return i, err
} }
const SetInitialData = `-- name: SetInitialData :exec
INSERT INTO settings (key, value)
VALUES ('max_number_of_outcomes', '30') ON CONFLICT (key) DO
UPDATE
SET value = EXCLUDED.value
`
func (q *Queries) SetInitialData(ctx context.Context) error {
_, err := q.db.Exec(ctx, SetInitialData)
return err
}

View File

@ -159,6 +159,37 @@ func (q *Queries) DeleteUser(ctx context.Context, id int64) error {
return err return err
} }
const GetAdminByCompanyID = `-- name: GetAdminByCompanyID :one
SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended, users.referral_code, users.referred_by
FROM companies
JOIN users ON companies.admin_id = users.id
where companies.id = $1
`
func (q *Queries) GetAdminByCompanyID(ctx context.Context, id int64) (User, error) {
row := q.db.QueryRow(ctx, GetAdminByCompanyID, id)
var i User
err := row.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.Role,
&i.Password,
&i.EmailVerified,
&i.PhoneVerified,
&i.CreatedAt,
&i.UpdatedAt,
&i.CompanyID,
&i.SuspendedAt,
&i.Suspended,
&i.ReferralCode,
&i.ReferredBy,
)
return i, err
}
const GetAllUsers = `-- name: GetAllUsers :many const GetAllUsers = `-- name: GetAllUsers :many
SELECT id, SELECT id,
first_name, first_name,

View File

@ -46,17 +46,19 @@ INSERT INTO wallets (
is_withdraw, is_withdraw,
is_bettable, is_bettable,
is_transferable, is_transferable,
user_id user_id,
type
) )
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4, $5)
RETURNING id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, currency, bonus_balance, cash_balance RETURNING id, balance, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at, currency, bonus_balance, cash_balance
` `
type CreateWalletParams struct { type CreateWalletParams struct {
IsWithdraw bool `json:"is_withdraw"` IsWithdraw bool `json:"is_withdraw"`
IsBettable bool `json:"is_bettable"` IsBettable bool `json:"is_bettable"`
IsTransferable bool `json:"is_transferable"` IsTransferable bool `json:"is_transferable"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Type string `json:"type"`
} }
func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (Wallet, error) { func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (Wallet, error) {
@ -65,6 +67,7 @@ func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (Wal
arg.IsBettable, arg.IsBettable,
arg.IsTransferable, arg.IsTransferable,
arg.UserID, arg.UserID,
arg.Type,
) )
var i Wallet var i Wallet
err := row.Scan( err := row.Scan(
@ -74,6 +77,7 @@ func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (Wal
&i.IsBettable, &i.IsBettable,
&i.IsTransferable, &i.IsTransferable,
&i.UserID, &i.UserID,
&i.Type,
&i.IsActive, &i.IsActive,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@ -184,7 +188,7 @@ func (q *Queries) GetAllCustomerWallet(ctx context.Context) ([]CustomerWalletDet
} }
const GetAllWallets = `-- name: GetAllWallets :many const GetAllWallets = `-- name: GetAllWallets :many
SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, currency, bonus_balance, cash_balance SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at, currency, bonus_balance, cash_balance
FROM wallets FROM wallets
` `
@ -204,6 +208,7 @@ func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) {
&i.IsBettable, &i.IsBettable,
&i.IsTransferable, &i.IsTransferable,
&i.UserID, &i.UserID,
&i.Type,
&i.IsActive, &i.IsActive,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@ -314,7 +319,7 @@ func (q *Queries) GetCustomerWallet(ctx context.Context, customerID int64) (Cust
} }
const GetWalletByID = `-- name: GetWalletByID :one const GetWalletByID = `-- name: GetWalletByID :one
SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, currency, bonus_balance, cash_balance SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at, currency, bonus_balance, cash_balance
FROM wallets FROM wallets
WHERE id = $1 WHERE id = $1
` `
@ -329,6 +334,7 @@ func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) {
&i.IsBettable, &i.IsBettable,
&i.IsTransferable, &i.IsTransferable,
&i.UserID, &i.UserID,
&i.Type,
&i.IsActive, &i.IsActive,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@ -340,7 +346,7 @@ func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) {
} }
const GetWalletByUserID = `-- name: GetWalletByUserID :many const GetWalletByUserID = `-- name: GetWalletByUserID :many
SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, currency, bonus_balance, cash_balance SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at, currency, bonus_balance, cash_balance
FROM wallets FROM wallets
WHERE user_id = $1 WHERE user_id = $1
` `
@ -361,6 +367,7 @@ func (q *Queries) GetWalletByUserID(ctx context.Context, userID int64) ([]Wallet
&i.IsBettable, &i.IsBettable,
&i.IsTransferable, &i.IsTransferable,
&i.UserID, &i.UserID,
&i.Type,
&i.IsActive, &i.IsActive,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,

View File

@ -61,6 +61,15 @@ type BetFilter struct {
CreatedAfter ValidTime CreatedAfter ValidTime
} }
type Flag struct {
ID int64
BetID int64
OddID int64
Reason string
FlaggedAt time.Time
Resolved bool
}
type GetBet struct { type GetBet struct {
ID int64 ID int64
Amount Currency Amount Currency
@ -93,16 +102,22 @@ type CreateBetOutcomeReq struct {
} }
type CreateBetReq struct { type CreateBetReq struct {
Outcomes []CreateBetOutcomeReq `json:"outcomes" validate:"required"` Outcomes []CreateBetOutcomeReq `json:"outcomes" validate:"required"`
Amount float32 `json:"amount" validate:"required,gt=0" example:"100.0"` Amount float32 `json:"amount" validate:"required,gt=0" example:"100.0"`
BranchID *int64 `json:"branch_id,omitempty" validate:"required" example:"1"` BranchID *int64 `json:"branch_id,omitempty" example:"1"`
} }
type CreateBetWithFastCodeReq struct { type CreateBetWithFastCodeReq struct {
FastCode string `json:"fast_code"` FastCode string `json:"fast_code"`
Amount float32 `json:"amount"` Amount float32 `json:"amount"`
BranchID *int64 `json:"branch_id"` BranchID *int64 `json:"branch_id"`
} }
type CreateFlagReq struct {
BetID int64
OddID int64
Reason string
}
type RandomBetReq struct { type RandomBetReq struct {
BranchID int64 `json:"branch_id" validate:"required" example:"1"` BranchID int64 `json:"branch_id" validate:"required" example:"1"`
@ -117,6 +132,7 @@ type CreateBetRes struct {
UserID int64 `json:"user_id" example:"2"` UserID int64 `json:"user_id" example:"2"`
IsShopBet bool `json:"is_shop_bet" example:"false"` IsShopBet bool `json:"is_shop_bet" example:"false"`
CreatedNumber int64 `json:"created_number" example:"2"` CreatedNumber int64 `json:"created_number" example:"2"`
FastCode string `json:"fast_code"`
} }
type BetRes struct { type BetRes struct {
ID int64 `json:"id" example:"1"` ID int64 `json:"id" example:"1"`
@ -140,6 +156,8 @@ func ConvertCreateBet(bet Bet, createdNumber int64) CreateBetRes {
Status: bet.Status, Status: bet.Status,
UserID: bet.UserID, UserID: bet.UserID,
CreatedNumber: createdNumber, CreatedNumber: createdNumber,
IsShopBet: bet.IsShopBet,
FastCode: bet.FastCode,
} }
} }

View File

@ -1,14 +1,25 @@
package domain package domain
import (
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/jackc/pgx/v5/pgtype"
)
type Branch struct { type Branch struct {
ID int64 ID int64
Name string Name string
Location string Location string
WalletID int64 WalletID int64
BranchManagerID int64 BranchManagerID int64
CompanyID int64 CompanyID int64
IsActive bool IsActive bool
IsSelfOwned bool IsSelfOwned bool
ProfitPercentage float32
}
type BranchLocation struct {
Key string `json:"key" example:"addis_ababa" `
Name string `json:"name" example:"Addis Ababa"`
} }
type BranchFilter struct { type BranchFilter struct {
@ -33,6 +44,7 @@ type BranchDetail struct {
ManagerName string ManagerName string
ManagerPhoneNumber string ManagerPhoneNumber string
WalletIsActive bool WalletIsActive bool
ProfitPercentage float32
} }
type SupportedOperation struct { type SupportedOperation struct {
@ -48,22 +60,24 @@ type BranchOperation struct {
} }
type CreateBranch struct { type CreateBranch struct {
Name string Name string
Location string Location string
WalletID int64 WalletID int64
BranchManagerID int64 BranchManagerID int64
CompanyID int64 CompanyID int64
IsSelfOwned bool IsSelfOwned bool
ProfitPercentage float32
} }
type UpdateBranch struct { type UpdateBranch struct {
ID int64 ID int64
Name *string Name *string
Location *string Location *string
BranchManagerID *int64 BranchManagerID *int64
CompanyID *int64 CompanyID *int64
IsSelfOwned *bool IsSelfOwned *bool
IsActive *bool IsActive *bool
ProfitPercentage *float32
} }
type CreateSupportedOperation struct { type CreateSupportedOperation struct {
@ -76,21 +90,23 @@ type CreateBranchOperation struct {
} }
type CreateBranchReq struct { type CreateBranchReq struct {
Name string `json:"name" validate:"required,min=3,max=100" example:"4-kilo Branch"` Name string `json:"name" validate:"required,min=3,max=100" example:"4-kilo Branch"`
Location string `json:"location" validate:"required,min=3,max=100" example:"Addis Ababa"` Location string `json:"location" validate:"required,min=3,max=100" example:"Addis Ababa"`
BranchManagerID int64 `json:"branch_manager_id" validate:"required,gt=0" example:"1"` BranchManagerID int64 `json:"branch_manager_id" validate:"required,gt=0" example:"1"`
CompanyID *int64 `json:"company_id,omitempty" example:"1"` ProfitPercentage float32 `json:"profit_percentage" example:"0.1" validate:"lt=1" `
IsSelfOwned *bool `json:"is_self_owned,omitempty" example:"false"` CompanyID *int64 `json:"company_id,omitempty" example:"1"`
Operations []int64 `json:"operations" validate:"required,dive,gt=0"` IsSelfOwned *bool `json:"is_self_owned,omitempty" example:"false"`
Operations []int64 `json:"operations" validate:"required,dive,gt=0"`
} }
type UpdateBranchReq struct { type UpdateBranchReq struct {
Name *string `json:"name,omitempty" example:"4-kilo Branch"` Name *string `json:"name,omitempty" example:"4-kilo Branch"`
Location *string `json:"location,omitempty" example:"Addis Ababa"` Location *string `json:"location,omitempty" example:"Addis Ababa"`
BranchManagerID *int64 `json:"branch_manager_id,omitempty" example:"1"` BranchManagerID *int64 `json:"branch_manager_id,omitempty" example:"1"`
CompanyID *int64 `json:"company_id,omitempty" example:"1"` CompanyID *int64 `json:"company_id,omitempty" example:"1"`
IsSelfOwned *bool `json:"is_self_owned,omitempty" example:"false"` IsSelfOwned *bool `json:"is_self_owned,omitempty" example:"false"`
IsActive *bool `json:"is_active,omitempty" example:"false"` IsActive *bool `json:"is_active,omitempty" example:"false"`
ProfitPercentage *float32 `json:"profit_percentage,omitempty" example:"0.1" validate:"lt=1" `
} }
type CreateSupportedOperationReq struct { type CreateSupportedOperationReq struct {
@ -115,14 +131,15 @@ type BranchOperationRes struct {
} }
type BranchRes struct { type BranchRes struct {
ID int64 `json:"id" example:"1"` ID int64 `json:"id" example:"1"`
Name string `json:"name" example:"4-kilo Branch"` Name string `json:"name" example:"4-kilo Branch"`
Location string `json:"location" example:"Addis Ababa"` Location string `json:"location" example:"Addis Ababa"`
WalletID int64 `json:"wallet_id" example:"1"` WalletID int64 `json:"wallet_id" example:"1"`
BranchManagerID int64 `json:"branch_manager_id" example:"1"` BranchManagerID int64 `json:"branch_manager_id" example:"1"`
CompanyID int64 `json:"company_id" example:"1"` CompanyID int64 `json:"company_id" example:"1"`
IsSelfOwned bool `json:"is_self_owned" example:"false"` IsSelfOwned bool `json:"is_self_owned" example:"false"`
IsActive bool `json:"is_active" example:"false"` IsActive bool `json:"is_active" example:"false"`
ProfitPercentage float32 `json:"profit_percentage" example:"0.1"`
} }
type BranchDetailRes struct { type BranchDetailRes struct {
@ -138,18 +155,20 @@ type BranchDetailRes struct {
Balance float32 `json:"balance" example:"100.5"` Balance float32 `json:"balance" example:"100.5"`
IsActive bool `json:"is_active" example:"false"` IsActive bool `json:"is_active" example:"false"`
WalletIsActive bool `json:"is_wallet_active" example:"false"` WalletIsActive bool `json:"is_wallet_active" example:"false"`
ProfitPercentage float32 `json:"profit_percentage" example:"0.1"`
} }
func ConvertBranch(branch Branch) BranchRes { func ConvertBranch(branch Branch) BranchRes {
return BranchRes{ return BranchRes{
ID: branch.ID, ID: branch.ID,
Name: branch.Name, Name: branch.Name,
Location: branch.Location, Location: branch.Location,
WalletID: branch.WalletID, WalletID: branch.WalletID,
BranchManagerID: branch.BranchManagerID, BranchManagerID: branch.BranchManagerID,
CompanyID: branch.CompanyID, CompanyID: branch.CompanyID,
IsSelfOwned: branch.IsSelfOwned, IsSelfOwned: branch.IsSelfOwned,
IsActive: branch.IsActive, IsActive: branch.IsActive,
ProfitPercentage: branch.ProfitPercentage,
} }
} }
@ -167,5 +186,103 @@ func ConvertBranchDetail(branch BranchDetail) BranchDetailRes {
Balance: branch.Balance.Float32(), Balance: branch.Balance.Float32(),
IsActive: branch.IsActive, IsActive: branch.IsActive,
WalletIsActive: branch.WalletIsActive, WalletIsActive: branch.WalletIsActive,
ProfitPercentage: branch.ProfitPercentage,
} }
} }
func ConvertCreateBranch(branch CreateBranch) dbgen.CreateBranchParams {
return dbgen.CreateBranchParams{
Name: branch.Name,
Location: branch.Location,
WalletID: branch.WalletID,
BranchManagerID: branch.BranchManagerID,
CompanyID: branch.CompanyID,
IsSelfOwned: branch.IsSelfOwned,
ProfitPercent: branch.ProfitPercentage,
}
}
func ConvertDBBranchDetail(dbBranch dbgen.BranchDetail) BranchDetail {
return BranchDetail{
ID: dbBranch.ID,
Name: dbBranch.Name,
Location: dbBranch.Location,
WalletID: dbBranch.WalletID,
BranchManagerID: dbBranch.BranchManagerID,
CompanyID: dbBranch.CompanyID,
IsSelfOwned: dbBranch.IsSelfOwned,
ManagerName: dbBranch.ManagerName.(string),
ManagerPhoneNumber: dbBranch.ManagerPhoneNumber.String,
Balance: Currency(dbBranch.Balance.Int64),
IsActive: dbBranch.IsActive,
WalletIsActive: dbBranch.WalletIsActive.Bool,
ProfitPercentage: dbBranch.ProfitPercent,
}
}
func ConvertDBBranch(dbBranch dbgen.Branch) Branch {
return Branch{
ID: dbBranch.ID,
Name: dbBranch.Name,
Location: dbBranch.Location,
WalletID: dbBranch.WalletID,
BranchManagerID: dbBranch.BranchManagerID,
CompanyID: dbBranch.CompanyID,
IsSelfOwned: dbBranch.IsSelfOwned,
IsActive: dbBranch.IsActive,
ProfitPercentage: dbBranch.ProfitPercent,
}
}
func ConvertUpdateBranch(updateBranch UpdateBranch) dbgen.UpdateBranchParams {
var newUpdateBranch dbgen.UpdateBranchParams
newUpdateBranch.ID = updateBranch.ID
if updateBranch.Name != nil {
newUpdateBranch.Name = pgtype.Text{
String: *updateBranch.Name,
Valid: true,
}
}
if updateBranch.Location != nil {
newUpdateBranch.Location = pgtype.Text{
String: *updateBranch.Location,
Valid: true,
}
}
if updateBranch.BranchManagerID != nil {
newUpdateBranch.BranchManagerID = pgtype.Int8{
Int64: *updateBranch.BranchManagerID,
Valid: true,
}
}
if updateBranch.CompanyID != nil {
newUpdateBranch.CompanyID = pgtype.Int8{
Int64: *updateBranch.CompanyID,
Valid: true,
}
}
if updateBranch.IsSelfOwned != nil {
newUpdateBranch.IsSelfOwned = pgtype.Bool{
Bool: *updateBranch.IsSelfOwned,
Valid: true,
}
}
if updateBranch.IsActive != nil {
newUpdateBranch.IsActive = pgtype.Bool{
Bool: *updateBranch.IsActive,
Valid: true,
}
}
if updateBranch.ProfitPercentage != nil {
newUpdateBranch.ProfitPercent = pgtype.Float4{
Float32: *updateBranch.ProfitPercentage,
Valid: true,
}
}
return newUpdateBranch
}

View File

@ -54,8 +54,9 @@ type UpdateCompany struct {
} }
type CreateCompanyReq struct { type CreateCompanyReq struct {
Name string `json:"name" example:"CompanyName"` Name string `json:"name" example:"CompanyName"`
AdminID int64 `json:"admin_id" example:"1"` AdminID int64 `json:"admin_id" example:"1"`
DeductedPercentage float32 `json:"deducted_percentage" example:"0.1" validate:"lt=1"`
} }
type UpdateCompanyReq struct { type UpdateCompanyReq struct {
Name *string `json:"name,omitempty" example:"CompanyName"` Name *string `json:"name,omitempty" example:"CompanyName"`
@ -111,6 +112,7 @@ func ConvertGetCompany(company GetCompany) GetCompanyRes {
AdminFirstName: company.AdminFirstName, AdminFirstName: company.AdminFirstName,
AdminLastName: company.AdminLastName, AdminLastName: company.AdminLastName,
AdminPhoneNumber: company.AdminPhoneNumber, AdminPhoneNumber: company.AdminPhoneNumber,
} }
} }

View File

@ -101,7 +101,8 @@ type UpcomingEvent struct {
StartTime time.Time `json:"start_time"` // Converted from "time" field in UNIX format StartTime time.Time `json:"start_time"` // Converted from "time" field in UNIX format
Source string `json:"source"` // bet api provider (bet365, betfair) Source string `json:"source"` // bet api provider (bet365, betfair)
Status EventStatus `json:"status"` //Match Status for event Status EventStatus `json:"status"` //Match Status for event
Flagged bool `json:"flagged"` //Whether the event is flagged or not IsFeatured bool `json:"is_featured"` //Whether the event is featured or not
IsActive bool `json:"is_active"` //Whether the event is featured or not
} }
type MatchResult struct { type MatchResult struct {
EventID string EventID string
@ -120,6 +121,7 @@ type Odds struct {
} }
type EventFilter struct { type EventFilter struct {
Query ValidString
SportID ValidInt32 SportID ValidInt32
LeagueID ValidInt32 LeagueID ValidInt32
CountryCode ValidString CountryCode ValidString
@ -128,5 +130,5 @@ type EventFilter struct {
Limit ValidInt64 Limit ValidInt64
Offset ValidInt64 Offset ValidInt64
MatchStatus ValidString // e.g., "upcoming", "in_play", "ended" MatchStatus ValidString // e.g., "upcoming", "in_play", "ended"
Flagged ValidBool Featured ValidBool
} }

View File

@ -12,7 +12,7 @@ var (
ISSUE_TYPE_ODDS ReportedIssueType = "odds" ISSUE_TYPE_ODDS ReportedIssueType = "odds"
ISSUE_TYPE_EVENTS ReportedIssueType = "events" ISSUE_TYPE_EVENTS ReportedIssueType = "events"
ISSUE_TYPE_BRANCH ReportedIssueType = "branch" ISSUE_TYPE_BRANCH ReportedIssueType = "branch"
ISSUE_TYPE_USER ReportedIssueType = "branch" ISSUE_TYPE_USER ReportedIssueType = "user"
ISSUE_TYPE_LOGIN ReportedIssueType = "login" ISSUE_TYPE_LOGIN ReportedIssueType = "login"
ISSUE_TYPE_REGISTER ReportedIssueType = "register" ISSUE_TYPE_REGISTER ReportedIssueType = "register"
ISSUE_TYPE_RESET_PASSWORD ReportedIssueType = "reset_password" ISSUE_TYPE_RESET_PASSWORD ReportedIssueType = "reset_password"

View File

@ -26,12 +26,7 @@ const (
OtpMediumSms OtpMedium = "sms" OtpMediumSms OtpMedium = "sms"
) )
type OtpProvider string
const (
TwilioSms OtpProvider = "twilio"
AfroMessage OtpProvider = "aformessage"
)
type Otp struct { type Otp struct {
ID int64 ID int64

View File

@ -17,15 +17,17 @@ type SettingRes struct {
} }
type SettingList struct { type SettingList struct {
MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"` SMSProvider SMSProvider `json:"sms_provider"`
BetAmountLimit Currency `json:"bet_amount_limit"` MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"`
DailyTicketPerIP int64 `json:"daily_ticket_limit"` BetAmountLimit Currency `json:"bet_amount_limit"`
TotalWinningLimit Currency `json:"total_winning_limit"` DailyTicketPerIP int64 `json:"daily_ticket_limit"`
AmountForBetReferral Currency `json:"amount_for_bet_referral"` TotalWinningLimit Currency `json:"total_winning_limit"`
CashbackAmountCap Currency `json:"cashback_amount_cap"` AmountForBetReferral Currency `json:"amount_for_bet_referral"`
CashbackAmountCap Currency `json:"cashback_amount_cap"`
} }
type DBSettingList struct { type DBSettingList struct {
SMSProvider ValidString
MaxNumberOfOutcomes ValidInt64 MaxNumberOfOutcomes ValidInt64
BetAmountLimit ValidInt64 BetAmountLimit ValidInt64
DailyTicketPerIP ValidInt64 DailyTicketPerIP ValidInt64
@ -45,8 +47,27 @@ func ConvertInt64SettingsMap(dbSettingList *DBSettingList) map[string]*ValidInt6
} }
} }
func ConvertStringSettingsMap(dbSettingList *DBSettingList) map[string]*ValidString {
return map[string]*ValidString{
"sms_provider": &dbSettingList.SMSProvider,
}
}
func ConvertBoolSettingsMap(dbSettingList *DBSettingList) map[string]*ValidBool {
return map[string]*ValidBool{}
}
func ConvertFloat32SettingsMap(dbSettingList *DBSettingList) map[string]*ValidFloat32 {
return map[string]*ValidFloat32{}
}
func ConvertTimeSettingsMap(dbSettingList *DBSettingList) map[string]*ValidTime {
return map[string]*ValidTime{}
}
func ConvertDBSetting(dbSettingList DBSettingList) SettingList { func ConvertDBSetting(dbSettingList DBSettingList) SettingList {
return SettingList{ return SettingList{
SMSProvider: SMSProvider(dbSettingList.SMSProvider.Value),
MaxNumberOfOutcomes: dbSettingList.MaxNumberOfOutcomes.Value, MaxNumberOfOutcomes: dbSettingList.MaxNumberOfOutcomes.Value,
BetAmountLimit: Currency(dbSettingList.BetAmountLimit.Value), BetAmountLimit: Currency(dbSettingList.BetAmountLimit.Value),
DailyTicketPerIP: dbSettingList.DailyTicketPerIP.Value, DailyTicketPerIP: dbSettingList.DailyTicketPerIP.Value,

18
internal/domain/sms.go Normal file
View File

@ -0,0 +1,18 @@
package domain
type SMSProvider string
const (
TwilioSms SMSProvider = "twilio"
AfroMessage SMSProvider = "afro_message"
)
// IsValid checks if the SMSProvider is a valid enum value
func (s SMSProvider) IsValid() bool {
switch s {
case TwilioSms, AfroMessage:
return true
default:
return false
}
}

View File

@ -11,6 +11,7 @@ type Wallet struct {
IsTransferable bool IsTransferable bool
IsActive bool IsActive bool
UserID int64 UserID int64
Type WalletType
UpdatedAt time.Time UpdatedAt time.Time
CreatedAt time.Time CreatedAt time.Time
} }
@ -63,6 +64,7 @@ type CreateWallet struct {
IsBettable bool IsBettable bool
IsTransferable bool IsTransferable bool
UserID int64 UserID int64
Type WalletType
} }
type CreateCustomerWallet struct { type CreateCustomerWallet struct {

View File

@ -76,6 +76,17 @@ func convertDBBetWithOutcomes(bet dbgen.BetWithOutcome) domain.GetBet {
} }
} }
func convertDBFlag(flag dbgen.Flag) domain.Flag {
return domain.Flag{
ID: flag.ID,
BetID: flag.BetID.Int64,
OddID: flag.OddID.Int64,
Reason: flag.Reason.String,
FlaggedAt: flag.FlaggedAt.Time,
Resolved: flag.Resolved.Bool,
}
}
func convertDBCreateBetOutcome(betOutcome domain.CreateBetOutcome) dbgen.CreateBetOutcomeParams { func convertDBCreateBetOutcome(betOutcome domain.CreateBetOutcome) dbgen.CreateBetOutcomeParams {
return dbgen.CreateBetOutcomeParams{ return dbgen.CreateBetOutcomeParams{
BetID: betOutcome.BetID, BetID: betOutcome.BetID,
@ -140,6 +151,35 @@ func (s *Store) CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBe
return rows, nil return rows, nil
} }
func (s *Store) CreateFlag(ctx context.Context, flag domain.CreateFlagReq) (domain.Flag, error) {
createFlag := dbgen.CreateFlagParams{
BetID: pgtype.Int8{
Int64: flag.BetID,
Valid: flag.BetID != 0,
},
OddID: pgtype.Int8{
Int64: flag.OddID,
Valid: flag.OddID != 0,
},
Reason: pgtype.Text{
String: flag.Reason,
Valid: true,
},
}
f, err := s.queries.CreateFlag(ctx, createFlag)
if err != nil {
domain.MongoDBLogger.Error("failed to create flag",
zap.String("flag", f.Reason.String),
zap.Any("flag_id", f.ID),
zap.Error(err),
)
return domain.Flag{}, err
}
return convertDBFlag(f), nil
}
func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) { func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) {
bet, err := s.queries.GetBetByID(ctx, id) bet, err := s.queries.GetBetByID(ctx, id)
if err != nil { if err != nil {
@ -237,8 +277,8 @@ func (s *Store) GetBetsForCashback(ctx context.Context) ([]domain.GetBet, error)
return res, nil return res, nil
} }
func (s *Store) GetBetCount(ctx context.Context, UserID int64, outcomesHash string) (int64, error) { func (s *Store) GetBetCountByUserID(ctx context.Context, UserID int64, outcomesHash string) (int64, error) {
count, err := s.queries.GetBetCount(ctx, dbgen.GetBetCountParams{ count, err := s.queries.GetBetCountByUserID(ctx, dbgen.GetBetCountByUserIDParams{
UserID: UserID, UserID: UserID,
OutcomesHash: outcomesHash, OutcomesHash: outcomesHash,
}) })
@ -250,6 +290,24 @@ func (s *Store) GetBetCount(ctx context.Context, UserID int64, outcomesHash stri
return count, nil return count, nil
} }
func (s *Store) GetBetCountByOutcomesHash(ctx context.Context, outcomesHash string) (int64, error) {
count, err := s.queries.GetBetCountByOutcomesHash(ctx, outcomesHash)
if err != nil {
return 0, err
}
return count, nil
}
func (s *Store) GetBetOutcomeCountByOddID(ctx context.Context, oddID int64) (int64, error) {
count, err := s.queries.GetBetOutcomeCountByOddID(ctx, oddID)
if err != nil {
return 0, err
}
return count, nil
}
func (s *Store) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error { func (s *Store) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error {
err := s.queries.UpdateCashOut(ctx, dbgen.UpdateCashOutParams{ err := s.queries.UpdateCashOut(ctx, dbgen.UpdateCashOutParams{
ID: id, ID: id,
@ -486,16 +544,16 @@ func (s *Store) GetBetSummary(ctx context.Context, filter domain.ReportFilter) (
return 0, 0, 0, 0, 0, 0, fmt.Errorf("failed to get bet summary: %w", err) return 0, 0, 0, 0, 0, 0, fmt.Errorf("failed to get bet summary: %w", err)
} }
domain.MongoDBLogger.Info("GetBetSummary executed successfully", // domain.MongoDBLogger.Info("GetBetSummary executed successfully",
zap.String("query", query), // zap.String("query", query),
zap.Any("args", args), // zap.Any("args", args),
zap.Float64("totalStakes", float64(totalStakes)), // convert if needed // zap.Float64("totalStakes", float64(totalStakes)), // convert if needed
zap.Int64("totalBets", totalBets), // zap.Int64("totalBets", totalBets),
zap.Int64("activeBets", activeBets), // zap.Int64("activeBets", activeBets),
zap.Int64("totalWins", totalWins), // zap.Int64("totalWins", totalWins),
zap.Int64("totalLosses", totalLosses), // zap.Int64("totalLosses", totalLosses),
zap.Float64("winBalance", float64(winBalance)), // convert if needed // zap.Float64("winBalance", float64(winBalance)), // convert if needed
) // )
return totalStakes, totalBets, activeBets, totalWins, totalLosses, winBalance, nil return totalStakes, totalBets, activeBets, totalWins, totalLosses, winBalance, nil
} }

View File

@ -9,100 +9,15 @@ import (
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
func convertCreateBranch(branch domain.CreateBranch) dbgen.CreateBranchParams {
return dbgen.CreateBranchParams{
Name: branch.Name,
Location: branch.Location,
WalletID: branch.WalletID,
BranchManagerID: branch.BranchManagerID,
CompanyID: branch.CompanyID,
IsSelfOwned: branch.IsSelfOwned,
}
}
func convertDBBranchDetail(dbBranch dbgen.BranchDetail) domain.BranchDetail {
return domain.BranchDetail{
ID: dbBranch.ID,
Name: dbBranch.Name,
Location: dbBranch.Location,
WalletID: dbBranch.WalletID,
BranchManagerID: dbBranch.BranchManagerID,
CompanyID: dbBranch.CompanyID,
IsSelfOwned: dbBranch.IsSelfOwned,
ManagerName: dbBranch.ManagerName.(string),
ManagerPhoneNumber: dbBranch.ManagerPhoneNumber.String,
Balance: domain.Currency(dbBranch.Balance.Int64),
IsActive: dbBranch.IsActive,
WalletIsActive: dbBranch.WalletIsActive.Bool,
}
}
func convertDBBranch(dbBranch dbgen.Branch) domain.Branch {
return domain.Branch{
ID: dbBranch.ID,
Name: dbBranch.Name,
Location: dbBranch.Location,
WalletID: dbBranch.WalletID,
BranchManagerID: dbBranch.BranchManagerID,
CompanyID: dbBranch.CompanyID,
IsSelfOwned: dbBranch.IsSelfOwned,
}
}
func convertUpdateBranch(updateBranch domain.UpdateBranch) dbgen.UpdateBranchParams {
var newUpdateBranch dbgen.UpdateBranchParams
newUpdateBranch.ID = updateBranch.ID
if updateBranch.Name != nil {
newUpdateBranch.Name = pgtype.Text{
String: *updateBranch.Name,
Valid: true,
}
}
if updateBranch.Location != nil {
newUpdateBranch.Location = pgtype.Text{
String: *updateBranch.Location,
Valid: true,
}
}
if updateBranch.BranchManagerID != nil {
newUpdateBranch.BranchManagerID = pgtype.Int8{
Int64: *updateBranch.BranchManagerID,
Valid: true,
}
}
if updateBranch.CompanyID != nil {
newUpdateBranch.CompanyID = pgtype.Int8{
Int64: *updateBranch.CompanyID,
Valid: true,
}
}
if updateBranch.IsSelfOwned != nil {
newUpdateBranch.IsSelfOwned = pgtype.Bool{
Bool: *updateBranch.IsSelfOwned,
Valid: true,
}
}
if updateBranch.IsActive != nil {
newUpdateBranch.IsActive = pgtype.Bool{
Bool: *updateBranch.IsActive,
Valid: true,
}
}
return newUpdateBranch
}
func (s *Store) CreateBranch(ctx context.Context, branch domain.CreateBranch) (domain.Branch, error) { func (s *Store) CreateBranch(ctx context.Context, branch domain.CreateBranch) (domain.Branch, error) {
dbBranch, err := s.queries.CreateBranch(ctx, convertCreateBranch(branch)) dbBranch, err := s.queries.CreateBranch(ctx, domain.ConvertCreateBranch(branch))
if err != nil { if err != nil {
return domain.Branch{}, err return domain.Branch{}, err
} }
return convertDBBranch(dbBranch), nil return domain.ConvertDBBranch(dbBranch), nil
} }
func (s *Store) GetBranchByID(ctx context.Context, id int64) (domain.BranchDetail, error) { func (s *Store) GetBranchByID(ctx context.Context, id int64) (domain.BranchDetail, error) {
@ -110,7 +25,7 @@ func (s *Store) GetBranchByID(ctx context.Context, id int64) (domain.BranchDetai
if err != nil { if err != nil {
return domain.BranchDetail{}, err return domain.BranchDetail{}, err
} }
return convertDBBranchDetail(dbBranch), nil return domain.ConvertDBBranchDetail(dbBranch), nil
} }
func (s *Store) GetBranchByManagerID(ctx context.Context, branchManagerID int64) ([]domain.BranchDetail, error) { func (s *Store) GetBranchByManagerID(ctx context.Context, branchManagerID int64) ([]domain.BranchDetail, error) {
@ -120,7 +35,7 @@ func (s *Store) GetBranchByManagerID(ctx context.Context, branchManagerID int64)
} }
var branches []domain.BranchDetail = make([]domain.BranchDetail, 0, len(dbBranches)) var branches []domain.BranchDetail = make([]domain.BranchDetail, 0, len(dbBranches))
for _, dbBranch := range dbBranches { for _, dbBranch := range dbBranches {
branches = append(branches, convertDBBranchDetail(dbBranch)) branches = append(branches, domain.ConvertDBBranchDetail(dbBranch))
} }
return branches, nil return branches, nil
} }
@ -131,7 +46,7 @@ func (s *Store) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]do
} }
var branches []domain.BranchDetail = make([]domain.BranchDetail, 0, len(dbBranches)) var branches []domain.BranchDetail = make([]domain.BranchDetail, 0, len(dbBranches))
for _, dbBranch := range dbBranches { for _, dbBranch := range dbBranches {
branches = append(branches, convertDBBranchDetail(dbBranch)) branches = append(branches, domain.ConvertDBBranchDetail(dbBranch))
} }
return branches, nil return branches, nil
} }
@ -164,7 +79,7 @@ func (s *Store) GetAllBranches(ctx context.Context, filter domain.BranchFilter)
} }
var branches []domain.BranchDetail = make([]domain.BranchDetail, 0, len(dbBranches)) var branches []domain.BranchDetail = make([]domain.BranchDetail, 0, len(dbBranches))
for _, dbBranch := range dbBranches { for _, dbBranch := range dbBranches {
branches = append(branches, convertDBBranchDetail(dbBranch)) branches = append(branches, domain.ConvertDBBranchDetail(dbBranch))
} }
return branches, nil return branches, nil
} }
@ -177,18 +92,18 @@ func (s *Store) SearchBranchByName(ctx context.Context, name string) ([]domain.B
var branches []domain.BranchDetail = make([]domain.BranchDetail, 0, len(dbBranches)) var branches []domain.BranchDetail = make([]domain.BranchDetail, 0, len(dbBranches))
for _, dbBranch := range dbBranches { for _, dbBranch := range dbBranches {
branches = append(branches, convertDBBranchDetail(dbBranch)) branches = append(branches, domain.ConvertDBBranchDetail(dbBranch))
} }
return branches, nil return branches, nil
} }
func (s *Store) UpdateBranch(ctx context.Context, branch domain.UpdateBranch) (domain.Branch, error) { func (s *Store) UpdateBranch(ctx context.Context, branch domain.UpdateBranch) (domain.Branch, error) {
dbBranch, err := s.queries.UpdateBranch(ctx, convertUpdateBranch(branch)) dbBranch, err := s.queries.UpdateBranch(ctx, domain.ConvertUpdateBranch(branch))
if err != nil { if err != nil {
return domain.Branch{}, err return domain.Branch{}, err
} }
return convertDBBranch(dbBranch), nil return domain.ConvertDBBranch(dbBranch), nil
} }
func (s *Store) DeleteBranch(ctx context.Context, id int64) error { func (s *Store) DeleteBranch(ctx context.Context, id int64) error {
@ -272,7 +187,7 @@ func (s *Store) GetBranchByCashier(ctx context.Context, userID int64) (domain.Br
return domain.Branch{}, err return domain.Branch{}, err
} }
return convertDBBranch(branch), err return domain.ConvertDBBranch(branch), err
} }
func (s *Store) DeleteBranchOperation(ctx context.Context, branchID int64, operationID int64) error { func (s *Store) DeleteBranchOperation(ctx context.Context, branchID int64, operationID int64) error {

View File

@ -89,7 +89,7 @@ func (s *Store) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEven
StartTime: e.StartTime.Time.UTC(), StartTime: e.StartTime.Time.UTC(),
Source: e.Source.String, Source: e.Source.String,
Status: domain.EventStatus(e.Status.String), Status: domain.EventStatus(e.Status.String),
Flagged: e.Flagged, IsFeatured: e.IsFeatured,
} }
} }
return upcomingEvents, nil return upcomingEvents, nil
@ -122,7 +122,8 @@ func (s *Store) GetExpiredUpcomingEvents(ctx context.Context, filter domain.Even
StartTime: e.StartTime.Time.UTC(), StartTime: e.StartTime.Time.UTC(),
Source: e.Source.String, Source: e.Source.String,
Status: domain.EventStatus(e.Status.String), Status: domain.EventStatus(e.Status.String),
Flagged: e.Flagged, IsFeatured: e.IsFeatured,
IsActive: e.IsActive,
} }
} }
return upcomingEvents, nil return upcomingEvents, nil
@ -139,6 +140,10 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.Ev
Int32: int32(filter.SportID.Value), Int32: int32(filter.SportID.Value),
Valid: filter.SportID.Valid, Valid: filter.SportID.Valid,
}, },
Query: pgtype.Text{
String: filter.Query.Value,
Valid: filter.Query.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,
@ -159,9 +164,9 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.Ev
String: filter.CountryCode.Value, String: filter.CountryCode.Value,
Valid: filter.CountryCode.Valid, Valid: filter.CountryCode.Valid,
}, },
Flagged: pgtype.Bool{ IsFeatured: pgtype.Bool{
Bool: filter.Flagged.Valid, Bool: filter.Featured.Valid,
Valid: filter.Flagged.Valid, Valid: filter.Featured.Valid,
}, },
}) })
@ -186,7 +191,8 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.Ev
StartTime: e.StartTime.Time.UTC(), StartTime: e.StartTime.Time.UTC(),
Source: e.Source.String, Source: e.Source.String,
Status: domain.EventStatus(e.Status.String), Status: domain.EventStatus(e.Status.String),
Flagged: e.Flagged, IsFeatured: e.IsFeatured,
IsActive: e.IsActive,
} }
} }
totalCount, err := s.queries.GetTotalEvents(ctx, dbgen.GetTotalEventsParams{ totalCount, err := s.queries.GetTotalEvents(ctx, dbgen.GetTotalEventsParams{
@ -198,6 +204,10 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.Ev
Int32: int32(filter.SportID.Value), Int32: int32(filter.SportID.Value),
Valid: filter.SportID.Valid, Valid: filter.SportID.Valid,
}, },
Query: pgtype.Text{
String: filter.Query.Value,
Valid: filter.Query.Valid,
},
FirstStartTime: pgtype.Timestamp{ FirstStartTime: pgtype.Timestamp{
Time: filter.FirstStartTime.Value.UTC(), Time: filter.FirstStartTime.Value.UTC(),
Valid: filter.FirstStartTime.Valid, Valid: filter.FirstStartTime.Valid,
@ -210,9 +220,9 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.Ev
String: filter.CountryCode.Value, String: filter.CountryCode.Value,
Valid: filter.CountryCode.Valid, Valid: filter.CountryCode.Valid,
}, },
Flagged: pgtype.Bool{ IsFeatured: pgtype.Bool{
Bool: filter.Flagged.Valid, Bool: filter.Featured.Valid,
Valid: filter.Flagged.Valid, Valid: filter.Featured.Valid,
}, },
}) })
if err != nil { if err != nil {
@ -244,7 +254,7 @@ func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.Upc
StartTime: event.StartTime.Time.UTC(), StartTime: event.StartTime.Time.UTC(),
Source: event.Source.String, Source: event.Source.String,
Status: domain.EventStatus(event.Status.String), Status: domain.EventStatus(event.Status.String),
Flagged: event.Flagged, IsFeatured: event.IsFeatured,
}, nil }, nil
} }
func (s *Store) UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error { func (s *Store) UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error {
@ -280,10 +290,10 @@ func (s *Store) UpdateEventStatus(ctx context.Context, eventID string, status do
} }
func (s *Store) UpdateFlagged(ctx context.Context, eventID string, flagged bool) error { func (s *Store) UpdateFeatured(ctx context.Context, eventID string, isFeatured bool) error {
return s.queries.UpdateFlagged(ctx, dbgen.UpdateFlaggedParams{ return s.queries.UpdateFeatured(ctx, dbgen.UpdateFeaturedParams{
ID: eventID, ID: eventID,
Flagged: flagged, IsFeatured: isFeatured,
}) })
} }

View File

@ -123,7 +123,7 @@ func (s *Store) UpdateLeague(ctx context.Context, league domain.UpdateLeague) er
}, },
IsFeatured: pgtype.Bool{ IsFeatured: pgtype.Bool{
Bool: league.IsFeatured.Value, Bool: league.IsFeatured.Value,
Valid: league.IsActive.Valid, Valid: league.IsFeatured.Valid,
}, },
SportID: pgtype.Int4{ SportID: pgtype.Int4{
Int32: league.SportID.Value, Int32: league.SportID.Value,

View File

@ -0,0 +1,30 @@
package repository
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/jackc/pgx/v5/pgtype"
)
func (s *Store) GetAllBranchLocations (ctx context.Context, query domain.ValidString) ([]domain.BranchLocation, error) {
locations, err := s.queries.GetAllBranchLocations(ctx, pgtype.Text{
String: query.Value,
Valid: query.Valid,
})
if err != nil {
return nil, err
}
var result []domain.BranchLocation = make([]domain.BranchLocation, 0, len(locations))
for _, location := range locations {
result = append(result, domain.BranchLocation{
Key: location.Key,
Name: location.Value,
})
}
return result, nil
}

View File

@ -317,39 +317,7 @@ func (s *Store) CountUnreadNotifications(ctx context.Context, userID int64) (int
return count, nil return count, nil
} }
func (s *Store) GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error) {
dbCompany, err := s.queries.GetCompanyByWalletID(ctx, walletID)
if err != nil {
return domain.Company{}, err
}
return domain.Company{
ID: dbCompany.ID,
Name: dbCompany.Name,
AdminID: dbCompany.AdminID,
WalletID: dbCompany.WalletID,
}, nil
}
func (s *Store) GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error) {
dbBranch, err := s.queries.GetBranchByWalletID(ctx, walletID)
if err != nil {
return domain.Branch{}, err
}
return domain.Branch{
ID: dbBranch.ID,
Name: dbBranch.Name,
Location: dbBranch.Location,
IsActive: dbBranch.IsActive,
WalletID: dbBranch.WalletID,
BranchManagerID: dbBranch.BranchManagerID,
CompanyID: dbBranch.CompanyID,
IsSelfOwned: dbBranch.IsSelfOwned,
// Creat: dbBranch.CreatedAt.Time,
// UpdatedAt: dbBranch.UpdatedAt.Time,
}, nil
}
// func (s *Store) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) { // func (s *Store) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) {
// dbNotifications, err := s.queries.GetAllNotifications(ctx, dbgen.GetAllNotificationsParams{ // dbNotifications, err := s.queries.GetAllNotifications(ctx, dbgen.GetAllNotificationsParams{

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"strconv" "strconv"
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
@ -14,6 +15,10 @@ func GetDBSettingList(settings []dbgen.Setting) (domain.SettingList, error) {
var dbSettingList domain.DBSettingList var dbSettingList domain.DBSettingList
var int64SettingsMap = domain.ConvertInt64SettingsMap(&dbSettingList) var int64SettingsMap = domain.ConvertInt64SettingsMap(&dbSettingList)
var stringSettingsMap = domain.ConvertStringSettingsMap(&dbSettingList)
var boolSettingsMap = domain.ConvertBoolSettingsMap(&dbSettingList)
var float32SettingsMap = domain.ConvertFloat32SettingsMap(&dbSettingList)
var timeSettingsMap = domain.ConvertTimeSettingsMap(&dbSettingList)
for _, setting := range settings { for _, setting := range settings {
is_setting_unknown := true is_setting_unknown := true
@ -31,6 +36,57 @@ func GetDBSettingList(settings []dbgen.Setting) (domain.SettingList, error) {
} }
} }
for key, dbSetting := range stringSettingsMap {
if setting.Key == key {
*dbSetting = domain.ValidString{
Value: setting.Value,
Valid: true,
}
is_setting_unknown = false
}
}
for key, dbSetting := range boolSettingsMap {
if setting.Key == key {
value, err := strconv.ParseBool(setting.Value)
if err != nil {
return domain.SettingList{}, err
}
*dbSetting = domain.ValidBool{
Value: value,
Valid: true,
}
is_setting_unknown = false
}
}
for key, dbSetting := range float32SettingsMap {
if setting.Key == key {
value, err := strconv.ParseFloat(setting.Value, 32)
if err != nil {
return domain.SettingList{}, err
}
*dbSetting = domain.ValidFloat32{
Value: float32(value),
Valid: true,
}
is_setting_unknown = false
}
}
for key, dbSetting := range timeSettingsMap {
if setting.Key == key {
value, err := time.Parse(time.RFC3339, setting.Value)
if err != nil {
return domain.SettingList{}, err
}
*dbSetting = domain.ValidTime{
Value: value,
Valid: true,
}
is_setting_unknown = false
}
}
if is_setting_unknown { if is_setting_unknown {
domain.MongoDBLogger.Warn("unknown setting found on database", zap.String("setting", setting.Key)) domain.MongoDBLogger.Warn("unknown setting found on database", zap.String("setting", setting.Key))
} }

View File

@ -490,6 +490,27 @@ func (s *Store) CreateUserWithoutOtp(ctx context.Context, user domain.User, is_c
}, nil }, nil
} }
func (s *Store) GetAdminByCompanyID(ctx context.Context, companyID int64) (domain.User, error) {
userRes, err := s.queries.GetAdminByCompanyID(ctx, companyID)
if err != nil {
return domain.User{}, err
}
return domain.User{
ID: userRes.ID,
FirstName: userRes.FirstName,
LastName: userRes.LastName,
Email: userRes.Email.String,
PhoneNumber: userRes.PhoneNumber.String,
Role: domain.Role(userRes.Role),
EmailVerified: userRes.EmailVerified,
PhoneVerified: userRes.PhoneVerified,
CreatedAt: userRes.CreatedAt.Time,
UpdatedAt: userRes.UpdatedAt.Time,
Suspended: userRes.Suspended,
}, nil
}
// GetCustomerCounts returns total and active customer counts // GetCustomerCounts returns total and active customer counts
func (s *Store) GetCustomerCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) { func (s *Store) GetCustomerCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) {
query := `SELECT query := `SELECT

View File

@ -17,6 +17,7 @@ func convertDBWallet(wallet dbgen.Wallet) domain.Wallet {
IsTransferable: wallet.IsTransferable, IsTransferable: wallet.IsTransferable,
IsActive: wallet.IsActive, IsActive: wallet.IsActive,
UserID: wallet.UserID, UserID: wallet.UserID,
Type: domain.WalletType(wallet.Type),
UpdatedAt: wallet.UpdatedAt.Time, UpdatedAt: wallet.UpdatedAt.Time,
CreatedAt: wallet.CreatedAt.Time, CreatedAt: wallet.CreatedAt.Time,
} }
@ -28,6 +29,7 @@ func convertCreateWallet(wallet domain.CreateWallet) dbgen.CreateWalletParams {
IsBettable: wallet.IsBettable, IsBettable: wallet.IsBettable,
IsTransferable: wallet.IsTransferable, IsTransferable: wallet.IsTransferable,
UserID: wallet.UserID, UserID: wallet.UserID,
Type: string(wallet.Type),
} }
} }
@ -183,6 +185,40 @@ func (s *Store) UpdateWalletActive(ctx context.Context, id int64, isActive bool)
return err return err
} }
func (s *Store) GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error) {
dbCompany, err := s.queries.GetCompanyByWalletID(ctx, walletID)
if err != nil {
return domain.Company{}, err
}
return domain.Company{
ID: dbCompany.ID,
Name: dbCompany.Name,
AdminID: dbCompany.AdminID,
WalletID: dbCompany.WalletID,
}, nil
}
func (s *Store) GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error) {
dbBranch, err := s.queries.GetBranchByWalletID(ctx, walletID)
if err != nil {
return domain.Branch{}, err
}
return domain.Branch{
ID: dbBranch.ID,
Name: dbBranch.Name,
Location: dbBranch.Location,
IsActive: dbBranch.IsActive,
WalletID: dbBranch.WalletID,
BranchManagerID: dbBranch.BranchManagerID,
CompanyID: dbBranch.CompanyID,
IsSelfOwned: dbBranch.IsSelfOwned,
// Creat: dbBranch.CreatedAt.Time,
// UpdatedAt: dbBranch.UpdatedAt.Time,
}, nil
}
// GetBalanceSummary returns wallet balance summary // GetBalanceSummary returns wallet balance summary
func (s *Store) GetBalanceSummary(ctx context.Context, filter domain.ReportFilter) (domain.BalanceSummary, error) { func (s *Store) GetBalanceSummary(ctx context.Context, filter domain.ReportFilter) (domain.BalanceSummary, error) {
var summary domain.BalanceSummary var summary domain.BalanceSummary
@ -275,4 +311,3 @@ func (s *Store) GetTotalWallets(ctx context.Context, filter domain.ReportFilter)
return total, nil return total, nil
} }

View File

@ -10,13 +10,16 @@ import (
type BetStore interface { type BetStore interface {
CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error)
CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBetOutcome) (int64, error) CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBetOutcome) (int64, error)
CreateFlag(ctx context.Context, flag domain.CreateFlagReq) (domain.Flag, error)
GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error)
GetAllBets(ctx context.Context, filter domain.BetFilter) ([]domain.GetBet, error) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]domain.GetBet, error)
GetBetByUserID(ctx context.Context, UserID int64) ([]domain.GetBet, error) GetBetByUserID(ctx context.Context, UserID int64) ([]domain.GetBet, error)
GetBetByFastCode(ctx context.Context, fastcode string) (domain.GetBet, error) GetBetByFastCode(ctx context.Context, fastcode string) (domain.GetBet, error)
GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error) GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error)
GetBetOutcomeByBetID(ctx context.Context, betID int64) ([]domain.BetOutcome, error) GetBetOutcomeByBetID(ctx context.Context, betID int64) ([]domain.BetOutcome, error)
GetBetCount(ctx context.Context, userID int64, outcomesHash string) (int64, error) GetBetOutcomeCountByOddID(ctx context.Context, oddID int64) (int64, error)
GetBetCountByUserID(ctx context.Context, userID int64, outcomesHash string) (int64, error)
GetBetCountByOutcomesHash(ctx context.Context, outcomesHash string) (int64, error)
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)

View File

@ -22,9 +22,10 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -54,6 +55,7 @@ type Service struct {
branchSvc branch.Service branchSvc branch.Service
companySvc company.Service companySvc company.Service
settingSvc settings.Service settingSvc settings.Service
userSvc user.Service
notificationSvc *notificationservice.Service notificationSvc *notificationservice.Service
logger *slog.Logger logger *slog.Logger
mongoLogger *zap.Logger mongoLogger *zap.Logger
@ -67,6 +69,7 @@ func NewService(
branchSvc branch.Service, branchSvc branch.Service,
companySvc company.Service, companySvc company.Service,
settingSvc settings.Service, settingSvc settings.Service,
userSvc user.Service,
notificationSvc *notificationservice.Service, notificationSvc *notificationservice.Service,
logger *slog.Logger, logger *slog.Logger,
mongoLogger *zap.Logger, mongoLogger *zap.Logger,
@ -215,6 +218,9 @@ func (s *Service) GenerateBetOutcome(ctx context.Context, eventID int64, marketI
func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID int64, role domain.Role, companyID domain.ValidInt64) (domain.CreateBetRes, error) { func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID int64, role domain.Role, companyID domain.ValidInt64) (domain.CreateBetRes, error) {
settingsList, err := s.settingSvc.GetSettingList(ctx) settingsList, err := s.settingSvc.GetSettingList(ctx)
if err != nil {
return domain.CreateBetRes{}, err
}
if req.Amount < 1 { if req.Amount < 1 {
return domain.CreateBetRes{}, ErrInvalidAmount return domain.CreateBetRes{}, ErrInvalidAmount
} }
@ -269,7 +275,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
return domain.CreateBetRes{}, err return domain.CreateBetRes{}, err
} }
count, err := s.GetBetCount(ctx, userID, outcomesHash) count, err := s.GetBetCountByUserID(ctx, userID, outcomesHash)
if err != nil { if err != nil {
s.mongoLogger.Error("failed to generate cashout ID", s.mongoLogger.Error("failed to generate cashout ID",
zap.Int64("user_id", userID), zap.Int64("user_id", userID),
@ -398,6 +404,79 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
return domain.CreateBetRes{}, err return domain.CreateBetRes{}, err
} }
for i := range outcomes {
// flag odds with large amount of users betting on them
count, err := s.betStore.GetBetOutcomeCountByOddID(ctx, outcomes[i].OddID)
if err != nil {
s.mongoLogger.Error("failed to get count of bet outcome",
zap.Int64("bet_id", bet.ID),
zap.Int64("odd_id", outcomes[i].OddID),
zap.Error(err),
)
return domain.CreateBetRes{}, err
}
// TODO: fetch cap from settings in db
if count > 20 {
flag := domain.CreateFlagReq{
BetID: 0,
OddID: outcomes[i].OddID,
Reason: fmt.Sprintf("too many users targeting odd - (%d)", outcomes[i].OddID),
}
_, err := s.betStore.CreateFlag(ctx, flag)
if err != nil {
s.mongoLogger.Error("failed to create flag for bet",
zap.Int64("bet_id", bet.ID),
zap.Error(err),
)
}
}
}
// flag bets that have more than three outcomes
if len(outcomes) > 3 {
flag := domain.CreateFlagReq{
BetID: bet.ID,
OddID: 0,
Reason: fmt.Sprintf("too many outcomes - (%d)", len(outcomes)),
}
_, err := s.betStore.CreateFlag(ctx, flag)
if err != nil {
s.mongoLogger.Error("failed to create flag for bet",
zap.Int64("bet_id", bet.ID),
zap.Error(err),
)
}
}
// large amount of users betting on the same bet_outcomes
total_bet_count, err := s.betStore.GetBetCountByOutcomesHash(ctx, outcomesHash)
if err != nil {
s.mongoLogger.Error("failed to get bet outcomes count",
zap.String("outcomes_hash", outcomesHash),
zap.Error(err),
)
return domain.CreateBetRes{}, err
}
if total_bet_count > 10 {
flag := domain.CreateFlagReq{
BetID: bet.ID,
OddID: 0,
Reason: fmt.Sprintf("too many users bet on same outcomes - (%s)", outcomesHash),
}
_, err := s.betStore.CreateFlag(ctx, flag)
if err != nil {
s.mongoLogger.Error("failed to get bet outcomes count",
zap.String("outcomes_hash", outcomesHash),
zap.Error(err),
)
}
}
res := domain.ConvertCreateBet(bet, rows) res := domain.ConvertCreateBet(bet, rows)
return res, nil return res, nil
@ -417,7 +496,7 @@ func (s *Service) DeductBetFromBranchWallet(ctx context.Context, amount float32,
deductedAmount := amount * company.DeductedPercentage deductedAmount := amount * company.DeductedPercentage
_, err = s.walletSvc.DeductFromWallet(ctx, _, err = s.walletSvc.DeductFromWallet(ctx,
walletID, domain.ToCurrency(deductedAmount), domain.BranchWalletType, domain.ValidInt64{ walletID, domain.ToCurrency(deductedAmount), domain.ValidInt64{
Value: userID, Value: userID,
Valid: true, Valid: true,
}, domain.TRANSFER_DIRECT, }, domain.TRANSFER_DIRECT,
@ -446,7 +525,7 @@ func (s *Service) DeductBetFromCustomerWallet(ctx context.Context, amount float3
} }
if amount < wallets.RegularBalance.Float32() { if amount < wallets.RegularBalance.Float32() {
_, err = s.walletSvc.DeductFromWallet(ctx, wallets.RegularID, _, err = s.walletSvc.DeductFromWallet(ctx, wallets.RegularID,
domain.ToCurrency(amount), domain.CustomerWalletType, domain.ValidInt64{}, domain.ToCurrency(amount), domain.ValidInt64{},
domain.TRANSFER_DIRECT, fmt.Sprintf("Deducted %v amount from wallet by system while placing bet", amount)) domain.TRANSFER_DIRECT, fmt.Sprintf("Deducted %v amount from wallet by system while placing bet", amount))
if err != nil { if err != nil {
s.mongoLogger.Error("wallet deduction failed for customer regular wallet", s.mongoLogger.Error("wallet deduction failed for customer regular wallet",
@ -465,7 +544,7 @@ func (s *Service) DeductBetFromCustomerWallet(ctx context.Context, amount float3
} }
// Empty the regular balance // Empty the regular balance
_, err = s.walletSvc.DeductFromWallet(ctx, wallets.RegularID, _, err = s.walletSvc.DeductFromWallet(ctx, wallets.RegularID,
wallets.RegularBalance, domain.CustomerWalletType, domain.ValidInt64{}, domain.TRANSFER_DIRECT, wallets.RegularBalance, domain.ValidInt64{}, domain.TRANSFER_DIRECT,
fmt.Sprintf("Deducted %v amount from wallet by system while placing bet", wallets.RegularBalance.Float32())) fmt.Sprintf("Deducted %v amount from wallet by system while placing bet", wallets.RegularBalance.Float32()))
if err != nil { if err != nil {
s.mongoLogger.Error("wallet deduction failed for customer regular wallet", s.mongoLogger.Error("wallet deduction failed for customer regular wallet",
@ -480,7 +559,7 @@ func (s *Service) DeductBetFromCustomerWallet(ctx context.Context, amount float3
// Empty remaining from static balance // Empty remaining from static balance
remainingAmount := wallets.RegularBalance - domain.Currency(amount) remainingAmount := wallets.RegularBalance - domain.Currency(amount)
_, err = s.walletSvc.DeductFromWallet(ctx, wallets.StaticID, _, err = s.walletSvc.DeductFromWallet(ctx, wallets.StaticID,
remainingAmount, domain.CustomerWalletType, domain.ValidInt64{}, domain.TRANSFER_DIRECT, remainingAmount, domain.ValidInt64{}, domain.TRANSFER_DIRECT,
fmt.Sprintf("Deducted %v amount from wallet by system while placing bet", remainingAmount.Float32())) fmt.Sprintf("Deducted %v amount from wallet by system while placing bet", remainingAmount.Float32()))
if err != nil { if err != nil {
s.mongoLogger.Error("wallet deduction failed for customer static wallet", s.mongoLogger.Error("wallet deduction failed for customer static wallet",
@ -716,7 +795,7 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, le
return domain.CreateBetRes{}, err return domain.CreateBetRes{}, err
} }
count, err := s.GetBetCount(ctx, userID, outcomesHash) count, err := s.GetBetCountByUserID(ctx, userID, outcomesHash)
if err != nil { if err != nil {
s.mongoLogger.Error("failed to get bet count", s.mongoLogger.Error("failed to get bet count",
zap.Int64("user_id", userID), zap.Int64("user_id", userID),
@ -799,8 +878,12 @@ func (s *Service) GetBetByFastCode(ctx context.Context, fastcode string) (domain
return s.betStore.GetBetByFastCode(ctx, fastcode) return s.betStore.GetBetByFastCode(ctx, fastcode)
} }
func (s *Service) GetBetCount(ctx context.Context, UserID int64, outcomesHash string) (int64, error) { func (s *Service) GetBetCountByUserID(ctx context.Context, UserID int64, outcomesHash string) (int64, error) {
return s.betStore.GetBetCount(ctx, UserID, outcomesHash) return s.betStore.GetBetCountByUserID(ctx, UserID, outcomesHash)
}
func (s *Service) GetBetCountByOutcomesHash(ctx context.Context, outcomesHash string) (int64, error) {
return s.betStore.GetBetCountByOutcomesHash(ctx, outcomesHash)
} }
func (s *Service) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error { func (s *Service) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error {
@ -817,10 +900,19 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc
return err return err
} }
if bet.IsShopBet || switch {
status == domain.OUTCOME_STATUS_ERROR || case bet.IsShopBet:
status == domain.OUTCOME_STATUS_PENDING || return s.betStore.UpdateStatus(ctx, id, status)
status == domain.OUTCOME_STATUS_LOSS { case status == domain.OUTCOME_STATUS_ERROR, status == domain.OUTCOME_STATUS_PENDING:
s.SendErrorStatusNotification(ctx, status, bet.UserID, "")
s.SendAdminErrorAlertNotification(ctx, status, "")
s.mongoLogger.Error("Bet Status is error",
zap.Int64("bet_id", id),
zap.Error(err),
)
return s.betStore.UpdateStatus(ctx, id, status)
case status == domain.OUTCOME_STATUS_LOSS:
s.SendLosingStatusNotification(ctx, status, bet.UserID, "")
return s.betStore.UpdateStatus(ctx, id, status) return s.betStore.UpdateStatus(ctx, id, status)
} }
@ -837,10 +929,15 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc
switch status { switch status {
case domain.OUTCOME_STATUS_WIN: case domain.OUTCOME_STATUS_WIN:
amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds) amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds)
s.SendWinningStatusNotification(ctx, status, bet.UserID, amount, "")
case domain.OUTCOME_STATUS_HALF: case domain.OUTCOME_STATUS_HALF:
amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds) / 2 amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds) / 2
default: s.SendWinningStatusNotification(ctx, status, bet.UserID, amount, "")
case domain.OUTCOME_STATUS_VOID:
amount = bet.Amount amount = bet.Amount
s.SendWinningStatusNotification(ctx, status, bet.UserID, amount, "")
default:
return fmt.Errorf("invalid outcome status")
} }
_, err = s.walletSvc.AddToWallet(ctx, customerWallet.RegularID, amount, domain.ValidInt64{}, _, err = s.walletSvc.AddToWallet(ctx, customerWallet.RegularID, amount, domain.ValidInt64{},
@ -858,6 +955,207 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc
return s.betStore.UpdateStatus(ctx, id, status) return s.betStore.UpdateStatus(ctx, id, status)
} }
func (s *Service) SendWinningStatusNotification(ctx context.Context, status domain.OutcomeStatus, userID int64, winningAmount domain.Currency, extra string) error {
var headline string
var message string
switch status {
case domain.OUTCOME_STATUS_WIN:
headline = "You Bet Has Won!"
message = fmt.Sprintf(
"You have been awarded %.2f",
winningAmount.Float32(),
)
case domain.OUTCOME_STATUS_HALF:
headline = "You have a half win"
message = fmt.Sprintf(
"You have been awarded %.2f",
winningAmount.Float32(),
)
case domain.OUTCOME_STATUS_VOID:
headline = "Your bet has been refunded"
message = fmt.Sprintf(
"You have been awarded %.2f",
winningAmount.Float32(),
)
}
betNotification := &domain.Notification{
RecipientID: userID,
Type: domain.NOTIFICATION_TYPE_BET_RESULT,
Level: domain.NotificationLevelSuccess,
Reciever: domain.NotificationRecieverSideCustomer,
DeliveryChannel: domain.DeliveryChannelInApp,
Payload: domain.NotificationPayload{
Headline: headline,
Message: message,
},
Priority: 2,
Metadata: fmt.Appendf(nil, `{
"winning_amount":%.2f,
"status":%v
"more": %v
}`, winningAmount.Float32(), status, extra),
}
if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil {
return err
}
betNotification.DeliveryChannel = domain.DeliveryChannelEmail
if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil {
return err
}
return nil
}
func (s *Service) SendLosingStatusNotification(ctx context.Context, status domain.OutcomeStatus, userID int64, extra string) error {
var headline string
var message string
switch status {
case domain.OUTCOME_STATUS_LOSS:
headline = "Your bet has lost"
message = "Better luck next time"
}
betNotification := &domain.Notification{
RecipientID: userID,
Type: domain.NOTIFICATION_TYPE_BET_RESULT,
Level: domain.NotificationLevelSuccess,
Reciever: domain.NotificationRecieverSideCustomer,
DeliveryChannel: domain.DeliveryChannelInApp,
Payload: domain.NotificationPayload{
Headline: headline,
Message: message,
},
Priority: 2,
Metadata: fmt.Appendf(nil, `{
"status":%v
"more": %v
}`, status, extra),
}
if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil {
return err
}
betNotification.DeliveryChannel = domain.DeliveryChannelEmail
if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil {
return err
}
return nil
}
func (s *Service) SendErrorStatusNotification(ctx context.Context, status domain.OutcomeStatus, userID int64, extra string) error {
var headline string
var message string
switch status {
case domain.OUTCOME_STATUS_ERROR, domain.OUTCOME_STATUS_PENDING:
headline = "There was an error with your bet"
message = "We have encounter an error with your bet. We will fix it as soon as we can"
}
errorSeverityLevel := domain.NotificationErrorSeverityFatal
betNotification := &domain.Notification{
RecipientID: userID,
Type: domain.NOTIFICATION_TYPE_BET_RESULT,
Level: domain.NotificationLevelSuccess,
Reciever: domain.NotificationRecieverSideCustomer,
DeliveryChannel: domain.DeliveryChannelInApp,
Payload: domain.NotificationPayload{
Headline: headline,
Message: message,
},
Priority: 1,
ErrorSeverity: &errorSeverityLevel,
Metadata: fmt.Appendf(nil, `{
"status":%v
"more": %v
}`, status, extra),
}
if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil {
return err
}
betNotification.DeliveryChannel = domain.DeliveryChannelEmail
if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil {
return err
}
return nil
}
func (s *Service) SendAdminErrorAlertNotification(ctx context.Context, status domain.OutcomeStatus, extra string) error {
var headline string
var message string
switch status {
case domain.OUTCOME_STATUS_ERROR, domain.OUTCOME_STATUS_PENDING:
headline = "There was an error with your bet"
message = "We have encounter an error with your bet. We will fix it as soon as we can"
}
betNotification := &domain.Notification{
Type: domain.NOTIFICATION_TYPE_BET_RESULT,
Level: domain.NotificationLevelSuccess,
Reciever: domain.NotificationRecieverSideCustomer,
DeliveryChannel: domain.DeliveryChannelEmail,
Payload: domain.NotificationPayload{
Headline: headline,
Message: message,
},
Priority: 2,
Metadata: fmt.Appendf(nil, `{
"status":%v
"more": %v
}`, status, extra),
}
users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{
Role: string(domain.RoleAdmin),
})
if err != nil {
s.mongoLogger.Error("failed to get admin recipients",
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
for _, user := range users {
betNotification.RecipientID = user.ID
if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil {
s.mongoLogger.Error("failed to send admin notification",
zap.Int64("admin_id", user.ID),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
betNotification.DeliveryChannel = domain.DeliveryChannelEmail
if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil {
s.mongoLogger.Error("failed to send email admin notification",
zap.Int64("admin_id", user.ID),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
}
return nil
}
func (s *Service) CheckBetOutcomeForBet(ctx context.Context, betID int64) (domain.OutcomeStatus, error) { func (s *Service) CheckBetOutcomeForBet(ctx context.Context, betID int64) (domain.OutcomeStatus, error) {
betOutcomes, err := s.betStore.GetBetOutcomeByBetID(ctx, betID) betOutcomes, err := s.betStore.GetBetOutcomeByBetID(ctx, betID)
if err != nil { if err != nil {
@ -1033,7 +1331,7 @@ func (s *Service) ProcessBetCashback(ctx context.Context) error {
) )
continue continue
} }
cashbackAmount := math.Min(float64(settingsList.CashbackAmountCap), float64(calculateCashbackAmount(bet.Amount.Float32(), bet.TotalOdds))) cashbackAmount := math.Min(float64(settingsList.CashbackAmountCap.Float32()), float64(calculateCashbackAmount(bet.Amount.Float32(), bet.TotalOdds)))
_, err = s.walletSvc.AddToWallet(ctx, wallets.StaticID, domain.ToCurrency(float32(cashbackAmount)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, _, err = s.walletSvc.AddToWallet(ctx, wallets.StaticID, domain.ToCurrency(float32(cashbackAmount)), domain.ValidInt64{}, domain.TRANSFER_DIRECT,
domain.PaymentDetails{}, fmt.Sprintf("cashback amount of %f added to users static wallet", cashbackAmount)) domain.PaymentDetails{}, fmt.Sprintf("cashback amount of %f added to users static wallet", cashbackAmount))

View File

@ -0,0 +1,11 @@
package branch
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
func (s *Service) GetAllBranchLocations(ctx context.Context, query domain.ValidString) ([]domain.BranchLocation, error) {
return s.branchStore.GetAllBranchLocations(ctx, query)
}

View File

@ -29,4 +29,6 @@ type BranchStore interface {
GetAllCompaniesBranch(ctx context.Context) ([]domain.Company, error) GetAllCompaniesBranch(ctx context.Context) ([]domain.Company, error)
GetBranchesByCompany(ctx context.Context, companyID int64) ([]domain.Branch, error) GetBranchesByCompany(ctx context.Context, companyID int64) ([]domain.Branch, error)
GetAllBranchLocations(ctx context.Context, query domain.ValidString) ([]domain.BranchLocation, error)
} }

View File

@ -78,3 +78,4 @@ func (s *Service) GetAllCompaniesBranch(ctx context.Context) ([]domain.Company,
func (s *Service) GetBranchesByCompany(ctx context.Context, companyID int64) ([]domain.Branch, error) { func (s *Service) GetBranchesByCompany(ctx context.Context, companyID int64) ([]domain.Branch, error) {
return s.branchStore.GetBranchesByCompany(ctx, companyID) return s.branchStore.GetBranchesByCompany(ctx, companyID)
} }

View File

@ -16,5 +16,5 @@ type Service interface {
// GetAndStoreMatchResult(ctx context.Context, eventID string) error // GetAndStoreMatchResult(ctx context.Context, eventID string) error
UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error
UpdateEventStatus(ctx context.Context, eventID string, status domain.EventStatus) error UpdateEventStatus(ctx context.Context, eventID string, status domain.EventStatus) error
UpdateFlagged(ctx context.Context, eventID string, flagged bool) error UpdateFeatured(ctx context.Context, eventID string, flagged bool) error
} }

View File

@ -369,8 +369,8 @@ func (s *service) UpdateEventStatus(ctx context.Context, eventID string, status
return s.store.UpdateEventStatus(ctx, eventID, status) return s.store.UpdateEventStatus(ctx, eventID, status)
} }
func (s *service) UpdateFlagged(ctx context.Context, eventID string, flagged bool) error { func (s *service) UpdateFeatured(ctx context.Context, eventID string, flagged bool) error {
return s.store.UpdateFlagged(ctx, eventID, flagged) return s.store.UpdateFeatured(ctx, eventID, flagged)
} }
// func (s *service) GetAndStoreMatchResult(ctx context.Context, eventID string) error { // func (s *service) GetAndStoreMatchResult(ctx context.Context, eventID string) error {

View File

@ -0,0 +1,26 @@
package messenger
import (
"context"
"github.com/resend/resend-go/v2"
)
func (s *Service) SendEmail(ctx context.Context, receiverEmail, message string, subject string) error {
apiKey := s.config.ResendApiKey
client := resend.NewClient(apiKey)
formattedSenderEmail := "FortuneBets <" + s.config.ResendSenderEmail + ">"
params := &resend.SendEmailRequest{
From: formattedSenderEmail,
To: []string{receiverEmail},
Subject: subject,
Text: message,
}
_, err := client.Emails.Send(params)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,21 @@
package messenger
import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings"
)
type Service struct {
settingSvc *settings.Service
config *config.Config
}
func NewService(
settingSvc *settings.Service,
cfg *config.Config,
) *Service {
return &Service{
settingSvc: settingSvc,
config: cfg,
}
}

View File

@ -0,0 +1,85 @@
package messenger
import (
"context"
"errors"
"fmt"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
afro "github.com/amanuelabay/afrosms-go"
"github.com/twilio/twilio-go"
twilioApi "github.com/twilio/twilio-go/rest/api/v2010"
)
var (
ErrSMSProviderNotFound = errors.New("SMS Provider Not Found")
)
func (s *Service) SendSMS(ctx context.Context, receiverPhone, message string) error {
settingsList, err := s.settingSvc.GetSettingList(ctx)
if err != nil {
return err
}
switch settingsList.SMSProvider {
case domain.AfroMessage:
return s.SendAfroMessageSMS(ctx, receiverPhone, message)
case domain.TwilioSms:
return s.SendTwilioSMS(ctx, receiverPhone, message)
default:
return ErrSMSProviderNotFound
}
}
func (s *Service) SendAfroMessageSMS(ctx context.Context, receiverPhone, message string) error {
apiKey := s.config.AFRO_SMS_API_KEY
senderName := s.config.AFRO_SMS_SENDER_NAME
hostURL := s.config.ADRO_SMS_HOST_URL
endpoint := "/api/send"
// API endpoint has been updated
// TODO: no need for package for the afro message operations (pretty simple stuff)
request := afro.GetRequest(apiKey, endpoint, hostURL)
request.BaseURL = "https://api.afromessage.com/api/send"
request.Method = "GET"
request.Sender(senderName)
request.To(receiverPhone, message)
response, err := afro.MakeRequestWithContext(ctx, request)
if err != nil {
return err
}
if response["acknowledge"] == "success" {
return nil
} else {
fmt.Println(response["response"].(map[string]interface{}))
return errors.New("SMS delivery failed")
}
}
func (s *Service) SendTwilioSMS(ctx context.Context, receiverPhone, message string) error {
accountSid := s.config.TwilioAccountSid
authToken := s.config.TwilioAuthToken
senderPhone := s.config.TwilioSenderPhoneNumber
client := twilio.NewRestClientWithParams(twilio.ClientParams{
Username: accountSid,
Password: authToken,
})
params := &twilioApi.CreateMessageParams{}
params.SetTo(receiverPhone)
params.SetFrom(senderPhone)
params.SetBody(message)
_, err := client.Api.CreateMessage(params)
if err != nil {
return fmt.Errorf("%s", "Error sending SMS message: %s"+err.Error())
}
return nil
}

View File

@ -8,8 +8,6 @@ import (
) )
type NotificationStore interface { type NotificationStore interface {
GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error)
GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error)
SendNotification(ctx context.Context, notification *domain.Notification) error SendNotification(ctx context.Context, notification *domain.Notification) error
MarkAsRead(ctx context.Context, notificationID string, recipientID int64) error MarkAsRead(ctx context.Context, notificationID string, recipientID int64) error
ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error) ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error)

View File

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

View File

@ -16,7 +16,7 @@ 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" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
) )

View File

@ -9,7 +9,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings"
"go.uber.org/zap" "go.uber.org/zap"

View File

@ -2,40 +2,36 @@ package user
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers" "github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers"
afro "github.com/amanuelabay/afrosms-go"
"github.com/resend/resend-go/v2"
"github.com/twilio/twilio-go"
twilioApi "github.com/twilio/twilio-go/rest/api/v2010"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium, provider domain.OtpProvider) error { func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium, provider domain.SMSProvider) error {
otpCode := helpers.GenerateOTP() otpCode := helpers.GenerateOTP()
message := fmt.Sprintf("Welcome to Fortune bets, your OTP is %s please don't share with anyone.", otpCode) message := fmt.Sprintf("Welcome to Fortune bets, your OTP is %s please don't share with anyone.", otpCode)
switch medium { switch medium {
case domain.OtpMediumSms: case domain.OtpMediumSms:
switch provider { switch provider {
case "twilio": case domain.TwilioSms:
if err := s.SendTwilioSMSOTP(ctx, sentTo, message, provider); err != nil { if err := s.messengerSvc.SendTwilioSMS(ctx, sentTo, message); err != nil {
return err return err
} }
case "afromessage": case domain.AfroMessage:
if err := s.SendAfroMessageSMSOTP(ctx, sentTo, message, provider); err != nil { if err := s.messengerSvc.SendAfroMessageSMS(ctx, sentTo, message); err != nil {
return err return err
} }
default: default:
return fmt.Errorf("invalid sms provider: %s", provider) return fmt.Errorf("invalid sms provider: %s", provider)
} }
case domain.OtpMediumEmail: case domain.OtpMediumEmail:
if err := s.SendEmailOTP(ctx, sentTo, message); err != nil { if err := s.messengerSvc.SendEmail(ctx, sentTo, message, "FortuneBets - One Time Password"); err != nil {
return err return err
} }
} }
@ -61,73 +57,3 @@ func hashPassword(plaintextPassword string) ([]byte, error) {
return hash, nil return hash, nil
} }
func (s *Service) SendAfroMessageSMSOTP(ctx context.Context, receiverPhone, message string, provider domain.OtpProvider) error {
apiKey := s.config.AFRO_SMS_API_KEY
senderName := s.config.AFRO_SMS_SENDER_NAME
hostURL := s.config.ADRO_SMS_HOST_URL
endpoint := "/api/send"
// API endpoint has been updated
// TODO: no need for package for the afro message operations (pretty simple stuff)
request := afro.GetRequest(apiKey, endpoint, hostURL)
request.BaseURL = "https://api.afromessage.com/api/send"
request.Method = "GET"
request.Sender(senderName)
request.To(receiverPhone, message)
response, err := afro.MakeRequestWithContext(ctx, request)
if err != nil {
return err
}
if response["acknowledge"] == "success" {
return nil
} else {
fmt.Println(response["response"].(map[string]interface{}))
return errors.New("SMS delivery failed")
}
}
func (s *Service) SendTwilioSMSOTP(ctx context.Context, receiverPhone, message string, provider domain.OtpProvider) error {
accountSid := s.config.TwilioAccountSid
authToken := s.config.TwilioAuthToken
senderPhone := s.config.TwilioSenderPhoneNumber
client := twilio.NewRestClientWithParams(twilio.ClientParams{
Username: accountSid,
Password: authToken,
})
params := &twilioApi.CreateMessageParams{}
params.SetTo(receiverPhone)
params.SetFrom(senderPhone)
params.SetBody(message)
_, err := client.Api.CreateMessage(params)
if err != nil {
return fmt.Errorf("%s", "Error sending SMS message: %s" + err.Error())
}
return nil
}
func (s *Service) SendEmailOTP(ctx context.Context, receiverEmail, message string) error {
apiKey := s.config.ResendApiKey
client := resend.NewClient(apiKey)
formattedSenderEmail := "FortuneBets <" + s.config.ResendSenderEmail + ">"
params := &resend.SendEmailRequest{
From: formattedSenderEmail,
To: []string{receiverEmail},
Subject: "FortuneBets - One Time Password",
Text: message,
}
_, err := client.Emails.Send(params)
if err != nil {
return err
}
return nil
}

View File

@ -43,8 +43,6 @@ func (s *Service) DeleteUser(ctx context.Context, id int64) error {
return s.userStore.DeleteUser(ctx, id) return s.userStore.DeleteUser(ctx, id)
} }
func (s *Service) GetAllUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error) { func (s *Service) GetAllUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error) {
// Get all Users // Get all Users
return s.userStore.GetAllUsers(ctx, filter) return s.userStore.GetAllUsers(ctx, filter)
@ -58,7 +56,10 @@ func (s *Service) GetCashiersByBranch(ctx context.Context, branchID int64) ([]do
return s.userStore.GetCashiersByBranch(ctx, branchID) return s.userStore.GetCashiersByBranch(ctx, branchID)
} }
func (s *Service) GetAllCashiers(ctx context.Context, filter domain.UserFilter) ([]domain.GetCashier, int64, error){ func (s *Service) GetAdminByCompanyID(ctx context.Context, companyID int64) (domain.User, error) {
return s.userStore.GetAdminByCompanyID(ctx, companyID)
}
func (s *Service) GetAllCashiers(ctx context.Context, filter domain.UserFilter) ([]domain.GetCashier, int64, error) {
return s.userStore.GetAllCashiers(ctx, filter) return s.userStore.GetAllCashiers(ctx, filter)
} }

View File

@ -14,6 +14,7 @@ type UserStore interface {
GetAllCashiers(ctx context.Context, filter domain.UserFilter) ([]domain.GetCashier, int64, error) GetAllCashiers(ctx context.Context, filter domain.UserFilter) ([]domain.GetCashier, int64, error)
GetCashierByID(ctx context.Context, cashierID int64) (domain.GetCashier, error) GetCashierByID(ctx context.Context, cashierID int64) (domain.GetCashier, error)
GetCashiersByBranch(ctx context.Context, branchID int64) ([]domain.User, error) GetCashiersByBranch(ctx context.Context, branchID int64) ([]domain.User, error)
GetAdminByCompanyID(ctx context.Context, companyID int64) (domain.User, error)
UpdateUser(ctx context.Context, user domain.UpdateUserReq) error UpdateUser(ctx context.Context, user domain.UpdateUserReq) error
UpdateUserCompany(ctx context.Context, id int64, companyID int64) error UpdateUserCompany(ctx context.Context, id int64, companyID int64) error
UpdateUserSuspend(ctx context.Context, id int64, status bool) error UpdateUserSuspend(ctx context.Context, id int64, status bool) error

View File

@ -10,7 +10,7 @@ import (
func (s *Service) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) { // email,phone,error func (s *Service) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) { // email,phone,error
return s.userStore.CheckPhoneEmailExist(ctx, phoneNum, email) return s.userStore.CheckPhoneEmailExist(ctx, phoneNum, email)
} }
func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.OtpProvider) error { func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.SMSProvider) error {
var err error var err error
// check if user exists // check if user exists
switch medium { switch medium {

View File

@ -8,7 +8,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
) )
func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.OtpProvider) error { func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.SMSProvider) error {
var err error var err error
// check if user exists // check if user exists

View File

@ -4,6 +4,7 @@ import (
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/messenger"
) )
const ( const (
@ -11,19 +12,22 @@ const (
) )
type Service struct { type Service struct {
userStore UserStore userStore UserStore
otpStore OtpStore otpStore OtpStore
config *config.Config messengerSvc *messenger.Service
config *config.Config
} }
func NewService( func NewService(
userStore UserStore, userStore UserStore,
otpStore OtpStore, otpStore OtpStore,
messengerSvc *messenger.Service,
cfg *config.Config, cfg *config.Config,
) *Service { ) *Service {
return &Service{ return &Service{
userStore: userStore, userStore: userStore,
otpStore: otpStore, otpStore: otpStore,
config: cfg, messengerSvc: messengerSvc,
config: cfg,
} }
} }

View File

@ -254,7 +254,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")
} }
_, err = s.walletSvc.DeductFromWallet(ctx, claims.UserID, domain.Currency(amountCents), _, err = s.walletSvc.DeductFromWallet(ctx, claims.UserID, domain.Currency(amountCents),
domain.CustomerWalletType, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.ValidInt64{}, domain.TRANSFER_DIRECT,
fmt.Sprintf("Deducted %v amount from wallet by system while placing virtual game bet", amountCents)) fmt.Sprintf("Deducted %v amount from wallet by system while placing virtual game bet", amountCents))
if err != nil { if err != nil {
return nil, fmt.Errorf("insufficient balance") return nil, fmt.Errorf("insufficient balance")

View File

@ -115,7 +115,7 @@ func (c *Client) ProcessBet(ctx context.Context, req domain.BetRequest) (*domain
return &domain.BetResponse{}, err return &domain.BetResponse{}, err
} }
c.walletSvc.DeductFromWallet(ctx, wallets[0].ID, domain.Currency(req.Amount.Amount), domain.CustomerWalletType, domain.ValidInt64{}, domain.TRANSFER_DIRECT, c.walletSvc.DeductFromWallet(ctx, wallets[0].ID, domain.Currency(req.Amount.Amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT,
fmt.Sprintf("Deducting %v from wallet for creating Veli Game Bet", req.Amount.Amount), fmt.Sprintf("Deducting %v from wallet for creating Veli Game Bet", req.Amount.Amount),
) )

View File

@ -10,7 +10,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
) )

View File

@ -7,8 +7,8 @@ import (
) )
type WalletStore interface { type WalletStore interface {
// GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error) GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error)
// GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error) GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error)
CreateWallet(ctx context.Context, wallet domain.CreateWallet) (domain.Wallet, error) CreateWallet(ctx context.Context, wallet domain.CreateWallet) (domain.Wallet, error)
CreateCustomerWallet(ctx context.Context, customerWallet domain.CreateCustomerWallet) (domain.CustomerWallet, error) CreateCustomerWallet(ctx context.Context, customerWallet domain.CreateCustomerWallet) (domain.CustomerWallet, error)
GetWalletByID(ctx context.Context, id int64) (domain.Wallet, error) GetWalletByID(ctx context.Context, id int64) (domain.Wallet, error)

View File

@ -3,8 +3,14 @@ package wallet
import ( import (
"log/slog" "log/slog"
<<<<<<< HEAD
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/kafka" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/kafka"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
=======
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
"go.uber.org/zap"
>>>>>>> d43b12c589d32e4b6147cfb54a3b939c476bae6f
) )
type Service struct { type Service struct {
@ -13,17 +19,33 @@ type Service struct {
transferStore TransferStore transferStore TransferStore
notificationStore notificationservice.NotificationStore notificationStore notificationservice.NotificationStore
notificationSvc *notificationservice.Service notificationSvc *notificationservice.Service
userSvc *user.Service
mongoLogger *zap.Logger
logger *slog.Logger logger *slog.Logger
kafkaProducer *kafka.Producer kafkaProducer *kafka.Producer
} }
<<<<<<< HEAD
func NewService(walletStore WalletStore, transferStore TransferStore, notificationStore notificationservice.NotificationStore, notificationSvc *notificationservice.Service, logger *slog.Logger, kafkaProducer *kafka.Producer) *Service { func NewService(walletStore WalletStore, transferStore TransferStore, notificationStore notificationservice.NotificationStore, notificationSvc *notificationservice.Service, logger *slog.Logger, kafkaProducer *kafka.Producer) *Service {
=======
func NewService(
walletStore WalletStore,
transferStore TransferStore,
notificationStore notificationservice.NotificationStore,
notificationSvc *notificationservice.Service,
userSvc *user.Service,
mongoLogger *zap.Logger,
logger *slog.Logger,
) *Service {
>>>>>>> d43b12c589d32e4b6147cfb54a3b939c476bae6f
return &Service{ return &Service{
walletStore: walletStore, walletStore: walletStore,
transferStore: transferStore, transferStore: transferStore,
// approvalStore: approvalStore, // approvalStore: approvalStore,
notificationStore: notificationStore, notificationStore: notificationStore,
notificationSvc: notificationSvc, notificationSvc: notificationSvc,
userSvc: userSvc,
mongoLogger: mongoLogger,
logger: logger, logger: logger,
kafkaProducer: kafkaProducer, kafkaProducer: kafkaProducer,
} }

View File

@ -4,9 +4,14 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
<<<<<<< HEAD
"github.com/SamuelTariku/FortuneBet-Backend/internal/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/event"
=======
"go.uber.org/zap"
>>>>>>> d43b12c589d32e4b6147cfb54a3b939c476bae6f
) )
var ( var (
@ -60,6 +65,14 @@ func (s *Service) GetWalletsByUser(ctx context.Context, id int64) ([]domain.Wall
return s.walletStore.GetWalletsByUser(ctx, id) return s.walletStore.GetWalletsByUser(ctx, id)
} }
func (s *Service) GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error) {
return s.walletStore.GetCompanyByWalletID(ctx, walletID)
}
func (s *Service) GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error) {
return s.walletStore.GetBranchByWalletID(ctx, walletID)
}
func (s *Service) GetAllCustomerWallet(ctx context.Context) ([]domain.GetCustomerWallet, error) { func (s *Service) GetAllCustomerWallet(ctx context.Context) ([]domain.GetCustomerWallet, error) {
return s.walletStore.GetAllCustomerWallets(ctx) return s.walletStore.GetAllCustomerWallets(ctx)
} }
@ -77,11 +90,16 @@ func (s *Service) UpdateBalance(ctx context.Context, id int64, balance domain.Cu
return err return err
} }
<<<<<<< HEAD
wallet, err := s.walletStore.GetWalletByID(ctx, id) wallet, err := s.walletStore.GetWalletByID(ctx, id)
=======
_, err = s.GetWalletByID(ctx, id)
>>>>>>> d43b12c589d32e4b6147cfb54a3b939c476bae6f
if err != nil { if err != nil {
return err return err
} }
<<<<<<< HEAD
go func() { go func() {
s.kafkaProducer.Publish(ctx, fmt.Sprint(wallet.ID), event.WalletEvent{ s.kafkaProducer.Publish(ctx, fmt.Sprint(wallet.ID), event.WalletEvent{
EventType: event.WalletBalanceUpdated, EventType: event.WalletBalanceUpdated,
@ -92,6 +110,9 @@ func (s *Service) UpdateBalance(ctx context.Context, id int64, balance domain.Cu
}) })
}() }()
=======
// go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet)
>>>>>>> d43b12c589d32e4b6147cfb54a3b939c476bae6f
return nil return nil
} }
@ -135,7 +156,7 @@ func (s *Service) AddToWallet(
return newTransfer, err return newTransfer, err
} }
func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.Currency, walletType domain.WalletType, cashierID domain.ValidInt64, paymentMethod domain.PaymentMethod, message string) (domain.Transfer, error) { func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.Currency, cashierID domain.ValidInt64, paymentMethod domain.PaymentMethod, message string) (domain.Transfer, error) {
wallet, err := s.GetWalletByID(ctx, id) wallet, err := s.GetWalletByID(ctx, id)
if err != nil { if err != nil {
return domain.Transfer{}, err return domain.Transfer{}, err
@ -143,12 +164,32 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.
if wallet.Balance < amount { if wallet.Balance < amount {
// Send Wallet low to admin // Send Wallet low to admin
if walletType == domain.CompanyWalletType || walletType == domain.BranchWalletType { if wallet.Type == domain.CompanyWalletType || wallet.Type == domain.BranchWalletType {
s.SendAdminWalletLowNotification(ctx, wallet, amount) s.SendAdminWalletInsufficientNotification(ctx, wallet, amount)
} else {
s.SendCustomerWalletInsufficientNotification(ctx, wallet, amount)
} }
return domain.Transfer{}, ErrBalanceInsufficient return domain.Transfer{}, ErrBalanceInsufficient
} }
if wallet.Type == domain.BranchWalletType || wallet.Type == domain.CompanyWalletType {
var thresholds []float32
if wallet.Type == domain.CompanyWalletType {
thresholds = []float32{100000, 50000, 25000, 10000, 5000, 3000, 1000, 500}
} else {
thresholds = []float32{5000, 3000, 1000, 500}
}
balance := wallet.Balance.Float32()
for _, threshold := range thresholds {
if balance < threshold {
s.SendAdminWalletLowNotification(ctx, wallet)
break // only send once per check
}
}
}
err = s.walletStore.UpdateBalance(ctx, id, wallet.Balance-amount) err = s.walletStore.UpdateBalance(ctx, id, wallet.Balance-amount)
if err != nil { if err != nil {
@ -223,46 +264,208 @@ func (s *Service) UpdateWalletActive(ctx context.Context, id int64, isActive boo
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 { func (s *Service) GetAdminNotificationRecipients(ctx context.Context, walletID int64, walletType domain.WalletType) ([]int64, error) {
var recipients []int64
if walletType == domain.BranchWalletType {
branch, err := s.GetBranchByWalletID(ctx, walletID)
if err != nil {
return nil, err
}
recipients = append(recipients, branch.BranchManagerID)
cashiers, err := s.userSvc.GetCashiersByBranch(ctx, branch.ID)
if err != nil {
return nil, err
}
for _, cashier := range cashiers {
recipients = append(recipients, cashier.ID)
}
admin, err := s.userSvc.GetAdminByCompanyID(ctx, branch.CompanyID)
if err != nil {
return nil, err
}
recipients = append(recipients, admin.ID)
} else if walletType == domain.CompanyWalletType {
company, err := s.GetCompanyByWalletID(ctx, walletID)
if err != nil {
return nil, err
}
recipients = append(recipients, company.AdminID)
} else {
return nil, fmt.Errorf("Invalid wallet type")
}
users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{
Role: string(domain.RoleSuperAdmin),
})
if err != nil {
return nil, err
}
for _, user := range users {
recipients = append(recipients, user.ID)
}
return recipients, nil
}
func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWallet domain.Wallet) error {
// Send notification to admin team // Send notification to admin team
adminNotification := &domain.Notification{ adminNotification := &domain.Notification{
RecipientID: adminWallet.UserID, RecipientID: adminWallet.UserID,
Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT, Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT,
Level: domain.NotificationLevelError, Level: domain.NotificationLevelWarning,
Reciever: domain.NotificationRecieverSideAdmin, Reciever: domain.NotificationRecieverSideAdmin,
DeliveryChannel: domain.DeliveryChannelEmail, // Or any preferred admin channel DeliveryChannel: domain.DeliveryChannelInApp, // Or any preferred admin channel
Payload: domain.NotificationPayload{ Payload: domain.NotificationPayload{
Headline: "CREDIT WARNING: System Running Out of Funds", Headline: "CREDIT WARNING: System Running Out of Funds",
Message: fmt.Sprintf( Message: fmt.Sprintf(
"Wallet ID %d has insufficient balance for transfer. Current balance: %.2f, Attempted transfer: %.2f", "Wallet ID %d is running low. Current balance: %.2f",
adminWallet.ID, adminWallet.ID,
adminWallet.Balance.Float32(), adminWallet.Balance.Float32(),
amount.Float32(),
), ),
}, },
Priority: 1, // High priority for admin alerts Priority: 1, // High priority for admin alerts
Metadata: fmt.Appendf(nil, `{ Metadata: fmt.Appendf(nil, `{
"wallet_id": %d, "wallet_id": %d,
"balance": %d, "balance": %d,
"required_amount": %d,
"notification_type": "admin_alert" "notification_type": "admin_alert"
}`, adminWallet.ID, adminWallet.Balance, amount), }`, adminWallet.ID, adminWallet.Balance),
} }
// Get admin recipients and send to all // Get admin recipients and send to all
adminRecipients, err := s.notificationStore.ListRecipientIDs(ctx, domain.NotificationRecieverSideAdmin) adminRecipients, err := s.GetAdminNotificationRecipients(ctx, adminWallet.ID, adminWallet.Type)
if err != nil { if err != nil {
s.logger.Error("failed to get admin recipients", "error", err) s.mongoLogger.Error("failed to get admin recipients",
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err return err
} else { }
for _, adminID := range adminRecipients {
adminNotification.RecipientID = adminID for _, adminID := range adminRecipients {
if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil { adminNotification.RecipientID = adminID
s.logger.Error("failed to send admin notification", if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil {
"admin_id", adminID, s.mongoLogger.Error("failed to send admin notification",
"error", err) zap.Int64("admin_id", adminID),
} zap.Error(err),
zap.Time("timestamp", time.Now()),
)
}
adminNotification.DeliveryChannel = domain.DeliveryChannelEmail
if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil {
s.mongoLogger.Error("failed to send email admin notification",
zap.Int64("admin_id", adminID),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
} }
} }
return nil return nil
} }
func (s *Service) SendAdminWalletInsufficientNotification(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.DeliveryChannelInApp, // Or any preferred admin channel
Payload: domain.NotificationPayload{
Headline: "CREDIT Error: Admin Wallet insufficient to process customer request",
Message: fmt.Sprintf(
"Wallet ID %d. Transaction Amount %.2f. Current balance: %.2f",
adminWallet.ID,
amount.Float32(),
adminWallet.Balance.Float32(),
),
},
Priority: 1, // High priority for admin alerts
Metadata: fmt.Appendf(nil, `{
"wallet_id": %d,
"balance": %d,
"transaction amount": %.2f,
"notification_type": "admin_alert"
}`, adminWallet.ID, adminWallet.Balance, amount.Float32()),
}
// Get admin recipients and send to all
recipients, err := s.GetAdminNotificationRecipients(ctx, adminWallet.ID, adminWallet.Type)
if err != nil {
s.mongoLogger.Error("failed to get admin recipients",
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
for _, adminID := range recipients {
adminNotification.RecipientID = adminID
if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil {
s.mongoLogger.Error("failed to send admin notification",
zap.Int64("admin_id", adminID),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
}
adminNotification.DeliveryChannel = domain.DeliveryChannelEmail
if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil {
s.mongoLogger.Error("failed to send email admin notification",
zap.Int64("admin_id", adminID),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
}
return nil
}
func (s *Service) SendCustomerWalletInsufficientNotification(ctx context.Context, customerWallet domain.Wallet, amount domain.Currency) error {
// Send notification to admin team
customerNotification := &domain.Notification{
RecipientID: customerWallet.UserID,
Type: domain.NOTIFICATION_TYPE_WALLET,
Level: domain.NotificationLevelError,
Reciever: domain.NotificationRecieverSideCustomer,
DeliveryChannel: domain.DeliveryChannelInApp, // Or any preferred admin channel
Payload: domain.NotificationPayload{
Headline: "CREDIT Error: Wallet insufficient",
Message: fmt.Sprintf(
"Wallet ID %d. Transaction Amount %.2f. Current balance: %.2f",
customerWallet.ID,
amount.Float32(),
customerWallet.Balance.Float32(),
),
},
Priority: 1, // High priority for admin alerts
Metadata: fmt.Appendf(nil, `{
"wallet_id": %d,
"balance": %d,
"transaction amount": %.2f,
"notification_type": "admin_alert"
}`, customerWallet.ID, customerWallet.Balance, amount.Float32()),
}
if err := s.notificationStore.SendNotification(ctx, customerNotification); err != nil {
s.mongoLogger.Error("failed to create customer notification",
zap.Int64("customer_id", customerWallet.UserID),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
return nil
}

View File

@ -34,7 +34,7 @@ import (
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
"go.uber.org/zap" "go.uber.org/zap"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification"
"github.com/bytedance/sonic" "github.com/bytedance/sonic"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/cors"

View File

@ -24,22 +24,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 1 hour (since its takes that long to fetch all the events) spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events)
// task: func() { task: func() {
// 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() {

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
@ -14,7 +15,7 @@ import (
// loginCustomerReq represents the request body for the LoginCustomer endpoint. // loginCustomerReq represents the request body for the LoginCustomer endpoint.
type loginCustomerReq struct { type loginCustomerReq struct {
Email string `json:"email" validate:"email" example:"john.doe@example.com"` Email string `json:"email" validate:"required_without=PhoneNumber" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"` PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
Password string `json:"password" validate:"required" example:"password123"` Password string `json:"password" validate:"required" example:"password123"`
} }
@ -37,7 +38,7 @@ type loginCustomerRes struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /api/v1/auth/login [post] // @Router /api/v1/auth/customer-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 {
@ -59,7 +60,6 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error {
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)
if err != nil { if err != nil {
switch { switch {
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound): case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials", h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials",
@ -89,6 +89,133 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error {
} }
} }
if successRes.Role != domain.RoleCustomer {
h.mongoLoggerSvc.Info("Login attempt: customer login of other role",
zap.Int("status_code", fiber.StatusForbidden),
zap.String("role", string(successRes.Role)),
zap.String("email", req.Email),
zap.String("phone", req.PhoneNumber),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusForbidden, "Only customers are allowed to login ")
}
accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, successRes.CompanyID, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry)
if err != nil {
h.mongoLoggerSvc.Error("Failed to create access token",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Int64("user_id", successRes.UserId),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token")
}
res := loginCustomerRes{
AccessToken: accessToken,
RefreshToken: successRes.RfToken,
Role: string(successRes.Role),
}
h.mongoLoggerSvc.Info("Login successful",
zap.Int("status_code", fiber.StatusOK),
zap.Int64("user_id", successRes.UserId),
zap.String("role", string(successRes.Role)),
zap.Time("timestamp", time.Now()),
)
return response.WriteJSON(c, fiber.StatusOK, "Login successful", res, nil)
}
// loginAdminReq represents the request body for the LoginAdmin endpoint.
type loginAdminReq struct {
Email string `json:"email" validate:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
Password string `json:"password" validate:"required" example:"password123"`
}
// loginAdminRes represents the response body for the LoginAdmin endpoint.
type loginAdminRes struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
Role string `json:"role"`
}
// LoginAdmin godoc
// @Summary Login customer
// @Description Login customer
// @Tags auth
// @Accept json
// @Produce json
// @Param login body loginAdminReq true "Login admin"
// @Success 200 {object} loginAdminRes
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/auth/admin-login [post]
func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
var req loginAdminReq
if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse LoginAdmin request",
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body"+err.Error())
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
return fiber.NewError(fiber.StatusBadRequest, errMsg)
}
successRes, err := h.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password)
if err != nil {
switch {
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials",
zap.Int("status_code", fiber.StatusUnauthorized),
zap.String("email", req.Email),
zap.String("phone", req.PhoneNumber),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusUnauthorized, "Invalid credentials")
case errors.Is(err, authentication.ErrUserSuspended):
h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked",
zap.Int("status_code", fiber.StatusUnauthorized),
zap.String("email", req.Email),
zap.String("phone", req.PhoneNumber),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusUnauthorized, "User login has been locked")
default:
h.mongoLoggerSvc.Error("Login failed",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Internal server error")
}
}
if successRes.Role == domain.RoleCustomer {
h.mongoLoggerSvc.Warn("Login attempt: admin login of customer",
zap.Int("status_code", fiber.StatusForbidden),
zap.String("role", string(successRes.Role)),
zap.String("email", req.Email),
zap.String("phone", req.PhoneNumber),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusForbidden, "Only admin roles are allowed")
}
accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, successRes.CompanyID, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry) accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, successRes.CompanyID, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry)
if err != nil { if err != nil {
h.mongoLoggerSvc.Error("Failed to create access token", h.mongoLoggerSvc.Error("Failed to create access token",

View File

@ -45,6 +45,7 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error {
zap.Int("status_code", fiber.StatusInternalServerError), zap.Int("status_code", fiber.StatusInternalServerError),
zap.Int64("user_id", userID), zap.Int64("user_id", userID),
zap.String("role", string(role)), zap.String("role", string(role)),
zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create bet:"+err.Error()) return fiber.NewError(fiber.StatusInternalServerError, "Failed to create bet:"+err.Error())
@ -97,6 +98,15 @@ func (h *Handler) CreateBetWithFastCode(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "failed to get bet with fast code:"+err.Error()) return fiber.NewError(fiber.StatusBadRequest, "failed to get bet with fast code:"+err.Error())
} }
if bet.UserID == userID {
h.mongoLoggerSvc.Info("User cannot refer himself",
zap.Int64("bet_id", bet.ID),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Time("timestamp", time.Now()),
zap.Error(err),
)
return fiber.NewError(fiber.StatusBadRequest, "User cannot use his own referral code")
}
outcomes, err := h.betSvc.GetBetOutcomeByBetID(c.Context(), bet.ID) outcomes, err := h.betSvc.GetBetOutcomeByBetID(c.Context(), bet.ID)
if err != nil { if err != nil {
h.mongoLoggerSvc.Info("failed to get BetOutcomes by BetID", h.mongoLoggerSvc.Info("failed to get BetOutcomes by BetID",
@ -118,7 +128,7 @@ func (h *Handler) CreateBetWithFastCode(c *fiber.Ctx) error {
} }
// This can be for both online and offline bets // This can be for both online and offline bets
// If bet is an online bet (if the customer role creates the bet on their own) // If bet is an online bet (if the customer role creates the bet on their own)
// then the branchID is null // then the branchID is null
newReq := domain.CreateBetReq{ newReq := domain.CreateBetReq{
Amount: req.Amount, Amount: req.Amount,
@ -190,7 +200,7 @@ func (h *Handler) CreateBetInternal(c *fiber.Ctx, req domain.CreateBetReq, userI
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
return domain.CreateBetRes{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) return domain.CreateBetRes{}, err
} }
h.mongoLoggerSvc.Error("PlaceBet failed", h.mongoLoggerSvc.Error("PlaceBet failed",
@ -202,7 +212,7 @@ func (h *Handler) CreateBetInternal(c *fiber.Ctx, req domain.CreateBetReq, userI
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
return domain.CreateBetRes{}, fiber.NewError(fiber.StatusInternalServerError, "Unable to create bet") return domain.CreateBetRes{}, err
} }
return res, nil return res, nil
@ -490,6 +500,42 @@ func (h *Handler) GetBetByID(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil) return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil)
} }
// GetBetByFastCode godoc
// @Summary Gets bet by fast_code
// @Description Gets a single bet by fast_code
// @Tags bet
// @Accept json
// @Produce json
// @Param fast_code path int true "Bet ID"
// @Success 200 {object} domain.BetRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/sport/bet/fastcode/{fast_code} [get]
func (h *Handler) GetBetByFastCode(c *fiber.Ctx) error {
fastCode := c.Params("fast_code")
bet, err := h.betSvc.GetBetByFastCode(c.Context(), fastCode)
if err != nil {
h.mongoLoggerSvc.Info("Failed to get bet by fast code",
zap.String("fast_code", fastCode),
zap.Int("status_code", fiber.StatusNotFound),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusNotFound, "Failed to find bet by fast code")
}
res := domain.ConvertBet(bet)
// h.mongoLoggerSvc.Info("Bet retrieved successfully",
// zap.Int64("betID", id),
// zap.Int("status_code", fiber.StatusOK),
// zap.Time("timestamp", time.Now()),
// )
return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil)
}
type UpdateCashOutReq struct { type UpdateCashOutReq struct {
CashedOut bool CashedOut bool
} }

View File

@ -99,12 +99,13 @@ func (h *Handler) CreateBranch(c *fiber.Ctx) error {
} }
branch, err := h.branchSvc.CreateBranch(c.Context(), domain.CreateBranch{ branch, err := h.branchSvc.CreateBranch(c.Context(), domain.CreateBranch{
Name: req.Name, Name: req.Name,
Location: req.Location, Location: req.Location,
WalletID: newWallet.ID, WalletID: newWallet.ID,
BranchManagerID: req.BranchManagerID, BranchManagerID: req.BranchManagerID,
CompanyID: checkedCompanyID, CompanyID: checkedCompanyID,
IsSelfOwned: IsSelfOwned, IsSelfOwned: IsSelfOwned,
ProfitPercentage: req.ProfitPercentage,
}) })
if err != nil { if err != nil {
@ -619,6 +620,38 @@ func (h *Handler) GetBranchOperations(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusOK, "Branch Operations retrieved successfully", result, nil) return response.WriteJSON(c, fiber.StatusOK, "Branch Operations retrieved successfully", result, nil)
} }
// GetAllBranchLocations godoc
// @Summary Gets all branch locations
// @Description Gets all branch locations
// @Tags branch
// @Accept json
// @Produce json
// @Success 200 {array} domain.BranchLocation
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/branchLocation [get]
func (h *Handler) GetAllBranchLocations(c *fiber.Ctx) error {
searchQuery := c.Query("query")
searchString := domain.ValidString{
Value: searchQuery,
Valid: searchQuery != "",
}
locations, err := h.branchSvc.GetAllBranchLocations(c.Context(), searchString)
if err != nil {
h.mongoLoggerSvc.Error("Failed to get branch locations",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return response.WriteJSON(c, fiber.StatusOK, "Branch Location successfully fetched", locations, nil)
}
// GetBranchCashiers godoc // GetBranchCashiers godoc
// @Summary Gets branch cashiers // @Summary Gets branch cashiers
// @Description Gets branch cashiers // @Description Gets branch cashiers

View File

@ -50,6 +50,7 @@ func (h *Handler) CreateCompany(c *fiber.Ctx) error {
user, err := h.userSvc.GetUserByID(c.Context(), req.AdminID) user, err := h.userSvc.GetUserByID(c.Context(), req.AdminID)
if err != nil { if err != nil {
h.mongoLoggerSvc.Error("Error fetching user", h.mongoLoggerSvc.Error("Error fetching user",
zap.Int("admin_id", int(req.AdminID)),
zap.Int("status_code", fiber.StatusInternalServerError), zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
@ -76,9 +77,10 @@ func (h *Handler) CreateCompany(c *fiber.Ctx) error {
} }
company, err := h.companySvc.CreateCompany(c.Context(), domain.CreateCompany{ company, err := h.companySvc.CreateCompany(c.Context(), domain.CreateCompany{
Name: req.Name, Name: req.Name,
AdminID: user.ID, AdminID: user.ID,
WalletID: newWallet.ID, WalletID: newWallet.ID,
DeductedPercentage: req.DeductedPercentage,
}) })
if err != nil { if err != nil {

View File

@ -74,6 +74,13 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error {
Valid: true, Valid: true,
} }
} }
searchQuery := c.Query("query")
searchString := domain.ValidString{
Value: searchQuery,
Valid: searchQuery != "",
}
firstStartTimeQuery := c.Query("first_start_time") firstStartTimeQuery := c.Query("first_start_time")
var firstStartTime domain.ValidTime var firstStartTime domain.ValidTime
if firstStartTimeQuery != "" { if firstStartTimeQuery != "" {
@ -98,7 +105,7 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error {
if lastStartTimeQuery != "" { if lastStartTimeQuery != "" {
lastStartTimeParsed, err := time.Parse(time.RFC3339, lastStartTimeQuery) lastStartTimeParsed, err := time.Parse(time.RFC3339, lastStartTimeQuery)
if err != nil { if err != nil {
h.mongoLoggerSvc.Info("invalid start_time format", h.mongoLoggerSvc.Info("invalid last_start_time format",
zap.String("last_start_time", lastStartTimeQuery), zap.String("last_start_time", lastStartTimeQuery),
zap.Int("status_code", fiber.StatusBadRequest), zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err), zap.Error(err),
@ -118,12 +125,12 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error {
Valid: countryCodeQuery != "", Valid: countryCodeQuery != "",
} }
flaggedQuery := c.Query("flagged") isFeaturedQuery := c.Query("is_featured")
var flagged domain.ValidBool var isFeatured domain.ValidBool
if flaggedQuery != "" { if isFeaturedQuery != "" {
flaggedParsed, err := strconv.ParseBool(flaggedQuery) isFeaturedParsed, err := strconv.ParseBool(isFeaturedQuery)
if err != nil { if err != nil {
h.mongoLoggerSvc.Error("Failed to parse flagged", h.mongoLoggerSvc.Error("Failed to parse isFeatured",
zap.Int("status_code", fiber.StatusBadRequest), zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
@ -131,8 +138,8 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Failed to parse is_shop_bet") return fiber.NewError(fiber.StatusBadRequest, "Failed to parse is_shop_bet")
} }
flagged = domain.ValidBool{ isFeatured = domain.ValidBool{
Value: flaggedParsed, Value: isFeaturedParsed,
Valid: true, Valid: true,
} }
} }
@ -141,12 +148,13 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error {
c.Context(), domain.EventFilter{ c.Context(), domain.EventFilter{
SportID: sportID, SportID: sportID,
LeagueID: leagueID, LeagueID: leagueID,
Query: searchString,
FirstStartTime: firstStartTime, FirstStartTime: firstStartTime,
LastStartTime: lastStartTime, LastStartTime: lastStartTime,
Limit: limit, Limit: limit,
Offset: offset, Offset: offset,
CountryCode: countryCode, CountryCode: countryCode,
Flagged: flagged, Featured: isFeatured,
}) })
// fmt.Printf("League ID: %v", leagueID) // fmt.Printf("League ID: %v", leagueID)
@ -299,13 +307,13 @@ func (h *Handler) SetEventStatusToRemoved(c *fiber.Ctx) error {
} }
type UpdateEventFlaggedReq struct { type UpdateEventFeaturedReq struct {
Flagged bool `json:"flagged" example:"true"` Featured bool `json:"is_featured" example:"true"`
} }
// UpdateEventFlagged godoc // UpdateEventFeatured godoc
// @Summary update the event flagged // @Summary update the event featured
// @Description Update the event flagged // @Description Update the event featured
// @Tags event // @Tags event
// @Accept json // @Accept json
// @Produce json // @Produce json
@ -314,10 +322,10 @@ type UpdateEventFlaggedReq struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /api/v1/events/{id}/flag [put] // @Router /api/v1/events/{id}/flag [put]
func (h *Handler) UpdateEventFlagged(c *fiber.Ctx) error { func (h *Handler) UpdateEventFeatured(c *fiber.Ctx) error {
eventID := c.Params("id") eventID := c.Params("id")
var req UpdateEventFlaggedReq var req UpdateEventFeaturedReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse user id", h.mongoLoggerSvc.Info("Failed to parse user id",
@ -335,17 +343,17 @@ func (h *Handler) UpdateEventFlagged(c *fiber.Ctx) error {
for field, msg := range valErrs { for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg) errMsg += fmt.Sprintf("%s: %s; ", field, msg)
} }
h.mongoLoggerSvc.Error("Failed to update event flagged", h.mongoLoggerSvc.Error("Failed to update event featured",
zap.Any("request", req), zap.Any("request", req),
zap.Int("status_code", fiber.StatusInternalServerError), zap.Int("status_code", fiber.StatusInternalServerError),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
return fiber.NewError(fiber.StatusBadRequest, errMsg) return fiber.NewError(fiber.StatusBadRequest, errMsg)
} }
err := h.eventSvc.UpdateFlagged(c.Context(), eventID, req.Flagged) err := h.eventSvc.UpdateFeatured(c.Context(), eventID, req.Featured)
if err != nil { if err != nil {
h.mongoLoggerSvc.Error("Failed to update event flagged", h.mongoLoggerSvc.Error("Failed to update event featured",
zap.String("eventID", eventID), zap.String("eventID", eventID),
zap.Int("status_code", fiber.StatusInternalServerError), zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err), zap.Error(err),

View File

@ -16,7 +16,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions"
issuereporting "github.com/SamuelTariku/FortuneBet-Backend/internal/services/issue_reporting" issuereporting "github.com/SamuelTariku/FortuneBet-Backend/internal/services/issue_reporting"
"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" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation"
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"

View File

@ -115,7 +115,23 @@ func (h *Handler) GetAllIssues(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get all issues:"+err.Error()) return fiber.NewError(fiber.StatusInternalServerError, "Failed to get all issues:"+err.Error())
} }
return c.JSON(issues) results := make([]domain.ReportedIssue, len(issues))
for i, issue := range issues {
results[i] = domain.ReportedIssue{
ID: issue.ID,
UserID: issue.UserID,
UserRole: domain.Role(issue.UserRole),
Subject: issue.Subject,
Description: issue.Description,
IssueType: domain.ReportedIssueType(issue.IssueType),
Status: domain.ReportedIssueStatus(issue.Status),
// Metadata: issue.Metadata,
CreatedAt: issue.CreatedAt.Time,
UpdatedAt: issue.UpdatedAt.Time,
}
}
return c.JSON(results)
} }
// UpdateIssueStatus godoc // UpdateIssueStatus godoc

View File

@ -140,7 +140,7 @@ func (h *Handler) SetLeagueActive(c *fiber.Ctx) error {
if err := h.leagueSvc.SetLeagueActive(c.Context(), int64(leagueId), req.IsActive); err != nil { if err := h.leagueSvc.SetLeagueActive(c.Context(), int64(leagueId), req.IsActive); err != nil {
h.mongoLoggerSvc.Error("Failed to update league active", h.mongoLoggerSvc.Error("Failed to update league active",
zap.Int64("userID", int64(leagueId)), zap.Int64("leagueID", int64(leagueId)),
zap.Bool("is_active", req.IsActive), zap.Bool("is_active", req.IsActive),
zap.Int("status_code", fiber.StatusInternalServerError), zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err), zap.Error(err),
@ -149,6 +149,14 @@ func (h *Handler) SetLeagueActive(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update league:"+err.Error()) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update league:"+err.Error())
} }
h.mongoLoggerSvc.Info("League Active has been successfully updated",
zap.Int64("userID", int64(leagueId)),
zap.Int64("leagueID", int64(leagueId)),
zap.Bool("is_active", req.IsActive),
zap.Int("status_code", fiber.StatusOK),
zap.Time("timestamp", time.Now()),
)
return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil) return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil)
} }
@ -206,6 +214,10 @@ func (h *Handler) SetLeagueFeatured(c *fiber.Ctx) error {
} }
err = h.leagueSvc.UpdateLeague(c.Context(), domain.UpdateLeague{ err = h.leagueSvc.UpdateLeague(c.Context(), domain.UpdateLeague{
ID: int64(leagueId), ID: int64(leagueId),
IsFeatured: domain.ValidBool{
Value: req.IsFeatured,
Valid: true,
},
}) })
if err != nil { if err != nil {
h.mongoLoggerSvc.Error("Failed to update league", h.mongoLoggerSvc.Error("Failed to update league",
@ -216,6 +228,12 @@ func (h *Handler) SetLeagueFeatured(c *fiber.Ctx) error {
) )
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update league:"+err.Error()) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update league:"+err.Error())
} }
h.mongoLoggerSvc.Info("League Featured has been successfully updated",
zap.Int64("userID", int64(leagueId)),
zap.Int64("leagueID", int64(leagueId)),
zap.Bool("is_featured", req.IsFeatured),
zap.Int("status_code", fiber.StatusOK),
zap.Time("timestamp", time.Now()),
)
return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil) return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil)
} }

View File

@ -99,7 +99,7 @@ func GetLogsHandler(appCtx context.Context) fiber.Handler {
} }
defer cursor.Close(appCtx) defer cursor.Close(appCtx)
var logs []domain.LogEntry var logs []domain.LogEntry = make([]domain.LogEntry, 0)
if err := cursor.All(appCtx, &logs); err != nil { if err := cursor.All(appCtx, &logs); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Cursor decoding error: "+err.Error()) return fiber.NewError(fiber.StatusInternalServerError, "Cursor decoding error: "+err.Error())
} }
@ -113,7 +113,7 @@ func GetLogsHandler(appCtx context.Context) fiber.Handler {
// Prepare response // Prepare response
response := domain.LogResponse{ response := domain.LogResponse{
Message: "Logs fetched successfully", Message: "Logs fetched successfully",
Data: logs, Data: logs,
Pagination: domain.Pagination{ Pagination: domain.Pagination{
Total: int(total), Total: int(total),
TotalPages: totalPages, TotalPages: totalPages,

View File

@ -117,7 +117,7 @@ func (h *Handler) ConnectSocket(c *fiber.Ctx) error {
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
} else { } else {
h.mongoLoggerSvc.Warn("Unexpected WebSocket closure", h.mongoLoggerSvc.Info("Unexpected WebSocket closure",
zap.Int64("userID", userID), zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusBadRequest), zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err), zap.Error(err),

View File

@ -35,11 +35,13 @@ import (
// @Failure 500 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/reports/dashboard [get] // @Router /api/v1/reports/dashboard [get]
func (h *Handler) GetDashboardReport(c *fiber.Ctx) error { func (h *Handler) GetDashboardReport(c *fiber.Ctx) error {
role := c.Locals("role").(domain.Role)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() defer cancel()
// Parse query parameters // Parse query parameters
filter, err := parseReportFilter(c) filter, err := parseReportFilter(c, role)
if err != nil { if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid filter parameters", Message: "Invalid filter parameters",
@ -70,24 +72,30 @@ func (h *Handler) GetDashboardReport(c *fiber.Ctx) error {
} }
// parseReportFilter parses query parameters into ReportFilter // parseReportFilter parses query parameters into ReportFilter
func parseReportFilter(c *fiber.Ctx) (domain.ReportFilter, error) { func parseReportFilter(c *fiber.Ctx, role domain.Role) (domain.ReportFilter, error) {
var filter domain.ReportFilter var filter domain.ReportFilter
var err error var err error
if c.Query("company_id") != "" { if c.Query("company_id") != "" && role == domain.RoleSuperAdmin {
companyID, err := strconv.ParseInt(c.Query("company_id"), 10, 64) companyID, err := strconv.ParseInt(c.Query("company_id"), 10, 64)
if err != nil { if err != nil {
return domain.ReportFilter{}, fmt.Errorf("invalid company_id: %w", err) return domain.ReportFilter{}, fmt.Errorf("invalid company_id: %w", err)
} }
filter.CompanyID = domain.ValidInt64{Value: companyID, Valid: true} filter.CompanyID = domain.ValidInt64{Value: companyID, Valid: true}
} else {
filter.CompanyID = c.Locals("company_id").(domain.ValidInt64)
} }
if c.Query("branch_id") != "" { if c.Query("branch_id") != "" && role == domain.RoleSuperAdmin {
branchID, err := strconv.ParseInt(c.Query("branch_id"), 10, 64) branchID, err := strconv.ParseInt(c.Query("branch_id"), 10, 64)
if err != nil { if err != nil {
return domain.ReportFilter{}, fmt.Errorf("invalid branch_id: %w", err) return domain.ReportFilter{}, fmt.Errorf("invalid branch_id: %w", err)
} }
filter.BranchID = domain.ValidInt64{Value: branchID, Valid: true} filter.BranchID = domain.ValidInt64{Value: branchID, Valid: true}
} else {
filter.BranchID = c.Locals("branch_id").(domain.ValidInt64)
} }
if c.Query("user_id") != "" { if c.Query("user_id") != "" {

View File

@ -116,6 +116,89 @@ func (h *Handler) GetShopBetByBetID(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusOK, "Shop bet fetched successfully", res, nil) return response.WriteJSON(c, fiber.StatusOK, "Shop bet fetched successfully", res, nil)
} }
// GetAllShopBets godoc
// @Summary Gets all shop bets
// @Description Gets all the shop bets
// @Tags bet
// @Accept json
// @Produce json
// @Success 200 {array} domain.ShopBetRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/shop/bet [get]
func (h *Handler) GetAllShopBets(c *fiber.Ctx) error {
// role := c.Locals("role").(domain.Role)
companyID := c.Locals("company_id").(domain.ValidInt64)
branchID := c.Locals("branch_id").(domain.ValidInt64)
searchQuery := c.Query("query")
searchString := domain.ValidString{
Value: searchQuery,
Valid: searchQuery != "",
}
createdBeforeQuery := c.Query("created_before")
var createdBefore domain.ValidTime
if createdBeforeQuery != "" {
createdBeforeParsed, err := time.Parse(time.RFC3339, createdBeforeQuery)
if err != nil {
h.mongoLoggerSvc.Info("invalid created_before format",
zap.String("time", createdBeforeQuery),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid created_before format")
}
createdBefore = domain.ValidTime{
Value: createdBeforeParsed,
Valid: true,
}
}
createdAfterQuery := c.Query("created_after")
var createdAfter domain.ValidTime
if createdAfterQuery != "" {
createdAfterParsed, err := time.Parse(time.RFC3339, createdAfterQuery)
if err != nil {
h.mongoLoggerSvc.Info("invalid created_after format",
zap.String("created_after", createdAfterQuery),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid created_after format")
}
createdAfter = domain.ValidTime{
Value: createdAfterParsed,
Valid: true,
}
}
bets, err := h.transactionSvc.GetAllShopBet(c.Context(), domain.ShopBetFilter{
Query: searchString,
CreatedBefore: createdBefore,
CreatedAfter: createdAfter,
CompanyID: companyID,
BranchID: branchID,
})
if err != nil {
h.mongoLoggerSvc.Error("Failed to get all bets",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve bets፡"+err.Error())
}
res := make([]domain.ShopBetRes, len(bets))
for i, bet := range bets {
res[i] = domain.ConvertShopBetDetail(bet)
}
return response.WriteJSON(c, fiber.StatusOK, "All bets retrieved successfully", res, nil)
}
// CashoutBet godoc // CashoutBet godoc
// @Summary Cashout bet at branch // @Summary Cashout bet at branch
// @Description Cashout bet at branch // @Description Cashout bet at branch

View File

@ -120,7 +120,7 @@ func (h *Handler) SendRegisterCode(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided") return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided")
} }
if err := h.userSvc.SendRegisterCode(c.Context(), medium, sentTo, "twilio"); err != nil { if err := h.userSvc.SendRegisterCode(c.Context(), medium, sentTo, domain.AfroMessage); err != nil {
h.mongoLoggerSvc.Error("Failed to send register code", h.mongoLoggerSvc.Error("Failed to send register code",
zap.String("Medium", string(medium)), zap.String("Medium", string(medium)),
zap.String("Send To", string(sentTo)), zap.String("Send To", string(sentTo)),
@ -248,7 +248,7 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error {
// TODO: Remove later // TODO: Remove later
_, err = h.walletSvc.AddToWallet( _, err = h.walletSvc.AddToWallet(
c.Context(), newWallet.RegularID, domain.ToCurrency(10000.0), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, c.Context(), newWallet.RegularID, domain.ToCurrency(10000.0), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{},
"Added 100.0 to wallet only as test for deployment") "Added 10000.0 to wallet only as test for deployment")
if err != nil { if err != nil {
h.mongoLoggerSvc.Error("Failed to update wallet for user", h.mongoLoggerSvc.Error("Failed to update wallet for user",
@ -318,7 +318,7 @@ func (h *Handler) SendResetCode(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided") return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided")
} }
if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo, "twilio"); err != nil { if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo, domain.AfroMessage); err != nil {
h.mongoLoggerSvc.Error("Failed to send reset code", h.mongoLoggerSvc.Error("Failed to send reset code",
zap.String("medium", string(medium)), zap.String("medium", string(medium)),
zap.String("sentTo", string(sentTo)), zap.String("sentTo", string(sentTo)),
@ -417,20 +417,121 @@ type UserProfileRes struct {
LastLogin time.Time `json:"last_login"` LastLogin time.Time `json:"last_login"`
SuspendedAt time.Time `json:"suspended_at"` SuspendedAt time.Time `json:"suspended_at"`
Suspended bool `json:"suspended"` Suspended bool `json:"suspended"`
ReferralCode string `json:"referral_code"`
} }
// UserProfile godoc type CustomerProfileRes struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
PhoneNumber string `json:"phone_number"`
Role domain.Role `json:"role"`
EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastLogin time.Time `json:"last_login"`
SuspendedAt time.Time `json:"suspended_at"`
Suspended bool `json:"suspended"`
ReferralCode string `json:"referral_code"`
}
// CustomerProfile godoc
// @Summary Get user profile // @Summary Get user profile
// @Description Get user profile // @Description Get user profile
// @Tags user // @Tags user
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} UserProfileRes // @Success 200 {object} CustomerProfileRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Security Bearer // @Security Bearer
// @Router /api/v1/user/profile [get] // @Router /api/v1/user/customer-profile [get]
func (h *Handler) UserProfile(c *fiber.Ctx) error { func (h *Handler) CustomerProfile(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 {
h.mongoLoggerSvc.Error("Invalid user ID in context",
zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification")
}
user, err := h.userSvc.GetUserByID(c.Context(), userID)
if err != nil {
h.mongoLoggerSvc.Error("Failed to get user profile",
zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user profile:"+err.Error())
}
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
if err != nil {
if err != authentication.ErrRefreshTokenNotFound {
h.mongoLoggerSvc.Error("Failed to get user last login",
zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login:"+err.Error())
}
lastLogin = &user.CreatedAt
}
res := CustomerProfileRes{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Role: user.Role,
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
SuspendedAt: user.SuspendedAt,
Suspended: user.Suspended,
LastLogin: *lastLogin,
}
return response.WriteJSON(c, fiber.StatusOK, "User profile retrieved successfully", res, nil)
}
type AdminProfileRes struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
PhoneNumber string `json:"phone_number"`
Role domain.Role `json:"role"`
EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastLogin time.Time `json:"last_login"`
SuspendedAt time.Time `json:"suspended_at"`
Suspended bool `json:"suspended"`
}
// AdminProfile godoc
// @Summary Get user profile
// @Description Get user profile
// @Tags user
// @Accept json
// @Produce json
// @Success 200 {object} AdminProfileRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Security Bearer
// @Router /api/v1/user/admin-profile [get]
func (h *Handler) AdminProfile(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(int64) userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 { if !ok || userID == 0 {

View File

@ -56,7 +56,7 @@ func (a *App) initAppRoutes() {
a.fiber.Get("/", func(c *fiber.Ctx) error { a.fiber.Get("/", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{ return c.JSON(fiber.Map{
"message": "Welcome to the FortuneBet API", "message": "Welcome to the FortuneBet API",
"version": "1.0dev10", "version": "1.0dev11",
}) })
}) })
@ -67,12 +67,13 @@ func (a *App) initAppRoutes() {
groupV1.Get("/", func(c *fiber.Ctx) error { groupV1.Get("/", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{ return c.JSON(fiber.Map{
"message": "FortuneBet API V1 pre-alpha", "message": "FortuneBet API V1 pre-alpha",
"version": "1.0dev10", "version": "1.0dev11",
}) })
}) })
// Auth Routes // Auth Routes
groupV1.Post("/auth/login", h.LoginCustomer) groupV1.Post("/auth/customer-login", h.LoginCustomer)
groupV1.Post("/auth/admin-login", h.LoginAdmin)
groupV1.Post("/auth/refresh", h.RefreshToken) groupV1.Post("/auth/refresh", h.RefreshToken)
groupV1.Post("/auth/logout", a.authMiddleware, h.LogOutCustomer) groupV1.Post("/auth/logout", a.authMiddleware, h.LogOutCustomer)
groupV1.Get("/auth/test", a.authMiddleware, func(c *fiber.Ctx) error { groupV1.Get("/auth/test", a.authMiddleware, func(c *fiber.Ctx) error {
@ -113,7 +114,8 @@ func (a *App) initAppRoutes() {
groupV1.Post("/user/register", h.RegisterUser) groupV1.Post("/user/register", h.RegisterUser)
groupV1.Post("/user/sendRegisterCode", h.SendRegisterCode) groupV1.Post("/user/sendRegisterCode", h.SendRegisterCode)
groupV1.Post("/user/checkPhoneEmailExist", h.CheckPhoneEmailExist) groupV1.Post("/user/checkPhoneEmailExist", h.CheckPhoneEmailExist)
groupV1.Get("/user/profile", a.authMiddleware, h.UserProfile) groupV1.Get("/user/customer-profile", a.authMiddleware, h.CustomerProfile)
groupV1.Get("/user/admin-profile", a.authMiddleware, h.AdminProfile)
groupV1.Get("/user/single/:id", a.authMiddleware, h.GetUserByID) groupV1.Get("/user/single/:id", a.authMiddleware, h.GetUserByID)
groupV1.Delete("/user/delete/:id", a.authMiddleware, h.DeleteUser) groupV1.Delete("/user/delete/:id", a.authMiddleware, h.DeleteUser)
groupV1.Post("/user/suspend", a.authMiddleware, h.UpdateUserSuspend) groupV1.Post("/user/suspend", a.authMiddleware, h.UpdateUserSuspend)
@ -162,7 +164,7 @@ func (a *App) initAppRoutes() {
groupV1.Get("/events/:id", h.GetUpcomingEventByID) groupV1.Get("/events/:id", h.GetUpcomingEventByID)
groupV1.Delete("/events/:id", a.authMiddleware, a.SuperAdminOnly, h.SetEventStatusToRemoved) groupV1.Delete("/events/:id", a.authMiddleware, a.SuperAdminOnly, h.SetEventStatusToRemoved)
groupV1.Get("/top-leagues", h.GetTopLeagues) groupV1.Get("/top-leagues", h.GetTopLeagues)
groupV1.Get("/events/:id/flag", h.UpdateEventFlagged) groupV1.Put("/events/:id/featured", h.UpdateEventFeatured)
// Leagues // Leagues
groupV1.Get("/leagues", h.GetAllLeagues) groupV1.Get("/leagues", h.GetAllLeagues)
@ -180,9 +182,11 @@ func (a *App) initAppRoutes() {
groupV1.Put("/branch/:id/set-active", a.authMiddleware, h.UpdateBranchStatus) groupV1.Put("/branch/:id/set-active", a.authMiddleware, h.UpdateBranchStatus)
groupV1.Put("/branch/:id/set-inactive", a.authMiddleware, h.UpdateBranchStatus) groupV1.Put("/branch/:id/set-inactive", a.authMiddleware, h.UpdateBranchStatus)
groupV1.Delete("/branch/:id", a.authMiddleware, h.DeleteBranch) groupV1.Delete("/branch/:id", a.authMiddleware, h.DeleteBranch)
groupV1.Get("/search/branch", a.authMiddleware, h.SearchBranch) groupV1.Get("/search/branch", a.authMiddleware, h.SearchBranch)
// /branch/search
// branch/wallet groupV1.Get("/branchLocation", a.authMiddleware, h.GetAllBranchLocations)
groupV1.Get("/branch/:id/cashiers", a.authMiddleware, h.GetBranchCashiers) groupV1.Get("/branch/:id/cashiers", a.authMiddleware, h.GetBranchCashiers)
groupV1.Get("/branchCashier", a.authMiddleware, h.GetBranchForCashier) groupV1.Get("/branchCashier", a.authMiddleware, h.GetBranchForCashier)
@ -212,6 +216,7 @@ func (a *App) initAppRoutes() {
// Bet Routes // Bet Routes
groupV1.Post("/sport/bet", a.authMiddleware, h.CreateBet) groupV1.Post("/sport/bet", a.authMiddleware, h.CreateBet)
groupV1.Post("/sport/bet/fastcode", a.authMiddleware, h.CreateBetWithFastCode) groupV1.Post("/sport/bet/fastcode", a.authMiddleware, h.CreateBetWithFastCode)
groupV1.Get("/sport/bet/fastcode/:fast_code", h.GetBetByFastCode)
groupV1.Get("/sport/bet", a.authMiddleware, h.GetAllBet) groupV1.Get("/sport/bet", a.authMiddleware, h.GetAllBet)
groupV1.Get("/sport/bet/:id", h.GetBetByID) groupV1.Get("/sport/bet/:id", h.GetBetByID)
groupV1.Patch("/sport/bet/:id", a.authMiddleware, h.UpdateCashOut) groupV1.Patch("/sport/bet/:id", a.authMiddleware, h.UpdateCashOut)
@ -245,7 +250,7 @@ func (a *App) initAppRoutes() {
groupV1.Get("/currencies/convert", h.ConvertCurrency) groupV1.Get("/currencies/convert", h.ConvertCurrency)
//Report Routes //Report Routes
groupV1.Get("/reports/dashboard", h.GetDashboardReport) groupV1.Get("/reports/dashboard", a.authMiddleware, a.OnlyAdminAndAbove, h.GetDashboardReport)
groupV1.Get("/report-files/download/:filename", a.authMiddleware, a.OnlyAdminAndAbove, h.DownloadReportFile) groupV1.Get("/report-files/download/:filename", a.authMiddleware, a.OnlyAdminAndAbove, h.DownloadReportFile)
groupV1.Get("/report-files/list", a.authMiddleware, a.OnlyAdminAndAbove, h.ListReportFiles) groupV1.Get("/report-files/list", a.authMiddleware, a.OnlyAdminAndAbove, h.ListReportFiles)
@ -269,6 +274,7 @@ func (a *App) initAppRoutes() {
// Transactions /shop/transactions // Transactions /shop/transactions
groupV1.Post("/shop/bet", a.authMiddleware, a.CompanyOnly, h.CreateShopBet) groupV1.Post("/shop/bet", a.authMiddleware, a.CompanyOnly, h.CreateShopBet)
groupV1.Get("/shop/bet", a.authMiddleware, a.CompanyOnly, h.GetAllShopBets)
groupV1.Get("/shop/bet/:id", a.authMiddleware, a.CompanyOnly, h.GetShopBetByBetID) groupV1.Get("/shop/bet/:id", a.authMiddleware, a.CompanyOnly, h.GetShopBetByBetID)
groupV1.Post("/shop/bet/:id/cashout", a.authMiddleware, a.CompanyOnly, h.CashoutBet) groupV1.Post("/shop/bet/:id/cashout", a.authMiddleware, a.CompanyOnly, h.CashoutBet)
groupV1.Post("/shop/bet/:id/generate", a.authMiddleware, a.CompanyOnly, h.CashoutBet) groupV1.Post("/shop/bet/:id/generate", a.authMiddleware, a.CompanyOnly, h.CashoutBet)