fix: outcome and league optimization

This commit is contained in:
Samuel Tariku 2025-06-19 00:27:39 +03:00
parent 76518fcbd1
commit 1c7ae8232c
22 changed files with 707 additions and 250 deletions

62
.env
View File

@ -1,62 +0,0 @@
# REPORT_EXPORT_PATH="C:\\ProgramData\\FortuneBet\\exported_reports" #prod env
REPORT_EXPORT_PATH ="./exported_reports" #dev env
RESEND_SENDER_EMAIL=customer@fortunebets.net
RESEND_API_KEY=re_GSTRa9Pp_JkRWBpST9MvaCVULJF8ybGKE
ENV=development
PORT=8080
DB_URL=postgresql://root:secret@localhost:5422/gh?sslmode=disable
REFRESH_EXPIRY=2592000
JWT_KEY=mysecretkey
ACCESS_EXPIRY=600
LOG_LEVEL=debug
AFRO_SMS_API_KEY=eyJhbGciOiJIUzI1NiJ9.eyJpZGVudGlmaWVyIjoiQlR5ZDFIYmJFYXZ6YUo3dzZGell1RUlieGozSElJeTYiLCJleHAiOjE4OTYwMTM5MTksImlhdCI6MTczODI0NzUxOSwianRpIjoiOWIyNTJkNWQtODcxOC00NGYzLWIzMDQtMGYxOTRhY2NiNTU3In0.XPw8s6mCx1Tp1CfxGmjFRROmdkVnghnqfmsniB-Ze8I
AFRO_SMS_SENDER_NAME=FortuneBets
AFRO_SMS_RECEIVER_PHONE_NUMBER=
BET365_TOKEN=158046-hesJDP2Cay2M5G
POPOK_CLIENT_ID=1
POPOK_PLATFORM=111
POPOK_SECRET_KEY=XwFQ76Y59zBxGryh
# POPOK_BASE_URL=https://api.pokgaming.com/game/launch #Production
# POPOK_BASE_URL=https://games.pokgaming.com/launch #Production
# POPOK_BASE_URL=https://sandbox.pokgaming.com/game/launch #Staging
# POPOK_BASE_URL=https://test-api.pokgaming.com/launch #Staging
POPOK_BASE_URL=https://st.pokgaming.com/ #Staging
POPOK_CALLBACK_URL=1
#Muli-currency Support
FIXER_API_KEY=3b0f1eb30d-63c875026d-sxy9pl
BASE_CURRENCY=ETB
FIXER_BASE_URL=https://api.apilayer.com/fixer
# Chapa API Configuration
CHAPA_TRANSFER_TYPE="Payout"
CHAPA_PAYMENT_TYPE="API"
CHAPA_BASE_URL="https://api.chapa.co/v1"
CHAPA_ENCRYPTION_KEY=zLdYrjnBCknMvFikmP5jBfen
CHAPA_PUBLIC_KEY=CHAPUBK_TEST-HJR0qhQRPLTkauNy9Q8UrmskPTOR31aC
CHAPA_SECRET_KEY=CHASECK_TEST-q3jypwmFK6XJGYOK3aX4z9Kogd9KaHhF
CHAPA_CALLBACK_URL="https://fortunebet.com/api/v1/payments/callback" # Optional
CHAPA_RETURN_URL="https://fortunebet.com/api/v1/payment-success" # Optional
#Alea Play
ALEA_ENABLED=true
ALEA_BASE_URL=https://api.aleaplay.com
ALEA_OPERATOR_ID=operator_id
ALEA_SECRET_KEY=hmac_secret
ALEA_GAME_LIST_URL=https://api.aleaplay.com/games/list # Optional
ALEA_DEFAULT_CURRENCY=USD # Optional (default: USD)
ALEA_SESSION_TIMEOUT=24 # Optional (hours, default: 24)
ALEA_GAME_ID_AVIATOR=aviator_prod
# Veli Games
VELI_ENABLED=true
VELI_API_URL=https://api.velitech.games
VELI_OPERATOR_KEY=Veli123
VELI_SECRET_KEY=hmac_secret
VELI_GAME_ID_AVIATOR=veli_aviator_v1
VELI_DEFAULT_CURRENCY=USD

View File

@ -122,7 +122,7 @@ func main() {
companySvc := company.NewService(store) companySvc := company.NewService(store)
leagueSvc := league.New(store) leagueSvc := league.New(store)
betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, logger, domain.MongoDBLogger) betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, logger, domain.MongoDBLogger)
resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc) resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc)
referalRepo := repository.NewReferralRepository(store) referalRepo := repository.NewReferralRepository(store)
vitualGameRepo := repository.NewVirtualGameRepository(store) vitualGameRepo := repository.NewVirtualGameRepository(store)
recommendationRepo := repository.NewRecommendationRepository(store) recommendationRepo := repository.NewRecommendationRepository(store)

View File

