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:
parent
0bab186a8f
commit
3dfa1255b0
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ type GetBet struct {
|
|||
PhoneNumber string
|
||||
UserID int64
|
||||
CompanyID int64
|
||||
CompanySlug string
|
||||
IsShopBet bool
|
||||
CashedOut bool
|
||||
Outcomes []BetOutcome
|
||||
|
|
@ -157,6 +158,7 @@ type BetRes struct {
|
|||
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"`
|
||||
|
|
@ -166,6 +168,7 @@ type BetRes struct {
|
|||
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"`
|
||||
|
|
@ -215,6 +218,7 @@ func ConvertBet(bet GetBet) BetRes {
|
|||
Fullname: bet.FullName,
|
||||
UserID: bet.UserID,
|
||||
CompanyID: bet.CompanyID,
|
||||
CompanySlug: bet.CompanySlug,
|
||||
Outcomes: bet.Outcomes,
|
||||
IsShopBet: bet.IsShopBet,
|
||||
CashedOut: bet.CashedOut,
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -68,7 +68,10 @@ func (s *Store) GetEventsWithSettings(ctx context.Context, companyID int64, filt
|
|||
SportID: filter.SportID.ToPG(),
|
||||
Query: filter.Query.ToPG(),
|
||||
Limit: filter.Limit.ToPG(),
|
||||
Offset: filter.Offset.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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
292
internal/services/result/notification.go
Normal file
292
internal/services/result/notification.go
Normal 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
|
||||
}
|
||||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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] {
|
||||
for _, thresholds := range thresholds {
|
||||
if thresholds < balance && thresholds > (balance-amount.Float32()) {
|
||||
s.SendAdminWalletLowNotification(ctx, wallet)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user