diff --git a/db/migrations/000001_fortune.down.sql b/db/migrations/000001_fortune.down.sql index 2332e39..d854608 100644 --- a/db/migrations/000001_fortune.down.sql +++ b/db/migrations/000001_fortune.down.sql @@ -79,4 +79,6 @@ DROP TABLE IF EXISTS events; DROP TABLE IF EXISTS leagues; DROP TABLE IF EXISTS teams; DROP TABLE IF EXISTS settings; +DROP TABLE IF EXISTS bonus; +DROP TABLE IF EXISTS flags; -- DELETE FROM wallet_transfer; \ No newline at end of file diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index a5ebe1c..77ef8ee 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -208,7 +208,7 @@ CREATE TABLE IF NOT EXISTS branches ( id BIGSERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, location TEXT NOT NULL, - profit_percent REAL NOt NULL, + profit_percent REAL NOT NULL, is_active BOOLEAN NOT NULL DEFAULT false, wallet_id BIGINT NOT NULL, branch_manager_id BIGINT NOT NULL, @@ -328,6 +328,21 @@ CREATE TABLE bonus ( multiplier REAL NOT NULL, balance_cap BIGINT NOT NULL DEFAULT 0 ); +CREATE TABLE flags ( + id BIGSERIAL PRIMARY KEY, + bet_id BIGINT REFERENCES bets(id) ON DELETE CASCADE, + odd_id BIGINT REFERENCES odds(id), + reason TEXT, + flagged_at TIMESTAMP DEFAULT NOW(), + resolved BOOLEAN DEFAULT FALSE, + + -- either bet or odd is flagged (not at the same time) + CHECK ( + (bet_id IS NOT NULL AND odd_id IS NULL) + OR + (bet_id IS NULL AND odd_id IS NOT NULL) + ) +); -- Views CREATE VIEW companies_details AS SELECT companies.*, diff --git a/db/query/bet.sql b/db/query/bet.sql index 8cca28e..47018e1 100644 --- a/db/query/bet.sql +++ b/db/query/bet.sql @@ -101,11 +101,19 @@ WHERE (event_id = $1) SELECT * FROM bet_outcomes WHERE bet_id = $1; --- name: GetBetCount :one +-- name: GetBetOutcomeCountByOddID :one +SELECT COUNT(*) +FROM bet_outcomes +WHERE odd_id = $1; +-- name: GetBetCountByUserID :one SELECT COUNT(*) FROM bets WHERE user_id = $1 AND outcomes_hash = $2; +-- name: GetBetCountByOutcomesHash :one +SELECT COUNT(*) +FROM bets +WHERE outcomes_hash = $1; -- name: UpdateCashOut :exec UPDATE bets SET cashed_out = $2, diff --git a/db/query/flags.sql b/db/query/flags.sql new file mode 100644 index 0000000..60093c5 --- /dev/null +++ b/db/query/flags.sql @@ -0,0 +1,8 @@ +-- name: CreateFlag :one +INSERT INTO flags ( + bet_id, + odd_id, + reason +) VALUES ( + $1, $2, $3 +) RETURNING *; \ No newline at end of file diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index cae3d8b..31ca511 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -282,20 +282,33 @@ func (q *Queries) GetBetByUserID(ctx context.Context, userID int64) ([]BetWithOu return items, nil } -const GetBetCount = `-- name: GetBetCount :one +const GetBetCountByOutcomesHash = `-- name: GetBetCountByOutcomesHash :one +SELECT COUNT(*) +FROM bets +WHERE outcomes_hash = $1 +` + +func (q *Queries) GetBetCountByOutcomesHash(ctx context.Context, outcomesHash string) (int64, error) { + row := q.db.QueryRow(ctx, GetBetCountByOutcomesHash, outcomesHash) + var count int64 + err := row.Scan(&count) + return count, err +} + +const GetBetCountByUserID = `-- name: GetBetCountByUserID :one SELECT COUNT(*) FROM bets WHERE user_id = $1 AND outcomes_hash = $2 ` -type GetBetCountParams struct { +type GetBetCountByUserIDParams struct { UserID int64 `json:"user_id"` OutcomesHash string `json:"outcomes_hash"` } -func (q *Queries) GetBetCount(ctx context.Context, arg GetBetCountParams) (int64, error) { - row := q.db.QueryRow(ctx, GetBetCount, arg.UserID, arg.OutcomesHash) +func (q *Queries) GetBetCountByUserID(ctx context.Context, arg GetBetCountByUserIDParams) (int64, error) { + row := q.db.QueryRow(ctx, GetBetCountByUserID, arg.UserID, arg.OutcomesHash) var count int64 err := row.Scan(&count) return count, err @@ -397,6 +410,19 @@ func (q *Queries) GetBetOutcomeByEventID(ctx context.Context, arg GetBetOutcomeB return items, nil } +const GetBetOutcomeCountByOddID = `-- name: GetBetOutcomeCountByOddID :one +SELECT COUNT(*) +FROM bet_outcomes +WHERE odd_id = $1 +` + +func (q *Queries) GetBetOutcomeCountByOddID(ctx context.Context, oddID int64) (int64, error) { + row := q.db.QueryRow(ctx, GetBetOutcomeCountByOddID, oddID) + var count int64 + err := row.Scan(&count) + return count, err +} + const GetBetsForCashback = `-- name: GetBetsForCashback :many SELECT 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 diff --git a/gen/db/flags.sql.go b/gen/db/flags.sql.go new file mode 100644 index 0000000..17b406e --- /dev/null +++ b/gen/db/flags.sql.go @@ -0,0 +1,42 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: flags.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const CreateFlag = `-- name: CreateFlag :one +INSERT INTO flags ( + bet_id, + odd_id, + reason +) VALUES ( + $1, $2, $3 +) RETURNING id, bet_id, odd_id, reason, flagged_at, resolved +` + +type CreateFlagParams struct { + BetID pgtype.Int8 `json:"bet_id"` + OddID pgtype.Int8 `json:"odd_id"` + Reason pgtype.Text `json:"reason"` +} + +func (q *Queries) CreateFlag(ctx context.Context, arg CreateFlagParams) (Flag, error) { + row := q.db.QueryRow(ctx, CreateFlag, arg.BetID, arg.OddID, arg.Reason) + var i Flag + err := row.Scan( + &i.ID, + &i.BetID, + &i.OddID, + &i.Reason, + &i.FlaggedAt, + &i.Resolved, + ) + return i, err +} diff --git a/gen/db/models.go b/gen/db/models.go index 15f24e5..705b55f 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -277,6 +277,15 @@ type FavoriteGame struct { CreatedAt pgtype.Timestamp `json:"created_at"` } +type Flag struct { + ID int64 `json:"id"` + BetID pgtype.Int8 `json:"bet_id"` + OddID pgtype.Int8 `json:"odd_id"` + Reason pgtype.Text `json:"reason"` + FlaggedAt pgtype.Timestamp `json:"flagged_at"` + Resolved pgtype.Bool `json:"resolved"` +} + type League struct { ID int64 `json:"id"` Name string `json:"name"` diff --git a/internal/domain/bet.go b/internal/domain/bet.go index 53522ab..83c537e 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -61,6 +61,15 @@ type BetFilter struct { CreatedAfter ValidTime } +type Flag struct { + ID int64 + BetID int64 + OddID int64 + Reason string + FlaggedAt time.Time + Resolved bool +} + type GetBet struct { ID int64 Amount Currency @@ -95,7 +104,7 @@ type CreateBetOutcomeReq struct { type CreateBetReq struct { Outcomes []CreateBetOutcomeReq `json:"outcomes" validate:"required"` Amount float32 `json:"amount" validate:"required,gt=0" example:"100.0"` - BranchID *int64 `json:"branch_id,omitempty" example:"1"` + BranchID *int64 `json:"branch_id,omitempty" validate:"required" example:"1"` } type CreateBetWithFastCodeReq struct { @@ -104,6 +113,12 @@ type CreateBetWithFastCodeReq struct { BranchID *int64 `json:"branch_id"` } +type CreateFlagReq struct { + BetID int64 + OddID int64 + Reason string +} + type RandomBetReq struct { BranchID int64 `json:"branch_id" validate:"required" example:"1"` NumberOfBets int64 `json:"number_of_bets" validate:"required" example:"1"` diff --git a/internal/repository/bet.go b/internal/repository/bet.go index bdf83d3..2dee7b1 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -76,6 +76,17 @@ func convertDBBetWithOutcomes(bet dbgen.BetWithOutcome) domain.GetBet { } } +func convertDBFlag(flag dbgen.Flag) domain.Flag { + return domain.Flag{ + ID: flag.ID, + BetID: flag.BetID.Int64, + OddID: flag.OddID.Int64, + Reason: flag.Reason.String, + FlaggedAt: flag.FlaggedAt.Time, + Resolved: flag.Resolved.Bool, + } +} + func convertDBCreateBetOutcome(betOutcome domain.CreateBetOutcome) dbgen.CreateBetOutcomeParams { return dbgen.CreateBetOutcomeParams{ BetID: betOutcome.BetID, @@ -140,6 +151,35 @@ func (s *Store) CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBe return rows, nil } +func (s *Store) CreateFlag(ctx context.Context, flag domain.CreateFlagReq) (domain.Flag, error) { + createFlag := dbgen.CreateFlagParams{ + BetID: pgtype.Int8{ + Int64: flag.BetID, + Valid: flag.BetID != 0, + }, + OddID: pgtype.Int8{ + Int64: flag.OddID, + Valid: flag.OddID != 0, + }, + Reason: pgtype.Text{ + String: flag.Reason, + Valid: true, + }, + } + + f, err := s.queries.CreateFlag(ctx, createFlag) + if err != nil { + domain.MongoDBLogger.Error("failed to create flag", + zap.String("flag", f.Reason.String), + zap.Any("flag_id", f.ID), + zap.Error(err), + ) + return domain.Flag{}, err + } + + return convertDBFlag(f), nil +} + func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) { bet, err := s.queries.GetBetByID(ctx, id) if err != nil { @@ -237,8 +277,8 @@ func (s *Store) GetBetsForCashback(ctx context.Context) ([]domain.GetBet, error) return res, nil } -func (s *Store) GetBetCount(ctx context.Context, UserID int64, outcomesHash string) (int64, error) { - count, err := s.queries.GetBetCount(ctx, dbgen.GetBetCountParams{ +func (s *Store) GetBetCountByUserID(ctx context.Context, UserID int64, outcomesHash string) (int64, error) { + count, err := s.queries.GetBetCountByUserID(ctx, dbgen.GetBetCountByUserIDParams{ UserID: UserID, OutcomesHash: outcomesHash, }) @@ -250,6 +290,24 @@ func (s *Store) GetBetCount(ctx context.Context, UserID int64, outcomesHash stri return count, nil } +func (s *Store) GetBetCountByOutcomesHash(ctx context.Context, outcomesHash string) (int64, error) { + count, err := s.queries.GetBetCountByOutcomesHash(ctx, outcomesHash) + if err != nil { + return 0, err + } + + return count, nil +} + +func (s *Store) GetBetOutcomeCountByOddID(ctx context.Context, oddID int64) (int64, error) { + count, err := s.queries.GetBetOutcomeCountByOddID(ctx, oddID) + if err != nil { + return 0, err + } + + return count, nil +} + func (s *Store) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error { err := s.queries.UpdateCashOut(ctx, dbgen.UpdateCashOutParams{ ID: id, diff --git a/internal/services/bet/port.go b/internal/services/bet/port.go index 9a116be..e29b68e 100644 --- a/internal/services/bet/port.go +++ b/internal/services/bet/port.go @@ -10,13 +10,16 @@ import ( type BetStore interface { CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) 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) 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) GetBetOutcomeByBetID(ctx context.Context, betID int64) ([]domain.BetOutcome, error) - GetBetCount(ctx context.Context, userID int64, outcomesHash string) (int64, error) + GetBetOutcomeCountByOddID(ctx context.Context, oddID int64) (int64, error) + GetBetCountByUserID(ctx context.Context, userID int64, outcomesHash string) (int64, error) + GetBetCountByOutcomesHash(ctx context.Context, outcomesHash string) (int64, error) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index 891d8a5..1d34bc0 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -269,7 +269,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID return domain.CreateBetRes{}, err } - count, err := s.GetBetCount(ctx, userID, outcomesHash) + count, err := s.GetBetCountByUserID(ctx, userID, outcomesHash) if err != nil { s.mongoLogger.Error("failed to generate cashout ID", zap.Int64("user_id", userID), @@ -398,6 +398,79 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID return domain.CreateBetRes{}, err } + for i := range outcomes { + // flag odds with large amount of users betting on them + count, err := s.betStore.GetBetOutcomeCountByOddID(ctx, outcomes[i].OddID) + if err != nil { + s.mongoLogger.Error("failed to get count of bet outcome", + zap.Int64("bet_id", bet.ID), + zap.Int64("odd_id", outcomes[i].OddID), + zap.Error(err), + ) + return domain.CreateBetRes{}, err + } + + // TODO: fetch cap from settings in db + if count > 20 { + flag := domain.CreateFlagReq{ + BetID: 0, + OddID: outcomes[i].OddID, + Reason: fmt.Sprintf("too many users targeting odd - (%d)", outcomes[i].OddID), + } + + _, err := s.betStore.CreateFlag(ctx, flag) + if err != nil { + s.mongoLogger.Error("failed to create flag for bet", + zap.Int64("bet_id", bet.ID), + zap.Error(err), + ) + } + } + } + + // flag bets that have more than three outcomes + if len(outcomes) > 3 { + flag := domain.CreateFlagReq{ + BetID: bet.ID, + OddID: 0, + Reason: fmt.Sprintf("too many outcomes - (%d)", len(outcomes)), + } + + _, err := s.betStore.CreateFlag(ctx, flag) + if err != nil { + s.mongoLogger.Error("failed to create flag for bet", + zap.Int64("bet_id", bet.ID), + zap.Error(err), + ) + } + } + + // large amount of users betting on the same bet_outcomes + total_bet_count, err := s.betStore.GetBetCountByOutcomesHash(ctx, outcomesHash) + if err != nil { + s.mongoLogger.Error("failed to get bet outcomes count", + zap.String("outcomes_hash", outcomesHash), + zap.Error(err), + ) + return domain.CreateBetRes{}, err + } + + if total_bet_count > 10 { + flag := domain.CreateFlagReq{ + BetID: bet.ID, + OddID: 0, + Reason: fmt.Sprintf("too many users bet on same outcomes - (%s)", outcomesHash), + } + _, err := s.betStore.CreateFlag(ctx, flag) + + if err != nil { + s.mongoLogger.Error("failed to get bet outcomes count", + zap.String("outcomes_hash", outcomesHash), + zap.Error(err), + ) + } + } + res := domain.ConvertCreateBet(bet, rows) return res, nil @@ -716,7 +789,7 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, le return domain.CreateBetRes{}, err } - count, err := s.GetBetCount(ctx, userID, outcomesHash) + count, err := s.GetBetCountByUserID(ctx, userID, outcomesHash) if err != nil { s.mongoLogger.Error("failed to get bet count", zap.Int64("user_id", userID), @@ -799,8 +872,12 @@ func (s *Service) GetBetByFastCode(ctx context.Context, fastcode string) (domain return s.betStore.GetBetByFastCode(ctx, fastcode) } -func (s *Service) GetBetCount(ctx context.Context, UserID int64, outcomesHash string) (int64, error) { - return s.betStore.GetBetCount(ctx, UserID, outcomesHash) +func (s *Service) GetBetCountByUserID(ctx context.Context, UserID int64, outcomesHash string) (int64, error) { + return s.betStore.GetBetCountByUserID(ctx, UserID, outcomesHash) +} + +func (s *Service) GetBetCountByOutcomesHash(ctx context.Context, outcomesHash string) (int64, error) { + return s.betStore.GetBetCountByOutcomesHash(ctx, outcomesHash) } func (s *Service) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error { diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 67df201..8d45a17 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -24,22 +24,22 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S spec string task func() }{ - // { - // spec: "0 0 * * * *", // Every 1 hour - // task: func() { - // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { - // log.Printf("FetchUpcomingEvents error: %v", err) - // } - // }, - // }, - // { - // spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events) - // task: func() { - // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { - // log.Printf("FetchNonLiveOdds error: %v", err) - // } - // }, - // }, + { + spec: "0 0 * * * *", // Every 1 hour + task: func() { + if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { + log.Printf("FetchUpcomingEvents error: %v", err) + } + }, + }, + { + spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events) + task: func() { + if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { + log.Printf("FetchNonLiveOdds error: %v", err) + } + }, + }, { spec: "0 */5 * * * *", // Every 5 Minutes task: func() {