From 3dfa1255b02223cb764e3bd7e40e74388ffe4bea Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Fri, 10 Oct 2025 14:59:19 +0300 Subject: [PATCH] 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. --- db/migrations/000001_fortune.up.sql | 14 +- db/query/bet.sql | 8 +- db/query/bet_stat.sql | 12 + gen/db/bet.sql.go | 37 ++- gen/db/bet_stat.sql.go | 50 ++- gen/db/events.sql.go | 25 +- gen/db/models.go | 6 +- internal/domain/bet.go | 54 ++-- internal/repository/bet.go | 18 ++ internal/repository/event.go | 15 +- internal/services/bet/notification.go | 4 +- internal/services/bet/port.go | 1 + internal/services/bet/service.go | 44 ++- internal/services/result/notification.go | 292 ++++++++++++++++++ internal/services/result/service.go | 269 ---------------- internal/services/wallet/notification.go | 18 +- internal/services/wallet/transfer.go | 8 +- internal/services/wallet/wallet.go | 26 +- internal/web_server/cron.go | 130 ++++---- internal/web_server/handlers/odd_handler.go | 42 +++ .../web_server/handlers/ticket_handler.go | 87 +++++- internal/web_server/routes.go | 23 +- 22 files changed, 749 insertions(+), 434 deletions(-) create mode 100644 internal/services/result/notification.go diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 8f86485..e3c22c0 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -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 @@ -767,4 +771,4 @@ ADD CONSTRAINT fk_event_settings_company FOREIGN KEY (company_id) REFERENCES com ADD CONSTRAINT fk_event_settings_event FOREIGN KEY (event_id) REFERENCES events (id) ON DELETE CASCADE; ALTER TABLE company_odd_settings ADD CONSTRAINT fk_odds_settings_company FOREIGN KEY (company_id) REFERENCES companies (id) ON DELETE CASCADE, - ADD CONSTRAINT fk_odds_settings_odds_market FOREIGN KEY (odds_market_id) REFERENCES odds_market (id) ON DELETE CASCADE; + ADD CONSTRAINT fk_odds_settings_odds_market FOREIGN KEY (odds_market_id) REFERENCES odds_market (id) ON DELETE CASCADE; \ No newline at end of file diff --git a/db/query/bet.sql b/db/query/bet.sql index c0682a4..4a5307a 100644 --- a/db/query/bet.sql +++ b/db/query/bet.sql @@ -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, diff --git a/db/query/bet_stat.sql b/db/query/bet_stat.sql index 76d8129..223c5e4 100644 --- a/db/query/bet_stat.sql +++ b/db/query/bet_stat.sql @@ -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 diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index 8e6254c..a2e631d 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -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 } diff --git a/gen/db/bet_stat.sql.go b/gen/db/bet_stat.sql.go index 275ef07..03ffd04 100644 --- a/gen/db/bet_stat.sql.go +++ b/gen/db/bet_stat.sql.go @@ -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 diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index a8345fb..380793e 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -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 diff --git a/gen/db/models.go b/gen/db/models.go index 7f7d4f9..62182e5 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -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"` } diff --git a/internal/domain/bet.go b/internal/domain/bet.go index dcb78f0..fcad545 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -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), diff --git a/internal/repository/bet.go b/internal/repository/bet.go index 8de6a2a..d8d3394 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -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, diff --git a/internal/repository/event.go b/internal/repository/event.go index d5d217f..dca45ff 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -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(), diff --git a/internal/services/bet/notification.go b/internal/services/bet/notification.go index f891b94..edeb70c 100644 --- a/internal/services/bet/notification.go +++ b/internal/services/bet/notification.go @@ -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, diff --git a/internal/services/bet/port.go b/internal/services/bet/port.go index 1b52474..84b0fc2 100644 --- a/internal/services/bet/port.go +++ b/internal/services/bet/port.go @@ -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, diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index 1e33f8b..d6ff26a 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -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 { diff --git a/internal/services/result/notification.go b/internal/services/result/notification.go new file mode 100644 index 0000000..3c88649 --- /dev/null +++ b/internal/services/result/notification.go @@ -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(`

%s

+

Weekly Results Summary

+

Period: %s

+ +

All events were processed successfully, and no issues were detected.

+

Best regards,
The System

`, + 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("
  • %d Incomplete Events (%d Bets)
  • ", 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("
  • %d Requires Review (%d Bets)
  • ", 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("
  • %d Postponed Events (%d Bets)
  • ", 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("
  • %d Discarded Events (%d Bets)
  • ", 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("
  • %d Successfully Ended Events (%d Bets)
  • ", 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(`

    %s

    +

    Weekly Results Summary

    +

    Period: %s

    + +

    Totals

    + +

    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(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 +} diff --git a/internal/services/result/service.go b/internal/services/result/service.go index 698e743..e5f09b4 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -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(`

    %s

    -

    Weekly Results Summary

    - -

    All events were processed successfully, and no issues were detected.

    -

    Best regards,
    The System

    `, - 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("
  • %d Incomplete Events (%d Bets)
  • ", 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("
  • %d Requires Review (%d Bets)
  • ", 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("
  • %d Postponed Events (%d Bets)
  • ", 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("
  • %d Discarded Events (%d Bets)
  • ", 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("
  • %d Successfully Ended Events (%d Bets)
  • ", 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(`

    %s

    -

    Weekly Results Summary

    - -

    Totals

    - -

    Next Steps:
    Some events require your attention. Please log into the admin dashboard to review pending issues.

    -

    Best regards,
    The System

    `, - 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{ diff --git a/internal/services/wallet/notification.go b/internal/services/wallet/notification.go index 0112492..8b9bfbe 100644 --- a/internal/services/wallet/notification.go +++ b/internal/services/wallet/notification.go @@ -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 } diff --git a/internal/services/wallet/transfer.go b/internal/services/wallet/transfer.go index 461b51a..b5a19c0 100644 --- a/internal/services/wallet/transfer.go +++ b/internal/services/wallet/transfer.go @@ -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 diff --git a/internal/services/wallet/wallet.go b/internal/services/wallet/wallet.go index a5b3f49..ba48240 100644 --- a/internal/services/wallet/wallet.go +++ b/internal/services/wallet/wallet.go @@ -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 + } } } @@ -213,7 +227,7 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain. zap.Int64("wallet_id", wallet.ID), zap.Error(err)) } - + // Log the transfer here for reference newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ Message: message, diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 6119670..34a8b9e 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -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 { diff --git a/internal/web_server/handlers/odd_handler.go b/internal/web_server/handlers/odd_handler.go index 8c6457a..f7d04c6 100644 --- a/internal/web_server/handlers/odd_handler.go +++ b/internal/web_server/handlers/odd_handler.go @@ -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) +} diff --git a/internal/web_server/handlers/ticket_handler.go b/internal/web_server/handlers/ticket_handler.go index 8f335c3..ff265df 100644 --- a/internal/web_server/handlers/ticket_handler.go +++ b/internal/web_server/handlers/ticket_handler.go @@ -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) +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 8c1a58a..a3d1a36 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -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)