@ -250,10 +250,12 @@ CREATE TABLE companies (
CREATE TABLE leagues ( CREATE TABLE leagues (
id BIGINT PRIMARY KEY, id BIGINT PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
img TEXT,
country_code TEXT, country_code TEXT,
bet365_id INT, bet365_id INT,
sport_id INT NOT NULL, sport_id INT NOT NULL,
is_active BOOLEAN DEFAULT true is_active BOOLEAN DEFAULT true,
is_featured BOOLEAN DEFAULT false
); );
CREATE TABLE teams ( CREATE TABLE teams (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@ -309,7 +311,7 @@ ALTER TABLE bets
ADD CONSTRAINT fk_bets_users FOREIGN KEY (user_id) REFERENCES users(id), ADD CONSTRAINT fk_bets_users FOREIGN KEY (user_id) REFERENCES users(id),
ADD CONSTRAINT fk_bets_branches FOREIGN KEY (branch_id) REFERENCES branches(id); ADD CONSTRAINT fk_bets_branches FOREIGN KEY (branch_id) REFERENCES branches(id);
ALTER TABLE wallets ALTER TABLE wallets
ADD CONSTRAINT fk_wallets_users FOREIGN KEY (user_id) REFERENCES users(id), ADD CONSTRAINT fk_wallets_users FOREIGN KEY (user_id) REFERENCES users(id),
ADD COLUMN currency VARCHAR(3) NOT NULL DEFAULT 'ETB'; ADD COLUMN currency VARCHAR(3) NOT NULL DEFAULT 'ETB';
ALTER TABLE customer_wallets ALTER TABLE customer_wallets
ADD CONSTRAINT fk_customer_wallets_customers FOREIGN KEY (customer_id) REFERENCES users(id), ADD CONSTRAINT fk_customer_wallets_customers FOREIGN KEY (customer_id) REFERENCES users(id),

View File

@ -103,6 +103,11 @@ UPDATE bet_outcomes
SET status = $1 SET status = $1
WHERE id = $2 WHERE id = $2
RETURNING *; RETURNING *;
-- name: UpdateBetOutcomeStatusForEvent :many
UPDATE bet_outcomes
SEt status = $1
WHERE event_id = $2
RETURNING *;
-- name: UpdateStatus :exec -- name: UpdateStatus :exec
UPDATE bets UPDATE bets
SET status = $1, SET status = $1,

View File

@ -5,14 +5,16 @@ INSERT INTO leagues (
country_code, country_code,
bet365_id, bet365_id,
sport_id, sport_id,
is_active is_active,
is_featured
) )
VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (id) DO
UPDATE UPDATE
SET name = EXCLUDED.name, SET name = EXCLUDED.name,
country_code = EXCLUDED.country_code, country_code = EXCLUDED.country_code,
bet365_id = EXCLUDED.bet365_id, bet365_id = EXCLUDED.bet365_id,
is_active = EXCLUDED.is_active, is_active = EXCLUDED.is_active,
is_featured = EXCLUDED.is_featured,
sport_id = EXCLUDED.sport_id; sport_id = EXCLUDED.sport_id;
-- name: GetAllLeagues :many -- name: GetAllLeagues :many
SELECT id, SELECT id,
@ -20,6 +22,7 @@ SELECT id,
country_code, country_code,
bet365_id, bet365_id,
is_active, is_active,
is_featured,
sport_id sport_id
FROM leagues FROM leagues
WHERE ( WHERE (
@ -34,7 +37,21 @@ WHERE (
is_active = sqlc.narg('is_active') is_active = sqlc.narg('is_active')
OR sqlc.narg('is_active') IS NULL OR sqlc.narg('is_active') IS NULL
) )
AND (
is_featured = sqlc.narg('is_featured')
OR sqlc.narg('is_featured') IS NULL
)
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
-- name: GetFeaturedLeagues :many
SELECT id,
name,
country_code,
bet365_id,
is_active,
is_featured,
sport_id
FROM leagues
WHERE is_featured = true;
-- name: CheckLeagueSupport :one -- name: CheckLeagueSupport :one
SELECT EXISTS( SELECT EXISTS(
SELECT 1 SELECT 1
@ -48,6 +65,7 @@ SET name = COALESCE(sqlc.narg('name'), name),
country_code = COALESCE(sqlc.narg('country_code'), country_code), country_code = COALESCE(sqlc.narg('country_code'), country_code),
bet365_id = COALESCE(sqlc.narg('bet365_id'), bet365_id), bet365_id = COALESCE(sqlc.narg('bet365_id'), bet365_id),
is_active = COALESCE(sqlc.narg('is_active'), is_active), is_active = COALESCE(sqlc.narg('is_active'), is_active),
is_featured = COALESCE(sqlc.narg('is_featured'), is_featured),
sport_id = COALESCE(sqlc.narg('sport_id'), sport_id) sport_id = COALESCE(sqlc.narg('sport_id'), sport_id)
WHERE id = $1; WHERE id = $1;
-- name: UpdateLeagueByBet365ID :exec -- name: UpdateLeagueByBet365ID :exec
@ -56,6 +74,7 @@ SET name = COALESCE(sqlc.narg('name'), name),
id = COALESCE(sqlc.narg('id'), id), id = COALESCE(sqlc.narg('id'), id),
country_code = COALESCE(sqlc.narg('country_code'), country_code), country_code = COALESCE(sqlc.narg('country_code'), country_code),
is_active = COALESCE(sqlc.narg('is_active'), is_active), is_active = COALESCE(sqlc.narg('is_active'), is_active),
is_featured = COALESCE(sqlc.narg('is_featured'), is_featured),
sport_id = COALESCE(sqlc.narg('sport_id'), sport_id) sport_id = COALESCE(sqlc.narg('sport_id'), sport_id)
WHERE bet365_id = $1; WHERE bet365_id = $1;
-- name: SetLeagueActive :exec -- name: SetLeagueActive :exec

View File

@ -468,6 +468,54 @@ func (q *Queries) UpdateBetOutcomeStatus(ctx context.Context, arg UpdateBetOutco
return i, err return i, err
} }
const UpdateBetOutcomeStatusForEvent = `-- name: UpdateBetOutcomeStatusForEvent :many
UPDATE bet_outcomes
SEt status = $1
WHERE event_id = $2
RETURNING id, bet_id, sport_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, status, expires
`
type UpdateBetOutcomeStatusForEventParams struct {
Status int32 `json:"status"`
EventID int64 `json:"event_id"`
}
func (q *Queries) UpdateBetOutcomeStatusForEvent(ctx context.Context, arg UpdateBetOutcomeStatusForEventParams) ([]BetOutcome, error) {
rows, err := q.db.Query(ctx, UpdateBetOutcomeStatusForEvent, arg.Status, arg.EventID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []BetOutcome
for rows.Next() {
var i BetOutcome
if err := rows.Scan(
&i.ID,
&i.BetID,
&i.SportID,
&i.EventID,
&i.OddID,
&i.HomeTeamName,
&i.AwayTeamName,
&i.MarketID,
&i.MarketName,
&i.Odd,
&i.OddName,
&i.OddHeader,
&i.OddHandicap,
&i.Status,
&i.Expires,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const UpdateCashOut = `-- name: UpdateCashOut :exec const UpdateCashOut = `-- name: UpdateCashOut :exec
UPDATE bets UPDATE bets
SET cashed_out = $2, SET cashed_out = $2,

View File

@ -33,6 +33,7 @@ SELECT id,
country_code, country_code,
bet365_id, bet365_id,
is_active, is_active,
is_featured,
sport_id sport_id
FROM leagues FROM leagues
WHERE ( WHERE (
@ -47,13 +48,18 @@ WHERE (
is_active = $3 is_active = $3
OR $3 IS NULL OR $3 IS NULL
) )
LIMIT $5 OFFSET $4 AND (
is_featured = $4
OR $4 IS NULL
)
LIMIT $6 OFFSET $5
` `
type GetAllLeaguesParams struct { type GetAllLeaguesParams struct {
CountryCode pgtype.Text `json:"country_code"` CountryCode pgtype.Text `json:"country_code"`
SportID pgtype.Int4 `json:"sport_id"` SportID pgtype.Int4 `json:"sport_id"`
IsActive pgtype.Bool `json:"is_active"` IsActive pgtype.Bool `json:"is_active"`
IsFeatured pgtype.Bool `json:"is_featured"`
Offset pgtype.Int4 `json:"offset"` Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"` Limit pgtype.Int4 `json:"limit"`
} }
@ -64,6 +70,7 @@ type GetAllLeaguesRow struct {
CountryCode pgtype.Text `json:"country_code"` CountryCode pgtype.Text `json:"country_code"`
Bet365ID pgtype.Int4 `json:"bet365_id"` Bet365ID pgtype.Int4 `json:"bet365_id"`
IsActive pgtype.Bool `json:"is_active"` IsActive pgtype.Bool `json:"is_active"`
IsFeatured pgtype.Bool `json:"is_featured"`
SportID int32 `json:"sport_id"` SportID int32 `json:"sport_id"`
} }
@ -72,6 +79,7 @@ func (q *Queries) GetAllLeagues(ctx context.Context, arg GetAllLeaguesParams) ([
arg.CountryCode, arg.CountryCode,
arg.SportID, arg.SportID,
arg.IsActive, arg.IsActive,
arg.IsFeatured,
arg.Offset, arg.Offset,
arg.Limit, arg.Limit,
) )
@ -88,6 +96,57 @@ func (q *Queries) GetAllLeagues(ctx context.Context, arg GetAllLeaguesParams) ([
&i.CountryCode, &i.CountryCode,
&i.Bet365ID, &i.Bet365ID,
&i.IsActive, &i.IsActive,
&i.IsFeatured,
&i.SportID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetFeaturedLeagues = `-- name: GetFeaturedLeagues :many
SELECT id,
name,
country_code,
bet365_id,
is_active,
is_featured,
sport_id
FROM leagues
WHERE is_featured = true
`
type GetFeaturedLeaguesRow struct {
ID int64 `json:"id"`
Name string `json:"name"`
CountryCode pgtype.Text `json:"country_code"`
Bet365ID pgtype.Int4 `json:"bet365_id"`
IsActive pgtype.Bool `json:"is_active"`
IsFeatured pgtype.Bool `json:"is_featured"`
SportID int32 `json:"sport_id"`
}
func (q *Queries) GetFeaturedLeagues(ctx context.Context) ([]GetFeaturedLeaguesRow, error) {
rows, err := q.db.Query(ctx, GetFeaturedLeagues)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetFeaturedLeaguesRow
for rows.Next() {
var i GetFeaturedLeaguesRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.CountryCode,
&i.Bet365ID,
&i.IsActive,
&i.IsFeatured,
&i.SportID, &i.SportID,
); err != nil { ); err != nil {
return nil, err return nil, err
@ -107,14 +166,16 @@ INSERT INTO leagues (
country_code, country_code,
bet365_id, bet365_id,
sport_id, sport_id,
is_active is_active,
is_featured
) )
VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (id) DO
UPDATE UPDATE
SET name = EXCLUDED.name, SET name = EXCLUDED.name,
country_code = EXCLUDED.country_code, country_code = EXCLUDED.country_code,
bet365_id = EXCLUDED.bet365_id, bet365_id = EXCLUDED.bet365_id,
is_active = EXCLUDED.is_active, is_active = EXCLUDED.is_active,
is_featured = EXCLUDED.is_featured,
sport_id = EXCLUDED.sport_id sport_id = EXCLUDED.sport_id
` `
@ -125,6 +186,7 @@ type InsertLeagueParams struct {
Bet365ID pgtype.Int4 `json:"bet365_id"` Bet365ID pgtype.Int4 `json:"bet365_id"`
SportID int32 `json:"sport_id"` SportID int32 `json:"sport_id"`
IsActive pgtype.Bool `json:"is_active"` IsActive pgtype.Bool `json:"is_active"`
IsFeatured pgtype.Bool `json:"is_featured"`
} }
func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) error { func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) error {
@ -135,6 +197,7 @@ func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) erro
arg.Bet365ID, arg.Bet365ID,
arg.SportID, arg.SportID,
arg.IsActive, arg.IsActive,
arg.IsFeatured,
) )
return err return err
} }
@ -161,7 +224,8 @@ SET name = COALESCE($2, name),
country_code = COALESCE($3, country_code), country_code = COALESCE($3, country_code),
bet365_id = COALESCE($4, bet365_id), bet365_id = COALESCE($4, bet365_id),
is_active = COALESCE($5, is_active), is_active = COALESCE($5, is_active),
sport_id = COALESCE($6, sport_id) is_featured = COALESCE($6, is_featured),
sport_id = COALESCE($7, sport_id)
WHERE id = $1 WHERE id = $1
` `
@ -171,6 +235,7 @@ type UpdateLeagueParams struct {
CountryCode pgtype.Text `json:"country_code"` CountryCode pgtype.Text `json:"country_code"`
Bet365ID pgtype.Int4 `json:"bet365_id"` Bet365ID pgtype.Int4 `json:"bet365_id"`
IsActive pgtype.Bool `json:"is_active"` IsActive pgtype.Bool `json:"is_active"`
IsFeatured pgtype.Bool `json:"is_featured"`
SportID pgtype.Int4 `json:"sport_id"` SportID pgtype.Int4 `json:"sport_id"`
} }
@ -181,6 +246,7 @@ func (q *Queries) UpdateLeague(ctx context.Context, arg UpdateLeagueParams) erro
arg.CountryCode, arg.CountryCode,
arg.Bet365ID, arg.Bet365ID,
arg.IsActive, arg.IsActive,
arg.IsFeatured,
arg.SportID, arg.SportID,
) )
return err return err
@ -192,7 +258,8 @@ SET name = COALESCE($2, name),
id = COALESCE($3, id), id = COALESCE($3, id),
country_code = COALESCE($4, country_code), country_code = COALESCE($4, country_code),
is_active = COALESCE($5, is_active), is_active = COALESCE($5, is_active),
sport_id = COALESCE($6, sport_id) is_featured = COALESCE($6, is_featured),
sport_id = COALESCE($7, sport_id)
WHERE bet365_id = $1 WHERE bet365_id = $1
` `
@ -202,6 +269,7 @@ type UpdateLeagueByBet365IDParams struct {
ID pgtype.Int8 `json:"id"` ID pgtype.Int8 `json:"id"`
CountryCode pgtype.Text `json:"country_code"` CountryCode pgtype.Text `json:"country_code"`
IsActive pgtype.Bool `json:"is_active"` IsActive pgtype.Bool `json:"is_active"`
IsFeatured pgtype.Bool `json:"is_featured"`
SportID pgtype.Int4 `json:"sport_id"` SportID pgtype.Int4 `json:"sport_id"`
} }
@ -212,6 +280,7 @@ func (q *Queries) UpdateLeagueByBet365ID(ctx context.Context, arg UpdateLeagueBy
arg.ID, arg.ID,
arg.CountryCode, arg.CountryCode,
arg.IsActive, arg.IsActive,
arg.IsFeatured,
arg.SportID, arg.SportID,
) )
return err return err

