From 7377e50a264c1bf8ebe4103dbfad005048b2fed4 Mon Sep 17 00:00:00 2001 From: Kerod-Fresenbet-Gebremedhin2660 Date: Tue, 30 Sep 2025 16:26:42 +0300 Subject: [PATCH 03/23] CICD Test Version Change --- internal/web_server/routes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index b987fec..f270bbf 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.dev15", + "version": "1.0.dev16-cicdtest", }) }) From 391bf89ea8c398e6b30108965a767ed30810bae6 Mon Sep 17 00:00:00 2001 From: Kerod-Fresenbet-Gebremedhin2660 Date: Tue, 30 Sep 2025 16:43:17 +0300 Subject: [PATCH 04/23] removed cicd test --- internal/web_server/routes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index f270bbf..c1eb759 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.dev16-cicdtest", + "version": "1.0.dev16", }) }) From c00110a50305308d6ec2025b4985541db7e447a8 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Sun, 5 Oct 2025 23:45:31 +0300 Subject: [PATCH 05/23] feat: Enhance league, odds, events and bets functionality - Updated league handling to ensure valid page size checks and improved error handling for sport ID parsing. - Introduced new endpoint to update global league settings with comprehensive validation and error logging. - Refactored odds settings management, including saving, removing, and updating odds settings with enhanced validation. - Added tenant slug retrieval by token, ensuring proper user and company validation. - Improved middleware to check for active company status and adjusted route permissions for various endpoints. - Added SQL script to fix auto-increment desynchronization across multiple tables. --- db/migrations/000001_fortune.up.sql | 4 +- db/query/bet.sql | 37 ++ db/query/company.sql | 4 +- db/query/events.sql | 24 +- db/query/leagues.sql | 11 +- db/query/odds.sql | 13 +- db/scripts/003_fix_autoincrement_desync.sql | 31 ++ gen/db/bet.sql.go | 173 +++++++ gen/db/company.sql.go | 24 +- gen/db/events.sql.go | 53 +- gen/db/leagues.sql.go | 82 ++-- gen/db/models.go | 4 +- gen/db/odds.sql.go | 42 ++ internal/domain/bet.go | 52 ++ internal/domain/event.go | 110 ++++- internal/domain/league.go | 55 ++- internal/domain/odds.go | 12 + internal/domain/result.go | 25 +- internal/repository/bet.go | 53 +- internal/repository/company.go | 10 +- internal/repository/event.go | 15 +- internal/repository/league.go | 6 +- internal/repository/odds.go | 30 +- internal/services/bet/notification.go | 2 +- internal/services/bet/port.go | 3 +- internal/services/bet/service.go | 16 + internal/services/company/port.go | 2 +- internal/services/company/service.go | 4 +- internal/services/event/port.go | 3 +- internal/services/event/service.go | 7 +- internal/services/league/port.go | 3 +- internal/services/league/service.go | 4 + internal/services/odds/port.go | 24 +- internal/services/odds/service.go | 12 + internal/web_server/cron.go | 130 ++--- internal/web_server/handlers/admin.go | 36 +- internal/web_server/handlers/auth_handler.go | 2 +- internal/web_server/handlers/bet_handler.go | 20 + internal/web_server/handlers/event_handler.go | 461 +++++++++++++++++- internal/web_server/handlers/leagues.go | 56 ++- internal/web_server/handlers/odd_handler.go | 139 +++++- internal/web_server/handlers/user.go | 68 +++ internal/web_server/middleware.go | 12 +- internal/web_server/routes.go | 114 +++-- 44 files changed, 1672 insertions(+), 316 deletions(-) create mode 100644 db/scripts/003_fix_autoincrement_desync.sql diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index a30f872..c2e98cb 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -75,8 +75,6 @@ CREATE TABLE IF NOT EXISTS wallets ( updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(user_id, type) ); - - CREATE TABLE refresh_tokens ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, @@ -342,7 +340,7 @@ CREATE TABLE company_event_settings ( event_id BIGINT NOT NULL, is_active BOOLEAN, is_featured BOOLEAN, - winning_upper_limit INT, + winning_upper_limit BIGINT, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE (company_id, event_id) ); diff --git a/db/query/bet.sql b/db/query/bet.sql index 811dd1f..c0682a4 100644 --- a/db/query/bet.sql +++ b/db/query/bet.sql @@ -133,6 +133,38 @@ SELECT * FROM bet_with_outcomes WHERE status = 2 AND processed = false; +-- name: GetBetOutcomeViewByEventID :many +SELECT bet_outcomes.*, + users.first_name, + users.last_name, + bets.amount, + bets.total_odds +FROM bet_outcomes + JOIN bets ON bets.id = bet_outcomes.bet_id + JOIN users ON bets.user_id = users.id +WHERE bet_outcomes.event_id = $1 + AND ( + bets.company_id = sqlc.narg('company_id') + OR sqlc.narg('company_id') IS NULL + ) + AND ( + bet_outcomes.status = sqlc.narg('filter_status') + OR sqlc.narg('filter_status') IS NULL + ) +LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); +-- name: TotalBetOutcomeViewByEventID :one +SELECT count(*) +FROM bet_outcomes + JOIN bets ON bets.id = bet_outcomes.bet_id +WHERE bet_outcomes.event_id = $1 + AND ( + bets.company_id = sqlc.narg('company_id') + OR sqlc.narg('company_id') IS NULL + ) + AND ( + bet_outcomes.status = sqlc.narg('filter_status') + OR sqlc.narg('filter_status') IS NULL + ); -- name: GetBetOutcomeByEventID :many SELECT * FROM bet_outcomes @@ -180,6 +212,11 @@ UPDATE bet_outcomes SEt status = $1 WHERE event_id = $2 RETURNING *; +-- name: UpdateBetOutcomeStatusForOddID :many +UPDATE bet_outcomes +SEt status = $1 +WHERE odd_id = $2 +RETURNING *; -- name: UpdateStatus :exec UPDATE bets SET status = $1, diff --git a/db/query/company.sql b/db/query/company.sql index 00128f5..516775f 100644 --- a/db/query/company.sql +++ b/db/query/company.sql @@ -30,8 +30,8 @@ WHERE ( SELECT * FROM companies_details WHERE id = $1; --- name: GetCompanyIDUsingSlug :one -SELECT id +-- name: GetCompanyUsingSlug :one +SELECT * FROM companies WHERE slug = $1; -- name: SearchCompanyByName :many diff --git a/db/query/events.sql b/db/query/events.sql index ff7b03f..84b10b8 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -56,7 +56,7 @@ SET sport_id = EXCLUDED.sport_id, source = EXCLUDED.source, default_winning_upper_limit = EXCLUDED.default_winning_upper_limit, fetched_at = now(); --- name: SaveEventSettings :exec +-- name: SaveTenantEventSettings :exec INSERT INTO company_event_settings ( company_id, event_id, @@ -300,7 +300,9 @@ FROM events e WHERE e.id = $1 LIMIT 1; -- name: GetSportAndLeagueIDs :one -SELECT sport_id, league_id FROM events +SELECT sport_id, + league_id +FROM events WHERE id = $1; -- name: UpdateMatchResult :exec UPDATE events @@ -313,8 +315,22 @@ FROM events WHERE id = $1; -- name: UpdateEventMonitored :exec UPDATE events -SET is_monitored = $1 +SET is_monitored = $1, + updated_at = CURRENT_TIMESTAMP WHERE id = $2; +-- name: UpdateGlobalEventSettings :exec +UPDATE events +SET default_is_active = COALESCE(sqlc.narg(default_is_active), default_is_active), + default_is_featured = COALESCE( + sqlc.narg(default_is_featured), + default_is_featured + ), + default_winning_upper_limit = COALESCE( + sqlc.narg(default_winning_upper_limit), + default_winning_upper_limit + ), + updated_at = CURRENT_TIMESTAMP +WHERE id = $1; -- name: DeleteEvent :exec DELETE FROM events -WHERE id = $1; +WHERE id = $1; \ No newline at end of file diff --git a/db/query/leagues.sql b/db/query/leagues.sql index 476c3e8..b45460f 100644 --- a/db/query/leagues.sql +++ b/db/query/leagues.sql @@ -14,7 +14,7 @@ SET name = EXCLUDED.name, country_code = EXCLUDED.country_code, bet365_id = EXCLUDED.bet365_id, sport_id = EXCLUDED.sport_id; --- name: InsertLeagueSettings :exec +-- name: SaveLeagueSettings :exec INSERT INTO company_league_settings ( company_id, league_id, @@ -118,7 +118,7 @@ SET name = COALESCE(sqlc.narg('name'), name), bet365_id = COALESCE(sqlc.narg('bet365_id'), bet365_id), sport_id = COALESCE(sqlc.narg('sport_id'), sport_id) WHERE id = $1; --- name: UpdateLeagueSettings :exec +-- name: UpdateCompanyLeagueSettings :exec UPDATE company_league_settings SET is_active = COALESCE(sqlc.narg('is_active'), is_active), is_featured = COALESCE( @@ -126,4 +126,9 @@ SET is_active = COALESCE(sqlc.narg('is_active'), is_active), is_featured ) WHERE league_id = $1 - AND company_id = $2; \ No newline at end of file + AND company_id = $2; +-- name: UpdateGlobalLeagueSettings :exec +UPDATE leagues +SET default_is_active = COALESCE(sqlc.narg('is_active'), default_is_active), + default_is_featured = COALESCE(sqlc.narg('is_featured'), default_is_featured) +WHERE id = $1; \ No newline at end of file diff --git a/db/query/odds.sql b/db/query/odds.sql index dc467c6..8950868 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -143,4 +143,15 @@ WHERE event_id = $1 LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- name: DeleteOddsForEvent :exec DELETE FROM odds_market -Where event_id = $1; \ No newline at end of file +Where event_id = $1; +-- name: DeleteAllCompanyOddsSetting :exec +DELETE FROM company_odd_settings +WHERE company_id = $1; +-- name: DeleteCompanyOddsSettingByOddMarketID :exec +DELETE FROM company_odd_settings +WHERE company_id = $1 + AND odds_market_id = $2; +-- name: UpdateGlobalOddsSetting :exec +UPDATE odds_market +SET default_is_active = COALESCE(sqlc.narg(default_is_active), default_is_active) +WHERE id = $1; \ No newline at end of file diff --git a/db/scripts/003_fix_autoincrement_desync.sql b/db/scripts/003_fix_autoincrement_desync.sql new file mode 100644 index 0000000..835e10e --- /dev/null +++ b/db/scripts/003_fix_autoincrement_desync.sql @@ -0,0 +1,31 @@ +-- For each table with an id sequence +SELECT setval( + pg_get_serial_sequence('users', 'id'), + COALESCE(MAX(id), 1) + ) +FROM users; +SELECT setval( + pg_get_serial_sequence('wallets', 'id'), + COALESCE(MAX(id), 1) + ) +FROM wallets; +SELECT setval( + pg_get_serial_sequence('customer_wallets', 'id'), + COALESCE(MAX(id), 1) + ) +FROM customer_wallets; +SELECT setval( + pg_get_serial_sequence('companies', 'id'), + COALESCE(MAX(id), 1) + ) +FROM companies; +SELECT setval( + pg_get_serial_sequence('branches', 'id'), + COALESCE(MAX(id), 1) + ) +FROM branches; +SELECT setval( + pg_get_serial_sequence('supported_operations', 'id'), + COALESCE(MAX(id), 1) + ) +FROM supported_operations; \ No newline at end of file diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index 9813c89..8e6254c 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -448,6 +448,103 @@ func (q *Queries) GetBetOutcomeCountByOddID(ctx context.Context, oddID int64) (i return count, err } +const GetBetOutcomeViewByEventID = `-- name: GetBetOutcomeViewByEventID :many +SELECT bet_outcomes.id, bet_outcomes.bet_id, bet_outcomes.sport_id, bet_outcomes.event_id, bet_outcomes.odd_id, bet_outcomes.home_team_name, bet_outcomes.away_team_name, bet_outcomes.market_id, bet_outcomes.market_name, bet_outcomes.odd, bet_outcomes.odd_name, bet_outcomes.odd_header, bet_outcomes.odd_handicap, bet_outcomes.status, bet_outcomes.expires, + users.first_name, + users.last_name, + bets.amount, + bets.total_odds +FROM bet_outcomes + JOIN bets ON bets.id = bet_outcomes.bet_id + JOIN users ON bets.user_id = users.id +WHERE bet_outcomes.event_id = $1 + AND ( + bets.company_id = $2 + OR $2 IS NULL + ) + AND ( + bet_outcomes.status = $3 + OR $3 IS NULL + ) +LIMIT $5 OFFSET $4 +` + +type GetBetOutcomeViewByEventIDParams struct { + EventID int64 `json:"event_id"` + CompanyID pgtype.Int8 `json:"company_id"` + FilterStatus pgtype.Int4 `json:"filter_status"` + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` +} + +type GetBetOutcomeViewByEventIDRow struct { + ID int64 `json:"id"` + BetID int64 `json:"bet_id"` + SportID int64 `json:"sport_id"` + EventID int64 `json:"event_id"` + OddID int64 `json:"odd_id"` + HomeTeamName string `json:"home_team_name"` + AwayTeamName string `json:"away_team_name"` + MarketID int64 `json:"market_id"` + MarketName string `json:"market_name"` + Odd float32 `json:"odd"` + OddName string `json:"odd_name"` + OddHeader string `json:"odd_header"` + OddHandicap string `json:"odd_handicap"` + Status int32 `json:"status"` + Expires pgtype.Timestamp `json:"expires"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Amount int64 `json:"amount"` + TotalOdds float32 `json:"total_odds"` +} + +func (q *Queries) GetBetOutcomeViewByEventID(ctx context.Context, arg GetBetOutcomeViewByEventIDParams) ([]GetBetOutcomeViewByEventIDRow, error) { + rows, err := q.db.Query(ctx, GetBetOutcomeViewByEventID, + arg.EventID, + arg.CompanyID, + arg.FilterStatus, + arg.Offset, + arg.Limit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetBetOutcomeViewByEventIDRow + for rows.Next() { + var i GetBetOutcomeViewByEventIDRow + if err := rows.Scan( + &i.ID, + &i.BetID, + &i.SportID, + &i.EventID, + &i.OddID, + &i.HomeTeamName, + &i.AwayTeamName, + &i.MarketID, + &i.MarketName, + &i.Odd, + &i.OddName, + &i.OddHeader, + &i.OddHandicap, + &i.Status, + &i.Expires, + &i.FirstName, + &i.LastName, + &i.Amount, + &i.TotalOdds, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + 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 FROM bet_with_outcomes @@ -557,6 +654,34 @@ func (q *Queries) GetTotalBets(ctx context.Context, arg GetTotalBetsParams) (int return count, err } +const TotalBetOutcomeViewByEventID = `-- name: TotalBetOutcomeViewByEventID :one +SELECT count(*) +FROM bet_outcomes + JOIN bets ON bets.id = bet_outcomes.bet_id +WHERE bet_outcomes.event_id = $1 + AND ( + bets.company_id = $2 + OR $2 IS NULL + ) + AND ( + bet_outcomes.status = $3 + OR $3 IS NULL + ) +` + +type TotalBetOutcomeViewByEventIDParams struct { + EventID int64 `json:"event_id"` + CompanyID pgtype.Int8 `json:"company_id"` + FilterStatus pgtype.Int4 `json:"filter_status"` +} + +func (q *Queries) TotalBetOutcomeViewByEventID(ctx context.Context, arg TotalBetOutcomeViewByEventIDParams) (int64, error) { + row := q.db.QueryRow(ctx, TotalBetOutcomeViewByEventID, arg.EventID, arg.CompanyID, arg.FilterStatus) + var count int64 + err := row.Scan(&count) + return count, err +} + const UpdateBetOutcomeStatus = `-- name: UpdateBetOutcomeStatus :one UPDATE bet_outcomes SET status = $1 @@ -675,6 +800,54 @@ func (q *Queries) UpdateBetOutcomeStatusForEvent(ctx context.Context, arg Update return items, nil } +const UpdateBetOutcomeStatusForOddID = `-- name: UpdateBetOutcomeStatusForOddID :many +UPDATE bet_outcomes +SEt status = $1 +WHERE odd_id = $2 +RETURNING id, bet_id, sport_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, status, expires +` + +type UpdateBetOutcomeStatusForOddIDParams struct { + Status int32 `json:"status"` + OddID int64 `json:"odd_id"` +} + +func (q *Queries) UpdateBetOutcomeStatusForOddID(ctx context.Context, arg UpdateBetOutcomeStatusForOddIDParams) ([]BetOutcome, error) { + rows, err := q.db.Query(ctx, UpdateBetOutcomeStatusForOddID, arg.Status, arg.OddID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []BetOutcome + for rows.Next() { + var i BetOutcome + if err := rows.Scan( + &i.ID, + &i.BetID, + &i.SportID, + &i.EventID, + &i.OddID, + &i.HomeTeamName, + &i.AwayTeamName, + &i.MarketID, + &i.MarketName, + &i.Odd, + &i.OddName, + &i.OddHeader, + &i.OddHandicap, + &i.Status, + &i.Expires, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const UpdateBetWithCashback = `-- name: UpdateBetWithCashback :exec UPDATE bets SET processed = $1 diff --git a/gen/db/company.sql.go b/gen/db/company.sql.go index 506eaca..ba728e7 100644 --- a/gen/db/company.sql.go +++ b/gen/db/company.sql.go @@ -153,17 +153,27 @@ func (q *Queries) GetCompanyByID(ctx context.Context, id int64) (CompaniesDetail return i, err } -const GetCompanyIDUsingSlug = `-- name: GetCompanyIDUsingSlug :one -SELECT id +const GetCompanyUsingSlug = `-- name: GetCompanyUsingSlug :one +SELECT id, name, slug, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at FROM companies WHERE slug = $1 ` -func (q *Queries) GetCompanyIDUsingSlug(ctx context.Context, slug string) (int64, error) { - row := q.db.QueryRow(ctx, GetCompanyIDUsingSlug, slug) - var id int64 - err := row.Scan(&id) - return id, err +func (q *Queries) GetCompanyUsingSlug(ctx context.Context, slug string) (Company, error) { + row := q.db.QueryRow(ctx, GetCompanyUsingSlug, slug) + var i Company + err := row.Scan( + &i.ID, + &i.Name, + &i.Slug, + &i.AdminID, + &i.WalletID, + &i.DeductedPercentage, + &i.IsActive, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err } const SearchCompanyByName = `-- name: SearchCompanyByName :many diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index fc793b1..a8345fb 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -282,7 +282,7 @@ type GetEventWithSettingByIDRow struct { CompanyID pgtype.Int8 `json:"company_id"` IsActive bool `json:"is_active"` IsFeatured bool `json:"is_featured"` - WinningUpperLimit int32 `json:"winning_upper_limit"` + WinningUpperLimit int64 `json:"winning_upper_limit"` UpdatedAt pgtype.Timestamp `json:"updated_at"` LeagueCc pgtype.Text `json:"league_cc"` } @@ -440,7 +440,7 @@ type GetEventsWithSettingsRow struct { CompanyID pgtype.Int8 `json:"company_id"` IsActive bool `json:"is_active"` IsFeatured bool `json:"is_featured"` - WinningUpperLimit int32 `json:"winning_upper_limit"` + WinningUpperLimit int64 `json:"winning_upper_limit"` UpdatedAt pgtype.Timestamp `json:"updated_at"` LeagueCc pgtype.Text `json:"league_cc"` } @@ -514,7 +514,9 @@ func (q *Queries) GetEventsWithSettings(ctx context.Context, arg GetEventsWithSe } const GetSportAndLeagueIDs = `-- name: GetSportAndLeagueIDs :one -SELECT sport_id, league_id FROM events +SELECT sport_id, + league_id +FROM events WHERE id = $1 ` @@ -831,7 +833,7 @@ func (q *Queries) ListLiveEvents(ctx context.Context) ([]int64, error) { return items, nil } -const SaveEventSettings = `-- name: SaveEventSettings :exec +const SaveTenantEventSettings = `-- name: SaveTenantEventSettings :exec INSERT INTO company_event_settings ( company_id, event_id, @@ -846,16 +848,16 @@ SET is_active = EXCLUDED.is_active, winning_upper_limit = EXCLUDED.winning_upper_limit ` -type SaveEventSettingsParams struct { +type SaveTenantEventSettingsParams struct { CompanyID int64 `json:"company_id"` EventID int64 `json:"event_id"` IsActive pgtype.Bool `json:"is_active"` IsFeatured pgtype.Bool `json:"is_featured"` - WinningUpperLimit pgtype.Int4 `json:"winning_upper_limit"` + WinningUpperLimit pgtype.Int8 `json:"winning_upper_limit"` } -func (q *Queries) SaveEventSettings(ctx context.Context, arg SaveEventSettingsParams) error { - _, err := q.db.Exec(ctx, SaveEventSettings, +func (q *Queries) SaveTenantEventSettings(ctx context.Context, arg SaveTenantEventSettingsParams) error { + _, err := q.db.Exec(ctx, SaveTenantEventSettings, arg.CompanyID, arg.EventID, arg.IsActive, @@ -867,7 +869,8 @@ func (q *Queries) SaveEventSettings(ctx context.Context, arg SaveEventSettingsPa const UpdateEventMonitored = `-- name: UpdateEventMonitored :exec UPDATE events -SET is_monitored = $1 +SET is_monitored = $1, + updated_at = CURRENT_TIMESTAMP WHERE id = $2 ` @@ -881,6 +884,38 @@ func (q *Queries) UpdateEventMonitored(ctx context.Context, arg UpdateEventMonit return err } +const UpdateGlobalEventSettings = `-- name: UpdateGlobalEventSettings :exec +UPDATE events +SET default_is_active = COALESCE($2, default_is_active), + default_is_featured = COALESCE( + $3, + default_is_featured + ), + default_winning_upper_limit = COALESCE( + $4, + default_winning_upper_limit + ), + updated_at = CURRENT_TIMESTAMP +WHERE id = $1 +` + +type UpdateGlobalEventSettingsParams struct { + ID int64 `json:"id"` + DefaultIsActive pgtype.Bool `json:"default_is_active"` + DefaultIsFeatured pgtype.Bool `json:"default_is_featured"` + DefaultWinningUpperLimit pgtype.Int8 `json:"default_winning_upper_limit"` +} + +func (q *Queries) UpdateGlobalEventSettings(ctx context.Context, arg UpdateGlobalEventSettingsParams) error { + _, err := q.db.Exec(ctx, UpdateGlobalEventSettings, + arg.ID, + arg.DefaultIsActive, + arg.DefaultIsFeatured, + arg.DefaultWinningUpperLimit, + ) + return err +} + const UpdateMatchResult = `-- name: UpdateMatchResult :exec UPDATE events SET score = $1, diff --git a/gen/db/leagues.sql.go b/gen/db/leagues.sql.go index 1d2800b..912e257 100644 --- a/gen/db/leagues.sql.go +++ b/gen/db/leagues.sql.go @@ -292,7 +292,7 @@ func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) erro return err } -const InsertLeagueSettings = `-- name: InsertLeagueSettings :exec +const SaveLeagueSettings = `-- name: SaveLeagueSettings :exec INSERT INTO company_league_settings ( company_id, league_id, @@ -305,15 +305,15 @@ SET is_active = EXCLUDED.is_active, is_featured = EXCLUDED.is_featured ` -type InsertLeagueSettingsParams struct { +type SaveLeagueSettingsParams struct { CompanyID int64 `json:"company_id"` LeagueID int64 `json:"league_id"` IsActive pgtype.Bool `json:"is_active"` IsFeatured pgtype.Bool `json:"is_featured"` } -func (q *Queries) InsertLeagueSettings(ctx context.Context, arg InsertLeagueSettingsParams) error { - _, err := q.db.Exec(ctx, InsertLeagueSettings, +func (q *Queries) SaveLeagueSettings(ctx context.Context, arg SaveLeagueSettingsParams) error { + _, err := q.db.Exec(ctx, SaveLeagueSettings, arg.CompanyID, arg.LeagueID, arg.IsActive, @@ -322,6 +322,52 @@ func (q *Queries) InsertLeagueSettings(ctx context.Context, arg InsertLeagueSett return err } +const UpdateCompanyLeagueSettings = `-- name: UpdateCompanyLeagueSettings :exec +UPDATE company_league_settings +SET is_active = COALESCE($3, is_active), + is_featured = COALESCE( + $4, + is_featured + ) +WHERE league_id = $1 + AND company_id = $2 +` + +type UpdateCompanyLeagueSettingsParams struct { + LeagueID int64 `json:"league_id"` + CompanyID int64 `json:"company_id"` + IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` +} + +func (q *Queries) UpdateCompanyLeagueSettings(ctx context.Context, arg UpdateCompanyLeagueSettingsParams) error { + _, err := q.db.Exec(ctx, UpdateCompanyLeagueSettings, + arg.LeagueID, + arg.CompanyID, + arg.IsActive, + arg.IsFeatured, + ) + return err +} + +const UpdateGlobalLeagueSettings = `-- name: UpdateGlobalLeagueSettings :exec +UPDATE leagues +SET default_is_active = COALESCE($2, default_is_active), + default_is_featured = COALESCE($3, default_is_featured) +WHERE id = $1 +` + +type UpdateGlobalLeagueSettingsParams struct { + ID int64 `json:"id"` + IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` +} + +func (q *Queries) UpdateGlobalLeagueSettings(ctx context.Context, arg UpdateGlobalLeagueSettingsParams) error { + _, err := q.db.Exec(ctx, UpdateGlobalLeagueSettings, arg.ID, arg.IsActive, arg.IsFeatured) + return err +} + const UpdateLeague = `-- name: UpdateLeague :exec UPDATE leagues SET name = COALESCE($2, name), @@ -349,31 +395,3 @@ func (q *Queries) UpdateLeague(ctx context.Context, arg UpdateLeagueParams) erro ) return err } - -const UpdateLeagueSettings = `-- name: UpdateLeagueSettings :exec -UPDATE company_league_settings -SET is_active = COALESCE($3, is_active), - is_featured = COALESCE( - $4, - is_featured - ) -WHERE league_id = $1 - AND company_id = $2 -` - -type UpdateLeagueSettingsParams struct { - LeagueID int64 `json:"league_id"` - CompanyID int64 `json:"company_id"` - IsActive pgtype.Bool `json:"is_active"` - IsFeatured pgtype.Bool `json:"is_featured"` -} - -func (q *Queries) UpdateLeagueSettings(ctx context.Context, arg UpdateLeagueSettingsParams) error { - _, err := q.db.Exec(ctx, UpdateLeagueSettings, - arg.LeagueID, - arg.CompanyID, - arg.IsActive, - arg.IsFeatured, - ) - return err -} diff --git a/gen/db/models.go b/gen/db/models.go index cff8694..339efb8 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -177,7 +177,7 @@ type CompanyEventSetting struct { EventID int64 `json:"event_id"` IsActive pgtype.Bool `json:"is_active"` IsFeatured pgtype.Bool `json:"is_featured"` - WinningUpperLimit pgtype.Int4 `json:"winning_upper_limit"` + WinningUpperLimit pgtype.Int8 `json:"winning_upper_limit"` UpdatedAt pgtype.Timestamp `json:"updated_at"` } @@ -405,7 +405,7 @@ type EventWithSetting struct { CompanyID pgtype.Int8 `json:"company_id"` IsActive bool `json:"is_active"` IsFeatured bool `json:"is_featured"` - WinningUpperLimit int32 `json:"winning_upper_limit"` + WinningUpperLimit int64 `json:"winning_upper_limit"` UpdatedAt pgtype.Timestamp `json:"updated_at"` LeagueCc pgtype.Text `json:"league_cc"` } diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index ac9974c..e7c687e 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -11,6 +11,32 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const DeleteAllCompanyOddsSetting = `-- name: DeleteAllCompanyOddsSetting :exec +DELETE FROM company_odd_settings +WHERE company_id = $1 +` + +func (q *Queries) DeleteAllCompanyOddsSetting(ctx context.Context, companyID int64) error { + _, err := q.db.Exec(ctx, DeleteAllCompanyOddsSetting, companyID) + return err +} + +const DeleteCompanyOddsSettingByOddMarketID = `-- name: DeleteCompanyOddsSettingByOddMarketID :exec +DELETE FROM company_odd_settings +WHERE company_id = $1 + AND odds_market_id = $2 +` + +type DeleteCompanyOddsSettingByOddMarketIDParams struct { + CompanyID int64 `json:"company_id"` + OddsMarketID int64 `json:"odds_market_id"` +} + +func (q *Queries) DeleteCompanyOddsSettingByOddMarketID(ctx context.Context, arg DeleteCompanyOddsSettingByOddMarketIDParams) error { + _, err := q.db.Exec(ctx, DeleteCompanyOddsSettingByOddMarketID, arg.CompanyID, arg.OddsMarketID) + return err +} + const DeleteOddsForEvent = `-- name: DeleteOddsForEvent :exec DELETE FROM odds_market Where event_id = $1 @@ -568,3 +594,19 @@ func (q *Queries) SaveOddSettings(ctx context.Context, arg SaveOddSettingsParams ) return err } + +const UpdateGlobalOddsSetting = `-- name: UpdateGlobalOddsSetting :exec +UPDATE odds_market +SET default_is_active = COALESCE($2, default_is_active) +WHERE id = $1 +` + +type UpdateGlobalOddsSettingParams struct { + ID int64 `json:"id"` + DefaultIsActive pgtype.Bool `json:"default_is_active"` +} + +func (q *Queries) UpdateGlobalOddsSetting(ctx context.Context, arg UpdateGlobalOddsSettingParams) error { + _, err := q.db.Exec(ctx, UpdateGlobalOddsSetting, arg.ID, arg.DefaultIsActive) + return err +} diff --git a/internal/domain/bet.go b/internal/domain/bet.go index e4939ba..dcb78f0 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -163,6 +163,35 @@ type BetRes struct { FastCode string `json:"fast_code"` } +type BetOutcomeViewRes struct { + ID int64 `json:"id"` + BetID int64 `json:"bet_id"` + SportID int64 `json:"sport_id"` + EventID int64 `json:"event_id"` + OddID int64 `json:"odd_id"` + HomeTeamName string `json:"home_team_name"` + AwayTeamName string `json:"away_team_name"` + MarketID int64 `json:"market_id"` + MarketName string `json:"market_name"` + Odd float32 `json:"odd"` + OddName string `json:"odd_name"` + OddHeader string `json:"odd_header"` + OddHandicap string `json:"odd_handicap"` + Status OutcomeStatus `json:"status"` + Expires time.Time `json:"expires"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Amount int64 `json:"amount"` + TotalOdds float32 `json:"total_odds"` +} + +type BetOutcomeViewFilter struct { + OutcomeStatus ValidOutcomeStatus + CompanyID ValidInt64 + Limit ValidInt32 + Offset ValidInt32 +} + func ConvertCreateBetRes(bet Bet, createdNumber int64) CreateBetRes { return CreateBetRes{ ID: bet.ID, @@ -228,6 +257,29 @@ func ConvertDBBetOutcomes(outcome dbgen.BetOutcome) BetOutcome { Expires: outcome.Expires.Time, } } +func ConvertDBBetOutcomesView(outcome dbgen.GetBetOutcomeViewByEventIDRow) BetOutcomeViewRes { + return BetOutcomeViewRes{ + ID: outcome.ID, + BetID: outcome.BetID, + SportID: outcome.SportID, + EventID: outcome.EventID, + OddID: outcome.OddID, + HomeTeamName: outcome.HomeTeamName, + AwayTeamName: outcome.AwayTeamName, + MarketID: outcome.MarketID, + MarketName: outcome.MarketName, + Odd: outcome.Odd, + OddName: outcome.OddName, + OddHeader: outcome.OddHeader, + OddHandicap: outcome.OddHandicap, + Status: OutcomeStatus(outcome.Status), + Expires: outcome.Expires.Time, + FirstName: outcome.FirstName, + LastName: outcome.LastName, + Amount: outcome.Amount, + TotalOdds: outcome.TotalOdds, + } +} func ConvertDBBetWithOutcomes(bet dbgen.BetWithOutcome) GetBet { var outcomes []BetOutcome = make([]BetOutcome, 0, len(bet.Outcomes)) diff --git a/internal/domain/event.go b/internal/domain/event.go index d898150..de5ac3a 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -1,6 +1,7 @@ package domain import ( + "fmt" "time" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" @@ -50,6 +51,58 @@ const ( EVENT_SOURCE_ENET EventSource = "enetpulse" ) +// --- EventStatus Validation --- +func (s EventStatus) IsValid() bool { + switch s { + case STATUS_PENDING, + STATUS_IN_PLAY, + STATUS_TO_BE_FIXED, + STATUS_ENDED, + STATUS_POSTPONED, + STATUS_CANCELLED, + STATUS_WALKOVER, + STATUS_INTERRUPTED, + STATUS_ABANDONED, + STATUS_RETIRED, + STATUS_SUSPENDED, + STATUS_DECIDED_BY_FA, + STATUS_REMOVED: + return true + default: + return false + } +} + +func ParseEventStatus(val string) (EventStatus, error) { + s := EventStatus(val) + if !s.IsValid() { + return "", fmt.Errorf("invalid EventStatus: %q", val) + } + return s, nil +} + +// --- EventSource Validation --- +func (s EventSource) IsValid() bool { + switch s { + case EVENT_SOURCE_BET365, + EVENT_SOURCE_BWIN, + EVENT_SOURCE_BETFAIR, + EVENT_SOURCE_1XBET, + EVENT_SOURCE_ENET: + return true + default: + return false + } +} + +func ParseEventSource(val string) (EventSource, error) { + s := EventSource(val) + if !s.IsValid() { + return "", fmt.Errorf("invalid EventSource: %q", val) + } + return s, nil +} + type BaseEvent struct { ID int64 SourceEventID string @@ -128,7 +181,7 @@ type EventWithSettings struct { IsMonitored bool IsFeatured bool IsActive bool - WinningUpperLimit int32 + WinningUpperLimit int64 DefaultIsFeatured bool DefaultIsActive bool DefaultWinningUpperLimit int64 @@ -181,7 +234,7 @@ type EventWithSettingsRes struct { IsMonitored bool `json:"is_monitored"` IsFeatured bool `json:"is_featured"` IsActive bool `json:"is_active"` - WinningUpperLimit int32 `json:"winning_upper_limit"` + WinningUpperLimit int64 `json:"winning_upper_limit"` DefaultIsFeatured bool `json:"default_is_featured"` DefaultIsActive bool `json:"default_is_active"` DefaultWinningUpperLimit int64 `json:"default_winning_upper_limit"` @@ -204,12 +257,18 @@ type EventSettings struct { UpdatedAt time.Time } -type CreateEventSettings struct { +type UpdateTenantEventSettings struct { CompanyID int64 EventID int64 IsActive ValidBool IsFeatured ValidBool - WinningUpperLimit ValidInt + WinningUpperLimit ValidInt64 +} +type UpdateGlobalEventSettings struct { + EventID int64 + IsActive ValidBool + IsFeatured ValidBool + WinningUpperLimit ValidInt64 } type ValidEventStatus struct { @@ -331,8 +390,8 @@ func ConvertCreateEvent(e CreateEvent) dbgen.InsertEventParams { } } -func ConvertCreateEventSettings(eventSettings CreateEventSettings) dbgen.SaveEventSettingsParams { - return dbgen.SaveEventSettingsParams{ +func ConvertCreateEventSettings(eventSettings UpdateTenantEventSettings) dbgen.SaveTenantEventSettingsParams { + return dbgen.SaveTenantEventSettingsParams{ CompanyID: eventSettings.CompanyID, EventID: eventSettings.EventID, IsActive: eventSettings.IsActive.ToPG(), @@ -343,17 +402,19 @@ func ConvertCreateEventSettings(eventSettings CreateEventSettings) dbgen.SaveEve func ConvertDBEventWithSetting(event dbgen.EventWithSetting) EventWithSettings { return EventWithSettings{ - ID: event.ID, - SportID: event.SportID, - MatchName: event.MatchName, - HomeTeam: event.HomeTeam, - AwayTeam: event.AwayTeam, - HomeTeamID: event.HomeTeamID, - AwayTeamID: event.AwayTeamID, - HomeTeamImage: event.HomeKitImage, - AwayTeamImage: event.AwayKitImage, - LeagueID: event.LeagueID, - LeagueName: event.LeagueName, + ID: event.ID, + SourceEventID: event.SourceEventID, + WinningUpperLimit: event.WinningUpperLimit, + SportID: event.SportID, + MatchName: event.MatchName, + HomeTeam: event.HomeTeam, + AwayTeam: event.AwayTeam, + HomeTeamID: event.HomeTeamID, + AwayTeamID: event.AwayTeamID, + HomeTeamImage: event.HomeKitImage, + AwayTeamImage: event.AwayKitImage, + LeagueID: event.LeagueID, + LeagueName: event.LeagueName, LeagueCC: ValidString{ Value: event.LeagueCc.String, Valid: event.LeagueCc.Valid, @@ -401,8 +462,8 @@ func ConvertDBEventWithSettings(events []dbgen.EventWithSetting) []EventWithSett return result } -func ConvertUpdateEventSettings(event CreateEventSettings) dbgen.SaveEventSettingsParams { - return dbgen.SaveEventSettingsParams{ +func ConvertUpdateTenantEventSettings(event UpdateTenantEventSettings) dbgen.SaveTenantEventSettingsParams { + return dbgen.SaveTenantEventSettingsParams{ EventID: event.EventID, CompanyID: event.CompanyID, IsActive: event.IsActive.ToPG(), @@ -410,10 +471,19 @@ func ConvertUpdateEventSettings(event CreateEventSettings) dbgen.SaveEventSettin WinningUpperLimit: event.WinningUpperLimit.ToPG(), } } +func ConvertUpdateGlobalEventSettings(event UpdateGlobalEventSettings) dbgen.UpdateGlobalEventSettingsParams { + return dbgen.UpdateGlobalEventSettingsParams{ + ID: event.EventID, + DefaultIsActive: event.IsActive.ToPG(), + DefaultIsFeatured: event.IsFeatured.ToPG(), + DefaultWinningUpperLimit: event.WinningUpperLimit.ToPG(), + } +} func ConvertEventRes(event BaseEvent) BaseEventRes { return BaseEventRes{ ID: event.ID, + SourceEventID: event.SourceEventID, SportID: event.SportID, MatchName: event.MatchName, HomeTeam: event.HomeTeam, @@ -452,6 +522,7 @@ func ConvertEventResList(events []BaseEvent) []BaseEventRes { func ConvertEventWitSettingRes(event EventWithSettings) EventWithSettingsRes { return EventWithSettingsRes{ ID: event.ID, + SourceEventID: event.SourceEventID, SportID: event.SportID, MatchName: event.MatchName, HomeTeam: event.HomeTeam, @@ -480,6 +551,7 @@ func ConvertEventWitSettingRes(event EventWithSettings) EventWithSettingsRes { MatchPeriod: event.MatchPeriod.Value, IsLive: event.IsLive, FetchedAt: event.FetchedAt.UTC(), + UpdatedAt: event.UpdatedAt, } } diff --git a/internal/domain/league.go b/internal/domain/league.go index 6743b6e..c2de5d1 100644 --- a/internal/domain/league.go +++ b/internal/domain/league.go @@ -87,6 +87,17 @@ type UpdateLeague struct { SportID ValidInt32 `json:"sport_id" example:"1"` } +type UpdateLeagueSettingsReq struct { + IsFeatured *bool `json:"is_featured" example:"true"` + IsActive *bool `json:"is_active" example:"true"` +} + +type UpdateGlobalLeagueSettings struct { + ID int64 + DefaultIsActive ValidBool + DefaultIsFeatured ValidBool +} + type LeagueFilter struct { Query ValidString CountryCode ValidString @@ -109,8 +120,8 @@ func ConvertCreateLeague(league CreateLeague) dbgen.InsertLeagueParams { } } -func ConvertCreateLeagueSettings(leagueSetting CreateLeagueSettings) dbgen.InsertLeagueSettingsParams { - return dbgen.InsertLeagueSettingsParams{ +func ConvertCreateLeagueSettings(leagueSetting CreateLeagueSettings) dbgen.SaveLeagueSettingsParams { + return dbgen.SaveLeagueSettingsParams{ CompanyID: leagueSetting.CompanyID, LeagueID: leagueSetting.LeagueID, IsActive: leagueSetting.IsActive.ToPG(), @@ -149,7 +160,7 @@ func ConvertDBLeagueWithSetting(lws dbgen.GetAllLeaguesWithSettingsRow) LeagueWi ID: lws.ID, Name: lws.Name, CompanyID: lws.CompanyID.Int64, - CountryCode: ValidString{ + CountryCode: ValidString{ Value: lws.CountryCode.String, Valid: lws.CountryCode.Valid, }, @@ -187,15 +198,15 @@ func ConvertUpdateLeague(updateLeague UpdateLeague) dbgen.UpdateLeagueParams { func ConvertLeagueWithSettingRes(lws LeagueWithSettings) LeagueWithSettingsRes { return LeagueWithSettingsRes{ - ID: lws.ID, - Name: lws.Name, - CompanyID: lws.CompanyID, - CountryCode: lws.CountryCode.Value, - Bet365ID: lws.Bet365ID.Value, - IsActive: lws.IsActive, - SportID: lws.SportID, - IsFeatured: lws.IsFeatured, - UpdatedAt: lws.UpdatedAt, + ID: lws.ID, + Name: lws.Name, + CompanyID: lws.CompanyID, + CountryCode: lws.CountryCode.Value, + Bet365ID: lws.Bet365ID.Value, + IsActive: lws.IsActive, + SportID: lws.SportID, + IsFeatured: lws.IsFeatured, + UpdatedAt: lws.UpdatedAt, DefaultIsActive: lws.DefaultIsActive, DefaultIsFeatured: lws.DefaultIsFeatured, } @@ -213,12 +224,12 @@ func ConvertLeagueWithSettingResList(leagues []LeagueWithSettings) []LeagueWithS func ConvertBaseLeagueRes(league BaseLeague) BaseLeagueRes { return BaseLeagueRes{ - ID: league.ID, - Name: league.Name, - CountryCode: league.CountryCode.Value, - Bet365ID: league.Bet365ID.Value, - SportID: league.SportID, - DefaultIsActive: league.DefaultIsActive, + ID: league.ID, + Name: league.Name, + CountryCode: league.CountryCode.Value, + Bet365ID: league.Bet365ID.Value, + SportID: league.SportID, + DefaultIsActive: league.DefaultIsActive, DefaultIsFeatured: league.DefaultIsFeatured, } } @@ -231,3 +242,11 @@ func ConvertBaseLeagueResList(leagues []BaseLeague) []BaseLeagueRes { return result } + +func ConvertUpdateGlobalLeagueSetting(league UpdateGlobalLeagueSettings) dbgen.UpdateGlobalLeagueSettingsParams { + return dbgen.UpdateGlobalLeagueSettingsParams{ + ID: league.ID, + IsActive: league.DefaultIsActive.ToPG(), + IsFeatured: league.DefaultIsFeatured.ToPG(), + } +} diff --git a/internal/domain/odds.go b/internal/domain/odds.go index 2c94c78..27ac23c 100644 --- a/internal/domain/odds.go +++ b/internal/domain/odds.go @@ -61,6 +61,11 @@ type CreateOddMarketSettings struct { CustomRawOdds []map[string]interface{} } +type UpdateGlobalOddMarketSettings struct { + OddMarketID int64 + IsActive ValidBool +} + type CustomOdd struct { OddID int64 `json:"odd_id"` OddValue float32 `json:"odd_value"` @@ -72,6 +77,11 @@ type CreateOddMarketSettingsReq struct { CustomOdd []CustomOdd `json:"custom_odd,omitempty"` } +type UpdateGlobalOddMarketSettingsReq struct { + OddMarketID int64 `json:"odd_market_id"` + IsActive *bool `json:"is_active,omitempty"` +} + type RawOddsByMarketID struct { ID int64 `json:"id"` MarketName string `json:"market_name"` @@ -86,6 +96,8 @@ type OddMarketFilter struct { Offset ValidInt32 } type OddMarketWithEventFilter struct { + Status ValidString + IsLive ValidBool Limit ValidInt32 Offset ValidInt32 } diff --git a/internal/domain/result.go b/internal/domain/result.go index 698e01e..70993fc 100644 --- a/internal/domain/result.go +++ b/internal/domain/result.go @@ -1,6 +1,7 @@ package domain import ( + "fmt" "time" "github.com/jackc/pgx/v5/pgtype" @@ -48,6 +49,28 @@ const ( OUTCOME_STATUS_ERROR OutcomeStatus = 5 //Half Win and Half Given Back ) +func (o OutcomeStatus) IsValid() bool { + switch o { + case OUTCOME_STATUS_PENDING, + OUTCOME_STATUS_WIN, + OUTCOME_STATUS_LOSS, + OUTCOME_STATUS_VOID, + OUTCOME_STATUS_HALF, + OUTCOME_STATUS_ERROR: + return true + default: + return false + } +} + +func ParseOutcomeStatus(val int) (OutcomeStatus, error) { + o := OutcomeStatus(val) + if !o.IsValid() { + return 0, fmt.Errorf("invalid OutcomeStatus: %d", val) + } + return o, nil +} + func (o *OutcomeStatus) String() string { switch *o { case OUTCOME_STATUS_PENDING: @@ -72,7 +95,6 @@ type ValidOutcomeStatus struct { Valid bool } - func (v ValidOutcomeStatus) ToPG() pgtype.Int4 { return pgtype.Int4{ Int32: int32(v.Value), @@ -80,7 +102,6 @@ func (v ValidOutcomeStatus) ToPG() pgtype.Int4 { } } - type TimeStatus int32 const ( diff --git a/internal/repository/bet.go b/internal/repository/bet.go index bb2a7d7..9a6ad8f 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -123,7 +123,7 @@ func (s *Store) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]doma Query: filter.Query.ToPG(), CreatedBefore: filter.CreatedBefore.ToPG(), CreatedAfter: filter.CreatedAfter.ToPG(), - }); + }) var result []domain.GetBet = make([]domain.GetBet, 0, len(bets)) for _, bet := range bets { @@ -275,6 +275,36 @@ func (s *Store) SettleWinningBet(ctx context.Context, betID int64, userID int64, return nil } +func (s *Store) GetBetOutcomeViewByEventID(ctx context.Context, eventID int64, filter domain.BetOutcomeViewFilter) ([]domain.BetOutcomeViewRes, int64, error) { + + outcomes, err := s.queries.GetBetOutcomeViewByEventID(ctx, dbgen.GetBetOutcomeViewByEventIDParams{ + EventID: eventID, + FilterStatus: filter.OutcomeStatus.ToPG(), + CompanyID: filter.CompanyID.ToPG(), + Offset: filter.Offset.ToPG(), + Limit: filter.Limit.ToPG(), + }) + + if err != nil { + domain.MongoDBLogger.Error("failed to get bet outcomes by event ID", + zap.Int64("event_id", eventID), + zap.Error(err), + ) + return nil, 0, err + } + + total, err := s.queries.TotalBetOutcomeViewByEventID(ctx, dbgen.TotalBetOutcomeViewByEventIDParams{ + EventID: eventID, + FilterStatus: filter.OutcomeStatus.ToPG(), + CompanyID: filter.CompanyID.ToPG(), + }) + + var result []domain.BetOutcomeViewRes = make([]domain.BetOutcomeViewRes, 0, len(outcomes)) + for _, outcome := range outcomes { + result = append(result, domain.ConvertDBBetOutcomesView(outcome)) + } + return result, total, nil +} func (s *Store) GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error) { outcomes, err := s.queries.GetBetOutcomeByEventID(ctx, dbgen.GetBetOutcomeByEventIDParams{ @@ -377,6 +407,27 @@ func (s *Store) UpdateBetOutcomeStatusForEvent(ctx context.Context, eventID int6 } return result, nil } +func (s *Store) UpdateBetOutcomeStatusForOddId(ctx context.Context, oddID int64, status domain.OutcomeStatus) ([]domain.BetOutcome, error) { + outcomes, err := s.queries.UpdateBetOutcomeStatusForOddID(ctx, dbgen.UpdateBetOutcomeStatusForOddIDParams{ + OddID: oddID, + Status: int32(status), + }) + + if err != nil { + domain.MongoDBLogger.Error("failed to update bet outcome status for oddID", + zap.Int64("oddId", oddID), + zap.Int32("status", int32(status)), + zap.Error(err), + ) + return nil, err + } + + var result []domain.BetOutcome = make([]domain.BetOutcome, 0, len(outcomes)) + for _, outcome := range outcomes { + result = append(result, domain.ConvertDBBetOutcomes(outcome)) + } + return result, nil +} func (s *Store) UpdateBetWithCashback(ctx context.Context, betID int64, cashbackStatus bool) error { err := s.queries.UpdateBetWithCashback(ctx, dbgen.UpdateBetWithCashbackParams{ diff --git a/internal/repository/company.go b/internal/repository/company.go index 08f5251..1e290c9 100644 --- a/internal/repository/company.go +++ b/internal/repository/company.go @@ -17,7 +17,7 @@ func (s *Store) CreateCompany(ctx context.Context, company domain.CreateCompany) i := 1 for { - _, err := s.queries.GetCompanyIDUsingSlug(ctx, uniqueSlug) + _, err := s.queries.GetCompanyUsingSlug(ctx, uniqueSlug) if err != nil { if errors.Is(err, pgx.ErrNoRows) { // slug is unique @@ -78,13 +78,13 @@ func (s *Store) GetCompanyByID(ctx context.Context, id int64) (domain.GetCompany return domain.ConvertDBCompanyDetails(dbCompany), nil } -func (s *Store) GetCompanyIDBySlug(ctx context.Context, slug string) (int64, error) { - dbCompanyID, err := s.queries.GetCompanyIDUsingSlug(ctx, slug) +func (s *Store) GetCompanyBySlug(ctx context.Context, slug string) (domain.Company, error) { + dbCompany, err := s.queries.GetCompanyUsingSlug(ctx, slug) if err != nil { - return 0, err + return domain.Company{}, err } - return dbCompanyID, nil + return domain.ConvertDBCompany(dbCompany), nil } func (s *Store) UpdateCompany(ctx context.Context, company domain.UpdateCompany) (domain.Company, error) { diff --git a/internal/repository/event.go b/internal/repository/event.go index b1e5c56..236de50 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -3,7 +3,6 @@ package repository import ( "context" "fmt" - "math" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -55,8 +54,7 @@ func (s *Store) GetAllEvents(ctx context.Context, filter domain.EventFilter) ([] return nil, 0, err } - numberOfPages := math.Ceil(float64(totalCount) / float64(filter.Limit.Value)) - return domain.ConvertDBEvents(events), int64(numberOfPages), nil + return domain.ConvertDBEvents(events), totalCount, nil } func (s *Store) GetEventsWithSettings(ctx context.Context, companyID int64, filter domain.EventFilter) ([]domain.EventWithSettings, int64, error) { @@ -99,8 +97,6 @@ func (s *Store) GetEventsWithSettings(ctx context.Context, companyID int64, filt return nil, 0, err } - numberOfPages := math.Ceil(float64(totalCount) / float64(filter.Limit.Value)) - result := make([]domain.EventWithSettings, len(events)) for i, event := range events { @@ -155,7 +151,7 @@ func (s *Store) GetEventsWithSettings(ctx context.Context, companyID int64, filt } } - return result, int64(numberOfPages), nil + return result, totalCount, nil } func (s *Store) GetEventByID(ctx context.Context, ID int64) (domain.BaseEvent, error) { event, err := s.queries.GetEventByID(ctx, ID) @@ -281,10 +277,13 @@ func (s *Store) UpdateEventMonitored(ctx context.Context, eventID int64, IsMonit }) } -func (s *Store) UpdateEventSettings(ctx context.Context, event domain.CreateEventSettings) error { - return s.queries.SaveEventSettings(ctx, domain.ConvertUpdateEventSettings(event)) +func (s *Store) UpdateTenantEventSettings(ctx context.Context, event domain.UpdateTenantEventSettings) error { + return s.queries.SaveTenantEventSettings(ctx, domain.ConvertUpdateTenantEventSettings(event)) } +func (s *Store) UpdateGlobalEventSettings(ctx context.Context, event domain.UpdateGlobalEventSettings) error { + return s.queries.UpdateGlobalEventSettings(ctx, domain.ConvertUpdateGlobalEventSettings(event)) +} func (s *Store) DeleteEvent(ctx context.Context, eventID int64) error { err := s.queries.DeleteEvent(ctx, eventID) if err != nil { diff --git a/internal/repository/league.go b/internal/repository/league.go index ae0a4d5..ab01c14 100644 --- a/internal/repository/league.go +++ b/internal/repository/league.go @@ -13,7 +13,7 @@ func (s *Store) SaveLeague(ctx context.Context, league domain.CreateLeague) erro } func (s *Store) SaveLeagueSettings(ctx context.Context, leagueSettings domain.CreateLeagueSettings) error { - return s.queries.InsertLeagueSettings(ctx, domain.ConvertCreateLeagueSettings(leagueSettings)) + return s.queries.SaveLeagueSettings(ctx, domain.ConvertCreateLeagueSettings(leagueSettings)) } func (s *Store) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.BaseLeague, error) { @@ -85,3 +85,7 @@ func (s *Store) CheckLeagueSupport(ctx context.Context, leagueID int64, companyI func (s *Store) UpdateLeague(ctx context.Context, league domain.UpdateLeague) error { return s.queries.UpdateLeague(ctx, domain.ConvertUpdateLeague(league)) } + +func (s *Store) UpdateGlobalLeagueSettings(ctx context.Context, league domain.UpdateGlobalLeagueSettings) error { + return s.queries.UpdateGlobalLeagueSettings(ctx, domain.ConvertUpdateGlobalLeagueSetting(league)) +} diff --git a/internal/repository/odds.go b/internal/repository/odds.go index 51e1c6a..009e301 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -239,17 +239,11 @@ func (s *Store) GetOddsWithSettingsByID(ctx context.Context, ID int64, companyID func (s *Store) GetOddsByEventID(ctx context.Context, eventID int64, filter domain.OddMarketWithEventFilter) ([]domain.OddMarket, error) { odds, err := s.queries.GetOddsByEventID(ctx, dbgen.GetOddsByEventIDParams{ EventID: eventID, + Status: filter.Status.ToPG(), + IsLive: filter.IsLive.ToPG(), Limit: filter.Limit.ToPG(), Offset: filter.Offset.ToPG(), - IsLive: pgtype.Bool{ - Bool: false, - Valid: true, - }, - Status: pgtype.Text{ - String: string(domain.STATUS_PENDING), - Valid: true, - }, - Source: pgtype.Text{}, + Source: pgtype.Text{}, }) if err != nil { return nil, err @@ -322,3 +316,21 @@ func (s *Store) SaveOddsSetting(ctx context.Context, odd domain.CreateOddMarketS } return s.queries.SaveOddSettings(ctx, res) } + +func (s *Store) UpdateGlobalOddsSetting(ctx context.Context, odd domain.UpdateGlobalOddMarketSettings) error { + return s.queries.UpdateGlobalOddsSetting(ctx, dbgen.UpdateGlobalOddsSettingParams{ + ID: odd.OddMarketID, + DefaultIsActive: odd.IsActive.ToPG(), + }) +} + +func (s *Store) DeleteAllCompanyOddsSetting(ctx context.Context, companyID int64) error { + return s.queries.DeleteAllCompanyOddsSetting(ctx, companyID) +} + +func (s *Store) DeleteCompanyOddsSettingByOddMarketID(ctx context.Context, companyID int64, oddMarketID int64) error { + return s.queries.DeleteCompanyOddsSettingByOddMarketID(ctx, dbgen.DeleteCompanyOddsSettingByOddMarketIDParams{ + CompanyID: companyID, + OddsMarketID: oddMarketID, + }) +} diff --git a/internal/services/bet/notification.go b/internal/services/bet/notification.go index e9dd185..f891b94 100644 --- a/internal/services/bet/notification.go +++ b/internal/services/bet/notification.go @@ -247,7 +247,7 @@ func (s *Service) SendAdminErrorNotification(ctx context.Context, betID int64, s } func (s *Service) SendAdminLargeBetNotification(ctx context.Context, betID int64, totalWinnings float32, extra string, companyID int64) error { - headline := fmt.Sprintf("SYSTEM WARNING: High Risk Bet", betID, totalWinnings) + headline := "SYSTEM WARNING: High Risk Bet" message := fmt.Sprintf("Bet #%d has been created with %v payout", betID, totalWinnings) super_admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{ diff --git a/internal/services/bet/port.go b/internal/services/bet/port.go index dc582b4..1b52474 100644 --- a/internal/services/bet/port.go +++ b/internal/services/bet/port.go @@ -15,6 +15,7 @@ type BetStore interface { GetAllBets(ctx context.Context, filter domain.BetFilter) ([]domain.GetBet, int64, error) GetBetByUserID(ctx context.Context, UserID int64) ([]domain.GetBet, error) GetBetByFastCode(ctx context.Context, fastcode string) (domain.GetBet, error) + GetBetOutcomeViewByEventID(ctx context.Context, eventID int64, filter domain.BetOutcomeViewFilter) ([]domain.BetOutcomeViewRes, int64, error) GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error) GetBetOutcomeByBetID(ctx context.Context, betID int64) ([]domain.BetOutcome, error) GetBetOutcomeCountByOddID(ctx context.Context, oddID int64) (int64, error) @@ -25,7 +26,7 @@ type BetStore interface { UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) 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) 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 009f6fd..2379c3b 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -848,6 +848,9 @@ func (s *Service) GetBetOutcomeByBetID(ctx context.Context, UserID int64) ([]dom return s.betStore.GetBetOutcomeByBetID(ctx, UserID) } +func (s *Service) GetBetOutcomeViewByEventID(ctx context.Context, eventID int64, filter domain.BetOutcomeViewFilter) ([]domain.BetOutcomeViewRes, int64, error) { + return s.betStore.GetBetOutcomeViewByEventID(ctx, eventID, filter) +} func (s *Service) GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error) { return s.betStore.GetBetOutcomeByEventID(ctx, eventID, is_filtered) } @@ -1076,6 +1079,19 @@ func (s *Service) UpdateBetOutcomeStatusForEvent(ctx context.Context, eventID in return outcomes, nil } +func (s *Service) UpdateBetOutcomeStatusForOddId(ctx context.Context, oddID int64, status domain.OutcomeStatus) ([]domain.BetOutcome, error) { + outcomes, err := s.betStore.UpdateBetOutcomeStatusForOddId(ctx, oddID, status) + if err != nil { + s.mongoLogger.Error("failed to update bet outcome status", + zap.Int64("oddID", oddID), + zap.Error(err), + ) + return nil, err + } + + return outcomes, 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/company/port.go b/internal/services/company/port.go index 10bfa70..d3540f2 100644 --- a/internal/services/company/port.go +++ b/internal/services/company/port.go @@ -11,7 +11,7 @@ type CompanyStore interface { GetAllCompanies(ctx context.Context, filter domain.CompanyFilter) ([]domain.GetCompany, error) SearchCompanyByName(ctx context.Context, name string) ([]domain.GetCompany, error) GetCompanyByID(ctx context.Context, id int64) (domain.GetCompany, error) - GetCompanyIDBySlug(ctx context.Context, slug string) (int64, error) + GetCompanyBySlug(ctx context.Context, slug string) (domain.Company, error) UpdateCompany(ctx context.Context, company domain.UpdateCompany) (domain.Company, error) DeleteCompany(ctx context.Context, id int64) error diff --git a/internal/services/company/service.go b/internal/services/company/service.go index a396a10..1f69a22 100644 --- a/internal/services/company/service.go +++ b/internal/services/company/service.go @@ -26,8 +26,8 @@ func (s *Service) GetAllCompanies(ctx context.Context, filter domain.CompanyFilt func (s *Service) GetCompanyByID(ctx context.Context, id int64) (domain.GetCompany, error) { return s.companyStore.GetCompanyByID(ctx, id) } -func (s *Service) GetCompanyIDBySlug(ctx context.Context, slug string) (int64, error){ - return s.companyStore.GetCompanyIDBySlug(ctx, slug) +func (s *Service) GetCompanyBySlug(ctx context.Context, slug string) (domain.Company, error) { + return s.companyStore.GetCompanyBySlug(ctx, slug) } func (s *Service) SearchCompanyByName(ctx context.Context, name string) ([]domain.GetCompany, error) { diff --git a/internal/services/event/port.go b/internal/services/event/port.go index 546699e..8653ce6 100644 --- a/internal/services/event/port.go +++ b/internal/services/event/port.go @@ -18,6 +18,7 @@ type Service interface { UpdateEventMonitored(ctx context.Context, eventID int64, IsMonitored bool) error GetEventsWithSettings(ctx context.Context, companyID int64, filter domain.EventFilter) ([]domain.EventWithSettings, int64, error) GetEventWithSettingByID(ctx context.Context, ID int64, companyID int64) (domain.EventWithSettings, error) - UpdateEventSettings(ctx context.Context, event domain.CreateEventSettings) error + UpdateTenantEventSettings(ctx context.Context, event domain.UpdateTenantEventSettings) error + UpdateGlobalEventSettings(ctx context.Context, event domain.UpdateGlobalEventSettings) error GetSportAndLeagueIDs(ctx context.Context, eventID int64) ([]int64, error) } diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 5bc27e6..b083641 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -491,8 +491,11 @@ func (s *service) GetEventWithSettingByID(ctx context.Context, ID int64, company return s.store.GetEventWithSettingByID(ctx, ID, companyID) } -func (s *service) UpdateEventSettings(ctx context.Context, event domain.CreateEventSettings) error { - return s.store.UpdateEventSettings(ctx, event) +func (s *service) UpdateTenantEventSettings(ctx context.Context, event domain.UpdateTenantEventSettings) error { + return s.store.UpdateTenantEventSettings(ctx, event) +} +func (s *service) UpdateGlobalEventSettings(ctx context.Context, event domain.UpdateGlobalEventSettings) error { + return s.store.UpdateGlobalEventSettings(ctx, event) } func (s *service) GetSportAndLeagueIDs(ctx context.Context, eventID int64) ([]int64, error) { diff --git a/internal/services/league/port.go b/internal/services/league/port.go index 54dc626..277f602 100644 --- a/internal/services/league/port.go +++ b/internal/services/league/port.go @@ -10,7 +10,8 @@ type Service interface { SaveLeague(ctx context.Context, league domain.CreateLeague) error SaveLeagueSettings(ctx context.Context, leagueSettings domain.CreateLeagueSettings) error GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.BaseLeague, error) - GetAllLeaguesByCompany(ctx context.Context, companyID int64, filter domain.LeagueFilter) ([]domain.LeagueWithSettings, int64, error) + GetAllLeaguesByCompany(ctx context.Context, companyID int64, filter domain.LeagueFilter) ([]domain.LeagueWithSettings, int64, error) CheckLeagueSupport(ctx context.Context, leagueID int64, companyID int64) (bool, error) UpdateLeague(ctx context.Context, league domain.UpdateLeague) error + UpdateGlobalLeagueSettings(ctx context.Context, league domain.UpdateGlobalLeagueSettings) error } diff --git a/internal/services/league/service.go b/internal/services/league/service.go index 9a3e1a3..b07bc99 100644 --- a/internal/services/league/service.go +++ b/internal/services/league/service.go @@ -40,3 +40,7 @@ func (s *service) CheckLeagueSupport(ctx context.Context, leagueID int64, compan func (s *service) UpdateLeague(ctx context.Context, league domain.UpdateLeague) error { return s.store.UpdateLeague(ctx, league) } + +func (s *service) UpdateGlobalLeagueSettings(ctx context.Context, league domain.UpdateGlobalLeagueSettings) error { + return s.store.UpdateGlobalLeagueSettings(ctx, league) +} \ No newline at end of file diff --git a/internal/services/odds/port.go b/internal/services/odds/port.go index 3ec57b9..b67b91e 100644 --- a/internal/services/odds/port.go +++ b/internal/services/odds/port.go @@ -17,31 +17,17 @@ type Service interface { GetALLPrematchOdds(ctx context.Context) ([]domain.OddMarket, error) // GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) (domain.OddMarket, error) DeleteOddsForEvent(ctx context.Context, eventID string) error - GetOddByID(ctx context.Context, id int64) (domain.OddMarket, error) - GetOddsWithSettingsByID(ctx context.Context, ID int64, companyID int64) (domain.OddMarketWithSettings, error) + GetOddsWithSettingsByID(ctx context.Context, ID int64, companyID int64) (domain.OddMarketWithSettings, error) + // Settings SaveOddsSetting(ctx context.Context, odd domain.CreateOddMarketSettings) error + UpdateGlobalOddsSetting(ctx context.Context, odd domain.UpdateGlobalOddMarketSettings) error + DeleteAllCompanyOddsSetting(ctx context.Context, companyID int64) error + DeleteCompanyOddsSettingByOddMarketID(ctx context.Context, companyID int64, oddMarketID int64) error // Odd History InsertOddHistory(ctx context.Context, odd domain.CreateOddHistory) (domain.OddHistory, error) GetAllOddHistory(ctx context.Context, filter domain.OddHistoryFilter) ([]domain.OddHistory, error) GetInitialOddPerDay(ctx context.Context, filter domain.OddHistoryFilter) ([]domain.OddHistory, error) - - // Disabling Odds - InsertDisabledOdd(ctx context.Context, odd domain.CreateDisabledOdd) (domain.DisabledOdd, error) - GetAllDisabledOdds(ctx context.Context) ([]domain.DisabledOdd, error) - GetDisabledOddByRawOddID(ctx context.Context, rawOddID int64) (domain.DisabledOdd, error) - GetDisabledOddByID(ctx context.Context, id int64) (domain.DisabledOdd, error) - DeleteDisabledOddsByID(ctx context.Context, id int64) error - DeleteDisabledOddsByRawOddID(ctx context.Context, id int64) error - - // Custom Odds - // InsertCustomOdds(ctx context.Context, odd domain.CreateCustomOdd) (domain.CustomOdd, error) - // GetAllCustomOdds(ctx context.Context, filter domain.CustomOddFilter) ([]domain.CustomOdd, error) - // GetCustomOddByID(ctx context.Context, id int64) (domain.CustomOdd, error) - // GetCustomOddByOddID(ctx context.Context, oddId int64, companyID int64) (domain.CustomOdd, error) - // DeleteCustomOddByID(ctx context.Context, id int64) error - // DeleteCustomOddsByOddID(ctx context.Context, oddId int64, companyID int64) error - // DeleteCustomOddByEventID(ctx context.Context, eventID string) error } diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 0a9f401..dcc1004 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -676,6 +676,10 @@ func (s *ServiceImpl) SaveOddsSetting(ctx context.Context, odd domain.CreateOddM return s.store.SaveOddsSetting(ctx, odd) } +func (s *ServiceImpl) UpdateGlobalOddsSetting(ctx context.Context, odd domain.UpdateGlobalOddMarketSettings) error { + return s.store.UpdateGlobalOddsSetting(ctx, odd); +} + func (s *ServiceImpl) SaveOddsSettingReq(ctx context.Context, companyID int64, req domain.CreateOddMarketSettingsReq) error { odd, err := s.GetOddsWithSettingsByID(ctx, req.OddMarketID, companyID) @@ -741,6 +745,14 @@ func (s *ServiceImpl) DeleteOddsForEvent(ctx context.Context, eventID int64) err return s.store.DeleteOddsForEvent(ctx, eventID) } +func (s *ServiceImpl) DeleteAllCompanyOddsSetting(ctx context.Context, companyID int64) error { + return s.store.DeleteAllCompanyOddsSetting(ctx, companyID) +} + +func (s *ServiceImpl) DeleteCompanyOddsSettingByOddMarketID(ctx context.Context, companyID int64, oddMarketID int64) error{ + return s.store.DeleteCompanyOddsSettingByOddMarketID(ctx, companyID, oddMarketID) +} + // func getString(v interface{}) string { // if str, ok := v.(string); ok { // return str diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index f1caaac..09aab85 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -27,71 +27,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/admin.go b/internal/web_server/handlers/admin.go index 6eb6122..b7bd75e 100644 --- a/internal/web_server/handlers/admin.go +++ b/internal/web_server/handlers/admin.go @@ -451,24 +451,24 @@ func (h *Handler) UpdateAdmin(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update admin:"+err.Error()) } - if req.CompanyID != nil { - _, err := h.companySvc.UpdateCompany(c.Context(), domain.UpdateCompany{ - ID: *req.CompanyID, - AdminID: domain.ValidInt64{ - Value: AdminID, - Valid: true, - }, - }) - if err != nil { - h.mongoLoggerSvc.Error("UpdateAdmin failed to update company", - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Int64("admin_id", AdminID), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update company:"+err.Error()) - } - } + // if req.CompanyID != nil { + // _, err := h.companySvc.UpdateCompany(c.Context(), domain.UpdateCompany{ + // ID: *req.CompanyID, + // AdminID: domain.ValidInt64{ + // Value: AdminID, + // Valid: true, + // }, + // }) + // if err != nil { + // h.mongoLoggerSvc.Error("UpdateAdmin failed to update company", + // zap.Int("status_code", fiber.StatusInternalServerError), + // zap.Int64("admin_id", AdminID), + // zap.Error(err), + // zap.Time("timestamp", time.Now()), + // ) + // return fiber.NewError(fiber.StatusInternalServerError, "Failed to update company:"+err.Error()) + // } + // } h.mongoLoggerSvc.Info("UpdateAdmin succeeded", zap.Int("status_code", fiber.StatusOK), diff --git a/internal/web_server/handlers/auth_handler.go b/internal/web_server/handlers/auth_handler.go index a64f285..930d841 100644 --- a/internal/web_server/handlers/auth_handler.go +++ b/internal/web_server/handlers/auth_handler.go @@ -169,7 +169,7 @@ type loginAdminRes struct { func (h *Handler) LoginAdmin(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) if !companyID.Valid { - h.BadRequestLogger().Error("invalid company id") + h.BadRequestLogger().Error("invalid company id") return fiber.NewError(fiber.StatusBadRequest, "invalid company id") } var req loginAdminReq diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index 7b33f09..7677b2e 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -552,6 +552,25 @@ func (h *Handler) GetAllBet(c *fiber.Ctx) error { Valid: true, } } + + var companyID domain.ValidInt64 + companyIDQuery := c.Query("company_id") + if companyIDQuery != "" { + companyIDParsed, err := strconv.ParseInt(companyIDQuery, 10, 64) + if err != nil { + h.mongoLoggerSvc.Info("invalid company_id format", + zap.String("company_id", companyIDQuery), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid company_id format") + } + companyID = domain.ValidInt64{ + Value: companyIDParsed, + Valid: true, + } + } bets, total, err := h.betSvc.GetAllBets(c.Context(), domain.BetFilter{ IsShopBet: isShopBet, Query: searchString, @@ -560,6 +579,7 @@ func (h *Handler) GetAllBet(c *fiber.Ctx) error { Status: statusFilter, Limit: limit, Offset: offset, + CompanyID: companyID, }) if err != nil { h.mongoLoggerSvc.Error("Failed to get all bets", diff --git a/internal/web_server/handlers/event_handler.go b/internal/web_server/handlers/event_handler.go index e32d295..27fa0cd 100644 --- a/internal/web_server/handlers/event_handler.go +++ b/internal/web_server/handlers/event_handler.go @@ -135,6 +135,40 @@ func (h *Handler) GetAllEvents(c *fiber.Ctx) error { } } + isActiveQuery := c.Query("is_active") + var isActive domain.ValidBool + if isActiveQuery != "" { + isActiveParsed, err := strconv.ParseBool(isActiveQuery) + if err != nil { + h.BadRequestLogger().Error("Failed to parse isActive", + zap.String("is_active", isActiveQuery), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, "Failed to parse is_active") + } + + isActive = domain.ValidBool{ + Value: isActiveParsed, + Valid: true, + } + } + + statusQuery := c.Query("status") + var eventStatus domain.ValidEventStatus + if statusQuery != "" { + eventStatusParsed, err := domain.ParseEventStatus(statusQuery) + if err != nil { + h.BadRequestLogger().Error("Failed to parse statusQuery", + zap.String("is_featured", isFeaturedQuery), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, "invalid event status string") + } + eventStatus = domain.ValidEventStatus{ + Value: eventStatusParsed, + Valid: true, + } + } events, total, err := h.eventSvc.GetAllEvents( c.Context(), domain.EventFilter{ SportID: sportID, @@ -146,6 +180,8 @@ func (h *Handler) GetAllEvents(c *fiber.Ctx) error { Offset: offset, CountryCode: countryCode, Featured: isFeatured, + Active: isActive, + Status: eventStatus, }) // fmt.Printf("League ID: %v", leagueID) @@ -294,18 +330,18 @@ func (h *Handler) GetTenantUpcomingEvents(c *fiber.Ctx) error { events, total, err := h.eventSvc.GetEventsWithSettings( c.Context(), companyID.Value, domain.EventFilter{ - SportID: sportID, - LeagueID: leagueID, - Query: searchString, + SportID: sportID, + LeagueID: leagueID, + Query: searchString, FirstStartTime: domain.ValidTime{ Value: time.Now(), Valid: true, }, - LastStartTime: lastStartTime, - Limit: limit, - Offset: offset, - CountryCode: countryCode, - Featured: isFeatured, + LastStartTime: lastStartTime, + Limit: limit, + Offset: offset, + CountryCode: countryCode, + Featured: isFeatured, Status: domain.ValidEventStatus{ Value: domain.STATUS_PENDING, Valid: true, @@ -334,6 +370,200 @@ func (h *Handler) GetTenantUpcomingEvents(c *fiber.Ctx) error { } +// @Summary Retrieve all upcoming events with settings +// @Description Retrieve all upcoming events settings from the database +// @Tags prematch +// @Accept json +// @Produce json +// @Param page query int false "Page number" +// @Param page_size query int false "Page size" +// @Param league_id query string false "League ID Filter" +// @Param sport_id query string false "Sport ID Filter" +// @Param cc query string false "Country Code Filter" +// @Param first_start_time query string false "Start Time" +// @Param last_start_time query string false "End Time" +// @Success 200 {array} domain.BaseEvent +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/{tenant_slug}/events [get] +func (h *Handler) GetTenantEvents(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + + page := c.QueryInt("page", 1) + pageSize := c.QueryInt("page_size", 10) + limit := domain.ValidInt32{ + Value: int32(pageSize), + Valid: true, + } + offset := domain.ValidInt32{ + Value: int32(page - 1), + Valid: true, + } + + leagueIDQuery := c.Query("league_id") + var leagueID domain.ValidInt64 + if leagueIDQuery != "" { + leagueIDInt, err := strconv.ParseInt(leagueIDQuery, 10, 64) + if err != nil { + h.BadRequestLogger().Error("invalid league id", + zap.String("league_id", leagueIDQuery), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, "invalid league id") + } + leagueID = domain.ValidInt64{ + Value: leagueIDInt, + Valid: true, + } + } + sportIDQuery := c.Query("sport_id") + var sportID domain.ValidInt32 + if sportIDQuery != "" { + sportIDint, err := strconv.Atoi(sportIDQuery) + if err != nil { + h.BadRequestLogger().Info("invalid sport id", + zap.String("sportID", sportIDQuery), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, "invalid sport id") + } + sportID = domain.ValidInt32{ + Value: int32(sportIDint), + Valid: true, + } + } + + searchQuery := c.Query("query") + searchString := domain.ValidString{ + Value: searchQuery, + Valid: searchQuery != "", + } + + firstStartTimeQuery := c.Query("first_start_time") + var firstStartTime domain.ValidTime + if firstStartTimeQuery != "" { + firstStartTimeParsed, err := time.Parse(time.RFC3339, firstStartTimeQuery) + if err != nil { + h.BadRequestLogger().Info("invalid start_time format", + zap.String("first_start_time", firstStartTimeQuery), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid start_time format") + } + firstStartTime = domain.ValidTime{ + Value: firstStartTimeParsed, + Valid: true, + } + } + + lastStartTimeQuery := c.Query("last_start_time") + var lastStartTime domain.ValidTime + if lastStartTimeQuery != "" { + lastStartTimeParsed, err := time.Parse(time.RFC3339, lastStartTimeQuery) + if err != nil { + h.BadRequestLogger().Info("invalid last_start_time format", + zap.String("last_start_time", lastStartTimeQuery), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid start_time format") + } + lastStartTime = domain.ValidTime{ + Value: lastStartTimeParsed, + Valid: true, + } + } + + countryCodeQuery := c.Query("cc") + countryCode := domain.ValidString{ + Value: countryCodeQuery, + Valid: countryCodeQuery != "", + } + + isFeaturedQuery := c.Query("is_featured") + var isFeatured domain.ValidBool + if isFeaturedQuery != "" { + isFeaturedParsed, err := strconv.ParseBool(isFeaturedQuery) + if err != nil { + h.BadRequestLogger().Error("Failed to parse isFeatured", + zap.String("is_featured", isFeaturedQuery), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, "Failed to parse is_featured") + } + + isFeatured = domain.ValidBool{ + Value: isFeaturedParsed, + Valid: true, + } + } + + isActiveQuery := c.Query("is_active") + var isActive domain.ValidBool + if isActiveQuery != "" { + isActiveParsed, err := strconv.ParseBool(isActiveQuery) + if err != nil { + h.BadRequestLogger().Error("Failed to parse isActive", + zap.String("is_active", isActiveQuery), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, "Failed to parse is_active") + } + + isActive = domain.ValidBool{ + Value: isActiveParsed, + Valid: true, + } + } + + statusQuery := c.Query("status") + var eventStatus domain.ValidEventStatus + if statusQuery != "" { + eventStatusParsed, err := domain.ParseEventStatus(statusQuery) + if err != nil { + h.BadRequestLogger().Error("Failed to parse statusQuery", + zap.String("is_featured", isFeaturedQuery), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, "invalid event status string") + } + eventStatus = domain.ValidEventStatus{ + Value: eventStatusParsed, + Valid: true, + } + } + + events, total, err := h.eventSvc.GetEventsWithSettings( + c.Context(), companyID.Value, domain.EventFilter{ + SportID: sportID, + LeagueID: leagueID, + Query: searchString, + FirstStartTime: firstStartTime, + LastStartTime: lastStartTime, + Limit: limit, + Offset: offset, + CountryCode: countryCode, + Featured: isFeatured, + Status: eventStatus, + Active: isActive, + }) + + // fmt.Printf("League ID: %v", leagueID) + if err != nil { + h.InternalServerErrorLogger().Error("Failed to retrieve all upcoming events", + zap.Error(err), + ) + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + res := domain.ConvertEventWithSettingResList(events) + + return response.WritePaginatedJSON(c, fiber.StatusOK, "All upcoming events retrieved successfully", res, nil, page, int(total)) + +} + type TopLeaguesRes struct { Leagues []TopLeague `json:"leagues"` } @@ -483,6 +713,145 @@ func (h *Handler) GetTenantEventByID(c *fiber.Ctx) error { } +// @Summary Retrieve bet outcomes by event id +// @Description Retrieve bet outcomes by event id +// @Tags prematch +// @Accept json +// @Produce json +// @Param id path string true "ID" +// @Success 200 {object} domain.BaseEvent +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/tenant/{tenant_slug}/events/{id}/bets [get] +func (h *Handler) GetTenantBetsByEventID(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + + idStr := c.Params("id") + eventID, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + h.BadRequestLogger().Info("Failed to parse event id", zap.String("id", idStr)) + return fiber.NewError(fiber.StatusBadRequest, "Missing id") + } + + page := c.QueryInt("page", 1) + pageSize := c.QueryInt("page_size", 10) + limit := domain.ValidInt32{ + Value: int32(pageSize), + Valid: true, + } + offset := domain.ValidInt32{ + Value: int32(page - 1), + Valid: true, + } + + statusQuery := c.Params("status") + var status domain.ValidOutcomeStatus + if statusQuery != "" { + statusIntParse, err := strconv.ParseInt(statusQuery, 10, 32) + if err != nil { + h.BadRequestLogger().Info("Failed to parse status", zap.String("status", statusQuery)) + return fiber.NewError(fiber.StatusBadRequest, "Invalid status query") + } + + statusParsed, err := domain.ParseOutcomeStatus(int(statusIntParse)) + if err != nil { + h.BadRequestLogger().Info("Failed to parse status", zap.String("status", statusQuery)) + return fiber.NewError(fiber.StatusBadRequest, "Invalid status query") + } + + status = domain.ValidOutcomeStatus{ + Value: statusParsed, + Valid: true, + } + } + + res, total, err := h.betSvc.GetBetOutcomeViewByEventID(c.Context(), eventID, domain.BetOutcomeViewFilter{ + OutcomeStatus: status, + CompanyID: companyID, + Limit: limit, + Offset: offset, + }) + + if err != nil { + h.InternalServerErrorLogger().Error("Failed to get upcoming event by id", + zap.Int64("eventID", eventID), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + return response.WritePaginatedJSON(c, fiber.StatusOK, "Bet Outcomes retrieved successfully", res, nil, page, int(total)) +} + +// @Summary Retrieve bet outcomes by event id +// @Description Retrieve bet outcomes by event id +// @Tags prematch +// @Accept json +// @Produce json +// @Param id path string true "ID" +// @Success 200 {object} domain.BaseEvent +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/events/{id}/bets [get] +func (h *Handler) GetBetsByEventID(c *fiber.Ctx) error { + idStr := c.Params("id") + eventID, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + h.BadRequestLogger().Info("Failed to parse event id", zap.String("id", idStr)) + return fiber.NewError(fiber.StatusBadRequest, "Missing id") + } + + page := c.QueryInt("page", 1) + pageSize := c.QueryInt("page_size", 10) + limit := domain.ValidInt32{ + Value: int32(pageSize), + Valid: true, + } + offset := domain.ValidInt32{ + Value: int32(page - 1), + Valid: true, + } + + statusQuery := c.Params("status") + var status domain.ValidOutcomeStatus + if statusQuery != "" { + statusIntParse, err := strconv.ParseInt(statusQuery, 10, 32) + if err != nil { + h.BadRequestLogger().Info("Failed to parse status", zap.String("status", statusQuery)) + return fiber.NewError(fiber.StatusBadRequest, "Invalid status query") + } + + statusParsed, err := domain.ParseOutcomeStatus(int(statusIntParse)) + if err != nil { + h.BadRequestLogger().Info("Failed to parse status", zap.String("status", statusQuery)) + return fiber.NewError(fiber.StatusBadRequest, "Invalid status query") + } + + status = domain.ValidOutcomeStatus{ + Value: statusParsed, + Valid: true, + } + } + + res, total, err := h.betSvc.GetBetOutcomeViewByEventID(c.Context(), eventID, domain.BetOutcomeViewFilter{ + OutcomeStatus: status, + Limit: limit, + Offset: offset, + }) + + if err != nil { + h.InternalServerErrorLogger().Error("Failed to get upcoming event by id", + zap.Int64("eventID", eventID), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + return response.WritePaginatedJSON(c, fiber.StatusOK, "Bet Outcomes retrieved successfully", res, nil, page, int(total)) +} + type UpdateEventStatusReq struct { } @@ -519,9 +888,9 @@ func (h *Handler) SetEventStatusToRemoved(c *fiber.Ctx) error { } type UpdateEventSettingsReq struct { - Featured *bool `json:"is_featured" example:"true"` - IsActive *bool `json:"is_active" example:"true"` - WinningUpperLimit *int `json:"winning_upper_limit" example:"10000"` + Featured *bool `json:"is_featured" example:"true"` + IsActive *bool `json:"is_active" example:"true"` + WinningUpperLimit *int64 `json:"winning_upper_limit" example:"10000"` } // UpdateEventSettings godoc @@ -534,8 +903,72 @@ type UpdateEventSettingsReq struct { // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/tenant/{tenant_slug}/events/{id}/settings [put] +// @Router /api/v1/events/{id}/settings [put] func (h *Handler) UpdateEventSettings(c *fiber.Ctx) error { + eventIDStr := c.Params("id") + + eventID, err := strconv.ParseInt(eventIDStr, 10, 64) + if err != nil { + h.BadRequestLogger().Error("invalid event id") + return fiber.NewError(fiber.StatusBadRequest, "invalid event id") + } + + var req UpdateEventSettingsReq + + if err := c.BodyParser(&req); err != nil { + h.BadRequestLogger().Info("Failed to parse event id", + zap.Int64("eventID", eventID), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + logFields := []zap.Field{ + zap.Int64("eventID", eventID), + zap.Any("is_featured", req.Featured), + zap.Any("is_active", req.IsActive), + zap.Any("winning_upper_limit", req.WinningUpperLimit), + } + 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 update event settings", + append(logFields, zap.String("errMsg", errMsg))..., + ) + return fiber.NewError(fiber.StatusBadRequest, errMsg) + } + + err = h.eventSvc.UpdateGlobalEventSettings(c.Context(), domain.UpdateGlobalEventSettings{ + EventID: eventID, + IsFeatured: domain.ConvertBoolPtr(req.Featured), + IsActive: domain.ConvertBoolPtr(req.IsActive), + WinningUpperLimit: domain.ConvertInt64Ptr(req.WinningUpperLimit), + }) + + if err != nil { + h.InternalServerErrorLogger().Error("Failed to update event settings", append(logFields, zap.Error(err))...) + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + return response.WriteJSON(c, fiber.StatusOK, "Event updated successfully", nil, nil) + +} + +// UpdateTenantEventSettings godoc +// @Summary update the event settings +// @Description Update the event settings +// @Tags event +// @Accept json +// @Produce json +// @Param id path int true "Event ID" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/tenant/{tenant_slug}/events/{id}/settings [put] +func (h *Handler) UpdateTenantEventSettings(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) if !companyID.Valid { h.BadRequestLogger().Error("invalid company id") @@ -579,12 +1012,12 @@ func (h *Handler) UpdateEventSettings(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, errMsg) } - err = h.eventSvc.UpdateEventSettings(c.Context(), domain.CreateEventSettings{ + err = h.eventSvc.UpdateTenantEventSettings(c.Context(), domain.UpdateTenantEventSettings{ CompanyID: companyID.Value, EventID: eventID, IsFeatured: domain.ConvertBoolPtr(req.Featured), IsActive: domain.ConvertBoolPtr(req.IsActive), - WinningUpperLimit: domain.ConvertIntPtr(req.WinningUpperLimit), + WinningUpperLimit: domain.ConvertInt64Ptr(req.WinningUpperLimit), }) if err != nil { diff --git a/internal/web_server/handlers/leagues.go b/internal/web_server/handlers/leagues.go index 73abbf8..08c15a9 100644 --- a/internal/web_server/handlers/leagues.go +++ b/internal/web_server/handlers/leagues.go @@ -26,7 +26,7 @@ func (h *Handler) GetAllLeagues(c *fiber.Ctx) error { limit := domain.ValidInt64{ Value: int64(pageSize), - Valid: pageSize == 0, + Valid: pageSize != 0, } offset := domain.ValidInt64{ Value: int64(page - 1), @@ -54,7 +54,7 @@ func (h *Handler) GetAllLeagues(c *fiber.Ctx) error { sportIDQuery := c.Query("sport_id") var sportID domain.ValidInt32 if sportIDQuery != "" { - sportIDint, err := strconv.Atoi(sportIDQuery) + sportIDint, err := strconv.ParseInt(sportIDQuery, 10, 64) if err != nil { h.BadRequestLogger().Info("invalid sport id", zap.String("sport_id", sportIDQuery), @@ -156,7 +156,7 @@ func (h *Handler) GetAllLeaguesForTenant(c *fiber.Ctx) error { sportIDQuery := c.Query("sport_id") var sportID domain.ValidInt32 if sportIDQuery != "" { - sportIDint, err := strconv.Atoi(sportIDQuery) + sportIDint, err := strconv.ParseInt(sportIDQuery, 10, 64) if err != nil { h.BadRequestLogger().Info("invalid sport id", zap.String("sport_id", sportIDQuery), @@ -235,7 +235,7 @@ func (h *Handler) SetLeagueActive(c *fiber.Ctx) error { if leagueIdStr == "" { return fiber.NewError(fiber.StatusBadRequest, "Missing league id") } - leagueId, err := strconv.Atoi(leagueIdStr) + leagueId, err := strconv.ParseInt(leagueIdStr, 10, 64) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "invalid league id") } @@ -266,7 +266,7 @@ func (h *Handler) SetLeagueActive(c *fiber.Ctx) error { } if err := h.leagueSvc.SaveLeagueSettings(c.Context(), domain.CreateLeagueSettings{ - LeagueID: int64(leagueId), + LeagueID: leagueId, CompanyID: companyID.Value, IsActive: domain.ValidBool{ Value: req.IsActive, @@ -308,7 +308,7 @@ func (h *Handler) SetLeagueFeatured(c *fiber.Ctx) error { if leagueIdStr == "" { return fiber.NewError(fiber.StatusBadRequest, "Missing league id") } - leagueId, err := strconv.Atoi(leagueIdStr) + leagueId, err := strconv.ParseInt(leagueIdStr, 10, 64) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "invalid league id") } @@ -336,7 +336,7 @@ func (h *Handler) SetLeagueFeatured(c *fiber.Ctx) error { } err = h.leagueSvc.SaveLeagueSettings(c.Context(), domain.CreateLeagueSettings{ - LeagueID: int64(leagueId), + LeagueID: leagueId, CompanyID: companyID.Value, IsFeatured: domain.ValidBool{ Value: req.IsFeatured, @@ -351,3 +351,45 @@ func (h *Handler) SetLeagueFeatured(c *fiber.Ctx) error { h.SuccessResLogger().Info("League Featured has been successfully updated", queryLogFields...) return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil) } + +func (h *Handler) UpdateGlobalLeagueSetting(c *fiber.Ctx) error { + leagueIdStr := c.Params("id") + if leagueIdStr == "" { + return fiber.NewError(fiber.StatusBadRequest, "Missing league id") + } + leagueId, err := strconv.ParseInt(leagueIdStr, 10, 64) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "invalid league id") + } + + var req domain.UpdateLeagueSettingsReq + + if err := c.BodyParser(&req); err != nil { + h.BadRequestLogger().Info("UpdateLeagueSettingsReq failed to parse request body", zap.String("league_id", leagueIdStr), zap.Error(err)) + return fiber.NewError(fiber.StatusBadRequest, "Failed to parse request body:"+err.Error()) + } + + 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().Info("Failed to validate UpdateLeagueSettingsReq", zap.Error(err)) + return fiber.NewError(fiber.StatusBadRequest, errMsg) + + } + + err = h.leagueSvc.UpdateGlobalLeagueSettings(c.Context(), domain.UpdateGlobalLeagueSettings{ + ID: leagueId, + DefaultIsActive: domain.ConvertBoolPtr(req.IsActive), + DefaultIsFeatured: domain.ConvertBoolPtr(req.IsFeatured), + }) + + if err != nil { + h.InternalServerErrorLogger().Error("Failed to update league", zap.Error(err), zap.String("leagueId", leagueIdStr)) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update league:"+err.Error()) + } + return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil) + +} diff --git a/internal/web_server/handlers/odd_handler.go b/internal/web_server/handlers/odd_handler.go index ec4ec59..8c6457a 100644 --- a/internal/web_server/handlers/odd_handler.go +++ b/internal/web_server/handlers/odd_handler.go @@ -276,7 +276,6 @@ func (h *Handler) GetTenantOddsByUpcomingID(c *fiber.Ctx) error { zap.Int64("company_id", companyID.Value), } - eventIDStr := c.Params("upcoming_id") eventID, err := strconv.ParseInt(eventIDStr, 10, 64) if err != nil { @@ -309,16 +308,16 @@ func (h *Handler) GetTenantOddsByUpcomingID(c *fiber.Ctx) error { } -func (h *Handler) SaveOddsSetting(c *fiber.Ctx) error { +func (h *Handler) SaveTenantOddsSetting(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) if !companyID.Valid { h.BadRequestLogger().Error("invalid company id") return fiber.NewError(fiber.StatusBadRequest, "invalid company id") } - var req domain.CreateOddMarketSettingsReq + var req domain.CreateOddMarketSettingsReq if err := c.BodyParser(&req); err != nil { - h.BadRequestLogger().Info("Failed to parse event id", + h.BadRequestLogger().Info("Failed to parse CreateOddMarketSettingsReq", zap.Int64("CompanyID", companyID.Value), zap.Error(err), ) @@ -337,7 +336,7 @@ func (h *Handler) SaveOddsSetting(c *fiber.Ctx) error { for field, msg := range valErrs { errMsg += fmt.Sprintf("%s: %s; ", field, msg) } - h.BadRequestLogger().Error("Failed to insert odd settings", + h.BadRequestLogger().Error("Failed to validate insert odd settings", append(logFields, zap.String("errMsg", errMsg))..., ) return fiber.NewError(fiber.StatusBadRequest, errMsg) @@ -354,3 +353,133 @@ func (h *Handler) SaveOddsSetting(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Odds Settings saved successfully", nil, nil) } + +func (h *Handler) SaveOddSettings(c *fiber.Ctx) error { + var req domain.UpdateGlobalOddMarketSettingsReq + + if err := c.BodyParser(&req); err != nil { + h.BadRequestLogger().Info("Failed to parse CreateOddMarketSettingsReq", + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + logFields := []zap.Field{ + zap.Any("is_active", req.IsActive), + } + + 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 validate insert odd settings", + append(logFields, zap.String("errMsg", errMsg))..., + ) + return fiber.NewError(fiber.StatusBadRequest, errMsg) + } + + err := h.prematchSvc.UpdateGlobalOddsSetting(c.Context(), domain.UpdateGlobalOddMarketSettings{ + OddMarketID: req.OddMarketID, + IsActive: domain.ConvertBoolPtr(req.IsActive), + }) + + if err != nil { + logFields = append(logFields, zap.Error(err)) + h.InternalServerErrorLogger().Error("Failed to save odds settings", append(logFields, zap.Error(err))...) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to save odds settings"+err.Error()) + } + + return response.WriteJSON(c, fiber.StatusOK, "Odds Settings saved successfully", nil, nil) +} + +func (h *Handler) RemoveOddsSettings(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + + idStr := c.Params("id") + oddID, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + h.BadRequestLogger().Info("Failed to parse odd id", zap.String("id", idStr)) + return fiber.NewError(fiber.StatusBadRequest, "Missing id") + } + + err = h.prematchSvc.DeleteCompanyOddsSettingByOddMarketID(c.Context(), companyID.Value, oddID) + + if err != nil { + h.InternalServerErrorLogger().Error("Failed to retrieve odds", zap.Error(err)) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to remove odds settings"+err.Error()) + } + + return response.WriteJSON(c, fiber.StatusOK, "Odds settings successfully removed ", nil, nil) +} +func (h *Handler) RemoveAllOddsSettings(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + + err := h.prematchSvc.DeleteAllCompanyOddsSetting(c.Context(), companyID.Value) + + if err != nil { + h.InternalServerErrorLogger().Error("Failed to remove all odd settings", zap.Int64("company_id", companyID.Value), zap.Error(err)) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to remove all odds settings"+err.Error()) + } + + return response.WriteJSON(c, fiber.StatusOK, "Odds settings successfully removed ", nil, nil) +} + +type UpdateAllBetStatusByOddIDReq struct { + Status domain.OutcomeStatus `json:"status"` +} + +func (h *Handler) UpdateAllBetOutcomeStatusByOddID(c *fiber.Ctx) error { + + idStr := c.Params("id") + oddID, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + h.BadRequestLogger().Info("Failed to parse odd_id", zap.String("id", idStr)) + return fiber.NewError(fiber.StatusBadRequest, "Missing id") + } + + var req UpdateAllBetStatusByOddIDReq + 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.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.UpdateBetOutcomeStatusForOddId(c.Context(), oddID, req.Status) + + if err != nil { + h.InternalServerErrorLogger().Error("Failed to update bet status by odd id", + zap.Int64("oddID", oddID), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + return response.WriteJSON(c, fiber.StatusOK, "Updated All Bet Outcome Status Successfully", nil, nil) +} diff --git a/internal/web_server/handlers/user.go b/internal/web_server/handlers/user.go index a6ff298..fe89de9 100644 --- a/internal/web_server/handlers/user.go +++ b/internal/web_server/handlers/user.go @@ -14,6 +14,74 @@ import ( "go.uber.org/zap" ) +type GetTenantSlugByToken struct { + Slug string `json:"slug"` +} + +// GetTenantSlugByToken godoc +// @Summary Check if phone number or email exist +// @Description Check if phone number or email exist +// @Tags user +// @Accept json +// @Produce json +// @Param checkPhoneEmailExist body CheckPhoneEmailExistReq true "Check phone number or email exist" +// @Success 200 {object} CheckPhoneEmailExistRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/tenant [get] +func (h *Handler) GetTenantSlugByToken(c *fiber.Ctx) error { + userID, ok := c.Locals("user_id").(int64) + if !ok || userID == 0 { + h.mongoLoggerSvc.Error("Invalid user ID in context", + zap.Int64("userID", userID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification") + } + + user, err := h.userSvc.GetUserByID(c.Context(), userID) + if err != nil { + h.mongoLoggerSvc.Error("Failed to get user profile", + zap.Int64("userID", userID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user profile:"+err.Error()) + } + + if !user.CompanyID.Valid { + if user.Role == domain.RoleSuperAdmin { + return fiber.NewError(fiber.StatusBadRequest, "Role Super-Admin Doesn't have a company-id") + } + h.mongoLoggerSvc.Error("Unknown Error: User doesn't have a company-id", + zap.Int64("userID", userID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Unknown Error: User doesn't have a company-id") + } + company, err := h.companySvc.GetCompanyByID(c.Context(), user.CompanyID.Value) + + if err != nil { + h.mongoLoggerSvc.Error("Failed to get company by id", + zap.Int64("company", user.CompanyID.Value), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve company:"+err.Error()) + } + + res := GetTenantSlugByToken{ + Slug: company.Slug, + } + + return response.WriteJSON(c, fiber.StatusOK, "Tenant Slug retrieved successfully", res, nil) +} + type CheckPhoneEmailExistReq struct { Email string `json:"email" example:"john.doe@example.com"` PhoneNumber string `json:"phone_number" example:"1234567890"` diff --git a/internal/web_server/middleware.go b/internal/web_server/middleware.go index 68d4889..179cf59 100644 --- a/internal/web_server/middleware.go +++ b/internal/web_server/middleware.go @@ -236,7 +236,7 @@ func (a *App) TenantMiddleware(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "tenant is required for this route") } - companyID, err := a.companySvc.GetCompanyIDBySlug(c.Context(), tenantSlug) + company, err := a.companySvc.GetCompanyBySlug(c.Context(), tenantSlug) if err != nil { a.mongoLoggerSvc.Info("failed to resolve tenant", zap.String("tenant_slug", tenantSlug), @@ -245,8 +245,16 @@ func (a *App) TenantMiddleware(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "failed to resolve tenant") } + if !company.IsActive { + a.mongoLoggerSvc.Info("request using deactivated tenant", + zap.String("tenant_slug", tenantSlug), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusForbidden, "this tenant has been deactivated") + } + c.Locals("company_id", domain.ValidInt64{ - Value: companyID, + Value: company.ID, Valid: true, }) return c.Next() diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 6b3dedc..05aede1 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -96,6 +96,10 @@ func (a *App) initAppRoutes() { "message": "Company Tenant Active", }) }) + + // Get S + groupV1.Get("/tenant", a.authMiddleware, h.GetTenantSlugByToken) + //Direct_deposit groupV1.Post("/direct_deposit", a.authMiddleware, h.InitiateDirectDeposit) groupV1.Post("/direct_deposit/verify", a.authMiddleware, h.VerifyDirectDeposit) @@ -219,55 +223,63 @@ func (a *App) initAppRoutes() { // groupV1.Post("/bonus/create", a.authMiddleware, h.CreateBonusMultiplier) // groupV1.Put("/bonus/update", a.authMiddleware, h.UpdateBonusMultiplier) - groupV1.Get("/cashiers", a.authMiddleware, h.GetAllCashiers) - groupV1.Get("/cashiers/:id", a.authMiddleware, h.GetCashierByID) - groupV1.Post("/cashiers", a.authMiddleware, h.CreateCashier) - groupV1.Put("/cashiers/:id", a.authMiddleware, h.UpdateCashier) + groupV1.Get("/cashiers", a.authMiddleware, a.CompanyOnly, h.GetAllCashiers) + groupV1.Get("/cashiers/:id", a.authMiddleware, a.CompanyOnly, h.GetCashierByID) + groupV1.Post("/cashiers", a.authMiddleware, a.CompanyOnly, h.CreateCashier) + groupV1.Put("/cashiers/:id", a.authMiddleware, a.CompanyOnly, h.UpdateCashier) - tenant.Get("/customer", a.authMiddleware, h.GetAllTenantCustomers) - tenant.Get("/customer/:id", a.authMiddleware, h.GetTenantCustomerByID) - tenant.Put("/customer/:id", a.authMiddleware, h.UpdateTenantCustomer) - tenant.Get("/customer/:id/bets", a.authMiddleware, h.GetTenantCustomerBets) + tenant.Get("/customer", a.authMiddleware, a.CompanyOnly, h.GetAllTenantCustomers) + tenant.Get("/customer/:id", a.authMiddleware, a.CompanyOnly, h.GetTenantCustomerByID) + tenant.Put("/customer/:id", a.authMiddleware, a.CompanyOnly, h.UpdateTenantCustomer) + tenant.Get("/customer/:id/bets", a.authMiddleware, a.CompanyOnly, h.GetTenantCustomerBets) groupV1.Get("/customer", a.authMiddleware, a.SuperAdminOnly, h.GetAllCustomers) groupV1.Get("/customer/:id", a.authMiddleware, a.SuperAdminOnly, h.GetCustomerByID) groupV1.Put("/customer/:id", a.authMiddleware, a.SuperAdminOnly, h.UpdateCustomer) groupV1.Get("/customer/:id/bets", a.authMiddleware, h.GetCustomerBets) - groupV1.Get("/admin", a.authMiddleware, h.GetAllAdmins) - groupV1.Get("/admin/:id", a.authMiddleware, h.GetAdminByID) - groupV1.Post("/admin", a.authMiddleware, h.CreateAdmin) - groupV1.Put("/admin/:id", a.authMiddleware, h.UpdateAdmin) + groupV1.Get("/admin", a.authMiddleware, a.SuperAdminOnly, h.GetAllAdmins) + groupV1.Get("/admin/:id", a.authMiddleware, a.SuperAdminOnly, h.GetAdminByID) + groupV1.Post("/admin", a.authMiddleware, a.SuperAdminOnly, h.CreateAdmin) + groupV1.Put("/admin/:id", a.authMiddleware, a.SuperAdminOnly, h.UpdateAdmin) - groupV1.Get("/t-approver", a.authMiddleware, h.GetAllTransactionApprovers) - groupV1.Get("/t-approver/:id", a.authMiddleware, h.GetTransactionApproverByID) - groupV1.Post("/t-approver", a.authMiddleware, h.CreateTransactionApprover) - groupV1.Put("/t-approver/:id", a.authMiddleware, h.UpdateTransactionApprover) + groupV1.Get("/t-approver", a.authMiddleware, a.OnlyAdminAndAbove, h.GetAllTransactionApprovers) + groupV1.Get("/t-approver/:id", a.authMiddleware, a.OnlyAdminAndAbove, h.GetTransactionApproverByID) + groupV1.Post("/t-approver", a.authMiddleware, a.OnlyAdminAndAbove, h.CreateTransactionApprover) + groupV1.Put("/t-approver/:id", a.authMiddleware, a.OnlyAdminAndAbove, h.UpdateTransactionApprover) - groupV1.Get("/managers", a.authMiddleware, h.GetAllManagers) + groupV1.Get("/managers", a.authMiddleware, a.OnlyAdminAndAbove, h.GetAllManagers) groupV1.Get("/managers/:id", a.authMiddleware, h.GetManagerByID) - groupV1.Post("/managers", a.authMiddleware, h.CreateManager) - groupV1.Put("/managers/:id", a.authMiddleware, h.UpdateManagers) - groupV1.Get("/manager/:id/branch", a.authMiddleware, h.GetBranchByManagerID) + groupV1.Post("/managers", a.authMiddleware, a.OnlyAdminAndAbove, h.CreateManager) + groupV1.Put("/managers/:id", a.authMiddleware, a.OnlyAdminAndAbove, h.UpdateManagers) + groupV1.Get("/manager/:id/branch", a.authMiddleware, a.OnlyAdminAndAbove, h.GetBranchByManagerID) 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) tenant.Get("/odds", h.GetAllTenantOdds) tenant.Get("/odds/upcoming/:upcoming_id", h.GetTenantOddsByUpcomingID) tenant.Get("/odds/upcoming/:upcoming_id/market/:market_id", h.GetTenantOddsByMarketID) - tenant.Post("/odds/settings", a.CompanyOnly, h.SaveOddsSetting) + tenant.Post("/odds/settings", a.CompanyOnly, h.SaveTenantOddsSetting) + tenant.Delete("/odds/settings/:id", a.CompanyOnly, h.RemoveOddsSettings) + tenant.Delete("/odds/settings", a.CompanyOnly, h.RemoveAllOddsSettings) groupV1.Get("/events", a.authMiddleware, h.GetAllEvents) groupV1.Get("/events/:id", a.authMiddleware, h.GetEventByID) groupV1.Delete("/events/:id", a.authMiddleware, a.SuperAdminOnly, h.SetEventStatusToRemoved) groupV1.Patch("/events/:id/is_monitored", a.authMiddleware, a.SuperAdminOnly, h.SetEventIsMonitored) + groupV1.Put("/events/:id/settings", a.authMiddleware, a.SuperAdminOnly, h.UpdateGlobalSettingList) + groupV1.Get("/events/:id/bets", a.authMiddleware, a.SuperAdminOnly, h.GetBetsByEventID) tenant.Get("/upcoming-events", h.GetTenantUpcomingEvents) - tenant.Get("/events/:id", h.GetTenantEventByID) tenant.Get("/top-leagues", h.GetTopLeagues) - tenant.Put("/events/:id/settings", h.UpdateEventSettings) + tenant.Get("/events", h.GetTenantEvents) + tenant.Get("/events/:id", h.GetTenantEventByID) + tenant.Put("/events/:id/settings", a.authMiddleware, a.CompanyOnly, h.UpdateTenantEventSettings) + tenant.Get("/events/:id/bets", a.authMiddleware, a.CompanyOnly, h.GetTenantBetsByEventID) //EnetPulse groupV1.Get("/odds/pre-match", h.GetPreMatchOdds) @@ -277,38 +289,40 @@ func (a *App) initAppRoutes() { groupV1.Get("/tournament_stages", h.GetAllTournamentStages) // Leagues - tenant.Get("/leagues", h.GetAllLeagues) - tenant.Put("/leagues/:id/set-active", h.SetLeagueActive) - tenant.Put("/leagues/:id/featured", h.SetLeagueFeatured) groupV1.Get("/leagues", a.authMiddleware, a.SuperAdminOnly, h.GetAllLeagues) + groupV1.Put("/leagues/:id/settings", a.authMiddleware, a.SuperAdminOnly, h.UpdateGlobalLeagueSetting) + + tenant.Get("/leagues", h.GetAllLeagues) + tenant.Put("/leagues/:id/featured", a.authMiddleware, a.CompanyOnly, h.SetLeagueFeatured) + tenant.Put("/leagues/:id/set-active", a.authMiddleware, a.CompanyOnly, h.SetLeagueActive) groupV1.Get("/result/b365/:id", h.GetBet365ResultsByEventID) // Branch - groupV1.Post("/branch", a.authMiddleware, h.CreateBranch) - groupV1.Get("/branch", a.authMiddleware, h.GetAllBranches) - groupV1.Get("/branch/:id", a.authMiddleware, h.GetBranchByID) - groupV1.Post("/branch/:id/return", a.authMiddleware, h.ReturnBranchWallet) - groupV1.Get("/branch/:id/bets", a.authMiddleware, h.GetBetByBranchID) - groupV1.Put("/branch/:id", a.authMiddleware, h.UpdateBranch) - groupV1.Put("/branch/:id/set-active", a.authMiddleware, h.UpdateBranchStatus) - groupV1.Put("/branch/:id/set-inactive", a.authMiddleware, h.UpdateBranchStatus) - groupV1.Delete("/branch/:id", a.authMiddleware, h.DeleteBranch) + groupV1.Post("/branch", a.authMiddleware, a.CompanyOnly, h.CreateBranch) + groupV1.Get("/branch", a.authMiddleware, a.CompanyOnly, h.GetAllBranches) + groupV1.Get("/branch/:id", a.authMiddleware, a.CompanyOnly, h.GetBranchByID) + groupV1.Post("/branch/:id/return", a.authMiddleware, a.CompanyOnly, h.ReturnBranchWallet) + groupV1.Get("/branch/:id/bets", a.authMiddleware, a.CompanyOnly, h.GetBetByBranchID) + groupV1.Put("/branch/:id", a.authMiddleware, a.CompanyOnly, h.UpdateBranch) + groupV1.Put("/branch/:id/set-active", a.authMiddleware, a.CompanyOnly, h.UpdateBranchStatus) + groupV1.Put("/branch/:id/set-inactive", a.authMiddleware, a.CompanyOnly, h.UpdateBranchStatus) + groupV1.Delete("/branch/:id", a.authMiddleware, a.CompanyOnly, h.DeleteBranch) - groupV1.Get("/search/branch", a.authMiddleware, h.SearchBranch) + groupV1.Get("/search/branch", a.authMiddleware, a.CompanyOnly, h.SearchBranch) - groupV1.Get("/branchLocation", a.authMiddleware, h.GetAllBranchLocations) + groupV1.Get("/branchLocation", a.authMiddleware, a.CompanyOnly, h.GetAllBranchLocations) - groupV1.Get("/branch/:id/cashiers", a.authMiddleware, h.GetBranchCashiers) - groupV1.Get("/branchCashier", a.authMiddleware, h.GetBranchForCashier) + groupV1.Get("/branch/:id/cashiers", a.authMiddleware, a.CompanyOnly, h.GetBranchCashiers) + groupV1.Get("/branchCashier", a.authMiddleware, a.CompanyOnly, h.GetBranchForCashier) // Branch Operation groupV1.Get("/supportedOperation", a.authMiddleware, a.SuperAdminOnly, h.GetAllSupportedOperations) groupV1.Post("/supportedOperation", a.authMiddleware, a.SuperAdminOnly, h.CreateSupportedOperation) - groupV1.Post("/operation", a.authMiddleware, h.CreateBranchOperation) - groupV1.Get("/branch/:id/operation", a.authMiddleware, h.GetBranchOperations) + groupV1.Post("/operation", a.authMiddleware, a.CompanyOnly, h.CreateBranchOperation) + groupV1.Get("/branch/:id/operation", a.authMiddleware, a.CompanyOnly, h.GetBranchOperations) - groupV1.Delete("/branch/:id/operation/:opID", a.authMiddleware, h.DeleteBranchOperation) + groupV1.Delete("/branch/:id/operation/:opID", a.authMiddleware, a.CompanyOnly, h.DeleteBranchOperation) // Company groupV1.Post("/company", a.authMiddleware, a.SuperAdminOnly, h.CreateCompany) @@ -316,9 +330,9 @@ func (a *App) initAppRoutes() { groupV1.Get("/company/:id", a.authMiddleware, a.SuperAdminOnly, h.GetCompanyByID) groupV1.Put("/company/:id", a.authMiddleware, a.SuperAdminOnly, h.UpdateCompany) groupV1.Delete("/company/:id", a.authMiddleware, a.SuperAdminOnly, h.DeleteCompany) - groupV1.Get("/company/:id/branch", a.authMiddleware, h.GetBranchByCompanyID) - groupV1.Get("/search/company", a.authMiddleware, h.SearchCompany) - groupV1.Get("/admin-company", a.authMiddleware, h.GetCompanyForAdmin) + groupV1.Get("/company/:id/branch", a.authMiddleware, a.CompanyOnly, h.GetBranchByCompanyID) + groupV1.Get("/search/company", a.authMiddleware, a.CompanyOnly, h.SearchCompany) + groupV1.Get("/admin-company", a.authMiddleware, a.CompanyOnly, h.GetCompanyForAdmin) // Ticket Routes tenant.Post("/ticket", h.CreateTicket) @@ -329,7 +343,7 @@ func (a *App) initAppRoutes() { tenant.Post("/sport/bet", a.authMiddleware, h.CreateBet) tenant.Post("/sport/bet/fastcode", a.authMiddleware, h.CreateBetWithFastCode) tenant.Get("/sport/bet/fastcode/:fast_code", h.GetBetByFastCode) - tenant.Get("/sport/bet", a.authMiddleware, h.GetAllTenantBets) + tenant.Get("/sport/bet", a.authMiddleware, a.CompanyOnly, h.GetAllTenantBets) tenant.Get("/sport/bet/:id", a.authMiddleware, h.GetTenantBetByID) tenant.Patch("/sport/bet/:id", a.authMiddleware, h.UpdateCashOut) tenant.Delete("/sport/bet/:id", a.authMiddleware, h.DeleteTenantBet) @@ -458,9 +472,9 @@ func (a *App) initAppRoutes() { groupV1.Get("/settings/:key", a.authMiddleware, a.SuperAdminOnly, h.GetGlobalSettingByKey) groupV1.Put("/settings", a.authMiddleware, a.SuperAdminOnly, h.UpdateGlobalSettingList) - tenant.Get("/settings", a.authMiddleware, h.GetCompanySettingList) - tenant.Put("/settings", a.authMiddleware, h.SaveCompanySettingList) - tenant.Delete("/settings/:key", a.authMiddleware, h.DeleteCompanySetting) - tenant.Delete("/settings", a.authMiddleware, h.DeleteAllCompanySetting) + tenant.Get("/settings", a.authMiddleware, a.OnlyAdminAndAbove, h.GetCompanySettingList) + tenant.Put("/settings", a.authMiddleware, a.OnlyAdminAndAbove, h.SaveCompanySettingList) + tenant.Delete("/settings/:key", a.authMiddleware, a.OnlyAdminAndAbove, h.DeleteCompanySetting) + tenant.Delete("/settings", a.authMiddleware, a.OnlyAdminAndAbove, h.DeleteAllCompanySetting) } From 62258b7ecb6c1afe308ca2c367f181c6e9c8466f Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Mon, 6 Oct 2025 14:47:01 +0300 Subject: [PATCH 06/23] raffle ticket limit --- db/migrations/000001_fortune.up.sql | 4 +- db/query/raffle.sql | 20 +++++- gen/db/auth.sql.go | 2 +- gen/db/bet.sql.go | 2 +- gen/db/bet_stat.sql.go | 2 +- gen/db/bonus.sql.go | 2 +- gen/db/branch.sql.go | 2 +- gen/db/cashier.sql.go | 2 +- gen/db/company.sql.go | 2 +- gen/db/copyfrom.go | 2 +- gen/db/db.go | 2 +- gen/db/direct_deposit.sql.go | 2 +- gen/db/disabled_odds.sql.go | 2 +- gen/db/enet_pulse.sql.go | 2 +- gen/db/event_history.sql.go | 2 +- gen/db/events.sql.go | 2 +- gen/db/events_stat.sql.go | 2 +- gen/db/flags.sql.go | 2 +- gen/db/institutions.sql.go | 2 +- gen/db/issue_reporting.sql.go | 2 +- gen/db/leagues.sql.go | 2 +- gen/db/location.sql.go | 2 +- gen/db/models.go | 17 ++--- gen/db/monitor.sql.go | 2 +- gen/db/notification.sql.go | 2 +- gen/db/odd_history.sql.go | 2 +- gen/db/odds.sql.go | 2 +- gen/db/otp.sql.go | 2 +- gen/db/raffle.sql.go | 70 ++++++++++++++++++--- gen/db/referal.sql.go | 2 +- gen/db/report.sql.go | 2 +- gen/db/result.sql.go | 2 +- gen/db/result_log.sql.go | 2 +- gen/db/settings.sql.go | 2 +- gen/db/shop_transactions.sql.go | 2 +- gen/db/ticket.sql.go | 2 +- gen/db/transfer.sql.go | 2 +- gen/db/user.sql.go | 2 +- gen/db/virtual_games.sql.go | 2 +- gen/db/wallet.sql.go | 2 +- internal/domain/raffle.go | 24 +++---- internal/repository/raffel.go | 33 +++++++--- internal/services/raffle/port.go | 3 + internal/services/raffle/service.go | 12 ++++ internal/web_server/handlers/bet_handler.go | 43 ++++++++++--- 45 files changed, 215 insertions(+), 83 deletions(-) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index c2e98cb..d0e992b 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -509,6 +509,8 @@ CREATE TABLE IF NOT EXISTS raffles ( name VARCHAR(255) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT NOW(), expires_at TIMESTAMP NOT NULL, + -- -1 means there is no limit for the raffle + ticket_limit INT NOT NULL DEFAULT -1, type VARCHAR(50) NOT NULL CHECK (type IN ('virtual', 'sport')), status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'completed')) ); @@ -762,4 +764,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; \ No newline at end of file + ADD CONSTRAINT fk_odds_settings_odds_market FOREIGN KEY (odds_market_id) REFERENCES odds_market (id) ON DELETE CASCADE; diff --git a/db/query/raffle.sql b/db/query/raffle.sql index 55f302c..68318a6 100644 --- a/db/query/raffle.sql +++ b/db/query/raffle.sql @@ -1,6 +1,6 @@ -- name: CreateRaffle :one -INSERT INTO raffles (company_id, name, expires_at, type) -VALUES ($1, $2, $3, $4) +INSERT INTO raffles (company_id, name, expires_at, ticket_limit, type) +VALUES ($1, $2, $3, $4, $5) RETURNING *; -- name: GetRafflesOfCompany :many @@ -71,3 +71,19 @@ FROM raffle_sport_filters WHERE raffle_id = $1 AND sport_id = $2 AND league_id = $3; + +-- name: CheckSportRaffleHasFilter :one +SELECT EXISTS ( + SELECT 1 FROM raffle_sport_filters WHERE raffle_id = $1 +) AS has_filter; + +-- name: GetRaffleTicketLimit :one +SELECT ticket_limit +FROM raffles +WHERE id = $1; + +-- name: GetRaffleTicketCount :one +SELECT COUNT(*) +FROM raffle_tickets +WHERE raffle_id = $1 + AND user_id = $2; diff --git a/gen/db/auth.sql.go b/gen/db/auth.sql.go index 7d8d59d..8dd2280 100644 --- a/gen/db/auth.sql.go +++ b/gen/db/auth.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: auth.sql package dbgen diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index 8e6254c..a9fac0e 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: bet.sql package dbgen diff --git a/gen/db/bet_stat.sql.go b/gen/db/bet_stat.sql.go index 275ef07..9a7b494 100644 --- a/gen/db/bet_stat.sql.go +++ b/gen/db/bet_stat.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: bet_stat.sql package dbgen diff --git a/gen/db/bonus.sql.go b/gen/db/bonus.sql.go index 7c6f168..1a5d8e9 100644 --- a/gen/db/bonus.sql.go +++ b/gen/db/bonus.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: bonus.sql package dbgen diff --git a/gen/db/branch.sql.go b/gen/db/branch.sql.go index a9a57b8..89d2959 100644 --- a/gen/db/branch.sql.go +++ b/gen/db/branch.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: branch.sql package dbgen diff --git a/gen/db/cashier.sql.go b/gen/db/cashier.sql.go index fc4a7f8..55e69d2 100644 --- a/gen/db/cashier.sql.go +++ b/gen/db/cashier.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: cashier.sql package dbgen diff --git a/gen/db/company.sql.go b/gen/db/company.sql.go index ba728e7..0ec7a34 100644 --- a/gen/db/company.sql.go +++ b/gen/db/company.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: company.sql package dbgen diff --git a/gen/db/copyfrom.go b/gen/db/copyfrom.go index 1212253..f7a4793 100644 --- a/gen/db/copyfrom.go +++ b/gen/db/copyfrom.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: copyfrom.go package dbgen diff --git a/gen/db/db.go b/gen/db/db.go index 84de07c..8134784 100644 --- a/gen/db/db.go +++ b/gen/db/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 package dbgen diff --git a/gen/db/direct_deposit.sql.go b/gen/db/direct_deposit.sql.go index be02750..ff5a3b2 100644 --- a/gen/db/direct_deposit.sql.go +++ b/gen/db/direct_deposit.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: direct_deposit.sql package dbgen diff --git a/gen/db/disabled_odds.sql.go b/gen/db/disabled_odds.sql.go index b9cc744..58913cf 100644 --- a/gen/db/disabled_odds.sql.go +++ b/gen/db/disabled_odds.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: disabled_odds.sql package dbgen diff --git a/gen/db/enet_pulse.sql.go b/gen/db/enet_pulse.sql.go index a2c131b..9e72da8 100644 --- a/gen/db/enet_pulse.sql.go +++ b/gen/db/enet_pulse.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: enet_pulse.sql package dbgen diff --git a/gen/db/event_history.sql.go b/gen/db/event_history.sql.go index a4f1c2e..35946cd 100644 --- a/gen/db/event_history.sql.go +++ b/gen/db/event_history.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: event_history.sql package dbgen diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index a8345fb..1feaf00 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: events.sql package dbgen diff --git a/gen/db/events_stat.sql.go b/gen/db/events_stat.sql.go index 677fa2a..615e2fa 100644 --- a/gen/db/events_stat.sql.go +++ b/gen/db/events_stat.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: events_stat.sql package dbgen diff --git a/gen/db/flags.sql.go b/gen/db/flags.sql.go index 653543f..4b82cac 100644 --- a/gen/db/flags.sql.go +++ b/gen/db/flags.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: flags.sql package dbgen diff --git a/gen/db/institutions.sql.go b/gen/db/institutions.sql.go index 324ac3e..61ca108 100644 --- a/gen/db/institutions.sql.go +++ b/gen/db/institutions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: institutions.sql package dbgen diff --git a/gen/db/issue_reporting.sql.go b/gen/db/issue_reporting.sql.go index 7fcb4af..e35fba1 100644 --- a/gen/db/issue_reporting.sql.go +++ b/gen/db/issue_reporting.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: issue_reporting.sql package dbgen diff --git a/gen/db/leagues.sql.go b/gen/db/leagues.sql.go index 912e257..b724e00 100644 --- a/gen/db/leagues.sql.go +++ b/gen/db/leagues.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: leagues.sql package dbgen diff --git a/gen/db/location.sql.go b/gen/db/location.sql.go index 008aa61..254c73a 100644 --- a/gen/db/location.sql.go +++ b/gen/db/location.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: location.sql package dbgen diff --git a/gen/db/models.go b/gen/db/models.go index 339efb8..04f3ff9 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 package dbgen @@ -554,13 +554,14 @@ type Otp struct { } type Raffle struct { - ID int32 `json:"id"` - CompanyID int32 `json:"company_id"` - Name string `json:"name"` - CreatedAt pgtype.Timestamp `json:"created_at"` - ExpiresAt pgtype.Timestamp `json:"expires_at"` - Type string `json:"type"` - Status string `json:"status"` + ID int32 `json:"id"` + CompanyID int32 `json:"company_id"` + Name string `json:"name"` + CreatedAt pgtype.Timestamp `json:"created_at"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + TicketLimit int32 `json:"ticket_limit"` + Type string `json:"type"` + Status string `json:"status"` } type RaffleGameFilter struct { diff --git a/gen/db/monitor.sql.go b/gen/db/monitor.sql.go index a9a7ecb..b5f248f 100644 --- a/gen/db/monitor.sql.go +++ b/gen/db/monitor.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: monitor.sql package dbgen diff --git a/gen/db/notification.sql.go b/gen/db/notification.sql.go index f6747fb..14d3a4c 100644 --- a/gen/db/notification.sql.go +++ b/gen/db/notification.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: notification.sql package dbgen diff --git a/gen/db/odd_history.sql.go b/gen/db/odd_history.sql.go index 3fe7dd9..dd834c5 100644 --- a/gen/db/odd_history.sql.go +++ b/gen/db/odd_history.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: odd_history.sql package dbgen diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index e7c687e..d1e676d 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: odds.sql package dbgen diff --git a/gen/db/otp.sql.go b/gen/db/otp.sql.go index 7dba175..c96aaaa 100644 --- a/gen/db/otp.sql.go +++ b/gen/db/otp.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: otp.sql package dbgen diff --git a/gen/db/raffle.sql.go b/gen/db/raffle.sql.go index a7a364e..8d0be34 100644 --- a/gen/db/raffle.sql.go +++ b/gen/db/raffle.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: raffle.sql package dbgen @@ -35,6 +35,19 @@ func (q *Queries) AddSportRaffleFilter(ctx context.Context, arg AddSportRaffleFi return i, err } +const CheckSportRaffleHasFilter = `-- name: CheckSportRaffleHasFilter :one +SELECT EXISTS ( + SELECT 1 FROM raffle_sport_filters WHERE raffle_id = $1 +) AS has_filter +` + +func (q *Queries) CheckSportRaffleHasFilter(ctx context.Context, raffleID int32) (bool, error) { + row := q.db.QueryRow(ctx, CheckSportRaffleHasFilter, raffleID) + var has_filter bool + err := row.Scan(&has_filter) + return has_filter, err +} + const CheckValidSportRaffleFilter = `-- name: CheckValidSportRaffleFilter :one SELECT COUNT(*) > 0 AS exists FROM raffle_sport_filters @@ -57,16 +70,17 @@ func (q *Queries) CheckValidSportRaffleFilter(ctx context.Context, arg CheckVali } const CreateRaffle = `-- name: CreateRaffle :one -INSERT INTO raffles (company_id, name, expires_at, type) -VALUES ($1, $2, $3, $4) -RETURNING id, company_id, name, created_at, expires_at, type, status +INSERT INTO raffles (company_id, name, expires_at, ticket_limit, type) +VALUES ($1, $2, $3, $4, $5) +RETURNING id, company_id, name, created_at, expires_at, ticket_limit, type, status ` type CreateRaffleParams struct { - CompanyID int32 `json:"company_id"` - Name string `json:"name"` - ExpiresAt pgtype.Timestamp `json:"expires_at"` - Type string `json:"type"` + CompanyID int32 `json:"company_id"` + Name string `json:"name"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + TicketLimit int32 `json:"ticket_limit"` + Type string `json:"type"` } func (q *Queries) CreateRaffle(ctx context.Context, arg CreateRaffleParams) (Raffle, error) { @@ -74,6 +88,7 @@ func (q *Queries) CreateRaffle(ctx context.Context, arg CreateRaffleParams) (Raf arg.CompanyID, arg.Name, arg.ExpiresAt, + arg.TicketLimit, arg.Type, ) var i Raffle @@ -83,6 +98,7 @@ func (q *Queries) CreateRaffle(ctx context.Context, arg CreateRaffleParams) (Raf &i.Name, &i.CreatedAt, &i.ExpiresAt, + &i.TicketLimit, &i.Type, &i.Status, ) @@ -140,7 +156,7 @@ func (q *Queries) CreateRaffleWinner(ctx context.Context, arg CreateRaffleWinner const DeleteRaffle = `-- name: DeleteRaffle :one DELETE FROM raffles WHERE id = $1 -RETURNING id, company_id, name, created_at, expires_at, type, status +RETURNING id, company_id, name, created_at, expires_at, ticket_limit, type, status ` func (q *Queries) DeleteRaffle(ctx context.Context, id int32) (Raffle, error) { @@ -152,6 +168,7 @@ func (q *Queries) DeleteRaffle(ctx context.Context, id int32) (Raffle, error) { &i.Name, &i.CreatedAt, &i.ExpiresAt, + &i.TicketLimit, &i.Type, &i.Status, ) @@ -219,8 +236,40 @@ func (q *Queries) GetRaffleStanding(ctx context.Context, arg GetRaffleStandingPa return items, nil } +const GetRaffleTicketCount = `-- name: GetRaffleTicketCount :one +SELECT COUNT(*) +FROM raffle_tickets +WHERE raffle_id = $1 + AND user_id = $2 +` + +type GetRaffleTicketCountParams struct { + RaffleID int32 `json:"raffle_id"` + UserID int32 `json:"user_id"` +} + +func (q *Queries) GetRaffleTicketCount(ctx context.Context, arg GetRaffleTicketCountParams) (int64, error) { + row := q.db.QueryRow(ctx, GetRaffleTicketCount, arg.RaffleID, arg.UserID) + var count int64 + err := row.Scan(&count) + return count, err +} + +const GetRaffleTicketLimit = `-- name: GetRaffleTicketLimit :one +SELECT ticket_limit +FROM raffles +WHERE id = $1 +` + +func (q *Queries) GetRaffleTicketLimit(ctx context.Context, id int32) (int32, error) { + row := q.db.QueryRow(ctx, GetRaffleTicketLimit, id) + var ticket_limit int32 + err := row.Scan(&ticket_limit) + return ticket_limit, err +} + const GetRafflesOfCompany = `-- name: GetRafflesOfCompany :many -SELECT id, company_id, name, created_at, expires_at, type, status FROM raffles WHERE company_id = $1 +SELECT id, company_id, name, created_at, expires_at, ticket_limit, type, status FROM raffles WHERE company_id = $1 ` func (q *Queries) GetRafflesOfCompany(ctx context.Context, companyID int32) ([]Raffle, error) { @@ -238,6 +287,7 @@ func (q *Queries) GetRafflesOfCompany(ctx context.Context, companyID int32) ([]R &i.Name, &i.CreatedAt, &i.ExpiresAt, + &i.TicketLimit, &i.Type, &i.Status, ); err != nil { diff --git a/gen/db/referal.sql.go b/gen/db/referal.sql.go index caaa01a..99d8bb2 100644 --- a/gen/db/referal.sql.go +++ b/gen/db/referal.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: referal.sql package dbgen diff --git a/gen/db/report.sql.go b/gen/db/report.sql.go index 1a1ccde..d6193c1 100644 --- a/gen/db/report.sql.go +++ b/gen/db/report.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: report.sql package dbgen diff --git a/gen/db/result.sql.go b/gen/db/result.sql.go index bff7b1e..899561b 100644 --- a/gen/db/result.sql.go +++ b/gen/db/result.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: result.sql package dbgen diff --git a/gen/db/result_log.sql.go b/gen/db/result_log.sql.go index 468795e..3f11e16 100644 --- a/gen/db/result_log.sql.go +++ b/gen/db/result_log.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: result_log.sql package dbgen diff --git a/gen/db/settings.sql.go b/gen/db/settings.sql.go index 96ea916..76eb504 100644 --- a/gen/db/settings.sql.go +++ b/gen/db/settings.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: settings.sql package dbgen diff --git a/gen/db/shop_transactions.sql.go b/gen/db/shop_transactions.sql.go index bcd884e..7664dbb 100644 --- a/gen/db/shop_transactions.sql.go +++ b/gen/db/shop_transactions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: shop_transactions.sql package dbgen diff --git a/gen/db/ticket.sql.go b/gen/db/ticket.sql.go index bc9bb5f..45603ba 100644 --- a/gen/db/ticket.sql.go +++ b/gen/db/ticket.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: ticket.sql package dbgen diff --git a/gen/db/transfer.sql.go b/gen/db/transfer.sql.go index b2a1066..fe25cbe 100644 --- a/gen/db/transfer.sql.go +++ b/gen/db/transfer.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: transfer.sql package dbgen diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index f2f9fff..9b16163 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: user.sql package dbgen diff --git a/gen/db/virtual_games.sql.go b/gen/db/virtual_games.sql.go index 5a2809a..b98f602 100644 --- a/gen/db/virtual_games.sql.go +++ b/gen/db/virtual_games.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: virtual_games.sql package dbgen diff --git a/gen/db/wallet.sql.go b/gen/db/wallet.sql.go index fcde631..ccb2d37 100644 --- a/gen/db/wallet.sql.go +++ b/gen/db/wallet.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: wallet.sql package dbgen diff --git a/internal/domain/raffle.go b/internal/domain/raffle.go index a8307b8..ebbd9bf 100644 --- a/internal/domain/raffle.go +++ b/internal/domain/raffle.go @@ -3,13 +3,14 @@ package domain import "time" type Raffle struct { - ID int32 - CompanyID int32 - Name string - CreatedAt time.Time - ExpiresAt time.Time - Type string - Status string + ID int32 + CompanyID int32 + Name string + CreatedAt time.Time + ExpiresAt time.Time + TicketLimit int32 + Type string + Status string } type RaffleFilter struct { @@ -64,10 +65,11 @@ type RaffleTicketRes struct { } type CreateRaffle struct { - CompanyID int32 `json:"company_id" validate:"required"` - Name string `json:"name" validate:"required"` - ExpiresAt *time.Time `json:"expires_at" validate:"required"` - Type string `json:"type" validate:"required"` + CompanyID int32 `json:"company_id" validate:"required"` + Name string `json:"name" validate:"required"` + ExpiresAt *time.Time `json:"expires_at" validate:"required"` + TicketLimit int32 `json:"ticket_limit" validate:"required"` + Type string `json:"type" validate:"required"` } type CreateRaffleTicket struct { diff --git a/internal/repository/raffel.go b/internal/repository/raffel.go index 6e37013..298ed9e 100644 --- a/internal/repository/raffel.go +++ b/internal/repository/raffel.go @@ -10,13 +10,14 @@ import ( func convertRaffleOutcome(raffle dbgen.Raffle) domain.Raffle { return domain.Raffle{ - ID: raffle.ID, - CompanyID: raffle.CompanyID, - Name: raffle.Name, - CreatedAt: raffle.CreatedAt.Time, - ExpiresAt: raffle.ExpiresAt.Time, - Type: raffle.Type, - Status: raffle.Status, + ID: raffle.ID, + CompanyID: raffle.CompanyID, + Name: raffle.Name, + CreatedAt: raffle.CreatedAt.Time, + ExpiresAt: raffle.ExpiresAt.Time, + TicketLimit: raffle.TicketLimit, + Type: raffle.Type, + Status: raffle.Status, } } @@ -48,7 +49,8 @@ func convertCreateRaffle(raffle domain.CreateRaffle) dbgen.CreateRaffleParams { Time: *raffle.ExpiresAt, Valid: true, }, - Type: raffle.Type, + TicketLimit: raffle.TicketLimit, + Type: raffle.Type, } } @@ -191,3 +193,18 @@ func (s *Store) CheckValidSportRaffleFilter(ctx context.Context, raffleID int32, return res, nil } + +func (s *Store) GetRaffleTicketLimit(ctx context.Context, raffleID int32) (int32, error) { + return s.queries.GetRaffleTicketLimit(ctx, raffleID) +} + +func (s *Store) GetRaffleTicketCount(ctx context.Context, raffleID, userID int32) (int64, error) { + return s.queries.GetRaffleTicketCount(ctx, dbgen.GetRaffleTicketCountParams{ + RaffleID: raffleID, + UserID: userID, + }) +} + +func (s *Store) CheckSportRaffleHasFilter(ctx context.Context, raffleID int32) (bool, error) { + return s.queries.CheckSportRaffleHasFilter(ctx, raffleID) +} diff --git a/internal/services/raffle/port.go b/internal/services/raffle/port.go index 39f5bfa..edbe4d3 100644 --- a/internal/services/raffle/port.go +++ b/internal/services/raffle/port.go @@ -16,9 +16,12 @@ type RaffleStore interface { CreateRaffleWinner(ctx context.Context, raffleWinnerParams domain.RaffleWinnerParams) error SetRaffleComplete(ctx context.Context, raffleID int32) error CheckValidSportRaffleFilter(ctx context.Context, raffleID int32, sportID, leagueID int64) (bool, error) + CheckSportRaffleHasFilter(ctx context.Context, raffleID int32) (bool, error) CreateRaffleTicket(ctx context.Context, raffleTicketParams domain.CreateRaffleTicket) (domain.RaffleTicket, error) GetUserRaffleTickets(ctx context.Context, userID int32) ([]domain.RaffleTicketRes, error) SuspendRaffleTicket(ctx context.Context, raffleTicketID int32) error UnSuspendRaffleTicket(ctx context.Context, raffleID int32) error + GetRaffleTicketCount(ctx context.Context, raffleID, userID int32) (int64, error) + GetRaffleTicketLimit(ctx context.Context, raffleID int32) (int32, error) } diff --git a/internal/services/raffle/service.go b/internal/services/raffle/service.go index 017d164..3483839 100644 --- a/internal/services/raffle/service.go +++ b/internal/services/raffle/service.go @@ -64,3 +64,15 @@ func (s *Service) UnSuspendRaffleTicket(ctx context.Context, raffleID int32) err func (s *Service) CheckValidSportRaffleFilter(ctx context.Context, raffleID int32, sportID, leagueID int64) (bool, error) { return s.raffleStore.CheckValidSportRaffleFilter(ctx, raffleID, sportID, leagueID) } + +func (s *Service) CheckSportRaffleHasFilter(ctx context.Context, raffleID int32) (bool, error) { + return s.raffleStore.CheckSportRaffleHasFilter(ctx, raffleID) +} + +func (s *Service) GetRaffleTicketCount(ctx context.Context, raffleID, userID int32) (int64, error) { + return s.raffleStore.GetRaffleTicketCount(ctx, raffleID, userID) +} + +func (s *Service) GetRaffleTicketLimit(ctx context.Context, raffleID int32) (int32, error) { + return s.raffleStore.GetRaffleTicketLimit(ctx, raffleID) +} diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index 7677b2e..8fe7d5f 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -259,24 +259,53 @@ func (h *Handler) CreateBetInternal(c *fiber.Ctx, req domain.CreateBetReq, userI sportAndLeagueIDs = append(sportAndLeagueIDs, ids) } - fmt.Println("sportAndLeagueIDs: ", sportAndLeagueIDs) - for _, raffle := range raffles { // TODO: only fetch pending raffles from db if raffle.Status == "completed" { continue } - // only require one sport and league combo to be valide to make the raffle ticket - foundValid := false + raffleTicketLimit, err := h.raffleSvc.GetRaffleTicketLimit(c.Context(), raffle.ID) + if err != nil { + continue + } + + // check raffle ticke count + userTicketCount, err := h.raffleSvc.GetRaffleTicketCount(c.Context(), raffle.ID, int32(userID)) + if err != nil { + continue + } + + if userTicketCount == int64(raffleTicketLimit) { + h.mongoLoggerSvc.Info("User reached max ticket count allowed for current raffle", + zap.Int("status_code", fiber.StatusForbidden), + zap.Int64("raffleID", int64(raffle.ID)), + zap.Int64("userID", userID), + zap.Int64("companyID", companyID), + zap.Time("timestamp", time.Now()), + ) + continue + } + + // empty raffle filter means there is no filter (all is allowed) + hasFilter, err := h.raffleSvc.CheckSportRaffleHasFilter(c.Context(), raffle.ID) + if err != nil { + continue + } + + foundValid := !hasFilter + + // only require one sport and league combo to be valid to make the raffle ticket for _, sportAndLeagueID := range sportAndLeagueIDs { + if foundValid { + break + } + res, err := h.raffleSvc.CheckValidSportRaffleFilter(c.Context(), raffle.ID, sportAndLeagueID[0], sportAndLeagueID[1]) if err != nil { continue } - fmt.Println(sportAndLeagueID, res) - foundValid = foundValid || res } @@ -289,7 +318,7 @@ func (h *Handler) CreateBetInternal(c *fiber.Ctx, req domain.CreateBetReq, userI UserID: int32(userID), } - _, err := h.raffleSvc.CreateRaffleTicket(c.Context(), raffleTicket) + _, err = h.raffleSvc.CreateRaffleTicket(c.Context(), raffleTicket) if err != nil { h.mongoLoggerSvc.Error("Failed to create raffle ticket", zap.Int("status_code", fiber.StatusInternalServerError), From d997cde3878c08c3d525e4d04e56eacac7136a68 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Tue, 7 Oct 2025 13:39:36 +0300 Subject: [PATCH 07/23] fix: integration fixes --- db/migrations/000001_fortune.up.sql | 37 +- db/query/company.sql | 11 +- db/query/leagues.sql | 23 + docs/docs.go | 1879 ++++++++++++++--- docs/swagger.json | 1879 ++++++++++++++--- docs/swagger.yaml | 1233 +++++++++-- gen/db/company.sql.go | 31 +- gen/db/leagues.sql.go | 48 +- gen/db/models.go | 1 + gen/db/shop_transactions.sql.go | 15 +- internal/domain/company.go | 44 +- internal/domain/shop_bet.go | 3 + internal/domain/wallet.go | 9 +- internal/repository/bet.go | 7 +- internal/repository/common.go | 12 + internal/repository/company.go | 49 +- internal/repository/event.go | 14 +- internal/repository/league.go | 13 +- internal/repository/shop_bet.go | 5 +- internal/repository/user.go | 2 +- internal/repository/virtual_game.go | 56 +- internal/repository/wallet.go | 3 + internal/services/company/port.go | 2 +- internal/services/company/service.go | 2 +- internal/services/league/port.go | 2 +- internal/services/league/service.go | 4 +- internal/services/report/service.go | 10 +- internal/services/virtualGame/port.go | 2 +- internal/services/virtualGame/service.go | 6 +- internal/web_server/cron.go | 2 +- internal/web_server/handlers/admin.go | 42 +- .../web_server/handlers/company_handler.go | 24 +- internal/web_server/handlers/leagues.go | 4 +- internal/web_server/handlers/user.go | 6 +- 34 files changed, 4654 insertions(+), 826 deletions(-) create mode 100644 internal/repository/common.go diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index c2e98cb..0fd37cb 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -73,7 +73,8 @@ CREATE TABLE IF NOT EXISTS wallets ( is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(user_id, type) + UNIQUE(user_id, type), + CONSTRAINT balance_positve CHECK (balance >= 0) ); CREATE TABLE refresh_tokens ( id BIGSERIAL PRIMARY KEY, @@ -184,19 +185,19 @@ CREATE TABLE IF NOT EXISTS banks ( currency VARCHAR(10) NOT NULL, bank_logo TEXT -- URL or base64 string ); -CREATE TABLE IF NOT EXISTS wallets ( - id BIGSERIAL PRIMARY KEY, - balance BIGINT NOT NULL DEFAULT 0, - is_withdraw BOOLEAN NOT NULL, - is_bettable BOOLEAN NOT NULL, - is_transferable BOOLEAN NOT NULL, - user_id BIGINT NOT NULL, - type VARCHAR(255) NOT NULL, - is_active BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT balance_positve CHECK (balance >= 0) -); +-- CREATE TABLE IF NOT EXISTS wallets ( +-- id BIGSERIAL PRIMARY KEY, +-- balance BIGINT NOT NULL DEFAULT 0, +-- is_withdraw BOOLEAN NOT NULL, +-- is_bettable BOOLEAN NOT NULL, +-- is_transferable BOOLEAN NOT NULL, +-- user_id BIGINT NOT NULL, +-- type VARCHAR(255) NOT NULL, +-- is_active BOOLEAN NOT NULL DEFAULT true, +-- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, +-- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, +-- CONSTRAINT balance_positve CHECK (balance >= 0) +-- ); CREATE TABLE IF NOT EXISTS customer_wallets ( id BIGSERIAL PRIMARY KEY, customer_id BIGINT NOT NULL, @@ -270,7 +271,7 @@ CREATE TABLE IF NOT EXISTS branches ( name VARCHAR(255) NOT NULL, location TEXT NOT NULL, profit_percent REAL NOT NULL, - is_active BOOLEAN NOT NULL DEFAULT false, + is_active BOOLEAN NOT NULL DEFAULT true, wallet_id BIGINT NOT NULL, branch_manager_id BIGINT NOT NULL, company_id BIGINT NOT NULL, @@ -406,7 +407,7 @@ CREATE TABLE companies ( admin_id BIGINT NOT NULL, wallet_id BIGINT NOT NULL, deducted_percentage REAL NOT NULL, - is_active BOOLEAN NOT NULL DEFAULT false, + is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT deducted_percentage_check CHECK ( @@ -642,6 +643,7 @@ SELECT sb.*, st.verified AS transaction_verified, bets.status, bets.total_odds, + bets.fast_code, JSON_AGG (bet_outcomes.*) AS outcomes FROM shop_bets AS sb JOIN shop_transactions st ON st.id = sb.shop_transaction_id @@ -655,7 +657,8 @@ GROUP BY sb.id, st.amount, st.verified, bets.status, - bets.total_odds; + bets.total_odds, + bets.fast_code; CREATE VIEW shop_deposit_detail AS SELECT sd.*, st.full_name, diff --git a/db/query/company.sql b/db/query/company.sql index 516775f..4f5952c 100644 --- a/db/query/company.sql +++ b/db/query/company.sql @@ -4,9 +4,10 @@ INSERT INTO companies ( slug, admin_id, wallet_id, - deducted_percentage + deducted_percentage, + is_active ) -VALUES ($1, $2, $3, $4, $5) +VALUES ($1, $2, $3, $4, $5, $6) RETURNING *; -- name: GetAllCompanies :many SELECT * @@ -38,7 +39,7 @@ WHERE slug = $1; SELECT * FROM companies_details WHERE name ILIKE '%' || $1 || '%'; --- name: UpdateCompany :one +-- name: UpdateCompany :exec UPDATE companies SET name = COALESCE(sqlc.narg(name), name), admin_id = COALESCE(sqlc.narg(admin_id), admin_id), @@ -47,9 +48,9 @@ SET name = COALESCE(sqlc.narg(name), name), sqlc.narg(deducted_percentage), deducted_percentage ), + slug = COALESCE(sqlc.narg(slug), slug), updated_at = CURRENT_TIMESTAMP -WHERE id = $1 -RETURNING *; +WHERE id = $1; -- name: DeleteCompany :exec DELETE FROM companies WHERE id = $1; \ No newline at end of file diff --git a/db/query/leagues.sql b/db/query/leagues.sql index b45460f..047ce5d 100644 --- a/db/query/leagues.sql +++ b/db/query/leagues.sql @@ -40,8 +40,31 @@ WHERE ( name ILIKE '%' || sqlc.narg('query') || '%' OR sqlc.narg('query') IS NULL ) + AND ( + default_is_active = sqlc.narg('is_active') + OR sqlc.narg('is_active') IS NULL + ) ORDER BY name ASC LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); +-- name: GetTotalLeagues :one +SELECT COUNT(*) +FROM leagues +WHERE ( + country_code = sqlc.narg('country_code') + OR sqlc.narg('country_code') IS NULL + ) + AND ( + sport_id = sqlc.narg('sport_id') + OR sqlc.narg('sport_id') IS NULL + ) + AND ( + name ILIKE '%' || sqlc.narg('query') || '%' + OR sqlc.narg('query') IS NULL + ) + AND ( + default_is_active = sqlc.narg('is_active') + OR sqlc.narg('is_active') IS NULL + ); -- name: GetTotalLeaguesWithSettings :one SELECT COUNT(*) FROM leagues l diff --git a/docs/docs.go b/docs/docs.go index a7f73c8..3114939 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -125,7 +125,7 @@ const docTemplate = `{ } }, "post": { - "description": "Create Admin", + "description": "Create transaction approver", "consumes": [ "application/json" ], @@ -135,15 +135,15 @@ const docTemplate = `{ "tags": [ "admin" ], - "summary": "Create Admin", + "summary": "Create transaction approver", "parameters": [ { - "description": "Create admin", + "description": "Create transaction approver", "name": "manger", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handlers.CreateAdminReq" + "$ref": "#/definitions/handlers.CreateTransactionApproverReq" } } ], @@ -800,6 +800,47 @@ const docTemplate = `{ } } }, + "/api/v1/atlas/games": { + "get": { + "description": "Retrieves available Atlas virtual games from the provider", + "produces": [ + "application/json" + ], + "tags": [ + "Virtual Games - Atlas" + ], + "summary": "List Atlas virtual games", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.AtlasGameEntity" + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/atlas/init-game": { "post": { "description": "Initializes a game session for the given player using Atlas virtual game provider", @@ -1613,6 +1654,50 @@ const docTemplate = `{ } } }, + "/api/v1/branch/{id}/return": { + "post": { + "description": "Unassign the branch wallet to company", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Unassign the branch wallet to company", + "parameters": [ + { + "type": "integer", + "description": "Branch ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BranchDetailRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/branchCashier": { "get": { "description": "Gets branch for cahier", @@ -2751,6 +2836,56 @@ const docTemplate = `{ } } }, + "/api/v1/customer/{id}/bets": { + "get": { + "description": "Get customer bets", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "customer" + ], + "summary": "Get customer bets", + "parameters": [ + { + "type": "integer", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CustomersRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/customerWallet": { "get": { "description": "Retrieve all customer wallets", @@ -2913,6 +3048,182 @@ const docTemplate = `{ } } }, + "/api/v1/enetpulse/sports": { + "get": { + "description": "Fetches all sports stored in the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EnetPulse - Sports" + ], + "summary": "Get all sports", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulseSport" + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/enetpulse/tournament-stages": { + "get": { + "description": "Fetches all tournament stages stored in the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EnetPulse - Tournament Stages" + ], + "summary": "Get all tournament stages", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulseTournamentStage" + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/enetpulse/tournament-templates": { + "get": { + "description": "Fetches all tournament templates stored in the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EnetPulse - Tournament Templates" + ], + "summary": "Get all tournament templates", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulseTournamentTemplate" + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/enetpulse/tournaments": { + "get": { + "description": "Fetches all tournaments stored in the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EnetPulse - Tournaments" + ], + "summary": "Get all tournaments", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulseTournament" + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/events": { "get": { "description": "Retrieve all upcoming events from the database", @@ -3075,6 +3386,138 @@ const docTemplate = `{ } } }, + "/api/v1/events/{id}/bets": { + "get": { + "description": "Retrieve bet outcomes by event id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve bet outcomes by event id", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BaseEvent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/events/{id}/is_monitored": { + "patch": { + "description": "Update the event is_monitored", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "event" + ], + "summary": "update the event is_monitored", + "parameters": [ + { + "type": "integer", + "description": "Event ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/events/{id}/settings": { + "put": { + "description": "Update the event settings", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "event" + ], + "summary": "update the event settings", + "parameters": [ + { + "type": "integer", + "description": "Event ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/issues": { "get": { "description": "Admin endpoint to list all reported issues with pagination", @@ -3681,6 +4124,108 @@ const docTemplate = `{ } } }, + "/api/v1/odds/pre-match": { + "get": { + "description": "Fetches pre-match odds from EnetPulse for a given event", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EnetPulse - PreMatch" + ], + "summary": "Get pre-match odds for an event", + "parameters": [ + { + "type": "integer", + "description": "Event ID", + "name": "objectFK", + "in": "query", + "required": true + }, + { + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "csv", + "description": "Odds provider IDs (comma separated)", + "name": "oddsProviderFK", + "in": "query" + }, + { + "type": "integer", + "description": "Outcome type ID", + "name": "outcomeTypeFK", + "in": "query" + }, + { + "type": "integer", + "description": "Outcome scope ID", + "name": "outcomeScopeFK", + "in": "query" + }, + { + "type": "integer", + "description": "Outcome subtype ID", + "name": "outcomeSubtypeFK", + "in": "query" + }, + { + "type": "integer", + "description": "Limit results", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset results", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Language type ID", + "name": "languageTypeFK", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.PreMatchOddsResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/odds/upcoming/{upcoming_id}": { "get": { "description": "Retrieve prematch odds by upcoming event ID (FI from Bet365) with optional pagination", @@ -3916,147 +4461,6 @@ const docTemplate = `{ } } }, - "/api/v1/referral/settings": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Retrieves current referral settings (admin only)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "referral" - ], - "summary": "Get referral settings", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/domain.ReferralSettings" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - }, - "put": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Updates referral settings (admin only)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "referral" - ], - "summary": "Update referral settings", - "parameters": [ - { - "description": "Referral settings", - "name": "settings", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.ReferralSettings" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/api/v1/referral/stats": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Retrieves referral statistics for the authenticated user", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "referral" - ], - "summary": "Get referral statistics", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/domain.ReferralStats" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/api/v1/report-files/download/{filename}": { "get": { "description": "Downloads a generated report CSV file from the server", @@ -4248,7 +4652,7 @@ const docTemplate = `{ } } }, - "/api/v1/result/{id}": { + "/api/v1/result/b365/{id}": { "get": { "description": "Get results for an event", "consumes": [ @@ -5128,6 +5532,92 @@ const docTemplate = `{ } } }, + "/api/v1/sport/bet/{id}": { + "get": { + "description": "Gets a single bet by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets bet by id", + "parameters": [ + { + "type": "integer", + "description": "Bet ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BetRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "delete": { + "description": "Deletes bet by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Deletes bet by id", + "parameters": [ + { + "type": "integer", + "description": "Bet ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/super-login": { "post": { "description": "Login super-admin", @@ -5262,6 +5752,161 @@ const docTemplate = `{ } } }, + "/api/v1/t-approver": { + "get": { + "description": "Get all Admins", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get all Admins", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.AdminRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/t-approver/{id}": { + "get": { + "description": "Get a single admin by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get admin by id", + "parameters": [ + { + "type": "integer", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.AdminRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "description": "Update Admin", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Update Admin", + "parameters": [ + { + "description": "Update Admin", + "name": "admin", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateAdminReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/telebirr/callback": { "post": { "description": "Processes the Telebirr payment result and updates wallet balance.", @@ -5354,6 +5999,385 @@ const docTemplate = `{ } } }, + "/api/v1/tenant": { + "get": { + "description": "Check if phone number or email exist", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Check if phone number or email exist", + "parameters": [ + { + "description": "Check phone number or email exist", + "name": "checkPhoneEmailExist", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CheckPhoneEmailExistReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CheckPhoneEmailExistRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/tenant/{tenant_slug}/customer": { + "get": { + "description": "Get all Customers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "customer" + ], + "summary": "Get all Customers", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CustomersRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/tenant/{tenant_slug}/customer/{id}": { + "get": { + "description": "Get a single customer by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "customer" + ], + "summary": "Get customer by id", + "parameters": [ + { + "type": "integer", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CustomersRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "description": "Update Customers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "customer" + ], + "summary": "Update Customers", + "parameters": [ + { + "description": "Update Customers", + "name": "Customers", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateCustomerReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/tenant/{tenant_slug}/customer/{id}/bets": { + "get": { + "description": "Get tenant customer bets", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "customer" + ], + "summary": "Get tenant customer bets", + "parameters": [ + { + "type": "integer", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CustomersRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/tenant/{tenant_slug}/events/{id}/bets": { + "get": { + "description": "Retrieve bet outcomes by event id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve bet outcomes by event id", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BaseEvent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/tenant/{tenant_slug}/events/{id}/settings": { + "put": { + "description": "Update the event settings", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "event" + ], + "summary": "update the event settings", + "parameters": [ + { + "type": "integer", + "description": "Event ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/tenant/{tenant_slug}/referral/stats": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Retrieves referral statistics for the authenticated user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "referral" + ], + "summary": "Get referral statistics", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ReferralStats" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/transfer/refill/:id": { "post": { "description": "Super Admin route to refill a wallet", @@ -5536,6 +6560,144 @@ const docTemplate = `{ } } }, + "/api/v1/user/resetPassword": { + "post": { + "description": "Reset password", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Reset password", + "parameters": [ + { + "description": "Reset password", + "name": "resetPassword", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.ResetPasswordReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/user/search": { + "post": { + "description": "Search for user using name or phone", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Search for user using name or phone", + "parameters": [ + { + "description": "Search for using his name or phone", + "name": "searchUserByNameOrPhone", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.SearchUserByNameOrPhoneReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.UserProfileRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/user/sendResetCode": { + "post": { + "description": "Send reset code", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Send reset code", + "parameters": [ + { + "description": "Send reset code", + "name": "resetCode", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.ResetCodeReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/user/single/{id}": { "get": { "description": "Get a single user by id", @@ -6702,50 +7864,6 @@ const docTemplate = `{ } } }, - "/api/v1/{tenant_slug}/events/{id}/settings": { - "put": { - "description": "Update the event settings", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "event" - ], - "summary": "update the event settings", - "parameters": [ - { - "type": "integer", - "description": "Event ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/api/v1/{tenant_slug}/leagues": { "get": { "description": "Gets all leagues", @@ -7760,7 +8878,7 @@ const docTemplate = `{ }, "/api/v1/{tenant_slug}/user/resetPassword": { "post": { - "description": "Reset password", + "description": "Reset tenant password", "consumes": [ "application/json" ], @@ -7770,7 +8888,7 @@ const docTemplate = `{ "tags": [ "user" ], - "summary": "Reset password", + "summary": "Reset tenant password", "parameters": [ { "description": "Reset password", @@ -8590,6 +9708,40 @@ const docTemplate = `{ } } }, + "domain.AtlasGameEntity": { + "type": "object", + "properties": { + "demo_url": { + "description": "✅ new field", + "type": "string" + }, + "deviceType": { + "type": "string" + }, + "game_id": { + "type": "string" + }, + "hasFreeBets": { + "type": "boolean" + }, + "has_demo": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "providerId": { + "type": "string" + }, + "thumbnail_img_url": { + "description": "✅ new field", + "type": "string" + }, + "type": { + "type": "string" + } + } + }, "domain.AtlasGameInitRequest": { "type": "object", "properties": { @@ -8733,7 +9885,7 @@ const docTemplate = `{ "type": "string" }, "id": { - "type": "string" + "type": "integer" }, "isLive": { "type": "boolean" @@ -8765,6 +9917,9 @@ const docTemplate = `{ "source": { "$ref": "#/definitions/domain.EventSource" }, + "sourceEventID": { + "type": "string" + }, "sportID": { "type": "integer" }, @@ -9347,9 +10502,15 @@ const docTemplate = `{ "type": "number", "example": 0.1 }, + "is_active": { + "type": "boolean" + }, "name": { "type": "string", "example": "CompanyName" + }, + "slug": { + "type": "string" } } }, @@ -9591,6 +10752,172 @@ const docTemplate = `{ } } }, + "domain.EnetpulseSport": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "description": "DB primary key", + "type": "integer" + }, + "last_updated_at": { + "type": "string" + }, + "name": { + "description": "from API \"name\"", + "type": "string" + }, + "sport_id": { + "description": "from API \"id\"", + "type": "string" + }, + "status": { + "description": "active/inactive", + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "updates_count": { + "description": "from API \"n\"", + "type": "integer" + } + } + }, + "domain.EnetpulseTournament": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "id": { + "description": "internal DB PK", + "type": "integer" + }, + "lastUpdatedAt": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "tournamentID": { + "type": "string" + }, + "tournamentTemplateFK": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "updatesCount": { + "type": "integer" + } + } + }, + "domain.EnetpulseTournamentStage": { + "type": "object", + "properties": { + "country_fk": { + "description": "country FK from API", + "type": "string" + }, + "country_name": { + "description": "country name from API", + "type": "string" + }, + "created_at": { + "type": "string" + }, + "end_date": { + "description": "end date/time", + "type": "string" + }, + "gender": { + "description": "male/female/mixed/unknown", + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_updated_at": { + "description": "ut from API", + "type": "string" + }, + "name": { + "description": "API name", + "type": "string" + }, + "stage_id": { + "description": "API id", + "type": "string" + }, + "start_date": { + "description": "start date/time", + "type": "string" + }, + "status": { + "description": "active/inactive", + "type": "integer" + }, + "tournament_fk": { + "description": "Foreign key to tournament", + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "updates_count": { + "description": "n from API", + "type": "integer" + } + } + }, + "domain.EnetpulseTournamentTemplate": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "gender": { + "description": "male, female, mixed, unknown", + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_updated_at": { + "type": "string" + }, + "name": { + "description": "from API \"name\"", + "type": "string" + }, + "sport_fk": { + "description": "related sport id", + "type": "string" + }, + "status": { + "description": "optional", + "type": "integer" + }, + "template_id": { + "description": "from API \"id\"", + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "updates_count": { + "description": "from API \"n\"", + "type": "integer" + } + } + }, "domain.ErrorResponse": { "type": "object", "properties": { @@ -9667,6 +10994,15 @@ const docTemplate = `{ "away_team_image": { "type": "string" }, + "default_is_active": { + "type": "boolean" + }, + "default_is_featured": { + "type": "boolean" + }, + "default_winning_upper_limit": { + "type": "integer" + }, "fetched_at": { "type": "string" }, @@ -9680,7 +11016,7 @@ const docTemplate = `{ "type": "string" }, "id": { - "type": "string" + "type": "integer" }, "is_active": { "type": "boolean" @@ -9718,6 +11054,9 @@ const docTemplate = `{ "source": { "$ref": "#/definitions/domain.EventSource" }, + "source_event_id": { + "type": "string" + }, "sport_id": { "type": "integer" }, @@ -10344,11 +11683,17 @@ const docTemplate = `{ "domain.OddMarketWithEventFilter": { "type": "object", "properties": { + "isLive": { + "$ref": "#/definitions/domain.ValidBool" + }, "limit": { "$ref": "#/definitions/domain.ValidInt32" }, "offset": { "$ref": "#/definitions/domain.ValidInt32" + }, + "status": { + "$ref": "#/definitions/domain.ValidString" } } }, @@ -10517,6 +11862,41 @@ const docTemplate = `{ } } }, + "domain.PreMatchOddsResponse": { + "type": "object", + "properties": { + "eventFK": { + "description": "Define fields according to the Enetpulse preodds response structure\nExample:", + "type": "integer" + }, + "odds": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.PreMatchOutcome" + } + } + } + }, + "domain.PreMatchOutcome": { + "type": "object", + "properties": { + "oddsProviderFK": { + "type": "integer" + }, + "oddsValue": { + "type": "number" + }, + "outcomeFK": { + "type": "integer" + }, + "outcomeTypeFK": { + "type": "integer" + }, + "outcomeValue": { + "type": "string" + } + } + }, "domain.ProviderRequest": { "type": "object", "properties": { @@ -10603,55 +11983,14 @@ const docTemplate = `{ } } }, - "domain.ReferralSettings": { - "type": "object", - "properties": { - "betReferralBonusPercentage": { - "type": "number" - }, - "cashbackPercentage": { - "type": "number" - }, - "createdAt": { - "type": "string" - }, - "expiresAfterDays": { - "type": "integer" - }, - "id": { - "type": "integer" - }, - "maxReferrals": { - "type": "integer" - }, - "referralRewardAmount": { - "type": "number" - }, - "updatedAt": { - "type": "string" - }, - "updatedBy": { - "type": "string" - }, - "version": { - "type": "integer" - } - } - }, "domain.ReferralStats": { "type": "object", "properties": { - "completedReferrals": { - "type": "integer" - }, - "pendingRewards": { - "type": "number" - }, "totalReferrals": { "type": "integer" }, "totalRewardEarned": { - "type": "number" + "type": "integer" } } }, @@ -10713,14 +12052,16 @@ const docTemplate = `{ "admin", "branch_manager", "customer", - "cashier" + "cashier", + "transaction_approver" ], "x-enum-varnames": [ "RoleSuperAdmin", "RoleAdmin", "RoleBranchManager", "RoleCustomer", - "RoleCashier" + "RoleCashier", + "RoleTransactionApprover" ] }, "domain.RollbackRequest": { @@ -10955,6 +12296,10 @@ const docTemplate = `{ "type": "string", "example": "2025-04-08T12:00:00Z" }, + "fast_code": { + "type": "string", + "example": "12SD1" + }, "full_name": { "type": "string", "example": "John" @@ -11362,6 +12707,9 @@ const docTemplate = `{ "category": { "type": "string" }, + "demoUrl": { + "type": "string" + }, "deviceType": { "type": "string" }, @@ -11415,6 +12763,9 @@ const docTemplate = `{ "name": { "type": "string", "example": "CompanyName" + }, + "slug": { + "type": "string" } } }, @@ -11427,6 +12778,17 @@ const docTemplate = `{ } } }, + "domain.ValidBool": { + "type": "object", + "properties": { + "valid": { + "type": "boolean" + }, + "value": { + "type": "boolean" + } + } + }, "domain.ValidInt": { "type": "object", "properties": { @@ -11748,6 +13110,35 @@ const docTemplate = `{ } } }, + "handlers.CreateTransactionApproverReq": { + "type": "object", + "properties": { + "company_id": { + "type": "integer", + "example": 1 + }, + "email": { + "type": "string", + "example": "john.doe@example.com" + }, + "first_name": { + "type": "string", + "example": "John" + }, + "last_name": { + "type": "string", + "example": "Doe" + }, + "password": { + "type": "string", + "example": "password123" + }, + "phone_number": { + "type": "string", + "example": "1234567890" + } + } + }, "handlers.CreateTransferReq": { "type": "object", "properties": { @@ -11869,6 +13260,12 @@ const docTemplate = `{ "handlers.CustomersRes": { "type": "object", "properties": { + "company_id": { + "type": "integer" + }, + "company_name": { + "type": "string" + }, "created_at": { "type": "string" }, @@ -12050,7 +13447,7 @@ const docTemplate = `{ "type": "string", "example": "1234567890" }, - "referal_code": { + "referral_code": { "type": "string", "example": "ABC123" } @@ -12513,10 +13910,6 @@ const docTemplate = `{ "handlers.updateCustomerReq": { "type": "object", "properties": { - "company_id": { - "type": "integer", - "example": 1 - }, "first_name": { "type": "string", "example": "John" diff --git a/docs/swagger.json b/docs/swagger.json index 672b70e..bae0014 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -117,7 +117,7 @@ } }, "post": { - "description": "Create Admin", + "description": "Create transaction approver", "consumes": [ "application/json" ], @@ -127,15 +127,15 @@ "tags": [ "admin" ], - "summary": "Create Admin", + "summary": "Create transaction approver", "parameters": [ { - "description": "Create admin", + "description": "Create transaction approver", "name": "manger", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handlers.CreateAdminReq" + "$ref": "#/definitions/handlers.CreateTransactionApproverReq" } } ], @@ -792,6 +792,47 @@ } } }, + "/api/v1/atlas/games": { + "get": { + "description": "Retrieves available Atlas virtual games from the provider", + "produces": [ + "application/json" + ], + "tags": [ + "Virtual Games - Atlas" + ], + "summary": "List Atlas virtual games", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.AtlasGameEntity" + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/atlas/init-game": { "post": { "description": "Initializes a game session for the given player using Atlas virtual game provider", @@ -1605,6 +1646,50 @@ } } }, + "/api/v1/branch/{id}/return": { + "post": { + "description": "Unassign the branch wallet to company", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Unassign the branch wallet to company", + "parameters": [ + { + "type": "integer", + "description": "Branch ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BranchDetailRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/branchCashier": { "get": { "description": "Gets branch for cahier", @@ -2743,6 +2828,56 @@ } } }, + "/api/v1/customer/{id}/bets": { + "get": { + "description": "Get customer bets", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "customer" + ], + "summary": "Get customer bets", + "parameters": [ + { + "type": "integer", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CustomersRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/customerWallet": { "get": { "description": "Retrieve all customer wallets", @@ -2905,6 +3040,182 @@ } } }, + "/api/v1/enetpulse/sports": { + "get": { + "description": "Fetches all sports stored in the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EnetPulse - Sports" + ], + "summary": "Get all sports", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulseSport" + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/enetpulse/tournament-stages": { + "get": { + "description": "Fetches all tournament stages stored in the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EnetPulse - Tournament Stages" + ], + "summary": "Get all tournament stages", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulseTournamentStage" + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/enetpulse/tournament-templates": { + "get": { + "description": "Fetches all tournament templates stored in the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EnetPulse - Tournament Templates" + ], + "summary": "Get all tournament templates", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulseTournamentTemplate" + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/enetpulse/tournaments": { + "get": { + "description": "Fetches all tournaments stored in the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EnetPulse - Tournaments" + ], + "summary": "Get all tournaments", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulseTournament" + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/events": { "get": { "description": "Retrieve all upcoming events from the database", @@ -3067,6 +3378,138 @@ } } }, + "/api/v1/events/{id}/bets": { + "get": { + "description": "Retrieve bet outcomes by event id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve bet outcomes by event id", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BaseEvent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/events/{id}/is_monitored": { + "patch": { + "description": "Update the event is_monitored", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "event" + ], + "summary": "update the event is_monitored", + "parameters": [ + { + "type": "integer", + "description": "Event ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/events/{id}/settings": { + "put": { + "description": "Update the event settings", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "event" + ], + "summary": "update the event settings", + "parameters": [ + { + "type": "integer", + "description": "Event ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/issues": { "get": { "description": "Admin endpoint to list all reported issues with pagination", @@ -3673,6 +4116,108 @@ } } }, + "/api/v1/odds/pre-match": { + "get": { + "description": "Fetches pre-match odds from EnetPulse for a given event", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EnetPulse - PreMatch" + ], + "summary": "Get pre-match odds for an event", + "parameters": [ + { + "type": "integer", + "description": "Event ID", + "name": "objectFK", + "in": "query", + "required": true + }, + { + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "csv", + "description": "Odds provider IDs (comma separated)", + "name": "oddsProviderFK", + "in": "query" + }, + { + "type": "integer", + "description": "Outcome type ID", + "name": "outcomeTypeFK", + "in": "query" + }, + { + "type": "integer", + "description": "Outcome scope ID", + "name": "outcomeScopeFK", + "in": "query" + }, + { + "type": "integer", + "description": "Outcome subtype ID", + "name": "outcomeSubtypeFK", + "in": "query" + }, + { + "type": "integer", + "description": "Limit results", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset results", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Language type ID", + "name": "languageTypeFK", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.PreMatchOddsResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/odds/upcoming/{upcoming_id}": { "get": { "description": "Retrieve prematch odds by upcoming event ID (FI from Bet365) with optional pagination", @@ -3908,147 +4453,6 @@ } } }, - "/api/v1/referral/settings": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Retrieves current referral settings (admin only)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "referral" - ], - "summary": "Get referral settings", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/domain.ReferralSettings" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - }, - "put": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Updates referral settings (admin only)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "referral" - ], - "summary": "Update referral settings", - "parameters": [ - { - "description": "Referral settings", - "name": "settings", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.ReferralSettings" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/api/v1/referral/stats": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Retrieves referral statistics for the authenticated user", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "referral" - ], - "summary": "Get referral statistics", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/domain.ReferralStats" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/api/v1/report-files/download/{filename}": { "get": { "description": "Downloads a generated report CSV file from the server", @@ -4240,7 +4644,7 @@ } } }, - "/api/v1/result/{id}": { + "/api/v1/result/b365/{id}": { "get": { "description": "Get results for an event", "consumes": [ @@ -5120,6 +5524,92 @@ } } }, + "/api/v1/sport/bet/{id}": { + "get": { + "description": "Gets a single bet by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets bet by id", + "parameters": [ + { + "type": "integer", + "description": "Bet ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BetRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "delete": { + "description": "Deletes bet by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Deletes bet by id", + "parameters": [ + { + "type": "integer", + "description": "Bet ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/super-login": { "post": { "description": "Login super-admin", @@ -5254,6 +5744,161 @@ } } }, + "/api/v1/t-approver": { + "get": { + "description": "Get all Admins", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get all Admins", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.AdminRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/t-approver/{id}": { + "get": { + "description": "Get a single admin by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get admin by id", + "parameters": [ + { + "type": "integer", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.AdminRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "description": "Update Admin", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Update Admin", + "parameters": [ + { + "description": "Update Admin", + "name": "admin", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateAdminReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/telebirr/callback": { "post": { "description": "Processes the Telebirr payment result and updates wallet balance.", @@ -5346,6 +5991,385 @@ } } }, + "/api/v1/tenant": { + "get": { + "description": "Check if phone number or email exist", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Check if phone number or email exist", + "parameters": [ + { + "description": "Check phone number or email exist", + "name": "checkPhoneEmailExist", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CheckPhoneEmailExistReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CheckPhoneEmailExistRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/tenant/{tenant_slug}/customer": { + "get": { + "description": "Get all Customers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "customer" + ], + "summary": "Get all Customers", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CustomersRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/tenant/{tenant_slug}/customer/{id}": { + "get": { + "description": "Get a single customer by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "customer" + ], + "summary": "Get customer by id", + "parameters": [ + { + "type": "integer", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CustomersRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "description": "Update Customers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "customer" + ], + "summary": "Update Customers", + "parameters": [ + { + "description": "Update Customers", + "name": "Customers", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateCustomerReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/tenant/{tenant_slug}/customer/{id}/bets": { + "get": { + "description": "Get tenant customer bets", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "customer" + ], + "summary": "Get tenant customer bets", + "parameters": [ + { + "type": "integer", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CustomersRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/tenant/{tenant_slug}/events/{id}/bets": { + "get": { + "description": "Retrieve bet outcomes by event id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve bet outcomes by event id", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BaseEvent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/tenant/{tenant_slug}/events/{id}/settings": { + "put": { + "description": "Update the event settings", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "event" + ], + "summary": "update the event settings", + "parameters": [ + { + "type": "integer", + "description": "Event ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/tenant/{tenant_slug}/referral/stats": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Retrieves referral statistics for the authenticated user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "referral" + ], + "summary": "Get referral statistics", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ReferralStats" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/transfer/refill/:id": { "post": { "description": "Super Admin route to refill a wallet", @@ -5528,6 +6552,144 @@ } } }, + "/api/v1/user/resetPassword": { + "post": { + "description": "Reset password", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Reset password", + "parameters": [ + { + "description": "Reset password", + "name": "resetPassword", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.ResetPasswordReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/user/search": { + "post": { + "description": "Search for user using name or phone", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Search for user using name or phone", + "parameters": [ + { + "description": "Search for using his name or phone", + "name": "searchUserByNameOrPhone", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.SearchUserByNameOrPhoneReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.UserProfileRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/user/sendResetCode": { + "post": { + "description": "Send reset code", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Send reset code", + "parameters": [ + { + "description": "Send reset code", + "name": "resetCode", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.ResetCodeReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/user/single/{id}": { "get": { "description": "Get a single user by id", @@ -6694,50 +7856,6 @@ } } }, - "/api/v1/{tenant_slug}/events/{id}/settings": { - "put": { - "description": "Update the event settings", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "event" - ], - "summary": "update the event settings", - "parameters": [ - { - "type": "integer", - "description": "Event ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/api/v1/{tenant_slug}/leagues": { "get": { "description": "Gets all leagues", @@ -7752,7 +8870,7 @@ }, "/api/v1/{tenant_slug}/user/resetPassword": { "post": { - "description": "Reset password", + "description": "Reset tenant password", "consumes": [ "application/json" ], @@ -7762,7 +8880,7 @@ "tags": [ "user" ], - "summary": "Reset password", + "summary": "Reset tenant password", "parameters": [ { "description": "Reset password", @@ -8582,6 +9700,40 @@ } } }, + "domain.AtlasGameEntity": { + "type": "object", + "properties": { + "demo_url": { + "description": "✅ new field", + "type": "string" + }, + "deviceType": { + "type": "string" + }, + "game_id": { + "type": "string" + }, + "hasFreeBets": { + "type": "boolean" + }, + "has_demo": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "providerId": { + "type": "string" + }, + "thumbnail_img_url": { + "description": "✅ new field", + "type": "string" + }, + "type": { + "type": "string" + } + } + }, "domain.AtlasGameInitRequest": { "type": "object", "properties": { @@ -8725,7 +9877,7 @@ "type": "string" }, "id": { - "type": "string" + "type": "integer" }, "isLive": { "type": "boolean" @@ -8757,6 +9909,9 @@ "source": { "$ref": "#/definitions/domain.EventSource" }, + "sourceEventID": { + "type": "string" + }, "sportID": { "type": "integer" }, @@ -9339,9 +10494,15 @@ "type": "number", "example": 0.1 }, + "is_active": { + "type": "boolean" + }, "name": { "type": "string", "example": "CompanyName" + }, + "slug": { + "type": "string" } } }, @@ -9583,6 +10744,172 @@ } } }, + "domain.EnetpulseSport": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "description": "DB primary key", + "type": "integer" + }, + "last_updated_at": { + "type": "string" + }, + "name": { + "description": "from API \"name\"", + "type": "string" + }, + "sport_id": { + "description": "from API \"id\"", + "type": "string" + }, + "status": { + "description": "active/inactive", + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "updates_count": { + "description": "from API \"n\"", + "type": "integer" + } + } + }, + "domain.EnetpulseTournament": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "id": { + "description": "internal DB PK", + "type": "integer" + }, + "lastUpdatedAt": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "tournamentID": { + "type": "string" + }, + "tournamentTemplateFK": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "updatesCount": { + "type": "integer" + } + } + }, + "domain.EnetpulseTournamentStage": { + "type": "object", + "properties": { + "country_fk": { + "description": "country FK from API", + "type": "string" + }, + "country_name": { + "description": "country name from API", + "type": "string" + }, + "created_at": { + "type": "string" + }, + "end_date": { + "description": "end date/time", + "type": "string" + }, + "gender": { + "description": "male/female/mixed/unknown", + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_updated_at": { + "description": "ut from API", + "type": "string" + }, + "name": { + "description": "API name", + "type": "string" + }, + "stage_id": { + "description": "API id", + "type": "string" + }, + "start_date": { + "description": "start date/time", + "type": "string" + }, + "status": { + "description": "active/inactive", + "type": "integer" + }, + "tournament_fk": { + "description": "Foreign key to tournament", + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "updates_count": { + "description": "n from API", + "type": "integer" + } + } + }, + "domain.EnetpulseTournamentTemplate": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "gender": { + "description": "male, female, mixed, unknown", + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_updated_at": { + "type": "string" + }, + "name": { + "description": "from API \"name\"", + "type": "string" + }, + "sport_fk": { + "description": "related sport id", + "type": "string" + }, + "status": { + "description": "optional", + "type": "integer" + }, + "template_id": { + "description": "from API \"id\"", + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "updates_count": { + "description": "from API \"n\"", + "type": "integer" + } + } + }, "domain.ErrorResponse": { "type": "object", "properties": { @@ -9659,6 +10986,15 @@ "away_team_image": { "type": "string" }, + "default_is_active": { + "type": "boolean" + }, + "default_is_featured": { + "type": "boolean" + }, + "default_winning_upper_limit": { + "type": "integer" + }, "fetched_at": { "type": "string" }, @@ -9672,7 +11008,7 @@ "type": "string" }, "id": { - "type": "string" + "type": "integer" }, "is_active": { "type": "boolean" @@ -9710,6 +11046,9 @@ "source": { "$ref": "#/definitions/domain.EventSource" }, + "source_event_id": { + "type": "string" + }, "sport_id": { "type": "integer" }, @@ -10336,11 +11675,17 @@ "domain.OddMarketWithEventFilter": { "type": "object", "properties": { + "isLive": { + "$ref": "#/definitions/domain.ValidBool" + }, "limit": { "$ref": "#/definitions/domain.ValidInt32" }, "offset": { "$ref": "#/definitions/domain.ValidInt32" + }, + "status": { + "$ref": "#/definitions/domain.ValidString" } } }, @@ -10509,6 +11854,41 @@ } } }, + "domain.PreMatchOddsResponse": { + "type": "object", + "properties": { + "eventFK": { + "description": "Define fields according to the Enetpulse preodds response structure\nExample:", + "type": "integer" + }, + "odds": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.PreMatchOutcome" + } + } + } + }, + "domain.PreMatchOutcome": { + "type": "object", + "properties": { + "oddsProviderFK": { + "type": "integer" + }, + "oddsValue": { + "type": "number" + }, + "outcomeFK": { + "type": "integer" + }, + "outcomeTypeFK": { + "type": "integer" + }, + "outcomeValue": { + "type": "string" + } + } + }, "domain.ProviderRequest": { "type": "object", "properties": { @@ -10595,55 +11975,14 @@ } } }, - "domain.ReferralSettings": { - "type": "object", - "properties": { - "betReferralBonusPercentage": { - "type": "number" - }, - "cashbackPercentage": { - "type": "number" - }, - "createdAt": { - "type": "string" - }, - "expiresAfterDays": { - "type": "integer" - }, - "id": { - "type": "integer" - }, - "maxReferrals": { - "type": "integer" - }, - "referralRewardAmount": { - "type": "number" - }, - "updatedAt": { - "type": "string" - }, - "updatedBy": { - "type": "string" - }, - "version": { - "type": "integer" - } - } - }, "domain.ReferralStats": { "type": "object", "properties": { - "completedReferrals": { - "type": "integer" - }, - "pendingRewards": { - "type": "number" - }, "totalReferrals": { "type": "integer" }, "totalRewardEarned": { - "type": "number" + "type": "integer" } } }, @@ -10705,14 +12044,16 @@ "admin", "branch_manager", "customer", - "cashier" + "cashier", + "transaction_approver" ], "x-enum-varnames": [ "RoleSuperAdmin", "RoleAdmin", "RoleBranchManager", "RoleCustomer", - "RoleCashier" + "RoleCashier", + "RoleTransactionApprover" ] }, "domain.RollbackRequest": { @@ -10947,6 +12288,10 @@ "type": "string", "example": "2025-04-08T12:00:00Z" }, + "fast_code": { + "type": "string", + "example": "12SD1" + }, "full_name": { "type": "string", "example": "John" @@ -11354,6 +12699,9 @@ "category": { "type": "string" }, + "demoUrl": { + "type": "string" + }, "deviceType": { "type": "string" }, @@ -11407,6 +12755,9 @@ "name": { "type": "string", "example": "CompanyName" + }, + "slug": { + "type": "string" } } }, @@ -11419,6 +12770,17 @@ } } }, + "domain.ValidBool": { + "type": "object", + "properties": { + "valid": { + "type": "boolean" + }, + "value": { + "type": "boolean" + } + } + }, "domain.ValidInt": { "type": "object", "properties": { @@ -11740,6 +13102,35 @@ } } }, + "handlers.CreateTransactionApproverReq": { + "type": "object", + "properties": { + "company_id": { + "type": "integer", + "example": 1 + }, + "email": { + "type": "string", + "example": "john.doe@example.com" + }, + "first_name": { + "type": "string", + "example": "John" + }, + "last_name": { + "type": "string", + "example": "Doe" + }, + "password": { + "type": "string", + "example": "password123" + }, + "phone_number": { + "type": "string", + "example": "1234567890" + } + } + }, "handlers.CreateTransferReq": { "type": "object", "properties": { @@ -11861,6 +13252,12 @@ "handlers.CustomersRes": { "type": "object", "properties": { + "company_id": { + "type": "integer" + }, + "company_name": { + "type": "string" + }, "created_at": { "type": "string" }, @@ -12042,7 +13439,7 @@ "type": "string", "example": "1234567890" }, - "referal_code": { + "referral_code": { "type": "string", "example": "ABC123" } @@ -12505,10 +13902,6 @@ "handlers.updateCustomerReq": { "type": "object", "properties": { - "company_id": { - "type": "integer", - "example": 1 - }, "first_name": { "type": "string", "example": "John" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index b54bc7b..9a56b7a 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -92,6 +92,29 @@ definitions: player_id: type: string type: object + domain.AtlasGameEntity: + properties: + demo_url: + description: ✅ new field + type: string + deviceType: + type: string + game_id: + type: string + has_demo: + type: boolean + hasFreeBets: + type: boolean + name: + type: string + providerId: + type: string + thumbnail_img_url: + description: ✅ new field + type: string + type: + type: string + type: object domain.AtlasGameInitRequest: properties: currency: @@ -187,7 +210,7 @@ definitions: homeTeamImage: type: string id: - type: string + type: integer isLive: type: boolean isMonitored: @@ -208,6 +231,8 @@ definitions: $ref: '#/definitions/domain.ValidString' source: $ref: '#/definitions/domain.EventSource' + sourceEventID: + type: string sportID: type: integer startTime: @@ -613,9 +638,13 @@ definitions: deducted_percentage: example: 0.1 type: number + is_active: + type: boolean name: example: CompanyName type: string + slug: + type: string type: object domain.CreateSupportedOperationReq: properties: @@ -777,6 +806,123 @@ definitions: - customer_id - sender_account type: object + domain.EnetpulseSport: + properties: + created_at: + type: string + id: + description: DB primary key + type: integer + last_updated_at: + type: string + name: + description: from API "name" + type: string + sport_id: + description: from API "id" + type: string + status: + description: active/inactive + type: integer + updated_at: + type: string + updates_count: + description: from API "n" + type: integer + type: object + domain.EnetpulseTournament: + properties: + createdAt: + type: string + id: + description: internal DB PK + type: integer + lastUpdatedAt: + type: string + name: + type: string + status: + type: integer + tournamentID: + type: string + tournamentTemplateFK: + type: string + updatedAt: + type: string + updatesCount: + type: integer + type: object + domain.EnetpulseTournamentStage: + properties: + country_fk: + description: country FK from API + type: string + country_name: + description: country name from API + type: string + created_at: + type: string + end_date: + description: end date/time + type: string + gender: + description: male/female/mixed/unknown + type: string + id: + type: integer + last_updated_at: + description: ut from API + type: string + name: + description: API name + type: string + stage_id: + description: API id + type: string + start_date: + description: start date/time + type: string + status: + description: active/inactive + type: integer + tournament_fk: + description: Foreign key to tournament + type: string + updated_at: + type: string + updates_count: + description: n from API + type: integer + type: object + domain.EnetpulseTournamentTemplate: + properties: + created_at: + type: string + gender: + description: male, female, mixed, unknown + type: string + id: + type: integer + last_updated_at: + type: string + name: + description: from API "name" + type: string + sport_fk: + description: related sport id + type: string + status: + description: optional + type: integer + template_id: + description: from API "id" + type: string + updated_at: + type: string + updates_count: + description: from API "n" + type: integer + type: object domain.ErrorResponse: properties: error: @@ -838,6 +984,12 @@ definitions: type: integer away_team_image: type: string + default_is_active: + type: boolean + default_is_featured: + type: boolean + default_winning_upper_limit: + type: integer fetched_at: type: string home_team: @@ -847,7 +999,7 @@ definitions: home_team_image: type: string id: - type: string + type: integer is_active: type: boolean is_featured: @@ -872,6 +1024,8 @@ definitions: type: string source: $ref: '#/definitions/domain.EventSource' + source_event_id: + type: string sport_id: type: integer start_time: @@ -1289,10 +1443,14 @@ definitions: type: object domain.OddMarketWithEventFilter: properties: + isLive: + $ref: '#/definitions/domain.ValidBool' limit: $ref: '#/definitions/domain.ValidInt32' offset: $ref: '#/definitions/domain.ValidInt32' + status: + $ref: '#/definitions/domain.ValidString' type: object domain.OutcomeStatus: enum: @@ -1411,6 +1569,31 @@ definitions: thumbnail: type: string type: object + domain.PreMatchOddsResponse: + properties: + eventFK: + description: |- + Define fields according to the Enetpulse preodds response structure + Example: + type: integer + odds: + items: + $ref: '#/definitions/domain.PreMatchOutcome' + type: array + type: object + domain.PreMatchOutcome: + properties: + oddsProviderFK: + type: integer + oddsValue: + type: number + outcomeFK: + type: integer + outcomeTypeFK: + type: integer + outcomeValue: + type: string + type: object domain.ProviderRequest: properties: brandId: @@ -1468,39 +1651,12 @@ definitions: type: object type: array type: object - domain.ReferralSettings: - properties: - betReferralBonusPercentage: - type: number - cashbackPercentage: - type: number - createdAt: - type: string - expiresAfterDays: - type: integer - id: - type: integer - maxReferrals: - type: integer - referralRewardAmount: - type: number - updatedAt: - type: string - updatedBy: - type: string - version: - type: integer - type: object domain.ReferralStats: properties: - completedReferrals: - type: integer - pendingRewards: - type: number totalReferrals: type: integer totalRewardEarned: - type: number + type: integer type: object domain.ReportedIssue: properties: @@ -1543,6 +1699,7 @@ definitions: - branch_manager - customer - cashier + - transaction_approver type: string x-enum-varnames: - RoleSuperAdmin @@ -1550,6 +1707,7 @@ definitions: - RoleBranchManager - RoleCustomer - RoleCashier + - RoleTransactionApprover domain.RollbackRequest: properties: bet_transaction_id: @@ -1706,6 +1864,9 @@ definitions: created_at: example: "2025-04-08T12:00:00Z" type: string + fast_code: + example: 12SD1 + type: string full_name: example: John type: string @@ -1992,6 +2153,8 @@ definitions: type: array category: type: string + demoUrl: + type: string deviceType: type: string gameId: @@ -2029,6 +2192,8 @@ definitions: name: example: CompanyName type: string + slug: + type: string type: object domain.UpdateTransactionVerifiedReq: properties: @@ -2036,6 +2201,13 @@ definitions: example: true type: boolean type: object + domain.ValidBool: + properties: + valid: + type: boolean + value: + type: boolean + type: object domain.ValidInt: properties: valid: @@ -2253,6 +2425,27 @@ definitions: example: "1234567890" type: string type: object + handlers.CreateTransactionApproverReq: + properties: + company_id: + example: 1 + type: integer + email: + example: john.doe@example.com + type: string + first_name: + example: John + type: string + last_name: + example: Doe + type: string + password: + example: password123 + type: string + phone_number: + example: "1234567890" + type: string + type: object handlers.CreateTransferReq: properties: amount: @@ -2337,6 +2530,10 @@ definitions: type: object handlers.CustomersRes: properties: + company_id: + type: integer + company_name: + type: string created_at: type: string email: @@ -2459,7 +2656,7 @@ definitions: phone_number: example: "1234567890" type: string - referal_code: + referral_code: example: ABC123 type: string type: object @@ -2774,9 +2971,6 @@ definitions: type: object handlers.updateCustomerReq: properties: - company_id: - example: 1 - type: integer first_name: example: John type: string @@ -2986,35 +3180,6 @@ paths: summary: Retrieve all upcoming events with settings tags: - prematch - /api/v1/{tenant_slug}/events/{id}/settings: - put: - consumes: - - application/json - description: Update the event settings - parameters: - - description: Event ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/response.APIResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: update the event settings - tags: - - event /api/v1/{tenant_slug}/leagues: get: consumes: @@ -3684,7 +3849,7 @@ paths: post: consumes: - application/json - description: Reset password + description: Reset tenant password parameters: - description: Reset password in: body @@ -3707,7 +3872,7 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/response.APIResponse' - summary: Reset password + summary: Reset tenant password tags: - user /api/v1/{tenant_slug}/user/search: @@ -3899,14 +4064,14 @@ paths: post: consumes: - application/json - description: Create Admin + description: Create transaction approver parameters: - - description: Create admin + - description: Create transaction approver in: body name: manger required: true schema: - $ref: '#/definitions/handlers.CreateAdminReq' + $ref: '#/definitions/handlers.CreateTransactionApproverReq' produces: - application/json responses: @@ -3926,7 +4091,7 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/response.APIResponse' - summary: Create Admin + summary: Create transaction approver tags: - admin /api/v1/admin-company: @@ -4339,6 +4504,30 @@ paths: summary: Create free spins for a player tags: - Virtual Games - Atlas + /api/v1/atlas/games: + get: + description: Retrieves available Atlas virtual games from the provider + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + items: + $ref: '#/definitions/domain.AtlasGameEntity' + type: array + type: object + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: List Atlas virtual games + tags: + - Virtual Games - Atlas /api/v1/atlas/init-game: post: consumes: @@ -4873,6 +5062,35 @@ paths: summary: Delete the branch operation tags: - branch + /api/v1/branch/{id}/return: + post: + consumes: + - application/json + description: Unassign the branch wallet to company + parameters: + - description: Branch ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.BranchDetailRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Unassign the branch wallet to company + tags: + - branch /api/v1/branchCashier: get: consumes: @@ -5616,6 +5834,39 @@ paths: summary: Update Customers tags: - customer + /api/v1/customer/{id}/bets: + get: + consumes: + - application/json + description: Get customer bets + parameters: + - description: User ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.CustomersRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Get customer bets + tags: + - customer /api/v1/customerWallet: get: consumes: @@ -5722,6 +5973,110 @@ paths: summary: Verify a direct deposit tags: - Direct Deposits + /api/v1/enetpulse/sports: + get: + consumes: + - application/json + description: Fetches all sports stored in the database + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + items: + $ref: '#/definitions/domain.EnetpulseSport' + type: array + type: object + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get all sports + tags: + - EnetPulse - Sports + /api/v1/enetpulse/tournament-stages: + get: + consumes: + - application/json + description: Fetches all tournament stages stored in the database + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + items: + $ref: '#/definitions/domain.EnetpulseTournamentStage' + type: array + type: object + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get all tournament stages + tags: + - EnetPulse - Tournament Stages + /api/v1/enetpulse/tournament-templates: + get: + consumes: + - application/json + description: Fetches all tournament templates stored in the database + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + items: + $ref: '#/definitions/domain.EnetpulseTournamentTemplate' + type: array + type: object + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get all tournament templates + tags: + - EnetPulse - Tournament Templates + /api/v1/enetpulse/tournaments: + get: + consumes: + - application/json + description: Fetches all tournaments stored in the database + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + items: + $ref: '#/definitions/domain.EnetpulseTournament' + type: array + type: object + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get all tournaments + tags: + - EnetPulse - Tournaments /api/v1/events: get: consumes: @@ -5829,6 +6184,93 @@ paths: summary: Retrieve an upcoming by ID tags: - prematch + /api/v1/events/{id}/bets: + get: + consumes: + - application/json + description: Retrieve bet outcomes by event id + parameters: + - description: ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.BaseEvent' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Retrieve bet outcomes by event id + tags: + - prematch + /api/v1/events/{id}/is_monitored: + patch: + consumes: + - application/json + description: Update the event is_monitored + parameters: + - description: Event ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: update the event is_monitored + tags: + - event + /api/v1/events/{id}/settings: + put: + consumes: + - application/json + description: Update the event settings + parameters: + - description: Event ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: update the event settings + tags: + - event /api/v1/issues: get: description: Admin endpoint to list all reported issues with pagination @@ -6231,6 +6673,71 @@ paths: summary: Retrieve all odds tags: - prematch + /api/v1/odds/pre-match: + get: + consumes: + - application/json + description: Fetches pre-match odds from EnetPulse for a given event + parameters: + - description: Event ID + in: query + name: objectFK + required: true + type: integer + - collectionFormat: csv + description: Odds provider IDs (comma separated) + in: query + items: + type: integer + name: oddsProviderFK + type: array + - description: Outcome type ID + in: query + name: outcomeTypeFK + type: integer + - description: Outcome scope ID + in: query + name: outcomeScopeFK + type: integer + - description: Outcome subtype ID + in: query + name: outcomeSubtypeFK + type: integer + - description: Limit results + in: query + name: limit + type: integer + - description: Offset results + in: query + name: offset + type: integer + - description: Language type ID + in: query + name: languageTypeFK + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + $ref: '#/definitions/domain.PreMatchOddsResponse' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get pre-match odds for an event + tags: + - EnetPulse - PreMatch /api/v1/odds/upcoming/{upcoming_id}: get: consumes: @@ -6385,95 +6892,6 @@ paths: summary: List all virtual games tags: - VirtualGames - Orchestration - /api/v1/referral/settings: - get: - consumes: - - application/json - description: Retrieves current referral settings (admin only) - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/domain.ReferralSettings' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/response.APIResponse' - "403": - description: Forbidden - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - Bearer: [] - summary: Get referral settings - tags: - - referral - put: - consumes: - - application/json - description: Updates referral settings (admin only) - parameters: - - description: Referral settings - in: body - name: settings - required: true - schema: - $ref: '#/definitions/domain.ReferralSettings' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/response.APIResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/response.APIResponse' - "403": - description: Forbidden - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - Bearer: [] - summary: Update referral settings - tags: - - referral - /api/v1/referral/stats: - get: - consumes: - - application/json - description: Retrieves referral statistics for the authenticated user - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/domain.ReferralStats' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - Bearer: [] - summary: Get referral statistics - tags: - - referral /api/v1/report-files/download/{filename}: get: description: Downloads a generated report CSV file from the server @@ -6600,7 +7018,7 @@ paths: summary: Get dashboard report tags: - Reports - /api/v1/result/{id}: + /api/v1/result/b365/{id}: get: consumes: - application/json @@ -7181,6 +7599,63 @@ paths: summary: Gets shop bet by transaction id tags: - transaction + /api/v1/sport/bet/{id}: + delete: + consumes: + - application/json + description: Deletes bet by id + parameters: + - description: Bet ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Deletes bet by id + tags: + - bet + get: + consumes: + - application/json + description: Gets a single bet by id + parameters: + - description: Bet ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.BetRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets bet by id + tags: + - bet /api/v1/super-login: post: consumes: @@ -7269,6 +7744,108 @@ paths: summary: Create a supported operation tags: - branch + /api/v1/t-approver: + get: + consumes: + - application/json + description: Get all Admins + parameters: + - description: Page number + in: query + name: page + type: integer + - description: Page size + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.AdminRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Get all Admins + tags: + - admin + /api/v1/t-approver/{id}: + get: + consumes: + - application/json + description: Get a single admin by id + parameters: + - description: User ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.AdminRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Get admin by id + tags: + - admin + put: + consumes: + - application/json + description: Update Admin + parameters: + - description: Update Admin + in: body + name: admin + required: true + schema: + $ref: '#/definitions/handlers.updateAdminReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Update Admin + tags: + - admin /api/v1/telebirr/callback: post: consumes: @@ -7329,6 +7906,254 @@ paths: summary: Create Telebirr Payment Session tags: - Telebirr + /api/v1/tenant: + get: + consumes: + - application/json + description: Check if phone number or email exist + parameters: + - description: Check phone number or email exist + in: body + name: checkPhoneEmailExist + required: true + schema: + $ref: '#/definitions/handlers.CheckPhoneEmailExistReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.CheckPhoneEmailExistRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Check if phone number or email exist + tags: + - user + /api/v1/tenant/{tenant_slug}/customer: + get: + consumes: + - application/json + description: Get all Customers + parameters: + - description: Page number + in: query + name: page + type: integer + - description: Page size + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.CustomersRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Get all Customers + tags: + - customer + /api/v1/tenant/{tenant_slug}/customer/{id}: + get: + consumes: + - application/json + description: Get a single customer by id + parameters: + - description: User ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.CustomersRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Get customer by id + tags: + - customer + put: + consumes: + - application/json + description: Update Customers + parameters: + - description: Update Customers + in: body + name: Customers + required: true + schema: + $ref: '#/definitions/handlers.updateCustomerReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Update Customers + tags: + - customer + /api/v1/tenant/{tenant_slug}/customer/{id}/bets: + get: + consumes: + - application/json + description: Get tenant customer bets + parameters: + - description: User ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.CustomersRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Get tenant customer bets + tags: + - customer + /api/v1/tenant/{tenant_slug}/events/{id}/bets: + get: + consumes: + - application/json + description: Retrieve bet outcomes by event id + parameters: + - description: ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.BaseEvent' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Retrieve bet outcomes by event id + tags: + - prematch + /api/v1/tenant/{tenant_slug}/events/{id}/settings: + put: + consumes: + - application/json + description: Update the event settings + parameters: + - description: Event ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: update the event settings + tags: + - event + /api/v1/tenant/{tenant_slug}/referral/stats: + get: + consumes: + - application/json + description: Retrieves referral statistics for the authenticated user + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.ReferralStats' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - Bearer: [] + summary: Get referral statistics + tags: + - referral /api/v1/transfer/refill/:id: post: consumes: @@ -7448,6 +8273,96 @@ paths: summary: Delete user by ID tags: - user + /api/v1/user/resetPassword: + post: + consumes: + - application/json + description: Reset password + parameters: + - description: Reset password + in: body + name: resetPassword + required: true + schema: + $ref: '#/definitions/handlers.ResetPasswordReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Reset password + tags: + - user + /api/v1/user/search: + post: + consumes: + - application/json + description: Search for user using name or phone + parameters: + - description: Search for using his name or phone + in: body + name: searchUserByNameOrPhone + required: true + schema: + $ref: '#/definitions/handlers.SearchUserByNameOrPhoneReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.UserProfileRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Search for user using name or phone + tags: + - user + /api/v1/user/sendResetCode: + post: + consumes: + - application/json + description: Send reset code + parameters: + - description: Send reset code + in: body + name: resetCode + required: true + schema: + $ref: '#/definitions/handlers.ResetCodeReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Send reset code + tags: + - user /api/v1/user/single/{id}: get: consumes: diff --git a/gen/db/company.sql.go b/gen/db/company.sql.go index ba728e7..32d9ee2 100644 --- a/gen/db/company.sql.go +++ b/gen/db/company.sql.go @@ -17,9 +17,10 @@ INSERT INTO companies ( slug, admin_id, wallet_id, - deducted_percentage + deducted_percentage, + is_active ) -VALUES ($1, $2, $3, $4, $5) +VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, name, slug, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at ` @@ -29,6 +30,7 @@ type CreateCompanyParams struct { AdminID int64 `json:"admin_id"` WalletID int64 `json:"wallet_id"` DeductedPercentage float32 `json:"deducted_percentage"` + IsActive bool `json:"is_active"` } func (q *Queries) CreateCompany(ctx context.Context, arg CreateCompanyParams) (Company, error) { @@ -38,6 +40,7 @@ func (q *Queries) CreateCompany(ctx context.Context, arg CreateCompanyParams) (C arg.AdminID, arg.WalletID, arg.DeductedPercentage, + arg.IsActive, ) var i Company err := row.Scan( @@ -217,7 +220,7 @@ func (q *Queries) SearchCompanyByName(ctx context.Context, dollar_1 pgtype.Text) return items, nil } -const UpdateCompany = `-- name: UpdateCompany :one +const UpdateCompany = `-- name: UpdateCompany :exec UPDATE companies SET name = COALESCE($2, name), admin_id = COALESCE($3, admin_id), @@ -226,9 +229,9 @@ SET name = COALESCE($2, name), $5, deducted_percentage ), + slug = COALESCE($6, slug), updated_at = CURRENT_TIMESTAMP WHERE id = $1 -RETURNING id, name, slug, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at ` type UpdateCompanyParams struct { @@ -237,27 +240,17 @@ type UpdateCompanyParams struct { AdminID pgtype.Int8 `json:"admin_id"` IsActive pgtype.Bool `json:"is_active"` DeductedPercentage pgtype.Float4 `json:"deducted_percentage"` + Slug pgtype.Text `json:"slug"` } -func (q *Queries) UpdateCompany(ctx context.Context, arg UpdateCompanyParams) (Company, error) { - row := q.db.QueryRow(ctx, UpdateCompany, +func (q *Queries) UpdateCompany(ctx context.Context, arg UpdateCompanyParams) error { + _, err := q.db.Exec(ctx, UpdateCompany, arg.ID, arg.Name, arg.AdminID, arg.IsActive, arg.DeductedPercentage, + arg.Slug, ) - var i Company - err := row.Scan( - &i.ID, - &i.Name, - &i.Slug, - &i.AdminID, - &i.WalletID, - &i.DeductedPercentage, - &i.IsActive, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err + return err } diff --git a/gen/db/leagues.sql.go b/gen/db/leagues.sql.go index 912e257..1413699 100644 --- a/gen/db/leagues.sql.go +++ b/gen/db/leagues.sql.go @@ -48,14 +48,19 @@ WHERE ( name ILIKE '%' || $3 || '%' OR $3 IS NULL ) + AND ( + default_is_active = $4 + OR $4 IS NULL + ) ORDER BY name ASC -LIMIT $5 OFFSET $4 +LIMIT $6 OFFSET $5 ` type GetAllLeaguesParams struct { CountryCode pgtype.Text `json:"country_code"` SportID pgtype.Int4 `json:"sport_id"` Query pgtype.Text `json:"query"` + IsActive pgtype.Bool `json:"is_active"` Offset pgtype.Int4 `json:"offset"` Limit pgtype.Int4 `json:"limit"` } @@ -65,6 +70,7 @@ func (q *Queries) GetAllLeagues(ctx context.Context, arg GetAllLeaguesParams) ([ arg.CountryCode, arg.SportID, arg.Query, + arg.IsActive, arg.Offset, arg.Limit, ) @@ -199,6 +205,46 @@ func (q *Queries) GetAllLeaguesWithSettings(ctx context.Context, arg GetAllLeagu return items, nil } +const GetTotalLeagues = `-- name: GetTotalLeagues :one +SELECT COUNT(*) +FROM leagues +WHERE ( + country_code = $1 + OR $1 IS NULL + ) + AND ( + sport_id = $2 + OR $2 IS NULL + ) + AND ( + name ILIKE '%' || $3 || '%' + OR $3 IS NULL + ) + AND ( + default_is_active = $4 + OR $4 IS NULL + ) +` + +type GetTotalLeaguesParams struct { + CountryCode pgtype.Text `json:"country_code"` + SportID pgtype.Int4 `json:"sport_id"` + Query pgtype.Text `json:"query"` + IsActive pgtype.Bool `json:"is_active"` +} + +func (q *Queries) GetTotalLeagues(ctx context.Context, arg GetTotalLeaguesParams) (int64, error) { + row := q.db.QueryRow(ctx, GetTotalLeagues, + arg.CountryCode, + arg.SportID, + arg.Query, + arg.IsActive, + ) + var count int64 + err := row.Scan(&count) + return count, err +} + const GetTotalLeaguesWithSettings = `-- name: GetTotalLeaguesWithSettings :one SELECT COUNT(*) FROM leagues l diff --git a/gen/db/models.go b/gen/db/models.go index 339efb8..d91297f 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -687,6 +687,7 @@ type ShopBetDetail struct { TransactionVerified bool `json:"transaction_verified"` Status int32 `json:"status"` TotalOdds float32 `json:"total_odds"` + FastCode string `json:"fast_code"` Outcomes []BetOutcome `json:"outcomes"` } diff --git a/gen/db/shop_transactions.sql.go b/gen/db/shop_transactions.sql.go index bcd884e..5c4b52e 100644 --- a/gen/db/shop_transactions.sql.go +++ b/gen/db/shop_transactions.sql.go @@ -173,7 +173,7 @@ func (q *Queries) CreateShopTransaction(ctx context.Context, arg CreateShopTrans } const GetAllShopBets = `-- name: GetAllShopBets :many -SELECT id, shop_transaction_id, cashout_id, cashed_out_by, bet_id, number_of_outcomes, cashed_out, created_at, updated_at, customer_full_name, customer_phone_number, branch_id, company_id, amount, transaction_verified, status, total_odds, outcomes +SELECT id, shop_transaction_id, cashout_id, cashed_out_by, bet_id, number_of_outcomes, cashed_out, created_at, updated_at, customer_full_name, customer_phone_number, branch_id, company_id, amount, transaction_verified, status, total_odds, fast_code, outcomes FROM shop_bet_detail WHERE ( full_name ILIKE '%' || $1 || '%' @@ -239,6 +239,7 @@ func (q *Queries) GetAllShopBets(ctx context.Context, arg GetAllShopBetsParams) &i.TransactionVerified, &i.Status, &i.TotalOdds, + &i.FastCode, &i.Outcomes, ); err != nil { return nil, err @@ -419,7 +420,7 @@ func (q *Queries) GetAllShopTransactions(ctx context.Context, arg GetAllShopTran } const GetShopBetByBetID = `-- name: GetShopBetByBetID :one -SELECT id, shop_transaction_id, cashout_id, cashed_out_by, bet_id, number_of_outcomes, cashed_out, created_at, updated_at, customer_full_name, customer_phone_number, branch_id, company_id, amount, transaction_verified, status, total_odds, outcomes +SELECT id, shop_transaction_id, cashout_id, cashed_out_by, bet_id, number_of_outcomes, cashed_out, created_at, updated_at, customer_full_name, customer_phone_number, branch_id, company_id, amount, transaction_verified, status, total_odds, fast_code, outcomes FROM shop_bet_detail WHERE bet_id = $1 ` @@ -445,13 +446,14 @@ func (q *Queries) GetShopBetByBetID(ctx context.Context, betID int64) (ShopBetDe &i.TransactionVerified, &i.Status, &i.TotalOdds, + &i.FastCode, &i.Outcomes, ) return i, err } const GetShopBetByCashoutID = `-- name: GetShopBetByCashoutID :one -SELECT id, shop_transaction_id, cashout_id, cashed_out_by, bet_id, number_of_outcomes, cashed_out, created_at, updated_at, customer_full_name, customer_phone_number, branch_id, company_id, amount, transaction_verified, status, total_odds, outcomes +SELECT id, shop_transaction_id, cashout_id, cashed_out_by, bet_id, number_of_outcomes, cashed_out, created_at, updated_at, customer_full_name, customer_phone_number, branch_id, company_id, amount, transaction_verified, status, total_odds, fast_code, outcomes FROM shop_bet_detail WHERE cashout_id = $1 ` @@ -477,13 +479,14 @@ func (q *Queries) GetShopBetByCashoutID(ctx context.Context, cashoutID string) ( &i.TransactionVerified, &i.Status, &i.TotalOdds, + &i.FastCode, &i.Outcomes, ) return i, err } const GetShopBetByID = `-- name: GetShopBetByID :one -SELECT id, shop_transaction_id, cashout_id, cashed_out_by, bet_id, number_of_outcomes, cashed_out, created_at, updated_at, customer_full_name, customer_phone_number, branch_id, company_id, amount, transaction_verified, status, total_odds, outcomes +SELECT id, shop_transaction_id, cashout_id, cashed_out_by, bet_id, number_of_outcomes, cashed_out, created_at, updated_at, customer_full_name, customer_phone_number, branch_id, company_id, amount, transaction_verified, status, total_odds, fast_code, outcomes FROM shop_bet_detail WHERE id = $1 ` @@ -509,13 +512,14 @@ func (q *Queries) GetShopBetByID(ctx context.Context, id int64) (ShopBetDetail, &i.TransactionVerified, &i.Status, &i.TotalOdds, + &i.FastCode, &i.Outcomes, ) return i, err } const GetShopBetByShopTransactionID = `-- name: GetShopBetByShopTransactionID :one -SELECT id, shop_transaction_id, cashout_id, cashed_out_by, bet_id, number_of_outcomes, cashed_out, created_at, updated_at, customer_full_name, customer_phone_number, branch_id, company_id, amount, transaction_verified, status, total_odds, outcomes +SELECT id, shop_transaction_id, cashout_id, cashed_out_by, bet_id, number_of_outcomes, cashed_out, created_at, updated_at, customer_full_name, customer_phone_number, branch_id, company_id, amount, transaction_verified, status, total_odds, fast_code, outcomes FROM shop_bet_detail WHERE shop_transaction_id = $1 ` @@ -541,6 +545,7 @@ func (q *Queries) GetShopBetByShopTransactionID(ctx context.Context, shopTransac &i.TransactionVerified, &i.Status, &i.TotalOdds, + &i.FastCode, &i.Outcomes, ) return i, err diff --git a/internal/domain/company.go b/internal/domain/company.go index 0d7eccd..f4ee64a 100644 --- a/internal/domain/company.go +++ b/internal/domain/company.go @@ -45,6 +45,8 @@ type CreateCompany struct { AdminID int64 WalletID int64 DeductedPercentage float32 + Slug string + IsActive bool } type UpdateCompany struct { @@ -53,18 +55,22 @@ type UpdateCompany struct { AdminID ValidInt64 IsActive ValidBool DeductedPercentage ValidFloat32 + Slug ValidString } type CreateCompanyReq struct { Name string `json:"name" example:"CompanyName"` AdminID int64 `json:"admin_id" example:"1"` DeductedPercentage float32 `json:"deducted_percentage" example:"0.1" validate:"lt=1"` + Slug string `json:"slug"` + IsActive bool `json:"is_active"` } type UpdateCompanyReq struct { Name *string `json:"name,omitempty" example:"CompanyName"` AdminID *int64 `json:"admin_id,omitempty" example:"1"` IsActive *bool `json:"is_active,omitempty" example:"true"` DeductedPercentage *float32 `json:"deducted_percentage,omitempty" example:"0.1" validate:"lt=1"` + Slug *string `json:"slug"` } type CompanyRes struct { @@ -113,13 +119,14 @@ func ConvertGetCompany(company GetCompany) GetCompanyRes { } } -func ConvertCreateCompany(company CreateCompany, uniqueSlug string) dbgen.CreateCompanyParams { +func ConvertCreateCompany(company CreateCompany) dbgen.CreateCompanyParams { return dbgen.CreateCompanyParams{ Name: company.Name, - Slug: uniqueSlug, + Slug: company.Slug, AdminID: company.AdminID, WalletID: company.WalletID, DeductedPercentage: company.DeductedPercentage, + IsActive: company.IsActive, } } @@ -154,31 +161,22 @@ func ConvertDBCompanyDetails(dbCompany dbgen.CompaniesDetail) GetCompany { func ConvertUpdateCompany(updateCompany UpdateCompany) dbgen.UpdateCompanyParams { newUpdateCompany := dbgen.UpdateCompanyParams{ - ID: updateCompany.ID, - Name: pgtype.Text{ - String: updateCompany.Name.Value, - Valid: updateCompany.Name.Valid, - }, - AdminID: pgtype.Int8{ - Int64: updateCompany.AdminID.Value, - Valid: updateCompany.AdminID.Valid, - }, - IsActive: pgtype.Bool{ - Bool: updateCompany.IsActive.Value, - Valid: updateCompany.IsActive.Valid, - }, - DeductedPercentage: pgtype.Float4{ - Float32: updateCompany.DeductedPercentage.Value, - Valid: updateCompany.DeductedPercentage.Valid, - }, + ID: updateCompany.ID, + Name: updateCompany.Name.ToPG(), + AdminID: updateCompany.AdminID.ToPG(), + IsActive: updateCompany.IsActive.ToPG(), + DeductedPercentage: updateCompany.DeductedPercentage.ToPG(), + Slug: updateCompany.Slug.ToPG(), } return newUpdateCompany } -func ConvertUpdateCompanyReq(req UpdateCompanyReq) UpdateCompany { +func ConvertUpdateCompanyReq(req UpdateCompanyReq, companyID int64) UpdateCompany { var updateCompany UpdateCompany + updateCompany.ID = companyID + if req.Name != nil { updateCompany.Name = ValidString{ Value: *req.Name, @@ -206,6 +204,12 @@ func ConvertUpdateCompanyReq(req UpdateCompanyReq) UpdateCompany { Valid: true, } } + if req.Slug != nil { + updateCompany.Slug = ValidString{ + Value: *req.Slug, + Valid: true, + } + } return updateCompany } diff --git a/internal/domain/shop_bet.go b/internal/domain/shop_bet.go index 72f280b..b46b4b6 100644 --- a/internal/domain/shop_bet.go +++ b/internal/domain/shop_bet.go @@ -34,6 +34,7 @@ type ShopBetDetail struct { CompanyID int64 FullName string PhoneNumber string + FastCode string CashoutID string CashedOut bool BetID int64 @@ -80,6 +81,7 @@ type ShopBetRes struct { CompanyID int64 `json:"company_id" example:"2"` FullName string `json:"full_name" example:"John"` PhoneNumber string `json:"phone_number" example:"1234567890"` + FastCode string `json:"fast_code" example:"12SD1"` CashoutID string `json:"cashout_id" example:"21234"` CashedOut bool `json:"cashed_out" example:"false"` BetID int64 `json:"bet_id" example:"1"` @@ -111,6 +113,7 @@ func ConvertShopBetDetail(shopBet ShopBetDetail) ShopBetRes { CompanyID: shopBet.CompanyID, FullName: shopBet.FullName, PhoneNumber: shopBet.PhoneNumber, + FastCode: shopBet.FastCode, CashoutID: shopBet.CashoutID, CashedOut: shopBet.CashedOut, BetID: shopBet.BetID, diff --git a/internal/domain/wallet.go b/internal/domain/wallet.go index 947f3c8..17d69d9 100644 --- a/internal/domain/wallet.go +++ b/internal/domain/wallet.go @@ -1,6 +1,13 @@ package domain -import "time" +import ( + "errors" + "time" +) + +var ( + ErrWalletIDDuplicate = errors.New("there already exists user id with wallet_type") +) type Wallet struct { ID int64 diff --git a/internal/repository/bet.go b/internal/repository/bet.go index 9a6ad8f..8de6a2a 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -103,8 +103,11 @@ func (s *Store) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]doma Query: filter.Query.ToPG(), CreatedBefore: filter.CreatedBefore.ToPG(), CreatedAfter: filter.CreatedAfter.ToPG(), - Offset: filter.Offset.ToPG(), - Limit: filter.Limit.ToPG(), + Offset: pgtype.Int4{ + Int32: int32(filter.Offset.Value * filter.Limit.Value), + Valid: filter.Offset.Valid, + }, + Limit: filter.Limit.ToPG(), }) if err != nil { domain.MongoDBLogger.Error("failed to get all bets", diff --git a/internal/repository/common.go b/internal/repository/common.go new file mode 100644 index 0000000..5cf051e --- /dev/null +++ b/internal/repository/common.go @@ -0,0 +1,12 @@ +package repository + +import ( + "errors" + + "github.com/jackc/pgx/v5/pgconn" +) + +func IsUniqueViolation(err error) bool { + var pgErr *pgconn.PgError + return errors.As(err, &pgErr) && pgErr.Code == "23505" +} \ No newline at end of file diff --git a/internal/repository/company.go b/internal/repository/company.go index 1e290c9..a72310b 100644 --- a/internal/repository/company.go +++ b/internal/repository/company.go @@ -2,36 +2,34 @@ package repository import ( "context" - "errors" "fmt" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - "github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers" - "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) func (s *Store) CreateCompany(ctx context.Context, company domain.CreateCompany) (domain.Company, error) { - baseSlug := helpers.GenerateSlug(company.Name) - uniqueSlug := baseSlug - i := 1 + // baseSlug := helpers.GenerateSlug(company.Name) + // uniqueSlug := baseSlug + // i := 1 - for { - _, err := s.queries.GetCompanyUsingSlug(ctx, uniqueSlug) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - // slug is unique - break - } else { - // real DB error - return domain.Company{}, err - } - } - uniqueSlug = fmt.Sprintf("%s-%d", baseSlug, i) - i++ - } + // for { + // _, err := s.queries.GetCompanyUsingSlug(ctx, uniqueSlug) + // if err != nil { + // if errors.Is(err, pgx.ErrNoRows) { + // // slug is unique + // break + // } else { + // // real DB error + // return domain.Company{}, err + // } + // } + // uniqueSlug = fmt.Sprintf("%s-%d", baseSlug, i) + // i++ + // } + fmt.Printf("\ncompany %v\n\n", company) + dbCompany, err := s.queries.CreateCompany(ctx, domain.ConvertCreateCompany(company)) - dbCompany, err := s.queries.CreateCompany(ctx, domain.ConvertCreateCompany(company, uniqueSlug)) if err != nil { return domain.Company{}, err } @@ -87,14 +85,15 @@ func (s *Store) GetCompanyBySlug(ctx context.Context, slug string) (domain.Compa return domain.ConvertDBCompany(dbCompany), nil } -func (s *Store) UpdateCompany(ctx context.Context, company domain.UpdateCompany) (domain.Company, error) { - dbCompany, err := s.queries.UpdateCompany(ctx, domain.ConvertUpdateCompany(company)) +func (s *Store) UpdateCompany(ctx context.Context, company domain.UpdateCompany) error { + fmt.Printf("company %v\n", company) + err := s.queries.UpdateCompany(ctx, domain.ConvertUpdateCompany(company)) if err != nil { - return domain.Company{}, err + return err } - return domain.ConvertDBCompany(dbCompany), nil + return nil } func (s *Store) DeleteCompany(ctx context.Context, id int64) error { diff --git a/internal/repository/event.go b/internal/repository/event.go index 236de50..d5d217f 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -22,11 +22,15 @@ func (s *Store) GetLiveEventIDs(ctx context.Context) ([]int64, error) { func (s *Store) GetAllEvents(ctx context.Context, filter domain.EventFilter) ([]domain.BaseEvent, int64, error) { events, err := s.queries.GetAllEvents(ctx, dbgen.GetAllEventsParams{ - LeagueID: filter.LeagueID.ToPG(), - SportID: filter.SportID.ToPG(), - Query: filter.Query.ToPG(), - Limit: filter.Limit.ToPG(), - Offset: filter.Offset.ToPG(), + 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/repository/league.go b/internal/repository/league.go index ab01c14..e2e614e 100644 --- a/internal/repository/league.go +++ b/internal/repository/league.go @@ -16,7 +16,7 @@ func (s *Store) SaveLeagueSettings(ctx context.Context, leagueSettings domain.Cr return s.queries.SaveLeagueSettings(ctx, domain.ConvertCreateLeagueSettings(leagueSettings)) } -func (s *Store) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.BaseLeague, error) { +func (s *Store) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.BaseLeague,int64, error) { l, err := s.queries.GetAllLeagues(ctx, dbgen.GetAllLeaguesParams{ Query: filter.Query.ToPG(), CountryCode: filter.CountryCode.ToPG(), @@ -31,10 +31,17 @@ func (s *Store) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ( }, }) if err != nil { - return nil, err + return nil, 0, err } - return domain.ConvertDBBaseLeagues(l), nil + total, err := s.queries.GetTotalLeagues(ctx, dbgen.GetTotalLeaguesParams{ + Query: filter.Query.ToPG(), + CountryCode: filter.CountryCode.ToPG(), + SportID: filter.SportID.ToPG(), + IsActive: filter.IsActive.ToPG(), + }) + + return domain.ConvertDBBaseLeagues(l), total, nil } func (s *Store) GetAllLeaguesByCompany(ctx context.Context, companyID int64, filter domain.LeagueFilter) ([]domain.LeagueWithSettings, int64, error) { diff --git a/internal/repository/shop_bet.go b/internal/repository/shop_bet.go index 66e3e63..3a5bd5a 100644 --- a/internal/repository/shop_bet.go +++ b/internal/repository/shop_bet.go @@ -35,6 +35,7 @@ func convertDBShopBetDetail(bet dbgen.ShopBetDetail) domain.ShopBetDetail { CompanyID: bet.CompanyID, FullName: bet.CustomerFullName, PhoneNumber: bet.CustomerPhoneNumber, + FastCode: bet.FastCode, CashoutID: bet.CashoutID, CashedOut: bet.CashedOut, BetID: bet.BetID, @@ -63,7 +64,7 @@ func (s *Store) CreateShopBet(ctx context.Context, bet domain.CreateShopBet) (do if err != nil { return domain.ShopBet{}, err } - + return convertDBShopBet(newShopBet), err } @@ -108,7 +109,7 @@ func (s *Store) GetShopBetByID(ctx context.Context, id int64) (domain.ShopBetDet fmt.Printf("GetShopBetByID Repo BetID %d err %v \n", id, err.Error()) return domain.ShopBetDetail{}, err } - + return convertDBShopBetDetail(bet), nil } diff --git a/internal/repository/user.go b/internal/repository/user.go index 8102bb6..40f620f 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -111,7 +111,7 @@ func (s *Store) GetAllUsers(ctx context.Context, filter domain.UserFilter) ([]do Valid: filter.PageSize.Valid, }, Offset: pgtype.Int4{ - Int32: int32(filter.Page.Value), + Int32: int32(filter.Page.Value * filter.PageSize.Value), Valid: filter.Page.Valid, }, Query: pgtype.Text{ diff --git a/internal/repository/virtual_game.go b/internal/repository/virtual_game.go index ab93c29..c792801 100644 --- a/internal/repository/virtual_game.go +++ b/internal/repository/virtual_game.go @@ -4,7 +4,6 @@ import ( "context" "database/sql" "errors" - "fmt" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -30,7 +29,7 @@ type VirtualGameRepository interface { RemoveFavoriteGame(ctx context.Context, userID, gameID int64) error ListFavoriteGames(ctx context.Context, userID int64) ([]int64, error) - GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) + // GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) GetUserGameHistory(ctx context.Context, userID int64) ([]domain.VirtualGameHistory, error) CreateVirtualGameHistory(ctx context.Context, his *domain.VirtualGameHistory) error @@ -255,36 +254,36 @@ func (r *VirtualGameRepo) UpdateVirtualGameTransactionStatus(ctx context.Context }) } -func (r *VirtualGameRepo) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) { - query := `SELECT - COUNT(*) as total, - COUNT(CASE WHEN is_active = true THEN 1 END) as active, - COUNT(CASE WHEN is_active = false THEN 1 END) as inactive - FROM virtual_games` +// func (r *VirtualGameRepo) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) { +// query := `SELECT +// COUNT(*) as total, +// COUNT(CASE WHEN is_active = true THEN 1 END) as active, +// COUNT(CASE WHEN is_active = false THEN 1 END) as inactive +// FROM virtual_games` - args := []interface{}{} - argPos := 1 +// args := []interface{}{} +// argPos := 1 - // Add filters if provided - if filter.StartTime.Valid { - query += fmt.Sprintf(" WHERE created_at >= $%d", argPos) - args = append(args, filter.StartTime.Value) - argPos++ - } - if filter.EndTime.Valid { - query += fmt.Sprintf(" AND created_at <= $%d", argPos) - args = append(args, filter.EndTime.Value) - argPos++ - } +// // Add filters if provided +// if filter.StartTime.Valid { +// query += fmt.Sprintf(" WHERE created_at >= $%d", argPos) +// args = append(args, filter.StartTime.Value) +// argPos++ +// } +// if filter.EndTime.Valid { +// query += fmt.Sprintf(" AND created_at <= $%d", argPos) +// args = append(args, filter.EndTime.Value) +// argPos++ +// } - row := r.store.conn.QueryRow(ctx, query, args...) - err = row.Scan(&total, &active, &inactive) - if err != nil { - return 0, 0, 0, fmt.Errorf("failed to get game counts: %w", err) - } +// row := r.store.conn.QueryRow(ctx, query, args...) +// err = row.Scan(&total, &active, &inactive) +// if err != nil { +// return 0, 0, 0, fmt.Errorf("failed to get game counts: %w", err) +// } - return total, active, inactive, nil -} +// return total, active, inactive, nil +// } func (r *VirtualGameRepo) GetUserGameHistory(ctx context.Context, userID int64) ([]domain.VirtualGameHistory, error) { query := `SELECT game_id FROM virtual_game_histories WHERE user_id = $1 AND transaction_type = 'BET' ORDER BY created_at DESC LIMIT 100` @@ -315,4 +314,3 @@ func (r *VirtualGameRepo) ListAllVirtualGames(ctx context.Context, arg dbgen.Get func (r *VirtualGameRepo) RemoveAllVirtualGames(ctx context.Context) error { return r.store.queries.DeleteAllVirtualGames(ctx) } - diff --git a/internal/repository/wallet.go b/internal/repository/wallet.go index 6aa8d4d..02767ef 100644 --- a/internal/repository/wallet.go +++ b/internal/repository/wallet.go @@ -71,6 +71,9 @@ func convertDBGetCustomerWallet(customerWallet dbgen.CustomerWalletDetail) domai func (s *Store) CreateWallet(ctx context.Context, wallet domain.CreateWallet) (domain.Wallet, error) { newWallet, err := s.queries.CreateWallet(ctx, convertCreateWallet(wallet)) if err != nil { + if IsUniqueViolation(err) { + return domain.Wallet{}, domain.ErrWalletIDDuplicate + } return domain.Wallet{}, err } return convertDBWallet(newWallet), nil diff --git a/internal/services/company/port.go b/internal/services/company/port.go index d3540f2..3f43db8 100644 --- a/internal/services/company/port.go +++ b/internal/services/company/port.go @@ -12,7 +12,7 @@ type CompanyStore interface { SearchCompanyByName(ctx context.Context, name string) ([]domain.GetCompany, error) GetCompanyByID(ctx context.Context, id int64) (domain.GetCompany, error) GetCompanyBySlug(ctx context.Context, slug string) (domain.Company, error) - UpdateCompany(ctx context.Context, company domain.UpdateCompany) (domain.Company, error) + UpdateCompany(ctx context.Context, company domain.UpdateCompany) (error) DeleteCompany(ctx context.Context, id int64) error GetCompanyCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) diff --git a/internal/services/company/service.go b/internal/services/company/service.go index 1f69a22..2dd6624 100644 --- a/internal/services/company/service.go +++ b/internal/services/company/service.go @@ -34,7 +34,7 @@ func (s *Service) SearchCompanyByName(ctx context.Context, name string) ([]domai return s.companyStore.SearchCompanyByName(ctx, name) } -func (s *Service) UpdateCompany(ctx context.Context, company domain.UpdateCompany) (domain.Company, error) { +func (s *Service) UpdateCompany(ctx context.Context, company domain.UpdateCompany) (error) { return s.companyStore.UpdateCompany(ctx, company) } func (s *Service) DeleteCompany(ctx context.Context, id int64) error { diff --git a/internal/services/league/port.go b/internal/services/league/port.go index 277f602..31d38d2 100644 --- a/internal/services/league/port.go +++ b/internal/services/league/port.go @@ -9,7 +9,7 @@ import ( type Service interface { SaveLeague(ctx context.Context, league domain.CreateLeague) error SaveLeagueSettings(ctx context.Context, leagueSettings domain.CreateLeagueSettings) error - GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.BaseLeague, error) + GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.BaseLeague, int64, error) GetAllLeaguesByCompany(ctx context.Context, companyID int64, filter domain.LeagueFilter) ([]domain.LeagueWithSettings, int64, error) CheckLeagueSupport(ctx context.Context, leagueID int64, companyID int64) (bool, error) UpdateLeague(ctx context.Context, league domain.UpdateLeague) error diff --git a/internal/services/league/service.go b/internal/services/league/service.go index b07bc99..0e40604 100644 --- a/internal/services/league/service.go +++ b/internal/services/league/service.go @@ -25,7 +25,7 @@ func (s *service) SaveLeagueSettings(ctx context.Context, leagueSettings domain. return s.store.SaveLeagueSettings(ctx, leagueSettings) } -func (s *service) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.BaseLeague, error) { +func (s *service) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.BaseLeague, int64, error) { return s.store.GetAllLeagues(ctx, filter) } @@ -43,4 +43,4 @@ func (s *service) UpdateLeague(ctx context.Context, league domain.UpdateLeague) func (s *service) UpdateGlobalLeagueSettings(ctx context.Context, league domain.UpdateGlobalLeagueSettings) error { return s.store.UpdateGlobalLeagueSettings(ctx, league) -} \ No newline at end of file +} diff --git a/internal/services/report/service.go b/internal/services/report/service.go index 1454bae..d19cef3 100644 --- a/internal/services/report/service.go +++ b/internal/services/report/service.go @@ -135,11 +135,11 @@ func (s *Service) GetDashboardSummary(ctx context.Context, filter domain.ReportF } // Get sport/game metrics - summary.TotalGames, summary.ActiveGames, summary.InactiveGames, err = s.virtulaGamesStore.GetGameCounts(ctx, filter) - if err != nil { - s.logger.Error("failed to get game counts", "error", err) - return domain.DashboardSummary{}, err - } + // summary.TotalGames, summary.ActiveGames, summary.InactiveGames, err = s.virtulaGamesStore.GetGameCounts(ctx, filter) + // if err != nil { + // s.logger.Error("failed to get game counts", "error", err) + // return domain.DashboardSummary{}, err + // } // Get company metrics summary.TotalCompanies, summary.ActiveCompanies, summary.InactiveCompanies, err = s.companyStore.GetCompanyCounts(ctx, filter) diff --git a/internal/services/virtualGame/port.go b/internal/services/virtualGame/port.go index ca320e1..8121ea1 100644 --- a/internal/services/virtualGame/port.go +++ b/internal/services/virtualGame/port.go @@ -23,7 +23,7 @@ type VirtualGameService interface { ProcessTournamentWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error) ProcessPromoWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error) - GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) + // GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) ListGames(ctx context.Context, currency string) ([]domain.PopOKGame, error) RecommendGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error) AddFavoriteGame(ctx context.Context, userID, gameID int64) error diff --git a/internal/services/virtualGame/service.go b/internal/services/virtualGame/service.go index 3cd1fb8..46ddb66 100644 --- a/internal/services/virtualGame/service.go +++ b/internal/services/virtualGame/service.go @@ -657,9 +657,9 @@ func (s *service) verifySignature(callback *domain.PopOKCallback) bool { return expected == callback.Signature } -func (s *service) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) { - return s.repo.GetGameCounts(ctx, filter) -} +// func (s *service) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) { +// return s.repo.GetGameCounts(ctx, filter) +// } func (s *service) ListGames(ctx context.Context, currency string) ([]domain.PopOKGame, error) { now := time.Now().Format("02-01-2006 15:04:05") // dd-mm-yyyy hh:mm:ss diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 09aab85..a03c34b 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -95,7 +95,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S } for _, job := range schedule { - // job.task() + job.task() if _, err := c.AddFunc(job.spec, job.task); err != nil { mongoLogger.Error("Failed to schedule data fetching cron job", zap.Error(err), diff --git a/internal/web_server/handlers/admin.go b/internal/web_server/handlers/admin.go index b7bd75e..036f96a 100644 --- a/internal/web_server/handlers/admin.go +++ b/internal/web_server/handlers/admin.go @@ -103,25 +103,25 @@ func (h *Handler) CreateAdmin(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create admin:"+err.Error()) } - if req.CompanyID != nil { - _, err := h.companySvc.UpdateCompany(c.Context(), domain.UpdateCompany{ - ID: *req.CompanyID, - AdminID: domain.ValidInt64{ - Value: newUser.ID, - Valid: true, - }, - }) - if err != nil { - h.mongoLoggerSvc.Error("failed to update company with new admin", - zap.Int64("status_code", fiber.StatusInternalServerError), - zap.Int64("company_id", *req.CompanyID), - zap.Int64("admin_id", newUser.ID), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update company"+err.Error()) - } - } + // if req.CompanyID != nil { + // _, err := h.companySvc.UpdateCompany(c.Context(), domain.UpdateCompany{ + // ID: *req.CompanyID, + // AdminID: domain.ValidInt64{ + // Value: newUser.ID, + // Valid: true, + // }, + // }) + // if err != nil { + // h.mongoLoggerSvc.Error("failed to update company with new admin", + // zap.Int64("status_code", fiber.StatusInternalServerError), + // zap.Int64("company_id", *req.CompanyID), + // zap.Int64("admin_id", newUser.ID), + // zap.Error(err), + // zap.Time("timestamp", time.Now()), + // ) + // return fiber.NewError(fiber.StatusInternalServerError, "Failed to update company"+err.Error()) + // } + // } h.mongoLoggerSvc.Info("admin created successfully", zap.Int64("admin_id", newUser.ID), @@ -196,7 +196,7 @@ func (h *Handler) GetAllAdmins(c *fiber.Ctx) error { Valid: true, } } - + companyFilter := int64(c.QueryInt("company_id")) filter := domain.UserFilter{ Role: string(domain.RoleAdmin), @@ -283,7 +283,7 @@ func (h *Handler) GetAllAdmins(c *fiber.Ctx) error { zap.Time("timestamp", time.Now()), ) - return response.WritePaginatedJSON(c, fiber.StatusOK, "Admins retrieved successfully", result, nil, filter.Page.Value, int(total)) + return response.WritePaginatedJSON(c, fiber.StatusOK, "Admins retrieved successfully", result, nil, filter.Page.Value+1, int(total)) } // GetAdminByID godoc diff --git a/internal/web_server/handlers/company_handler.go b/internal/web_server/handlers/company_handler.go index 64fd401..d353a29 100644 --- a/internal/web_server/handlers/company_handler.go +++ b/internal/web_server/handlers/company_handler.go @@ -1,6 +1,7 @@ package handlers import ( + "errors" "fmt" "strconv" "time" @@ -82,17 +83,31 @@ func (h *Handler) CreateCompany(c *fiber.Ctx) error { AdminID: user.ID, WalletID: newWallet.ID, DeductedPercentage: req.DeductedPercentage, + Slug: req.Slug, + IsActive: req.IsActive, }) if err != nil { + if errors.Is(err, domain.ErrWalletIDDuplicate) { + h.mongoLoggerSvc.Error("CreateCompanyReq failed to create company", + zap.Int64("userID", user.ID), + zap.String("name", req.Name), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + + return fiber.NewError(fiber.StatusBadRequest, "this admin already has a company assigned to him") + } + h.mongoLoggerSvc.Error("CreateCompanyReq failed to create company", zap.Int64("userID", user.ID), zap.String("name", req.Name), - zap.String("Name", req.Name), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } @@ -361,11 +376,12 @@ func (h *Handler) UpdateCompany(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, errMsg) } - company, err := h.companySvc.UpdateCompany(c.Context(), domain.ConvertUpdateCompanyReq(req)) + err = h.companySvc.UpdateCompany(c.Context(), domain.ConvertUpdateCompanyReq(req, id)) if err != nil { h.mongoLoggerSvc.Error("Failed to update company", zap.Int64("companyID", id), + zap.Any("req", req), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), @@ -373,9 +389,7 @@ func (h *Handler) UpdateCompany(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } - res := domain.ConvertCompany(company) - - return response.WriteJSON(c, fiber.StatusOK, "Company Updated", res, nil) + return response.WriteJSON(c, fiber.StatusOK, "Company Updated", nil, nil) } diff --git a/internal/web_server/handlers/leagues.go b/internal/web_server/handlers/leagues.go index 08c15a9..f801df8 100644 --- a/internal/web_server/handlers/leagues.go +++ b/internal/web_server/handlers/leagues.go @@ -86,7 +86,7 @@ func (h *Handler) GetAllLeagues(c *fiber.Ctx) error { zap.Bool("sport_id_valid", sportID.Valid), } - leagues, err := h.leagueSvc.GetAllLeagues(c.Context(), domain.LeagueFilter{ + leagues, total, err := h.leagueSvc.GetAllLeagues(c.Context(), domain.LeagueFilter{ CountryCode: countryCode, IsActive: isActive, SportID: sportID, @@ -104,7 +104,7 @@ func (h *Handler) GetAllLeagues(c *fiber.Ctx) error { res := domain.ConvertBaseLeagueResList(leagues) - return response.WriteJSON(c, fiber.StatusOK, "All leagues retrieved", res, nil) + return response.WritePaginatedJSON(c, fiber.StatusOK, "All leagues retrieved successfully", res, nil, page, int(total)) } // GetAllLeaguesForTenant godoc diff --git a/internal/web_server/handlers/user.go b/internal/web_server/handlers/user.go index fe89de9..8d36762 100644 --- a/internal/web_server/handlers/user.go +++ b/internal/web_server/handlers/user.go @@ -328,9 +328,9 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error { } // TODO: Remove later - _, err = h.walletSvc.AddToWallet( - c.Context(), newWallet.RegularID, domain.ToCurrency(10000.0), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, - "Added 10000.0 to wallet only as test for deployment") + // _, err = h.walletSvc.AddToWallet( + // c.Context(), newWallet.RegularID, domain.ToCurrency(10000.0), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, + // "Added 10000.0 to wallet only as test for deployment") if err != nil { h.mongoLoggerSvc.Error("Failed to update wallet for user", From 0121b31838132a2fc6a4906f113e164fde6eed5b Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Tue, 7 Oct 2025 13:49:29 +0300 Subject: [PATCH 08/23] feat: Update cron job scheduling and user wallet creation logic; increment API version to 1.0.dev17 --- internal/web_server/cron.go | 132 +++++++++++++-------------- internal/web_server/handlers/user.go | 2 +- internal/web_server/routes.go | 2 +- 3 files changed, 68 insertions(+), 68 deletions(-) diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index a03c34b..f1caaac 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -27,75 +27,75 @@ 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 { - job.task() + // job.task() if _, err := c.AddFunc(job.spec, job.task); err != nil { mongoLogger.Error("Failed to schedule data fetching cron job", zap.Error(err), diff --git a/internal/web_server/handlers/user.go b/internal/web_server/handlers/user.go index 8d36762..5103d0d 100644 --- a/internal/web_server/handlers/user.go +++ b/internal/web_server/handlers/user.go @@ -304,7 +304,7 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "failed to register user:"+err.Error()) } - newWallet, err := h.walletSvc.CreateCustomerWallet(c.Context(), newUser.ID) + _, err = h.walletSvc.CreateCustomerWallet(c.Context(), newUser.ID) if err != nil { h.mongoLoggerSvc.Error("Failed to create wallet for user", zap.Int64("userID", newUser.ID), diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 05aede1..df3eb90 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.dev16", + "version": "1.0.dev17", }) }) From 80129828e0de070ba34edba43aa93b3cec14785e Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 8 Oct 2025 12:46:50 +0300 Subject: [PATCH 09/23] fix: removed kafka and redis and added integration changes --- cmd/main.go | 10 +- db/migrations/000002_notification.up.sql | 2 + db/query/branch.sql | 6 +- db/query/notification.sql | 12 +- docker-compose.yml | 57 +-- gen/db/auth.sql.go | 2 +- gen/db/bet.sql.go | 2 +- gen/db/bet_stat.sql.go | 2 +- gen/db/bonus.sql.go | 2 +- gen/db/branch.sql.go | 15 +- gen/db/cashier.sql.go | 2 +- gen/db/company.sql.go | 2 +- gen/db/copyfrom.go | 2 +- gen/db/db.go | 2 +- gen/db/direct_deposit.sql.go | 2 +- gen/db/disabled_odds.sql.go | 2 +- gen/db/enet_pulse.sql.go | 2 +- gen/db/event_history.sql.go | 2 +- gen/db/events.sql.go | 2 +- gen/db/events_stat.sql.go | 2 +- gen/db/flags.sql.go | 2 +- gen/db/institutions.sql.go | 2 +- gen/db/issue_reporting.sql.go | 2 +- gen/db/leagues.sql.go | 2 +- gen/db/location.sql.go | 2 +- gen/db/models.go | 4 +- gen/db/monitor.sql.go | 2 +- gen/db/notification.sql.go | 46 ++- gen/db/odd_history.sql.go | 2 +- gen/db/odds.sql.go | 2 +- gen/db/otp.sql.go | 2 +- gen/db/raffle.sql.go | 2 +- gen/db/referal.sql.go | 2 +- gen/db/report.sql.go | 2 +- gen/db/result.sql.go | 2 +- gen/db/result_log.sql.go | 2 +- gen/db/settings.sql.go | 2 +- gen/db/shop_transactions.sql.go | 2 +- gen/db/ticket.sql.go | 2 +- gen/db/transfer.sql.go | 2 +- gen/db/user.sql.go | 2 +- gen/db/virtual_games.sql.go | 2 +- gen/db/wallet.sql.go | 2 +- internal/domain/currency.go | 114 ++++-- internal/domain/notification.go | 4 + internal/domain/shop_bet.go | 4 +- internal/repository/branch.go | 7 +- internal/repository/notification.go | 13 +- internal/services/bet/service.go | 27 +- internal/services/branch/port.go | 2 +- internal/services/branch/service.go | 4 +- internal/services/kafka/consumer.go | 110 +++--- internal/services/kafka/producer.go | 58 +-- internal/services/notification/service.go | 343 +++++++++--------- internal/services/transaction/shop_deposit.go | 2 +- internal/services/wallet/direct_deposit.go | 23 +- internal/services/wallet/notification.go | 59 ++- internal/services/wallet/service.go | 5 +- internal/services/wallet/wallet.go | 83 +++-- internal/web_server/cron.go | 148 ++++---- internal/web_server/handlers/bet_handler.go | 14 +- .../web_server/handlers/branch_handler.go | 4 +- internal/web_server/handlers/event_handler.go | 53 ++- internal/web_server/routes.go | 2 +- makefile | 2 +- 65 files changed, 737 insertions(+), 566 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 1297a2c..5b0f003 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -41,7 +41,6 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions" issuereporting "github.com/SamuelTariku/FortuneBet-Backend/internal/services/issue_reporting" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/kafka" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/messenger" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification" @@ -122,10 +121,10 @@ func main() { // var userStore user.UserStore // Initialize producer - topic := "wallet-balance-topic" - producer := kafka.NewProducer(cfg.KafkaBrokers, topic) + // topic := "wallet-balance-topic" + // producer := kafka.NewProducer(cfg.KafkaBrokers, topic) - notificationSvc := notificationservice.New(notificationRepo, domain.MongoDBLogger, logger, cfg, messengerSvc, userSvc, cfg.KafkaBrokers) + notificationSvc := notificationservice.New(notificationRepo, domain.MongoDBLogger, logger, cfg, messengerSvc, userSvc) walletSvc := wallet.NewService( wallet.WalletStore(store), @@ -135,7 +134,6 @@ func main() { userSvc, domain.MongoDBLogger, logger, - producer, ) branchSvc := branch.NewService(store) @@ -239,7 +237,7 @@ func main() { go walletMonitorSvc.Start() httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc, domain.MongoDBLogger) - httpserver.StartTicketCrons(*ticketSvc, domain.MongoDBLogger) + httpserver.StartCleanupCrons(*ticketSvc, notificationSvc, domain.MongoDBLogger) issueReportingRepo := repository.NewReportedIssueRepository(store) diff --git a/db/migrations/000002_notification.up.sql b/db/migrations/000002_notification.up.sql index 3d74954..78b28c8 100644 --- a/db/migrations/000002_notification.up.sql +++ b/db/migrations/000002_notification.up.sql @@ -37,6 +37,8 @@ CREATE TABLE IF NOT EXISTS notifications ( priority INTEGER, version INTEGER NOT NULL DEFAULT 0, timestamp TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + img TEXT, + expires TIMESTAMPTZ NOT NULL, metadata JSONB ); CREATE TABLE IF NOT EXISTS wallet_threshold_notifications ( diff --git a/db/query/branch.sql b/db/query/branch.sql index 1e09f40..14fd768 100644 --- a/db/query/branch.sql +++ b/db/query/branch.sql @@ -65,7 +65,11 @@ WHERE branch_manager_id = $1; -- name: SearchBranchByName :many SELECT * FROM branch_details -WHERE name ILIKE '%' || $1 || '%'; +WHERE name ILIKE '%' || $1 || '%' + AND ( + company_id = sqlc.narg('company_id') + OR sqlc.narg('company_id') IS NULL + ); -- name: GetAllSupportedOperations :many SELECT * FROM supported_operations; diff --git a/db/query/notification.sql b/db/query/notification.sql index 76d0edc..c1dfa06 100644 --- a/db/query/notification.sql +++ b/db/query/notification.sql @@ -12,6 +12,8 @@ INSERT INTO notifications ( payload, priority, timestamp, + expires, + img, metadata ) VALUES ( @@ -27,7 +29,9 @@ VALUES ( $10, $11, $12, - $13 + $13, + $14, + $15 ) RETURNING *; -- name: GetNotification :one @@ -88,4 +92,8 @@ SELECT COUNT(*) as total, WHEN is_read = false THEN 1 END ) as unread -FROM notifications; \ No newline at end of file +FROM notifications; + +-- name: DeleteOldNotifications :exec +DELETE FROM notifications +WHERE expires < now(); \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index b480236..d80ff06 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -54,46 +54,17 @@ services: ] networks: - app - redis: - image: redis:7-alpine - ports: - - "6379:6379" - networks: - - app - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - - - zookeeper: - image: confluentinc/cp-zookeeper:7.5.0 - container_name: zookeeper - ports: - - "2181:2181" - environment: - ZOOKEEPER_CLIENT_PORT: 2181 - ZOOKEEPER_TICK_TIME: 2000 - networks: - - app - kafka: - image: confluentinc/cp-kafka:7.5.0 - depends_on: - - zookeeper - environment: - KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 - KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,PLAINTEXT_HOST://0.0.0.0:29092 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - - ports: - - "9092:9092" - - "29092:29092" - networks: - - app + # redis: + # image: redis:7-alpine + # ports: + # - "6379:6379" + # networks: + # - app + # healthcheck: + # test: ["CMD", "redis-cli", "ping"] + # interval: 10s + # timeout: 5s + # retries: 5 app: build: context: . @@ -104,15 +75,9 @@ services: environment: - DB_URL=postgresql://root:secret@postgres:5432/gh?sslmode=disable - MONGO_URI=mongodb://root:secret@mongo:27017 - - REDIS_ADDR=redis:6379 - - KAFKA_BROKERS=kafka:9092 depends_on: migrate: condition: service_completed_successfully - mongo: - condition: service_healthy - redis: - condition: service_healthy networks: - app command: ["/app/bin/web"] diff --git a/gen/db/auth.sql.go b/gen/db/auth.sql.go index 8dd2280..7d8d59d 100644 --- a/gen/db/auth.sql.go +++ b/gen/db/auth.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: auth.sql package dbgen diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index a9fac0e..8e6254c 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: bet.sql package dbgen diff --git a/gen/db/bet_stat.sql.go b/gen/db/bet_stat.sql.go index 9a7b494..275ef07 100644 --- a/gen/db/bet_stat.sql.go +++ b/gen/db/bet_stat.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: bet_stat.sql package dbgen diff --git a/gen/db/bonus.sql.go b/gen/db/bonus.sql.go index 1a5d8e9..7c6f168 100644 --- a/gen/db/bonus.sql.go +++ b/gen/db/bonus.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: bonus.sql package dbgen diff --git a/gen/db/branch.sql.go b/gen/db/branch.sql.go index 89d2959..64cd62e 100644 --- a/gen/db/branch.sql.go +++ b/gen/db/branch.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: branch.sql package dbgen @@ -455,10 +455,19 @@ const SearchBranchByName = `-- name: SearchBranchByName :many SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active FROM branch_details WHERE name ILIKE '%' || $1 || '%' + AND ( + company_id = $2 + OR $2 IS NULL + ) ` -func (q *Queries) SearchBranchByName(ctx context.Context, dollar_1 pgtype.Text) ([]BranchDetail, error) { - rows, err := q.db.Query(ctx, SearchBranchByName, dollar_1) +type SearchBranchByNameParams struct { + Column1 pgtype.Text `json:"column_1"` + CompanyID pgtype.Int8 `json:"company_id"` +} + +func (q *Queries) SearchBranchByName(ctx context.Context, arg SearchBranchByNameParams) ([]BranchDetail, error) { + rows, err := q.db.Query(ctx, SearchBranchByName, arg.Column1, arg.CompanyID) if err != nil { return nil, err } diff --git a/gen/db/cashier.sql.go b/gen/db/cashier.sql.go index 55e69d2..fc4a7f8 100644 --- a/gen/db/cashier.sql.go +++ b/gen/db/cashier.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: cashier.sql package dbgen diff --git a/gen/db/company.sql.go b/gen/db/company.sql.go index ec851a4..32d9ee2 100644 --- a/gen/db/company.sql.go +++ b/gen/db/company.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: company.sql package dbgen diff --git a/gen/db/copyfrom.go b/gen/db/copyfrom.go index f7a4793..1212253 100644 --- a/gen/db/copyfrom.go +++ b/gen/db/copyfrom.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: copyfrom.go package dbgen diff --git a/gen/db/db.go b/gen/db/db.go index 8134784..84de07c 100644 --- a/gen/db/db.go +++ b/gen/db/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 package dbgen diff --git a/gen/db/direct_deposit.sql.go b/gen/db/direct_deposit.sql.go index ff5a3b2..be02750 100644 --- a/gen/db/direct_deposit.sql.go +++ b/gen/db/direct_deposit.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: direct_deposit.sql package dbgen diff --git a/gen/db/disabled_odds.sql.go b/gen/db/disabled_odds.sql.go index 58913cf..b9cc744 100644 --- a/gen/db/disabled_odds.sql.go +++ b/gen/db/disabled_odds.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: disabled_odds.sql package dbgen diff --git a/gen/db/enet_pulse.sql.go b/gen/db/enet_pulse.sql.go index 9e72da8..a2c131b 100644 --- a/gen/db/enet_pulse.sql.go +++ b/gen/db/enet_pulse.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: enet_pulse.sql package dbgen diff --git a/gen/db/event_history.sql.go b/gen/db/event_history.sql.go index 35946cd..a4f1c2e 100644 --- a/gen/db/event_history.sql.go +++ b/gen/db/event_history.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: event_history.sql package dbgen diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index 1feaf00..a8345fb 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: events.sql package dbgen diff --git a/gen/db/events_stat.sql.go b/gen/db/events_stat.sql.go index 615e2fa..677fa2a 100644 --- a/gen/db/events_stat.sql.go +++ b/gen/db/events_stat.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: events_stat.sql package dbgen diff --git a/gen/db/flags.sql.go b/gen/db/flags.sql.go index 4b82cac..653543f 100644 --- a/gen/db/flags.sql.go +++ b/gen/db/flags.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: flags.sql package dbgen diff --git a/gen/db/institutions.sql.go b/gen/db/institutions.sql.go index 61ca108..324ac3e 100644 --- a/gen/db/institutions.sql.go +++ b/gen/db/institutions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: institutions.sql package dbgen diff --git a/gen/db/issue_reporting.sql.go b/gen/db/issue_reporting.sql.go index e35fba1..7fcb4af 100644 --- a/gen/db/issue_reporting.sql.go +++ b/gen/db/issue_reporting.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: issue_reporting.sql package dbgen diff --git a/gen/db/leagues.sql.go b/gen/db/leagues.sql.go index 8fb31ca..1413699 100644 --- a/gen/db/leagues.sql.go +++ b/gen/db/leagues.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: leagues.sql package dbgen diff --git a/gen/db/location.sql.go b/gen/db/location.sql.go index 254c73a..008aa61 100644 --- a/gen/db/location.sql.go +++ b/gen/db/location.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: location.sql package dbgen diff --git a/gen/db/models.go b/gen/db/models.go index af11573..7f7d4f9 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 package dbgen @@ -482,6 +482,8 @@ type Notification struct { Priority pgtype.Int4 `json:"priority"` Version int32 `json:"version"` Timestamp pgtype.Timestamptz `json:"timestamp"` + Img pgtype.Text `json:"img"` + Expires pgtype.Timestamptz `json:"expires"` Metadata []byte `json:"metadata"` } diff --git a/gen/db/monitor.sql.go b/gen/db/monitor.sql.go index b5f248f..a9a7ecb 100644 --- a/gen/db/monitor.sql.go +++ b/gen/db/monitor.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: monitor.sql package dbgen diff --git a/gen/db/notification.sql.go b/gen/db/notification.sql.go index 14d3a4c..3c029fa 100644 --- a/gen/db/notification.sql.go +++ b/gen/db/notification.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: notification.sql package dbgen @@ -39,6 +39,8 @@ INSERT INTO notifications ( payload, priority, timestamp, + expires, + img, metadata ) VALUES ( @@ -54,9 +56,11 @@ VALUES ( $10, $11, $12, - $13 + $13, + $14, + $15 ) -RETURNING id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata +RETURNING id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, img, expires, metadata ` type CreateNotificationParams struct { @@ -72,6 +76,8 @@ type CreateNotificationParams struct { Payload []byte `json:"payload"` Priority pgtype.Int4 `json:"priority"` Timestamp pgtype.Timestamptz `json:"timestamp"` + Expires pgtype.Timestamptz `json:"expires"` + Img pgtype.Text `json:"img"` Metadata []byte `json:"metadata"` } @@ -89,6 +95,8 @@ func (q *Queries) CreateNotification(ctx context.Context, arg CreateNotification arg.Payload, arg.Priority, arg.Timestamp, + arg.Expires, + arg.Img, arg.Metadata, ) var i Notification @@ -106,13 +114,25 @@ func (q *Queries) CreateNotification(ctx context.Context, arg CreateNotification &i.Priority, &i.Version, &i.Timestamp, + &i.Img, + &i.Expires, &i.Metadata, ) return i, err } +const DeleteOldNotifications = `-- name: DeleteOldNotifications :exec +DELETE FROM notifications +WHERE expires < now() +` + +func (q *Queries) DeleteOldNotifications(ctx context.Context) error { + _, err := q.db.Exec(ctx, DeleteOldNotifications) + return err +} + const GetAllNotifications = `-- name: GetAllNotifications :many -SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata +SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, img, expires, metadata FROM notifications ORDER BY timestamp DESC LIMIT $1 OFFSET $2 @@ -146,6 +166,8 @@ func (q *Queries) GetAllNotifications(ctx context.Context, arg GetAllNotificatio &i.Priority, &i.Version, &i.Timestamp, + &i.Img, + &i.Expires, &i.Metadata, ); err != nil { return nil, err @@ -159,7 +181,7 @@ func (q *Queries) GetAllNotifications(ctx context.Context, arg GetAllNotificatio } const GetNotification = `-- name: GetNotification :one -SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata +SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, img, expires, metadata FROM notifications WHERE id = $1 LIMIT 1 @@ -182,6 +204,8 @@ func (q *Queries) GetNotification(ctx context.Context, id string) (Notification, &i.Priority, &i.Version, &i.Timestamp, + &i.Img, + &i.Expires, &i.Metadata, ) return i, err @@ -254,7 +278,7 @@ func (q *Queries) GetUserNotificationCount(ctx context.Context, recipientID int6 } const GetUserNotifications = `-- name: GetUserNotifications :many -SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata +SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, img, expires, metadata FROM notifications WHERE recipient_id = $1 ORDER BY timestamp DESC @@ -290,6 +314,8 @@ func (q *Queries) GetUserNotifications(ctx context.Context, arg GetUserNotificat &i.Priority, &i.Version, &i.Timestamp, + &i.Img, + &i.Expires, &i.Metadata, ); err != nil { return nil, err @@ -303,7 +329,7 @@ func (q *Queries) GetUserNotifications(ctx context.Context, arg GetUserNotificat } const ListFailedNotifications = `-- name: ListFailedNotifications :many -SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata +SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, img, expires, metadata FROM notifications WHERE delivery_status = 'failed' AND timestamp < NOW() - INTERVAL '1 hour' @@ -334,6 +360,8 @@ func (q *Queries) ListFailedNotifications(ctx context.Context, limit int32) ([]N &i.Priority, &i.Version, &i.Timestamp, + &i.Img, + &i.Expires, &i.Metadata, ); err != nil { return nil, err @@ -378,7 +406,7 @@ SET delivery_status = $2, is_read = $3, metadata = $4 WHERE id = $1 -RETURNING id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata +RETURNING id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, img, expires, metadata ` type UpdateNotificationStatusParams struct { @@ -410,6 +438,8 @@ func (q *Queries) UpdateNotificationStatus(ctx context.Context, arg UpdateNotifi &i.Priority, &i.Version, &i.Timestamp, + &i.Img, + &i.Expires, &i.Metadata, ) return i, err diff --git a/gen/db/odd_history.sql.go b/gen/db/odd_history.sql.go index dd834c5..3fe7dd9 100644 --- a/gen/db/odd_history.sql.go +++ b/gen/db/odd_history.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: odd_history.sql package dbgen diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index d1e676d..e7c687e 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: odds.sql package dbgen diff --git a/gen/db/otp.sql.go b/gen/db/otp.sql.go index c96aaaa..7dba175 100644 --- a/gen/db/otp.sql.go +++ b/gen/db/otp.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: otp.sql package dbgen diff --git a/gen/db/raffle.sql.go b/gen/db/raffle.sql.go index 8d0be34..d4b85d3 100644 --- a/gen/db/raffle.sql.go +++ b/gen/db/raffle.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: raffle.sql package dbgen diff --git a/gen/db/referal.sql.go b/gen/db/referal.sql.go index 99d8bb2..caaa01a 100644 --- a/gen/db/referal.sql.go +++ b/gen/db/referal.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: referal.sql package dbgen diff --git a/gen/db/report.sql.go b/gen/db/report.sql.go index d6193c1..1a1ccde 100644 --- a/gen/db/report.sql.go +++ b/gen/db/report.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: report.sql package dbgen diff --git a/gen/db/result.sql.go b/gen/db/result.sql.go index 899561b..bff7b1e 100644 --- a/gen/db/result.sql.go +++ b/gen/db/result.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: result.sql package dbgen diff --git a/gen/db/result_log.sql.go b/gen/db/result_log.sql.go index 3f11e16..468795e 100644 --- a/gen/db/result_log.sql.go +++ b/gen/db/result_log.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: result_log.sql package dbgen diff --git a/gen/db/settings.sql.go b/gen/db/settings.sql.go index 76eb504..96ea916 100644 --- a/gen/db/settings.sql.go +++ b/gen/db/settings.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: settings.sql package dbgen diff --git a/gen/db/shop_transactions.sql.go b/gen/db/shop_transactions.sql.go index 68d770e..5c4b52e 100644 --- a/gen/db/shop_transactions.sql.go +++ b/gen/db/shop_transactions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: shop_transactions.sql package dbgen diff --git a/gen/db/ticket.sql.go b/gen/db/ticket.sql.go index 45603ba..bc9bb5f 100644 --- a/gen/db/ticket.sql.go +++ b/gen/db/ticket.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: ticket.sql package dbgen diff --git a/gen/db/transfer.sql.go b/gen/db/transfer.sql.go index fe25cbe..b2a1066 100644 --- a/gen/db/transfer.sql.go +++ b/gen/db/transfer.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: transfer.sql package dbgen diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index 9b16163..f2f9fff 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: user.sql package dbgen diff --git a/gen/db/virtual_games.sql.go b/gen/db/virtual_games.sql.go index b98f602..5a2809a 100644 --- a/gen/db/virtual_games.sql.go +++ b/gen/db/virtual_games.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: virtual_games.sql package dbgen diff --git a/gen/db/wallet.sql.go b/gen/db/wallet.sql.go index ccb2d37..fcde631 100644 --- a/gen/db/wallet.sql.go +++ b/gen/db/wallet.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: wallet.sql package dbgen diff --git a/internal/domain/currency.go b/internal/domain/currency.go index 7ce3b3c..a15ba52 100644 --- a/internal/domain/currency.go +++ b/internal/domain/currency.go @@ -25,49 +25,83 @@ func (m Currency) String() string { return fmt.Sprintf("$%.2f", m.Float32()) } + +// TODO: Change the currency to this format when implementing multi-currency +// type Currency struct { +// Value int64 +// Type IntCurrency +// } + +// // ToCurrency converts a float32 (like 12.34) into Currency (stored in cents). +// func NewCurrency(f float32, currencyType IntCurrency) Currency { +// cents := math.Round(float64(f) * 100) // avoid float32 precision issues +// return Currency{ +// Value: int64(cents), +// Type: currencyType, +// } +// } + +// func NewBase(v int64) Currency { +// return Currency{ +// Value: v, +// Type: BASE, +// } +// } + +// // Float32 converts a Currency back into float32 (like 12.34). +// func (m Currency) Float32() float32 { +// return float32(m.Value) / 100 +// } + +// // String returns a formatted Currency value for display. +// func (m Currency) String() string { +// return fmt.Sprintf("$%.2f", m.Float32()) +// } + type IntCurrency string const ( - ETB IntCurrency = "ETB" // Ethiopian Birr - NGN IntCurrency = "NGN" // Nigerian Naira - ZAR IntCurrency = "ZAR" // South African Rand - EGP IntCurrency = "EGP" // Egyptian Pound - KES IntCurrency = "KES" // Kenyan Shilling - UGX IntCurrency = "UGX" // Ugandan Shilling - TZS IntCurrency = "TZS" // Tanzanian Shilling - RWF IntCurrency = "RWF" // Rwandan Franc - BIF IntCurrency = "BIF" // Burundian Franc - XOF IntCurrency = "XOF" // West African CFA Franc (BCEAO) - XAF IntCurrency = "XAF" // Central African CFA Franc (BEAC) - GHS IntCurrency = "GHS" // Ghanaian Cedi - SDG IntCurrency = "SDG" // Sudanese Pound - SSP IntCurrency = "SSP" // South Sudanese Pound - DZD IntCurrency = "DZD" // Algerian Dinar - MAD IntCurrency = "MAD" // Moroccan Dirham - TND IntCurrency = "TND" // Tunisian Dinar - LYD IntCurrency = "LYD" // Libyan Dinar - MZN IntCurrency = "MZN" // Mozambican Metical - AOA IntCurrency = "AOA" // Angolan Kwanza - BWP IntCurrency = "BWP" // Botswana Pula - ZMW IntCurrency = "ZMW" // Zambian Kwacha - MWK IntCurrency = "MWK" // Malawian Kwacha - LSL IntCurrency = "LSL" // Lesotho Loti - NAD IntCurrency = "NAD" // Namibian Dollar - SZL IntCurrency = "SZL" // Swazi Lilangeni - CVE IntCurrency = "CVE" // Cape Verdean Escudo - GMD IntCurrency = "GMD" // Gambian Dalasi - SLL IntCurrency = "SLL" // Sierra Leonean Leone - LRD IntCurrency = "LRD" // Liberian Dollar - GNF IntCurrency = "GNF" // Guinean Franc - XCD IntCurrency = "XCD" // Eastern Caribbean Dollar (used in Saint Lucia) - MRU IntCurrency = "MRU" // Mauritanian Ouguiya - KMF IntCurrency = "KMF" // Comorian Franc - DJF IntCurrency = "DJF" // Djiboutian Franc - SOS IntCurrency = "SOS" // Somali Shilling - ERN IntCurrency = "ERN" // Eritrean Nakfa - MGA IntCurrency = "MGA" // Malagasy Ariary - SCR IntCurrency = "SCR" // Seychellois Rupee - MUR IntCurrency = "MUR" // Mauritian Rupee + BASE IntCurrency = "BASE" + ETB IntCurrency = "ETB" // Ethiopian Birr + NGN IntCurrency = "NGN" // Nigerian Naira + ZAR IntCurrency = "ZAR" // South African Rand + EGP IntCurrency = "EGP" // Egyptian Pound + KES IntCurrency = "KES" // Kenyan Shilling + UGX IntCurrency = "UGX" // Ugandan Shilling + TZS IntCurrency = "TZS" // Tanzanian Shilling + RWF IntCurrency = "RWF" // Rwandan Franc + BIF IntCurrency = "BIF" // Burundian Franc + XOF IntCurrency = "XOF" // West African CFA Franc (BCEAO) + XAF IntCurrency = "XAF" // Central African CFA Franc (BEAC) + GHS IntCurrency = "GHS" // Ghanaian Cedi + SDG IntCurrency = "SDG" // Sudanese Pound + SSP IntCurrency = "SSP" // South Sudanese Pound + DZD IntCurrency = "DZD" // Algerian Dinar + MAD IntCurrency = "MAD" // Moroccan Dirham + TND IntCurrency = "TND" // Tunisian Dinar + LYD IntCurrency = "LYD" // Libyan Dinar + MZN IntCurrency = "MZN" // Mozambican Metical + AOA IntCurrency = "AOA" // Angolan Kwanza + BWP IntCurrency = "BWP" // Botswana Pula + ZMW IntCurrency = "ZMW" // Zambian Kwacha + MWK IntCurrency = "MWK" // Malawian Kwacha + LSL IntCurrency = "LSL" // Lesotho Loti + NAD IntCurrency = "NAD" // Namibian Dollar + SZL IntCurrency = "SZL" // Swazi Lilangeni + CVE IntCurrency = "CVE" // Cape Verdean Escudo + GMD IntCurrency = "GMD" // Gambian Dalasi + SLL IntCurrency = "SLL" // Sierra Leonean Leone + LRD IntCurrency = "LRD" // Liberian Dollar + GNF IntCurrency = "GNF" // Guinean Franc + XCD IntCurrency = "XCD" // Eastern Caribbean Dollar (used in Saint Lucia) + MRU IntCurrency = "MRU" // Mauritanian Ouguiya + KMF IntCurrency = "KMF" // Comorian Franc + DJF IntCurrency = "DJF" // Djiboutian Franc + SOS IntCurrency = "SOS" // Somali Shilling + ERN IntCurrency = "ERN" // Eritrean Nakfa + MGA IntCurrency = "MGA" // Malagasy Ariary + SCR IntCurrency = "SCR" // Seychellois Rupee + MUR IntCurrency = "MUR" // Mauritian Rupee // International currencies (already listed) USD IntCurrency = "USD" // US Dollar diff --git a/internal/domain/notification.go b/internal/domain/notification.go index b7193d7..046b3a9 100644 --- a/internal/domain/notification.go +++ b/internal/domain/notification.go @@ -84,6 +84,8 @@ type Notification struct { Priority int `json:"priority,omitempty"` Version int `json:"-"` Timestamp time.Time `json:"timestamp"` + Expires time.Time `json:"expires"` + Image string `json:"image"` Metadata json.RawMessage `json:"metadata,omitempty"` } type CreateNotification struct { @@ -97,6 +99,8 @@ type CreateNotification struct { DeliveryChannel DeliveryChannel `json:"delivery_channel,omitempty"` Payload NotificationPayload `json:"payload"` Priority int `json:"priority,omitempty"` + Expires time.Time `json:"expires"` + Image string `json:"image,omitempty"` Metadata json.RawMessage `json:"metadata,omitempty"` } diff --git a/internal/domain/shop_bet.go b/internal/domain/shop_bet.go index b46b4b6..c321a02 100644 --- a/internal/domain/shop_bet.go +++ b/internal/domain/shop_bet.go @@ -87,7 +87,7 @@ type ShopBetRes struct { BetID int64 `json:"bet_id" example:"1"` NumberOfOutcomes int64 `json:"number_of_outcomes" example:"1"` Status OutcomeStatus `json:"status" example:"1"` - Amount Currency `json:"amount"` + Amount float32 `json:"amount"` Outcomes []BetOutcome `json:"outcomes"` TransactionVerified bool `json:"transaction_verified" example:"true"` UpdatedAt time.Time `json:"updated_at" example:"2025-04-08T12:00:00Z"` @@ -119,7 +119,7 @@ func ConvertShopBetDetail(shopBet ShopBetDetail) ShopBetRes { BetID: shopBet.BetID, NumberOfOutcomes: shopBet.NumberOfOutcomes, Status: shopBet.Status, - Amount: shopBet.Amount, + Amount: shopBet.Amount.Float32(), Outcomes: shopBet.Outcomes, TransactionVerified: shopBet.TransactionVerified, UpdatedAt: shopBet.UpdatedAt, diff --git a/internal/repository/branch.go b/internal/repository/branch.go index 89b329e..4c5041d 100644 --- a/internal/repository/branch.go +++ b/internal/repository/branch.go @@ -68,8 +68,11 @@ func (s *Store) GetAllBranches(ctx context.Context, filter domain.BranchFilter) return branches, nil } -func (s *Store) SearchBranchByName(ctx context.Context, name string) ([]domain.BranchDetail, error) { - dbBranches, err := s.queries.SearchBranchByName(ctx, pgtype.Text{String: name, Valid: true}) +func (s *Store) SearchBranchByName(ctx context.Context, name string, companyID domain.ValidInt64) ([]domain.BranchDetail, error) { + dbBranches, err := s.queries.SearchBranchByName(ctx, dbgen.SearchBranchByNameParams{ + Column1: pgtype.Text{String: name, Valid: true}, + CompanyID: companyID.ToPG(), + }) if err != nil { return nil, err } diff --git a/internal/repository/notification.go b/internal/repository/notification.go index eea21e5..0c91e82 100644 --- a/internal/repository/notification.go +++ b/internal/repository/notification.go @@ -14,12 +14,13 @@ import ( type NotificationRepository interface { CreateNotification(ctx context.Context, notification *domain.Notification) (*domain.Notification, error) UpdateNotificationStatus(ctx context.Context, id, status string, isRead bool, metadata []byte) (*domain.Notification, error) - GetUserNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, int64, error) + GetUserNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, int64, error) ListFailedNotifications(ctx context.Context, limit int) ([]domain.Notification, error) ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) GetNotificationCounts(ctx context.Context, filter domain.ReportFilter) (total, read, unread int64, err error) + DeleteOldNotifications(ctx context.Context) error } type Repository struct { @@ -69,6 +70,8 @@ func (r *Repository) CreateNotification(ctx context.Context, notification *domai Payload: marshalPayload(notification.Payload), Priority: priority, Timestamp: pgtype.Timestamptz{Time: notification.Timestamp, Valid: true}, + Expires: pgtype.Timestamptz{Time: notification.Expires, Valid: true}, + Img: pgtype.Text{String: notification.Image, Valid: notification.Image != ""}, Metadata: notification.Metadata, } @@ -113,7 +116,7 @@ func (r *Repository) GetUserNotifications(ctx context.Context, recipientID int64 if err != nil { return nil, 0, err } - + var result []domain.Notification = make([]domain.Notification, 0, len(dbNotifications)) for _, dbNotif := range dbNotifications { domainNotif := r.mapDBToDomain(&dbNotif) @@ -160,6 +163,10 @@ func (r *Repository) ListRecipientIDs(ctx context.Context, receiver domain.Notif return r.store.queries.ListRecipientIDsByReceiver(ctx, string(receiver)) } +func (s *Repository) DeleteOldNotifications(ctx context.Context) error { + return s.store.queries.DeleteOldNotifications(ctx) +} + func (r *Repository) mapDBToDomain(dbNotif *dbgen.Notification) *domain.Notification { var errorSeverity domain.NotificationErrorSeverity if dbNotif.ErrorSeverity.Valid { @@ -199,6 +206,8 @@ func (r *Repository) mapDBToDomain(dbNotif *dbgen.Notification) *domain.Notifica Payload: payload, Priority: priority, Timestamp: dbNotif.Timestamp.Time, + Expires: dbNotif.Expires.Time, + Image: dbNotif.Img.String, Metadata: dbNotif.Metadata, } } diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index 2379c3b..1e33f8b 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -46,6 +46,8 @@ var ( ErrInvalidAmount = errors.New("invalid amount") ErrBetAmountTooHigh = errors.New("cannot create a bet with an amount above limit") ErrBetWinningTooHigh = errors.New("total Winnings over set limit") + + ErrCompanyDeductedPercentInvalid = errors.New("invalid company deducted percentage") ) type Service struct { @@ -303,8 +305,9 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID } fastCode := helpers.GenerateFastCode() - accumulator := calculateAccumulator(len(outcomes)) - amount := req.Amount + (req.Amount * accumulator) + // accumulator := calculateAccumulator(len(outcomes)) + // amount := req.Amount + (req.Amount * accumulator) + amount := req.Amount newBet := domain.CreateBet{ Amount: domain.ToCurrency(amount), @@ -524,7 +527,25 @@ func (s *Service) DeductBetFromBranchWallet(ctx context.Context, amount float32, return err } - deductedAmount := amount * company.DeductedPercentage + if company.DeductedPercentage > 1 { + s.mongoLogger.Error("Invalid company deducted percentage", + zap.Int64("wallet_id", walletID), + zap.Float32("amount", company.DeductedPercentage), + zap.Error(err), + ) + return ErrCompanyDeductedPercentInvalid + } + + deductedAmount := amount - (amount * company.DeductedPercentage) + + if deductedAmount == 0 { + s.mongoLogger.Fatal("Amount", + zap.Int64("wallet_id", walletID), + zap.Float32("amount", deductedAmount), + zap.Error(err), + ) + return err + } _, err = s.walletSvc.DeductFromWallet(ctx, walletID, domain.ToCurrency(deductedAmount), domain.ValidInt64{ Value: userID, diff --git a/internal/services/branch/port.go b/internal/services/branch/port.go index 8b17ae1..d23f91a 100644 --- a/internal/services/branch/port.go +++ b/internal/services/branch/port.go @@ -12,7 +12,7 @@ type BranchStore interface { GetBranchByManagerID(ctx context.Context, branchManagerID int64) ([]domain.BranchDetail, error) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]domain.BranchDetail, error) GetAllBranches(ctx context.Context, filter domain.BranchFilter) ([]domain.BranchDetail, error) - SearchBranchByName(ctx context.Context, name string) ([]domain.BranchDetail, error) + SearchBranchByName(ctx context.Context, name string, companyID domain.ValidInt64) ([]domain.BranchDetail, error) UpdateBranch(ctx context.Context, branch domain.UpdateBranch) (domain.Branch, error) DeleteBranch(ctx context.Context, id int64) error CreateBranchOperation(ctx context.Context, branchOperation domain.CreateBranchOperation) error diff --git a/internal/services/branch/service.go b/internal/services/branch/service.go index 9e4f641..1b44651 100644 --- a/internal/services/branch/service.go +++ b/internal/services/branch/service.go @@ -54,8 +54,8 @@ func (s *Service) GetAllSupportedOperations(ctx context.Context) ([]domain.Suppo return s.branchStore.GetAllSupportedOperations(ctx) } -func (s *Service) SearchBranchByName(ctx context.Context, name string) ([]domain.BranchDetail, error) { - return s.branchStore.SearchBranchByName(ctx, name) +func (s *Service) SearchBranchByName(ctx context.Context, name string, companyID domain.ValidInt64) ([]domain.BranchDetail, error) { + return s.branchStore.SearchBranchByName(ctx, name, companyID) } func (s *Service) UpdateBranch(ctx context.Context, branch domain.UpdateBranch) (domain.Branch, error) { return s.branchStore.UpdateBranch(ctx, branch) diff --git a/internal/services/kafka/consumer.go b/internal/services/kafka/consumer.go index c2343fe..fb55eea 100644 --- a/internal/services/kafka/consumer.go +++ b/internal/services/kafka/consumer.go @@ -1,67 +1,67 @@ package kafka -import ( - "context" - "encoding/json" - "log" +// import ( +// "context" +// "encoding/json" +// "log" - "github.com/SamuelTariku/FortuneBet-Backend/internal/event" - "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/ws" - "github.com/segmentio/kafka-go" -) +// "github.com/SamuelTariku/FortuneBet-Backend/internal/event" +// "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/ws" +// "github.com/segmentio/kafka-go" +// ) -type WalletConsumer struct { - reader *kafka.Reader - hub *ws.NotificationHub - topic string - groupID string - brokers []string -} +// type WalletConsumer struct { +// reader *kafka.Reader +// hub *ws.NotificationHub +// topic string +// groupID string +// brokers []string +// } -func NewWalletConsumer(brokers []string, topic, groupID string, hub *ws.NotificationHub) *WalletConsumer { - return &WalletConsumer{ - brokers: brokers, - topic: topic, - groupID: groupID, - hub: hub, - reader: kafka.NewReader(kafka.ReaderConfig{ - Brokers: brokers, - GroupID: groupID, - Topic: topic, - }), - } -} +// func NewWalletConsumer(brokers []string, topic, groupID string, hub *ws.NotificationHub) *WalletConsumer { +// return &WalletConsumer{ +// brokers: brokers, +// topic: topic, +// groupID: groupID, +// hub: hub, +// reader: kafka.NewReader(kafka.ReaderConfig{ +// Brokers: brokers, +// GroupID: groupID, +// Topic: topic, +// }), +// } +// } -func (c *WalletConsumer) Start(ctx context.Context) { - go func() { - for { - m, err := c.reader.ReadMessage(ctx) - if err != nil { - log.Printf("Error reading wallet Kafka message: %v", err) - continue - } +// func (c *WalletConsumer) Start(ctx context.Context) { +// go func() { +// for { +// m, err := c.reader.ReadMessage(ctx) +// if err != nil { +// log.Printf("Error reading wallet Kafka message: %v", err) +// continue +// } - var evt event.WalletEvent - if err := json.Unmarshal(m.Value, &evt); err != nil { - log.Printf("Failed to unmarshal wallet event: %v", err) - continue - } +// var evt event.WalletEvent +// if err := json.Unmarshal(m.Value, &evt); err != nil { +// log.Printf("Failed to unmarshal wallet event: %v", err) +// continue +// } - payload := map[string]interface{}{ - "type": evt.EventType, - "wallet_id": evt.WalletID, - "user_id": evt.UserID, - "balance": evt.Balance, - "wallet_type": evt.WalletType, - "trigger": evt.Trigger, - "recipient_id": evt.UserID, - } +// payload := map[string]interface{}{ +// "type": evt.EventType, +// "wallet_id": evt.WalletID, +// "user_id": evt.UserID, +// "balance": evt.Balance, +// "wallet_type": evt.WalletType, +// "trigger": evt.Trigger, +// "recipient_id": evt.UserID, +// } - // Broadcast to appropriate WebSocket clients - c.hub.Broadcast <- payload - } - }() -} +// // Broadcast to appropriate WebSocket clients +// c.hub.Broadcast <- payload +// } +// }() +// } // func (c *WalletConsumer) Shutdown() error { // return c.reader.Close() diff --git a/internal/services/kafka/producer.go b/internal/services/kafka/producer.go index 8c03b5c..e2720ee 100644 --- a/internal/services/kafka/producer.go +++ b/internal/services/kafka/producer.go @@ -1,36 +1,36 @@ package kafka -import ( - "context" - "encoding/json" - "time" +// import ( +// "context" +// "encoding/json" +// "time" - "github.com/segmentio/kafka-go" -) +// "github.com/segmentio/kafka-go" +// ) -type Producer struct { - writer *kafka.Writer -} +// type Producer struct { +// writer *kafka.Writer +// } -func NewProducer(brokers []string, topic string) *Producer { - return &Producer{ - writer: &kafka.Writer{ - Addr: kafka.TCP(brokers...), - Topic: topic, - Balancer: &kafka.LeastBytes{}, - }, - } -} +// func NewProducer(brokers []string, topic string) *Producer { +// return &Producer{ +// writer: &kafka.Writer{ +// Addr: kafka.TCP(brokers...), +// Topic: topic, +// Balancer: &kafka.LeastBytes{}, +// }, +// } +// } -func (p *Producer) Publish(ctx context.Context, key string, event any) error { - msgBytes, err := json.Marshal(event) - if err != nil { - return err - } +// func (p *Producer) Publish(ctx context.Context, key string, event any) error { +// msgBytes, err := json.Marshal(event) +// if err != nil { +// return err +// } - return p.writer.WriteMessages(ctx, kafka.Message{ - Key: []byte(key), - Value: msgBytes, - Time: time.Now(), - }) -} +// return p.writer.WriteMessages(ctx, kafka.Message{ +// Key: []byte(key), +// Value: msgBytes, +// Time: time.Now(), +// }) +// } diff --git a/internal/services/notification/service.go b/internal/services/notification/service.go index bc31c02..283ed9a 100644 --- a/internal/services/notification/service.go +++ b/internal/services/notification/service.go @@ -2,7 +2,6 @@ package notificationservice import ( "context" - "encoding/json" "fmt" // "errors" @@ -12,19 +11,19 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - "github.com/SamuelTariku/FortuneBet-Backend/internal/event" + "github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/messenger" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" - "github.com/segmentio/kafka-go" + // "github.com/segmentio/kafka-go" "go.uber.org/zap" // "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/ws" // afro "github.com/amanuelabay/afrosms-go" "github.com/gorilla/websocket" - "github.com/redis/go-redis/v9" + // "github.com/redis/go-redis/v9" ) type Service struct { @@ -39,8 +38,6 @@ type Service struct { messengerSvc *messenger.Service mongoLogger *zap.Logger logger *slog.Logger - redisClient *redis.Client - reader *kafka.Reader } func New(repo repository.NotificationRepository, @@ -49,17 +46,8 @@ func New(repo repository.NotificationRepository, cfg *config.Config, messengerSvc *messenger.Service, userSvc *user.Service, - kafkaBrokers []string, ) *Service { hub := ws.NewNotificationHub() - rdb := redis.NewClient(&redis.Options{ - Addr: cfg.RedisAddr, // e.g., "redis:6379" - }) - walletReader := kafka.NewReader(kafka.ReaderConfig{ - Brokers: kafkaBrokers, - Topic: "wallet-balance-topic", - GroupID: "notification-service-group", // Each service should have its own group - }) svc := &Service{ repo: repo, @@ -72,15 +60,13 @@ func New(repo repository.NotificationRepository, messengerSvc: messengerSvc, userSvc: userSvc, config: cfg, - redisClient: rdb, - reader: walletReader, } go hub.Run() go svc.startWorker() go svc.startRetryWorker() - go svc.RunRedisSubscriber(context.Background()) - go svc.StartKafkaConsumer(context.Background()) + // go svc.RunRedisSubscriber(context.Background()) + // go svc.StartKafkaConsumer(context.Background()) return svc } @@ -484,189 +470,192 @@ func (s *Service) CountUnreadNotifications(ctx context.Context, recipient_id int return s.repo.CountUnreadNotifications(ctx, recipient_id) } +func (s *Service) DeleteOldNotifications(ctx context.Context) error { + return s.repo.DeleteOldNotifications(ctx) +} + // func (s *Service) GetNotificationCounts(ctx context.Context, filter domain.ReportFilter) (total, read, unread int64, err error){ // return s.repo.Get(ctx, filter) // } -func (s *Service) RunRedisSubscriber(ctx context.Context) { - pubsub := s.redisClient.Subscribe(ctx, "live_metrics") - defer pubsub.Close() +// func (s *Service) RunRedisSubscriber(ctx context.Context) { +// pubsub := s.redisClient.Subscribe(ctx, "live_metrics") +// defer pubsub.Close() - ch := pubsub.Channel() - for msg := range ch { - var parsed map[string]interface{} - if err := json.Unmarshal([]byte(msg.Payload), &parsed); err != nil { - // s.logger.Error("invalid Redis message format", "payload", msg.Payload, "error", err) - s.mongoLogger.Error("invalid Redis message format", - zap.String("payload", msg.Payload), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - continue - } +// ch := pubsub.Channel() +// for msg := range ch { +// var parsed map[string]interface{} +// if err := json.Unmarshal([]byte(msg.Payload), &parsed); err != nil { +// // s.logger.Error("invalid Redis message format", "payload", msg.Payload, "error", err) +// s.mongoLogger.Error("invalid Redis message format", +// zap.String("payload", msg.Payload), +// zap.Error(err), +// zap.Time("timestamp", time.Now()), +// ) +// continue +// } - eventType, _ := parsed["type"].(string) - payload := parsed["payload"] - recipientID, hasRecipient := parsed["recipient_id"] - recipientType, _ := parsed["recipient_type"].(string) +// eventType, _ := parsed["type"].(string) +// payload := parsed["payload"] +// recipientID, hasRecipient := parsed["recipient_id"] +// recipientType, _ := parsed["recipient_type"].(string) - message := map[string]interface{}{ - "type": eventType, - "payload": payload, - } +// message := map[string]interface{}{ +// "type": eventType, +// "payload": payload, +// } - if hasRecipient { - message["recipient_id"] = recipientID - message["recipient_type"] = recipientType - } +// if hasRecipient { +// message["recipient_id"] = recipientID +// message["recipient_type"] = recipientType +// } - s.Hub.Broadcast <- message - } -} +// s.Hub.Broadcast <- message +// } +// } -func (s *Service) UpdateLiveWalletMetrics(ctx context.Context, companies []domain.GetCompany, branches []domain.BranchWallet) error { - const key = "live_metrics" +// func (s *Service) UpdateLiveWalletMetrics(ctx context.Context, companies []domain.GetCompany, branches []domain.BranchWallet) error { +// const key = "live_metrics" - companyBalances := make([]domain.CompanyWalletBalance, 0, len(companies)) - for _, c := range companies { - companyBalances = append(companyBalances, domain.CompanyWalletBalance{ - CompanyID: c.ID, - CompanyName: c.Name, - Balance: float64(c.WalletBalance.Float32()), - }) - } +// companyBalances := make([]domain.CompanyWalletBalance, 0, len(companies)) +// for _, c := range companies { +// companyBalances = append(companyBalances, domain.CompanyWalletBalance{ +// CompanyID: c.ID, +// CompanyName: c.Name, +// Balance: float64(c.WalletBalance.Float32()), +// }) +// } - branchBalances := make([]domain.BranchWalletBalance, 0, len(branches)) - for _, b := range branches { - branchBalances = append(branchBalances, domain.BranchWalletBalance{ - BranchID: b.ID, - BranchName: b.Name, - CompanyID: b.CompanyID, - Balance: float64(b.Balance.Float32()), - }) - } +// branchBalances := make([]domain.BranchWalletBalance, 0, len(branches)) +// for _, b := range branches { +// branchBalances = append(branchBalances, domain.BranchWalletBalance{ +// BranchID: b.ID, +// BranchName: b.Name, +// CompanyID: b.CompanyID, +// Balance: float64(b.Balance.Float32()), +// }) +// } - payload := domain.LiveWalletMetrics{ - Timestamp: time.Now(), - CompanyBalances: companyBalances, - BranchBalances: branchBalances, - } +// payload := domain.LiveWalletMetrics{ +// Timestamp: time.Now(), +// CompanyBalances: companyBalances, +// BranchBalances: branchBalances, +// } - updatedData, err := json.Marshal(payload) - if err != nil { - return err - } +// updatedData, err := json.Marshal(payload) +// if err != nil { +// return err +// } - if err := s.redisClient.Set(ctx, key, updatedData, 0).Err(); err != nil { - return err - } +// if err := s.redisClient.Set(ctx, key, updatedData, 0).Err(); err != nil { +// return err +// } - if err := s.redisClient.Publish(ctx, key, updatedData).Err(); err != nil { - return err - } - return nil -} +// if err := s.redisClient.Publish(ctx, key, updatedData).Err(); err != nil { +// return err +// } +// return nil +// } -func (s *Service) GetLiveMetrics(ctx context.Context) (domain.LiveMetric, error) { - const key = "live_metrics" - var metric domain.LiveMetric +// func (s *Service) GetLiveMetrics(ctx context.Context) (domain.LiveMetric, error) { +// const key = "live_metrics" +// var metric domain.LiveMetric - val, err := s.redisClient.Get(ctx, key).Result() - if err == redis.Nil { - // Key does not exist yet, return zero-valued struct - return domain.LiveMetric{}, nil - } else if err != nil { - return domain.LiveMetric{}, err - } +// val, err := s.redisClient.Get(ctx, key).Result() +// if err == redis.Nil { +// // Key does not exist yet, return zero-valued struct +// return domain.LiveMetric{}, nil +// } else if err != nil { +// return domain.LiveMetric{}, err +// } - if err := json.Unmarshal([]byte(val), &metric); err != nil { - return domain.LiveMetric{}, err - } +// if err := json.Unmarshal([]byte(val), &metric); err != nil { +// return domain.LiveMetric{}, err +// } - return metric, nil -} +// return metric, nil +// } +// func (s *Service) StartKafkaConsumer(ctx context.Context) { +// go func() { +// for { +// m, err := s.reader.ReadMessage(ctx) +// if err != nil { +// if err == context.Canceled { +// s.mongoLogger.Info("[NotificationSvc.KafkaConsumer] Stopped by context") +// return +// } +// s.mongoLogger.Error("[NotificationSvc.KafkaConsumer] Error reading message", +// zap.Error(err), +// zap.Time("timestamp", time.Now()), +// ) +// time.Sleep(1 * time.Second) // backoff +// continue +// } -func (s *Service) StartKafkaConsumer(ctx context.Context) { - go func() { - for { - m, err := s.reader.ReadMessage(ctx) - if err != nil { - if err == context.Canceled { - s.mongoLogger.Info("[NotificationSvc.KafkaConsumer] Stopped by context") - return - } - s.mongoLogger.Error("[NotificationSvc.KafkaConsumer] Error reading message", - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - time.Sleep(1 * time.Second) // backoff - continue - } +// var walletEvent event.WalletEvent +// if err := json.Unmarshal(m.Value, &walletEvent); err != nil { +// s.mongoLogger.Error("[NotificationSvc.KafkaConsumer] Failed to unmarshal wallet event", +// zap.String("message", string(m.Value)), +// zap.Error(err), +// zap.Time("timestamp", time.Now()), +// ) +// continue +// } - var walletEvent event.WalletEvent - if err := json.Unmarshal(m.Value, &walletEvent); err != nil { - s.mongoLogger.Error("[NotificationSvc.KafkaConsumer] Failed to unmarshal wallet event", - zap.String("message", string(m.Value)), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - continue - } +// raw, _ := json.Marshal(map[string]any{ +// "balance": walletEvent.Balance.Float32(), +// "type": walletEvent.WalletType, +// "timestamp": time.Now(), +// }) - raw, _ := json.Marshal(map[string]any{ - "balance": walletEvent.Balance.Float32(), - "type": walletEvent.WalletType, - "timestamp": time.Now(), - }) +// headline := "" +// message := "" +// var receiver domain.NotificationRecieverSide +// switch walletEvent.WalletType { - headline := "" - message := "" - var receiver domain.NotificationRecieverSide - switch walletEvent.WalletType { +// case domain.StaticWalletType: +// headline = "Referral and Bonus Wallet Updated" +// message = fmt.Sprintf("Your referral and bonus wallet balance is now %.2f", walletEvent.Balance.Float32()) +// receiver = domain.NotificationRecieverSideCustomer +// case domain.RegularWalletType: +// headline = "Wallet Updated" +// message = fmt.Sprintf("Your wallet balance is now %.2f", walletEvent.Balance.Float32()) +// receiver = domain.NotificationRecieverSideCustomer +// case domain.BranchWalletType: +// headline = "Branch Wallet Updated" +// message = fmt.Sprintf("branch wallet balance is now %.2f", walletEvent.Balance.Float32()) +// receiver = domain.NotificationRecieverSideBranchManager +// case domain.CompanyWalletType: +// headline = "Company Wallet Updated" +// message = fmt.Sprintf("company wallet balance is now %.2f", walletEvent.Balance.Float32()) +// receiver = domain.NotificationRecieverSideAdmin +// } +// // Handle the wallet event: send notification +// notification := &domain.Notification{ +// RecipientID: walletEvent.UserID, +// DeliveryChannel: domain.DeliveryChannelInApp, +// Reciever: receiver, +// Type: domain.NotificationTypeWalletUpdated, +// DeliveryStatus: domain.DeliveryStatusPending, +// IsRead: false, +// Level: domain.NotificationLevelInfo, +// Priority: 2, +// Metadata: raw, +// Payload: domain.NotificationPayload{ +// Headline: headline, +// Message: message, +// }, +// } - case domain.StaticWalletType: - headline = "Referral and Bonus Wallet Updated" - message = fmt.Sprintf("Your referral and bonus wallet balance is now %.2f", walletEvent.Balance.Float32()) - receiver = domain.NotificationRecieverSideCustomer - case domain.RegularWalletType: - headline = "Wallet Updated" - message = fmt.Sprintf("Your wallet balance is now %.2f", walletEvent.Balance.Float32()) - receiver = domain.NotificationRecieverSideCustomer - case domain.BranchWalletType: - headline = "Branch Wallet Updated" - message = fmt.Sprintf("branch wallet balance is now %.2f", walletEvent.Balance.Float32()) - receiver = domain.NotificationRecieverSideBranchManager - case domain.CompanyWalletType: - headline = "Company Wallet Updated" - message = fmt.Sprintf("company wallet balance is now %.2f", walletEvent.Balance.Float32()) - receiver = domain.NotificationRecieverSideAdmin - } - // Handle the wallet event: send notification - notification := &domain.Notification{ - RecipientID: walletEvent.UserID, - DeliveryChannel: domain.DeliveryChannelInApp, - Reciever: receiver, - Type: domain.NotificationTypeWalletUpdated, - DeliveryStatus: domain.DeliveryStatusPending, - IsRead: false, - Level: domain.NotificationLevelInfo, - Priority: 2, - Metadata: raw, - Payload: domain.NotificationPayload{ - Headline: headline, - Message: message, - }, - } - - if err := s.SendNotification(ctx, notification); err != nil { - s.mongoLogger.Error("[NotificationSvc.KafkaConsumer] Failed to send notification", - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - } - } - }() -} +// if err := s.SendNotification(ctx, notification); err != nil { +// s.mongoLogger.Error("[NotificationSvc.KafkaConsumer] Failed to send notification", +// zap.Error(err), +// zap.Time("timestamp", time.Now()), +// ) +// } +// } +// }() +// } // func (s *Service) UpdateLiveWalletMetricForWallet(ctx context.Context, wallet domain.Wallet) { // var ( diff --git a/internal/services/transaction/shop_deposit.go b/internal/services/transaction/shop_deposit.go index cc08874..aaef480 100644 --- a/internal/services/transaction/shop_deposit.go +++ b/internal/services/transaction/shop_deposit.go @@ -54,7 +54,7 @@ func (s *Service) CreateShopDeposit(ctx context.Context, userID int64, role doma // } newTransaction, err := s.CreateShopTransaction(ctx, domain.CreateShopTransaction{ - Amount: domain.Currency(req.Amount), + Amount: domain.ToCurrency(req.Amount), BranchID: branchID, CompanyID: companyID, UserID: userID, diff --git a/internal/services/wallet/direct_deposit.go b/internal/services/wallet/direct_deposit.go index a049d66..11ef1bf 100644 --- a/internal/services/wallet/direct_deposit.go +++ b/internal/services/wallet/direct_deposit.go @@ -8,7 +8,6 @@ import ( "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - "github.com/SamuelTariku/FortuneBet-Backend/internal/event" ) // InitiateDirectDeposit creates a pending deposit request @@ -73,8 +72,8 @@ func (s *Service) VerifyDirectDeposit( } // Publish wallet update event - go s.publishWalletUpdate(ctx, deposit.WalletID, deposit.Wallet.UserID, - deposit.Wallet.Balance+deposit.Amount, "direct_deposit_verified") + // go s.publishWalletUpdate(ctx, deposit.WalletID, deposit.Wallet.UserID, + // deposit.Wallet.Balance+deposit.Amount, "direct_deposit_verified") // Update deposit status deposit.Status = domain.DepositStatusCompleted @@ -206,12 +205,12 @@ func (s *Service) notifyCustomerVerificationResult(ctx context.Context, deposit } } -func (s *Service) publishWalletUpdate(ctx context.Context, walletID, userID int64, newBalance domain.Currency, trigger string) { - s.kafkaProducer.Publish(ctx, fmt.Sprint(walletID), event.WalletEvent{ - EventType: event.WalletBalanceUpdated, - WalletID: walletID, - UserID: userID, - Balance: newBalance, - Trigger: trigger, - }) -} +// func (s *Service) publishWalletUpdate(ctx context.Context, walletID, userID int64, newBalance domain.Currency, trigger string) { +// s.kafkaProducer.Publish(ctx, fmt.Sprint(walletID), event.WalletEvent{ +// EventType: event.WalletBalanceUpdated, +// WalletID: walletID, +// UserID: userID, +// Balance: newBalance, +// Trigger: trigger, +// }) +// } diff --git a/internal/services/wallet/notification.go b/internal/services/wallet/notification.go index 6580ef6..0112492 100644 --- a/internal/services/wallet/notification.go +++ b/internal/services/wallet/notification.go @@ -2,10 +2,12 @@ package wallet import ( "context" + "encoding/json" "fmt" + "time" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "go.uber.org/zap" - "time" ) func (s *Service) GetAdminNotificationRecipients(ctx context.Context, walletID int64, walletType domain.WalletType) ([]int64, error) { @@ -63,12 +65,65 @@ func (s *Service) GetAdminNotificationRecipients(ctx context.Context, walletID i return recipients, nil } +func (s *Service) SendWalletUpdateNotification(ctx context.Context, wallet domain.Wallet) error { + raw, _ := json.Marshal(map[string]any{ + "balance": wallet.Balance.Float32(), + "type": wallet.Type, + "timestamp": time.Now(), + }) + headline := "" + message := "" + var receiver domain.NotificationRecieverSide + switch wallet.Type { + case domain.StaticWalletType: + headline = "Referral and Bonus Wallet Updated" + message = fmt.Sprintf("Your referral and bonus wallet balance is now %.2f", wallet.Balance.Float32()) + receiver = domain.NotificationRecieverSideCustomer + case domain.RegularWalletType: + headline = "Wallet Updated" + message = fmt.Sprintf("Your wallet balance is now %.2f", wallet.Balance.Float32()) + receiver = domain.NotificationRecieverSideCustomer + case domain.BranchWalletType: + headline = "Branch Wallet Updated" + message = fmt.Sprintf("branch wallet balance is now %.2f", wallet.Balance.Float32()) + receiver = domain.NotificationRecieverSideBranchManager + case domain.CompanyWalletType: + headline = "Company Wallet Updated" + message = fmt.Sprintf("company wallet balance is now %.2f", wallet.Balance.Float32()) + receiver = domain.NotificationRecieverSideAdmin + } + // Handle the wallet event: send notification + notification := &domain.Notification{ + RecipientID: wallet.UserID, + DeliveryChannel: domain.DeliveryChannelInApp, + Reciever: receiver, + Type: domain.NotificationTypeWalletUpdated, + DeliveryStatus: domain.DeliveryStatusPending, + IsRead: false, + Level: domain.NotificationLevelInfo, + Priority: 2, + Metadata: raw, + Payload: domain.NotificationPayload{ + Headline: headline, + Message: message, + }, + } + if err := s.notificationSvc.SendNotification(ctx, notification); err != nil { + s.mongoLogger.Error("[WalletSvc.SendWalletUpdateNotification] Failed to send notification", + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return err + } + + return nil +} func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWallet domain.Wallet) error { // Send different messages - + // Send notification to admin team adminNotification := &domain.Notification{ ErrorSeverity: "low", diff --git a/internal/services/wallet/service.go b/internal/services/wallet/service.go index 2e248a5..27616cb 100644 --- a/internal/services/wallet/service.go +++ b/internal/services/wallet/service.go @@ -3,7 +3,7 @@ package wallet import ( "log/slog" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/kafka" + // "github.com/SamuelTariku/FortuneBet-Backend/internal/services/kafka" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "go.uber.org/zap" @@ -18,7 +18,6 @@ type Service struct { userSvc *user.Service mongoLogger *zap.Logger logger *slog.Logger - kafkaProducer *kafka.Producer } func NewService( @@ -29,7 +28,6 @@ func NewService( userSvc *user.Service, mongoLogger *zap.Logger, logger *slog.Logger, - kafkaProducer *kafka.Producer, ) *Service { return &Service{ walletStore: walletStore, @@ -40,6 +38,5 @@ func NewService( userSvc: userSvc, mongoLogger: mongoLogger, logger: logger, - kafkaProducer: kafkaProducer, } } diff --git a/internal/services/wallet/wallet.go b/internal/services/wallet/wallet.go index 54ad2e6..a5b3f49 100644 --- a/internal/services/wallet/wallet.go +++ b/internal/services/wallet/wallet.go @@ -3,10 +3,12 @@ package wallet import ( "context" "errors" - "fmt" + + // "fmt" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - "github.com/SamuelTariku/FortuneBet-Backend/internal/event" + "go.uber.org/zap" + // "github.com/SamuelTariku/FortuneBet-Backend/internal/event" ) var ( @@ -92,16 +94,22 @@ func (s *Service) UpdateBalance(ctx context.Context, id int64, balance domain.Cu return err } - go func() { - s.kafkaProducer.Publish(ctx, fmt.Sprint(wallet.UserID), event.WalletEvent{ - EventType: event.WalletBalanceUpdated, - WalletID: wallet.ID, - UserID: wallet.UserID, - Balance: balance, - WalletType: wallet.Type, - Trigger: "UpdateBalance", - }) - }() + // go func() { + // s.kafkaProducer.Publish(ctx, fmt.Sprint(wallet.UserID), event.WalletEvent{ + // EventType: event.WalletBalanceUpdated, + // WalletID: wallet.ID, + // UserID: wallet.UserID, + // Balance: balance, + // WalletType: wallet.Type, + // Trigger: "UpdateBalance", + // }) + // }() + + if err := s.SendWalletUpdateNotification(ctx, wallet); err != nil { + s.mongoLogger.Info("Failed to send wallet update notification", + zap.Int64("wallet_id", wallet.ID), + zap.Error(err)) + } return nil } @@ -118,16 +126,21 @@ func (s *Service) AddToWallet( return domain.Transfer{}, err } - go func() { - s.kafkaProducer.Publish(ctx, fmt.Sprint(wallet.ID), event.WalletEvent{ - EventType: event.WalletBalanceUpdated, - WalletID: wallet.ID, - UserID: wallet.UserID, - Balance: wallet.Balance + amount, - WalletType: wallet.Type, - Trigger: "AddToWallet", - }) - }() + // go func() { + // s.kafkaProducer.Publish(ctx, fmt.Sprint(wallet.ID), event.WalletEvent{ + // EventType: event.WalletBalanceUpdated, + // WalletID: wallet.ID, + // UserID: wallet.UserID, + // Balance: wallet.Balance + amount, + // WalletType: wallet.Type, + // Trigger: "AddToWallet", + // }) + // }() + if err := s.SendWalletUpdateNotification(ctx, wallet); err != nil { + s.mongoLogger.Info("Failed to send wallet update notification", + zap.Int64("wallet_id", wallet.ID), + zap.Error(err)) + } // Log the transfer here for reference newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ @@ -184,17 +197,23 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain. return domain.Transfer{}, nil } - go func() { - s.kafkaProducer.Publish(ctx, fmt.Sprint(wallet.ID), event.WalletEvent{ - EventType: event.WalletBalanceUpdated, - WalletID: wallet.ID, - UserID: wallet.UserID, - Balance: wallet.Balance - amount, - WalletType: wallet.Type, - Trigger: "DeductFromWallet", - }) - }() + // go func() { + // s.kafkaProducer.Publish(ctx, fmt.Sprint(wallet.ID), event.WalletEvent{ + // EventType: event.WalletBalanceUpdated, + // WalletID: wallet.ID, + // UserID: wallet.UserID, + // Balance: wallet.Balance - amount, + // WalletType: wallet.Type, + // Trigger: "DeductFromWallet", + // }) + // }() + if err := s.SendWalletUpdateNotification(ctx, wallet); err != nil { + s.mongoLogger.Info("Failed to send wallet update notification", + 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 f1caaac..bcdce17 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -11,6 +11,7 @@ import ( betSvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" enetpulse "github.com/SamuelTariku/FortuneBet-Backend/internal/services/enet_pulse" eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification" oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/report" resultsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" @@ -27,75 +28,75 @@ 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 { - // job.task() + job.task() if _, err := c.AddFunc(job.spec, job.task); err != nil { mongoLogger.Error("Failed to schedule data fetching cron job", zap.Error(err), @@ -108,7 +109,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S mongoLogger.Info("Cron jobs started for event and odds services") } -func StartTicketCrons(ticketService ticket.Service, mongoLogger *zap.Logger) { +func StartCleanupCrons(ticketService ticket.Service, notificationSvc *notificationservice.Service, mongoLogger *zap.Logger) { c := cron.New(cron.WithSeconds()) schedule := []struct { @@ -128,6 +129,19 @@ func StartTicketCrons(ticketService ticket.Service, mongoLogger *zap.Logger) { } }, }, + { + spec: "0 0 * * * *", + task: func() { + mongoLogger.Info("Deleting old notifications") + if err := notificationSvc.DeleteOldNotifications(context.Background()); err != nil { + mongoLogger.Error("Failed to remove old notifications", + zap.Error(err), + ) + } else { + mongoLogger.Info("Successfully deleted old notifications") + } + }, + }, } for _, job := range schedule { diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index 8fe7d5f..4550395 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -643,7 +643,6 @@ func (h *Handler) GetAllTenantBets(c *fiber.Ctx) error { h.BadRequestLogger().Error("invalid company id", zap.Any("company_id", companyID)) return fiber.NewError(fiber.StatusBadRequest, "invalid company id") } - role := c.Locals("role").(domain.Role) page := c.QueryInt("page", 1) pageSize := c.QueryInt("page_size", 10) @@ -657,7 +656,7 @@ func (h *Handler) GetAllTenantBets(c *fiber.Ctx) error { } var isShopBet domain.ValidBool isShopBetQuery := c.Query("is_shop") - if isShopBetQuery != "" && role == domain.RoleSuperAdmin { + if isShopBetQuery != "" { isShopBetParse, err := strconv.ParseBool(isShopBetQuery) if err != nil { h.mongoLoggerSvc.Info("failed to parse is_shop_bet", @@ -775,6 +774,8 @@ func (h *Handler) GetAllTenantBets(c *fiber.Ctx) error { // @Failure 500 {object} response.APIResponse // @Router /api/v1/sport/bet/{id} [get] func (h *Handler) GetBetByID(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + betID := c.Params("id") id, err := strconv.ParseInt(betID, 10, 64) if err != nil { @@ -800,6 +801,15 @@ func (h *Handler) GetBetByID(c *fiber.Ctx) error { res := domain.ConvertBet(bet) + if companyID.Valid && bet.CompanyID != companyID.Value { + h.mongoLoggerSvc.Warn("Warn - Company is trying to access another companies bet", + zap.Int64("betID", id), + zap.Int("status_code", fiber.StatusNotFound), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusNotFound, "Failed to retrieve bet") + } // h.mongoLoggerSvc.Info("Bet retrieved successfully", // zap.Int64("betID", id), // zap.Int("status_code", fiber.StatusOK), diff --git a/internal/web_server/handlers/branch_handler.go b/internal/web_server/handlers/branch_handler.go index d187b21..94a91c6 100644 --- a/internal/web_server/handlers/branch_handler.go +++ b/internal/web_server/handlers/branch_handler.go @@ -611,6 +611,8 @@ func (h *Handler) GetAllBranches(c *fiber.Ctx) error { // @Failure 500 {object} response.APIResponse // @Router /api/v1/search/branch [get] func (h *Handler) SearchBranch(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + // Get search query from request searchQuery := c.Query("q") if searchQuery == "" { @@ -622,7 +624,7 @@ func (h *Handler) SearchBranch(c *fiber.Ctx) error { } // Call the service to search for branches - branches, err := h.branchSvc.SearchBranchByName(c.Context(), searchQuery) + branches, err := h.branchSvc.SearchBranchByName(c.Context(), searchQuery, companyID) if err != nil { h.mongoLoggerSvc.Info("Failed to search branches", zap.String("query", searchQuery), diff --git a/internal/web_server/handlers/event_handler.go b/internal/web_server/handlers/event_handler.go index 27fa0cd..bad2fdf 100644 --- a/internal/web_server/handlers/event_handler.go +++ b/internal/web_server/handlers/event_handler.go @@ -270,22 +270,22 @@ func (h *Handler) GetTenantUpcomingEvents(c *fiber.Ctx) error { Valid: searchQuery != "", } - // firstStartTimeQuery := c.Query("first_start_time") - // var firstStartTime domain.ValidTime - // if firstStartTimeQuery != "" { - // firstStartTimeParsed, err := time.Parse(time.RFC3339, firstStartTimeQuery) - // if err != nil { - // h.BadRequestLogger().Info("invalid start_time format", - // zap.String("first_start_time", firstStartTimeQuery), - // zap.Error(err), - // ) - // return fiber.NewError(fiber.StatusBadRequest, "Invalid start_time format") - // } - // firstStartTime = domain.ValidTime{ - // Value: firstStartTimeParsed, - // Valid: true, - // } - // } + firstStartTimeQuery := c.Query("first_start_time") + var firstStartTime domain.ValidTime + if firstStartTimeQuery != "" { + firstStartTimeParsed, err := time.Parse(time.RFC3339, firstStartTimeQuery) + if err != nil { + h.BadRequestLogger().Info("invalid start_time format", + zap.String("first_start_time", firstStartTimeQuery), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid start_time format") + } + firstStartTime = domain.ValidTime{ + Value: firstStartTimeParsed, + Valid: true, + } + } lastStartTimeQuery := c.Query("last_start_time") var lastStartTime domain.ValidTime @@ -330,18 +330,15 @@ func (h *Handler) GetTenantUpcomingEvents(c *fiber.Ctx) error { events, total, err := h.eventSvc.GetEventsWithSettings( c.Context(), companyID.Value, domain.EventFilter{ - SportID: sportID, - LeagueID: leagueID, - Query: searchString, - FirstStartTime: domain.ValidTime{ - Value: time.Now(), - Valid: true, - }, - LastStartTime: lastStartTime, - Limit: limit, - Offset: offset, - CountryCode: countryCode, - Featured: isFeatured, + SportID: sportID, + LeagueID: leagueID, + Query: searchString, + FirstStartTime: firstStartTime, + LastStartTime: lastStartTime, + Limit: limit, + Offset: offset, + CountryCode: countryCode, + Featured: isFeatured, Status: domain.ValidEventStatus{ Value: domain.STATUS_PENDING, Valid: true, diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index df3eb90..8c1a58a 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -348,8 +348,8 @@ func (a *App) initAppRoutes() { tenant.Patch("/sport/bet/:id", a.authMiddleware, h.UpdateCashOut) tenant.Delete("/sport/bet/:id", a.authMiddleware, h.DeleteTenantBet) + groupV1.Get("/sport/bet/:id", a.authMiddleware, a.CompanyOnly, h.GetBetByID) groupV1.Get("/sport/bet", a.authMiddleware, a.SuperAdminOnly, h.GetAllBet) - groupV1.Get("/sport/bet/:id", a.authMiddleware, a.SuperAdminOnly, h.GetBetByID) groupV1.Delete("/sport/bet/:id", a.authMiddleware, a.SuperAdminOnly, h.DeleteBet) tenant.Post("/sport/random/bet", a.authMiddleware, h.RandomBet) diff --git a/makefile b/makefile index 83e8ba1..ac8ec7b 100644 --- a/makefile +++ b/makefile @@ -79,7 +79,7 @@ logs: @mkdir -p logs db-up: | logs @mkdir -p logs - @docker compose up -d postgres migrate mongo redis kafka + @docker compose up -d postgres migrate mongo @docker logs fortunebet-backend-postgres-1 > logs/postgres.log 2>&1 & .PHONY: db-down db-down: From 0bab186a8fd70d27df555112ee0c9a41cd0a72a8 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 8 Oct 2025 12:54:11 +0300 Subject: [PATCH 10/23] fix: uncommenting out crons --- internal/web_server/cron.go | 132 ++++++++++++++++++------------------ 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index bcdce17..6119670 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -28,75 +28,75 @@ 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 { - job.task() + // job.task() if _, err := c.AddFunc(job.spec, job.task); err != nil { mongoLogger.Error("Failed to schedule data fetching cron job", zap.Error(err), From 3dfa1255b02223cb764e3bd7e40e74388ffe4bea Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Fri, 10 Oct 2025 14:59:19 +0300 Subject: [PATCH 11/23] 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

+
    +
  • %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) + + 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

    +
      +%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(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

    -
      -
    • %d Ended Events
    • -
    • %d Total Bets
    • -
    -

    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

    -
      -%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(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) From 160e1f6ed5043a07df3223c84577e146df153356 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Fri, 10 Oct 2025 17:21:27 +0300 Subject: [PATCH 12/23] chore: uncommenting out crons --- internal/web_server/cron.go | 104 +++++++++++++++++----------------- internal/web_server/routes.go | 2 +- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 34a8b9e..13019f3 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -28,58 +28,58 @@ 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 * * * *", // 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() { diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index a3d1a36..23ffd66 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -342,7 +342,7 @@ func (a *App) initAppRoutes() { // Ticket Routes tenant.Post("/ticket", h.CreateTenantTicket) tenant.Get("/ticket", h.GetAllTenantTickets) - tenant.Get("/ticket/:id", h.GetTenantBetByID) + tenant.Get("/ticket/:id", h.GetTenantTicketByID) // Bet Routes tenant.Post("/sport/bet", a.authMiddleware, h.CreateBet) From c829cb166ef51c65e4f81aef24533c52c51680df Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Fri, 10 Oct 2025 18:30:21 +0300 Subject: [PATCH 13/23] fix: changed pgdump for backup to data only --- makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/makefile b/makefile index ac8ec7b..ba69745 100644 --- a/makefile +++ b/makefile @@ -46,7 +46,7 @@ postgres: .PHONY: backup backup: @mkdir -p backup - @docker exec -t fortunebet-backend-postgres-1 pg_dumpall -c -U root | gzip > backup/dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql.gz + @docker exec -t fortunebet-backend-postgres-1 pg_dumpall --data-only -U root | gzip > backup/dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql.gz restore: @echo "Restoring latest backup..." From 4395c9fd6ab302f539fc36c78e06e9e7940b70ae Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Fri, 10 Oct 2025 18:42:46 +0300 Subject: [PATCH 15/23] fix: pgdump fix --- makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/makefile b/makefile index ba69745..b846a6d 100644 --- a/makefile +++ b/makefile @@ -46,7 +46,7 @@ postgres: .PHONY: backup backup: @mkdir -p backup - @docker exec -t fortunebet-backend-postgres-1 pg_dumpall --data-only -U root | gzip > backup/dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql.gz + @docker exec -t fortunebet-backend-postgres-1 pg_dump -U root --data-only gh | gzip > backup/dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql.gz restore: @echo "Restoring latest backup..." From ce863cbe0eb0f3e76813a73cc40b94abcf1c07c4 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Fri, 10 Oct 2025 19:07:37 +0300 Subject: [PATCH 19/23] fix: added exclude-table --- makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/makefile b/makefile index b846a6d..ee00452 100644 --- a/makefile +++ b/makefile @@ -46,7 +46,7 @@ postgres: .PHONY: backup backup: @mkdir -p backup - @docker exec -t fortunebet-backend-postgres-1 pg_dump -U root --data-only gh | gzip > backup/dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql.gz + @docker exec -t fortunebet-backend-postgres-1 pg_dump -U root --data-only --exclude-table=schema_migrations gh | gzip > backup/dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql.gz restore: @echo "Restoring latest backup..." From b5932df20661e43f212689d7d3179cc151c681d9 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Sat, 11 Oct 2025 11:32:27 +0300 Subject: [PATCH 21/23] - --- internal/services/bet/service.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index d6ff26a..ab1f033 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -1236,6 +1236,8 @@ func (s *Service) ProcessBetCashback(ctx context.Context) error { zap.Int64("userID", bet.UserID), zap.Error(err)) } + + // Add a notification here } return nil } From a9025ca8442f907bd5dffaa69e7f3890e6c38778 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Tue, 14 Oct 2025 04:39:27 +0300 Subject: [PATCH 22/23] feat: Add number_of_outcomes to odds market queries and models - Updated SQL queries to include number_of_outcomes in GetAllOdds, GetOddByID, GetOddsByEventID, and GetOddsByMarketID. - Modified data structures in domain and repository layers to accommodate number_of_outcomes. - Enhanced event models to track total odd outcomes. - Introduced new SQL scripts for development data seeding. --- db/dev_data/betfidel_data.sql | 221 ++++++++++++++++++ db/migrations/000001_fortune.up.sql | 24 +- db/query/events.sql | 18 +- db/query/odds.sql | 9 +- ...esync.sql => fix_autoincrement_desync.sql} | 0 gen/db/events.sql.go | 31 ++- gen/db/models.go | 79 ++++--- gen/db/odds.sql.go | 151 ++++++------ internal/domain/event.go | 8 + internal/domain/odds.go | 136 +++++------ internal/repository/event.go | 6 + internal/repository/odds.go | 63 ++--- internal/services/bet/service.go | 2 - internal/services/odds/service.go | 21 +- internal/services/result/service.go | 2 - internal/web_server/cron.go | 2 +- internal/web_server/handlers/cashier.go | 7 +- internal/web_server/handlers/event_handler.go | 5 + makefile | 14 ++ 19 files changed, 570 insertions(+), 229 deletions(-) create mode 100644 db/dev_data/betfidel_data.sql rename db/scripts/{003_fix_autoincrement_desync.sql => fix_autoincrement_desync.sql} (100%) diff --git a/db/dev_data/betfidel_data.sql b/db/dev_data/betfidel_data.sql new file mode 100644 index 0000000..29bc5e3 --- /dev/null +++ b/db/dev_data/betfidel_data.sql @@ -0,0 +1,221 @@ +CREATE EXTENSION IF NOT EXISTS pgcrypto; +DO $$ +DECLARE _admin_id bigint; +_manager_id bigint; +_company_wallet_id bigint; +_company_id bigint; +_branch_id bigint; +_branch_wallet_id bigint; +_cashier_id bigint; +BEGIN +INSERT INTO users ( + first_name, + last_name, + email, + phone_number, + password, + role, + email_verified, + phone_verified, + created_at, + updated_at, + suspended + ) +VALUES ( + 'Admin', + 'BetFidel', + 'admin.betfidel@gmail.com', + NULL, + crypt('password@123', gen_salt('bf'))::bytea, + 'admin', + TRUE, + FALSE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + FALSE + ) ON CONFLICT (email) DO +UPDATE +SET updated_at = EXCLUDED.updated_at +RETURNING id INTO STRICT _admin_id; +INSERT INTO users ( + first_name, + last_name, + email, + phone_number, + password, + role, + email_verified, + phone_verified, + created_at, + updated_at, + suspended, + company_id + ) +VALUES ( + 'Manager', + 'BetFidel', + 'manager.betfidel@gmail.com', + NULL, + crypt('password@123', gen_salt('bf'))::bytea, + 'branch_manager', + TRUE, + FALSE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + FALSE, + _company_id + ) ON CONFLICT (email) DO +UPDATE +SET updated_at = EXCLUDED.updated_at +RETURNING id INTO STRICT _manager_id; +INSERT INTO wallets ( + balance, + is_withdraw, + is_bettable, + is_transferable, + user_id, + type, + currency, + is_active, + created_at, + updated_at + ) +VALUES ( + 10000, + TRUE, + TRUE, + TRUE, + _admin_id, + 'company_wallet', + 'ETB', + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ) ON CONFLICT (user_id, type) DO +UPDATE +SET updated_at = EXCLUDED.updated_at +RETURNING id INTO STRICT _company_wallet_id; +INSERT INTO companies ( + name, + slug, + admin_id, + wallet_id, + deducted_percentage, + is_active, + created_at, + updated_at + ) +VALUES ( + 'FidelBet', + 'betfidel', + _admin_id, + _company_wallet_id, + 0.15, + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ) ON CONFLICT (slug) DO +UPDATE +SET updated_at = EXCLUDED.updated_at +RETURNING id INTO STRICT _company_id; +UPDATE users +SET company_id = _company_id +WHERE id = _admin_id; +INSERT INTO wallets ( + balance, + is_withdraw, + is_bettable, + is_transferable, + user_id, + type, + currency, + is_active, + created_at, + updated_at + ) +VALUES ( + 10000, + TRUE, + TRUE, + TRUE, + _admin_id, + 'branch_wallet', + 'ETB', + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ) ON CONFLICT (user_id, type) DO +UPDATE +SET updated_at = EXCLUDED.updated_at +RETURNING id INTO STRICT _branch_wallet_id; +INSERT INTO branches ( + name, + location, + wallet_id, + branch_manager_id, + company_id, + is_self_owned, + profit_percent, + is_active, + created_at, + updated_at + ) +VALUES ( + 'Test Branch', + 'addis_ababa', + _branch_wallet_id, + _manager_id, + _company_id, + TRUE, + 0.10, + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ) ON CONFLICT (wallet_id) DO +UPDATE +SET updated_at = EXCLUDED.updated_at +RETURNING id INTO STRICT _branch_id; +INSERT INTO users ( + first_name, + last_name, + email, + phone_number, + password, + role, + email_verified, + phone_verified, + created_at, + updated_at, + suspended, + company_id + ) +VALUES ( + 'Cashier', + 'BetFidel', + 'cashier.betfidel@gmail.com', + NULL, + crypt('password@123', gen_salt('bf'))::bytea, + 'cashier', + TRUE, + FALSE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + FALSE, + _company_id + ) ON CONFLICT (email) DO +UPDATE +SET updated_at = EXCLUDED.updated_at +RETURNING id INTO STRICT _cashier_id; +INSERT INTO branch_cashiers (user_id, branch_id) +VALUES (_cashier_id, _branch_id); +RAISE NOTICE 'BETFIDEL_DEV_DATA (Admin ID: %, Company Wallet ID: %, Company ID: %)', +_admin_id, +_company_wallet_id, +_company_id; +RAISE NOTICE 'BETFIDEL_DEV_DATA (Branch ID: %, Branch Wallet ID: %, Manager ID: %)', +_branch_id, +_branch_wallet_id, +_manager_id; +RAISE NOTICE 'BETFIDEL_DEV_DATA (Cashier ID: %)', +_cashier_id; +END $$; \ No newline at end of file diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index e3c22c0..4b636ec 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -354,6 +354,7 @@ CREATE TABLE odds_market ( market_category TEXT NOT NULL, market_id BIGINT NOT NULL, raw_odds JSONB NOT NULL, + number_of_outcomes BIGINT NOT NULL, default_is_active BOOLEAN NOT NULL DEFAULT true, fetched_at TIMESTAMP DEFAULT now (), expires_at TIMESTAMP NOT NULL, @@ -693,15 +694,29 @@ SELECT e.*, e.default_winning_upper_limit ) AS winning_upper_limit, ces.updated_at as company_updated_at, - l.country_code as league_cc + l.country_code as league_cc, + COALESCE(om.total_outcomes, 0) AS total_outcomes FROM events e LEFT JOIN company_event_settings ces ON e.id = ces.event_id - JOIN leagues l ON l.id = e.league_id; + JOIN leagues l ON l.id = e.league_id + LEFT JOIN ( + SELECT event_id, + SUM(number_of_outcomes) AS total_outcomes + FROM odds_market + GROUP BY event_id + ) om ON om.event_id = e.id; CREATE VIEW event_with_country AS SELECT events.*, - leagues.country_code as league_cc + leagues.country_code as league_cc, + COALESCE(om.total_outcomes, 0) AS total_outcomes FROM events - LEFT JOIN leagues ON leagues.id = events.league_id; + LEFT JOIN leagues ON leagues.id = events.league_id + LEFT JOIN ( + SELECT event_id, + SUM(number_of_outcomes) AS total_outcomes + FROM odds_market + GROUP BY event_id + ) om ON om.event_id = events.id; CREATE VIEW odds_market_with_settings AS SELECT o.id, o.event_id, @@ -709,6 +724,7 @@ SELECT o.id, o.market_name, o.market_category, o.market_id, + o.number_of_outcomes, o.default_is_active, o.fetched_at, o.expires_at, diff --git a/db/query/events.sql b/db/query/events.sql index 84b10b8..d62b380 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -218,11 +218,18 @@ SELECT e.*, e.default_winning_upper_limit ) AS winning_upper_limit, ces.updated_at, - l.country_code as league_cc + l.country_code as league_cc, + COALESCE(om.total_outcomes, 0) AS total_outcomes FROM events e LEFT JOIN company_event_settings ces ON e.id = ces.event_id AND ces.company_id = $1 JOIN leagues l ON l.id = e.league_id + LEFT JOIN ( + SELECT event_id, + SUM(number_of_outcomes) AS total_outcomes + FROM odds_market + GROUP BY event_id + ) om ON om.event_id = e.id WHERE ( is_live = sqlc.narg('is_live') OR sqlc.narg('is_live') IS NULL @@ -292,11 +299,18 @@ SELECT e.*, e.default_winning_upper_limit ) AS winning_upper_limit, ces.updated_at, - l.country_code as league_cc + l.country_code as league_cc, + COALESCE(om.total_outcomes, 0) AS total_outcomes FROM events e LEFT JOIN company_event_settings ces ON e.id = ces.event_id AND ces.company_id = $2 JOIN leagues l ON l.id = e.league_id + LEFT JOIN ( + SELECT event_id, + SUM(number_of_outcomes) AS total_outcomes + FROM odds_market + GROUP BY event_id + ) om ON om.event_id = e.id WHERE e.id = $1 LIMIT 1; -- name: GetSportAndLeagueIDs :one diff --git a/db/query/odds.sql b/db/query/odds.sql index 8950868..6979426 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -5,6 +5,7 @@ INSERT INTO odds_market ( market_name, market_category, market_id, + number_of_outcomes, raw_odds, fetched_at, expires_at @@ -17,13 +18,15 @@ VALUES ( $5, $6, $7, - $8 + $8, + $9 ) ON CONFLICT (event_id, market_id) DO UPDATE SET market_type = EXCLUDED.market_type, market_name = EXCLUDED.market_name, market_category = EXCLUDED.market_category, raw_odds = EXCLUDED.raw_odds, + number_of_outcomes = EXCLUDED.number_of_outcomes, fetched_at = EXCLUDED.fetched_at, expires_at = EXCLUDED.expires_at; -- name: SaveOddSettings :exec @@ -48,6 +51,7 @@ SELECT o.id, o.market_name, o.market_category, o.market_id, + o.number_of_outcomes, o.default_is_active, o.fetched_at, o.expires_at, @@ -75,6 +79,7 @@ SELECT o.id, o.market_name, o.market_category, o.market_id, + o.number_of_outcomes, o.default_is_active, o.fetched_at, o.expires_at, @@ -94,6 +99,7 @@ SELECT o.id, o.market_name, o.market_category, o.market_id, + o.number_of_outcomes, o.default_is_active, o.fetched_at, o.expires_at, @@ -129,6 +135,7 @@ SELECT o.id, o.market_name, o.market_category, o.market_id, + o.number_of_outcomes, o.default_is_active, o.fetched_at, o.expires_at, diff --git a/db/scripts/003_fix_autoincrement_desync.sql b/db/scripts/fix_autoincrement_desync.sql similarity index 100% rename from db/scripts/003_fix_autoincrement_desync.sql rename to db/scripts/fix_autoincrement_desync.sql diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index 380793e..767a4e2 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, updated_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, total_outcomes FROM event_with_country WHERE ( is_live = $1 @@ -129,6 +129,7 @@ func (q *Queries) GetAllEvents(ctx context.Context, arg GetAllEventsParams) ([]E &i.DefaultWinningUpperLimit, &i.IsMonitored, &i.LeagueCc, + &i.TotalOutcomes, ); err != nil { return nil, err } @@ -141,7 +142,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, updated_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, total_outcomes FROM event_with_country WHERE id = $1 LIMIT 1 @@ -179,12 +180,13 @@ func (q *Queries) GetEventByID(ctx context.Context, id int64) (EventWithCountry, &i.DefaultWinningUpperLimit, &i.IsMonitored, &i.LeagueCc, + &i.TotalOutcomes, ) return i, err } 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, updated_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, total_outcomes FROM event_with_country WHERE source_event_id = $1 AND source = $2 @@ -227,6 +229,7 @@ func (q *Queries) GetEventBySourceID(ctx context.Context, arg GetEventBySourceID &i.DefaultWinningUpperLimit, &i.IsMonitored, &i.LeagueCc, + &i.TotalOutcomes, ) return i, err } @@ -241,11 +244,18 @@ SELECT e.id, e.source_event_id, e.sport_id, e.match_name, e.home_team, e.away_te e.default_winning_upper_limit ) AS winning_upper_limit, ces.updated_at, - l.country_code as league_cc + l.country_code as league_cc, + COALESCE(om.total_outcomes, 0) AS total_outcomes FROM events e LEFT JOIN company_event_settings ces ON e.id = ces.event_id AND ces.company_id = $2 JOIN leagues l ON l.id = e.league_id + LEFT JOIN ( + SELECT event_id, + SUM(number_of_outcomes) AS total_outcomes + FROM odds_market + GROUP BY event_id + ) om ON om.event_id = e.id WHERE e.id = $1 LIMIT 1 ` @@ -289,6 +299,7 @@ type GetEventWithSettingByIDRow struct { WinningUpperLimit int64 `json:"winning_upper_limit"` UpdatedAt_2 pgtype.Timestamp `json:"updated_at_2"` LeagueCc pgtype.Text `json:"league_cc"` + TotalOutcomes int64 `json:"total_outcomes"` } func (q *Queries) GetEventWithSettingByID(ctx context.Context, arg GetEventWithSettingByIDParams) (GetEventWithSettingByIDRow, error) { @@ -328,6 +339,7 @@ func (q *Queries) GetEventWithSettingByID(ctx context.Context, arg GetEventWithS &i.WinningUpperLimit, &i.UpdatedAt_2, &i.LeagueCc, + &i.TotalOutcomes, ) return i, err } @@ -342,11 +354,18 @@ SELECT e.id, e.source_event_id, e.sport_id, e.match_name, e.home_team, e.away_te e.default_winning_upper_limit ) AS winning_upper_limit, ces.updated_at, - l.country_code as league_cc + l.country_code as league_cc, + COALESCE(om.total_outcomes, 0) AS total_outcomes FROM events e LEFT JOIN company_event_settings ces ON e.id = ces.event_id AND ces.company_id = $1 JOIN leagues l ON l.id = e.league_id + LEFT JOIN ( + SELECT event_id, + SUM(number_of_outcomes) AS total_outcomes + FROM odds_market + GROUP BY event_id + ) om ON om.event_id = e.id WHERE ( is_live = $2 OR $2 IS NULL @@ -449,6 +468,7 @@ type GetEventsWithSettingsRow struct { WinningUpperLimit int64 `json:"winning_upper_limit"` UpdatedAt_2 pgtype.Timestamp `json:"updated_at_2"` LeagueCc pgtype.Text `json:"league_cc"` + TotalOutcomes int64 `json:"total_outcomes"` } func (q *Queries) GetEventsWithSettings(ctx context.Context, arg GetEventsWithSettingsParams) ([]GetEventsWithSettingsRow, error) { @@ -509,6 +529,7 @@ func (q *Queries) GetEventsWithSettings(ctx context.Context, arg GetEventsWithSe &i.WinningUpperLimit, &i.UpdatedAt_2, &i.LeagueCc, + &i.TotalOutcomes, ); err != nil { return nil, err } diff --git a/gen/db/models.go b/gen/db/models.go index 62182e5..f2cbd47 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -376,6 +376,7 @@ type EventWithCountry struct { DefaultWinningUpperLimit int64 `json:"default_winning_upper_limit"` IsMonitored bool `json:"is_monitored"` LeagueCc pgtype.Text `json:"league_cc"` + TotalOutcomes int64 `json:"total_outcomes"` } type EventWithSetting struct { @@ -412,6 +413,7 @@ type EventWithSetting struct { WinningUpperLimit int64 `json:"winning_upper_limit"` CompanyUpdatedAt pgtype.Timestamp `json:"company_updated_at"` LeagueCc pgtype.Text `json:"league_cc"` + TotalOutcomes int64 `json:"total_outcomes"` } type ExchangeRate struct { @@ -502,49 +504,52 @@ type OddHistory struct { } type OddsMarket struct { - ID int64 `json:"id"` - EventID int64 `json:"event_id"` - MarketType string `json:"market_type"` - MarketName string `json:"market_name"` - MarketCategory string `json:"market_category"` - MarketID int64 `json:"market_id"` - RawOdds []byte `json:"raw_odds"` - DefaultIsActive bool `json:"default_is_active"` - FetchedAt pgtype.Timestamp `json:"fetched_at"` - ExpiresAt pgtype.Timestamp `json:"expires_at"` + ID int64 `json:"id"` + EventID int64 `json:"event_id"` + MarketType string `json:"market_type"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID int64 `json:"market_id"` + RawOdds []byte `json:"raw_odds"` + NumberOfOutcomes int64 `json:"number_of_outcomes"` + DefaultIsActive bool `json:"default_is_active"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` } type OddsMarketWithEvent struct { - ID int64 `json:"id"` - EventID int64 `json:"event_id"` - MarketType string `json:"market_type"` - MarketName string `json:"market_name"` - MarketCategory string `json:"market_category"` - MarketID int64 `json:"market_id"` - RawOdds []byte `json:"raw_odds"` - DefaultIsActive bool `json:"default_is_active"` - FetchedAt pgtype.Timestamp `json:"fetched_at"` - ExpiresAt pgtype.Timestamp `json:"expires_at"` - IsMonitored bool `json:"is_monitored"` - IsLive bool `json:"is_live"` - Status string `json:"status"` - Source string `json:"source"` + ID int64 `json:"id"` + EventID int64 `json:"event_id"` + MarketType string `json:"market_type"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID int64 `json:"market_id"` + RawOdds []byte `json:"raw_odds"` + NumberOfOutcomes int64 `json:"number_of_outcomes"` + DefaultIsActive bool `json:"default_is_active"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + IsMonitored bool `json:"is_monitored"` + IsLive bool `json:"is_live"` + Status string `json:"status"` + Source string `json:"source"` } type OddsMarketWithSetting struct { - ID int64 `json:"id"` - EventID int64 `json:"event_id"` - MarketType string `json:"market_type"` - MarketName string `json:"market_name"` - MarketCategory string `json:"market_category"` - MarketID int64 `json:"market_id"` - DefaultIsActive bool `json:"default_is_active"` - FetchedAt pgtype.Timestamp `json:"fetched_at"` - ExpiresAt pgtype.Timestamp `json:"expires_at"` - CompanyID pgtype.Int8 `json:"company_id"` - IsActive bool `json:"is_active"` - RawOdds []byte `json:"raw_odds"` - UpdatedAt pgtype.Timestamp `json:"updated_at"` + ID int64 `json:"id"` + EventID int64 `json:"event_id"` + MarketType string `json:"market_type"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID int64 `json:"market_id"` + NumberOfOutcomes int64 `json:"number_of_outcomes"` + DefaultIsActive bool `json:"default_is_active"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + CompanyID pgtype.Int8 `json:"company_id"` + IsActive bool `json:"is_active"` + RawOdds []byte `json:"raw_odds"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` } type Otp struct { diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index e7c687e..632f03c 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -48,7 +48,7 @@ func (q *Queries) DeleteOddsForEvent(ctx context.Context, eventID int64) error { } const GetAllOdds = `-- name: GetAllOdds :many -SELECT id, event_id, market_type, market_name, market_category, market_id, raw_odds, default_is_active, fetched_at, expires_at, is_monitored, is_live, status, source +SELECT id, event_id, market_type, market_name, market_category, market_id, raw_odds, number_of_outcomes, default_is_active, fetched_at, expires_at, is_monitored, is_live, status, source FROM odds_market_with_event LIMIT $2 OFFSET $1 ` @@ -75,6 +75,7 @@ func (q *Queries) GetAllOdds(ctx context.Context, arg GetAllOddsParams) ([]OddsM &i.MarketCategory, &i.MarketID, &i.RawOdds, + &i.NumberOfOutcomes, &i.DefaultIsActive, &i.FetchedAt, &i.ExpiresAt, @@ -100,6 +101,7 @@ SELECT o.id, o.market_name, o.market_category, o.market_id, + o.number_of_outcomes, o.default_is_active, o.fetched_at, o.expires_at, @@ -120,19 +122,20 @@ type GetAllOddsWithSettingsParams struct { } type GetAllOddsWithSettingsRow struct { - ID int64 `json:"id"` - EventID int64 `json:"event_id"` - MarketType string `json:"market_type"` - MarketName string `json:"market_name"` - MarketCategory string `json:"market_category"` - MarketID int64 `json:"market_id"` - DefaultIsActive bool `json:"default_is_active"` - FetchedAt pgtype.Timestamp `json:"fetched_at"` - ExpiresAt pgtype.Timestamp `json:"expires_at"` - CompanyID pgtype.Int8 `json:"company_id"` - IsActive bool `json:"is_active"` - RawOdds []byte `json:"raw_odds"` - UpdatedAt pgtype.Timestamp `json:"updated_at"` + ID int64 `json:"id"` + EventID int64 `json:"event_id"` + MarketType string `json:"market_type"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID int64 `json:"market_id"` + NumberOfOutcomes int64 `json:"number_of_outcomes"` + DefaultIsActive bool `json:"default_is_active"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + CompanyID pgtype.Int8 `json:"company_id"` + IsActive bool `json:"is_active"` + RawOdds []byte `json:"raw_odds"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` } func (q *Queries) GetAllOddsWithSettings(ctx context.Context, arg GetAllOddsWithSettingsParams) ([]GetAllOddsWithSettingsRow, error) { @@ -151,6 +154,7 @@ func (q *Queries) GetAllOddsWithSettings(ctx context.Context, arg GetAllOddsWith &i.MarketName, &i.MarketCategory, &i.MarketID, + &i.NumberOfOutcomes, &i.DefaultIsActive, &i.FetchedAt, &i.ExpiresAt, @@ -170,7 +174,7 @@ func (q *Queries) GetAllOddsWithSettings(ctx context.Context, arg GetAllOddsWith } const GetOddByID = `-- name: GetOddByID :one -SELECT id, event_id, market_type, market_name, market_category, market_id, raw_odds, default_is_active, fetched_at, expires_at, is_monitored, is_live, status, source +SELECT id, event_id, market_type, market_name, market_category, market_id, raw_odds, number_of_outcomes, default_is_active, fetched_at, expires_at, is_monitored, is_live, status, source FROM odds_market_with_event WHERE id = $1 ` @@ -186,6 +190,7 @@ func (q *Queries) GetOddByID(ctx context.Context, id int64) (OddsMarketWithEvent &i.MarketCategory, &i.MarketID, &i.RawOdds, + &i.NumberOfOutcomes, &i.DefaultIsActive, &i.FetchedAt, &i.ExpiresAt, @@ -198,7 +203,7 @@ func (q *Queries) GetOddByID(ctx context.Context, id int64) (OddsMarketWithEvent } const GetOddsByEventID = `-- name: GetOddsByEventID :many -SELECT id, event_id, market_type, market_name, market_category, market_id, raw_odds, default_is_active, fetched_at, expires_at, is_monitored, is_live, status, source +SELECT id, event_id, market_type, market_name, market_category, market_id, raw_odds, number_of_outcomes, default_is_active, fetched_at, expires_at, is_monitored, is_live, status, source FROM odds_market_with_event WHERE event_id = $1 AND ( @@ -249,6 +254,7 @@ func (q *Queries) GetOddsByEventID(ctx context.Context, arg GetOddsByEventIDPara &i.MarketCategory, &i.MarketID, &i.RawOdds, + &i.NumberOfOutcomes, &i.DefaultIsActive, &i.FetchedAt, &i.ExpiresAt, @@ -268,7 +274,7 @@ func (q *Queries) GetOddsByEventID(ctx context.Context, arg GetOddsByEventIDPara } const GetOddsByMarketID = `-- name: GetOddsByMarketID :one -SELECT id, event_id, market_type, market_name, market_category, market_id, raw_odds, default_is_active, fetched_at, expires_at, is_monitored, is_live, status, source +SELECT id, event_id, market_type, market_name, market_category, market_id, raw_odds, number_of_outcomes, default_is_active, fetched_at, expires_at, is_monitored, is_live, status, source FROM odds_market_with_event WHERE market_id = $1 AND event_id = $2 @@ -290,6 +296,7 @@ func (q *Queries) GetOddsByMarketID(ctx context.Context, arg GetOddsByMarketIDPa &i.MarketCategory, &i.MarketID, &i.RawOdds, + &i.NumberOfOutcomes, &i.DefaultIsActive, &i.FetchedAt, &i.ExpiresAt, @@ -308,6 +315,7 @@ SELECT o.id, o.market_name, o.market_category, o.market_id, + o.number_of_outcomes, o.default_is_active, o.fetched_at, o.expires_at, @@ -330,19 +338,20 @@ type GetOddsWithSettingsByEventIDParams struct { } type GetOddsWithSettingsByEventIDRow struct { - ID int64 `json:"id"` - EventID int64 `json:"event_id"` - MarketType string `json:"market_type"` - MarketName string `json:"market_name"` - MarketCategory string `json:"market_category"` - MarketID int64 `json:"market_id"` - DefaultIsActive bool `json:"default_is_active"` - FetchedAt pgtype.Timestamp `json:"fetched_at"` - ExpiresAt pgtype.Timestamp `json:"expires_at"` - CompanyID pgtype.Int8 `json:"company_id"` - IsActive bool `json:"is_active"` - RawOdds []byte `json:"raw_odds"` - UpdatedAt pgtype.Timestamp `json:"updated_at"` + ID int64 `json:"id"` + EventID int64 `json:"event_id"` + MarketType string `json:"market_type"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID int64 `json:"market_id"` + NumberOfOutcomes int64 `json:"number_of_outcomes"` + DefaultIsActive bool `json:"default_is_active"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + CompanyID pgtype.Int8 `json:"company_id"` + IsActive bool `json:"is_active"` + RawOdds []byte `json:"raw_odds"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` } func (q *Queries) GetOddsWithSettingsByEventID(ctx context.Context, arg GetOddsWithSettingsByEventIDParams) ([]GetOddsWithSettingsByEventIDRow, error) { @@ -366,6 +375,7 @@ func (q *Queries) GetOddsWithSettingsByEventID(ctx context.Context, arg GetOddsW &i.MarketName, &i.MarketCategory, &i.MarketID, + &i.NumberOfOutcomes, &i.DefaultIsActive, &i.FetchedAt, &i.ExpiresAt, @@ -391,6 +401,7 @@ SELECT o.id, o.market_name, o.market_category, o.market_id, + o.number_of_outcomes, o.default_is_active, o.fetched_at, o.expires_at, @@ -410,19 +421,20 @@ type GetOddsWithSettingsByIDParams struct { } type GetOddsWithSettingsByIDRow struct { - ID int64 `json:"id"` - EventID int64 `json:"event_id"` - MarketType string `json:"market_type"` - MarketName string `json:"market_name"` - MarketCategory string `json:"market_category"` - MarketID int64 `json:"market_id"` - DefaultIsActive bool `json:"default_is_active"` - FetchedAt pgtype.Timestamp `json:"fetched_at"` - ExpiresAt pgtype.Timestamp `json:"expires_at"` - CompanyID pgtype.Int8 `json:"company_id"` - IsActive bool `json:"is_active"` - RawOdds []byte `json:"raw_odds"` - UpdatedAt pgtype.Timestamp `json:"updated_at"` + ID int64 `json:"id"` + EventID int64 `json:"event_id"` + MarketType string `json:"market_type"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID int64 `json:"market_id"` + NumberOfOutcomes int64 `json:"number_of_outcomes"` + DefaultIsActive bool `json:"default_is_active"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + CompanyID pgtype.Int8 `json:"company_id"` + IsActive bool `json:"is_active"` + RawOdds []byte `json:"raw_odds"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` } func (q *Queries) GetOddsWithSettingsByID(ctx context.Context, arg GetOddsWithSettingsByIDParams) (GetOddsWithSettingsByIDRow, error) { @@ -435,6 +447,7 @@ func (q *Queries) GetOddsWithSettingsByID(ctx context.Context, arg GetOddsWithSe &i.MarketName, &i.MarketCategory, &i.MarketID, + &i.NumberOfOutcomes, &i.DefaultIsActive, &i.FetchedAt, &i.ExpiresAt, @@ -453,6 +466,7 @@ SELECT o.id, o.market_name, o.market_category, o.market_id, + o.number_of_outcomes, o.default_is_active, o.fetched_at, o.expires_at, @@ -474,19 +488,20 @@ type GetOddsWithSettingsByMarketIDParams struct { } type GetOddsWithSettingsByMarketIDRow struct { - ID int64 `json:"id"` - EventID int64 `json:"event_id"` - MarketType string `json:"market_type"` - MarketName string `json:"market_name"` - MarketCategory string `json:"market_category"` - MarketID int64 `json:"market_id"` - DefaultIsActive bool `json:"default_is_active"` - FetchedAt pgtype.Timestamp `json:"fetched_at"` - ExpiresAt pgtype.Timestamp `json:"expires_at"` - CompanyID pgtype.Int8 `json:"company_id"` - IsActive bool `json:"is_active"` - RawOdds []byte `json:"raw_odds"` - UpdatedAt pgtype.Timestamp `json:"updated_at"` + ID int64 `json:"id"` + EventID int64 `json:"event_id"` + MarketType string `json:"market_type"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID int64 `json:"market_id"` + NumberOfOutcomes int64 `json:"number_of_outcomes"` + DefaultIsActive bool `json:"default_is_active"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + CompanyID pgtype.Int8 `json:"company_id"` + IsActive bool `json:"is_active"` + RawOdds []byte `json:"raw_odds"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` } func (q *Queries) GetOddsWithSettingsByMarketID(ctx context.Context, arg GetOddsWithSettingsByMarketIDParams) (GetOddsWithSettingsByMarketIDRow, error) { @@ -499,6 +514,7 @@ func (q *Queries) GetOddsWithSettingsByMarketID(ctx context.Context, arg GetOdds &i.MarketName, &i.MarketCategory, &i.MarketID, + &i.NumberOfOutcomes, &i.DefaultIsActive, &i.FetchedAt, &i.ExpiresAt, @@ -517,6 +533,7 @@ INSERT INTO odds_market ( market_name, market_category, market_id, + number_of_outcomes, raw_odds, fetched_at, expires_at @@ -529,26 +546,29 @@ VALUES ( $5, $6, $7, - $8 + $8, + $9 ) ON CONFLICT (event_id, market_id) DO UPDATE SET market_type = EXCLUDED.market_type, market_name = EXCLUDED.market_name, market_category = EXCLUDED.market_category, raw_odds = EXCLUDED.raw_odds, + number_of_outcomes = EXCLUDED.number_of_outcomes, fetched_at = EXCLUDED.fetched_at, expires_at = EXCLUDED.expires_at ` type InsertOddsMarketParams struct { - EventID int64 `json:"event_id"` - MarketType string `json:"market_type"` - MarketName string `json:"market_name"` - MarketCategory string `json:"market_category"` - MarketID int64 `json:"market_id"` - RawOdds []byte `json:"raw_odds"` - FetchedAt pgtype.Timestamp `json:"fetched_at"` - ExpiresAt pgtype.Timestamp `json:"expires_at"` + EventID int64 `json:"event_id"` + MarketType string `json:"market_type"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID int64 `json:"market_id"` + NumberOfOutcomes int64 `json:"number_of_outcomes"` + RawOdds []byte `json:"raw_odds"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` } func (q *Queries) InsertOddsMarket(ctx context.Context, arg InsertOddsMarketParams) error { @@ -558,6 +578,7 @@ func (q *Queries) InsertOddsMarket(ctx context.Context, arg InsertOddsMarketPara arg.MarketName, arg.MarketCategory, arg.MarketID, + arg.NumberOfOutcomes, arg.RawOdds, arg.FetchedAt, arg.ExpiresAt, diff --git a/internal/domain/event.go b/internal/domain/event.go index de5ac3a..f46032d 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -120,6 +120,7 @@ type BaseEvent struct { StartTime time.Time Source EventSource Status EventStatus + TotalOddOutcomes int64 IsMonitored bool DefaultIsFeatured bool DefaultIsActive bool @@ -149,6 +150,7 @@ type BaseEventRes struct { StartTime time.Time `json:"start_time"` Source EventSource `json:"source"` Status EventStatus `json:"status"` + TotalOddOutcomes int64 `json:"total_odd_outcomes"` IsMonitored bool `json:"is_monitored"` DefaultIsFeatured bool `json:"default_is_featured"` DefaultIsActive bool `json:"default_is_active"` @@ -178,6 +180,7 @@ type EventWithSettings struct { StartTime time.Time Source EventSource Status EventStatus + TotalOddOutcomes int64 IsMonitored bool IsFeatured bool IsActive bool @@ -231,6 +234,7 @@ type EventWithSettingsRes struct { StartTime time.Time `json:"start_time"` Source EventSource `json:"source"` Status EventStatus `json:"status"` + TotalOddOutcomes int64 `json:"total_odd_outcomes"` IsMonitored bool `json:"is_monitored"` IsFeatured bool `json:"is_featured"` IsActive bool `json:"is_active"` @@ -332,6 +336,7 @@ func ConvertDBEvent(event dbgen.EventWithCountry) BaseEvent { StartTime: event.StartTime.Time.UTC(), Source: EventSource(event.Source), Status: EventStatus(event.Status), + TotalOddOutcomes: event.TotalOutcomes, DefaultIsFeatured: event.DefaultIsFeatured, IsMonitored: event.IsMonitored, DefaultIsActive: event.DefaultIsActive, @@ -422,6 +427,7 @@ func ConvertDBEventWithSetting(event dbgen.EventWithSetting) EventWithSettings { StartTime: event.StartTime.Time.UTC(), Source: EventSource(event.Source), Status: EventStatus(event.Status), + TotalOddOutcomes: event.TotalOutcomes, IsFeatured: event.IsFeatured, IsMonitored: event.IsMonitored, IsActive: event.IsActive, @@ -498,6 +504,7 @@ func ConvertEventRes(event BaseEvent) BaseEventRes { StartTime: event.StartTime.UTC(), Source: EventSource(event.Source), Status: EventStatus(event.Status), + TotalOddOutcomes: event.TotalOddOutcomes, DefaultIsFeatured: event.DefaultIsFeatured, IsMonitored: event.IsMonitored, DefaultIsActive: event.DefaultIsActive, @@ -537,6 +544,7 @@ func ConvertEventWitSettingRes(event EventWithSettings) EventWithSettingsRes { StartTime: event.StartTime.UTC(), Source: EventSource(event.Source), Status: EventStatus(event.Status), + TotalOddOutcomes: event.TotalOddOutcomes, IsFeatured: event.IsFeatured, IsMonitored: event.IsMonitored, IsActive: event.IsActive, diff --git a/internal/domain/odds.go b/internal/domain/odds.go index 27ac23c..9d6ff2f 100644 --- a/internal/domain/odds.go +++ b/internal/domain/odds.go @@ -9,42 +9,45 @@ import ( ) type CreateOddMarket struct { - EventID int64 - MarketCategory string - MarketType string - MarketName string - MarketID int64 - UpdatedAt time.Time - Odds []map[string]interface{} + EventID int64 + MarketCategory string + MarketType string + MarketName string + MarketID int64 + NumberOfOutcomes int64 + UpdatedAt time.Time + Odds []map[string]interface{} } type OddMarket struct { - ID int64 `json:"id"` - EventID int64 `json:"event_id"` - MarketType string `json:"market_type"` - MarketName string `json:"market_name"` - MarketCategory string `json:"market_category"` - MarketID int64 `json:"market_id"` - RawOdds []json.RawMessage `json:"raw_odds"` - FetchedAt time.Time `json:"fetched_at"` - ExpiresAt time.Time `json:"expires_at"` - DefaultIsActive bool `json:"is_active"` - IsMonitored bool `json:"is_monitored"` - IsLive bool `json:"is_live"` - Status EventStatus `json:"status"` - Source EventSource `json:"source"` + ID int64 `json:"id"` + EventID int64 `json:"event_id"` + MarketType string `json:"market_type"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID int64 `json:"market_id"` + NumberOfOutcomes int64 `json:"number_of_outcomes"` + RawOdds []json.RawMessage `json:"raw_odds"` + FetchedAt time.Time `json:"fetched_at"` + ExpiresAt time.Time `json:"expires_at"` + DefaultIsActive bool `json:"is_active"` + IsMonitored bool `json:"is_monitored"` + IsLive bool `json:"is_live"` + Status EventStatus `json:"status"` + Source EventSource `json:"source"` } type OddMarketWithSettings struct { - ID int64 `json:"id"` - EventID int64 `json:"event_id"` - MarketType string `json:"market_type"` - MarketName string `json:"market_name"` - MarketCategory string `json:"market_category"` - MarketID int64 `json:"market_id"` - RawOdds []json.RawMessage `json:"raw_odds"` - FetchedAt time.Time `json:"fetched_at"` - ExpiresAt time.Time `json:"expires_at"` - IsActive bool `json:"is_active"` + ID int64 `json:"id"` + EventID int64 `json:"event_id"` + MarketType string `json:"market_type"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID int64 `json:"market_id"` + NumberOfOutcomes int64 `json:"number_of_outcomes"` + RawOdds []json.RawMessage `json:"raw_odds"` + FetchedAt time.Time `json:"fetched_at"` + ExpiresAt time.Time `json:"expires_at"` + IsActive bool `json:"is_active"` } type OddMarketSettings struct { @@ -78,8 +81,8 @@ type CreateOddMarketSettingsReq struct { } type UpdateGlobalOddMarketSettingsReq struct { - OddMarketID int64 `json:"odd_market_id"` - IsActive *bool `json:"is_active,omitempty"` + OddMarketID int64 `json:"odd_market_id"` + IsActive *bool `json:"is_active,omitempty"` } type RawOddsByMarketID struct { @@ -112,20 +115,21 @@ func ConvertDBOddMarket(oddMarket dbgen.OddsMarketWithEvent) (OddMarket, error) rawOdds = []json.RawMessage{} // explicit empty slice } return OddMarket{ - ID: oddMarket.ID, - EventID: oddMarket.EventID, - MarketType: oddMarket.MarketType, - MarketName: oddMarket.MarketName, - MarketCategory: oddMarket.MarketCategory, - MarketID: oddMarket.MarketID, - RawOdds: rawOdds, - FetchedAt: oddMarket.FetchedAt.Time, - ExpiresAt: oddMarket.ExpiresAt.Time, - DefaultIsActive: oddMarket.DefaultIsActive, - IsMonitored: oddMarket.IsMonitored, - IsLive: oddMarket.IsLive, - Status: EventStatus(oddMarket.Status), - Source: EventSource(oddMarket.Source), + ID: oddMarket.ID, + EventID: oddMarket.EventID, + MarketType: oddMarket.MarketType, + MarketName: oddMarket.MarketName, + MarketCategory: oddMarket.MarketCategory, + MarketID: oddMarket.MarketID, + NumberOfOutcomes: oddMarket.NumberOfOutcomes, + RawOdds: rawOdds, + FetchedAt: oddMarket.FetchedAt.Time, + ExpiresAt: oddMarket.ExpiresAt.Time, + DefaultIsActive: oddMarket.DefaultIsActive, + IsMonitored: oddMarket.IsMonitored, + IsLive: oddMarket.IsLive, + Status: EventStatus(oddMarket.Status), + Source: EventSource(oddMarket.Source), }, nil } @@ -148,14 +152,15 @@ func ConvertCreateOddMarket(oddMarket CreateOddMarket) (dbgen.InsertOddsMarketPa } return dbgen.InsertOddsMarketParams{ - EventID: oddMarket.EventID, - MarketType: oddMarket.MarketType, - MarketName: oddMarket.MarketName, - MarketCategory: oddMarket.MarketCategory, - MarketID: oddMarket.MarketID, - RawOdds: rawOddsBytes, - FetchedAt: pgtype.Timestamp{Time: time.Now(), Valid: true}, - ExpiresAt: pgtype.Timestamp{Time: (time.Now()).Add(time.Hour), Valid: true}, + EventID: oddMarket.EventID, + MarketType: oddMarket.MarketType, + MarketName: oddMarket.MarketName, + MarketCategory: oddMarket.MarketCategory, + MarketID: oddMarket.MarketID, + NumberOfOutcomes: oddMarket.NumberOfOutcomes, + RawOdds: rawOddsBytes, + FetchedAt: pgtype.Timestamp{Time: time.Now(), Valid: true}, + ExpiresAt: pgtype.Timestamp{Time: (time.Now()).Add(time.Hour), Valid: true}, }, nil } @@ -182,16 +187,17 @@ func ConvertDBOddMarketWithSetting(oms dbgen.OddsMarketWithSetting) (OddMarketWi rawOdds = []json.RawMessage{} // explicit empty slice } return OddMarketWithSettings{ - ID: oms.ID, - EventID: oms.EventID, - MarketType: oms.MarketType, - MarketName: oms.MarketName, - MarketCategory: oms.MarketCategory, - MarketID: oms.MarketID, - RawOdds: rawOdds, - FetchedAt: oms.FetchedAt.Time, - ExpiresAt: oms.ExpiresAt.Time, - IsActive: oms.IsActive, + ID: oms.ID, + EventID: oms.EventID, + MarketType: oms.MarketType, + MarketName: oms.MarketName, + MarketCategory: oms.MarketCategory, + MarketID: oms.MarketID, + NumberOfOutcomes: oms.NumberOfOutcomes, + RawOdds: rawOdds, + FetchedAt: oms.FetchedAt.Time, + ExpiresAt: oms.ExpiresAt.Time, + IsActive: oms.IsActive, }, nil } diff --git a/internal/repository/event.go b/internal/repository/event.go index dca45ff..fcc20c5 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -126,6 +126,9 @@ func (s *Store) GetEventsWithSettings(ctx context.Context, companyID int64, filt StartTime: event.StartTime.Time.UTC(), Source: domain.EventSource(event.Source), Status: domain.EventStatus(event.Status), + TotalOddOutcomes: event.TotalOutcomes, + SourceEventID: event.SourceEventID, + WinningUpperLimit: event.WinningUpperLimit, IsFeatured: event.IsFeatured, IsMonitored: event.IsMonitored, IsActive: event.IsActive, @@ -207,6 +210,9 @@ func (s *Store) GetEventWithSettingByID(ctx context.Context, ID int64, companyID StartTime: event.StartTime.Time.UTC(), Source: domain.EventSource(event.Source), Status: domain.EventStatus(event.Status), + TotalOddOutcomes: event.TotalOutcomes, + SourceEventID: event.SourceEventID, + WinningUpperLimit: event.WinningUpperLimit, IsFeatured: event.IsFeatured, IsMonitored: event.IsMonitored, IsActive: event.IsActive, diff --git a/internal/repository/odds.go b/internal/repository/odds.go index 009e301..8dd7c7f 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -180,16 +180,17 @@ func (s *Store) GetOddsWithSettingsByMarketID(ctx context.Context, marketID int6 } converted := domain.OddMarketWithSettings{ - ID: odds.ID, - EventID: odds.EventID, - MarketType: odds.MarketType, - MarketName: odds.MarketName, - MarketCategory: odds.MarketCategory, - MarketID: odds.MarketID, - RawOdds: rawOdds, - FetchedAt: odds.FetchedAt.Time, - ExpiresAt: odds.ExpiresAt.Time, - IsActive: odds.IsActive, + ID: odds.ID, + EventID: odds.EventID, + MarketType: odds.MarketType, + MarketName: odds.MarketName, + MarketCategory: odds.MarketCategory, + MarketID: odds.MarketID, + NumberOfOutcomes: odds.NumberOfOutcomes, + RawOdds: rawOdds, + FetchedAt: odds.FetchedAt.Time, + ExpiresAt: odds.ExpiresAt.Time, + IsActive: odds.IsActive, } return converted, nil } @@ -221,16 +222,17 @@ func (s *Store) GetOddsWithSettingsByID(ctx context.Context, ID int64, companyID } converted := domain.OddMarketWithSettings{ - ID: odds.ID, - EventID: odds.EventID, - MarketType: odds.MarketType, - MarketName: odds.MarketName, - MarketCategory: odds.MarketCategory, - MarketID: odds.MarketID, - RawOdds: rawOdds, - FetchedAt: odds.FetchedAt.Time, - ExpiresAt: odds.ExpiresAt.Time, - IsActive: odds.IsActive, + ID: odds.ID, + EventID: odds.EventID, + MarketType: odds.MarketType, + MarketName: odds.MarketName, + MarketCategory: odds.MarketCategory, + MarketID: odds.MarketID, + NumberOfOutcomes: odds.NumberOfOutcomes, + RawOdds: rawOdds, + FetchedAt: odds.FetchedAt.Time, + ExpiresAt: odds.ExpiresAt.Time, + IsActive: odds.IsActive, } return converted, nil @@ -287,16 +289,17 @@ func (s *Store) GetOddsWithSettingsByEventID(ctx context.Context, eventID int64, } result[i] = domain.OddMarketWithSettings{ - ID: o.ID, - EventID: o.EventID, - MarketType: o.MarketType, - MarketName: o.MarketName, - MarketCategory: o.MarketCategory, - MarketID: o.MarketID, - RawOdds: rawOdds, - FetchedAt: o.FetchedAt.Time, - ExpiresAt: o.ExpiresAt.Time, - IsActive: o.IsActive, + ID: o.ID, + EventID: o.EventID, + MarketType: o.MarketType, + MarketName: o.MarketName, + MarketCategory: o.MarketCategory, + MarketID: o.MarketID, + NumberOfOutcomes: o.NumberOfOutcomes, + RawOdds: rawOdds, + FetchedAt: o.FetchedAt.Time, + ExpiresAt: o.ExpiresAt.Time, + IsActive: o.IsActive, } } diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index ab1f033..d6ff26a 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -1236,8 +1236,6 @@ func (s *Service) ProcessBetCashback(ctx context.Context) error { zap.Int64("userID", bet.UserID), zap.Error(err)) } - - // Add a notification here } return nil } diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index dcc1004..0cf6574 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -557,13 +557,14 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID int64, fi, secti } marketRecord := domain.CreateOddMarket{ - EventID: eventID, - MarketCategory: sectionName, - MarketType: marketType, - MarketName: market.Name, - MarketID: marketIDint, - UpdatedAt: updatedAt, - Odds: marketOdds, + EventID: eventID, + MarketCategory: sectionName, + MarketType: marketType, + MarketName: market.Name, + MarketID: marketIDint, + NumberOfOutcomes: int64(len(market.Odds)), + UpdatedAt: updatedAt, + Odds: marketOdds, // bwin won't reach this code so bet365 is hardcoded for now } @@ -676,8 +677,8 @@ func (s *ServiceImpl) SaveOddsSetting(ctx context.Context, odd domain.CreateOddM return s.store.SaveOddsSetting(ctx, odd) } -func (s *ServiceImpl) UpdateGlobalOddsSetting(ctx context.Context, odd domain.UpdateGlobalOddMarketSettings) error { - return s.store.UpdateGlobalOddsSetting(ctx, odd); +func (s *ServiceImpl) UpdateGlobalOddsSetting(ctx context.Context, odd domain.UpdateGlobalOddMarketSettings) error { + return s.store.UpdateGlobalOddsSetting(ctx, odd) } func (s *ServiceImpl) SaveOddsSettingReq(ctx context.Context, companyID int64, req domain.CreateOddMarketSettingsReq) error { @@ -749,7 +750,7 @@ func (s *ServiceImpl) DeleteAllCompanyOddsSetting(ctx context.Context, companyID return s.store.DeleteAllCompanyOddsSetting(ctx, companyID) } -func (s *ServiceImpl) DeleteCompanyOddsSettingByOddMarketID(ctx context.Context, companyID int64, oddMarketID int64) error{ +func (s *ServiceImpl) DeleteCompanyOddsSettingByOddMarketID(ctx context.Context, companyID int64, oddMarketID int64) error { return s.store.DeleteCompanyOddsSettingByOddMarketID(ctx, companyID, oddMarketID) } diff --git a/internal/services/result/service.go b/internal/services/result/service.go index e5f09b4..1a12d1f 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -262,7 +262,6 @@ func (s *Service) FetchB365ResultAndUpdateBets(ctx context.Context) error { eventLogger := s.mongoLogger.With( zap.Int64("eventID", event.ID), ) - result, err := s.FetchB365Result(ctx, event.SourceEventID) if err != nil { if err == ErrEventIsNotActive { @@ -457,7 +456,6 @@ func (s *Service) FetchB365ResultAndUpdateBets(ctx context.Context) error { return nil } - func (s *Service) CheckAndUpdateExpiredB365Events(ctx context.Context) (int64, error) { events, _, err := s.repo.GetAllEvents(ctx, domain.EventFilter{ LastStartTime: domain.ValidTime{ diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 13019f3..9b0b21a 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -96,7 +96,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S } for _, job := range schedule { - // job.task() + job.task() if _, err := c.AddFunc(job.spec, job.task); err != nil { mongoLogger.Error("Failed to schedule data fetching cron job", zap.Error(err), diff --git a/internal/web_server/handlers/cashier.go b/internal/web_server/handlers/cashier.go index 139f4a4..1efb392 100644 --- a/internal/web_server/handlers/cashier.go +++ b/internal/web_server/handlers/cashier.go @@ -227,11 +227,8 @@ func (h *Handler) GetAllCashiers(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, errMsg) } - cashiers, total, err := h.userSvc.GetAllCashiers(c.Context(), domain.UserFilter{ - Query: searchString, - CreatedBefore: createdBefore, - CreatedAfter: createdAfter, - }) + cashiers, total, err := h.userSvc.GetAllCashiers(c.Context(), filter) + if err != nil { h.mongoLoggerSvc.Error("failed to get all cashiers", zap.Int("status_code", fiber.StatusInternalServerError), diff --git a/internal/web_server/handlers/event_handler.go b/internal/web_server/handlers/event_handler.go index bad2fdf..533c76f 100644 --- a/internal/web_server/handlers/event_handler.go +++ b/internal/web_server/handlers/event_handler.go @@ -285,6 +285,11 @@ func (h *Handler) GetTenantUpcomingEvents(c *fiber.Ctx) error { Value: firstStartTimeParsed, Valid: true, } + } else { + firstStartTime = domain.ValidTime{ + Value: time.Now(), + Valid: true, + } } lastStartTimeQuery := c.Query("last_start_time") diff --git a/makefile b/makefile index ee00452..936aee9 100644 --- a/makefile +++ b/makefile @@ -69,6 +69,20 @@ seed_data: echo "Seeding $$file..."; \ cat $$file | docker exec -i fortunebet-backend-postgres-1 psql -U root -d gh; \ done +.PHONY: seed_dev_data +seed_dev_data: + @echo "Waiting for PostgreSQL to be ready..." + @until docker exec fortunebet-backend-postgres-1 pg_isready -U root -d gh; do \ + echo "PostgreSQL is not ready yet..."; \ + sleep 1; \ + done + cat db/scripts/fix_autoincrement_desync.sql | docker exec -i fortunebet-backend-postgres-1 psql -U root -d gh; + @for file in db/dev_data/*.sql; do \ + if [ -f "$$file" ]; then \ + echo "Seeding $$file..."; \ + cat $$file | docker exec -i fortunebet-backend-postgres-1 psql -U root -d gh; \ + fi \ + done postgres_log: docker logs fortunebet-backend-postgres-1 .PHONY: swagger From f8878cd8b3b0697b79faa8e57f3eb83c1ae06551 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Tue, 14 Oct 2025 04:47:41 +0300 Subject: [PATCH 23/23] chore: updating version to v1.0.2 --- internal/web_server/cron.go | 30 +++++++++++++++--------------- internal/web_server/routes.go | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 9b0b21a..10e28f4 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -80,23 +80,23 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S } }, }, - // { - // 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 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 { - job.task() + // job.task() if _, err := c.AddFunc(job.spec, job.task); err != nil { mongoLogger.Error("Failed to schedule data fetching cron job", zap.Error(err), @@ -130,7 +130,7 @@ func StartCleanupCrons(ticketService ticket.Service, notificationSvc *notificati }, }, { - spec: "0 0 * * * *", + spec: "0 0 0 * * 1", // Every Monday (Weekly) task: func() { mongoLogger.Info("Deleting old notifications") if err := notificationSvc.DeleteOldNotifications(context.Background()); err != nil { diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 23ffd66..92dad34 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.1", + "version": "1.0.2", }) })