diff --git a/cmd/main.go b/cmd/main.go index 0969e35..6fe8751 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -119,13 +119,13 @@ func main() { oddsSvc := odds.New(store, cfg, eventSvc, logger, domain.MongoDBLogger) notificationRepo := repository.NewNotificationRepository(store) virtuaGamesRepo := repository.NewVirtualGameRepository(store) - notificationSvc := notificationservice.New(notificationRepo, domain.MongoDBLogger, logger, cfg, messengerSvc, userSvc) // var userStore user.UserStore // Initialize producer - brokers := []string{"localhost:9092"} topic := "wallet-balance-topic" - producer := kafka.NewProducer(brokers, topic) + producer := kafka.NewProducer(cfg.KafkaBrokers, topic) + + notificationSvc := notificationservice.New(notificationRepo, domain.MongoDBLogger, logger, cfg, messengerSvc, userSvc, cfg.KafkaBrokers) walletSvc := wallet.NewService( wallet.WalletStore(store), diff --git a/db/data/001_initial_seed_data.sql b/db/data/001_initial_seed_data.sql index 95a21d8..ba8937c 100644 --- a/db/data/001_initial_seed_data.sql +++ b/db/data/001_initial_seed_data.sql @@ -78,9 +78,11 @@ SET value = EXCLUDED.value; INSERT INTO global_settings (key, value) VALUES ('sms_provider', 'afro_message'), ('max_number_of_outcomes', '30'), + ('max_unsettled_bets', '100'), ('bet_amount_limit', '10000000'), ('daily_ticket_limit', '50'), - ('total_winnings_limit', '1000000'), + ('total_winnings_limit', '100000000000'), + ('total_winnings_notify', '100000000'), ('amount_for_bet_referral', '1000000'), ('cashback_amount_cap', '1000'), ('default_winning_limit', '5000000'), diff --git a/db/data/002_veli_user.sql b/db/data/002_veli_user.sql index 1dfe96a..25a429b 100644 --- a/db/data/002_veli_user.sql +++ b/db/data/002_veli_user.sql @@ -76,7 +76,7 @@ VALUES ( TRUE, TRUE, TRUE, - 1, + 5, 'regular_wallet', 'ETB', TRUE, @@ -89,7 +89,7 @@ VALUES ( FALSE, TRUE, TRUE, - 1, + 5, 'static_wallet', 'ETB', TRUE, @@ -102,7 +102,7 @@ VALUES ( TRUE, TRUE, TRUE, - 1, + 6, 'regular_wallet', 'ETB', TRUE, @@ -115,7 +115,7 @@ VALUES ( FALSE, TRUE, TRUE, - 1, + 6, 'static_wallet', 'ETB', TRUE, diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 89e20a8..a30f872 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -72,8 +72,11 @@ 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 + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, type) ); + + CREATE TABLE refresh_tokens ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, @@ -537,6 +540,16 @@ CREATE TABLE IF NOT EXISTS raffle_game_filters ( game_id VARCHAR(150) NOT NULL, CONSTRAINT unique_raffle_game UNIQUE (raffle_id, game_id) ); +CREATE TABLE IF NOT EXISTS accumulator ( + outcome_count BIGINT PRIMARY KEY, + default_multiplier REAL NOT NULL +); +CREATE TABLE IF NOT EXISTS company_accumulator ( + id SERIAL PRIMARY KEY, + company_id BIGINT NOT NULL, + outcome_count BIGINT NOT NULL, + multiplier REAL NOT NULL +); ------ Views CREATE VIEW companies_details AS SELECT companies.*, @@ -751,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; + 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/migrations/000002_notification.up.sql b/db/migrations/000002_notification.up.sql index 8fd9ad8..3d74954 100644 --- a/db/migrations/000002_notification.up.sql +++ b/db/migrations/000002_notification.up.sql @@ -13,6 +13,7 @@ CREATE TABLE IF NOT EXISTS notifications ( 'signup_welcome', 'otp_sent', 'wallet_threshold', + 'wallet_updated', 'transfer_failed', 'transfer_success', 'admin_alert', diff --git a/db/query/bet.sql b/db/query/bet.sql index caef892..811dd1f 100644 --- a/db/query/bet.sql +++ b/db/query/bet.sql @@ -57,6 +57,47 @@ wHERE ( company_id = sqlc.narg('company_id') OR sqlc.narg('company_id') IS NULL ) + AND ( + status = sqlc.narg('status') + OR sqlc.narg('status') IS NULL + ) + AND ( + cashed_out = sqlc.narg('cashed_out') + OR sqlc.narg('cashed_out') IS NULL + ) + AND ( + full_name ILIKE '%' || sqlc.narg('query') || '%' + OR phone_number ILIKE '%' || sqlc.narg('query') || '%' + OR sqlc.narg('query') IS NULL + ) + AND ( + created_at > sqlc.narg('created_before') + OR sqlc.narg('created_before') IS NULL + ) + AND ( + created_at < sqlc.narg('created_after') + OR sqlc.narg('created_after') IS NULL + ) +LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); +-- name: GetTotalBets :one +SELECT COUNT(*) +FROM bets +wHERE ( + user_id = sqlc.narg('user_id') + OR sqlc.narg('user_id') IS NULL + ) + AND ( + is_shop_bet = sqlc.narg('is_shop_bet') + OR sqlc.narg('is_shop_bet') IS NULL + ) + AND ( + company_id = sqlc.narg('company_id') + OR sqlc.narg('company_id') IS NULL + ) + AND ( + status = sqlc.narg('status') + OR sqlc.narg('status') IS NULL + ) AND ( cashed_out = sqlc.narg('cashed_out') OR sqlc.narg('cashed_out') IS NULL diff --git a/db/query/notification.sql b/db/query/notification.sql index bcf52d5..76d0edc 100644 --- a/db/query/notification.sql +++ b/db/query/notification.sql @@ -40,12 +40,19 @@ SELECT * FROM notifications ORDER BY timestamp DESC LIMIT $1 OFFSET $2; --- name: ListNotifications :many +-- name: GetTotalNotificationCount :one +SELECT COUNT(*) +FROM notifications; +-- name: GetUserNotifications :many SELECT * FROM notifications WHERE recipient_id = $1 ORDER BY timestamp DESC LIMIT $2 OFFSET $3; +-- name: GetUserNotificationCount :one +SELECT COUNT(*) +FROM notifications +WHERE recipient_id = $1; -- name: CountUnreadNotifications :one SELECT count(id) FROM notifications @@ -69,10 +76,16 @@ LIMIT $1; SELECT recipient_id FROM notifications WHERE reciever = $1; - -- name: GetNotificationCounts :many -SELECT - COUNT(*) as total, - COUNT(CASE WHEN is_read = true THEN 1 END) as read, - COUNT(CASE WHEN is_read = false THEN 1 END) as unread -FROM notifications; +SELECT COUNT(*) as total, + COUNT( + CASE + WHEN is_read = true THEN 1 + END + ) as read, + COUNT( + CASE + WHEN is_read = false THEN 1 + END + ) as unread +FROM notifications; \ No newline at end of file diff --git a/db/query/user.sql b/db/query/user.sql index e749355..549d4fa 100644 --- a/db/query/user.sql +++ b/db/query/user.sql @@ -107,11 +107,14 @@ SELECT id, suspended_at, company_id FROM users -WHERE (company_id = $1) +WHERE ( + company_id = sqlc.narg('company_id') + OR sqlc.narg('company_id') IS NULL + ) AND ( - first_name ILIKE '%' || $2 || '%' - OR last_name ILIKE '%' || $2 || '%' - OR phone_number LIKE '%' || $2 || '%' + first_name ILIKE '%' || $1 || '%' + OR last_name ILIKE '%' || $1 || '%' + OR phone_number LIKE '%' || $1 || '%' ) AND ( role = sqlc.narg('role') diff --git a/docker-compose.yml b/docker-compose.yml index 3a5d5aa..391f0de 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -69,6 +69,37 @@ services: 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 + + + app: build: context: . @@ -80,6 +111,7 @@ services: - 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 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 573c4c2..9813c89 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 @@ -119,32 +119,40 @@ wHERE ( OR $3 IS NULL ) AND ( - cashed_out = $4 + status = $4 OR $4 IS NULL ) AND ( - full_name ILIKE '%' || $5 || '%' - OR phone_number ILIKE '%' || $5 || '%' + cashed_out = $5 OR $5 IS NULL ) AND ( - created_at > $6 + full_name ILIKE '%' || $6 || '%' + OR phone_number ILIKE '%' || $6 || '%' OR $6 IS NULL ) AND ( - created_at < $7 + created_at > $7 OR $7 IS NULL ) + AND ( + created_at < $8 + OR $8 IS NULL + ) +LIMIT $10 OFFSET $9 ` type GetAllBetsParams struct { UserID pgtype.Int8 `json:"user_id"` IsShopBet pgtype.Bool `json:"is_shop_bet"` CompanyID pgtype.Int8 `json:"company_id"` + Status pgtype.Int4 `json:"status"` CashedOut pgtype.Bool `json:"cashed_out"` Query pgtype.Text `json:"query"` CreatedBefore pgtype.Timestamp `json:"created_before"` CreatedAfter pgtype.Timestamp `json:"created_after"` + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` } func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWithOutcome, error) { @@ -152,10 +160,13 @@ func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWi arg.UserID, arg.IsShopBet, arg.CompanyID, + arg.Status, arg.CashedOut, arg.Query, arg.CreatedBefore, arg.CreatedAfter, + arg.Offset, + arg.Limit, ) if err != nil { return nil, err @@ -481,6 +492,71 @@ func (q *Queries) GetBetsForCashback(ctx context.Context) ([]BetWithOutcome, err return items, nil } +const GetTotalBets = `-- name: GetTotalBets :one +SELECT COUNT(*) +FROM bets +wHERE ( + user_id = $1 + OR $1 IS NULL + ) + AND ( + is_shop_bet = $2 + OR $2 IS NULL + ) + AND ( + company_id = $3 + OR $3 IS NULL + ) + AND ( + status = $4 + OR $4 IS NULL + ) + AND ( + cashed_out = $5 + OR $5 IS NULL + ) + AND ( + full_name ILIKE '%' || $6 || '%' + OR phone_number ILIKE '%' || $6 || '%' + OR $6 IS NULL + ) + AND ( + created_at > $7 + OR $7 IS NULL + ) + AND ( + created_at < $8 + OR $8 IS NULL + ) +` + +type GetTotalBetsParams struct { + UserID pgtype.Int8 `json:"user_id"` + IsShopBet pgtype.Bool `json:"is_shop_bet"` + CompanyID pgtype.Int8 `json:"company_id"` + Status pgtype.Int4 `json:"status"` + CashedOut pgtype.Bool `json:"cashed_out"` + Query pgtype.Text `json:"query"` + CreatedBefore pgtype.Timestamp `json:"created_before"` + CreatedAfter pgtype.Timestamp `json:"created_after"` +} + +func (q *Queries) GetTotalBets(ctx context.Context, arg GetTotalBetsParams) (int64, error) { + row := q.db.QueryRow(ctx, GetTotalBets, + arg.UserID, + arg.IsShopBet, + arg.CompanyID, + arg.Status, + arg.CashedOut, + arg.Query, + arg.CreatedBefore, + arg.CreatedAfter, + ) + var count int64 + err := row.Scan(&count) + return count, err +} + const UpdateBetOutcomeStatus = `-- name: UpdateBetOutcomeStatus :one UPDATE bet_outcomes SET status = $1 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..a9a57b8 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 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 18bc509..506eaca 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/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 0729560..fc793b1 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 @@ -513,166 +513,6 @@ func (q *Queries) GetEventsWithSettings(ctx context.Context, arg GetEventsWithSe return items, nil } -const GetExpiredEvents = `-- name: GetExpiredEvents :many -SELECT 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 -FROM event_with_country -WHERE start_time < now() - and ( - status = $1 - OR $1 IS NULL - ) -ORDER BY start_time ASC -` - -func (q *Queries) GetExpiredEvents(ctx context.Context, status pgtype.Text) ([]EventWithCountry, error) { - rows, err := q.db.Query(ctx, GetExpiredEvents, status) - if err != nil { - return nil, err - } - defer rows.Close() - var items []EventWithCountry - for rows.Next() { - var i EventWithCountry - if err := rows.Scan( - &i.ID, - &i.SportID, - &i.MatchName, - &i.HomeTeam, - &i.AwayTeam, - &i.HomeTeamID, - &i.AwayTeamID, - &i.HomeKitImage, - &i.AwayKitImage, - &i.LeagueID, - &i.LeagueName, - &i.StartTime, - &i.Score, - &i.MatchMinute, - &i.TimerStatus, - &i.AddedTime, - &i.MatchPeriod, - &i.IsLive, - &i.Status, - &i.FetchedAt, - &i.Source, - &i.DefaultIsActive, - &i.DefaultIsFeatured, - &i.DefaultWinningUpperLimit, - &i.IsMonitored, - &i.LeagueCc, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const GetPaginatedUpcomingEvents = `-- name: GetPaginatedUpcomingEvents :many -SELECT 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 -FROM event_with_country -WHERE start_time > now() - AND is_live = false - AND status = 'upcoming' - AND ( - league_id = $1 - OR $1 IS NULL - ) - AND ( - sport_id = $2 - OR $2 IS NULL - ) - AND ( - match_name ILIKE '%' || $3 || '%' - OR league_name ILIKE '%' || $3 || '%' - OR $3 IS NULL - ) - AND ( - start_time < $4 - OR $4 IS NULL - ) - AND ( - start_time > $5 - OR $5 IS NULL - ) - AND ( - league_cc = $6 - OR $6 IS NULL - ) -ORDER BY start_time ASC -LIMIT $8 OFFSET $7 -` - -type GetPaginatedUpcomingEventsParams struct { - LeagueID pgtype.Int8 `json:"league_id"` - SportID pgtype.Int4 `json:"sport_id"` - Query pgtype.Text `json:"query"` - LastStartTime pgtype.Timestamp `json:"last_start_time"` - FirstStartTime pgtype.Timestamp `json:"first_start_time"` - CountryCode pgtype.Text `json:"country_code"` - Offset pgtype.Int4 `json:"offset"` - Limit pgtype.Int4 `json:"limit"` -} - -func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginatedUpcomingEventsParams) ([]EventWithCountry, error) { - rows, err := q.db.Query(ctx, GetPaginatedUpcomingEvents, - arg.LeagueID, - arg.SportID, - arg.Query, - arg.LastStartTime, - arg.FirstStartTime, - arg.CountryCode, - arg.Offset, - arg.Limit, - ) - if err != nil { - return nil, err - } - defer rows.Close() - var items []EventWithCountry - for rows.Next() { - var i EventWithCountry - if err := rows.Scan( - &i.ID, - &i.SportID, - &i.MatchName, - &i.HomeTeam, - &i.AwayTeam, - &i.HomeTeamID, - &i.AwayTeamID, - &i.HomeKitImage, - &i.AwayKitImage, - &i.LeagueID, - &i.LeagueName, - &i.StartTime, - &i.Score, - &i.MatchMinute, - &i.TimerStatus, - &i.AddedTime, - &i.MatchPeriod, - &i.IsLive, - &i.Status, - &i.FetchedAt, - &i.Source, - &i.DefaultIsActive, - &i.DefaultIsFeatured, - &i.DefaultWinningUpperLimit, - &i.IsMonitored, - &i.LeagueCc, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - const GetSportAndLeagueIDs = `-- name: GetSportAndLeagueIDs :one SELECT sport_id, league_id FROM events WHERE id = $1 @@ -683,7 +523,7 @@ type GetSportAndLeagueIDsRow struct { LeagueID int64 `json:"league_id"` } -func (q *Queries) GetSportAndLeagueIDs(ctx context.Context, id string) (GetSportAndLeagueIDsRow, error) { +func (q *Queries) GetSportAndLeagueIDs(ctx context.Context, id int64) (GetSportAndLeagueIDsRow, error) { row := q.db.QueryRow(ctx, GetSportAndLeagueIDs, id) var i GetSportAndLeagueIDsRow err := row.Scan(&i.SportID, &i.LeagueID) 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 0aaad2c..1d2800b 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 7a3d25f..f8f9cd4 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 @@ -8,6 +8,11 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +type Accumulator struct { + OutcomeCount int64 `json:"outcome_count"` + DefaultMultiplier float32 `json:"default_multiplier"` +} + type Bank struct { ID int64 `json:"id"` Slug string `json:"slug"` @@ -159,6 +164,13 @@ type Company struct { UpdatedAt pgtype.Timestamp `json:"updated_at"` } +type CompanyAccumulator struct { + ID int32 `json:"id"` + CompanyID int64 `json:"company_id"` + OutcomeCount int64 `json:"outcome_count"` + Multiplier float32 `json:"multiplier"` +} + type CompanyEventSetting struct { ID int64 `json:"id"` CompanyID int64 `json:"company_id"` 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 9ce7e42..f6747fb 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 @@ -188,10 +188,17 @@ func (q *Queries) GetNotification(ctx context.Context, id string) (Notification, } const GetNotificationCounts = `-- name: GetNotificationCounts :many -SELECT - COUNT(*) as total, - COUNT(CASE WHEN is_read = true THEN 1 END) as read, - COUNT(CASE WHEN is_read = false THEN 1 END) as unread +SELECT COUNT(*) as total, + COUNT( + CASE + WHEN is_read = true THEN 1 + END + ) as read, + COUNT( + CASE + WHEN is_read = false THEN 1 + END + ) as unread FROM notifications ` @@ -221,17 +228,47 @@ func (q *Queries) GetNotificationCounts(ctx context.Context) ([]GetNotificationC return items, nil } -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 +const GetTotalNotificationCount = `-- name: GetTotalNotificationCount :one +SELECT COUNT(*) FROM notifications -WHERE delivery_status = 'failed' - AND timestamp < NOW() - INTERVAL '1 hour' -ORDER BY timestamp ASC -LIMIT $1 ` -func (q *Queries) ListFailedNotifications(ctx context.Context, limit int32) ([]Notification, error) { - rows, err := q.db.Query(ctx, ListFailedNotifications, limit) +func (q *Queries) GetTotalNotificationCount(ctx context.Context) (int64, error) { + row := q.db.QueryRow(ctx, GetTotalNotificationCount) + var count int64 + err := row.Scan(&count) + return count, err +} + +const GetUserNotificationCount = `-- name: GetUserNotificationCount :one +SELECT COUNT(*) +FROM notifications +WHERE recipient_id = $1 +` + +func (q *Queries) GetUserNotificationCount(ctx context.Context, recipientID int64) (int64, error) { + row := q.db.QueryRow(ctx, GetUserNotificationCount, recipientID) + var count int64 + err := row.Scan(&count) + return count, err +} + +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 +FROM notifications +WHERE recipient_id = $1 +ORDER BY timestamp DESC +LIMIT $2 OFFSET $3 +` + +type GetUserNotificationsParams struct { + RecipientID int64 `json:"recipient_id"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) GetUserNotifications(ctx context.Context, arg GetUserNotificationsParams) ([]Notification, error) { + rows, err := q.db.Query(ctx, GetUserNotifications, arg.RecipientID, arg.Limit, arg.Offset) if err != nil { return nil, err } @@ -265,22 +302,17 @@ func (q *Queries) ListFailedNotifications(ctx context.Context, limit int32) ([]N return items, nil } -const ListNotifications = `-- name: ListNotifications :many +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 FROM notifications -WHERE recipient_id = $1 -ORDER BY timestamp DESC -LIMIT $2 OFFSET $3 +WHERE delivery_status = 'failed' + AND timestamp < NOW() - INTERVAL '1 hour' +ORDER BY timestamp ASC +LIMIT $1 ` -type ListNotificationsParams struct { - RecipientID int64 `json:"recipient_id"` - Limit int32 `json:"limit"` - Offset int32 `json:"offset"` -} - -func (q *Queries) ListNotifications(ctx context.Context, arg ListNotificationsParams) ([]Notification, error) { - rows, err := q.db.Query(ctx, ListNotifications, arg.RecipientID, arg.Limit, arg.Offset) +func (q *Queries) ListFailedNotifications(ctx context.Context, limit int32) ([]Notification, error) { + rows, err := q.db.Query(ctx, ListFailedNotifications, limit) if err != nil { return nil, 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 1f181e4..ac9974c 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 a4888f9..a7a364e 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 7664dbb..bcd884e 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 9f9cd95..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 @@ -489,11 +489,14 @@ SELECT id, suspended_at, company_id FROM users -WHERE (company_id = $1) +WHERE ( + company_id = $2 + OR $2 IS NULL + ) AND ( - first_name ILIKE '%' || $2 || '%' - OR last_name ILIKE '%' || $2 || '%' - OR phone_number LIKE '%' || $2 || '%' + first_name ILIKE '%' || $1 || '%' + OR last_name ILIKE '%' || $1 || '%' + OR phone_number LIKE '%' || $1 || '%' ) AND ( role = $3 @@ -502,8 +505,8 @@ WHERE (company_id = $1) ` type SearchUserByNameOrPhoneParams struct { + Column1 pgtype.Text `json:"column_1"` CompanyID pgtype.Int8 `json:"company_id"` - Column2 pgtype.Text `json:"column_2"` Role pgtype.Text `json:"role"` } @@ -524,7 +527,7 @@ type SearchUserByNameOrPhoneRow struct { } func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, arg SearchUserByNameOrPhoneParams) ([]SearchUserByNameOrPhoneRow, error) { - rows, err := q.db.Query(ctx, SearchUserByNameOrPhone, arg.CompanyID, arg.Column2, arg.Role) + rows, err := q.db.Query(ctx, SearchUserByNameOrPhone, arg.Column1, arg.CompanyID, arg.Role) if err != nil { return nil, err } 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/config/config.go b/internal/config/config.go index 15b9421..28d9333 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -6,6 +6,7 @@ import ( "log/slog" "os" "strconv" + "strings" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -14,23 +15,29 @@ import ( ) var ( - ErrInvalidDbUrl = errors.New("db url is invalid") - ErrInvalidPort = errors.New("port number is invalid") - ErrRefreshExpiry = errors.New("refresh token expiry is invalid") - ErrAccessExpiry = errors.New("access token expiry is invalid") - ErrInvalidJwtKey = errors.New("jwt key is invalid") - ErrLogLevel = errors.New("log level not set") - ErrInvalidLevel = errors.New("invalid log level") - ErrInvalidEnv = errors.New("env not set or invalid") - ErrInvalidSMSAPIKey = errors.New("SMS API key is invalid") - ErrMissingBetToken = errors.New("missing BET365_TOKEN in .env") - ErrInvalidPopOKClientID = errors.New("PopOK client ID is invalid") - ErrInvalidPopOKSecretKey = errors.New("PopOK secret key is invalid") - ErrInvalidPopOKBaseURL = errors.New("PopOK base URL is invalid") - ErrInvalidPopOKCallbackURL = errors.New("PopOK callback URL is invalid") - ErrInvalidVeliAPIURL = errors.New("Veli API URL is invalid") - ErrInvalidVeliOperatorKey = errors.New("Veli operator key is invalid") - ErrInvalidVeliSecretKey = errors.New("Veli secret key is invalid") + ErrInvalidDbUrl = errors.New("db url is invalid") + ErrInvalidPort = errors.New("port number is invalid") + ErrRefreshExpiry = errors.New("refresh token expiry is invalid") + ErrAccessExpiry = errors.New("access token expiry is invalid") + ErrInvalidJwtKey = errors.New("jwt key is invalid") + ErrLogLevel = errors.New("log level not set") + ErrInvalidLevel = errors.New("invalid log level") + ErrInvalidEnv = errors.New("env not set or invalid") + ErrInvalidSMSAPIKey = errors.New("SMS API key is invalid") + ErrMissingBetToken = errors.New("missing BET365_TOKEN in .env") + ErrInvalidPopOKClientID = errors.New("PopOK client ID is invalid") + ErrInvalidPopOKSecretKey = errors.New("PopOK secret key is invalid") + ErrInvalidPopOKBaseURL = errors.New("PopOK base URL is invalid") + ErrInvalidPopOKCallbackURL = errors.New("PopOK callback URL is invalid") + ErrInvalidVeliAPIURL = errors.New("Veli API URL is invalid") + ErrInvalidVeliOperatorKey = errors.New("Veli operator key is invalid") + ErrInvalidVeliSecretKey = errors.New("Veli secret key is invalid") + ErrInvalidAtlasBaseUrl = errors.New("Atlas Base URL is invalid") + ErrInvalidAtlasOperatorID = errors.New("Atlas operator ID is invalid") + ErrInvalidAtlasSecretKey = errors.New("Atlas secret key is invalid") + ErrInvalidAtlasBrandID = errors.New("Atlas brand ID is invalid") + ErrInvalidAtlasPartnerID = errors.New("Atlas Partner ID is invalid") + ErrMissingResendApiKey = errors.New("missing Resend Api key") ErrMissingResendSenderEmail = errors.New("missing Resend sender name") ErrMissingTwilioAccountSid = errors.New("missing twilio account sid") @@ -151,6 +158,7 @@ type Config struct { TwilioAuthToken string TwilioSenderPhoneNumber string RedisAddr string + KafkaBrokers []string } func NewConfig() (*Config, error) { @@ -176,6 +184,7 @@ func (c *Config) loadEnv() error { c.ReportExportPath = os.Getenv("REPORT_EXPORT_PATH") c.RedisAddr = os.Getenv("REDIS_ADDR") + c.KafkaBrokers = strings.Split(os.Getenv("KAFKA_BROKERS"), ",") c.EnetPulseConfig.Token = os.Getenv("ENETPULSE_TOKEN") c.EnetPulseConfig.UserName = os.Getenv("ENETPULSE_USERNAME") @@ -410,6 +419,36 @@ func (c *Config) loadEnv() error { CallbackURL: popOKCallbackURL, Platform: popOKPlatform, } + + AtlasBaseUrl := os.Getenv("ATLAS_BASE_URL") + if AtlasBaseUrl == "" { + return ErrInvalidAtlasBaseUrl + } + AtlasSecretKey := os.Getenv("ATLAS_SECRET_KEY") + if AtlasSecretKey == "" { + return ErrInvalidAtlasSecretKey + } + AtlasBrandID := os.Getenv("ATLAS_BRAND_ID") + if AtlasBrandID == "" { + return ErrInvalidAtlasBrandID + } + AtlasPartnerID := os.Getenv("ATLAS_PARTNER_ID") + if AtlasPartnerID == "" { + return ErrInvalidAtlasPartnerID + } + AtlasOperatorID := os.Getenv("ATLAS_OPERATOR_ID") + if AtlasOperatorID == "" { + return ErrInvalidAtlasOperatorID + } + + c.Atlas = AtlasConfig{ + BaseURL: AtlasBaseUrl, + SecretKey: AtlasSecretKey, + CasinoID: AtlasBrandID, + PartnerID: AtlasPartnerID, + OperatorID: AtlasOperatorID, + } + betToken := os.Getenv("BET365_TOKEN") if betToken == "" { return ErrMissingBetToken diff --git a/internal/domain/bet.go b/internal/domain/bet.go index d59b68f..e4939ba 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -60,6 +60,7 @@ type Bet struct { } type BetFilter struct { + Status ValidOutcomeStatus UserID ValidInt64 CompanyID ValidInt64 CashedOut ValidBool @@ -67,6 +68,8 @@ type BetFilter struct { Query ValidString CreatedBefore ValidTime CreatedAfter ValidTime + Limit ValidInt32 + Offset ValidInt32 } type Flag struct { diff --git a/internal/domain/notification.go b/internal/domain/notification.go index 97fc9d1..b7193d7 100644 --- a/internal/domain/notification.go +++ b/internal/domain/notification.go @@ -14,6 +14,7 @@ type NotificationDeliveryStatus string type DeliveryChannel string const ( + NotificationTypeWalletUpdated NotificationType = "wallet_updated" NotificationTypeDepositResult NotificationType = "deposit_result" NotificationTypeDepositVerification NotificationType = "deposit_verification" NotificationTypeCashOutSuccess NotificationType = "cash_out_success" diff --git a/internal/domain/report.go b/internal/domain/report.go index 7e6ca55..bfa6f9d 100644 --- a/internal/domain/report.go +++ b/internal/domain/report.go @@ -282,10 +282,6 @@ type BetAnalysis struct { AverageOdds float64 `json:"average_odds"` } -type ValidOutcomeStatus struct { - Value OutcomeStatus - Valid bool // Valid is true if Value is not NULL -} // ReportFilter contains filters for report generation type ReportFilter struct { diff --git a/internal/domain/result.go b/internal/domain/result.go index 5369a2b..698e01e 100644 --- a/internal/domain/result.go +++ b/internal/domain/result.go @@ -3,7 +3,7 @@ package domain import ( "time" - dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/jackc/pgx/v5/pgtype" ) type MarketConfig struct { @@ -67,6 +67,20 @@ func (o *OutcomeStatus) String() string { } } +type ValidOutcomeStatus struct { + Value OutcomeStatus + Valid bool +} + + +func (v ValidOutcomeStatus) ToPG() pgtype.Int4 { + return pgtype.Int4{ + Int32: int32(v.Value), + Valid: v.Valid, + } +} + + type TimeStatus int32 const ( @@ -84,79 +98,3 @@ const ( TIME_STATUS_DECIDED_BY_FA TimeStatus = 11 TIME_STATUS_REMOVED TimeStatus = 99 ) - -type ResultLog struct { - ID int64 `json:"id"` - StatusNotFinishedCount int `json:"status_not_finished_count"` - StatusNotFinishedBets int `json:"status_not_finished_bets"` - StatusToBeFixedCount int `json:"status_to_be_fixed_count"` - StatusToBeFixedBets int `json:"status_to_be_fixed_bets"` - StatusPostponedCount int `json:"status_postponed_count"` - StatusPostponedBets int `json:"status_postponed_bets"` - StatusEndedCount int `json:"status_ended_count"` - StatusEndedBets int `json:"status_ended_bets"` - StatusRemovedCount int `json:"status_removed_count"` - StatusRemovedBets int `json:"status_removed_bets"` - RemovedCount int `json:"removed"` - CreatedAt time.Time `json:"created_at"` -} - -type CreateResultLog struct { - StatusNotFinishedCount int `json:"status_not_finished_count"` - StatusNotFinishedBets int `json:"status_not_finished_bets"` - StatusToBeFixedCount int `json:"status_to_be_fixed_count"` - StatusToBeFixedBets int `json:"status_to_be_fixed_bets"` - StatusPostponedCount int `json:"status_postponed_count"` - StatusPostponedBets int `json:"status_postponed_bets"` - StatusEndedCount int `json:"status_ended_count"` - StatusEndedBets int `json:"status_ended_bets"` - StatusRemovedCount int `json:"status_removed_count"` - StatusRemovedBets int `json:"status_removed_bets"` - RemovedCount int `json:"removed"` -} - -type ResultFilter struct { - CreatedBefore ValidTime - CreatedAfter ValidTime -} -type ResultStatusBets struct { - StatusNotFinished []int64 `json:"status_not_finished"` - StatusToBeFixed []int64 `json:"status_to_be_fixed"` - StatusPostponed []int64 `json:"status_postponed"` - StatusEnded []int64 `json:"status_ended"` - StatusRemoved []int64 `json:"status_removed"` -} - -func ConvertDBResultLog(result dbgen.ResultLog) ResultLog { - return ResultLog{ - ID: result.ID, - StatusNotFinishedCount: int(result.StatusNotFinishedCount), - StatusNotFinishedBets: int(result.StatusNotFinishedBets), - StatusToBeFixedCount: int(result.StatusToBeFixedCount), - StatusToBeFixedBets: int(result.StatusToBeFixedBets), - StatusPostponedCount: int(result.StatusPostponedCount), - StatusPostponedBets: int(result.StatusPostponedBets), - StatusEndedCount: int(result.StatusEndedCount), - StatusEndedBets: int(result.StatusEndedBets), - StatusRemovedCount: int(result.StatusRemovedCount), - StatusRemovedBets: int(result.StatusRemovedBets), - RemovedCount: int(result.RemovedCount), - CreatedAt: result.CreatedAt.Time, - } -} - -func ConvertCreateResultLog(result CreateResultLog) dbgen.CreateResultLogParams { - return dbgen.CreateResultLogParams{ - StatusNotFinishedCount: int32(result.StatusNotFinishedCount), - StatusNotFinishedBets: int32(result.StatusNotFinishedBets), - StatusToBeFixedCount: int32(result.StatusToBeFixedCount), - StatusToBeFixedBets: int32(result.StatusToBeFixedBets), - StatusPostponedCount: int32(result.StatusPostponedCount), - StatusPostponedBets: int32(result.StatusPostponedBets), - StatusEndedCount: int32(result.StatusEndedCount), - StatusEndedBets: int32(result.StatusEndedBets), - StatusRemovedCount: int32(result.StatusRemovedCount), - StatusRemovedBets: int32(result.StatusRemovedBets), - RemovedCount: int32(result.RemovedCount), - } -} diff --git a/internal/domain/result_log.go b/internal/domain/result_log.go new file mode 100644 index 0000000..5e70829 --- /dev/null +++ b/internal/domain/result_log.go @@ -0,0 +1,84 @@ +package domain + +import ( + "time" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" +) + + +type ResultLog struct { + ID int64 `json:"id"` + StatusNotFinishedCount int `json:"status_not_finished_count"` + StatusNotFinishedBets int `json:"status_not_finished_bets"` + StatusToBeFixedCount int `json:"status_to_be_fixed_count"` + StatusToBeFixedBets int `json:"status_to_be_fixed_bets"` + StatusPostponedCount int `json:"status_postponed_count"` + StatusPostponedBets int `json:"status_postponed_bets"` + StatusEndedCount int `json:"status_ended_count"` + StatusEndedBets int `json:"status_ended_bets"` + StatusRemovedCount int `json:"status_removed_count"` + StatusRemovedBets int `json:"status_removed_bets"` + RemovedCount int `json:"removed"` + CreatedAt time.Time `json:"created_at"` +} + +type CreateResultLog struct { + StatusNotFinishedCount int `json:"status_not_finished_count"` + StatusNotFinishedBets int `json:"status_not_finished_bets"` + StatusToBeFixedCount int `json:"status_to_be_fixed_count"` + StatusToBeFixedBets int `json:"status_to_be_fixed_bets"` + StatusPostponedCount int `json:"status_postponed_count"` + StatusPostponedBets int `json:"status_postponed_bets"` + StatusEndedCount int `json:"status_ended_count"` + StatusEndedBets int `json:"status_ended_bets"` + StatusRemovedCount int `json:"status_removed_count"` + StatusRemovedBets int `json:"status_removed_bets"` + RemovedCount int `json:"removed"` +} + +type ResultLogFilter struct { + CreatedBefore ValidTime + CreatedAfter ValidTime +} +type ResultStatusBets struct { + StatusNotFinished []int64 `json:"status_not_finished"` + StatusToBeFixed []int64 `json:"status_to_be_fixed"` + StatusPostponed []int64 `json:"status_postponed"` + StatusEnded []int64 `json:"status_ended"` + StatusRemoved []int64 `json:"status_removed"` +} + +func ConvertDBResultLog(result dbgen.ResultLog) ResultLog { + return ResultLog{ + ID: result.ID, + StatusNotFinishedCount: int(result.StatusNotFinishedCount), + StatusNotFinishedBets: int(result.StatusNotFinishedBets), + StatusToBeFixedCount: int(result.StatusToBeFixedCount), + StatusToBeFixedBets: int(result.StatusToBeFixedBets), + StatusPostponedCount: int(result.StatusPostponedCount), + StatusPostponedBets: int(result.StatusPostponedBets), + StatusEndedCount: int(result.StatusEndedCount), + StatusEndedBets: int(result.StatusEndedBets), + StatusRemovedCount: int(result.StatusRemovedCount), + StatusRemovedBets: int(result.StatusRemovedBets), + RemovedCount: int(result.RemovedCount), + CreatedAt: result.CreatedAt.Time, + } +} + +func ConvertCreateResultLog(result CreateResultLog) dbgen.CreateResultLogParams { + return dbgen.CreateResultLogParams{ + StatusNotFinishedCount: int32(result.StatusNotFinishedCount), + StatusNotFinishedBets: int32(result.StatusNotFinishedBets), + StatusToBeFixedCount: int32(result.StatusToBeFixedCount), + StatusToBeFixedBets: int32(result.StatusToBeFixedBets), + StatusPostponedCount: int32(result.StatusPostponedCount), + StatusPostponedBets: int32(result.StatusPostponedBets), + StatusEndedCount: int32(result.StatusEndedCount), + StatusEndedBets: int32(result.StatusEndedBets), + StatusRemovedCount: int32(result.StatusRemovedCount), + StatusRemovedBets: int32(result.StatusRemovedBets), + RemovedCount: int32(result.RemovedCount), + } +} diff --git a/internal/domain/setting_list.go b/internal/domain/setting_list.go index 5a5c86e..bd30ad9 100644 --- a/internal/domain/setting_list.go +++ b/internal/domain/setting_list.go @@ -18,9 +18,11 @@ var ( type SettingList struct { SMSProvider SMSProvider `json:"sms_provider"` MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"` + MaxUnsettledBets int64 `json:"max_unsettled_bets"` BetAmountLimit Currency `json:"bet_amount_limit"` DailyTicketPerIP int64 `json:"daily_ticket_limit"` TotalWinningLimit Currency `json:"total_winning_limit"` + TotalWinningNotify Currency `json:"total_winning_notify"` AmountForBetReferral Currency `json:"amount_for_bet_referral"` CashbackAmountCap Currency `json:"cashback_amount_cap"` DefaultWinningLimit int64 `json:"default_winning_limit"` @@ -41,9 +43,11 @@ type SettingList struct { type SettingListRes struct { SMSProvider SMSProvider `json:"sms_provider"` MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"` + MaxUnsettledBets int64 `json:"max_unsettled_bets"` BetAmountLimit float32 `json:"bet_amount_limit"` DailyTicketPerIP int64 `json:"daily_ticket_limit"` TotalWinningLimit float32 `json:"total_winning_limit"` + TotalWinningNotify float32 `json:"total_winning_notify"` AmountForBetReferral float32 `json:"amount_for_bet_referral"` CashbackAmountCap float32 `json:"cashback_amount_cap"` DefaultWinningLimit int64 `json:"default_winning_limit"` @@ -65,9 +69,11 @@ func ConvertSettingListRes(settings SettingList) SettingListRes { return SettingListRes{ SMSProvider: settings.SMSProvider, MaxNumberOfOutcomes: settings.MaxNumberOfOutcomes, + MaxUnsettledBets: settings.MaxUnsettledBets, BetAmountLimit: settings.BetAmountLimit.Float32(), DailyTicketPerIP: settings.DailyTicketPerIP, TotalWinningLimit: settings.TotalWinningLimit.Float32(), + TotalWinningNotify: settings.TotalWinningNotify.Float32(), AmountForBetReferral: settings.AmountForBetReferral.Float32(), CashbackAmountCap: settings.CashbackAmountCap.Float32(), DefaultWinningLimit: settings.DefaultWinningLimit, @@ -89,32 +95,36 @@ func ConvertSettingListRes(settings SettingList) SettingListRes { type SaveSettingListReq struct { SMSProvider *string `json:"sms_provider,omitempty"` MaxNumberOfOutcomes *int64 `json:"max_number_of_outcomes,omitempty"` + MaxUnsettledBets *int64 `json:"max_unsettled_bets,omitempty"` BetAmountLimit *float32 `json:"bet_amount_limit,omitempty"` DailyTicketPerIP *int64 `json:"daily_ticket_limit,omitempty"` TotalWinningLimit *float32 `json:"total_winning_limit,omitempty"` + TotalWinningNotify *float32 `json:"total_winning_notify,omitempty"` AmountForBetReferral *float32 `json:"amount_for_bet_referral,omitempty"` CashbackAmountCap *float32 `json:"cashback_amount_cap,omitempty"` DefaultWinningLimit *int64 `json:"default_winning_limit,omitempty"` - ReferralRewardAmount *float32 `json:"referral_reward_amount"` - CashbackPercentage *float32 `json:"cashback_percentage"` - DefaultMaxReferrals *int64 `json:"default_max_referrals"` - MinimumBetAmount *float32 `json:"minimum_bet_amount"` - BetDuplicateLimit *int64 `json:"bet_duplicate_limit"` - SendEmailOnBetFinish *bool `json:"send_email_on_bet_finish"` - SendSMSOnBetFinish *bool `json:"send_sms_on_bet_finish"` - WelcomeBonusActive *bool `json:"welcome_bonus_active"` - WelcomeBonusMultiplier *float32 `json:"welcome_bonus_multiplier"` - WelcomeBonusCap *float32 `json:"welcome_bonus_cap"` - WelcomeBonusCount *int64 `json:"welcome_bonus_count"` - WelcomeBonusExpire *int64 `json:"welcome_bonus_expiry"` + ReferralRewardAmount *float32 `json:"referral_reward_amount,omitempty"` + CashbackPercentage *float32 `json:"cashback_percentage,omitempty"` + DefaultMaxReferrals *int64 `json:"default_max_referrals,omitempty"` + MinimumBetAmount *float32 `json:"minimum_bet_amount,omitempty"` + BetDuplicateLimit *int64 `json:"bet_duplicate_limit,omitempty"` + SendEmailOnBetFinish *bool `json:"send_email_on_bet_finish,omitempty"` + SendSMSOnBetFinish *bool `json:"send_sms_on_bet_finish,omitempty"` + WelcomeBonusActive *bool `json:"welcome_bonus_active,omitempty"` + WelcomeBonusMultiplier *float32 `json:"welcome_bonus_multiplier,omitempty"` + WelcomeBonusCap *float32 `json:"welcome_bonus_cap,omitempty"` + WelcomeBonusCount *int64 `json:"welcome_bonus_count,omitempty"` + WelcomeBonusExpire *int64 `json:"welcome_bonus_expiry,omitempty"` } type ValidSettingList struct { SMSProvider ValidString MaxNumberOfOutcomes ValidInt64 + MaxUnsettledBets ValidInt64 BetAmountLimit ValidCurrency DailyTicketPerIP ValidInt64 TotalWinningLimit ValidCurrency + TotalWinningNotify ValidCurrency AmountForBetReferral ValidCurrency CashbackAmountCap ValidCurrency DefaultWinningLimit ValidInt64 @@ -136,9 +146,11 @@ func ConvertSaveSettingListReq(settings SaveSettingListReq) ValidSettingList { return ValidSettingList{ SMSProvider: ConvertStringPtr(settings.SMSProvider), MaxNumberOfOutcomes: ConvertInt64Ptr(settings.MaxNumberOfOutcomes), + MaxUnsettledBets: ConvertInt64Ptr(settings.MaxUnsettledBets), BetAmountLimit: ConvertFloat32PtrToCurrency(settings.BetAmountLimit), DailyTicketPerIP: ConvertInt64Ptr(settings.DailyTicketPerIP), TotalWinningLimit: ConvertFloat32PtrToCurrency(settings.TotalWinningLimit), + TotalWinningNotify: ConvertFloat32PtrToCurrency(settings.TotalWinningNotify), AmountForBetReferral: ConvertFloat32PtrToCurrency(settings.AmountForBetReferral), CashbackAmountCap: ConvertFloat32PtrToCurrency(settings.CashbackAmountCap), DefaultWinningLimit: ConvertInt64Ptr(settings.DefaultWinningLimit), @@ -162,9 +174,11 @@ func (vsl *ValidSettingList) ToSettingList() SettingList { return SettingList{ SMSProvider: SMSProvider(vsl.SMSProvider.Value), MaxNumberOfOutcomes: vsl.MaxNumberOfOutcomes.Value, + MaxUnsettledBets: vsl.MaxUnsettledBets.Value, BetAmountLimit: vsl.BetAmountLimit.Value, DailyTicketPerIP: vsl.DailyTicketPerIP.Value, TotalWinningLimit: vsl.TotalWinningLimit.Value, + TotalWinningNotify: vsl.TotalWinningNotify.Value, AmountForBetReferral: vsl.AmountForBetReferral.Value, CashbackAmountCap: vsl.CashbackAmountCap.Value, DefaultWinningLimit: vsl.DefaultWinningLimit.Value, @@ -194,6 +208,7 @@ func (vsl *ValidSettingList) CustomValidationSettings() error { func (vsl *ValidSettingList) GetInt64SettingsMap() map[string]*ValidInt64 { return map[string]*ValidInt64{ "max_number_of_outcomes": &vsl.MaxNumberOfOutcomes, + "max_unsettled_bets": &vsl.MaxUnsettledBets, "daily_ticket_limit": &vsl.DailyTicketPerIP, "default_winning_limit": &vsl.DefaultWinningLimit, "default_max_referrals": &vsl.DefaultMaxReferrals, @@ -207,6 +222,7 @@ func (vsl *ValidSettingList) GetCurrencySettingsMap() map[string]*ValidCurrency return map[string]*ValidCurrency{ "bet_amount_limit": &vsl.BetAmountLimit, "total_winnings_limit": &vsl.TotalWinningLimit, + "total_winnings_notify": &vsl.TotalWinningNotify, "amount_for_bet_referral": &vsl.AmountForBetReferral, "cashback_amount_cap": &vsl.CashbackAmountCap, "referral_reward_amount": &vsl.ReferralRewardAmount, diff --git a/internal/event/wallet_event.go b/internal/event/wallet_event.go index 129104e..54fc158 100644 --- a/internal/event/wallet_event.go +++ b/internal/event/wallet_event.go @@ -10,9 +10,10 @@ const ( ) type WalletEvent struct { - EventType WalletEventType `json:"event_type"` - WalletID int64 `json:"wallet_id"` - UserID int64 `json:"user_id"` - Balance domain.Currency `json:"balance"` - Trigger string `json:"trigger"` // e.g. "AddToWallet", "DeductFromWallet" + EventType WalletEventType `json:"event_type"` + WalletID int64 `json:"wallet_id"` + UserID int64 `json:"user_id"` + Balance domain.Currency `json:"balance"` + WalletType domain.WalletType `json:"wallet_type"` + Trigger string `json:"trigger"` // e.g. "AddToWallet", "DeductFromWallet" } diff --git a/internal/repository/bet.go b/internal/repository/bet.go index 09a667b..bb2a7d7 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -93,30 +93,44 @@ func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) return domain.ConvertDBBetWithOutcomes(bet), nil } -func (s *Store) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]domain.GetBet, error) { +func (s *Store) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]domain.GetBet, int64, error) { bets, err := s.queries.GetAllBets(ctx, dbgen.GetAllBetsParams{ UserID: filter.UserID.ToPG(), CompanyID: filter.CompanyID.ToPG(), + Status: filter.Status.ToPG(), CashedOut: filter.CashedOut.ToPG(), IsShopBet: filter.IsShopBet.ToPG(), Query: filter.Query.ToPG(), CreatedBefore: filter.CreatedBefore.ToPG(), CreatedAfter: filter.CreatedAfter.ToPG(), + Offset: filter.Offset.ToPG(), + Limit: filter.Limit.ToPG(), }) if err != nil { domain.MongoDBLogger.Error("failed to get all bets", zap.Any("filter", filter), zap.Error(err), ) - return nil, err + return nil, 0, err } + total, err := s.queries.GetTotalBets(ctx, dbgen.GetTotalBetsParams{ + UserID: filter.UserID.ToPG(), + CompanyID: filter.CompanyID.ToPG(), + Status: filter.Status.ToPG(), + CashedOut: filter.CashedOut.ToPG(), + IsShopBet: filter.IsShopBet.ToPG(), + 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 { result = append(result, domain.ConvertDBBetWithOutcomes(bet)) } - return result, nil + return result, total, nil } func (s *Store) GetBetByUserID(ctx context.Context, UserID int64) ([]domain.GetBet, error) { diff --git a/internal/repository/event.go b/internal/repository/event.go index 5458e6e..b1e5c56 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -293,7 +293,7 @@ func (s *Store) DeleteEvent(ctx context.Context, eventID int64) error { return nil } -func (s *Store) GetSportAndLeagueIDs(ctx context.Context, eventID string) ([]int64, error) { +func (s *Store) GetSportAndLeagueIDs(ctx context.Context, eventID int64) ([]int64, error) { sportAndLeagueIDs, err := s.queries.GetSportAndLeagueIDs(ctx, eventID) if err != nil { return nil, err diff --git a/internal/repository/notification.go b/internal/repository/notification.go index 29e7b8c..eea21e5 100644 --- a/internal/repository/notification.go +++ b/internal/repository/notification.go @@ -14,7 +14,7 @@ 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) - ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, 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) @@ -96,25 +96,31 @@ func (r *Repository) UpdateNotificationStatus(ctx context.Context, id, status st return r.mapDBToDomain(&dbNotification), nil } -func (r *Repository) ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error) { - params := dbgen.ListNotificationsParams{ +func (r *Repository) GetUserNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, int64, error) { + params := dbgen.GetUserNotificationsParams{ RecipientID: recipientID, Limit: int32(limit), Offset: int32(offset), } - dbNotifications, err := r.store.queries.ListNotifications(ctx, params) + dbNotifications, err := r.store.queries.GetUserNotifications(ctx, params) if err != nil { - return nil, err + return nil, 0, err } + total, err := r.store.queries.GetUserNotificationCount(ctx, recipientID) + + 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) result = append(result, *domainNotif) } - return result, nil + return result, total, nil } func (r *Repository) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) { @@ -158,7 +164,7 @@ func (r *Repository) mapDBToDomain(dbNotif *dbgen.Notification) *domain.Notifica var errorSeverity domain.NotificationErrorSeverity if dbNotif.ErrorSeverity.Valid { errorSeverity = domain.NotificationErrorSeverity(dbNotif.ErrorSeverity.String) - + } else { errorSeverity = "" } diff --git a/internal/repository/result.go b/internal/repository/result.go index ed4aa89..2dda4bd 100644 --- a/internal/repository/result.go +++ b/internal/repository/result.go @@ -8,9 +8,6 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) - - - func (s *Store) CreateResultLog(ctx context.Context, result domain.CreateResultLog) (domain.ResultLog, error) { dbResult, err := s.queries.CreateResultLog(ctx, domain.ConvertCreateResultLog(result)) if err != nil { @@ -19,7 +16,7 @@ func (s *Store) CreateResultLog(ctx context.Context, result domain.CreateResultL return domain.ConvertDBResultLog(dbResult), nil } -func (s *Store) GetAllResultLog(ctx context.Context, filter domain.ResultFilter) ([]domain.ResultLog, error) { +func (s *Store) GetAllResultLog(ctx context.Context, filter domain.ResultLogFilter) ([]domain.ResultLog, error) { dbResultLogs, err := s.queries.GetAllResultLog(ctx, dbgen.GetAllResultLogParams{ CreatedBefore: pgtype.Timestamp{ Time: filter.CreatedBefore.Value, diff --git a/internal/repository/user.go b/internal/repository/user.go index 50c6593..8102bb6 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -260,7 +260,7 @@ func (s *Store) SearchUserByNameOrPhone(ctx context.Context, searchString string query := dbgen.SearchUserByNameOrPhoneParams{ CompanyID: companyID.ToPG(), - Column2: pgtype.Text{ + Column1: pgtype.Text{ String: searchString, Valid: true, }, diff --git a/internal/services/bet/notification.go b/internal/services/bet/notification.go index d239e4f..e9dd185 100644 --- a/internal/services/bet/notification.go +++ b/internal/services/bet/notification.go @@ -245,19 +245,10 @@ func (s *Service) SendAdminErrorNotification(ctx context.Context, betID int64, s return nil } -func (s *Service) SendAdminLargeNotification(ctx context.Context, betID int64, status domain.OutcomeStatus, extra string, companyID int64) error { +func (s *Service) SendAdminLargeBetNotification(ctx context.Context, betID int64, totalWinnings float32, extra string, companyID int64) error { - var headline string - var message string - - switch status { - case domain.OUTCOME_STATUS_ERROR, domain.OUTCOME_STATUS_PENDING: - headline = fmt.Sprintf("Processing Error for Bet #%v", betID) - message = "A processing error occurred with this bet. Please review and take corrective action." - - default: - return fmt.Errorf("unsupported status: %v", status) - } + headline := fmt.Sprintf("SYSTEM WARNING: High Risk Bet", betID, totalWinnings) + message := fmt.Sprintf("Bet #%d has been created with %v payout", betID, totalWinnings) super_admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{ Role: string(domain.RoleSuperAdmin), @@ -294,10 +285,27 @@ func (s *Service) SendAdminLargeNotification(ctx context.Context, betID int64, s domain.DeliveryChannelInApp, domain.DeliveryChannelEmail, } { - n := newBetResultNotification(user.ID, domain.NotificationLevelError, channel, headline, message, map[string]any{ - "status": status, - "more": extra, + raw, _ := json.Marshal(map[string]any{ + "winnings": totalWinnings, + "more": extra, }) + + n := &domain.Notification{ + RecipientID: user.ID, + DeliveryStatus: domain.DeliveryStatusPending, + IsRead: false, + Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT, + Level: domain.NotificationLevelWarning, + Reciever: domain.NotificationRecieverSideAdmin, + DeliveryChannel: channel, + Payload: domain.NotificationPayload{ + Headline: headline, + Message: message, + }, + Priority: 2, + Metadata: raw, + } + // n := newBetResultNotification(user.ID, domain.NotificationLevelWarning, channel, headline, message) if err := s.notificationSvc.SendNotification(ctx, n); err != nil { return err } diff --git a/internal/services/bet/port.go b/internal/services/bet/port.go index e29b68e..dc582b4 100644 --- a/internal/services/bet/port.go +++ b/internal/services/bet/port.go @@ -12,7 +12,7 @@ type BetStore interface { CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBetOutcome) (int64, error) CreateFlag(ctx context.Context, flag domain.CreateFlagReq) (domain.Flag, error) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) - GetAllBets(ctx context.Context, filter domain.BetFilter) ([]domain.GetBet, error) + 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) GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error) diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index 4f869ed..009f6fd 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -42,6 +42,7 @@ var ( ErrOutcomeLimit = errors.New("too many outcomes on a single bet") ErrTotalBalanceNotEnough = errors.New("total Wallet balance is insufficient to create bet") + ErrTooManyUnsettled = errors.New("too many unsettled bets") 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") @@ -215,6 +216,25 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID if err != nil { return domain.CreateBetRes{}, err } + + _, totalUnsettledBets, err := s.GetAllBets(ctx, domain.BetFilter{ + Status: domain.ValidOutcomeStatus{ + Value: domain.OUTCOME_STATUS_ERROR, + Valid: true, + }, + }) + + if err != nil { + return domain.CreateBetRes{}, err + } + + if totalUnsettledBets > settingsList.MaxUnsettledBets { + s.mongoLogger.Error("System block bet creation until unsettled bets fixed", + zap.Int64("total_unsettled_bets", totalUnsettledBets), + zap.Int64("user_id", userID), + ) + return domain.CreateBetRes{}, ErrTooManyUnsettled + } if req.Amount < settingsList.MinimumBetAmount.Float32() { return domain.CreateBetRes{}, ErrInvalidAmount } @@ -283,7 +303,8 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID } fastCode := helpers.GenerateFastCode() - amount := req.Amount + (req.Amount * calculateAccumulator(len(outcomes))) + accumulator := calculateAccumulator(len(outcomes)) + amount := req.Amount + (req.Amount * accumulator) newBet := domain.CreateBet{ Amount: domain.ToCurrency(amount), @@ -316,6 +337,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID return domain.CreateBetRes{}, err } + // For case domain.RoleBranchManager, domain.RoleAdmin, domain.RoleSuperAdmin: newBet.IsShopBet = true // Branch Manager, Admin and Super Admin are required to pass a branch id if they want to create a bet @@ -367,7 +389,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID newBet.IsShopBet = false err = s.DeductBetFromCustomerWallet(ctx, req.Amount, userID) if err != nil { - s.mongoLogger.Error("customer wallet deduction failed", + s.mongoLogger.Warn("customer wallet deduction failed", zap.Float32("amount", req.Amount), zap.Int64("user_id", userID), zap.Error(err), @@ -477,6 +499,14 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID } } + if totalWinnings > settingsList.TotalWinningNotify.Float32() { + err = s.SendAdminLargeBetNotification(ctx, bet.ID, totalWinnings, "", companyID) + if err != nil { + s.mongoLogger.Error("Failed to send large bet notification", zap.Int64("betID", bet.ID), + zap.Int64("companyID", companyID), zap.Float32("totalWinnings", totalWinnings)) + } + } + res := domain.ConvertCreateBetRes(bet, rows) return res, nil @@ -557,7 +587,7 @@ func (s *Service) DeductBetFromCustomerWallet(ctx context.Context, amount float3 return err } // Empty remaining from static balance - remainingAmount := wallets.RegularBalance - domain.Currency(amount) + remainingAmount := wallets.RegularBalance - domain.ToCurrency(amount) _, err = s.walletSvc.DeductFromWallet(ctx, wallets.StaticID, remainingAmount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, fmt.Sprintf("Deducted %v amount from wallet by system while placing bet", remainingAmount.Float32())) @@ -806,7 +836,7 @@ func (s *Service) CreateBetOutcome(ctx context.Context, outcomes []domain.Create func (s *Service) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) { return s.betStore.GetBetByID(ctx, id) } -func (s *Service) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]domain.GetBet, error) { +func (s *Service) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]domain.GetBet, int64, error) { return s.betStore.GetAllBets(ctx, filter) } diff --git a/internal/services/bonus/notification.go b/internal/services/bonus/notification.go index 8497a53..ae20a36 100644 --- a/internal/services/bonus/notification.go +++ b/internal/services/bonus/notification.go @@ -40,7 +40,7 @@ func (s *Service) SendBonusNotification(ctx context.Context, param SendBonusNoti headline = "You've been awarded a welcome bonus!" message = fmt.Sprintf( "Congratulations! A you've been given %.2f as a welcome bonus for you to bet on.", - param.Amount, + param.Amount.Float32(), ) default: return fmt.Errorf("unsupported bonus type: %v", param.Type) diff --git a/internal/services/event/port.go b/internal/services/event/port.go index faf2957..546699e 100644 --- a/internal/services/event/port.go +++ b/internal/services/event/port.go @@ -19,5 +19,5 @@ type Service interface { 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 - GetSportAndLeagueIDs(ctx context.Context, eventID string) ([]int64, error) + GetSportAndLeagueIDs(ctx context.Context, eventID int64) ([]int64, error) } diff --git a/internal/services/event/service.go b/internal/services/event/service.go index b61391c..5bc27e6 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -225,7 +225,7 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_ur // Restricting the page to 1 on development, which drastically reduces the amount of events that is fetched if s.cfg.Env == "development" { - pageLimit = 1 + pageLimit = 2 sportIDs = []int{1} } else { pageLimit = 200 @@ -465,7 +465,6 @@ func (s *service) GetAllEvents(ctx context.Context, filter domain.EventFilter) ( return s.store.GetAllEvents(ctx, filter) } - func (s *service) GetEventByID(ctx context.Context, ID int64) (domain.BaseEvent, error) { return s.store.GetEventByID(ctx, ID) } @@ -496,6 +495,6 @@ func (s *service) UpdateEventSettings(ctx context.Context, event domain.CreateEv return s.store.UpdateEventSettings(ctx, event) } -func (s *service) GetSportAndLeagueIDs(ctx context.Context, eventID string) ([]int64, error) { +func (s *service) GetSportAndLeagueIDs(ctx context.Context, eventID int64) ([]int64, error) { return s.store.GetSportAndLeagueIDs(ctx, eventID) } diff --git a/internal/services/kafka/consumer.go b/internal/services/kafka/consumer.go index c9775f8..c2343fe 100644 --- a/internal/services/kafka/consumer.go +++ b/internal/services/kafka/consumer.go @@ -52,6 +52,7 @@ func (c *WalletConsumer) Start(ctx context.Context) { "wallet_id": evt.WalletID, "user_id": evt.UserID, "balance": evt.Balance, + "wallet_type": evt.WalletType, "trigger": evt.Trigger, "recipient_id": evt.UserID, } diff --git a/internal/services/notification/port.go b/internal/services/notification/port.go index 2d03f80..359148b 100644 --- a/internal/services/notification/port.go +++ b/internal/services/notification/port.go @@ -10,7 +10,7 @@ import ( type NotificationStore interface { SendNotification(ctx context.Context, notification *domain.Notification) error MarkAsRead(ctx context.Context, notificationID string, recipientID int64) error - ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error) + GetUserNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, int64, error) ConnectWebSocket(ctx context.Context, recipientID int64, c *websocket.Conn) error DisconnectWebSocket(recipientID int64) SendSMS(ctx context.Context, recipientID int64, message string) error diff --git a/internal/services/notification/service.go b/internal/services/notification/service.go index 6ba4044..bc31c02 100644 --- a/internal/services/notification/service.go +++ b/internal/services/notification/service.go @@ -12,10 +12,12 @@ 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" "go.uber.org/zap" // "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" @@ -38,6 +40,7 @@ type Service struct { mongoLogger *zap.Logger logger *slog.Logger redisClient *redis.Client + reader *kafka.Reader } func New(repo repository.NotificationRepository, @@ -46,11 +49,17 @@ 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, @@ -64,12 +73,14 @@ func New(repo repository.NotificationRepository, 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()) return svc } @@ -167,24 +178,25 @@ func (s *Service) MarkAsRead(ctx context.Context, notificationIDs []string, reci return nil } -func (s *Service) ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error) { - notifications, err := s.repo.ListNotifications(ctx, recipientID, limit, offset) +func (s *Service) GetUserNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, int64, error) { + notifications, total, err := s.repo.GetUserNotifications(ctx, recipientID, limit, offset) if err != nil { - s.mongoLogger.Error("[NotificationSvc.ListNotifications] Failed to list notifications", + s.mongoLogger.Error("[NotificationSvc.GetUserNotifications] Failed to list notifications", zap.Int64("recipientID", recipientID), zap.Int("limit", limit), zap.Int("offset", offset), zap.Error(err), zap.Time("timestamp", time.Now()), ) - return nil, err + return nil, 0, err } - s.mongoLogger.Info("[NotificationSvc.ListNotifications] Successfully listed notifications", + s.mongoLogger.Info("[NotificationSvc.GetUserNotifications] Successfully listed notifications", zap.Int64("recipientID", recipientID), zap.Int("count", len(notifications)), + zap.Int64("total", total), zap.Time("timestamp", time.Now()), ) - return notifications, nil + return notifications, total, nil } func (s *Service) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) { @@ -574,6 +586,88 @@ func (s *Service) GetLiveMetrics(ctx context.Context) (domain.LiveMetric, error) 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 + } + + 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(), + }) + + 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, + }, + } + + 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 ( // payload domain.LiveWalletMetrics diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index fcbabb3..0a9f401 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -80,10 +80,6 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { func (s *ServiceImpl) ProcessBet365Odds(ctx context.Context) error { eventIDs, _, err := s.eventSvc.GetAllEvents(ctx, domain.EventFilter{ - LastStartTime: domain.ValidTime{ - Value: time.Now(), - Valid: true, - }, Status: domain.ValidEventStatus{ Value: domain.STATUS_PENDING, Valid: true, diff --git a/internal/services/result/port.go b/internal/services/result/port.go index 8890385..b472c6d 100644 --- a/internal/services/result/port.go +++ b/internal/services/result/port.go @@ -13,5 +13,5 @@ type ResultService interface { type ResultLogStore interface { CreateResultLog(ctx context.Context, result domain.CreateResultLog) (domain.ResultLog, error) - GetAllResultLog(ctx context.Context, filter domain.ResultFilter) ([]domain.ResultLog, error) + GetAllResultLog(ctx context.Context, filter domain.ResultLogFilter) ([]domain.ResultLog, error) } diff --git a/internal/services/result/service.go b/internal/services/result/service.go index 48cebb0..698e743 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -459,7 +459,7 @@ func (s *Service) FetchB365ResultAndUpdateBets(ctx context.Context) error { func (s *Service) CheckAndSendResultNotifications(ctx context.Context, createdAfter time.Time) error { - resultLog, err := s.repo.GetAllResultLog(ctx, domain.ResultFilter{ + resultLog, err := s.repo.GetAllResultLog(ctx, domain.ResultLogFilter{ CreatedAfter: domain.ValidTime{ Value: createdAfter, Valid: true, @@ -557,10 +557,10 @@ func buildHeadlineAndMessageEmail(counts domain.ResultLog, user domain.User) (st greeting := fmt.Sprintf("Hi %s %s,", user.FirstName, user.LastName) if totalIssues == 0 { - headline := "✅ Daily Results Report — All Events Processed Successfully" + headline := "✅ Weekly Results Report — All Events Processed Successfully" plain := fmt.Sprintf(`%s -Daily Results Summary: +Weekly Results Summary: - %d Ended Events - %d Total Bets @@ -570,7 +570,7 @@ Best regards, The System`, greeting, counts.StatusEndedCount, totalBets) html := fmt.Sprintf(`

%s

-

Daily Results Summary

+

Weekly Results Summary