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/
app_logs/
backup/
reports/

View File

@ -7,5 +7,12 @@
],
"cSpell.enabledFileTypes": {
"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"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/kafka"
"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/recommendation"
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
@ -104,13 +105,15 @@ func main() {
// Initialize services
settingSvc := settings.NewService(store)
messengerSvc := messenger.NewService(settingSvc, cfg)
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)
oddsSvc := odds.New(store, cfg, logger)
notificationRepo := repository.NewNotificationRepository(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 userStore user.UserStore
@ -121,6 +124,8 @@ func main() {
notificatioStore,
// userStore,
notificationSvc,
userSvc,
domain.MongoDBLogger,
logger,
kafka.NewProducer([]string{"localhost:9092"}, "wallet-events"),
)
@ -129,7 +134,7 @@ func main() {
companySvc := company.NewService(store)
leagueSvc := league.New(store)
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)
bonusSvc := bonus.NewService(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 teams;
DROP TABLE IF EXISTS settings;
DROP TABLE IF EXISTS bonus;
DROP TABLE IF EXISTS flags;
-- DELETE FROM wallet_transfer;

View File

@ -133,6 +133,7 @@ CREATE TABLE IF NOT EXISTS wallets (
is_bettable BOOLEAN NOT NULL,
is_transferable BOOLEAN NOT NULL,
user_id BIGINT NOT NULL,
type VARCHAR(255) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
created_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,
name VARCHAR(255) NOT NULL,
location TEXT NOT NULL,
profit_percent REAL NOt NULL,
profit_percent REAL NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT false,
wallet_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,
created_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 (
id BIGSERIAL PRIMARY KEY,
@ -258,7 +263,8 @@ CREATE TABLE events (
status TEXT,
fetched_at TIMESTAMP DEFAULT now(),
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 (
id SERIAL PRIMARY KEY,
@ -289,7 +295,11 @@ CREATE TABLE companies (
deducted_percentage REAL NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT false,
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 (
id BIGINT PRIMARY KEY,
@ -319,6 +329,25 @@ CREATE TABLE bonus (
multiplier REAL NOT NULL,
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
CREATE VIEW companies_details AS
SELECT companies.*,

View File

@ -1,11 +1,22 @@
-- Settings Initial Data
INSERT INTO settings (key, value)
<<<<<<< HEAD
VALUES
('max_number_of_outcomes', '30'),
=======
VALUES ('sms_provider', '30'),
('max_number_of_outcomes', '30'),
>>>>>>> d43b12c589d32e4b6147cfb54a3b939c476bae6f
('bet_amount_limit', '100000'),
('daily_ticket_limit', '50'),
('total_winnings_limit', '1000000'),
('amount_for_bet_referral', '1000000'),
<<<<<<< HEAD
('cashback_amount_cap', '1000')
ON CONFLICT (key)
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'),
('negele_borana', 'Negele Borana'),
('alaba_kulito', 'Alaba Kulito'),
('alamata 14,', 'Alamata 14,'),
('030', '030'),
('alamata,', 'Alamata,'),
('chiro', 'Chiro'),
('tepi', 'Tepi'),
('durame', 'Durame'),

View File

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

View File

@ -5,9 +5,10 @@ INSERT INTO branches (
wallet_id,
branch_manager_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 *;
-- name: CreateSupportedOperation :one
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),
is_self_owned = COALESCE(sqlc.narg(is_self_owned), is_self_owned),
is_active = COALESCE(sqlc.narg(is_active), is_active),
profit_percent = COALESCE(sqlc.narg(profit_percent), profit_percent),
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
RETURNING *;

View File

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

@ -11,25 +11,3 @@ VALUES ($1, $2, CURRENT_TIMESTAMP) ON CONFLICT (key) DO
UPDATE
SET value = EXCLUDED.value
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

@ -193,3 +193,8 @@ WHERE (
email = $2
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_bettable,
is_transferable,
user_id
user_id,
type
)
VALUES ($1, $2, $3, $4)
VALUES ($1, $2, $3, $4, $5)
RETURNING *;
-- name: CreateCustomerWallet :one
INSERT INTO customer_wallets (

View File

@ -282,20 +282,33 @@ func (q *Queries) GetBetByUserID(ctx context.Context, userID int64) ([]BetWithOu
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(*)
FROM bets
WHERE user_id = $1
AND outcomes_hash = $2
`
type GetBetCountParams struct {
type GetBetCountByUserIDParams struct {
UserID int64 `json:"user_id"`
OutcomesHash string `json:"outcomes_hash"`
}
func (q *Queries) GetBetCount(ctx context.Context, arg GetBetCountParams) (int64, error) {
row := q.db.QueryRow(ctx, GetBetCount, arg.UserID, arg.OutcomesHash)
func (q *Queries) GetBetCountByUserID(ctx context.Context, arg GetBetCountByUserIDParams) (int64, error) {
row := q.db.QueryRow(ctx, GetBetCountByUserID, arg.UserID, arg.OutcomesHash)
var count int64
err := row.Scan(&count)
return count, err
@ -397,6 +410,19 @@ func (q *Queries) GetBetOutcomeByEventID(ctx context.Context, arg GetBetOutcomeB
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
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

View File

@ -18,9 +18,10 @@ INSERT INTO branches (
wallet_id,
branch_manager_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
`
@ -31,6 +32,7 @@ type CreateBranchParams struct {
BranchManagerID int64 `json:"branch_manager_id"`
CompanyID int64 `json:"company_id"`
IsSelfOwned bool `json:"is_self_owned"`
ProfitPercent float32 `json:"profit_percent"`
}
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.CompanyID,
arg.IsSelfOwned,
arg.ProfitPercent,
)
var i Branch
err := row.Scan(
@ -498,6 +501,7 @@ SET name = COALESCE($2, name),
company_id = COALESCE($5, company_id),
is_self_owned = COALESCE($6, is_self_owned),
is_active = COALESCE($7, is_active),
profit_percent = COALESCE($8, profit_percent),
updated_at = CURRENT_TIMESTAMP
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
@ -511,6 +515,7 @@ type UpdateBranchParams struct {
CompanyID pgtype.Int8 `json:"company_id"`
IsSelfOwned pgtype.Bool `json:"is_self_owned"`
IsActive pgtype.Bool `json:"is_active"`
ProfitPercent pgtype.Float4 `json:"profit_percent"`
}
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.IsSelfOwned,
arg.IsActive,
arg.ProfitPercent,
)
var i Branch
err := row.Scan(

View File

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

View File

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

View File

@ -61,6 +61,15 @@ type BetFilter struct {
CreatedAfter ValidTime
}
type Flag struct {
ID int64
BetID int64
OddID int64
Reason string
FlaggedAt time.Time
Resolved bool
}
type GetBet struct {
ID int64
Amount Currency
@ -95,7 +104,7 @@ type CreateBetOutcomeReq struct {
type CreateBetReq struct {
Outcomes []CreateBetOutcomeReq `json:"outcomes" validate:"required"`
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 {
@ -104,6 +113,12 @@ type CreateBetWithFastCodeReq struct {
BranchID *int64 `json:"branch_id"`
}
type CreateFlagReq struct {
BetID int64
OddID int64
Reason string
}
type RandomBetReq struct {
BranchID int64 `json:"branch_id" validate:"required" example:"1"`
NumberOfBets int64 `json:"number_of_bets" validate:"required" example:"1"`
@ -117,6 +132,7 @@ type CreateBetRes struct {
UserID int64 `json:"user_id" example:"2"`
IsShopBet bool `json:"is_shop_bet" example:"false"`
CreatedNumber int64 `json:"created_number" example:"2"`
FastCode string `json:"fast_code"`
}
type BetRes struct {
ID int64 `json:"id" example:"1"`
@ -140,6 +156,8 @@ func ConvertCreateBet(bet Bet, createdNumber int64) CreateBetRes {
Status: bet.Status,
UserID: bet.UserID,
CreatedNumber: createdNumber,
IsShopBet: bet.IsShopBet,
FastCode: bet.FastCode,
}
}

View File

@ -1,5 +1,10 @@
package domain
import (
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/jackc/pgx/v5/pgtype"
)
type Branch struct {
ID int64
Name string
@ -9,6 +14,12 @@ type Branch struct {
CompanyID int64
IsActive 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 {
@ -33,6 +44,7 @@ type BranchDetail struct {
ManagerName string
ManagerPhoneNumber string
WalletIsActive bool
ProfitPercentage float32
}
type SupportedOperation struct {
@ -54,6 +66,7 @@ type CreateBranch struct {
BranchManagerID int64
CompanyID int64
IsSelfOwned bool
ProfitPercentage float32
}
type UpdateBranch struct {
@ -64,6 +77,7 @@ type UpdateBranch struct {
CompanyID *int64
IsSelfOwned *bool
IsActive *bool
ProfitPercentage *float32
}
type CreateSupportedOperation struct {
@ -79,6 +93,7 @@ type CreateBranchReq struct {
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"`
BranchManagerID int64 `json:"branch_manager_id" validate:"required,gt=0" example:"1"`
ProfitPercentage float32 `json:"profit_percentage" example:"0.1" validate:"lt=1" `
CompanyID *int64 `json:"company_id,omitempty" example:"1"`
IsSelfOwned *bool `json:"is_self_owned,omitempty" example:"false"`
Operations []int64 `json:"operations" validate:"required,dive,gt=0"`
@ -91,6 +106,7 @@ type UpdateBranchReq struct {
CompanyID *int64 `json:"company_id,omitempty" example:"1"`
IsSelfOwned *bool `json:"is_self_owned,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 {
@ -123,6 +139,7 @@ type BranchRes struct {
CompanyID int64 `json:"company_id" example:"1"`
IsSelfOwned bool `json:"is_self_owned" example:"false"`
IsActive bool `json:"is_active" example:"false"`
ProfitPercentage float32 `json:"profit_percentage" example:"0.1"`
}
type BranchDetailRes struct {
@ -138,6 +155,7 @@ type BranchDetailRes struct {
Balance float32 `json:"balance" example:"100.5"`
IsActive bool `json:"is_active" example:"false"`
WalletIsActive bool `json:"is_wallet_active" example:"false"`
ProfitPercentage float32 `json:"profit_percentage" example:"0.1"`
}
func ConvertBranch(branch Branch) BranchRes {
@ -150,6 +168,7 @@ func ConvertBranch(branch Branch) BranchRes {
CompanyID: branch.CompanyID,
IsSelfOwned: branch.IsSelfOwned,
IsActive: branch.IsActive,
ProfitPercentage: branch.ProfitPercentage,
}
}
@ -167,5 +186,103 @@ func ConvertBranchDetail(branch BranchDetail) BranchDetailRes {
Balance: branch.Balance.Float32(),
IsActive: branch.IsActive,
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

@ -56,6 +56,7 @@ type UpdateCompany struct {
type CreateCompanyReq struct {
Name string `json:"name" example:"CompanyName"`
AdminID int64 `json:"admin_id" example:"1"`
DeductedPercentage float32 `json:"deducted_percentage" example:"0.1" validate:"lt=1"`
}
type UpdateCompanyReq struct {
Name *string `json:"name,omitempty" example:"CompanyName"`
@ -111,6 +112,7 @@ func ConvertGetCompany(company GetCompany) GetCompanyRes {
AdminFirstName: company.AdminFirstName,
AdminLastName: company.AdminLastName,
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
Source string `json:"source"` // bet api provider (bet365, betfair)
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 {
EventID string
@ -120,6 +121,7 @@ type Odds struct {
}
type EventFilter struct {
Query ValidString
SportID ValidInt32
LeagueID ValidInt32
CountryCode ValidString
@ -128,5 +130,5 @@ type EventFilter struct {
Limit ValidInt64
Offset ValidInt64
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_EVENTS ReportedIssueType = "events"
ISSUE_TYPE_BRANCH ReportedIssueType = "branch"
ISSUE_TYPE_USER ReportedIssueType = "branch"
ISSUE_TYPE_USER ReportedIssueType = "user"
ISSUE_TYPE_LOGIN ReportedIssueType = "login"
ISSUE_TYPE_REGISTER ReportedIssueType = "register"
ISSUE_TYPE_RESET_PASSWORD ReportedIssueType = "reset_password"

View File

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

View File

@ -17,6 +17,7 @@ type SettingRes struct {
}
type SettingList struct {
SMSProvider SMSProvider `json:"sms_provider"`
MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"`
BetAmountLimit Currency `json:"bet_amount_limit"`
DailyTicketPerIP int64 `json:"daily_ticket_limit"`
@ -26,6 +27,7 @@ type SettingList struct {
}
type DBSettingList struct {
SMSProvider ValidString
MaxNumberOfOutcomes ValidInt64
BetAmountLimit 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 {
return SettingList{
SMSProvider: SMSProvider(dbSettingList.SMSProvider.Value),
MaxNumberOfOutcomes: dbSettingList.MaxNumberOfOutcomes.Value,
BetAmountLimit: Currency(dbSettingList.BetAmountLimit.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
IsActive bool
UserID int64
Type WalletType
UpdatedAt time.Time
CreatedAt time.Time
}
@ -63,6 +64,7 @@ type CreateWallet struct {
IsBettable bool
IsTransferable bool
UserID int64
Type WalletType
}
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 {
return dbgen.CreateBetOutcomeParams{
BetID: betOutcome.BetID,
@ -140,6 +151,35 @@ func (s *Store) CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBe
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) {
bet, err := s.queries.GetBetByID(ctx, id)
if err != nil {
@ -237,8 +277,8 @@ func (s *Store) GetBetsForCashback(ctx context.Context) ([]domain.GetBet, error)
return res, nil
}
func (s *Store) GetBetCount(ctx context.Context, UserID int64, outcomesHash string) (int64, error) {
count, err := s.queries.GetBetCount(ctx, dbgen.GetBetCountParams{
func (s *Store) GetBetCountByUserID(ctx context.Context, UserID int64, outcomesHash string) (int64, error) {
count, err := s.queries.GetBetCountByUserID(ctx, dbgen.GetBetCountByUserIDParams{
UserID: UserID,
OutcomesHash: outcomesHash,
})
@ -250,6 +290,24 @@ func (s *Store) GetBetCount(ctx context.Context, UserID int64, outcomesHash stri
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 {
err := s.queries.UpdateCashOut(ctx, dbgen.UpdateCashOutParams{
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)
}
domain.MongoDBLogger.Info("GetBetSummary executed successfully",
zap.String("query", query),
zap.Any("args", args),
zap.Float64("totalStakes", float64(totalStakes)), // convert if needed
zap.Int64("totalBets", totalBets),
zap.Int64("activeBets", activeBets),
zap.Int64("totalWins", totalWins),
zap.Int64("totalLosses", totalLosses),
zap.Float64("winBalance", float64(winBalance)), // convert if needed
)
// domain.MongoDBLogger.Info("GetBetSummary executed successfully",
// zap.String("query", query),
// zap.Any("args", args),
// zap.Float64("totalStakes", float64(totalStakes)), // convert if needed
// zap.Int64("totalBets", totalBets),
// zap.Int64("activeBets", activeBets),
// zap.Int64("totalWins", totalWins),
// zap.Int64("totalLosses", totalLosses),
// zap.Float64("winBalance", float64(winBalance)), // convert if needed
// )
return totalStakes, totalBets, activeBets, totalWins, totalLosses, winBalance, nil
}

View File

@ -9,100 +9,15 @@ import (
"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) {
dbBranch, err := s.queries.CreateBranch(ctx, convertCreateBranch(branch))
dbBranch, err := s.queries.CreateBranch(ctx, domain.ConvertCreateBranch(branch))
if err != nil {
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) {
@ -110,7 +25,7 @@ func (s *Store) GetBranchByID(ctx context.Context, id int64) (domain.BranchDetai
if err != nil {
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) {
@ -120,7 +35,7 @@ func (s *Store) GetBranchByManagerID(ctx context.Context, branchManagerID int64)
}
var branches []domain.BranchDetail = make([]domain.BranchDetail, 0, len(dbBranches))
for _, dbBranch := range dbBranches {
branches = append(branches, convertDBBranchDetail(dbBranch))
branches = append(branches, domain.ConvertDBBranchDetail(dbBranch))
}
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))
for _, dbBranch := range dbBranches {
branches = append(branches, convertDBBranchDetail(dbBranch))
branches = append(branches, domain.ConvertDBBranchDetail(dbBranch))
}
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))
for _, dbBranch := range dbBranches {
branches = append(branches, convertDBBranchDetail(dbBranch))
branches = append(branches, domain.ConvertDBBranchDetail(dbBranch))
}
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))
for _, dbBranch := range dbBranches {
branches = append(branches, convertDBBranchDetail(dbBranch))
branches = append(branches, domain.ConvertDBBranchDetail(dbBranch))
}
return branches, nil
}
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 {
return domain.Branch{}, err
}
return convertDBBranch(dbBranch), nil
return domain.ConvertDBBranch(dbBranch), nil
}
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 convertDBBranch(branch), err
return domain.ConvertDBBranch(branch), err
}
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(),
Source: e.Source.String,
Status: domain.EventStatus(e.Status.String),
Flagged: e.Flagged,
IsFeatured: e.IsFeatured,
}
}
return upcomingEvents, nil
@ -122,7 +122,8 @@ func (s *Store) GetExpiredUpcomingEvents(ctx context.Context, filter domain.Even
StartTime: e.StartTime.Time.UTC(),
Source: e.Source.String,
Status: domain.EventStatus(e.Status.String),
Flagged: e.Flagged,
IsFeatured: e.IsFeatured,
IsActive: e.IsActive,
}
}
return upcomingEvents, nil
@ -139,6 +140,10 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.Ev
Int32: int32(filter.SportID.Value),
Valid: filter.SportID.Valid,
},
Query: pgtype.Text{
String: filter.Query.Value,
Valid: filter.Query.Valid,
},
Limit: pgtype.Int4{
Int32: int32(filter.Limit.Value),
Valid: filter.Limit.Valid,
@ -159,9 +164,9 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.Ev
String: filter.CountryCode.Value,
Valid: filter.CountryCode.Valid,
},
Flagged: pgtype.Bool{
Bool: filter.Flagged.Valid,
Valid: filter.Flagged.Valid,
IsFeatured: pgtype.Bool{
Bool: filter.Featured.Valid,
Valid: filter.Featured.Valid,
},
})
@ -186,7 +191,8 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.Ev
StartTime: e.StartTime.Time.UTC(),
Source: e.Source.String,
Status: domain.EventStatus(e.Status.String),
Flagged: e.Flagged,
IsFeatured: e.IsFeatured,
IsActive: e.IsActive,
}
}
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),
Valid: filter.SportID.Valid,
},
Query: pgtype.Text{
String: filter.Query.Value,
Valid: filter.Query.Valid,
},
FirstStartTime: pgtype.Timestamp{
Time: filter.FirstStartTime.Value.UTC(),
Valid: filter.FirstStartTime.Valid,
@ -210,9 +220,9 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.Ev
String: filter.CountryCode.Value,
Valid: filter.CountryCode.Valid,
},
Flagged: pgtype.Bool{
Bool: filter.Flagged.Valid,
Valid: filter.Flagged.Valid,
IsFeatured: pgtype.Bool{
Bool: filter.Featured.Valid,
Valid: filter.Featured.Valid,
},
})
if err != nil {
@ -244,7 +254,7 @@ func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.Upc
StartTime: event.StartTime.Time.UTC(),
Source: event.Source.String,
Status: domain.EventStatus(event.Status.String),
Flagged: event.Flagged,
IsFeatured: event.IsFeatured,
}, nil
}
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 {
return s.queries.UpdateFlagged(ctx, dbgen.UpdateFlaggedParams{
func (s *Store) UpdateFeatured(ctx context.Context, eventID string, isFeatured bool) error {
return s.queries.UpdateFeatured(ctx, dbgen.UpdateFeaturedParams{
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{
Bool: league.IsFeatured.Value,
Valid: league.IsActive.Valid,
Valid: league.IsFeatured.Valid,
},
SportID: pgtype.Int4{
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
}
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) {
// dbNotifications, err := s.queries.GetAllNotifications(ctx, dbgen.GetAllNotificationsParams{

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"strconv"
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
@ -14,6 +15,10 @@ func GetDBSettingList(settings []dbgen.Setting) (domain.SettingList, error) {
var dbSettingList domain.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 {
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 {
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
}
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
func (s *Store) GetCustomerCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) {
query := `SELECT

View File

@ -17,6 +17,7 @@ func convertDBWallet(wallet dbgen.Wallet) domain.Wallet {
IsTransferable: wallet.IsTransferable,
IsActive: wallet.IsActive,
UserID: wallet.UserID,
Type: domain.WalletType(wallet.Type),
UpdatedAt: wallet.UpdatedAt.Time,
CreatedAt: wallet.CreatedAt.Time,
}
@ -28,6 +29,7 @@ func convertCreateWallet(wallet domain.CreateWallet) dbgen.CreateWalletParams {
IsBettable: wallet.IsBettable,
IsTransferable: wallet.IsTransferable,
UserID: wallet.UserID,
Type: string(wallet.Type),
}
}
@ -183,6 +185,40 @@ func (s *Store) UpdateWalletActive(ctx context.Context, id int64, isActive bool)
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
func (s *Store) GetBalanceSummary(ctx context.Context, filter domain.ReportFilter) (domain.BalanceSummary, error) {
var summary domain.BalanceSummary
@ -275,4 +311,3 @@ func (s *Store) GetTotalWallets(ctx context.Context, filter domain.ReportFilter)
return total, nil
}

View File

@ -10,13 +10,16 @@ import (
type BetStore interface {
CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, 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)
GetAllBets(ctx context.Context, filter domain.BetFilter) ([]domain.GetBet, error)
GetBetByUserID(ctx context.Context, UserID int64) ([]domain.GetBet, error)
GetBetByFastCode(ctx context.Context, fastcode string) (domain.GetBet, error)
GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]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
UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) 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/company"
"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/settings"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"go.uber.org/zap"
)
@ -54,6 +55,7 @@ type Service struct {
branchSvc branch.Service
companySvc company.Service
settingSvc settings.Service
userSvc user.Service
notificationSvc *notificationservice.Service
logger *slog.Logger
mongoLogger *zap.Logger
@ -67,6 +69,7 @@ func NewService(
branchSvc branch.Service,
companySvc company.Service,
settingSvc settings.Service,
userSvc user.Service,
notificationSvc *notificationservice.Service,
logger *slog.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) {
settingsList, err := s.settingSvc.GetSettingList(ctx)
if err != nil {
return domain.CreateBetRes{}, err
}
if req.Amount < 1 {
return domain.CreateBetRes{}, ErrInvalidAmount
}
@ -269,7 +275,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
return domain.CreateBetRes{}, err
}
count, err := s.GetBetCount(ctx, userID, outcomesHash)
count, err := s.GetBetCountByUserID(ctx, userID, outcomesHash)
if err != nil {
s.mongoLogger.Error("failed to generate cashout ID",
zap.Int64("user_id", userID),
@ -398,6 +404,79 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
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)
return res, nil
@ -417,7 +496,7 @@ func (s *Service) DeductBetFromBranchWallet(ctx context.Context, amount float32,
deductedAmount := amount * company.DeductedPercentage
_, err = s.walletSvc.DeductFromWallet(ctx,
walletID, domain.ToCurrency(deductedAmount), domain.BranchWalletType, domain.ValidInt64{
walletID, domain.ToCurrency(deductedAmount), domain.ValidInt64{
Value: userID,
Valid: true,
}, domain.TRANSFER_DIRECT,
@ -446,7 +525,7 @@ func (s *Service) DeductBetFromCustomerWallet(ctx context.Context, amount float3
}
if amount < wallets.RegularBalance.Float32() {
_, 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))
if err != nil {
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
_, 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()))
if err != nil {
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
remainingAmount := wallets.RegularBalance - domain.Currency(amount)
_, 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()))
if err != nil {
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
}
count, err := s.GetBetCount(ctx, userID, outcomesHash)
count, err := s.GetBetCountByUserID(ctx, userID, outcomesHash)
if err != nil {
s.mongoLogger.Error("failed to get bet count",
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)
}
func (s *Service) GetBetCount(ctx context.Context, UserID int64, outcomesHash string) (int64, error) {
return s.betStore.GetBetCount(ctx, UserID, outcomesHash)
func (s *Service) GetBetCountByUserID(ctx context.Context, UserID int64, outcomesHash string) (int64, error) {
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 {
@ -817,10 +900,19 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc
return err
}
if bet.IsShopBet ||
status == domain.OUTCOME_STATUS_ERROR ||
status == domain.OUTCOME_STATUS_PENDING ||
status == domain.OUTCOME_STATUS_LOSS {
switch {
case bet.IsShopBet:
return s.betStore.UpdateStatus(ctx, id, status)
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)
}
@ -837,10 +929,15 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc
switch status {
case domain.OUTCOME_STATUS_WIN:
amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds)
s.SendWinningStatusNotification(ctx, status, bet.UserID, amount, "")
case domain.OUTCOME_STATUS_HALF:
amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds) / 2
default:
s.SendWinningStatusNotification(ctx, status, bet.UserID, amount, "")
case domain.OUTCOME_STATUS_VOID:
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{},
@ -858,6 +955,207 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc
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) {
betOutcomes, err := s.betStore.GetBetOutcomeByBetID(ctx, betID)
if err != nil {
@ -1033,7 +1331,7 @@ func (s *Service) ProcessBetCashback(ctx context.Context) error {
)
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,
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)
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) {
return s.branchStore.GetBranchesByCompany(ctx, companyID)
}

View File

@ -16,5 +16,5 @@ type Service interface {
// GetAndStoreMatchResult(ctx context.Context, eventID string) error
UpdateFinalScore(ctx context.Context, eventID, fullScore 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)
}
func (s *service) UpdateFlagged(ctx context.Context, eventID string, flagged bool) error {
return s.store.UpdateFlagged(ctx, eventID, flagged)
func (s *service) UpdateFeatured(ctx context.Context, eventID string, flagged bool) error {
return s.store.UpdateFeatured(ctx, eventID, flagged)
}
// 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 {
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
MarkAsRead(ctx context.Context, notificationID string, recipientID int64) 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/event"
"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"
)

View File

@ -9,7 +9,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"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/settings"
"go.uber.org/zap"

View File

@ -2,40 +2,36 @@ package user
import (
"context"
"errors"
"fmt"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"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"
)
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()
message := fmt.Sprintf("Welcome to Fortune bets, your OTP is %s please don't share with anyone.", otpCode)
switch medium {
case domain.OtpMediumSms:
switch provider {
case "twilio":
if err := s.SendTwilioSMSOTP(ctx, sentTo, message, provider); err != nil {
case domain.TwilioSms:
if err := s.messengerSvc.SendTwilioSMS(ctx, sentTo, message); err != nil {
return err
}
case "afromessage":
if err := s.SendAfroMessageSMSOTP(ctx, sentTo, message, provider); err != nil {
case domain.AfroMessage:
if err := s.messengerSvc.SendAfroMessageSMS(ctx, sentTo, message); err != nil {
return err
}
default:
return fmt.Errorf("invalid sms provider: %s", provider)
}
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
}
}
@ -61,73 +57,3 @@ func hashPassword(plaintextPassword string) ([]byte, error) {
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)
}
func (s *Service) GetAllUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error) {
// Get all Users
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)
}
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)
}

View File

@ -14,6 +14,7 @@ type UserStore interface {
GetAllCashiers(ctx context.Context, filter domain.UserFilter) ([]domain.GetCashier, int64, error)
GetCashierByID(ctx context.Context, cashierID int64) (domain.GetCashier, 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
UpdateUserCompany(ctx context.Context, id int64, companyID int64) 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
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
// check if user exists
switch medium {

View File

@ -8,7 +8,7 @@ import (
"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
// check if user exists

View File

@ -4,6 +4,7 @@ import (
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/messenger"
)
const (
@ -13,17 +14,20 @@ const (
type Service struct {
userStore UserStore
otpStore OtpStore
messengerSvc *messenger.Service
config *config.Config
}
func NewService(
userStore UserStore,
otpStore OtpStore,
messengerSvc *messenger.Service,
cfg *config.Config,
) *Service {
return &Service{
userStore: userStore,
otpStore: otpStore,
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")
}
_, 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))
if err != nil {
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
}
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),
)

View File

@ -10,7 +10,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"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"
)

View File

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

View File

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

View File

@ -4,9 +4,14 @@ import (
"context"
"errors"
"fmt"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
<<<<<<< HEAD
"github.com/SamuelTariku/FortuneBet-Backend/internal/event"
=======
"go.uber.org/zap"
>>>>>>> d43b12c589d32e4b6147cfb54a3b939c476bae6f
)
var (
@ -60,6 +65,14 @@ func (s *Service) GetWalletsByUser(ctx context.Context, id int64) ([]domain.Wall
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) {
return s.walletStore.GetAllCustomerWallets(ctx)
}
@ -77,11 +90,16 @@ func (s *Service) UpdateBalance(ctx context.Context, id int64, balance domain.Cu
return err
}
<<<<<<< HEAD
wallet, err := s.walletStore.GetWalletByID(ctx, id)
=======
_, err = s.GetWalletByID(ctx, id)
>>>>>>> d43b12c589d32e4b6147cfb54a3b939c476bae6f
if err != nil {
return err
}
<<<<<<< HEAD
go func() {
s.kafkaProducer.Publish(ctx, fmt.Sprint(wallet.ID), event.WalletEvent{
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
}
@ -135,7 +156,7 @@ func (s *Service) AddToWallet(
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)
if err != nil {
return domain.Transfer{}, err
@ -143,12 +164,32 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.
if wallet.Balance < amount {
// Send Wallet low to admin
if walletType == domain.CompanyWalletType || walletType == domain.BranchWalletType {
s.SendAdminWalletLowNotification(ctx, wallet, amount)
if wallet.Type == domain.CompanyWalletType || wallet.Type == domain.BranchWalletType {
s.SendAdminWalletInsufficientNotification(ctx, wallet, amount)
} else {
s.SendCustomerWalletInsufficientNotification(ctx, wallet, amount)
}
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)
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)
}
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
adminNotification := &domain.Notification{
RecipientID: adminWallet.UserID,
Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT,
Level: domain.NotificationLevelError,
Level: domain.NotificationLevelWarning,
Reciever: domain.NotificationRecieverSideAdmin,
DeliveryChannel: domain.DeliveryChannelEmail, // Or any preferred admin channel
DeliveryChannel: domain.DeliveryChannelInApp, // Or any preferred admin channel
Payload: domain.NotificationPayload{
Headline: "CREDIT WARNING: System Running Out of Funds",
Message: fmt.Sprintf(
"Wallet ID %d has insufficient balance for transfer. Current balance: %.2f, Attempted transfer: %.2f",
"Wallet ID %d is running low. Current balance: %.2f",
adminWallet.ID,
adminWallet.Balance.Float32(),
amount.Float32(),
),
},
Priority: 1, // High priority for admin alerts
Metadata: fmt.Appendf(nil, `{
"wallet_id": %d,
"balance": %d,
"required_amount": %d,
"notification_type": "admin_alert"
}`, adminWallet.ID, adminWallet.Balance, amount),
}`, adminWallet.ID, adminWallet.Balance),
}
// 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 {
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
} else {
}
for _, adminID := range adminRecipients {
adminNotification.RecipientID = adminID
if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil {
s.logger.Error("failed to send admin notification",
"admin_id", adminID,
"error", err)
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) 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"
"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/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"

View File

@ -24,22 +24,22 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
spec string
task func()
}{
// {
// spec: "0 0 * * * *", // Every 1 hour
// task: func() {
// if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
// log.Printf("FetchUpcomingEvents error: %v", err)
// }
// },
// },
// {
// spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events)
// task: func() {
// if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
// log.Printf("FetchNonLiveOdds error: %v", err)
// }
// },
// },
{
spec: "0 0 * * * *", // Every 1 hour
task: func() {
if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
log.Printf("FetchUpcomingEvents error: %v", err)
}
},
},
{
spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events)
task: func() {
if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
log.Printf("FetchNonLiveOdds error: %v", err)
}
},
},
{
spec: "0 */5 * * * *", // Every 5 Minutes
task: func() {

View File

@ -5,6 +5,7 @@ import (
"fmt"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
@ -14,7 +15,7 @@ import (
// loginCustomerReq represents the request body for the LoginCustomer endpoint.
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"`
Password string `json:"password" validate:"required" example:"password123"`
}
@ -37,7 +38,7 @@ type loginCustomerRes struct {
// @Failure 400 {object} response.APIResponse
// @Failure 401 {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 {
var req loginCustomerReq
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)
if err != nil {
switch {
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
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)
if err != nil {
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.Int64("user_id", userID),
zap.String("role", string(role)),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
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())
}
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)
if err != nil {
h.mongoLoggerSvc.Info("failed to get BetOutcomes by BetID",
@ -190,7 +200,7 @@ func (h *Handler) CreateBetInternal(c *fiber.Ctx, req domain.CreateBetReq, userI
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return domain.CreateBetRes{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
return domain.CreateBetRes{}, err
}
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()),
)
return domain.CreateBetRes{}, fiber.NewError(fiber.StatusInternalServerError, "Unable to create bet")
return domain.CreateBetRes{}, err
}
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)
}
// 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 {
CashedOut bool
}

View File

@ -105,6 +105,7 @@ func (h *Handler) CreateBranch(c *fiber.Ctx) error {
BranchManagerID: req.BranchManagerID,
CompanyID: checkedCompanyID,
IsSelfOwned: IsSelfOwned,
ProfitPercentage: req.ProfitPercentage,
})
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)
}
// 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
// @Summary 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)
if err != nil {
h.mongoLoggerSvc.Error("Error fetching user",
zap.Int("admin_id", int(req.AdminID)),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
@ -79,6 +80,7 @@ func (h *Handler) CreateCompany(c *fiber.Ctx) error {
Name: req.Name,
AdminID: user.ID,
WalletID: newWallet.ID,
DeductedPercentage: req.DeductedPercentage,
})
if err != nil {

View File

@ -74,6 +74,13 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error {
Valid: true,
}
}
searchQuery := c.Query("query")
searchString := domain.ValidString{
Value: searchQuery,
Valid: searchQuery != "",
}
firstStartTimeQuery := c.Query("first_start_time")
var firstStartTime domain.ValidTime
if firstStartTimeQuery != "" {
@ -98,7 +105,7 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error {
if lastStartTimeQuery != "" {
lastStartTimeParsed, err := time.Parse(time.RFC3339, lastStartTimeQuery)
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.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
@ -118,12 +125,12 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error {
Valid: countryCodeQuery != "",
}
flaggedQuery := c.Query("flagged")
var flagged domain.ValidBool
if flaggedQuery != "" {
flaggedParsed, err := strconv.ParseBool(flaggedQuery)
isFeaturedQuery := c.Query("is_featured")
var isFeatured domain.ValidBool
if isFeaturedQuery != "" {
isFeaturedParsed, err := strconv.ParseBool(isFeaturedQuery)
if err != nil {
h.mongoLoggerSvc.Error("Failed to parse flagged",
h.mongoLoggerSvc.Error("Failed to parse isFeatured",
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
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")
}
flagged = domain.ValidBool{
Value: flaggedParsed,
isFeatured = domain.ValidBool{
Value: isFeaturedParsed,
Valid: true,
}
}
@ -141,12 +148,13 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error {
c.Context(), domain.EventFilter{
SportID: sportID,
LeagueID: leagueID,
Query: searchString,
FirstStartTime: firstStartTime,
LastStartTime: lastStartTime,
Limit: limit,
Offset: offset,
CountryCode: countryCode,
Flagged: flagged,
Featured: isFeatured,
})
// fmt.Printf("League ID: %v", leagueID)
@ -299,13 +307,13 @@ func (h *Handler) SetEventStatusToRemoved(c *fiber.Ctx) error {
}
type UpdateEventFlaggedReq struct {
Flagged bool `json:"flagged" example:"true"`
type UpdateEventFeaturedReq struct {
Featured bool `json:"is_featured" example:"true"`
}
// UpdateEventFlagged godoc
// @Summary update the event flagged
// @Description Update the event flagged
// UpdateEventFeatured godoc
// @Summary update the event featured
// @Description Update the event featured
// @Tags event
// @Accept json
// @Produce json
@ -314,10 +322,10 @@ type UpdateEventFlaggedReq struct {
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @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")
var req UpdateEventFlaggedReq
var req UpdateEventFeaturedReq
if err := c.BodyParser(&req); err != nil {
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 {
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.Int("status_code", fiber.StatusInternalServerError),
zap.Time("timestamp", time.Now()),
)
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 {
h.mongoLoggerSvc.Error("Failed to update event flagged",
h.mongoLoggerSvc.Error("Failed to update event featured",
zap.String("eventID", eventID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),

View File

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

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 {
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.Int("status_code", fiber.StatusInternalServerError),
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())
}
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)
}
@ -206,6 +214,10 @@ func (h *Handler) SetLeagueFeatured(c *fiber.Ctx) error {
}
err = h.leagueSvc.UpdateLeague(c.Context(), domain.UpdateLeague{
ID: int64(leagueId),
IsFeatured: domain.ValidBool{
Value: req.IsFeatured,
Valid: true,
},
})
if err != nil {
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())
}
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)
}

View File

@ -99,7 +99,7 @@ func GetLogsHandler(appCtx context.Context) fiber.Handler {
}
defer cursor.Close(appCtx)
var logs []domain.LogEntry
var logs []domain.LogEntry = make([]domain.LogEntry, 0)
if err := cursor.All(appCtx, &logs); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Cursor decoding error: "+err.Error())
}

View File

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

View File

@ -35,11 +35,13 @@ import (
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/reports/dashboard [get]
func (h *Handler) GetDashboardReport(c *fiber.Ctx) error {
role := c.Locals("role").(domain.Role)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Parse query parameters
filter, err := parseReportFilter(c)
filter, err := parseReportFilter(c, role)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid filter parameters",
@ -70,24 +72,30 @@ func (h *Handler) GetDashboardReport(c *fiber.Ctx) error {
}
// 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 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)
if err != nil {
return domain.ReportFilter{}, fmt.Errorf("invalid company_id: %w", err)
}
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)
if err != nil {
return domain.ReportFilter{}, fmt.Errorf("invalid branch_id: %w", err)
}
filter.BranchID = domain.ValidInt64{Value: branchID, Valid: true}
} else {
filter.BranchID = c.Locals("branch_id").(domain.ValidInt64)
}
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)
}
// 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
// @Summary 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")
}
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",
zap.String("Medium", string(medium)),
zap.String("Send To", string(sentTo)),
@ -248,7 +248,7 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error {
// TODO: Remove later
_, err = h.walletSvc.AddToWallet(
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 {
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")
}
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",
zap.String("medium", string(medium)),
zap.String("sentTo", string(sentTo)),
@ -417,20 +417,121 @@ type UserProfileRes struct {
LastLogin time.Time `json:"last_login"`
SuspendedAt time.Time `json:"suspended_at"`
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
// @Description Get user profile
// @Tags user
// @Accept json
// @Produce json
// @Success 200 {object} UserProfileRes
// @Success 200 {object} CustomerProfileRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Security Bearer
// @Router /api/v1/user/profile [get]
func (h *Handler) UserProfile(c *fiber.Ctx) error {
// @Router /api/v1/user/customer-profile [get]
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)
if !ok || userID == 0 {

View File

@ -56,7 +56,7 @@ func (a *App) initAppRoutes() {
a.fiber.Get("/", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"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 {
return c.JSON(fiber.Map{
"message": "FortuneBet API V1 pre-alpha",
"version": "1.0dev10",
"version": "1.0dev11",
})
})
// 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/logout", a.authMiddleware, h.LogOutCustomer)
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/sendRegisterCode", h.SendRegisterCode)
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.Delete("/user/delete/:id", a.authMiddleware, h.DeleteUser)
groupV1.Post("/user/suspend", a.authMiddleware, h.UpdateUserSuspend)
@ -162,7 +164,7 @@ func (a *App) initAppRoutes() {
groupV1.Get("/events/:id", h.GetUpcomingEventByID)
groupV1.Delete("/events/:id", a.authMiddleware, a.SuperAdminOnly, h.SetEventStatusToRemoved)
groupV1.Get("/top-leagues", h.GetTopLeagues)
groupV1.Get("/events/:id/flag", h.UpdateEventFlagged)
groupV1.Put("/events/:id/featured", h.UpdateEventFeatured)
// Leagues
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-inactive", a.authMiddleware, h.UpdateBranchStatus)
groupV1.Delete("/branch/:id", a.authMiddleware, h.DeleteBranch)
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("/branchCashier", a.authMiddleware, h.GetBranchForCashier)
@ -212,6 +216,7 @@ func (a *App) initAppRoutes() {
// Bet Routes
groupV1.Post("/sport/bet", a.authMiddleware, h.CreateBet)
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/:id", h.GetBetByID)
groupV1.Patch("/sport/bet/:id", a.authMiddleware, h.UpdateCashOut)
@ -245,7 +250,7 @@ func (a *App) initAppRoutes() {
groupV1.Get("/currencies/convert", h.ConvertCurrency)
//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/list", a.authMiddleware, a.OnlyAdminAndAbove, h.ListReportFiles)
@ -269,6 +274,7 @@ func (a *App) initAppRoutes() {
// Transactions /shop/transactions
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.Post("/shop/bet/:id/cashout", a.authMiddleware, a.CompanyOnly, h.CashoutBet)
groupV1.Post("/shop/bet/:id/generate", a.authMiddleware, a.CompanyOnly, h.CashoutBet)