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 5a74346..e1b8391 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, @@ -319,6 +319,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/migrations/000007_setting_data.up.sql b/db/migrations/000007_setting_data.up.sql index adbc6c2..22f5974 100644 --- a/db/migrations/000007_setting_data.up.sql +++ b/db/migrations/000007_setting_data.up.sql @@ -4,7 +4,7 @@ VALUES ('max_number_of_outcomes', '30'), ('bet_amount_limit', '100000'), ('daily_ticket_limit', '50'), ('total_winnings_limit', '1000000'), - ('amount_for_bet_referral', '1000000') + ('amount_for_bet_referral', '1000000'), ('cashback_amount_cap', '1000') ON CONFLICT (key) DO UPDATE diff --git a/db/query/bet.sql b/db/query/bet.sql index 8cca28e..588e3c0 100644 --- a/db/query/bet.sql +++ b/db/query/bet.sql @@ -101,7 +101,7 @@ WHERE (event_id = $1) SELECT * FROM bet_outcomes WHERE bet_id = $1; --- name: GetBetCount :one +-- name: GetBetCountByUserID :one SELECT COUNT(*) FROM bets WHERE user_id = $1 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..965ccdf 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -282,20 +282,20 @@ func (q *Queries) GetBetByUserID(ctx context.Context, userID int64) ([]BetWithOu return items, nil } -const GetBetCount = `-- name: GetBetCount :one +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 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 cccf340..66a9bed 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -276,6 +276,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 a3f0c2b..8b9ae32 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 @@ -93,16 +102,22 @@ 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" validate:"required" example:"1"` + Outcomes []CreateBetOutcomeReq `json:"outcomes" validate:"required"` + Amount float32 `json:"amount" validate:"required,gt=0" example:"100.0"` + BranchID *int64 `json:"branch_id,omitempty" validate:"required" example:"1"` } -type CreateBetWithFastCodeReq struct { +type CreateBetWithFastCodeReq struct { FastCode string `json:"fast_code"` Amount float32 `json:"amount"` 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"` diff --git a/internal/repository/bet.go b/internal/repository/bet.go index bdf83d3..75714f8 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, }) diff --git a/internal/services/bet/port.go b/internal/services/bet/port.go index 9a116be..e56cd40 100644 --- a/internal/services/bet/port.go +++ b/internal/services/bet/port.go @@ -10,13 +10,14 @@ 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) + GetBetCountByUserID(ctx context.Context, userID int64, 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 a3952db..81cd6bf 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), @@ -360,15 +360,15 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID case domain.RoleCustomer: // Only the customer is able to create a online bet newBet.IsShopBet = false - err = s.DeductBetFromCustomerWallet(ctx, req.Amount, userID) - if err != nil { - s.mongoLogger.Error("customer wallet deduction failed", - zap.Float32("amount", req.Amount), - zap.Int64("user_id", userID), - zap.Error(err), - ) - return domain.CreateBetRes{}, err - } + // err = s.DeductBetFromCustomerWallet(ctx, req.Amount, userID) + // if err != nil { + // s.mongoLogger.Error("customer wallet deduction failed", + // zap.Float32("amount", req.Amount), + // zap.Int64("user_id", userID), + // zap.Error(err), + // ) + // return domain.CreateBetRes{}, err + // } default: s.mongoLogger.Error("unknown role type", zap.String("role", string(role)), @@ -398,6 +398,23 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID return domain.CreateBetRes{}, 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), + ) + } + } + res := domain.ConvertCreateBet(bet, rows) return res, nil @@ -716,7 +733,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 +816,8 @@ 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) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error { diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index 0c335a6..024c2a7 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -118,7 +118,7 @@ func (h *Handler) CreateBetWithFastCode(c *fiber.Ctx) error { } // This can be for both online and offline bets - // If bet is an online bet (if the customer role creates the bet on their own) + // If bet is an online bet (if the customer role creates the bet on their own) // then the branchID is null newReq := domain.CreateBetReq{ Amount: req.Amount, diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index d5bf75a..5d08063 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -161,7 +161,7 @@ func (a *App) initAppRoutes() { groupV1.Put("/leagues/:id/featured", h.SetLeagueFeatured) groupV1.Get("/result/:id", h.GetResultsByEventID) - + // Branch groupV1.Post("/branch", a.authMiddleware, h.CreateBranch) groupV1.Get("/branch", a.authMiddleware, h.GetAllBranches) diff --git a/reports/report_5min_2025-07-14_18-11.csv b/reports/report_5min_2025-07-14_18-11.csv new file mode 100644 index 0000000..c9ad19d --- /dev/null +++ b/reports/report_5min_2025-07-14_18-11.csv @@ -0,0 +1,16 @@ +Sports Betting Reports (Periodic) +Period,Total Bets,Total Cash Made,Total Cash Out,Total Cash Backs,Total Deposits,Total Withdrawals,Total Tickets +5min,0,0.00,0.00,0.00,0.00,0.00,0 + +Virtual Game Reports (Periodic) +Game Name,Number of Bets,Total Transaction Sum + +Company Reports (Periodic) +Company ID,Company Name,Total Bets,Total Cash In,Total Cash Out,Total Cash Backs + +Branch Reports (Periodic) +Branch ID,Branch Name,Company ID,Total Bets,Total Cash In,Total Cash Out,Total Cash Backs + +Total Summary +Total Bets,Total Cash In,Total Cash Out,Total Cash Backs +0,0.00,0.00,0.00 diff --git a/reports/report_5min_2025-07-14_18-15.csv b/reports/report_5min_2025-07-14_18-15.csv new file mode 100644 index 0000000..c9ad19d --- /dev/null +++ b/reports/report_5min_2025-07-14_18-15.csv @@ -0,0 +1,16 @@ +Sports Betting Reports (Periodic) +Period,Total Bets,Total Cash Made,Total Cash Out,Total Cash Backs,Total Deposits,Total Withdrawals,Total Tickets +5min,0,0.00,0.00,0.00,0.00,0.00,0 + +Virtual Game Reports (Periodic) +Game Name,Number of Bets,Total Transaction Sum + +Company Reports (Periodic) +Company ID,Company Name,Total Bets,Total Cash In,Total Cash Out,Total Cash Backs + +Branch Reports (Periodic) +Branch ID,Branch Name,Company ID,Total Bets,Total Cash In,Total Cash Out,Total Cash Backs + +Total Summary +Total Bets,Total Cash In,Total Cash Out,Total Cash Backs +0,0.00,0.00,0.00 diff --git a/reports/report_5min_2025-07-14_18-16.csv b/reports/report_5min_2025-07-14_18-16.csv new file mode 100644 index 0000000..c9ad19d --- /dev/null +++ b/reports/report_5min_2025-07-14_18-16.csv @@ -0,0 +1,16 @@ +Sports Betting Reports (Periodic) +Period,Total Bets,Total Cash Made,Total Cash Out,Total Cash Backs,Total Deposits,Total Withdrawals,Total Tickets +5min,0,0.00,0.00,0.00,0.00,0.00,0 + +Virtual Game Reports (Periodic) +Game Name,Number of Bets,Total Transaction Sum + +Company Reports (Periodic) +Company ID,Company Name,Total Bets,Total Cash In,Total Cash Out,Total Cash Backs + +Branch Reports (Periodic) +Branch ID,Branch Name,Company ID,Total Bets,Total Cash In,Total Cash Out,Total Cash Backs + +Total Summary +Total Bets,Total Cash In,Total Cash Out,Total Cash Backs +0,0.00,0.00,0.00