flag too many outcomes

This commit is contained in:
Asher Samuel 2025-07-14 19:30:37 +03:00
parent ae56d253c2
commit e3545f3f8c
17 changed files with 227 additions and 30 deletions

View File

@ -79,4 +79,6 @@ DROP TABLE IF EXISTS events;
DROP TABLE IF EXISTS leagues; DROP TABLE IF EXISTS leagues;
DROP TABLE IF EXISTS teams; DROP TABLE IF EXISTS teams;
DROP TABLE IF EXISTS settings; DROP TABLE IF EXISTS settings;
DROP TABLE IF EXISTS bonus;
DROP TABLE IF EXISTS flags;
-- DELETE FROM wallet_transfer; -- DELETE FROM wallet_transfer;

View File

@ -208,7 +208,7 @@ CREATE TABLE IF NOT EXISTS branches (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
location TEXT NOT NULL, location TEXT NOT NULL,
profit_percent REAL NOt NULL, profit_percent REAL NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT false, is_active BOOLEAN NOT NULL DEFAULT false,
wallet_id BIGINT NOT NULL, wallet_id BIGINT NOT NULL,
branch_manager_id BIGINT NOT NULL, branch_manager_id BIGINT NOT NULL,
@ -319,6 +319,21 @@ CREATE TABLE bonus (
multiplier REAL NOT NULL, multiplier REAL NOT NULL,
balance_cap BIGINT NOT NULL DEFAULT 0 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 -- Views
CREATE VIEW companies_details AS CREATE VIEW companies_details AS
SELECT companies.*, SELECT companies.*,

View File

@ -4,7 +4,7 @@ VALUES ('max_number_of_outcomes', '30'),
('bet_amount_limit', '100000'), ('bet_amount_limit', '100000'),
('daily_ticket_limit', '50'), ('daily_ticket_limit', '50'),
('total_winnings_limit', '1000000'), ('total_winnings_limit', '1000000'),
('amount_for_bet_referral', '1000000') ('amount_for_bet_referral', '1000000'),
('cashback_amount_cap', '1000') ('cashback_amount_cap', '1000')
ON CONFLICT (key) DO ON CONFLICT (key) DO
UPDATE UPDATE

View File

@ -101,7 +101,7 @@ WHERE (event_id = $1)
SELECT * SELECT *
FROM bet_outcomes FROM bet_outcomes
WHERE bet_id = $1; WHERE bet_id = $1;
-- name: GetBetCount :one -- name: GetBetCountByUserID :one
SELECT COUNT(*) SELECT COUNT(*)
FROM bets FROM bets
WHERE user_id = $1 WHERE user_id = $1

8
db/query/flags.sql Normal file
View File

@ -0,0 +1,8 @@
-- name: CreateFlag :one
INSERT INTO flags (
bet_id,
odd_id,
reason
) VALUES (
$1, $2, $3
) RETURNING *;

View File

@ -282,20 +282,20 @@ func (q *Queries) GetBetByUserID(ctx context.Context, userID int64) ([]BetWithOu
return items, nil return items, nil
} }
const GetBetCount = `-- name: GetBetCount :one const GetBetCountByUserID = `-- name: GetBetCountByUserID :one
SELECT COUNT(*) SELECT COUNT(*)
FROM bets FROM bets
WHERE user_id = $1 WHERE user_id = $1
AND outcomes_hash = $2 AND outcomes_hash = $2
` `
type GetBetCountParams struct { type GetBetCountByUserIDParams struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
OutcomesHash string `json:"outcomes_hash"` OutcomesHash string `json:"outcomes_hash"`
} }
func (q *Queries) GetBetCount(ctx context.Context, arg GetBetCountParams) (int64, error) { func (q *Queries) GetBetCountByUserID(ctx context.Context, arg GetBetCountByUserIDParams) (int64, error) {
row := q.db.QueryRow(ctx, GetBetCount, arg.UserID, arg.OutcomesHash) row := q.db.QueryRow(ctx, GetBetCountByUserID, arg.UserID, arg.OutcomesHash)
var count int64 var count int64
err := row.Scan(&count) err := row.Scan(&count)
return count, err return count, err

42
gen/db/flags.sql.go Normal file
View File

@ -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
}

View File

@ -276,6 +276,15 @@ type FavoriteGame struct {
CreatedAt pgtype.Timestamp `json:"created_at"` 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 { type League struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`

View File

@ -61,6 +61,15 @@ type BetFilter struct {
CreatedAfter ValidTime CreatedAfter ValidTime
} }
type Flag struct {
ID int64
BetID int64
OddID int64
Reason string
FlaggedAt time.Time
Resolved bool
}
type GetBet struct { type GetBet struct {
ID int64 ID int64
Amount Currency Amount Currency
@ -93,16 +102,22 @@ type CreateBetOutcomeReq struct {
} }
type CreateBetReq struct { type CreateBetReq struct {
Outcomes []CreateBetOutcomeReq `json:"outcomes" validate:"required"` Outcomes []CreateBetOutcomeReq `json:"outcomes" validate:"required"`
Amount float32 `json:"amount" validate:"required,gt=0" example:"100.0"` Amount float32 `json:"amount" validate:"required,gt=0" example:"100.0"`
BranchID *int64 `json:"branch_id,omitempty" validate:"required" example:"1"` BranchID *int64 `json:"branch_id,omitempty" validate:"required" example:"1"`
} }
type CreateBetWithFastCodeReq struct { type CreateBetWithFastCodeReq struct {
FastCode string `json:"fast_code"` FastCode string `json:"fast_code"`
Amount float32 `json:"amount"` Amount float32 `json:"amount"`
BranchID *int64 `json:"branch_id"` BranchID *int64 `json:"branch_id"`
} }
type CreateFlagReq struct {
BetID int64
OddID int64
Reason string
}
type RandomBetReq struct { type RandomBetReq struct {
BranchID int64 `json:"branch_id" validate:"required" example:"1"` BranchID int64 `json:"branch_id" validate:"required" example:"1"`

View File

@ -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 { func convertDBCreateBetOutcome(betOutcome domain.CreateBetOutcome) dbgen.CreateBetOutcomeParams {
return dbgen.CreateBetOutcomeParams{ return dbgen.CreateBetOutcomeParams{
BetID: betOutcome.BetID, BetID: betOutcome.BetID,
@ -140,6 +151,35 @@ func (s *Store) CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBe
return rows, nil 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) { func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) {
bet, err := s.queries.GetBetByID(ctx, id) bet, err := s.queries.GetBetByID(ctx, id)
if err != nil { if err != nil {
@ -237,8 +277,8 @@ func (s *Store) GetBetsForCashback(ctx context.Context) ([]domain.GetBet, error)
return res, nil return res, nil
} }
func (s *Store) GetBetCount(ctx context.Context, UserID int64, outcomesHash string) (int64, error) { func (s *Store) GetBetCountByUserID(ctx context.Context, UserID int64, outcomesHash string) (int64, error) {
count, err := s.queries.GetBetCount(ctx, dbgen.GetBetCountParams{ count, err := s.queries.GetBetCountByUserID(ctx, dbgen.GetBetCountByUserIDParams{
UserID: UserID, UserID: UserID,
OutcomesHash: outcomesHash, OutcomesHash: outcomesHash,
}) })

View File

@ -10,13 +10,14 @@ import (
type BetStore interface { type BetStore interface {
CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error)
CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBetOutcome) (int64, 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) 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, error)
GetBetByUserID(ctx context.Context, UserID int64) ([]domain.GetBet, error) GetBetByUserID(ctx context.Context, UserID int64) ([]domain.GetBet, error)
GetBetByFastCode(ctx context.Context, fastcode string) (domain.GetBet, error) GetBetByFastCode(ctx context.Context, fastcode string) (domain.GetBet, error)
GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error) GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error)
GetBetOutcomeByBetID(ctx context.Context, betID int64) ([]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 UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error
UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error
UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error)

View File

@ -269,7 +269,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
return domain.CreateBetRes{}, err return domain.CreateBetRes{}, err
} }
count, err := s.GetBetCount(ctx, userID, outcomesHash) count, err := s.GetBetCountByUserID(ctx, userID, outcomesHash)
if err != nil { if err != nil {
s.mongoLogger.Error("failed to generate cashout ID", s.mongoLogger.Error("failed to generate cashout ID",
zap.Int64("user_id", userID), zap.Int64("user_id", userID),
@ -360,15 +360,15 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
case domain.RoleCustomer: case domain.RoleCustomer:
// Only the customer is able to create a online bet // Only the customer is able to create a online bet
newBet.IsShopBet = false newBet.IsShopBet = false
err = s.DeductBetFromCustomerWallet(ctx, req.Amount, userID) // err = s.DeductBetFromCustomerWallet(ctx, req.Amount, userID)
if err != nil { // if err != nil {
s.mongoLogger.Error("customer wallet deduction failed", // s.mongoLogger.Error("customer wallet deduction failed",
zap.Float32("amount", req.Amount), // zap.Float32("amount", req.Amount),
zap.Int64("user_id", userID), // zap.Int64("user_id", userID),
zap.Error(err), // zap.Error(err),
) // )
return domain.CreateBetRes{}, err // return domain.CreateBetRes{}, err
} // }
default: default:
s.mongoLogger.Error("unknown role type", s.mongoLogger.Error("unknown role type",
zap.String("role", string(role)), zap.String("role", string(role)),
@ -398,6 +398,23 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
return domain.CreateBetRes{}, err 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) res := domain.ConvertCreateBet(bet, rows)
return res, nil return res, nil
@ -716,7 +733,7 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, le
return domain.CreateBetRes{}, err return domain.CreateBetRes{}, err
} }
count, err := s.GetBetCount(ctx, userID, outcomesHash) count, err := s.GetBetCountByUserID(ctx, userID, outcomesHash)
if err != nil { if err != nil {
s.mongoLogger.Error("failed to get bet count", s.mongoLogger.Error("failed to get bet count",
zap.Int64("user_id", userID), 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) return s.betStore.GetBetByFastCode(ctx, fastcode)
} }
func (s *Service) GetBetCount(ctx context.Context, UserID int64, outcomesHash string) (int64, error) { func (s *Service) GetBetCountByUserID(ctx context.Context, UserID int64, outcomesHash string) (int64, error) {
return s.betStore.GetBetCount(ctx, UserID, outcomesHash) return s.betStore.GetBetCountByUserID(ctx, UserID, outcomesHash)
} }
func (s *Service) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error { func (s *Service) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error {

View File

@ -118,7 +118,7 @@ func (h *Handler) CreateBetWithFastCode(c *fiber.Ctx) error {
} }
// This can be for both online and offline bets // 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 // then the branchID is null
newReq := domain.CreateBetReq{ newReq := domain.CreateBetReq{
Amount: req.Amount, Amount: req.Amount,

View File

@ -161,7 +161,7 @@ func (a *App) initAppRoutes() {
groupV1.Put("/leagues/:id/featured", h.SetLeagueFeatured) groupV1.Put("/leagues/:id/featured", h.SetLeagueFeatured)
groupV1.Get("/result/:id", h.GetResultsByEventID) groupV1.Get("/result/:id", h.GetResultsByEventID)
// Branch // Branch
groupV1.Post("/branch", a.authMiddleware, h.CreateBranch) groupV1.Post("/branch", a.authMiddleware, h.CreateBranch)
groupV1.Get("/branch", a.authMiddleware, h.GetAllBranches) groupV1.Get("/branch", a.authMiddleware, h.GetAllBranches)

View File

@ -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
1 Sports Betting Reports (Periodic)
2 Period,Total Bets,Total Cash Made,Total Cash Out,Total Cash Backs,Total Deposits,Total Withdrawals,Total Tickets
3 5min,0,0.00,0.00,0.00,0.00,0.00,0
4 Virtual Game Reports (Periodic)
5 Game Name,Number of Bets,Total Transaction Sum
6 Company Reports (Periodic)
7 Company ID,Company Name,Total Bets,Total Cash In,Total Cash Out,Total Cash Backs
8 Branch Reports (Periodic)
9 Branch ID,Branch Name,Company ID,Total Bets,Total Cash In,Total Cash Out,Total Cash Backs
10 Total Summary
11 Total Bets,Total Cash In,Total Cash Out,Total Cash Backs
12 0,0.00,0.00,0.00

View File

@ -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
1 Sports Betting Reports (Periodic)
2 Period,Total Bets,Total Cash Made,Total Cash Out,Total Cash Backs,Total Deposits,Total Withdrawals,Total Tickets
3 5min,0,0.00,0.00,0.00,0.00,0.00,0
4 Virtual Game Reports (Periodic)
5 Game Name,Number of Bets,Total Transaction Sum
6 Company Reports (Periodic)
7 Company ID,Company Name,Total Bets,Total Cash In,Total Cash Out,Total Cash Backs
8 Branch Reports (Periodic)
9 Branch ID,Branch Name,Company ID,Total Bets,Total Cash In,Total Cash Out,Total Cash Backs
10 Total Summary
11 Total Bets,Total Cash In,Total Cash Out,Total Cash Backs
12 0,0.00,0.00,0.00

View File

@ -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
1 Sports Betting Reports (Periodic)
2 Period,Total Bets,Total Cash Made,Total Cash Out,Total Cash Backs,Total Deposits,Total Withdrawals,Total Tickets
3 5min,0,0.00,0.00,0.00,0.00,0.00,0
4 Virtual Game Reports (Periodic)
5 Game Name,Number of Bets,Total Transaction Sum
6 Company Reports (Periodic)
7 Company ID,Company Name,Total Bets,Total Cash In,Total Cash Out,Total Cash Backs
8 Branch Reports (Periodic)
9 Branch ID,Branch Name,Company ID,Total Bets,Total Cash In,Total Cash Out,Total Cash Backs
10 Total Summary
11 Total Bets,Total Cash In,Total Cash Out,Total Cash Backs
12 0,0.00,0.00,0.00