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
+Period: %s
+All events were processed successfully, and no issues were detected.
+Best regards,
The System
%s
+Period: %s
+Next Steps:
Some events require your attention. Please log into the admin dashboard to review pending issues.
Best regards,
The System
%s
-All events were processed successfully, and no issues were detected.
-Best regards,
The System
%s
-Next Steps:
Some events require your attention. Please log into the admin dashboard to review pending issues.
Best regards,
The System