Refactor result notification service and remove redundant code

- Removed the CheckAndSendResultNotifications method from the result service.
- Consolidated notification logic into a new notification.go file.
- Updated email and in-app notification formatting to include event processing periods.
- Added error handling for wallet operations to check if wallets are active before processing transfers.
- Introduced new error for disabled wallets.
- Updated cron jobs to comment out unnecessary tasks.
- Added bulk update functionality for bet outcomes by odd IDs in the odd handler.
- Renamed ticket handler methods for clarity and consistency.
- Updated API version in routes.
This commit is contained in:
Samuel Tariku 2025-10-10 14:59:19 +03:00
parent 0bab186a8f
commit 3dfa1255b0
22 changed files with 749 additions and 434 deletions

View File

@ -320,6 +320,7 @@ CREATE TABLE events (
is_live BOOLEAN NOT NULL DEFAULT false,
status TEXT NOT NULL,
fetched_at TIMESTAMP DEFAULT now (),
updated_at TIMESTAMP DEFAULT now (),
source TEXT NOT NULL DEFAULT 'b365api' CHECK (
source IN ('b365api', 'bfair', '1xbet', 'bwin', 'enetpulse')
),
@ -411,7 +412,7 @@ CREATE TABLE companies (
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT deducted_percentage_check CHECK (
deducted_percentage >= 0
deducted_percentage > 0
AND deducted_percentage < 1
)
);
@ -581,14 +582,17 @@ CREATE VIEW bet_with_outcomes AS
SELECT bets.*,
CONCAT (users.first_name, ' ', users.last_name) AS full_name,
users.phone_number,
JSON_AGG (bet_outcomes.*) AS outcomes
JSON_AGG (bet_outcomes.*) AS outcomes,
companies.slug as company_slug
FROM bets
LEFT JOIN bet_outcomes ON bets.id = bet_outcomes.bet_id
LEFT JOIN users ON bets.user_id = users.id
JOIN companies ON bets.company_id = companies.id
GROUP BY bets.id,
users.first_name,
users.last_name,
users.phone_number;
users.phone_number,
companies.slug;
CREATE VIEW ticket_with_outcomes AS
SELECT tickets.*,
JSON_AGG (ticket_outcomes.*) AS outcomes
@ -688,7 +692,7 @@ SELECT e.*,
ces.winning_upper_limit,
e.default_winning_upper_limit
) AS winning_upper_limit,
ces.updated_at,
ces.updated_at as company_updated_at,
l.country_code as league_cc
FROM events e
LEFT JOIN company_event_settings ces ON e.id = ces.event_id

View File