View File

@ -218,10 +218,12 @@ type ExchangeRate struct {
type League struct { type League struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Img pgtype.Text `json:"img"`
CountryCode pgtype.Text `json:"country_code"` CountryCode pgtype.Text `json:"country_code"`
Bet365ID pgtype.Int4 `json:"bet365_id"` Bet365ID pgtype.Int4 `json:"bet365_id"`
SportID int32 `json:"sport_id"` SportID int32 `json:"sport_id"`
IsActive pgtype.Bool `json:"is_active"` IsActive pgtype.Bool `json:"is_active"`
IsFeatured pgtype.Bool `json:"is_featured"`
} }
type Notification struct { type Notification struct {

View File

@ -7,6 +7,7 @@ type League struct {
Bet365ID int32 `json:"bet365_id" example:"1121"` Bet365ID int32 `json:"bet365_id" example:"1121"`
IsActive bool `json:"is_active" example:"false"` IsActive bool `json:"is_active" example:"false"`
SportID int32 `json:"sport_id" example:"1"` SportID int32 `json:"sport_id" example:"1"`
IsFeatured bool `json:"is_featured" example:"false"`
} }
type UpdateLeague struct { type UpdateLeague struct {
@ -15,6 +16,7 @@ type UpdateLeague struct {
CountryCode ValidString `json:"cc" example:"uk"` CountryCode ValidString `json:"cc" example:"uk"`
Bet365ID ValidInt32 `json:"bet365_id" example:"1121"` Bet365ID ValidInt32 `json:"bet365_id" example:"1121"`
IsActive ValidBool `json:"is_active" example:"false"` IsActive ValidBool `json:"is_active" example:"false"`
IsFeatured ValidBool `json:"is_featured" example:"false"`
SportID ValidInt32 `json:"sport_id" example:"1"` SportID ValidInt32 `json:"sport_id" example:"1"`
} }
@ -22,6 +24,69 @@ type LeagueFilter struct {
CountryCode ValidString CountryCode ValidString
SportID ValidInt32 SportID ValidInt32
IsActive ValidBool IsActive ValidBool
IsFeatured ValidBool
Limit ValidInt64 Limit ValidInt64
Offset ValidInt64 Offset ValidInt64
} }
// These leagues are automatically featured when the league is created
var FeaturedLeagues = []int64{
// Football
10044469, // Ethiopian Premier League
10041282, //Premier League
10083364, //La Liga
10041095, //German Bundesliga
10041100, //Ligue 1
10041809, //UEFA Champions League
10041957, //UEFA Europa League
10079560, //UEFA Conference League
10050282, //UEFA Nations League
10044685, //FIFA Club World Cup
10050346, //UEFA Super Cup
10081269, //CONCACAF Champions Cup
10070189, //CONCACAF Gold Cup
10067913, //Europe - World Cup Qualifying
10040162, //Asia - World Cup Qualifying
10067624, //South America - World Cup Qualifying
10073057, //North & Central America - World Cup Qualifying
10037075, //International Match
10077480, //Womens International
10037109, //Europe Friendlies
10068837, //Euro U21
10041315, //Italian Serie A
10036538, //Spain Segunda
10047168, // US MLS
10043156, //England FA Cup
10042103, //France Cup
10041088, //Premier League 2
10084250, //Turkiye Super League
10041187, //Kenya Super League
10041391, //Netherlands Eredivisie
// Basketball
10041830, //NBA
10049984, //WNBA
10037165, //German Bundesliga
10036608, //Italian Lega 1
10040795, //EuroLeague
10041534, //Basketball Africa League
// Ice Hockey
10037477, //NHL
10037447, //AHL
10069385, //IIHF World Championship
// AMERICAN FOOTBALL
10037219, //NFL
// BASEBALL
10037485, // MLB
// VOLLEYBALL
10069666, //FIVB Nations League
}

View File

@ -27,8 +27,9 @@ const (
NOTIFICATION_TYPE_TRANSFER_FAIL NotificationType = "transfer_failed" NOTIFICATION_TYPE_TRANSFER_FAIL NotificationType = "transfer_failed"
NOTIFICATION_TYPE_TRANSFER_SUCCESS NotificationType = "transfer_success" NOTIFICATION_TYPE_TRANSFER_SUCCESS NotificationType = "transfer_success"
NOTIFICATION_TYPE_ADMIN_ALERT NotificationType = "admin_alert" NOTIFICATION_TYPE_ADMIN_ALERT NotificationType = "admin_alert"
NOTIFICATION_RECEIVER_ADMIN NotificationRecieverSide = "admin" NOTIFICATION_TYPE_BET_RESULT NotificationType = "bet_result"
NOTIFICATION_RECEIVER_ADMIN NotificationRecieverSide = "admin"
NotificationRecieverSideAdmin NotificationRecieverSide = "admin" NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
NotificationRecieverSideCustomer NotificationRecieverSide = "customer" NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
NotificationRecieverSideCashier NotificationRecieverSide = "cashier" NotificationRecieverSideCashier NotificationRecieverSide = "cashier"

View File

@ -364,6 +364,28 @@ func (s *Store) UpdateBetOutcomeStatus(ctx context.Context, id int64, status dom
return res, nil return res, nil
} }
func (s *Store) UpdateBetOutcomeStatusForEvent(ctx context.Context, eventID int64, status domain.OutcomeStatus) ([]domain.BetOutcome, error) {
outcomes, err := s.queries.UpdateBetOutcomeStatusForEvent(ctx, dbgen.UpdateBetOutcomeStatusForEventParams{
EventID: eventID,
Status: int32(status),
})
if err != nil {
domain.MongoDBLogger.Error("failed to update bet outcome status for event",
zap.Int64("eventID", eventID),
zap.Int32("status", int32(status)),
zap.Error(err),
)
return nil, err
}
var result []domain.BetOutcome = make([]domain.BetOutcome, 0, len(outcomes))
for _, outcome := range outcomes {
result = append(result, convertDBBetOutcomes(outcome))
}
return result, nil
}
func (s *Store) DeleteBet(ctx context.Context, id int64) error { func (s *Store) DeleteBet(ctx context.Context, id int64) error {
return s.queries.DeleteBet(ctx, id) return s.queries.DeleteBet(ctx, id)
} }

View File

@ -15,6 +15,7 @@ func (s *Store) SaveLeague(ctx context.Context, l domain.League) error {
CountryCode: pgtype.Text{String: l.CountryCode, Valid: true}, CountryCode: pgtype.Text{String: l.CountryCode, Valid: true},
Bet365ID: pgtype.Int4{Int32: l.Bet365ID, Valid: true}, Bet365ID: pgtype.Int4{Int32: l.Bet365ID, Valid: true},
IsActive: pgtype.Bool{Bool: l.IsActive, Valid: true}, IsActive: pgtype.Bool{Bool: l.IsActive, Valid: true},
IsFeatured: pgtype.Bool{Bool: l.IsFeatured, Valid: true},
SportID: l.SportID, SportID: l.SportID,
}) })
} }
@ -33,6 +34,10 @@ func (s *Store) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) (
Bool: filter.IsActive.Value, Bool: filter.IsActive.Value,
Valid: filter.IsActive.Valid, Valid: filter.IsActive.Valid,
}, },
IsFeatured: pgtype.Bool{
Bool: filter.IsFeatured.Value,
Valid: filter.IsFeatured.Valid,
},
Limit: pgtype.Int4{ Limit: pgtype.Int4{
Int32: int32(filter.Limit.Value), Int32: int32(filter.Limit.Value),
Valid: filter.Limit.Valid, Valid: filter.Limit.Valid,
@ -54,6 +59,29 @@ func (s *Store) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) (
CountryCode: league.CountryCode.String, CountryCode: league.CountryCode.String,
Bet365ID: league.Bet365ID.Int32, Bet365ID: league.Bet365ID.Int32,
IsActive: league.IsActive.Bool, IsActive: league.IsActive.Bool,
IsFeatured: league.IsFeatured.Bool,
SportID: league.SportID,
}
}
return leagues, nil
}
func (s *Store) GetFeaturedLeagues(ctx context.Context) ([]domain.League, error) {
l, err := s.queries.GetFeaturedLeagues(ctx)
if err != nil {
return nil, err
}
leagues := make([]domain.League, len(l))
for i, league := range l {
leagues[i] = domain.League{
ID: league.ID,
Name: league.Name,
CountryCode: league.CountryCode.String,
Bet365ID: league.Bet365ID.Int32,
IsActive: league.IsActive.Bool,
SportID: league.SportID, SportID: league.SportID,
} }
} }
@ -93,6 +121,10 @@ func (s *Store) UpdateLeague(ctx context.Context, league domain.UpdateLeague) er
Bool: league.IsActive.Value, Bool: league.IsActive.Value,
Valid: league.IsActive.Valid, Valid: league.IsActive.Valid,
}, },
IsFeatured: pgtype.Bool{
Bool: league.IsFeatured.Value,
Valid: league.IsActive.Valid,
},
SportID: pgtype.Int4{ SportID: pgtype.Int4{
Int32: league.SportID.Value, Int32: league.SportID.Value,
Valid: league.SportID.Valid, Valid: league.SportID.Valid,

View File

@ -21,6 +21,7 @@ type BetStore interface {
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)
UpdateBetOutcomeStatusForEvent(ctx context.Context, eventID int64, status domain.OutcomeStatus) ([]domain.BetOutcome, error)
DeleteBet(ctx context.Context, id int64) error DeleteBet(ctx context.Context, id int64) error
GetBetSummary(ctx context.Context, filter domain.ReportFilter) ( GetBetSummary(ctx context.Context, filter domain.ReportFilter) (

View File

@ -819,6 +819,19 @@ func (s *Service) UpdateBetOutcomeStatus(ctx context.Context, id int64, status d
} }
func (s *Service) UpdateBetOutcomeStatusForEvent(ctx context.Context, eventID int64, status domain.OutcomeStatus) ([]domain.BetOutcome, error) {
outcomes, err := s.betStore.UpdateBetOutcomeStatusForEvent(ctx, eventID, status)
if err != nil {
s.mongoLogger.Error("failed to update bet outcome status",
zap.Int64("eventID", eventID),
zap.Error(err),
)
return nil, err
}
return outcomes, nil
}
func (s *Service) DeleteBet(ctx context.Context, id int64) error { func (s *Service) DeleteBet(ctx context.Context, id int64) error {
return s.betStore.DeleteBet(ctx, id) return s.betStore.DeleteBet(ctx, id)
} }

View File

@ -7,6 +7,7 @@ import (
"io" "io"
"log" "log"
"net/http" "net/http"
"slices"
"strconv" "strconv"
"sync" "sync"
"time" "time"
@ -202,8 +203,10 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error {
} }
func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, source string) { func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, source string) {
sportIDs := []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91} // sportIDs := []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91}
sportIDs := []int{1}
// TODO: Add the league skipping again when we have dynamic leagues // TODO: Add the league skipping again when we have dynamic leagues
// b, err := os.OpenFile("logs/skipped_leagues.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) // b, err := os.OpenFile("logs/skipped_leagues.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
// if err != nil { // if err != nil {
// log.Printf("❌ Failed to open leagues file %v", err) // log.Printf("❌ Failed to open leagues file %v", err)
@ -212,7 +215,7 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour
for sportIndex, sportID := range sportIDs { for sportIndex, sportID := range sportIDs {
var totalPages int = 1 var totalPages int = 1
var page int = 0 var page int = 0
var limit int = 200 var limit int = 1
var count int = 0 var count int = 0
log.Printf("Sport ID %d", sportID) log.Printf("Sport ID %d", sportID)
for page <= totalPages { for page <= totalPages {
@ -252,10 +255,12 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour
// doesn't make sense to save and check back to back, but for now it can be here // doesn't make sense to save and check back to back, but for now it can be here
// no this its fine to keep it here // no this its fine to keep it here
// but change the league id to bet365 id later // but change the league id to bet365 id later
//Automatically feature the league if its in the list
err = s.store.SaveLeague(ctx, domain.League{ err = s.store.SaveLeague(ctx, domain.League{
ID: leagueID, ID: leagueID,
Name: ev.League.Name, Name: ev.League.Name,
IsActive: true, IsActive: true,
IsFeatured: slices.Contains(domain.FeaturedLeagues, leagueID),
SportID: convertInt32(ev.SportID), SportID: convertInt32(ev.SportID),
}) })

View File

@ -9,6 +9,7 @@ import (
type Service interface { type Service interface {
SaveLeague(ctx context.Context, l domain.League) error SaveLeague(ctx context.Context, l domain.League) error
GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.League, error) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.League, error)
GetFeaturedLeagues(ctx context.Context) ([]domain.League, error)
SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error
UpdateLeague(ctx context.Context, league domain.UpdateLeague) error UpdateLeague(ctx context.Context, league domain.UpdateLeague) error
} }

View File

@ -25,6 +25,10 @@ func (s *service) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter)
return s.store.GetAllLeagues(ctx, filter) return s.store.GetAllLeagues(ctx, filter)
} }
func (s *service) GetFeaturedLeagues(ctx context.Context) ([]domain.League, error) {
return s.store.GetFeaturedLeagues(ctx)
}
func (s *service) SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error { func (s *service) SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error {
return s.store.SetLeagueActive(ctx, leagueId, isActive) return s.store.SetLeagueActive(ctx, leagueId, isActive)
} }

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"log/slog" "log/slog"
"net/http" "net/http"
"strconv" "strconv"
@ -17,6 +16,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
) )
@ -29,9 +29,10 @@ type Service struct {
oddSvc odds.ServiceImpl oddSvc odds.ServiceImpl
eventSvc event.Service eventSvc event.Service
leagueSvc league.Service leagueSvc league.Service
notificationSvc *notificationservice.Service
} }
func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, betSvc bet.Service, oddSvc odds.ServiceImpl, eventSvc event.Service, leagueSvc league.Service) *Service { func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, betSvc bet.Service, oddSvc odds.ServiceImpl, eventSvc event.Service, leagueSvc league.Service, notificationSvc *notificationservice.Service) *Service {
return &Service{ return &Service{
repo: repo, repo: repo,
config: cfg, config: cfg,
@ -41,6 +42,7 @@ func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger,
oddSvc: oddSvc, oddSvc: oddSvc,
eventSvc: eventSvc, eventSvc: eventSvc,
leagueSvc: leagueSvc, leagueSvc: leagueSvc,
notificationSvc: notificationSvc,
} }
} }
@ -48,6 +50,127 @@ var (
ErrEventIsNotActive = fmt.Errorf("event has been cancelled or postponed") ErrEventIsNotActive = fmt.Errorf("event has been cancelled or postponed")
) )
func (s *Service) UpdateResultForOutcomes(ctx context.Context, eventID int64, resultRes json.RawMessage, sportID int64) error {
// TODO: Optimize this since there could be many outcomes with the same event_id and market_id that could be updated the same time
outcomes, err := s.repo.GetBetOutcomeByEventID(ctx, eventID)
if err != nil {
s.logger.Error("Failed to get pending bet outcomes", "error", err)
return fmt.Errorf("failed to get pending bet outcomes for event %d: %w", eventID, err)
}
if len(outcomes) == 0 {
s.logger.Info("No bets have been placed on event", "event", eventID)
}
// if len(outcomes) == 0 {
// fmt.Printf("🕛 No bets have been placed on event %v (%d/%d) \n", eventID, i+1, len(events))
// } else {
// fmt.Printf("✅ %d bets have been placed on event %v (%d/%d) \n", len(outcomes), event.ID, i+1, len(events))
// }
for _, outcome := range outcomes {
if outcome.Expires.After(time.Now()) {
s.logger.Warn("Outcome is not expired yet", "event_id", "outcome_id", outcome.ID)
return fmt.Errorf("Outcome has not expired yet")
}
parseResult, err := s.parseResult(ctx, resultRes, outcome, sportID)
if err != nil {
s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err)
return err
}
outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, parseResult.Status)
if err != nil {
s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err)
return err
}
if outcome.Status == domain.OUTCOME_STATUS_ERROR || outcome.Status == domain.OUTCOME_STATUS_PENDING {
s.logger.Error("Outcome is pending or error", "event_id", outcome.EventID, "outcome_id", outcome.ID)
return fmt.Errorf("Error while updating outcome")
}
status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID)
if err != nil {
if err != bet.ErrOutcomesNotCompleted {
s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err)
}
return err
}
s.logger.Info("Updating bet status", outcome.BetID, "status:", status.String())
err = s.betSvc.UpdateStatus(ctx, outcome.BetID, status)
if err != nil {
s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err)
return err
}
}
return nil
}
func (s *Service) RefundAllOutcomes(ctx context.Context, eventID int64) error {
outcomes, err := s.repo.GetBetOutcomeByEventID(ctx, eventID)
if err != nil {
s.logger.Error("Failed to get pending bet outcomes", "error", err)
return fmt.Errorf("failed to get pending bet outcomes for event %d: %w", eventID, err)
}
if len(outcomes) == 0 {
s.logger.Info("No bets have been placed on event", "event", eventID)
}
outcomes, err = s.betSvc.UpdateBetOutcomeStatusForEvent(ctx, eventID, domain.OUTCOME_STATUS_VOID)
if err != nil {
s.logger.Error("Failed to update all outcomes for event")
}
// Get all the unique bet_ids and how many outcomes have this bet_id
betIDSet := make(map[int64]int64)
for _, outcome := range outcomes {
betIDSet[outcome.BetID] += 1
}
for betID := range betIDSet {
status, err := s.betSvc.CheckBetOutcomeForBet(ctx, betID)
if err != nil {
if err != bet.ErrOutcomesNotCompleted {
s.logger.Error("Failed to check bet outcome for bet", "event_id", eventID, "error", err)
}
return err
}
err = s.betSvc.UpdateStatus(ctx, betID, status)
if err != nil {
s.logger.Error("Failed to update bet status", "event id", eventID, "error", err)
continue
}
}
return nil
// for _, outcome := range outcomes {
// outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, domain.OUTCOME_STATUS_VOID)
// if err != nil {
// s.logger.Error("Failed to refund all outcome status", "bet_outcome_id", outcome.ID, "error", err)
// return err
// }
// // Check if all the bet outcomes have been set to refund for
// status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID)
// if err != nil {
// if err != bet.ErrOutcomesNotCompleted {
// s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err)
// }
// return err
// }
// err = s.betSvc.UpdateStatus(ctx, outcome.BetID, domain.OUTCOME_STATUS_VOID)
// if err != nil {
// s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err)
// return err
// }
// }
}
func (s *Service) FetchAndProcessResults(ctx context.Context) error { func (s *Service) FetchAndProcessResults(ctx context.Context) error {
// TODO: Optimize this because there could be many bet outcomes for the same odd // TODO: Optimize this because there could be many bet outcomes for the same odd
// Take market id and match result as param and update all the bet outcomes at the same time // Take market id and match result as param and update all the bet outcomes at the same time
@ -58,29 +181,15 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error {
} }
fmt.Printf("⚠️ Expired Events: %d \n", len(events)) fmt.Printf("⚠️ Expired Events: %d \n", len(events))
removed := 0 removed := 0
errs := make([]error, 0, len(events))
for i, event := range events { for _, event := range events {
eventID, err := strconv.ParseInt(event.ID, 10, 64) eventID, err := strconv.ParseInt(event.ID, 10, 64)
if err != nil { if err != nil {
s.logger.Error("Failed to parse event id") s.logger.Error("Failed to parse", "eventID", event.ID, "err", err)
errs = append(errs, fmt.Errorf("failed to parse event id %s: %w", event.ID, err))
continue
}
outcomes, err := s.repo.GetBetOutcomeByEventID(ctx, eventID)
if err != nil {
s.logger.Error("Failed to get pending bet outcomes", "error", err)
errs = append(errs, fmt.Errorf("failed to get pending bet outcomes for event %d: %w", eventID, err))
continue continue
} }
if len(outcomes) == 0 {
fmt.Printf("🕛 No bets have been placed on event %v (%d/%d) \n", event.ID, i+1, len(events))
} else {
fmt.Printf("✅ %d bets have been placed on event %v (%d/%d) \n", len(outcomes), event.ID, i+1, len(events))
}
isDeleted := true
result, err := s.fetchResult(ctx, eventID) result, err := s.fetchResult(ctx, eventID)
if err != nil { if err != nil {
if err == ErrEventIsNotActive { if err == ErrEventIsNotActive {
@ -108,78 +217,41 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error {
} }
// TODO: Figure out what to do with the events that have been cancelled or postponed, etc... // TODO: Figure out what to do with the events that have been cancelled or postponed, etc...
if timeStatusParsed != int64(domain.TIME_STATUS_ENDED) { // if timeStatusParsed != int64(domain.TIME_STATUS_ENDED) {
s.logger.Warn("Event is not ended yet", "event_id", eventID, "time_status", commonResp.TimeStatus) // s.logger.Warn("Event is not ended yet", "event_id", eventID, "time_status", commonResp.TimeStatus)
fmt.Printf("⚠️ Event %v is not ended yet (%d/%d) \n", event.ID, i+1, len(events)) // fmt.Printf("⚠️ Event %v is not ended yet (%d/%d) \n", event.ID, i+1, len(events))
isDeleted = false // isDeleted = false
// continue
// }
// notification := &domain.Notification{
// RecipientID: recipientID,
// Type: domain.NOTIFICATION_TYPE_WALLET,
// Level: domain.NotificationLevelWarning,
// Reciever: domain.NotificationRecieverSideAdmin,
// DeliveryChannel: domain.DeliveryChannelInApp,
// Payload: domain.NotificationPayload{
// Headline: "Wallet Threshold Alert",
// Message: message,
// },
// Priority: 2, // Medium priority
// }
switch timeStatusParsed {
case int64(domain.TIME_STATUS_NOT_STARTED), int64(domain.TIME_STATUS_IN_PLAY):
continue continue
}
for j, outcome := range outcomes { case int64(domain.TIME_STATUS_TO_BE_FIXED):
fmt.Printf("⚙️ Processing 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n", s.logger.Warn("Event needs to be rescheduled or corrected", "event_id", eventID)
outcome.MarketName, // Admin users will be able to review the events
event.HomeTeam+" "+event.AwayTeam, event.ID,
j+1, len(outcomes))
if outcome.Expires.After(time.Now()) { case int64(domain.TIME_STATUS_ENDED), int64(domain.TIME_STATUS_WALKOVER), int64(domain.TIME_STATUS_DECIDED_BY_FA):
isDeleted = false err = s.UpdateResultForOutcomes(ctx, eventID, result.Results[0], sportID)
s.logger.Warn("Outcome is not expired yet", "event_id", event.ID, "outcome_id", outcome.ID)
continue
}
parseResult, err := s.parseResult(ctx, result.Results[0], outcome, sportID)
if err != nil { if err != nil {
isDeleted = false s.logger.Error("Error while updating result for event", "event_id", eventID)
s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err)
errs = append(errs, fmt.Errorf("failed to parse result for event %d: %w", outcome.EventID, err))
continue
}
outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, parseResult.Status)
if err != nil {
isDeleted = false
s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err)
continue
}
if outcome.Status == domain.OUTCOME_STATUS_ERROR || outcome.Status == domain.OUTCOME_STATUS_PENDING {
fmt.Printf("❌ Error while updating 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n",
outcome.MarketName,
event.HomeTeam+" "+event.AwayTeam, event.ID,
j+1, len(outcomes))
s.logger.Error("Outcome is pending or error", "event_id", outcome.EventID, "outcome_id", outcome.ID)
isDeleted = false
continue
} }
fmt.Printf("✅ Successfully updated 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n", s.logger.Info("Removing Event", "eventID", event.ID)
outcome.MarketName,
event.HomeTeam+" "+event.AwayTeam, event.ID,
j+1, len(outcomes))
status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID)
if err != nil {
if err != bet.ErrOutcomesNotCompleted {
s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err)
}
isDeleted = false
continue
}
fmt.Printf("🧾 Updating bet %v - event %v (%d/%d) to %v\n", outcome.BetID, event.ID, j+1, len(outcomes), status.String())
err = s.betSvc.UpdateStatus(ctx, outcome.BetID, status)
if err != nil {
s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err)
isDeleted = false
continue
}
fmt.Printf("✅ Successfully updated 🎫 Bet %v - event %v(%v) (%d/%d) \n",
outcome.BetID,
event.HomeTeam+" "+event.AwayTeam, event.ID,
j+1, len(outcomes))
}
if isDeleted {
removed += 1
fmt.Printf("⚠️ Removing Event %v \n", event.ID)
err = s.repo.DeleteEvent(ctx, event.ID) err = s.repo.DeleteEvent(ctx, event.ID)
if err != nil { if err != nil {
s.logger.Error("Failed to remove event", "event_id", event.ID, "error", err) s.logger.Error("Failed to remove event", "event_id", event.ID, "error", err)
@ -190,17 +262,91 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error {
s.logger.Error("Failed to remove odds for event", "event_id", event.ID, "error", err) s.logger.Error("Failed to remove odds for event", "event_id", event.ID, "error", err)
return err return err
} }
removed += 1
case int64(domain.TIME_STATUS_ABANDONED), int64(domain.TIME_STATUS_CANCELLED), int64(domain.TIME_STATUS_REMOVED):
s.logger.Info("Event abandoned/cancelled/removed", "event_id", eventID, "status", timeStatusParsed)
err = s.RefundAllOutcomes(ctx, eventID)
s.logger.Info("Removing Event", "eventID", event.ID)
err = s.repo.DeleteEvent(ctx, event.ID)
if err != nil {
s.logger.Error("Failed to remove event", "event_id", event.ID, "error", err)
return err
}
err = s.repo.DeleteOddsForEvent(ctx, event.ID)
if err != nil {
s.logger.Error("Failed to remove odds for event", "event_id", event.ID, "error", err)
return err
}
removed += 1
} }
// for j, outcome := range outcomes {
// fmt.Printf("⚙️ Processing 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n",
// outcome.MarketName,
// event.HomeTeam+" "+event.AwayTeam, event.ID,
// j+1, len(outcomes))
// if outcome.Expires.After(time.Now()) {
// isDeleted = false
// s.logger.Warn("Outcome is not expired yet", "event_id", event.ID, "outcome_id", outcome.ID)
// continue
// }
// parseResult, err := s.parseResult(ctx, result.Results[0], outcome, sportID)
// if err != nil {
// isDeleted = false
// s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err)
// errs = append(errs, fmt.Errorf("failed to parse result for event %d: %w", outcome.EventID, err))
// continue
// }
// outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, parseResult.Status)
// if err != nil {
// isDeleted = false
// s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err)
// continue
// }
// if outcome.Status == domain.OUTCOME_STATUS_ERROR || outcome.Status == domain.OUTCOME_STATUS_PENDING {
// fmt.Printf("❌ Error while updating 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n",
// outcome.MarketName,
// event.HomeTeam+" "+event.AwayTeam, event.ID,
// j+1, len(outcomes))
// s.logger.Error("Outcome is pending or error", "event_id", outcome.EventID, "outcome_id", outcome.ID)
// isDeleted = false
// continue
// }
// fmt.Printf("✅ Successfully updated 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n",
// outcome.MarketName,
// event.HomeTeam+" "+event.AwayTeam, event.ID,
// j+1, len(outcomes))
// status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID)
// if err != nil {
// if err != bet.ErrOutcomesNotCompleted {
// s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err)
// }
// isDeleted = false
// continue
// }
// fmt.Printf("🧾 Updating bet %v - event %v (%d/%d) to %v\n", outcome.BetID, event.ID, j+1, len(outcomes), status.String())
// err = s.betSvc.UpdateStatus(ctx, outcome.BetID, status)
// if err != nil {
// s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err)
// isDeleted = false
// continue
// }
// fmt.Printf("✅ Successfully updated 🎫 Bet %v - event %v(%v) (%d/%d) \n",
// outcome.BetID,
// event.HomeTeam+" "+event.AwayTeam, event.ID,
// j+1, len(outcomes))
// }
} }
fmt.Printf("🗑️ Removed Events: %d \n", removed) s.logger.Info("Total Number of Removed Events", "count", removed)
if len(errs) > 0 {
s.logger.Error("Errors occurred while processing results", "errors", errs)
for _, err := range errs {
fmt.Println("Error:", err)
}
return fmt.Errorf("errors occurred while processing results: %v", errs)
}
s.logger.Info("Successfully processed results", "removed_events", removed, "total_events", len(events)) s.logger.Info("Successfully processed results", "removed_events", removed, "total_events", len(events))
return nil return nil
} }
@ -211,10 +357,9 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error
s.logger.Error("Failed to fetch events") s.logger.Error("Failed to fetch events")
return 0, err return 0, err
} }
fmt.Printf("⚠️ Expired Events: %d \n", len(events))
updated := 0 updated := 0
for i, event := range events { for _, event := range events {
fmt.Printf("⚙️ Processing event %v (%d/%d) \n", event.ID, i+1, len(events)) // fmt.Printf("⚙️ Processing event %v (%d/%d) \n", event.ID, i+1, len(events))
eventID, err := strconv.ParseInt(event.ID, 10, 64) eventID, err := strconv.ParseInt(event.ID, 10, 64)
if err != nil { if err != nil {
s.logger.Error("Failed to parse event id") s.logger.Error("Failed to parse event id")
@ -232,7 +377,6 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error
} }
if result.Success != 1 || len(result.Results) == 0 { if result.Success != 1 || len(result.Results) == 0 {
s.logger.Error("Invalid API response", "event_id", eventID) s.logger.Error("Invalid API response", "event_id", eventID)
fmt.Printf("⚠️ Invalid API response for event %v \n", result)
continue continue
} }
@ -282,12 +426,13 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error
continue continue
} }
updated++ updated++
fmt.Printf("✅ Successfully updated event %v to %v (%d/%d) \n", event.ID, eventStatus, i+1, len(events)) // fmt.Printf("✅ Successfully updated event %v to %v (%d/%d) \n", event.ID, eventStatus, i+1, len(events))
s.logger.Info("Updated Event Status", "event ID", event.ID, "status", eventStatus)
// Update the league because the league country code is only found on the result response // Update the league because the league country code is only found on the result response
leagueID, err := strconv.ParseInt(commonResp.League.ID, 10, 64) leagueID, err := strconv.ParseInt(commonResp.League.ID, 10, 64)
if err != nil { if err != nil {
log.Printf("❌ Invalid league id, leagueID %v", commonResp.League.ID) // log.Printf("❌ Invalid league id, leagueID %v", commonResp.League.ID)
s.logger.Error("Invalid League ID", "leagueID", commonResp.League.ID)
continue continue
} }
@ -304,12 +449,11 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error
}) })
if err != nil { if err != nil {
log.Printf("❌ Error Updating League %v", commonResp.League.Name) s.logger.Error("Error Updating League", "League Name", commonResp.League.Name, "err", err)
log.Printf("err:%v", err)
continue continue
} }
fmt.Printf("✅ Updated League %v with country code %v \n", leagueID, commonResp.League.CC) // fmt.Printf("✅ Updated League %v with country code %v \n", leagueID, commonResp.League.CC)
s.logger.Info("Updated League with country code", "leagueID", leagueID, "code", commonResp.League.CC)
} }
if updated == 0 { if updated == 0 {

View File

@ -46,50 +46,50 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
spec string spec string
task func() task func()
}{ }{
{ // {
spec: "0 0 * * * *", // Every 1 hour // spec: "0 0 * * * *", // Every 1 hour
task: func() { // task: func() {
if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
log.Printf("FetchUpcomingEvents error: %v", err) // log.Printf("FetchUpcomingEvents error: %v", err)
} // }
}, // },
}, // },
{ // {
spec: "0 0 * * * *", // Every 15 minutes // spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events)
task: func() { // task: func() {
if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
log.Printf("FetchNonLiveOdds error: %v", err) // log.Printf("FetchNonLiveOdds error: %v", err)
} // }
}, // },
}, // },
{ {
spec: "0 */5 * * * *", // Every 5 Minutes spec: "0 */5 * * * *", // Every 5 Minutes
task: func() { task: func() {
log.Println("Updating expired events status...") log.Println("Updating expired events status...")
// if _, err := resultService.CheckAndUpdateExpiredEvents(context.Background()); err != nil { if _, err := resultService.CheckAndUpdateExpiredEvents(context.Background()); err != nil {
// log.Printf("Failed to update events: %v", err) log.Printf("Failed to update events: %v", err)
// } else { } else {
// log.Printf("Successfully updated expired events") log.Printf("Successfully updated expired events")
// } }
// }, },
// }, },
// { {
// spec: "0 */15 * * * *", // Every 15 Minutes spec: "0 */15 * * * *", // Every 15 Minutes
// task: func() { task: func() {
// log.Println("Fetching results for upcoming events...") log.Println("Fetching results for upcoming events...")
// if err := resultService.FetchAndProcessResults(context.Background()); err != nil { if err := resultService.FetchAndProcessResults(context.Background()); err != nil {
// log.Printf("Failed to process result: %v", err) log.Printf("Failed to process result: %v", err)
// } else { } else {
// log.Printf("Successfully processed all outcomes") log.Printf("Successfully processed all outcomes")
// } }
// }, },
// }, },
} }
for _, job := range schedule { for _, job := range schedule {
// job.task() job.task()
if _, err := c.AddFunc(job.spec, job.task); err != nil { if _, err := c.AddFunc(job.spec, job.task); err != nil {
log.Fatalf("Failed to schedule cron job: %v", err) log.Fatalf("Failed to schedule cron job: %v", err)
} }

View File

@ -106,3 +106,32 @@ func (h *Handler) SetLeagueActive(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil) return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil)
} }
func (h *Handler) SetLeagueAsFeatured(c *fiber.Ctx) error {
fmt.Printf("Set Active Leagues")
leagueIdStr := c.Params("id")
if leagueIdStr == "" {
response.WriteJSON(c, fiber.StatusBadRequest, "Missing league id", nil, nil)
}
leagueId, err := strconv.Atoi(leagueIdStr)
if err != nil {
response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, nil)
}
var req SetLeagueActiveReq
if err := c.BodyParser(&req); err != nil {
h.logger.Error("SetLeagueReq failed", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Failed to parse request", err, nil)
}
valErrs, ok := h.validator.Validate(c, req)
if !ok {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
}
if err := h.leagueSvc.SetLeagueActive(c.Context(), int64(leagueId), req.IsActive); err != nil {
response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update league", err, nil)
}
return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil)
}