@ -138,10 +138,12 @@ SELECT bet_outcomes.*,
users.first_name,
users.last_name,
bets.amount,
bets.total_odds
bets.total_odds,
companies.name as company_name
FROM bet_outcomes
JOIN bets ON bets.id = bet_outcomes.bet_id
JOIN users ON bets.user_id = users.id
JOIN companies ON bets.company_id = companies.id
WHERE bet_outcomes.event_id = $1
AND (
bets.company_id = sqlc.narg('company_id')
@ -217,6 +219,10 @@ UPDATE bet_outcomes
SEt status = $1
WHERE odd_id = $2
RETURNING *;
-- name: BulkUpdateBetOutcomeStatusByOddIDs :exec
UPDATE bet_outcomes
SET status = $1
WHERE odd_id = ANY(sqlc.arg('odd_ids')::BIGINT []);
-- name: UpdateStatus :exec
UPDATE bets
SET status = $1,

View File

@ -30,6 +30,10 @@ wHERE (
user_id = sqlc.narg('user_id')
OR sqlc.narg('user_id') IS NULL
)
AND (
company_id = sqlc.narg('company_id')
OR sqlc.narg('company_id') IS NULL
)
AND (
created_at > sqlc.narg('created_before')
OR sqlc.narg('created_before') IS NULL
@ -60,6 +64,10 @@ wHERE (
user_id = sqlc.narg('user_id')
OR sqlc.narg('user_id') IS NULL
)
AND (
company_id = sqlc.narg('company_id')
OR sqlc.narg('company_id') IS NULL
)
AND (
is_shop_bet = sqlc.narg('is_shop_bet')
OR sqlc.narg('is_shop_bet') IS NULL
@ -117,6 +125,10 @@ WITH market_counts AS (
user_id = sqlc.narg('user_id')
OR sqlc.narg('user_id') IS NULL
)
AND (
company_id = sqlc.narg('company_id')
OR sqlc.narg('company_id') IS NULL
)
AND (
created_at > sqlc.narg('created_before')
OR sqlc.narg('created_before') IS NULL

View File

@ -11,6 +11,22 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
const BulkUpdateBetOutcomeStatusByOddIDs = `-- name: BulkUpdateBetOutcomeStatusByOddIDs :exec
UPDATE bet_outcomes
SET status = $1
WHERE odd_id = ANY($2::BIGINT [])
`
type BulkUpdateBetOutcomeStatusByOddIDsParams struct {
Status int32 `json:"status"`
OddIds []int64 `json:"odd_ids"`
}
func (q *Queries) BulkUpdateBetOutcomeStatusByOddIDs(ctx context.Context, arg BulkUpdateBetOutcomeStatusByOddIDsParams) error {
_, err := q.db.Exec(ctx, BulkUpdateBetOutcomeStatusByOddIDs, arg.Status, arg.OddIds)
return err
}
const CreateBet = `-- name: CreateBet :one
INSERT INTO bets (
amount,
@ -104,7 +120,7 @@ func (q *Queries) DeleteBetOutcome(ctx context.Context, betID int64) error {
}
const GetAllBets = `-- name: GetAllBets :many
SELECT id, company_id, amount, total_odds, status, user_id, is_shop_bet, cashed_out, outcomes_hash, fast_code, processed, created_at, updated_at, full_name, phone_number, outcomes
SELECT id, company_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, company_slug
FROM bet_with_outcomes
wHERE (
user_id = $1
@ -192,6 +208,7 @@ func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWi
&i.FullName,
&i.PhoneNumber,
&i.Outcomes,
&i.CompanySlug,
); err != nil {
return nil, err
}
@ -204,7 +221,7 @@ func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWi
}
const GetBetByFastCode = `-- name: GetBetByFastCode :one
SELECT id, company_id, amount, total_odds, status, user_id, is_shop_bet, cashed_out, outcomes_hash, fast_code, processed, created_at, updated_at, full_name, phone_number, outcomes
SELECT id, company_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, company_slug
FROM bet_with_outcomes
WHERE fast_code = $1
LIMIT 1
@ -230,12 +247,13 @@ func (q *Queries) GetBetByFastCode(ctx context.Context, fastCode string) (BetWit
&i.FullName,
&i.PhoneNumber,
&i.Outcomes,
&i.CompanySlug,
)
return i, err
}
const GetBetByID = `-- name: GetBetByID :one
SELECT id, company_id, amount, total_odds, status, user_id, is_shop_bet, cashed_out, outcomes_hash, fast_code, processed, created_at, updated_at, full_name, phone_number, outcomes
SELECT id, company_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, company_slug
FROM bet_with_outcomes
WHERE id = $1
`
@ -260,12 +278,13 @@ func (q *Queries) GetBetByID(ctx context.Context, id int64) (BetWithOutcome, err
&i.FullName,
&i.PhoneNumber,
&i.Outcomes,
&i.CompanySlug,
)
return i, err
}
const GetBetByUserID = `-- name: GetBetByUserID :many
SELECT id, company_id, amount, total_odds, status, user_id, is_shop_bet, cashed_out, outcomes_hash, fast_code, processed, created_at, updated_at, full_name, phone_number, outcomes
SELECT id, company_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, company_slug
FROM bet_with_outcomes
WHERE user_id = $1
`
@ -296,6 +315,7 @@ func (q *Queries) GetBetByUserID(ctx context.Context, userID int64) ([]BetWithOu
&i.FullName,
&i.PhoneNumber,
&i.Outcomes,
&i.CompanySlug,
); err != nil {
return nil, err
}
@ -453,10 +473,12 @@ SELECT bet_outcomes.id, bet_outcomes.bet_id, bet_outcomes.sport_id, bet_outcomes
users.first_name,
users.last_name,
bets.amount,
bets.total_odds
bets.total_odds,
companies.name as company_name
FROM bet_outcomes
JOIN bets ON bets.id = bet_outcomes.bet_id
JOIN users ON bets.user_id = users.id
JOIN companies ON bets.company_id = companies.id
WHERE bet_outcomes.event_id = $1
AND (
bets.company_id = $2
@ -497,6 +519,7 @@ type GetBetOutcomeViewByEventIDRow struct {
LastName string `json:"last_name"`
Amount int64 `json:"amount"`
TotalOdds float32 `json:"total_odds"`
CompanyName string `json:"company_name"`
}
func (q *Queries) GetBetOutcomeViewByEventID(ctx context.Context, arg GetBetOutcomeViewByEventIDParams) ([]GetBetOutcomeViewByEventIDRow, error) {
@ -534,6 +557,7 @@ func (q *Queries) GetBetOutcomeViewByEventID(ctx context.Context, arg GetBetOutc
&i.LastName,
&i.Amount,
&i.TotalOdds,
&i.CompanyName,
); err != nil {
return nil, err
}
@ -546,7 +570,7 @@ func (q *Queries) GetBetOutcomeViewByEventID(ctx context.Context, arg GetBetOutc
}
const GetBetsForCashback = `-- name: GetBetsForCashback :many
SELECT id, company_id, amount, total_odds, status, user_id, is_shop_bet, cashed_out, outcomes_hash, fast_code, processed, created_at, updated_at, full_name, phone_number, outcomes
SELECT id, company_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, company_slug
FROM bet_with_outcomes
WHERE status = 2
AND processed = false
@ -578,6 +602,7 @@ func (q *Queries) GetBetsForCashback(ctx context.Context) ([]BetWithOutcome, err
&i.FullName,
&i.PhoneNumber,
&i.Outcomes,
&i.CompanySlug,
); err != nil {
return nil, err
}

View File

@ -34,32 +34,37 @@ wHERE (
OR $1 IS NULL
)
AND (
is_shop_bet = $2
company_id = $2
OR $2 IS NULL
)
AND (
cashed_out = $3
is_shop_bet = $3
OR $3 IS NULL
)
AND (
full_name ILIKE '%' || $4 || '%'
OR phone_number ILIKE '%' || $4 || '%'
cashed_out = $4
OR $4 IS NULL
)
AND (
created_at > $5
full_name ILIKE '%' || $5 || '%'
OR phone_number ILIKE '%' || $5 || '%'
OR $5 IS NULL
)
AND (
created_at < $6
created_at > $6
OR $6 IS NULL
)
AND (
created_at < $7
OR $7 IS NULL
)
GROUP BY DATE(created_at)
ORDER BY DATE(created_at)
`
type GetBetStatsParams struct {
UserID pgtype.Int8 `json:"user_id"`
CompanyID pgtype.Int8 `json:"company_id"`
IsShopBet pgtype.Bool `json:"is_shop_bet"`
CashedOut pgtype.Bool `json:"cashed_out"`
Query pgtype.Text `json:"query"`
@ -79,6 +84,7 @@ type GetBetStatsRow struct {
func (q *Queries) GetBetStats(ctx context.Context, arg GetBetStatsParams) ([]GetBetStatsRow, error) {
rows, err := q.db.Query(ctx, GetBetStats,
arg.UserID,
arg.CompanyID,
arg.IsShopBet,
arg.CashedOut,
arg.Query,
@ -143,17 +149,22 @@ wHERE (
OR $1 IS NULL
)
AND (
created_at > $2
company_id = $2
OR $2 IS NULL
)
AND (
created_at < $3
created_at > $3
OR $3 IS NULL
)
AND (
created_at < $4
OR $4 IS NULL
)
`
type GetBetSummaryParams struct {
UserID pgtype.Int8 `json:"user_id"`
CompanyID pgtype.Int8 `json:"company_id"`
CreatedBefore pgtype.Timestamp `json:"created_before"`
CreatedAfter pgtype.Timestamp `json:"created_after"`
}
@ -168,7 +179,12 @@ type GetBetSummaryRow struct {
}
func (q *Queries) GetBetSummary(ctx context.Context, arg GetBetSummaryParams) (GetBetSummaryRow, error) {
row := q.db.QueryRow(ctx, GetBetSummary, arg.UserID, arg.CreatedBefore, arg.CreatedAfter)
row := q.db.QueryRow(ctx, GetBetSummary,
arg.UserID,
arg.CompanyID,
arg.CreatedBefore,
arg.CreatedAfter,
)
var i GetBetSummaryRow
err := row.Scan(
&i.TotalStakes,
@ -198,13 +214,17 @@ WITH market_counts AS (
OR $1 IS NULL
)
AND (
created_at > $2
company_id = $2
OR $2 IS NULL
)
AND (
created_at < $3
created_at > $3
OR $3 IS NULL
)
AND (
created_at < $4
OR $4 IS NULL
)
GROUP BY DATE(b.created_at),
bo.market_name
)
@ -216,6 +236,7 @@ WHERE rank = 1
type GetMarketPopularityParams struct {
UserID pgtype.Int8 `json:"user_id"`
CompanyID pgtype.Int8 `json:"company_id"`
CreatedBefore pgtype.Timestamp `json:"created_before"`
CreatedAfter pgtype.Timestamp `json:"created_after"`
}
@ -226,7 +247,12 @@ type GetMarketPopularityRow struct {
}
func (q *Queries) GetMarketPopularity(ctx context.Context, arg GetMarketPopularityParams) (GetMarketPopularityRow, error) {
row := q.db.QueryRow(ctx, GetMarketPopularity, arg.UserID, arg.CreatedBefore, arg.CreatedAfter)
row := q.db.QueryRow(ctx, GetMarketPopularity,
arg.UserID,
arg.CompanyID,
arg.CreatedBefore,
arg.CreatedAfter,
)
var i GetMarketPopularityRow
err := row.Scan(&i.Date, &i.MarketName)
return i, err

View File

@ -22,7 +22,7 @@ func (q *Queries) DeleteEvent(ctx context.Context, id int64) error {
}
const GetAllEvents = `-- name: GetAllEvents :many
SELECT id, source_event_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, start_time, score, match_minute, timer_status, added_time, match_period, is_live, status, fetched_at, source, default_is_active, default_is_featured, default_winning_upper_limit, is_monitored, league_cc
SELECT id, source_event_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, start_time, score, match_minute, timer_status, added_time, match_period, is_live, status, fetched_at, updated_at, source, default_is_active, default_is_featured, default_winning_upper_limit, is_monitored, league_cc
FROM event_with_country
WHERE (
is_live = $1
@ -122,6 +122,7 @@ func (q *Queries) GetAllEvents(ctx context.Context, arg GetAllEventsParams) ([]E
&i.IsLive,
&i.Status,
&i.FetchedAt,
&i.UpdatedAt,
&i.Source,
&i.DefaultIsActive,
&i.DefaultIsFeatured,
@ -140,7 +141,7 @@ func (q *Queries) GetAllEvents(ctx context.Context, arg GetAllEventsParams) ([]E
}
const GetEventByID = `-- name: GetEventByID :one
SELECT id, source_event_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, start_time, score, match_minute, timer_status, added_time, match_period, is_live, status, fetched_at, source, default_is_active, default_is_featured, default_winning_upper_limit, is_monitored, league_cc
SELECT id, source_event_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, start_time, score, match_minute, timer_status, added_time, match_period, is_live, status, fetched_at, updated_at, source, default_is_active, default_is_featured, default_winning_upper_limit, is_monitored, league_cc
FROM event_with_country
WHERE id = $1
LIMIT 1
@ -171,6 +172,7 @@ func (q *Queries) GetEventByID(ctx context.Context, id int64) (EventWithCountry,
&i.IsLive,
&i.Status,
&i.FetchedAt,
&i.UpdatedAt,
&i.Source,
&i.DefaultIsActive,
&i.DefaultIsFeatured,
@ -182,7 +184,7 @@ func (q *Queries) GetEventByID(ctx context.Context, id int64) (EventWithCountry,
}
const GetEventBySourceID = `-- name: GetEventBySourceID :one
SELECT id, source_event_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, start_time, score, match_minute, timer_status, added_time, match_period, is_live, status, fetched_at, source, default_is_active, default_is_featured, default_winning_upper_limit, is_monitored, league_cc
SELECT id, source_event_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, start_time, score, match_minute, timer_status, added_time, match_period, is_live, status, fetched_at, updated_at, source, default_is_active, default_is_featured, default_winning_upper_limit, is_monitored, league_cc
FROM event_with_country
WHERE source_event_id = $1
AND source = $2
@ -218,6 +220,7 @@ func (q *Queries) GetEventBySourceID(ctx context.Context, arg GetEventBySourceID
&i.IsLive,
&i.Status,
&i.FetchedAt,
&i.UpdatedAt,
&i.Source,
&i.DefaultIsActive,
&i.DefaultIsFeatured,
@ -229,7 +232,7 @@ func (q *Queries) GetEventBySourceID(ctx context.Context, arg GetEventBySourceID
}
const GetEventWithSettingByID = `-- name: GetEventWithSettingByID :one
SELECT e.id, e.source_event_id, e.sport_id, e.match_name, e.home_team, e.away_team, e.home_team_id, e.away_team_id, e.home_kit_image, e.away_kit_image, e.league_id, e.league_name, e.start_time, e.score, e.match_minute, e.timer_status, e.added_time, e.match_period, e.is_live, e.status, e.fetched_at, e.source, e.default_is_active, e.default_is_featured, e.default_winning_upper_limit, e.is_monitored,
SELECT e.id, e.source_event_id, e.sport_id, e.match_name, e.home_team, e.away_team, e.home_team_id, e.away_team_id, e.home_kit_image, e.away_kit_image, e.league_id, e.league_name, e.start_time, e.score, e.match_minute, e.timer_status, e.added_time, e.match_period, e.is_live, e.status, e.fetched_at, e.updated_at, e.source, e.default_is_active, e.default_is_featured, e.default_winning_upper_limit, e.is_monitored,
ces.company_id,
COALESCE(ces.is_active, e.default_is_active) AS is_active,
COALESCE(ces.is_featured, e.default_is_featured) AS is_featured,
@ -274,6 +277,7 @@ type GetEventWithSettingByIDRow struct {
IsLive bool `json:"is_live"`
Status string `json:"status"`
FetchedAt pgtype.Timestamp `json:"fetched_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
Source string `json:"source"`
DefaultIsActive bool `json:"default_is_active"`
DefaultIsFeatured bool `json:"default_is_featured"`
@ -283,7 +287,7 @@ type GetEventWithSettingByIDRow struct {
IsActive bool `json:"is_active"`
IsFeatured bool `json:"is_featured"`
WinningUpperLimit int64 `json:"winning_upper_limit"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
UpdatedAt_2 pgtype.Timestamp `json:"updated_at_2"`
LeagueCc pgtype.Text `json:"league_cc"`
}
@ -312,6 +316,7 @@ func (q *Queries) GetEventWithSettingByID(ctx context.Context, arg GetEventWithS
&i.IsLive,
&i.Status,
&i.FetchedAt,
&i.UpdatedAt,
&i.Source,
&i.DefaultIsActive,
&i.DefaultIsFeatured,
@ -321,14 +326,14 @@ func (q *Queries) GetEventWithSettingByID(ctx context.Context, arg GetEventWithS
&i.IsActive,
&i.IsFeatured,
&i.WinningUpperLimit,
&i.UpdatedAt,
&i.UpdatedAt_2,
&i.LeagueCc,
)
return i, err
}
const GetEventsWithSettings = `-- name: GetEventsWithSettings :many
SELECT e.id, e.source_event_id, e.sport_id, e.match_name, e.home_team, e.away_team, e.home_team_id, e.away_team_id, e.home_kit_image, e.away_kit_image, e.league_id, e.league_name, e.start_time, e.score, e.match_minute, e.timer_status, e.added_time, e.match_period, e.is_live, e.status, e.fetched_at, e.source, e.default_is_active, e.default_is_featured, e.default_winning_upper_limit, e.is_monitored,
SELECT e.id, e.source_event_id, e.sport_id, e.match_name, e.home_team, e.away_team, e.home_team_id, e.away_team_id, e.home_kit_image, e.away_kit_image, e.league_id, e.league_name, e.start_time, e.score, e.match_minute, e.timer_status, e.added_time, e.match_period, e.is_live, e.status, e.fetched_at, e.updated_at, e.source, e.default_is_active, e.default_is_featured, e.default_winning_upper_limit, e.is_monitored,
ces.company_id,
COALESCE(ces.is_active, e.default_is_active) AS is_active,
COALESCE(ces.is_featured, e.default_is_featured) AS is_featured,
@ -432,6 +437,7 @@ type GetEventsWithSettingsRow struct {
IsLive bool `json:"is_live"`
Status string `json:"status"`
FetchedAt pgtype.Timestamp `json:"fetched_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
Source string `json:"source"`
DefaultIsActive bool `json:"default_is_active"`
DefaultIsFeatured bool `json:"default_is_featured"`
@ -441,7 +447,7 @@ type GetEventsWithSettingsRow struct {
IsActive bool `json:"is_active"`
IsFeatured bool `json:"is_featured"`
WinningUpperLimit int64 `json:"winning_upper_limit"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
UpdatedAt_2 pgtype.Timestamp `json:"updated_at_2"`
LeagueCc pgtype.Text `json:"league_cc"`
}
@ -491,6 +497,7 @@ func (q *Queries) GetEventsWithSettings(ctx context.Context, arg GetEventsWithSe
&i.IsLive,
&i.Status,
&i.FetchedAt,
&i.UpdatedAt,
&i.Source,
&i.DefaultIsActive,
&i.DefaultIsFeatured,
@ -500,7 +507,7 @@ func (q *Queries) GetEventsWithSettings(ctx context.Context, arg GetEventsWithSe
&i.IsActive,
&i.IsFeatured,
&i.WinningUpperLimit,
&i.UpdatedAt,
&i.UpdatedAt_2,
&i.LeagueCc,
); err != nil {
return nil, err

View File

@ -82,6 +82,7 @@ type BetWithOutcome struct {
FullName interface{} `json:"full_name"`
PhoneNumber pgtype.Text `json:"phone_number"`
Outcomes []BetOutcome `json:"outcomes"`
CompanySlug string `json:"company_slug"`
}
type Branch struct {
@ -331,6 +332,7 @@ type Event struct {
IsLive bool `json:"is_live"`
Status string `json:"status"`
FetchedAt pgtype.Timestamp `json:"fetched_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
Source string `json:"source"`
DefaultIsActive bool `json:"default_is_active"`
DefaultIsFeatured bool `json:"default_is_featured"`
@ -367,6 +369,7 @@ type EventWithCountry struct {
IsLive bool `json:"is_live"`
Status string `json:"status"`
FetchedAt pgtype.Timestamp `json:"fetched_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
Source string `json:"source"`
DefaultIsActive bool `json:"default_is_active"`
DefaultIsFeatured bool `json:"default_is_featured"`
@ -397,6 +400,7 @@ type EventWithSetting struct {
IsLive bool `json:"is_live"`
Status string `json:"status"`
FetchedAt pgtype.Timestamp `json:"fetched_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
Source string `json:"source"`
DefaultIsActive bool `json:"default_is_active"`
DefaultIsFeatured bool `json:"default_is_featured"`
@ -406,7 +410,7 @@ type EventWithSetting struct {
IsActive bool `json:"is_active"`
IsFeatured bool `json:"is_featured"`
WinningUpperLimit int64 `json:"winning_upper_limit"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
CompanyUpdatedAt pgtype.Timestamp `json:"company_updated_at"`
LeagueCc pgtype.Text `json:"league_cc"`
}

View File

@ -90,6 +90,7 @@ type GetBet struct {
PhoneNumber string
UserID int64
CompanyID int64
CompanySlug string
IsShopBet bool
CashedOut bool
Outcomes []BetOutcome
@ -149,23 +150,25 @@ type CreateBetRes struct {
FastCode string `json:"fast_code"`
}
type BetRes struct {
ID int64 `json:"id" example:"1"`
Outcomes []BetOutcome `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
TotalOdds float32 `json:"total_odds" example:"4.22"`
Status OutcomeStatus `json:"status" example:"1"`
Fullname string `json:"full_name" example:"John Smith"`
UserID int64 `json:"user_id" example:"2"`
CompanyID int64 `json:"company_id" example:"1"`
IsShopBet bool `json:"is_shop_bet" example:"false"`
CashedOut bool `json:"cashed_out" example:"false"`
CreatedAt time.Time `json:"created_at" example:"2025-04-08T12:00:00Z"`
FastCode string `json:"fast_code"`
ID int64 `json:"id" example:"1"`
Outcomes []BetOutcome `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
TotalOdds float32 `json:"total_odds" example:"4.22"`
Status OutcomeStatus `json:"status" example:"1"`
Fullname string `json:"full_name" example:"John Smith"`
UserID int64 `json:"user_id" example:"2"`
CompanyID int64 `json:"company_id" example:"1"`
CompanySlug string `json:"company_slug" example:"fortune"`
IsShopBet bool `json:"is_shop_bet" example:"false"`
CashedOut bool `json:"cashed_out" example:"false"`
CreatedAt time.Time `json:"created_at" example:"2025-04-08T12:00:00Z"`
FastCode string `json:"fast_code"`
}
type BetOutcomeViewRes struct {
ID int64 `json:"id"`
BetID int64 `json:"bet_id"`
CompanyName string `json:"company_name"`
SportID int64 `json:"sport_id"`
EventID int64 `json:"event_id"`
OddID int64 `json:"odd_id"`
@ -208,18 +211,19 @@ func ConvertCreateBetRes(bet Bet, createdNumber int64) CreateBetRes {
func ConvertBet(bet GetBet) BetRes {
return BetRes{
ID: bet.ID,
Amount: bet.Amount.Float32(),
TotalOdds: bet.TotalOdds,
Status: bet.Status,
Fullname: bet.FullName,
UserID: bet.UserID,
CompanyID: bet.CompanyID,
Outcomes: bet.Outcomes,
IsShopBet: bet.IsShopBet,
CashedOut: bet.CashedOut,
CreatedAt: bet.CreatedAt,
FastCode: bet.FastCode,
ID: bet.ID,
Amount: bet.Amount.Float32(),
TotalOdds: bet.TotalOdds,
Status: bet.Status,
Fullname: bet.FullName,
UserID: bet.UserID,
CompanyID: bet.CompanyID,
CompanySlug: bet.CompanySlug,
Outcomes: bet.Outcomes,
IsShopBet: bet.IsShopBet,
CashedOut: bet.CashedOut,
CreatedAt: bet.CreatedAt,
FastCode: bet.FastCode,
}
}
@ -261,6 +265,7 @@ func ConvertDBBetOutcomesView(outcome dbgen.GetBetOutcomeViewByEventIDRow) BetOu
return BetOutcomeViewRes{
ID: outcome.ID,
BetID: outcome.BetID,
CompanyName: outcome.CompanyName,
SportID: outcome.SportID,
EventID: outcome.EventID,
OddID: outcome.OddID,
@ -291,6 +296,7 @@ func ConvertDBBetWithOutcomes(bet dbgen.BetWithOutcome) GetBet {
return GetBet{
ID: bet.ID,
CompanyID: bet.CompanyID,
CompanySlug: bet.CompanySlug,
Amount: Currency(bet.Amount),
TotalOdds: bet.TotalOdds,
Status: OutcomeStatus(bet.Status),

View File

@ -432,6 +432,24 @@ func (s *Store) UpdateBetOutcomeStatusForOddId(ctx context.Context, oddID int64,
return result, nil
}
func (s *Store) BulkUpdateBetOutcomeStatusForOddIds(ctx context.Context, oddID []int64, status domain.OutcomeStatus) (error) {
err := s.queries.BulkUpdateBetOutcomeStatusByOddIDs(ctx, dbgen.BulkUpdateBetOutcomeStatusByOddIDsParams{
Status: int32(status),
OddIds: oddID,
})
if err != nil {
domain.MongoDBLogger.Error("failed to update bet outcome status for oddIDs",
zap.Int64s("oddIds", oddID),
zap.Int32("status", int32(status)),
zap.Error(err),
)
return err
}
return nil
}
func (s *Store) UpdateBetWithCashback(ctx context.Context, betID int64, cashbackStatus bool) error {
err := s.queries.UpdateBetWithCashback(ctx, dbgen.UpdateBetWithCashbackParams{
ID: betID,

View File

@ -63,12 +63,15 @@ func (s *Store) GetAllEvents(ctx context.Context, filter domain.EventFilter) ([]
func (s *Store) GetEventsWithSettings(ctx context.Context, companyID int64, filter domain.EventFilter) ([]domain.EventWithSettings, int64, error) {
events, err := s.queries.GetEventsWithSettings(ctx, dbgen.GetEventsWithSettingsParams{
CompanyID: companyID,
LeagueID: filter.LeagueID.ToPG(),
SportID: filter.SportID.ToPG(),
Query: filter.Query.ToPG(),
Limit: filter.Limit.ToPG(),
Offset: filter.Offset.ToPG(),
CompanyID: companyID,
LeagueID: filter.LeagueID.ToPG(),
SportID: filter.SportID.ToPG(),
Query: filter.Query.ToPG(),
Limit: filter.Limit.ToPG(),
Offset: pgtype.Int4{
Int32: int32(filter.Offset.Value * filter.Limit.Value),
Valid: filter.Offset.Valid,
},
FirstStartTime: filter.FirstStartTime.ToPG(),
LastStartTime: filter.LastStartTime.ToPG(),
CountryCode: filter.CountryCode.ToPG(),

View File

@ -231,7 +231,7 @@ func (s *Service) SendAdminErrorNotification(ctx context.Context, betID int64, s
for _, user := range users {
for _, channel := range []domain.DeliveryChannel{
domain.DeliveryChannelInApp,
domain.DeliveryChannelEmail,
// domain.DeliveryChannelEmail,
} {
n := newBetResultNotification(user.ID, domain.NotificationLevelError, channel, headline, message, map[string]any{
"status": status,
@ -283,7 +283,7 @@ func (s *Service) SendAdminLargeBetNotification(ctx context.Context, betID int64
for _, user := range users {
for _, channel := range []domain.DeliveryChannel{
domain.DeliveryChannelInApp,
domain.DeliveryChannelEmail,
// domain.DeliveryChannelEmail,
} {
raw, _ := json.Marshal(map[string]any{
"winnings": totalWinnings,

View File

@ -27,6 +27,7 @@ type BetStore interface {
UpdateBetOutcomeStatusByBetID(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error)
UpdateBetOutcomeStatusForEvent(ctx context.Context, eventID int64, status domain.OutcomeStatus) ([]domain.BetOutcome, error)
UpdateBetOutcomeStatusForOddId(ctx context.Context, oddID int64, status domain.OutcomeStatus) ([]domain.BetOutcome, error)
BulkUpdateBetOutcomeStatusForOddIds(ctx context.Context, oddID []int64, status domain.OutcomeStatus) error
GetBetSummary(ctx context.Context, filter domain.ReportFilter) (
totalStakes domain.Currency,
totalBets int64,

View File

@ -35,8 +35,10 @@ var (
ErrGenerateRandomOutcome = errors.New("failed to generate any random outcome for events")
ErrOutcomesNotCompleted = errors.New("some bet outcomes are still pending")
ErrEventHasBeenRemoved = errors.New("event has been removed")
ErrEventHasBeenDisabled = errors.New("event has been disabled")
ErrEventHasNotEnded = errors.New("event has not ended yet")
ErrOddHasBeenDisabled = errors.New("odd has been disabled")
ErrRawOddInvalid = errors.New("prematch Raw Odd is Invalid")
ErrBranchIDRequired = errors.New("branch ID required for this role")
ErrOutcomeLimit = errors.New("too many outcomes on a single bet")
@ -108,10 +110,10 @@ func (s *Service) GenerateCashoutID() (string, error) {
return string(result), nil
}
func (s *Service) GenerateBetOutcome(ctx context.Context, eventID int64, marketID int64, oddID int64) (domain.CreateBetOutcome, error) {
func (s *Service) GenerateBetOutcome(ctx context.Context, eventID int64, marketID int64, oddID int64, companyID int64) (domain.CreateBetOutcome, error) {
oddIDStr := strconv.FormatInt(oddID, 10)
event, err := s.eventSvc.GetEventByID(ctx, eventID)
event, err := s.eventSvc.GetEventWithSettingByID(ctx, eventID, companyID)
if err != nil {
s.mongoLogger.Error("failed to fetch upcoming event by ID",
zap.Int64("event_id", eventID),
@ -120,6 +122,14 @@ func (s *Service) GenerateBetOutcome(ctx context.Context, eventID int64, marketI
return domain.CreateBetOutcome{}, ErrEventHasBeenRemoved
}
if !event.IsActive {
s.mongoLogger.Warn("attempting to create bet with disabled event",
zap.Int64("event_id", eventID),
zap.Error(err),
)
return domain.CreateBetOutcome{}, ErrEventHasBeenDisabled
}
currentTime := time.Now()
if event.StartTime.Before(currentTime) {
s.mongoLogger.Error("event has already started",
@ -130,7 +140,7 @@ func (s *Service) GenerateBetOutcome(ctx context.Context, eventID int64, marketI
return domain.CreateBetOutcome{}, ErrEventHasNotEnded
}
odds, err := s.prematchSvc.GetOddsByMarketID(ctx, marketID, eventID)
odds, err := s.prematchSvc.GetOddsWithSettingsByMarketID(ctx, marketID, eventID, companyID)
if err != nil {
s.mongoLogger.Error("failed to get raw odds by market ID",
zap.Int64("event_id", eventID),
@ -140,6 +150,15 @@ func (s *Service) GenerateBetOutcome(ctx context.Context, eventID int64, marketI
return domain.CreateBetOutcome{}, err
}
if !odds.IsActive {
s.mongoLogger.Error("failed to get raw odds by market ID",
zap.Int64("event_id", eventID),
zap.Int64("market_id", marketID),
zap.Error(err),
)
return domain.CreateBetOutcome{}, ErrOddHasBeenDisabled
}
type rawOddType struct {
ID string
Name string
@ -257,7 +276,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
var totalOdds float32 = 1
for _, outcomeReq := range req.Outcomes {
newOutcome, err := s.GenerateBetOutcome(ctx, outcomeReq.EventID, outcomeReq.MarketID, outcomeReq.OddID)
newOutcome, err := s.GenerateBetOutcome(ctx, outcomeReq.EventID, outcomeReq.MarketID, outcomeReq.OddID, companyID)
if err != nil {
s.mongoLogger.Error("failed to generate outcome",
zap.Int64("event_id", outcomeReq.EventID),
@ -536,7 +555,9 @@ func (s *Service) DeductBetFromBranchWallet(ctx context.Context, amount float32,
return ErrCompanyDeductedPercentInvalid
}
deductedAmount := amount - (amount * company.DeductedPercentage)
// This is the amount that we take from a company/tenant when they
// create a bet. I.e. if its 5% (0.05), then thats the percentage we take every
deductedAmount := amount * company.DeductedPercentage
if deductedAmount == 0 {
s.mongoLogger.Fatal("Amount",
@ -1113,6 +1134,19 @@ func (s *Service) UpdateBetOutcomeStatusForOddId(ctx context.Context, oddID int6
return outcomes, nil
}
func (s *Service) BulkUpdateBetOutcomeStatusForOddIds(ctx context.Context, oddID []int64, status domain.OutcomeStatus) error {
err := s.betStore.BulkUpdateBetOutcomeStatusForOddIds(ctx, oddID, status)
if err != nil {
s.mongoLogger.Error("failed to update bet outcome status by oddIds",
zap.Int64s("oddID", oddID),
zap.Error(err),
)
return err
}
return nil
}
func (s *Service) SetBetToRemoved(ctx context.Context, id int64) error {
_, err := s.betStore.UpdateBetOutcomeStatusByBetID(ctx, id, domain.OUTCOME_STATUS_VOID)
if err != nil {

View File

@ -0,0 +1,292 @@
package result
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"go.uber.org/zap"
)
func (s *Service) CheckAndSendResultNotifications(ctx context.Context, createdAfter time.Time) error {
resultLog, err := s.repo.GetAllResultLog(ctx, domain.ResultLogFilter{
CreatedAfter: domain.ValidTime{
Value: createdAfter,
Valid: true,
},
})
if err != nil {
s.mongoLogger.Error(
"Failed to get result log",
zap.Time("CreatedAfter", createdAfter),
zap.Error(err),
)
return err
}
if len(resultLog) == 0 {
s.mongoLogger.Info(
"No results found for check and send result notification",
zap.Time("CreatedAfter", createdAfter),
)
return nil
}
totalResultLog := domain.ResultLog{
StatusNotFinishedCount: resultLog[0].StatusNotFinishedCount,
StatusPostponedCount: resultLog[0].StatusPostponedCount,
}
for _, log := range resultLog {
// Add all the bets
totalResultLog.StatusNotFinishedBets += log.StatusNotFinishedBets
totalResultLog.StatusPostponedBets += log.StatusPostponedBets
totalResultLog.StatusToBeFixedBets += log.StatusToBeFixedBets
totalResultLog.StatusRemovedBets += log.StatusRemovedBets
totalResultLog.StatusEndedBets += log.StatusEndedBets
totalResultLog.StatusToBeFixedCount += log.StatusToBeFixedCount
totalResultLog.StatusRemovedCount += log.StatusRemovedCount
totalResultLog.StatusEndedCount += log.StatusEndedCount
totalResultLog.RemovedCount += log.RemovedCount
}
err = s.SendAdminResultStatusErrorNotification(ctx, totalResultLog, createdAfter, time.Now())
if err != nil {
s.mongoLogger.Error(
"Failed to send admin result status notification",
zap.Time("CreatedAfter", createdAfter),
zap.Error(err),
)
return err
}
return nil
}
func buildHeadlineAndMessage(counts domain.ResultLog, createdAfter time.Time, endTime time.Time) (string, string) {
period := fmt.Sprintf("%s - %s", createdAfter.Format("02 Jan 2006"), endTime.Format("02 Jan 2006"))
totalIssues := counts.StatusNotFinishedCount + counts.StatusToBeFixedCount + counts.StatusPostponedCount + counts.StatusRemovedCount
totalBets := counts.StatusEndedBets + counts.StatusNotFinishedBets + counts.StatusPostponedBets + counts.StatusRemovedBets + counts.StatusToBeFixedBets
if totalIssues == 0 {
return "✅ Successfully Processed Event Results", fmt.Sprintf(
"%d total ended events with %d total bets. No issues detected", counts.StatusEndedCount, totalBets,
)
}
parts := []string{}
if counts.StatusNotFinishedCount > 0 {
parts = append(parts, fmt.Sprintf("%d unfinished with %d bets", counts.StatusNotFinishedCount, counts.StatusNotFinishedBets))
}
if counts.StatusToBeFixedCount > 0 {
parts = append(parts, fmt.Sprintf("%d to-fix with %d bets", counts.StatusToBeFixedCount, counts.StatusToBeFixedBets))
}
if counts.StatusPostponedCount > 0 {
parts = append(parts, fmt.Sprintf("%d postponed with %d bets", counts.StatusPostponedCount, counts.StatusPostponedBets))
}
if counts.StatusRemovedCount > 0 {
parts = append(parts, fmt.Sprintf("%d removed with %d bets", counts.StatusRemovedCount, counts.StatusRemovedBets))
}
if counts.StatusEndedCount > 0 {
parts = append(parts, fmt.Sprintf("%d ended with %d bets", counts.StatusEndedCount, counts.StatusEndedBets))
}
headline := "⚠️ Issues Found Processing Event Results"
message := fmt.Sprintf("Processed expired event results (%s): %s. Please review pending entries.",
period, strings.Join(parts, ", "))
return headline, message
}
func buildHeadlineAndMessageEmail(counts domain.ResultLog, user domain.User, createdAfter time.Time, endTime time.Time) (string, string, string) {
period := fmt.Sprintf("%s - %s", createdAfter.Format("02 Jan 2006"), endTime.Format("02 Jan 2006"))
totalIssues := counts.StatusNotFinishedCount + counts.StatusToBeFixedCount +
counts.StatusPostponedCount + counts.StatusRemovedCount
totalEvents := counts.StatusEndedCount + counts.StatusNotFinishedCount +
counts.StatusToBeFixedCount + counts.StatusPostponedCount + counts.StatusRemovedCount
totalBets := counts.StatusEndedBets + counts.StatusNotFinishedBets +
counts.StatusPostponedBets + counts.StatusRemovedBets + counts.StatusToBeFixedBets
greeting := fmt.Sprintf("Hi %s %s,", user.FirstName, user.LastName)
if totalIssues == 0 {
headline := "✅ Weekly Results Report — All Events Processed Successfully"
plain := fmt.Sprintf(`%s
Weekly Results Summary (%s):
- %d Ended Events
- %d Total Bets
All events were processed successfully, and no issues were detected.
Best regards,
The System`, greeting, period, counts.StatusEndedCount, totalBets)
html := fmt.Sprintf(`<p>%s</p>
<h2>Weekly Results Summary</h2>
<p><em>Period: %s</em></p>
<ul>
<li><strong>%d Ended Events</strong></li>
<li><strong>%d Total Bets</strong></li>
</ul>
<p>All events were processed successfully, and no issues were detected.</p>
<p>Best regards,<br>The System</p>`,
greeting, period, counts.StatusEndedCount, totalBets)
return headline, plain, html
}
partsPlain := []string{}
partsHTML := []string{}
if counts.StatusNotFinishedCount > 0 {
partsPlain = append(partsPlain,
fmt.Sprintf("- %d Incomplete Events (%d Bets)", counts.StatusNotFinishedCount, counts.StatusNotFinishedBets))
partsHTML = append(partsHTML,
fmt.Sprintf("<li><strong>%d Incomplete Events</strong> (%d Bets)</li>", counts.StatusNotFinishedCount, counts.StatusNotFinishedBets))
}
if counts.StatusToBeFixedCount > 0 {
partsPlain = append(partsPlain,
fmt.Sprintf("- %d Requires Review (%d Bets)", counts.StatusToBeFixedCount, counts.StatusToBeFixedBets))
partsHTML = append(partsHTML,
fmt.Sprintf("<li><strong>%d Requires Review</strong> (%d Bets)</li>", counts.StatusToBeFixedCount, counts.StatusToBeFixedBets))
}
if counts.StatusPostponedCount > 0 {
partsPlain = append(partsPlain,
fmt.Sprintf("- %d Postponed Events (%d Bets)", counts.StatusPostponedCount, counts.StatusPostponedBets))
partsHTML = append(partsHTML,
fmt.Sprintf("<li><strong>%d Postponed Events</strong> (%d Bets)</li>", counts.StatusPostponedCount, counts.StatusPostponedBets))
}
if counts.StatusRemovedCount > 0 {
partsPlain = append(partsPlain,
fmt.Sprintf("- %d Discarded Events (%d Bets)", counts.StatusRemovedCount, counts.StatusRemovedBets))
partsHTML = append(partsHTML,
fmt.Sprintf("<li><strong>%d Discarded Events</strong> (%d Bets)</li>", counts.StatusRemovedCount, counts.StatusRemovedBets))
}
if counts.StatusEndedCount > 0 {
partsPlain = append(partsPlain,
fmt.Sprintf("- %d Successfully Ended Events (%d Bets)", counts.StatusEndedCount, counts.StatusEndedBets))
partsHTML = append(partsHTML,
fmt.Sprintf("<li><strong>%d Successfully Ended Events</strong> (%d Bets)</li>", counts.StatusEndedCount, counts.StatusEndedBets))
}
headline := "⚠️ Weekly Results Report — Review Required"
plain := fmt.Sprintf(`%s
Weekly Results Summary (%s):
%s
Totals:
- %d Events Processed
- %d Total Bets
Next Steps:
Some events require your attention. Please log into the admin dashboard to review pending issues.
Best regards,
The System`,
greeting,
period,
strings.Join(partsPlain, "\n"),
totalEvents,
totalBets,
)
html := fmt.Sprintf(`<p>%s</p>
<h2>Weekly Results Summary</h2>
<p><em>Period: %s</em></p>
<ul>
%s
</ul>
<h3>Totals</h3>
<ul>
<li><strong>%d Events Processed</strong></li>
<li><strong>%d Total Bets</strong></li>
</ul>
<p><strong>Next Steps:</strong><br>Some events require your attention. Please <a href="https://admin.fortunebets.net">log into the admin dashboard</a> to review pending issues.</p>
<p>Best regards,<br>The System</p>`,
greeting,
period,
strings.Join(partsHTML, "\n"),
totalEvents,
totalBets,
)
return headline, plain, html
}
func (s *Service) SendAdminResultStatusErrorNotification(
ctx context.Context,
counts domain.ResultLog,
createdAfter time.Time,
endTime time.Time,
) error {
superAdmins, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{
Role: string(domain.RoleSuperAdmin),
})
if err != nil {
s.mongoLogger.Error("failed to get super_admin recipients", zap.Error(err))
return err
}
metaBytes, err := json.Marshal(counts)
if err != nil {
s.mongoLogger.Error("failed to marshal metadata", zap.Error(err))
return err
}
headline, message := buildHeadlineAndMessage(counts, createdAfter, endTime)
notification := &domain.Notification{
ErrorSeverity: domain.NotificationErrorSeverityHigh,
DeliveryStatus: domain.DeliveryStatusPending,
IsRead: false,
Type: domain.NOTIFICATION_TYPE_BET_RESULT,
Level: domain.NotificationLevelWarning,
Reciever: domain.NotificationRecieverSideAdmin,
DeliveryChannel: domain.DeliveryChannelInApp,
Payload: domain.NotificationPayload{
Headline: headline,
Message: message,
},
Priority: 2,
Metadata: metaBytes,
}
var sendErrors []error
for _, user := range superAdmins {
notification.RecipientID = user.ID
if err := s.notificationSvc.SendNotification(ctx, notification); err != nil {
s.mongoLogger.Error("failed to send admin notification",
zap.Int64("admin_id", user.ID),
zap.Error(err),
)
sendErrors = append(sendErrors, err)
}
// notification.DeliveryChannel = domain.DeliveryChannelEmail
if user.Email == "" {
continue
}
subject, plain, html := buildHeadlineAndMessageEmail(counts, user, createdAfter, endTime)
if err := s.messengerSvc.SendEmail(ctx, user.Email, plain, html, subject); err != nil {
s.mongoLogger.Error("failed to send admin result report email",
zap.Int64("admin_id", user.ID),
zap.Error(err),
)
sendErrors = append(sendErrors, err)
}
}
if len(sendErrors) > 0 {
return fmt.Errorf("sent with partial failure: %d errors", len(sendErrors))
}
return nil
}

View File

@ -457,275 +457,6 @@ func (s *Service) FetchB365ResultAndUpdateBets(ctx context.Context) error {
return nil
}
func (s *Service) CheckAndSendResultNotifications(ctx context.Context, createdAfter time.Time) error {
resultLog, err := s.repo.GetAllResultLog(ctx, domain.ResultLogFilter{
CreatedAfter: domain.ValidTime{
Value: createdAfter,
Valid: true,
},
})
if err != nil {
s.mongoLogger.Error(
"Failed to get result log",
zap.Time("CreatedAfter", createdAfter),
zap.Error(err),
)
return err
}
if len(resultLog) == 0 {
s.mongoLogger.Info(
"No results found for check and send result notification",
zap.Time("CreatedAfter", createdAfter),
)
return nil
}
totalResultLog := domain.ResultLog{
StatusNotFinishedCount: resultLog[0].StatusNotFinishedCount,
StatusPostponedCount: resultLog[0].StatusPostponedCount,
}
for _, log := range resultLog {
// Add all the bets
totalResultLog.StatusNotFinishedBets += log.StatusNotFinishedBets
totalResultLog.StatusPostponedBets += log.StatusPostponedBets
totalResultLog.StatusToBeFixedBets += log.StatusToBeFixedBets
totalResultLog.StatusRemovedBets += log.StatusRemovedBets
totalResultLog.StatusEndedBets += log.StatusEndedBets
totalResultLog.StatusToBeFixedCount += log.StatusToBeFixedCount
totalResultLog.StatusRemovedCount += log.StatusRemovedCount
totalResultLog.StatusEndedCount += log.StatusEndedCount
totalResultLog.RemovedCount += log.RemovedCount
}
err = s.SendAdminResultStatusErrorNotification(ctx, totalResultLog)
if err != nil {
s.mongoLogger.Error(
"Failed to send admin result status notification",
zap.Time("CreatedAfter", createdAfter),
zap.Error(err),
)
return err
}
return nil
}
func buildHeadlineAndMessage(counts domain.ResultLog) (string, string) {
totalIssues := counts.StatusNotFinishedCount + counts.StatusToBeFixedCount + counts.StatusPostponedCount + counts.StatusRemovedCount
totalBets := counts.StatusEndedBets + counts.StatusNotFinishedBets + counts.StatusPostponedBets + counts.StatusRemovedBets + counts.StatusToBeFixedBets
if totalIssues == 0 {
return "✅ Successfully Processed Event Results", fmt.Sprintf(
"%d total ended events with %d total bets. No issues detected", counts.StatusEndedCount, totalBets,
)
}
parts := []string{}
if counts.StatusNotFinishedCount > 0 {
parts = append(parts, fmt.Sprintf("%d unfinished with %d bets", counts.StatusNotFinishedCount, counts.StatusNotFinishedBets))
}
if counts.StatusToBeFixedCount > 0 {
parts = append(parts, fmt.Sprintf("%d to-fix with %d bets", counts.StatusToBeFixedCount, counts.StatusToBeFixedBets))
}
if counts.StatusPostponedCount > 0 {
parts = append(parts, fmt.Sprintf("%d postponed with %d bets", counts.StatusPostponedCount, counts.StatusPostponedBets))
}
if counts.StatusRemovedCount > 0 {
parts = append(parts, fmt.Sprintf("%d removed with %d bets", counts.StatusRemovedCount, counts.StatusRemovedBets))
}
if counts.StatusEndedCount > 0 {
parts = append(parts, fmt.Sprintf("%d ended with %d bets", counts.StatusEndedCount, counts.StatusEndedBets))
}
headline := "⚠️ Issues Found Processing Event Results"
message := fmt.Sprintf("Processed expired event results: %s. Please review pending entries.", strings.Join(parts, ", "))
return headline, message
}
func buildHeadlineAndMessageEmail(counts domain.ResultLog, user domain.User) (string, string, string) {
totalIssues := counts.StatusNotFinishedCount + counts.StatusToBeFixedCount +
counts.StatusPostponedCount + counts.StatusRemovedCount
totalEvents := counts.StatusEndedCount + counts.StatusNotFinishedCount +
counts.StatusToBeFixedCount + counts.StatusPostponedCount + counts.StatusRemovedCount
totalBets := counts.StatusEndedBets + counts.StatusNotFinishedBets +
counts.StatusPostponedBets + counts.StatusRemovedBets + counts.StatusToBeFixedBets
greeting := fmt.Sprintf("Hi %s %s,", user.FirstName, user.LastName)
if totalIssues == 0 {
headline := "✅ Weekly Results Report — All Events Processed Successfully"
plain := fmt.Sprintf(`%s
Weekly Results Summary:
- %d Ended Events
- %d Total Bets
All events were processed successfully, and no issues were detected.
Best regards,
The System`, greeting, counts.StatusEndedCount, totalBets)
html := fmt.Sprintf(`<p>%s</p>
<h2>Weekly Results Summary</h2>
<ul>
<li><strong>%d Ended Events</strong></li>
<li><strong>%d Total Bets</strong></li>
</ul>
<p>All events were processed successfully, and no issues were detected.</p>
<p>Best regards,<br>The System</p>`,
greeting, counts.StatusEndedCount, totalBets)
return headline, plain, html
}
partsPlain := []string{}
partsHTML := []string{}
if counts.StatusNotFinishedCount > 0 {
partsPlain = append(partsPlain,
fmt.Sprintf("- %d Incomplete Events (%d Bets)", counts.StatusNotFinishedCount, counts.StatusNotFinishedBets))
partsHTML = append(partsHTML,
fmt.Sprintf("<li><strong>%d Incomplete Events</strong> (%d Bets)</li>", counts.StatusNotFinishedCount, counts.StatusNotFinishedBets))
}
if counts.StatusToBeFixedCount > 0 {
partsPlain = append(partsPlain,
fmt.Sprintf("- %d Requires Review (%d Bets)", counts.StatusToBeFixedCount, counts.StatusToBeFixedBets))
partsHTML = append(partsHTML,
fmt.Sprintf("<li><strong>%d Requires Review</strong> (%d Bets)</li>", counts.StatusToBeFixedCount, counts.StatusToBeFixedBets))
}
if counts.StatusPostponedCount > 0 {
partsPlain = append(partsPlain,
fmt.Sprintf("- %d Postponed Events (%d Bets)", counts.StatusPostponedCount, counts.StatusPostponedBets))
partsHTML = append(partsHTML,
fmt.Sprintf("<li><strong>%d Postponed Events</strong> (%d Bets)</li>", counts.StatusPostponedCount, counts.StatusPostponedBets))
}
if counts.StatusRemovedCount > 0 {
partsPlain = append(partsPlain,
fmt.Sprintf("- %d Discarded Events (%d Bets)", counts.StatusRemovedCount, counts.StatusRemovedBets))
partsHTML = append(partsHTML,
fmt.Sprintf("<li><strong>%d Discarded Events</strong> (%d Bets)</li>", counts.StatusRemovedCount, counts.StatusRemovedBets))
}
if counts.StatusEndedCount > 0 {
partsPlain = append(partsPlain,
fmt.Sprintf("- %d Successfully Ended Events (%d Bets)", counts.StatusEndedCount, counts.StatusEndedBets))
partsHTML = append(partsHTML,
fmt.Sprintf("<li><strong>%d Successfully Ended Events</strong> (%d Bets)</li>", counts.StatusEndedCount, counts.StatusEndedBets))
}
headline := "⚠️ Weekly Results Report — Review Required"
plain := fmt.Sprintf(`%s
Weekly Results Summary:
%s
Totals:
- %d Events Processed
- %d Total Bets
Next Steps:
Some events require your attention. Please log into the admin dashboard to review pending issues.
Best regards,
The System`,
greeting,
strings.Join(partsPlain, "\n"),
totalEvents,
totalBets,
)
html := fmt.Sprintf(`<p>%s</p>
<h2>Weekly Results Summary</h2>
<ul>
%s
</ul>
<h3>Totals</h3>
<ul>
<li><strong>%d Events Processed</strong></li>
<li><strong>%d Total Bets</strong></li>
</ul>
<p><strong>Next Steps:</strong><br>Some events require your attention. Please <a href="https://admin.fortunebets.net">log into the admin dashboard</a> to review pending issues.</p>
<p>Best regards,<br>The System</p>`,
greeting,
strings.Join(partsHTML, "\n"),
totalEvents,
totalBets,
)
return headline, plain, html
}
func (s *Service) SendAdminResultStatusErrorNotification(
ctx context.Context,
counts domain.ResultLog,
) error {
superAdmins, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{
Role: string(domain.RoleSuperAdmin),
})
if err != nil {
s.mongoLogger.Error("failed to get super_admin recipients", zap.Error(err))
return err
}
metaBytes, err := json.Marshal(counts)
if err != nil {
s.mongoLogger.Error("failed to marshal metadata", zap.Error(err))
return err
}
headline, message := buildHeadlineAndMessage(counts)
notification := &domain.Notification{
ErrorSeverity: domain.NotificationErrorSeverityHigh,
DeliveryStatus: domain.DeliveryStatusPending,
IsRead: false,
Type: domain.NOTIFICATION_TYPE_BET_RESULT,
Level: domain.NotificationLevelWarning,
Reciever: domain.NotificationRecieverSideAdmin,
DeliveryChannel: domain.DeliveryChannelInApp,
Payload: domain.NotificationPayload{
Headline: headline,
Message: message,
},
Priority: 2,
Metadata: metaBytes,
}
var sendErrors []error
for _, user := range superAdmins {
notification.RecipientID = user.ID
if err := s.notificationSvc.SendNotification(ctx, notification); err != nil {
s.mongoLogger.Error("failed to send admin notification",
zap.Int64("admin_id", user.ID),
zap.Error(err),
)
sendErrors = append(sendErrors, err)
}
// notification.DeliveryChannel = domain.DeliveryChannelEmail
if user.Email == "" {
continue
}
subject, plain, html := buildHeadlineAndMessageEmail(counts, user)
if err := s.messengerSvc.SendEmail(ctx, user.Email, plain, html, subject); err != nil {
s.mongoLogger.Error("failed to send admin result report email",
zap.Int64("admin_id", user.ID),
zap.Error(err),
)
sendErrors = append(sendErrors, err)
}
}
if len(sendErrors) > 0 {
return fmt.Errorf("sent with partial failure: %d errors", len(sendErrors))
}
return nil
}
func (s *Service) CheckAndUpdateExpiredB365Events(ctx context.Context) (int64, error) {
events, _, err := s.repo.GetAllEvents(ctx, domain.EventFilter{

View File

@ -170,16 +170,16 @@ func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWalle
)
}
adminNotification.DeliveryChannel = domain.DeliveryChannelEmail
// adminNotification.DeliveryChannel = domain.DeliveryChannelEmail
if err := s.notificationSvc.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
}
// if err := s.notificationSvc.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
}

View File

@ -52,9 +52,11 @@ func (s *Service) TransferToWallet(ctx context.Context, senderID int64, receiver
senderWallet, err := s.GetWalletByID(ctx, senderID)
if err != nil {
return domain.Transfer{}, err
}
if !senderWallet.IsActive {
return domain.Transfer{}, ErrWalletIsDisabled
}
if !senderWallet.IsTransferable {
fmt.Printf("Error: %d Sender Wallet is not transferable \n", senderWallet.ID)
@ -65,7 +67,9 @@ func (s *Service) TransferToWallet(ctx context.Context, senderID int64, receiver
if err != nil {
return domain.Transfer{}, err
}
if !receiverWallet.IsActive {
return domain.Transfer{}, ErrWalletIsDisabled
}
if !receiverWallet.IsTransferable {
fmt.Printf("Error: %d Receiver Wallet is not transferable \n", senderWallet.ID)
return domain.Transfer{}, ErrReceiverWalletNotTransferable

View File

@ -13,6 +13,7 @@ import (
var (
ErrBalanceInsufficient = errors.New("wallet balance is insufficient")
ErrWalletIsDisabled = errors.New("wallet is disabled")
)
func (s *Service) CreateWallet(ctx context.Context, wallet domain.CreateWallet) (domain.Wallet, error) {
@ -84,12 +85,17 @@ func (s *Service) GetAllBranchWallets(ctx context.Context) ([]domain.BranchWalle
}
func (s *Service) UpdateBalance(ctx context.Context, id int64, balance domain.Currency) error {
err := s.walletStore.UpdateBalance(ctx, id, balance)
wallet, err := s.GetWalletByID(ctx, id)
if err != nil {
return err
}
wallet, err := s.GetWalletByID(ctx, id)
if !wallet.IsActive {
return ErrWalletIsDisabled
}
err = s.walletStore.UpdateBalance(ctx, id, balance)
if err != nil {
return err
}
@ -120,7 +126,9 @@ func (s *Service) AddToWallet(
if err != nil {
return domain.Transfer{}, err
}
if !wallet.IsActive {
return domain.Transfer{}, ErrWalletIsDisabled
}
err = s.walletStore.UpdateBalance(ctx, id, wallet.Balance+amount)
if err != nil {
return domain.Transfer{}, err
@ -166,6 +174,9 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.
return domain.Transfer{}, err
}
if !wallet.IsActive {
return domain.Transfer{}, ErrWalletIsDisabled
}
if wallet.Balance < amount {
// Send Wallet low to admin
if wallet.Type == domain.CompanyWalletType || wallet.Type == domain.BranchWalletType {
@ -186,8 +197,11 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.
}
balance := wallet.Balance.Float32()
if balance < thresholds[0] {
s.SendAdminWalletLowNotification(ctx, wallet)
for _, thresholds := range thresholds {
if thresholds < balance && thresholds > (balance-amount.Float32()) {
s.SendAdminWalletLowNotification(ctx, wallet)
break
}
}
}

View File

@ -28,71 +28,71 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
spec string
task func()
}{
{
spec: "0 0 * * * *", // Every 1 hour
task: func() {
mongoLogger.Info("Began fetching upcoming events cron task")
if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
mongoLogger.Error("Failed to fetch upcoming events",
zap.Error(err),
)
} else {
mongoLogger.Info("Completed fetching upcoming events without errors")
}
},
},
{
spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events)
task: func() {
mongoLogger.Info("Began fetching non live odds cron task")
if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
mongoLogger.Error("Failed to fetch non live odds",
zap.Error(err),
)
} else {
mongoLogger.Info("Completed fetching non live odds without errors")
}
},
},
{
spec: "0 */5 * * * *", // Every 5 Minutes
task: func() {
mongoLogger.Info("Began update all expired events status cron task")
if _, err := resultService.CheckAndUpdateExpiredB365Events(context.Background()); err != nil {
mongoLogger.Error("Failed to update expired events status",
zap.Error(err),
)
} else {
mongoLogger.Info("Completed expired events without errors")
}
},
},
{
spec: "0 */15 * * * *", // Every 15 Minutes
task: func() {
mongoLogger.Info("Began updating bets based on event results cron task")
if err := resultService.FetchB365ResultAndUpdateBets(context.Background()); err != nil {
mongoLogger.Error("Failed to process result",
zap.Error(err),
)
} else {
mongoLogger.Info("Completed processing all event result outcomes without errors")
}
},
},
{
spec: "0 0 0 * * 1", // Every Monday
task: func() {
mongoLogger.Info("Began Send weekly result notification cron task")
if err := resultService.CheckAndSendResultNotifications(context.Background(), time.Now().Add(-7*24*time.Hour)); err != nil {
mongoLogger.Error("Failed to process result",
zap.Error(err),
)
} else {
mongoLogger.Info("Completed sending weekly result notification without errors")
}
},
},
// {
// spec: "0 0 * * * *", // Every 1 hour
// task: func() {
// mongoLogger.Info("Began fetching upcoming events cron task")
// if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
// mongoLogger.Error("Failed to fetch upcoming events",
// zap.Error(err),
// )
// } else {
// mongoLogger.Info("Completed fetching upcoming events without errors")
// }
// },
// },
// {
// spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events)
// task: func() {
// mongoLogger.Info("Began fetching non live odds cron task")
// if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
// mongoLogger.Error("Failed to fetch non live odds",
// zap.Error(err),
// )
// } else {
// mongoLogger.Info("Completed fetching non live odds without errors")
// }
// },
// },
// {
// spec: "0 */5 * * * *", // Every 5 Minutes
// task: func() {
// mongoLogger.Info("Began update all expired events status cron task")
// if _, err := resultService.CheckAndUpdateExpiredB365Events(context.Background()); err != nil {
// mongoLogger.Error("Failed to update expired events status",
// zap.Error(err),
// )
// } else {
// mongoLogger.Info("Completed expired events without errors")
// }
// },
// },
// {
// spec: "0 */15 * * * *", // Every 15 Minutes
// task: func() {
// mongoLogger.Info("Began updating bets based on event results cron task")
// if err := resultService.FetchB365ResultAndUpdateBets(context.Background()); err != nil {
// mongoLogger.Error("Failed to process result",
// zap.Error(err),
// )
// } else {
// mongoLogger.Info("Completed processing all event result outcomes without errors")
// }
// },
// },
// {
// spec: "0 0 0 * * 1", // Every Monday
// task: func() {
// mongoLogger.Info("Began Send weekly result notification cron task")
// if err := resultService.CheckAndSendResultNotifications(context.Background(), time.Now().Add(-7*24*time.Hour)); err != nil {
// mongoLogger.Error("Failed to process result",
// zap.Error(err),
// )
// } else {
// mongoLogger.Info("Completed sending weekly result notification without errors")
// }
// },
// },
}
for _, job := range schedule {

View File

@ -483,3 +483,45 @@ func (h *Handler) UpdateAllBetOutcomeStatusByOddID(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusOK, "Updated All Bet Outcome Status Successfully", nil, nil)
}
type BulkUpdateAllBetStatusByOddIDsReq struct {
OddIDs []int64 `json:"odd_ids"`
Status domain.OutcomeStatus `json:"status"`
}
func (h *Handler) BulkUpdateAllBetOutcomeStatusByOddID(c *fiber.Ctx) error {
var req BulkUpdateAllBetStatusByOddIDsReq
if err := c.BodyParser(&req); err != nil {
h.BadRequestLogger().Info("Failed to parse event id",
zap.Error(err),
)
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
logFields := []zap.Field{
zap.Int64s("odd_ids", req.OddIDs),
zap.Any("status", req.Status),
}
valErrs, ok := h.validator.Validate(c, req)
if !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
h.BadRequestLogger().Error("Failed to insert odd settings",
append(logFields, zap.String("errMsg", errMsg))...,
)
return fiber.NewError(fiber.StatusBadRequest, errMsg)
}
err := h.betSvc.BulkUpdateBetOutcomeStatusForOddIds(c.Context(), req.OddIDs, req.Status)
if err != nil {
h.InternalServerErrorLogger().Error("Failed to bulk update bet status by odd ids",
append(logFields, zap.Error(err))...,
)
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return response.WriteJSON(c, fiber.StatusOK, "Bulk Updated All Bet Outcome Status Successfully", nil, nil)
}

View File

@ -22,7 +22,7 @@ import (
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}/ticket [post]
func (h *Handler) CreateTicket(c *fiber.Ctx) error {
func (h *Handler) CreateTenantTicket(c *fiber.Ctx) error {
companyID := c.Locals("company_id").(domain.ValidInt64)
if !companyID.Valid {
h.BadRequestLogger().Error("invalid company id")
@ -91,7 +91,7 @@ func (h *Handler) CreateTicket(c *fiber.Ctx) error {
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}/ticket/{id} [get]
func (h *Handler) GetTicketByID(c *fiber.Ctx) error {
func (h *Handler) GetTenantTicketByID(c *fiber.Ctx) error {
companyID := c.Locals("company_id").(domain.ValidInt64)
if !companyID.Valid {
h.BadRequestLogger().Error("invalid company id")
@ -154,7 +154,7 @@ func (h *Handler) GetTicketByID(c *fiber.Ctx) error {
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}/ticket [get]
func (h *Handler) GetAllTickets(c *fiber.Ctx) error {
func (h *Handler) GetAllTenantTickets(c *fiber.Ctx) error {
companyID := c.Locals("company_id").(domain.ValidInt64)
if !companyID.Valid {
h.BadRequestLogger().Error("invalid company id")
@ -186,3 +186,84 @@ func (h *Handler) GetAllTickets(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusOK, "All tickets retrieved successfully", res, nil)
}
// GetTicketByID godoc
// @Summary Get ticket by ID
// @Description Retrieve ticket details by ticket ID
// @Tags ticket
// @Accept json
// @Produce json
// @Param id path int true "Ticket ID"
// @Success 200 {object} domain.TicketRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/ticket/{id} [get]
func (h *Handler) GetTicketByID(c *fiber.Ctx) error {
ticketID := c.Params("id")
id, err := strconv.ParseInt(ticketID, 10, 64)
if err != nil {
h.mongoLoggerSvc.Info("Invalid ticket ID",
zap.String("ticketID", ticketID),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid ticket ID")
}
ticket, err := h.ticketSvc.GetTicketByID(c.Context(), id)
if err != nil {
h.mongoLoggerSvc.Info("Failed to get ticket by ID",
zap.Int64("ticketID", id),
zap.Int("status_code", fiber.StatusNotFound),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusNotFound, "Failed to retrieve ticket")
}
res := domain.TicketRes{
ID: ticket.ID,
Outcomes: ticket.Outcomes,
Amount: ticket.Amount.Float32(),
TotalOdds: ticket.TotalOdds,
CompanyID: ticket.CompanyID,
}
return response.WriteJSON(c, fiber.StatusOK, "Ticket retrieved successfully", res, nil)
}
// GetAllTickets godoc
// @Summary Get all tickets
// @Description Retrieve all tickets
// @Tags ticket
// @Accept json
// @Produce json
// @Success 200 {array} domain.TicketRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/ticket [get]
func (h *Handler) GetAllTickets(c *fiber.Ctx) error {
tickets, err := h.ticketSvc.GetAllTickets(c.Context(), domain.TicketFilter{})
if err != nil {
h.mongoLoggerSvc.Error("Failed to get tickets",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve tickets")
}
res := make([]domain.TicketRes, len(tickets))
for i, ticket := range tickets {
res[i] = domain.TicketRes{
ID: ticket.ID,
Outcomes: ticket.Outcomes,
Amount: ticket.Amount.Float32(),
TotalOdds: ticket.TotalOdds,
}
}
return response.WriteJSON(c, fiber.StatusOK, "All tickets retrieved successfully", res, nil)
}

View File

@ -61,7 +61,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.0.dev17",
"version": "1.0.1",
})
})
@ -110,8 +110,8 @@ func (a *App) initAppRoutes() {
groupV1.Get("/", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"message": "FortuneBet API V1 pre-alpha",
"version": "1.0dev11",
"message": "FortuneBet API V1",
"version": "1.0.1",
})
})
@ -183,8 +183,9 @@ func (a *App) initAppRoutes() {
tenant.Post("/user/sendRegisterCode", h.SendRegisterCode)
tenant.Post("/user/checkPhoneEmailExist", h.CheckPhoneEmailExist)
groupV1.Get("/user/admin-profile", a.authMiddleware, h.AdminProfile)
tenant.Get("/user/customer-profile", a.authMiddleware, h.CustomerProfile)
tenant.Get("/user/admin-profile", a.authMiddleware, h.AdminProfile)
tenant.Get("/user/bets", a.authMiddleware, h.GetBetByUserID)
groupV1.Get("/user/single/:id", a.authMiddleware, h.GetUserByID)
@ -257,8 +258,9 @@ func (a *App) initAppRoutes() {
groupV1.Get("/odds", a.authMiddleware, a.SuperAdminOnly, h.GetAllOdds)
groupV1.Get("/odds/upcoming/:upcoming_id", a.authMiddleware, a.SuperAdminOnly, h.GetOddsByUpcomingID)
groupV1.Get("/odds/upcoming/:upcoming_id/market/:market_id", a.authMiddleware, a.SuperAdminOnly, h.GetOddsByMarketID)
groupV1.Post("/odds/settings", a.SuperAdminOnly, h.SaveOddSettings)
groupV1.Put("/odds/bet-outcome/:id", a.SuperAdminOnly, h.UpdateAllBetOutcomeStatusByOddID)
groupV1.Post("/odds/settings", a.authMiddleware, a.SuperAdminOnly, h.SaveOddSettings)
groupV1.Put("/odds/bet-outcome/:id", a.authMiddleware, a.SuperAdminOnly, h.UpdateAllBetOutcomeStatusByOddID)
groupV1.Put("/odds/bet-outcome", a.authMiddleware, a.SuperAdminOnly, h.BulkUpdateAllBetOutcomeStatusByOddID)
tenant.Get("/odds", h.GetAllTenantOdds)
tenant.Get("/odds/upcoming/:upcoming_id", h.GetTenantOddsByUpcomingID)
@ -334,10 +336,13 @@ func (a *App) initAppRoutes() {
groupV1.Get("/search/company", a.authMiddleware, a.CompanyOnly, h.SearchCompany)
groupV1.Get("/admin-company", a.authMiddleware, a.CompanyOnly, h.GetCompanyForAdmin)
groupV1.Get("/ticket", h.GetAllTickets)
groupV1.Get("/ticket/:id", h.GetTicketByID)
// Ticket Routes
tenant.Post("/ticket", h.CreateTicket)
tenant.Get("/ticket", h.GetAllTickets)
tenant.Get("/ticket/:id", h.GetTicketByID)
tenant.Post("/ticket", h.CreateTenantTicket)
tenant.Get("/ticket", h.GetAllTenantTickets)
tenant.Get("/ticket/:id", h.GetTenantBetByID)
// Bet Routes
tenant.Post("/sport/bet", a.authMiddleware, h.CreateBet)