View File

@ -56,8 +56,8 @@ func (h *Handler) GetRawOddsByMarketID(c *fiber.Ctx) error {
rawOdds, err := h.prematchSvc.GetRawOddsByMarketID(c.Context(), marketID, upcomingID) rawOdds, err := h.prematchSvc.GetRawOddsByMarketID(c.Context(), marketID, upcomingID)
if err != nil { if err != nil {
fmt.Printf("Failed to fetch raw odds: %v market_id:%v upcomingID:%v\n", err, marketID, upcomingID) // fmt.Printf("Failed to fetch raw odds: %v market_id:%v upcomingID:%v\n", err, marketID, upcomingID)
h.logger.Error("failed to fetch raw odds", "error", err) h.logger.Error("Failed to get raw odds by market ID", "marketID", marketID, "upcomingID", upcomingID, "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve raw odds", err, nil) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve raw odds", err, nil)
} }
@ -172,6 +172,62 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error {
} }
type TopLeaguesRes struct {
Leagues []TopLeague `json:"leagues"`
}
type TopLeague struct {
LeagueID int64 `json:"league_id"`
LeagueName string `json:"league_name"`
Events []domain.UpcomingEvent `json:"events"`
// Total int64 `json:"total"`
}
// @Summary Retrieve all top leagues
// @Description Retrieve all top leagues
// @Tags prematch
// @Accept json
// @Produce json
// @Success 200 {array} domain.UpcomingEvent
// @Failure 500 {object} response.APIResponse
// @Router /top-leagues [get]
func (h *Handler) GetTopLeagues(c *fiber.Ctx) error {
leagues, err := h.leagueSvc.GetFeaturedLeagues(c.Context())
if err != nil {
h.logger.Error("Error while fetching top leagues", "err", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get featured leagues", nil, nil)
}
var topLeague []TopLeague = make([]TopLeague, 0, len(leagues))
for _, league := range leagues {
events, _, err := h.eventSvc.GetPaginatedUpcomingEvents(
c.Context(), domain.EventFilter{
LeagueID: domain.ValidInt32{
Value: int32(league.ID),
Valid: true,
},
})
if err != nil {
fmt.Printf("Error while fetching events for top league %v \n", league.ID)
h.logger.Error("Error while fetching events for top league", "League ID", league.ID)
}
topLeague = append(topLeague, TopLeague{
LeagueID: league.ID,
LeagueName: league.Name,
Events: events,
})
}
res := TopLeaguesRes{
Leagues: topLeague,
}
return response.WriteJSON(c, fiber.StatusOK, "All top leagues events retrieved successfully", res, nil)
}
// @Summary Retrieve an upcoming by ID // @Summary Retrieve an upcoming by ID
// @Description Retrieve an upcoming event by ID // @Description Retrieve an upcoming event by ID
// @Tags prematch // @Tags prematch

View File

@ -130,6 +130,7 @@ func (a *App) initAppRoutes() {
a.fiber.Get("/events", h.GetAllUpcomingEvents) a.fiber.Get("/events", h.GetAllUpcomingEvents)
a.fiber.Get("/events/:id", h.GetUpcomingEventByID) a.fiber.Get("/events/:id", h.GetUpcomingEventByID)
a.fiber.Delete("/events/:id", a.authMiddleware, a.SuperAdminOnly, h.SetEventStatusToRemoved) a.fiber.Delete("/events/:id", a.authMiddleware, a.SuperAdminOnly, h.SetEventStatusToRemoved)
a.fiber.Get("/top-leagues", h.GetTopLeagues)
// Leagues // Leagues
a.fiber.Get("/leagues", h.GetAllLeagues) a.fiber.Get("/leagues", h.GetAllLeagues)
@ -192,7 +193,7 @@ func (a *App) initAppRoutes() {
a.fiber.Get("/wallet/:id", h.GetWalletByID) a.fiber.Get("/wallet/:id", h.GetWalletByID)
a.fiber.Put("/wallet/:id", h.UpdateWalletActive) a.fiber.Put("/wallet/:id", h.UpdateWalletActive)
a.fiber.Get("/branchWallet", a.authMiddleware, h.GetAllBranchWallets) a.fiber.Get("/branchWallet", a.authMiddleware, h.GetAllBranchWallets)
a.fiber.Get("/cashierWallet", a.authMiddleware, h.GetWalletForCashier) a.fiber.Get("/cashierWallet", a.authMiddleware, h.GetWalletForCashier)
// Transfer // Transfer
// /transfer/wallet - transfer from one wallet to another wallet // /transfer/wallet - transfer from one wallet to another wallet