diff --git a/cmd/main.go b/cmd/main.go index 102d78b..429b72b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,6 +2,7 @@ package main import ( // "context" + "fmt" "log/slog" "os" diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 97562cb..1db8ddb 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -203,7 +203,8 @@ CREATE TABLE events ( match_period INT, is_live BOOLEAN, status TEXT, - fetched_at TIMESTAMP DEFAULT now() + fetched_at TIMESTAMP DEFAULT now(), + source TEXT DEFAULT 'b365api' ); CREATE TABLE odds ( id SERIAL PRIMARY KEY, diff --git a/db/query/events.sql b/db/query/events.sql index 5fb5e46..e470aee 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -19,7 +19,8 @@ INSERT INTO events ( added_time, match_period, is_live, - status + status, + source ) VALUES ( $1, @@ -41,7 +42,8 @@ VALUES ( $17, $18, $19, - $20 + $20, + $21 ) ON CONFLICT (id) DO UPDATE SET sport_id = EXCLUDED.sport_id, @@ -63,6 +65,7 @@ SET sport_id = EXCLUDED.sport_id, match_period = EXCLUDED.match_period, is_live = EXCLUDED.is_live, status = EXCLUDED.status, + source = EXCLUDED.source, fetched_at = now(); -- name: InsertUpcomingEvent :exec INSERT INTO events ( @@ -80,7 +83,8 @@ INSERT INTO events ( league_cc, start_time, is_live, - status + status, + source ) VALUES ( $1, @@ -97,7 +101,8 @@ VALUES ( $12, $13, false, - 'upcoming' + 'upcoming', + $14 ) ON CONFLICT (id) DO UPDATE SET sport_id = EXCLUDED.sport_id, @@ -114,6 +119,7 @@ SET sport_id = EXCLUDED.sport_id, start_time = EXCLUDED.start_time, is_live = false, status = 'upcoming', + source = EXCLUDED.source, fetched_at = now(); -- name: ListLiveEvents :many SELECT id @@ -135,6 +141,7 @@ SELECT id, start_time, is_live, status, + source, fetched_at FROM events WHERE is_live = false @@ -156,6 +163,7 @@ SELECT id, start_time, is_live, status, + source, fetched_at FROM events WHERE start_time < now() @@ -197,6 +205,7 @@ SELECT id, start_time, is_live, status, + source, fetched_at FROM events WHERE is_live = false @@ -235,6 +244,7 @@ SELECT id, start_time, is_live, status, + source, fetched_at FROM events WHERE id = $1 diff --git a/gen/db/auth.sql.go b/gen/db/auth.sql.go index 527f25c..9c55b29 100644 --- a/gen/db/auth.sql.go +++ b/gen/db/auth.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: auth.sql package dbgen diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index 40182ae..e4cde1d 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: bet.sql package dbgen diff --git a/gen/db/branch.sql.go b/gen/db/branch.sql.go index 93e9b2b..5d236d3 100644 --- a/gen/db/branch.sql.go +++ b/gen/db/branch.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: branch.sql package dbgen diff --git a/gen/db/company.sql.go b/gen/db/company.sql.go index 3c5a6b1..449c8fd 100644 --- a/gen/db/company.sql.go +++ b/gen/db/company.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: company.sql package dbgen diff --git a/gen/db/copyfrom.go b/gen/db/copyfrom.go index 900af58..1212253 100644 --- a/gen/db/copyfrom.go +++ b/gen/db/copyfrom.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: copyfrom.go package dbgen diff --git a/gen/db/db.go b/gen/db/db.go index d892683..84de07c 100644 --- a/gen/db/db.go +++ b/gen/db/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 package dbgen diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index d95a9db..d7c6824 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: events.sql package dbgen @@ -37,6 +37,7 @@ SELECT id, start_time, is_live, status, + source, fetched_at FROM events WHERE is_live = false @@ -60,6 +61,7 @@ type GetAllUpcomingEventsRow struct { StartTime pgtype.Timestamp `json:"start_time"` IsLive pgtype.Bool `json:"is_live"` Status pgtype.Text `json:"status"` + Source pgtype.Text `json:"source"` FetchedAt pgtype.Timestamp `json:"fetched_at"` } @@ -88,6 +90,7 @@ func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]GetAllUpcomingEve &i.StartTime, &i.IsLive, &i.Status, + &i.Source, &i.FetchedAt, ); err != nil { return nil, err @@ -116,6 +119,7 @@ SELECT id, start_time, is_live, status, + source, fetched_at FROM events WHERE start_time < now() @@ -138,6 +142,7 @@ type GetExpiredUpcomingEventsRow struct { StartTime pgtype.Timestamp `json:"start_time"` IsLive pgtype.Bool `json:"is_live"` Status pgtype.Text `json:"status"` + Source pgtype.Text `json:"source"` FetchedAt pgtype.Timestamp `json:"fetched_at"` } @@ -166,6 +171,7 @@ func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context) ([]GetExpiredUpc &i.StartTime, &i.IsLive, &i.Status, + &i.Source, &i.FetchedAt, ); err != nil { return nil, err @@ -194,6 +200,7 @@ SELECT id, start_time, is_live, status, + source, fetched_at FROM events WHERE is_live = false @@ -243,6 +250,7 @@ type GetPaginatedUpcomingEventsRow struct { StartTime pgtype.Timestamp `json:"start_time"` IsLive pgtype.Bool `json:"is_live"` Status pgtype.Text `json:"status"` + Source pgtype.Text `json:"source"` FetchedAt pgtype.Timestamp `json:"fetched_at"` } @@ -278,6 +286,7 @@ func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginat &i.StartTime, &i.IsLive, &i.Status, + &i.Source, &i.FetchedAt, ); err != nil { return nil, err @@ -348,6 +357,7 @@ SELECT id, start_time, is_live, status, + source, fetched_at FROM events WHERE id = $1 @@ -372,6 +382,7 @@ type GetUpcomingByIDRow struct { StartTime pgtype.Timestamp `json:"start_time"` IsLive pgtype.Bool `json:"is_live"` Status pgtype.Text `json:"status"` + Source pgtype.Text `json:"source"` FetchedAt pgtype.Timestamp `json:"fetched_at"` } @@ -394,6 +405,7 @@ func (q *Queries) GetUpcomingByID(ctx context.Context, id string) (GetUpcomingBy &i.StartTime, &i.IsLive, &i.Status, + &i.Source, &i.FetchedAt, ) return i, err @@ -420,7 +432,8 @@ INSERT INTO events ( added_time, match_period, is_live, - status + status, + source ) VALUES ( $1, @@ -442,7 +455,8 @@ VALUES ( $17, $18, $19, - $20 + $20, + $21 ) ON CONFLICT (id) DO UPDATE SET sport_id = EXCLUDED.sport_id, @@ -464,6 +478,7 @@ SET sport_id = EXCLUDED.sport_id, match_period = EXCLUDED.match_period, is_live = EXCLUDED.is_live, status = EXCLUDED.status, + source = EXCLUDED.source, fetched_at = now() ` @@ -488,6 +503,7 @@ type InsertEventParams struct { MatchPeriod pgtype.Int4 `json:"match_period"` IsLive pgtype.Bool `json:"is_live"` Status pgtype.Text `json:"status"` + Source pgtype.Text `json:"source"` } func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error { @@ -512,6 +528,7 @@ func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error arg.MatchPeriod, arg.IsLive, arg.Status, + arg.Source, ) return err } @@ -532,7 +549,8 @@ INSERT INTO events ( league_cc, start_time, is_live, - status + status, + source ) VALUES ( $1, @@ -549,7 +567,8 @@ VALUES ( $12, $13, false, - 'upcoming' + 'upcoming', + $14 ) ON CONFLICT (id) DO UPDATE SET sport_id = EXCLUDED.sport_id, @@ -566,6 +585,7 @@ SET sport_id = EXCLUDED.sport_id, start_time = EXCLUDED.start_time, is_live = false, status = 'upcoming', + source = EXCLUDED.source, fetched_at = now() ` @@ -583,6 +603,7 @@ type InsertUpcomingEventParams struct { LeagueName pgtype.Text `json:"league_name"` LeagueCc pgtype.Text `json:"league_cc"` StartTime pgtype.Timestamp `json:"start_time"` + Source pgtype.Text `json:"source"` } func (q *Queries) InsertUpcomingEvent(ctx context.Context, arg InsertUpcomingEventParams) error { @@ -600,6 +621,7 @@ func (q *Queries) InsertUpcomingEvent(ctx context.Context, arg InsertUpcomingEve arg.LeagueName, arg.LeagueCc, arg.StartTime, + arg.Source, ) return err } diff --git a/gen/db/models.go b/gen/db/models.go index 56f95f0..d4b2712 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 package dbgen @@ -198,6 +198,7 @@ type Event struct { IsLive pgtype.Bool `json:"is_live"` Status pgtype.Text `json:"status"` FetchedAt pgtype.Timestamp `json:"fetched_at"` + Source pgtype.Text `json:"source"` } type Notification struct { diff --git a/gen/db/notification.sql.go b/gen/db/notification.sql.go index d30b3d1..8e91798 100644 --- a/gen/db/notification.sql.go +++ b/gen/db/notification.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: notification.sql package dbgen diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index 3d92299..ba59003 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: odds.sql package dbgen diff --git a/gen/db/otp.sql.go b/gen/db/otp.sql.go index 99cdd4c..7dba175 100644 --- a/gen/db/otp.sql.go +++ b/gen/db/otp.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: otp.sql package dbgen diff --git a/gen/db/referal.sql.go b/gen/db/referal.sql.go index d0ab21e..3a7f337 100644 --- a/gen/db/referal.sql.go +++ b/gen/db/referal.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: referal.sql package dbgen diff --git a/gen/db/result.sql.go b/gen/db/result.sql.go index cb3fdd8..bff7b1e 100644 --- a/gen/db/result.sql.go +++ b/gen/db/result.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: result.sql package dbgen diff --git a/gen/db/ticket.sql.go b/gen/db/ticket.sql.go index 054372d..8718bce 100644 --- a/gen/db/ticket.sql.go +++ b/gen/db/ticket.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: ticket.sql package dbgen diff --git a/gen/db/transactions.sql.go b/gen/db/transactions.sql.go index c95c84d..5bce39f 100644 --- a/gen/db/transactions.sql.go +++ b/gen/db/transactions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: transactions.sql package dbgen diff --git a/gen/db/transfer.sql.go b/gen/db/transfer.sql.go index 9bbf333..b9d2797 100644 --- a/gen/db/transfer.sql.go +++ b/gen/db/transfer.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: transfer.sql package dbgen diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index e0860c6..ca2da1e 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: user.sql package dbgen diff --git a/gen/db/virtual_games.sql.go b/gen/db/virtual_games.sql.go index eb832e7..16034ee 100644 --- a/gen/db/virtual_games.sql.go +++ b/gen/db/virtual_games.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: virtual_games.sql package dbgen diff --git a/gen/db/wallet.sql.go b/gen/db/wallet.sql.go index 64c3359..e46ea0b 100644 --- a/gen/db/wallet.sql.go +++ b/gen/db/wallet.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: wallet.sql package dbgen diff --git a/go.mod b/go.mod index 5a55392..e8b3f60 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,8 @@ require ( golang.org/x/crypto v0.36.0 ) +require github.com/gorilla/websocket v1.5.3 // indirect + require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/internal/domain/event.go b/internal/domain/event.go index 2a10da5..9a463ca 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -23,7 +23,35 @@ type Event struct { MatchPeriod int IsLive bool Status string + Source string } + +type BetResult struct { + Success int `json:"success"` + Pager struct { + Page int `json:"page"` + PerPage int `json:"per_page"` + Total int `json:"total"` + } + Results []struct { + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + League struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"league"` + Home struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"home"` + Away *struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"away"` + } `json:"results"` +} + type UpcomingEvent struct { ID string // Event ID SportID string // Sport ID @@ -38,6 +66,7 @@ type UpcomingEvent struct { LeagueName string // League name LeagueCC string // League country code StartTime time.Time // Converted from "time" field in UNIX format + Source string // bet api provider (bet365, betfair) } type MatchResult struct { EventID string diff --git a/internal/domain/resultres.go b/internal/domain/resultres.go index 8a17f24..493c0c9 100644 --- a/internal/domain/resultres.go +++ b/internal/domain/resultres.go @@ -151,3 +151,142 @@ type IceHockeyResultResponse struct { ConfirmedAt string `json:"confirmed_at"` Bet365ID string `json:"bet365_id"` } + +type CricketResultResponse struct { + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + TimeStatus string `json:"time_status"` + League struct { + ID string `json:"id"` + Name string `json:"name"` + CC string `json:"cc"` + } `json:"league"` + Home struct { + ID string `json:"id"` + Name string `json:"name"` + ImageID string `json:"image_id"` + CC string `json:"cc"` + } `json:"home"` + Away struct { + ID string `json:"id"` + Name string `json:"name"` + ImageID string `json:"image_id"` + CC string `json:"cc"` + } `json:"away"` + SS string `json:"ss"` + Extra struct { + HomePos string `json:"home_pos"` + AwayPos string `json:"away_pos"` + NumberOfPeriods string `json:"numberofperiods"` + PeriodLength string `json:"periodlength"` + StadiumData map[string]string `json:"stadium_data"` + } `json:"extra"` + HasLineup int `json:"has_lineup"` + ConfirmedAt string `json:"confirmed_at"` + Bet365ID string `json:"bet365_id"` +} + +type VolleyballResultResponse struct { + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + TimeStatus string `json:"time_status"` + League struct { + ID string `json:"id"` + Name string `json:"name"` + CC string `json:"cc"` + } `json:"league"` + Home struct { + ID string `json:"id"` + Name string `json:"name"` + ImageID string `json:"image_id"` + CC string `json:"cc"` + } `json:"home"` + Away struct { + ID string `json:"id"` + Name string `json:"name"` + ImageID string `json:"image_id"` + CC string `json:"cc"` + } `json:"away"` + SS string `json:"ss"` + Scores struct { + FirstSet Score `json:"1"` + SecondSet Score `json:"2"` + ThirdSet Score `json:"3"` + FourthSet Score `json:"4"` + FivethSet Score `json:"5"` + } `json:"scores"` + Stats struct { + PointsWonOnServe []string `json:"points_won_on_serve"` + LongestStreak []string `json:"longest_streak"` + } `json:"stats"` + InplayCreatedAt string `json:"inplay_created_at"` + InplayUpdatedAt string `json:"inplay_updated_at"` + Bet365ID string `json:"bet365_id"` +} + +type DartsResultResponse struct { + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + TimeStatus string `json:"time_status"` + League struct { + ID string `json:"id"` + Name string `json:"name"` + CC string `json:"cc"` + } `json:"league"` + Home struct { + ID string `json:"id"` + Name string `json:"name"` + ImageID string `json:"image_id"` + CC string `json:"cc"` + } `json:"home"` + Away struct { + ID string `json:"id"` + Name string `json:"name"` + ImageID string `json:"image_id"` + CC string `json:"cc"` + } `json:"away"` + SS string `json:"ss"` + InplayCreatedAt string `json:"inplay_created_at"` + InplayUpdatedAt string `json:"inplay_updated_at"` + ConfirmedAt string `json:"confirmed_at"` + Bet365ID string `json:"bet365_id"` +} + +type FutsalResultResponse struct { + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + TimeStatus string `json:"time_status"` + League struct { + ID string `json:"id"` + Name string `json:"name"` + CC string `json:"cc"` + } `json:"league"` + Home struct { + ID string `json:"id"` + Name string `json:"name"` + ImageID string `json:"image_id"` + CC string `json:"cc"` + } `json:"home"` + Away struct { + ID string `json:"id"` + Name string `json:"name"` + ImageID string `json:"image_id"` + CC string `json:"cc"` + } `json:"away"` + SS string `json:"ss"` + Scores struct { + FirstPeriod Score `json:"1"` + SecondPeriod Score `json:"2"` + ThirdPeriod Score `json:"3"` + TotalScore Score `json:"4"` + } `json:"scores"` + Events []map[string]string `json:"events"` + InplayCreatedAt string `json:"inplay_created_at"` + InplayUpdatedAt string `json:"inplay_updated_at"` + ConfirmedAt string `json:"confirmed_at"` + Bet365ID string `json:"bet365_id"` +} diff --git a/internal/domain/sportmarket.go b/internal/domain/sportmarket.go index c50a032..b5b7845 100644 --- a/internal/domain/sportmarket.go +++ b/internal/domain/sportmarket.go @@ -101,6 +101,72 @@ const ( ICE_HOCKEY_ALTERNATIVE_TOTAL_TWO_WAY IceHockeyMarket = 170240 ) +type CricketMarket int64 + +const ( + // Main + CRICKET_TO_WIN_THE_MATCH CricketMarket = 1246 + CRICKET_TEAM_TOP_BATTER CricketMarket = 1241 + CRICKET_TEAM_TOP_BOWLE CricketMarket = 1242 + CRICKET_PLAYER_OF_THE_MATCH CricketMarket = 346 + CRICKET_FIRST_WICKET_METHOD CricketMarket = 30205 + + // First Over + CRICKET_FIRST_OVER_TOTAL_RUNS CricketMarket = 300336 + CRICKET_FIRST_OVER_TOTAL_RUNS_Odd_Even CricketMarket = 300118 + + // Inninigs 1 + CRICKET_FIRST_INNINIGS_SCORE CricketMarket = 300338 + CRICKET_INNINGS_OF_MATCH_BOWLED_OUT CricketMarket = 300108 + + // Match + CRICKET_TOP_MATCH_BATTER CricketMarket = 30245 + CRICKET_TOP_MATCH_BOWLER CricketMarket = 30246 +) + +type VolleyballMarket int64 + +const ( + VOLLEYBALL_GAME_LINES VolleyballMarket = 910000 + VOLLEYBALL_CORRECT_SET_SCORE VolleyballMarket = 910201 + VOLLEYBALL_MATCH_TOTAL_ODD_EVEN VolleyballMarket = 910217 + VOLLEYBALL_SET_ONE_LINES VolleyballMarket = 910204 + VOLLEYBALL_SET_ONE_TO_GO_TO_EXTRA_POINTS VolleyballMarket = 910209 + VOLLEYBALL_SET_ONE_TOTAL_ODD_EVEN VolleyballMarket = 910218 +) + +type DartsMarket int64 + +const ( + // Main + DARTS_MATCH_WINNER DartsMarket = 703 // match_winner + DARTS_MATCH_DOUBLE DartsMarket = 150228 // match_double + DARTS_MATCH_TREBLE DartsMarket = 150230 // match_treble + DARTS_CORRECT_LEG_SCORE DartsMarket = 150015 // correct_leg_score + DARTS_TOTAL_LEGS DartsMarket = 150117 // total_legs + + DARTS_MOST_HUNDERED_EIGHTYS DartsMarket = 150030 // "most_180s" + DARTS_TOTAL_HUNDERED_EIGHTYS DartsMarket = 150012 // total_180s + DARTS_MOST_HUNDERED_EIGHTYS_HANDICAP DartsMarket = 150227 // most_180s_handicap + DARTS_PLAYER_HUNDERED_EIGHTYS DartsMarket = 150121 // player_180s + DARTS_FIRST_DART DartsMarket = 150125 // first_dart +) + +type FutsalMarket int64 + +const ( + // Main + FUTSAL_GAME_LINES FutsalMarket = 830001 + FUTSAL_MONEY_LINE FutsalMarket = 830130 + + // Others + FUTSAL_DOUBLE_RESULT_9_WAY FutsalMarket = 830124 + + // Score + FUTSAL_TEAM_TO_SCORE_FIRST FutsalMarket = 830141 + FUTSAL_RACE_TO_GOALS FutsalMarket = 830142 +) + type AmericanFootballMarket int64 const ( @@ -157,7 +223,7 @@ var SupportedMarkets = map[int64]bool{ int64(FOOTBALL_FIRST_HALF_ASIAN_CORNERS): true, int64(FOOTBALL_FIRST_HALF_GOALS_ODD_EVEN): true, int64(FOOTBALL_SECOND_HALF_GOALS_ODD_EVEN): true, - + // Basketball Markets int64(BASKETBALL_GAME_LINES): true, int64(BASKETBALL_RESULT_AND_BOTH_TEAMS_TO_SCORE_X_POINTS): true, @@ -214,6 +280,50 @@ var SupportedMarkets = map[int64]bool{ int64(ICE_HOCKEY_ALTERNATIVE_PUCK_LINE_TWO_WAY): false, int64(ICE_HOCKEY_ALTERNATIVE_TOTAL_TWO_WAY): false, + // Cricket Markets + int64(CRICKET_TO_WIN_THE_MATCH): true, + + int64(CRICKET_FIRST_OVER_TOTAL_RUNS_Odd_Even): false, + int64(CRICKET_FIRST_INNINIGS_SCORE): false, + int64(CRICKET_INNINGS_OF_MATCH_BOWLED_OUT): false, + int64(CRICKET_FIRST_OVER_TOTAL_RUNS): false, + int64(CRICKET_TEAM_TOP_BATTER): false, + int64(CRICKET_TEAM_TOP_BOWLE): false, + int64(CRICKET_PLAYER_OF_THE_MATCH): false, + int64(CRICKET_FIRST_WICKET_METHOD): false, + int64(CRICKET_TOP_MATCH_BATTER): false, + int64(CRICKET_TOP_MATCH_BOWLER): false, + + // Volleyball Markets + int64(VOLLEYBALL_GAME_LINES): true, + int64(VOLLEYBALL_CORRECT_SET_SCORE): true, + int64(VOLLEYBALL_MATCH_TOTAL_ODD_EVEN): true, + + int64(VOLLEYBALL_SET_ONE_LINES): false, + int64(VOLLEYBALL_SET_ONE_TO_GO_TO_EXTRA_POINTS): false, + int64(VOLLEYBALL_SET_ONE_TOTAL_ODD_EVEN): false, + + // Darts Markets + int64(DARTS_MATCH_WINNER): true, + int64(DARTS_TOTAL_LEGS): true, + int64(DARTS_CORRECT_LEG_SCORE): false, + int64(DARTS_MATCH_DOUBLE): false, + int64(DARTS_MATCH_TREBLE): false, + + int64(DARTS_MOST_HUNDERED_EIGHTYS): false, + int64(DARTS_TOTAL_HUNDERED_EIGHTYS): false, + int64(DARTS_MOST_HUNDERED_EIGHTYS_HANDICAP): false, + int64(DARTS_PLAYER_HUNDERED_EIGHTYS): false, + int64(DARTS_FIRST_DART): false, + + // Futsal Markets + int64(FUTSAL_MONEY_LINE): true, + int64(FUTSAL_GAME_LINES): true, + int64(FUTSAL_TEAM_TO_SCORE_FIRST): true, + + int64(FUTSAL_DOUBLE_RESULT_9_WAY): false, + int64(FUTSAL_RACE_TO_GOALS): false, + // American Football Markets int64(AMERICAN_FOOTBALL_MONEY_LINE): true, int64(AMERICAN_FOOTBALL_SPREAD): true, diff --git a/internal/repository/event.go b/internal/repository/event.go index b65e034..8f2ade8 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -40,6 +40,7 @@ func (s *Store) SaveEvent(ctx context.Context, e domain.Event) error { MatchPeriod: pgtype.Int4{Int32: int32(e.MatchPeriod), Valid: true}, IsLive: pgtype.Bool{Bool: e.IsLive, Valid: true}, Status: pgtype.Text{String: e.Status, Valid: true}, + Source: pgtype.Text{String: e.Source, Valid: true}, }) } func (s *Store) SaveUpcomingEvent(ctx context.Context, e domain.UpcomingEvent) error { @@ -57,6 +58,7 @@ func (s *Store) SaveUpcomingEvent(ctx context.Context, e domain.UpcomingEvent) e LeagueName: pgtype.Text{String: e.LeagueName, Valid: true}, LeagueCc: pgtype.Text{String: e.LeagueCC, Valid: true}, StartTime: pgtype.Timestamp{Time: e.StartTime, Valid: true}, + Source: pgtype.Text{String: e.Source, Valid: true}, }) } @@ -85,6 +87,7 @@ func (s *Store) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEven LeagueName: e.LeagueName.String, LeagueCC: e.LeagueCc.String, StartTime: e.StartTime.Time.UTC(), + Source: e.Source.String, } } return upcomingEvents, nil @@ -112,6 +115,7 @@ func (s *Store) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.Upcoming LeagueName: e.LeagueName.String, LeagueCC: e.LeagueCc.String, StartTime: e.StartTime.Time.UTC(), + Source: e.Source.String, } } return upcomingEvents, nil @@ -165,6 +169,7 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit domain.Val LeagueName: e.LeagueName.String, LeagueCC: e.LeagueCc.String, StartTime: e.StartTime.Time.UTC(), + Source: e.Source.String, } } totalCount, err := s.queries.GetTotalEvents(ctx, dbgen.GetTotalEventsParams{ @@ -212,6 +217,7 @@ func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.Upc LeagueName: event.LeagueName.String, LeagueCC: event.LeagueCc.String, StartTime: event.StartTime.Time.UTC(), + Source: event.Source.String, }, nil } func (s *Store) UpdateFinalScore(ctx context.Context, eventID, fullScore, status string) error { diff --git a/internal/services/event/service.go b/internal/services/event/service.go index dca4de6..b24a8ec 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -30,6 +30,30 @@ func New(token string, store *repository.Store) Service { } func (s *service) FetchLiveEvents(ctx context.Context) error { + var wg sync.WaitGroup + urls := []struct { + name string + source string + }{ + {"https://api.b365api.com/v1/bet365/inplay?sport_id=%d&token=%s", "bet365"}, + {"https://api.b365api.com/v1/betfair/sb/inplay?sport_id=%d&token=%s", "betfair"}, + {"https://api.b365api.com/v1/1xbet/inplay?sport_id=%d&token=%s", "1xbet"}, + } + + for _, url := range urls { + wg.Add(1) + + go func() { + defer wg.Done() + s.fetchLiveEvents(ctx, url.name, url.source) + }() + } + + wg.Wait() + return nil +} + +func (s *service) fetchLiveEvents(ctx context.Context, url, source string) error { sportIDs := []int{1, 13, 78, 18, 91, 16, 17, 14, 12, 3, 2, 4, 83, 15, 92, 94, 8, 19, 36, 66, 9, 75, 90, 95, 110, 107, 151, 162, 148} var wg sync.WaitGroup @@ -39,7 +63,7 @@ func (s *service) FetchLiveEvents(ctx context.Context) error { go func(sportID int) { defer wg.Done() - url := fmt.Sprintf("https://api.b365api.com/v1/bet365/inplay?sport_id=%d&token=%s", sportID, s.token) + url := fmt.Sprintf(url, sportID, s.token) resp, err := http.Get(url) if err != nil { fmt.Printf(" Failed request for sport_id=%d: %v\n", sportID, err) @@ -49,45 +73,20 @@ func (s *service) FetchLiveEvents(ctx context.Context) error { body, _ := io.ReadAll(resp.Body) - var data struct { - Success int `json:"success"` - Results [][]map[string]interface{} `json:"results"` - } - if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { - fmt.Printf(" Decode failed for sport_id=%d\nRaw: %s\n", sportID, string(body)) - return + events := []domain.Event{} + switch source { + case "bet365": + events = handleBet365prematch(body, sportID) + case "betfair": + events = handleBetfairprematch(body, sportID, source) + case "1xbet": + // betfair and 1xbet have the same result structure + events = handleBetfairprematch(body, sportID, source) } - for _, group := range data.Results { - for _, ev := range group { - if getString(ev["type"]) != "EV" { - continue - } - - event := domain.Event{ - ID: getString(ev["ID"]), - SportID: fmt.Sprintf("%d", sportID), - MatchName: getString(ev["NA"]), - Score: getString(ev["SS"]), - MatchMinute: getInt(ev["TM"]), - TimerStatus: getString(ev["TT"]), - HomeTeamID: getString(ev["HT"]), - AwayTeamID: getString(ev["AT"]), - HomeKitImage: getString(ev["K1"]), - AwayKitImage: getString(ev["K2"]), - LeagueName: getString(ev["CT"]), - LeagueID: getString(ev["C2"]), - LeagueCC: getString(ev["CB"]), - StartTime: time.Now().UTC().Format(time.RFC3339), - IsLive: true, - Status: "live", - MatchPeriod: getInt(ev["MD"]), - AddedTime: getInt(ev["TA"]), - } - - if err := s.store.SaveEvent(ctx, event); err != nil { - fmt.Printf("Could not store live event [id=%s]: %v\n", event.ID, err) - } + for _, event := range events { + if err := s.store.SaveEvent(ctx, event); err != nil { + fmt.Printf("Could not store live event [id=%s]: %v\n", event.ID, err) } } }(sportID) @@ -96,20 +95,128 @@ func (s *service) FetchLiveEvents(ctx context.Context) error { wg.Wait() fmt.Println("All live events fetched and stored.") return nil + +} + +func handleBet365prematch(body []byte, sportID int) []domain.Event { + var data struct { + Success int `json:"success"` + Results [][]map[string]interface{} `json:"results"` + } + + events := []domain.Event{} + if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { + fmt.Printf(" Decode failed for sport_id=%d\nRaw: %s\n", sportID, string(body)) + return events + } + + for _, group := range data.Results { + for _, ev := range group { + if getString(ev["type"]) != "EV" { + continue + } + + event := domain.Event{ + ID: getString(ev["ID"]), + SportID: fmt.Sprintf("%d", sportID), + MatchName: getString(ev["NA"]), + Score: getString(ev["SS"]), + MatchMinute: getInt(ev["TM"]), + TimerStatus: getString(ev["TT"]), + HomeTeamID: getString(ev["HT"]), + AwayTeamID: getString(ev["AT"]), + HomeKitImage: getString(ev["K1"]), + AwayKitImage: getString(ev["K2"]), + LeagueName: getString(ev["CT"]), + LeagueID: getString(ev["C2"]), + LeagueCC: getString(ev["CB"]), + StartTime: time.Now().UTC().Format(time.RFC3339), + IsLive: true, + Status: "live", + MatchPeriod: getInt(ev["MD"]), + AddedTime: getInt(ev["TA"]), + Source: "bet365", + } + + events = append(events, event) + } + } + + return events +} + +func handleBetfairprematch(body []byte, sportID int, source string) []domain.Event { + var data struct { + Success int `json:"success"` + Results []map[string]interface{} `json:"results"` + } + + events := []domain.Event{} + if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { + fmt.Printf(" Decode failed for sport_id=%d\nRaw: %s\n", sportID, string(body)) + return events + } + + for _, ev := range data.Results { + homeRaw, _ := ev["home"].(map[string]interface{}) + homeId, _ := homeRaw["id"].(string) + + awayRaw, _ := ev["home"].(map[string]interface{}) + awayId, _ := awayRaw["id"].(string) + + event := domain.Event{ + ID: getString(ev["id"]), + SportID: fmt.Sprintf("%d", sportID), + TimerStatus: getString(ev["time_status"]), + HomeTeamID: homeId, + AwayTeamID: awayId, + StartTime: time.Now().UTC().Format(time.RFC3339), + IsLive: true, + Status: "live", + Source: source, + } + + events = append(events, event) + } + + return events } func (s *service) FetchUpcomingEvents(ctx context.Context) error { - sportIDs := []int{1, 18, 17} + var wg sync.WaitGroup + urls := []struct { + name string + source string + }{ + {"https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s&page=%d", "bet365"}, + {"https://api.b365api.com/v1/betfair/sb/upcoming?sport_id=%d&token=%s&page=%d", "betfair"}, + {"https://api.b365api.com/v1/1xbet/upcoming?sport_id=%d&token=%s&page=%d", "1xbet"}, + } + for _, url := range urls { + wg.Add(1) + + go func() { + defer wg.Done() + s.fetchUpcomingEventsFromProvider(ctx, url.name, url.source) + }() + } + + wg.Wait() + return nil +} + +func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, source string) { + sportIDs := []int{1, 18, 17} + var totalPages int = 1 + var page int = 0 + var limit int = 100 + var count int = 0 for _, sportID := range sportIDs { - var totalPages int = 1 - var page int = 0 - var limit int = 150 - var count int = 0 for page <= totalPages { page = page + 1 url := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s&page=%d", sportID, s.token, page) - log.Printf("📡 Fetching data for sport %d event data page %d/%d", sportID, page, min(limit, totalPages)) + log.Printf("📡 Fetching data for event data page %d", page) resp, err := http.Get(url) if err != nil { log.Printf("❌ Failed to fetch event data for page %d: %v", page, err) @@ -118,31 +225,8 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) - var data struct { - Success int `json:"success"` - Pager struct { - Page int `json:"page"` - PerPage int `json:"per_page"` - Total int `json:"total"` - } - Results []struct { - ID string `json:"id"` - SportID string `json:"sport_id"` - Time string `json:"time"` - League struct { - ID string `json:"id"` - Name string `json:"name"` - } `json:"league"` - Home struct { - ID string `json:"id"` - Name string `json:"name"` - } `json:"home"` - Away *struct { - ID string `json:"id"` - Name string `json:"name"` - } `json:"away"` - } `json:"results"` - } + var data domain.BetResult + if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { log.Printf("❌ Failed to parse json data") continue @@ -182,6 +266,7 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { LeagueName: ev.League.Name, LeagueCC: "", StartTime: time.Unix(startUnix, 0).UTC(), + Source: source, } if ev.Away != nil { @@ -209,8 +294,6 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { count++ } } - - return nil } func getString(v interface{}) string { diff --git a/internal/services/result/eval.go b/internal/services/result/eval.go index cf078e6..218659c 100644 --- a/internal/services/result/eval.go +++ b/internal/services/result/eval.go @@ -182,32 +182,33 @@ func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away i if err != nil { return domain.OUTCOME_STATUS_ERROR, err } - continue + } else { + newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) + if err != nil { + fmt.Printf("multi outcome check error") + return domain.OUTCOME_STATUS_PENDING, err + } } - newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) - if err != nil { - return domain.OUTCOME_STATUS_ERROR, err - } - continue } else if adjustedHomeScore < adjustedAwayScore { if outcome.OddHeader == "2" { newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN) if err != nil { return domain.OUTCOME_STATUS_ERROR, err } - continue + } else { + newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) + if err != nil { + fmt.Printf("multi outcome check error") + return domain.OUTCOME_STATUS_PENDING, err + } } - newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) - if err != nil { - return domain.OUTCOME_STATUS_ERROR, err - } - continue - } else if adjustedHomeScore == adjustedAwayScore { + } + if newOutcome == domain.OUTCOME_STATUS_PENDING { newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID) if err != nil { - return domain.OUTCOME_STATUS_ERROR, err + fmt.Printf("multi outcome check error") + return domain.OUTCOME_STATUS_PENDING, err } - continue } } return newOutcome, nil @@ -498,491 +499,527 @@ func evaluateTotalOverUnder(outcome domain.BetOutcome, score struct{ Home, Away return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) } - // Result and Total betting is a type of bet where the bettor predicts - // the outcome of a match and whether the total number of points scored will be over or under a specified number. - func evaluateResultAndTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { - - // The handicap will be in the format "U {float}" or "O {float}" - // U and O denoting over and under for this case - overUnderStr := strings.Split(outcome.OddHandicap, " ") - if len(overUnderStr) != 2 { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName) - } - - overUnder := overUnderStr[0] - - if overUnder != "Over" && overUnder != "Under" { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over under: %s", outcome.OddHeader) - } - threshold, err := strconv.ParseFloat(overUnderStr[1], 64) - if err != nil { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName) - } - - // Since the threshold will come in a xx.5 format, there is no VOID for this kind of bet - totalScore := float64(score.Home + score.Away) - - switch outcome.OddHeader { - case "1": - if score.Home < score.Away { - return domain.OUTCOME_STATUS_LOSS, nil - } - - if overUnder == "Over" && totalScore > threshold { - return domain.OUTCOME_STATUS_WIN, nil - } else if overUnder == "Under" && totalScore < threshold { - return domain.OUTCOME_STATUS_WIN, nil - } - return domain.OUTCOME_STATUS_LOSS, nil - case "2": - if score.Away < score.Home { - return domain.OUTCOME_STATUS_LOSS, nil - } - if overUnder == "Over" && totalScore > threshold { - return domain.OUTCOME_STATUS_WIN, nil - } else if overUnder == "Under" && totalScore < threshold { - return domain.OUTCOME_STATUS_WIN, nil - } - - return domain.OUTCOME_STATUS_LOSS, nil - default: - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed to parse over and under: %s", outcome.OddName) - } +func evaluateTotalLegs(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + total, err := strconv.ParseFloat(outcome.OddName, 64) + if err != nil { + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid : %s", outcome.OddName) } - // Team Total betting is a type of bet where the bettor predicts the total number of points scored by a specific team in a match - // is over or under a specified number. - func evaluateTeamTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + totalLegs := float64(score.Home + score.Away) - // The handicap will be in the format "U {float}" or "O {float}" - // U and O denoting over and under for this case - overUnderStr := strings.Split(outcome.OddHandicap, " ") - if len(overUnderStr) != 2 { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddHandicap) + switch outcome.OddHeader { + case "Over": + if totalLegs > total { + return domain.OUTCOME_STATUS_WIN, nil } - - overUnder := overUnderStr[0] - - if overUnder != "Over" && overUnder != "Under" { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over under: %s", outcome.OddHeader) - } - threshold, err := strconv.ParseFloat(overUnderStr[1], 64) - if err != nil { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddHandicap) - } - - // Since the threshold will come in a xx.5 format, there is no VOID for this kind of bet - HomeScore := float64(score.Home) - AwayScore := float64(score.Away) - - switch outcome.OddHeader { - case "1": - if overUnder == "Over" && HomeScore > threshold { - return domain.OUTCOME_STATUS_WIN, nil - } else if overUnder == "Under" && HomeScore < threshold { - return domain.OUTCOME_STATUS_WIN, nil - } - return domain.OUTCOME_STATUS_LOSS, nil - case "2": - if overUnder == "Over" && AwayScore > threshold { - return domain.OUTCOME_STATUS_WIN, nil - } else if overUnder == "Under" && AwayScore < threshold { - return domain.OUTCOME_STATUS_WIN, nil - } - - return domain.OUTCOME_STATUS_LOSS, nil - default: - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed to parse over and under: %s", outcome.OddName) - } - } - - // Result and Both Teams To Score X Points is a type of bet where the bettor predicts whether both teams will score a certain number of points - // and also the result fo the match - func evaluateResultAndBTTSX(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { - - // The name parameter will hold value "name": "{team_name} and {Yes | No}" - // The best way to do this is to evaluate backwards since there might be - // teams with 'and' in their name - // We know that there is going to be "Yes" and "No " - oddNameSplit := strings.Split(outcome.OddName, " ") - - scoreCheckSplit := oddNameSplit[len(oddNameSplit)-1] - var isScorePoints bool - if scoreCheckSplit == "Yes" { - isScorePoints = true - } else if scoreCheckSplit == "No" { - isScorePoints = false - } else { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) - } - - teamName := strings.TrimSpace(strings.Join(oddNameSplit[:len(oddNameSplit)-2], "")) - - threshold, err := strconv.ParseInt(outcome.OddHeader, 10, 64) - if err != nil { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddHeader) - } - - switch teamName { - case outcome.HomeTeamName: - if score.Home > score.Away { - if isScorePoints && score.Home >= int(threshold) && score.Away >= int(threshold) { - return domain.OUTCOME_STATUS_WIN, nil - } else if !isScorePoints && score.Home < int(threshold) && score.Away < int(threshold) { - return domain.OUTCOME_STATUS_WIN, nil - } - } - case outcome.AwayTeamName: - if score.Away > score.Home { - if isScorePoints && score.Home >= int(threshold) && score.Away >= int(threshold) { - return domain.OUTCOME_STATUS_WIN, nil - } else if !isScorePoints && score.Home < int(threshold) && score.Away < int(threshold) { - return domain.OUTCOME_STATUS_WIN, nil - } - } - default: - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("team name error: %s", teamName) - } - return domain.OUTCOME_STATUS_LOSS, nil - - } - - // Both Teams To Score X Points is a type of bet where the bettor predicts whether both teams will score a certain number of points. - func evaluateBTTSX(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { - threshold, err := strconv.ParseInt(outcome.OddName, 10, 64) - if err != nil { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName) - } - - switch outcome.OddHeader { - case "Yes": - if score.Home >= int(threshold) && score.Away >= int(threshold) { - return domain.OUTCOME_STATUS_WIN, nil - } - case "No": - if score.Home < int(threshold) && score.Away < int(threshold) { - return domain.OUTCOME_STATUS_WIN, nil - } - - default: - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) - } - - return domain.OUTCOME_STATUS_LOSS, nil - } - - // Money Line 3 Way betting is a type of bet where the bettor predicts the outcome of a match with three possible outcomes: home win, away win, or draw. - func evaluateMoneyLine3Way(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { - switch outcome.OddName { - case "1": // Home win - if score.Home > score.Away { - return domain.OUTCOME_STATUS_WIN, nil - } - return domain.OUTCOME_STATUS_LOSS, nil - case "Tie": - if score.Home == score.Away { - return domain.OUTCOME_STATUS_WIN, nil - } - return domain.OUTCOME_STATUS_LOSS, nil - case "2": // Away win - if score.Away > score.Home { - return domain.OUTCOME_STATUS_WIN, nil - } - return domain.OUTCOME_STATUS_LOSS, nil - default: - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) - } - } - - // Double Result betting is a type of bet where the bettor predicts the outcome of a match at both half-time and full-time. - func evaluateDoubleResult(outcome domain.BetOutcome, firstHalfScore struct{ Home, Away int }, secondHalfScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { - halfWins := strings.Split(outcome.OddName, "-") - if len(halfWins) != 2 { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) - } - firstHalfWinner := strings.TrimSpace(halfWins[0]) - secondHalfWinner := strings.TrimSpace(halfWins[1]) - - if firstHalfWinner != outcome.HomeTeamName && firstHalfWinner != outcome.AwayTeamName && firstHalfWinner != "Tie" { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", firstHalfWinner) - } - if secondHalfWinner != outcome.HomeTeamName && secondHalfWinner != outcome.AwayTeamName && secondHalfWinner != "Tie" { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", firstHalfWinner) - } - - switch { - case firstHalfWinner == outcome.HomeTeamName && firstHalfScore.Home < firstHalfScore.Away: - return domain.OUTCOME_STATUS_LOSS, nil - case firstHalfWinner == outcome.AwayTeamName && firstHalfScore.Away < firstHalfScore.Home: - return domain.OUTCOME_STATUS_LOSS, nil - case firstHalfWinner == "Tie" && firstHalfScore.Home != firstHalfScore.Away: - return domain.OUTCOME_STATUS_LOSS, nil - } - - switch { - case secondHalfWinner == outcome.HomeTeamName && firstHalfScore.Home < firstHalfScore.Away: - return domain.OUTCOME_STATUS_LOSS, nil - case secondHalfWinner == outcome.AwayTeamName && firstHalfScore.Away < firstHalfScore.Home: - return domain.OUTCOME_STATUS_LOSS, nil - case secondHalfWinner == "Tie" && firstHalfScore.Home != firstHalfScore.Away: - return domain.OUTCOME_STATUS_LOSS, nil - } - - return domain.OUTCOME_STATUS_WIN, nil - } - - // Highest Scoring Half betting is a type of bet where the bettor predicts which half of the match will have the highest total score. - func evaluateHighestScoringHalf(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { - firstHalfTotal := firstScore.Home + firstScore.Away - secondHalfTotal := secondScore.Home + secondScore.Away - switch outcome.OddName { - case "1st Half": - if firstHalfTotal > secondHalfTotal { - return domain.OUTCOME_STATUS_WIN, nil - } - case "2nd Half": - if firstHalfTotal < secondHalfTotal { - return domain.OUTCOME_STATUS_WIN, nil - } - case "Tie": - if firstHalfTotal == secondHalfTotal { - return domain.OUTCOME_STATUS_WIN, nil - } - default: - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) + case "Under": + if totalLegs < total { + return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } - // Highest Scoring Quarter betting is a type of bet where the bettor predicts which quarter of the match will have the highest score. - func evaluateHighestScoringQuarter(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }, thirdScore struct{ Home, Away int }, fourthScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { - firstQuarterTotal := firstScore.Home + firstScore.Away - secondQuarterTotal := secondScore.Home + secondScore.Away - thirdQuarterTotal := thirdScore.Home + thirdScore.Away - fourthQuarterTotal := fourthScore.Home + fourthScore.Away + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) +} - switch outcome.OddName { - case "1st Quarter": - if firstQuarterTotal > secondQuarterTotal && firstQuarterTotal > thirdQuarterTotal && firstQuarterTotal > fourthQuarterTotal { - return domain.OUTCOME_STATUS_WIN, nil - } - case "2nd Quarter": - if secondQuarterTotal > firstQuarterTotal && secondQuarterTotal > thirdQuarterTotal && secondQuarterTotal > fourthQuarterTotal { - return domain.OUTCOME_STATUS_WIN, nil - } - case "3rd Quarter": - if thirdQuarterTotal > firstQuarterTotal && thirdQuarterTotal > secondQuarterTotal && thirdQuarterTotal > fourthQuarterTotal { - return domain.OUTCOME_STATUS_WIN, nil - } - case "4th Quarter": - if fourthQuarterTotal > firstQuarterTotal && fourthQuarterTotal > secondQuarterTotal && fourthQuarterTotal > thirdQuarterTotal { - return domain.OUTCOME_STATUS_WIN, nil - } - case "Tie": - if firstQuarterTotal == secondQuarterTotal || secondQuarterTotal == thirdQuarterTotal || thirdQuarterTotal == fourthQuarterTotal { - return domain.OUTCOME_STATUS_WIN, nil - } - default: - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) +// Result and Total betting is a type of bet where the bettor predicts +// the outcome of a match and whether the total number of points scored will be over or under a specified number. +func evaluateResultAndTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + + // The handicap will be in the format "U {float}" or "O {float}" + // U and O denoting over and under for this case + overUnderStr := strings.Split(outcome.OddHandicap, " ") + if len(overUnderStr) != 2 { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName) + } + + overUnder := overUnderStr[0] + + if overUnder != "Over" && overUnder != "Under" { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over under: %s", outcome.OddHeader) + } + threshold, err := strconv.ParseFloat(overUnderStr[1], 64) + if err != nil { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName) + } + + // Since the threshold will come in a xx.5 format, there is no VOID for this kind of bet + totalScore := float64(score.Home + score.Away) + + switch outcome.OddHeader { + case "1": + if score.Home < score.Away { + return domain.OUTCOME_STATUS_LOSS, nil } + + if overUnder == "Over" && totalScore > threshold { + return domain.OUTCOME_STATUS_WIN, nil + } else if overUnder == "Under" && totalScore < threshold { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case "2": + if score.Away < score.Home { + return domain.OUTCOME_STATUS_LOSS, nil + } + if overUnder == "Over" && totalScore > threshold { + return domain.OUTCOME_STATUS_WIN, nil + } else if overUnder == "Under" && totalScore < threshold { + return domain.OUTCOME_STATUS_WIN, nil + } + + return domain.OUTCOME_STATUS_LOSS, nil + default: + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed to parse over and under: %s", outcome.OddName) + } +} + +// Team Total betting is a type of bet where the bettor predicts the total number of points scored by a specific team in a match +// is over or under a specified number. +func evaluateTeamTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + + // The handicap will be in the format "U {float}" or "O {float}" + // U and O denoting over and under for this case + overUnderStr := strings.Split(outcome.OddHandicap, " ") + if len(overUnderStr) != 2 { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddHandicap) + } + + overUnder := overUnderStr[0] + + if overUnder != "Over" && overUnder != "Under" { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over under: %s", outcome.OddHeader) + } + threshold, err := strconv.ParseFloat(overUnderStr[1], 64) + if err != nil { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddHandicap) + } + + // Since the threshold will come in a xx.5 format, there is no VOID for this kind of bet + HomeScore := float64(score.Home) + AwayScore := float64(score.Away) + + switch outcome.OddHeader { + case "1": + if overUnder == "Over" && HomeScore > threshold { + return domain.OUTCOME_STATUS_WIN, nil + } else if overUnder == "Under" && HomeScore < threshold { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case "2": + if overUnder == "Over" && AwayScore > threshold { + return domain.OUTCOME_STATUS_WIN, nil + } else if overUnder == "Under" && AwayScore < threshold { + return domain.OUTCOME_STATUS_WIN, nil + } + + return domain.OUTCOME_STATUS_LOSS, nil + default: + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed to parse over and under: %s", outcome.OddName) + } +} + +// Result and Both Teams To Score X Points is a type of bet where the bettor predicts whether both teams will score a certain number of points +// and also the result fo the match +func evaluateResultAndBTTSX(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + + // The name parameter will hold value "name": "{team_name} and {Yes | No}" + // The best way to do this is to evaluate backwards since there might be + // teams with 'and' in their name + // We know that there is going to be "Yes" and "No " + oddNameSplit := strings.Split(outcome.OddName, " ") + + scoreCheckSplit := oddNameSplit[len(oddNameSplit)-1] + var isScorePoints bool + if scoreCheckSplit == "Yes" { + isScorePoints = true + } else if scoreCheckSplit == "No" { + isScorePoints = false + } else { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) + } + + teamName := strings.TrimSpace(strings.Join(oddNameSplit[:len(oddNameSplit)-2], "")) + + threshold, err := strconv.ParseInt(outcome.OddHeader, 10, 64) + if err != nil { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddHeader) + } + + // above code removes any space from team name, so do the same for outcome.HomeTeamName and outcome.AwayTeamName + outcome.HomeTeamName = strings.Join(strings.Split(outcome.HomeTeamName, " "), "") + outcome.AwayTeamName = strings.Join(strings.Split(outcome.AwayTeamName, " "), "") + + switch teamName { + case outcome.HomeTeamName: + if score.Home > score.Away { + if isScorePoints && score.Home >= int(threshold) && score.Away >= int(threshold) { + return domain.OUTCOME_STATUS_WIN, nil + } else if !isScorePoints && score.Home < int(threshold) && score.Away < int(threshold) { + return domain.OUTCOME_STATUS_WIN, nil + } + } + case outcome.AwayTeamName: + if score.Away > score.Home { + if isScorePoints && score.Home >= int(threshold) && score.Away >= int(threshold) { + return domain.OUTCOME_STATUS_WIN, nil + } else if !isScorePoints && score.Home < int(threshold) && score.Away < int(threshold) { + return domain.OUTCOME_STATUS_WIN, nil + } + } + default: + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("team name error: %s", teamName) + } + + return domain.OUTCOME_STATUS_LOSS, nil + +} + +// Both Teams To Score X Points is a type of bet where the bettor predicts whether both teams will score a certain number of points. +func evaluateBTTSX(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + threshold, err := strconv.ParseInt(outcome.OddName, 10, 64) + if err != nil { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName) + } + + switch outcome.OddHeader { + case "Yes": + if score.Home >= int(threshold) && score.Away >= int(threshold) { + return domain.OUTCOME_STATUS_WIN, nil + } + case "No": + if score.Home < int(threshold) && score.Away < int(threshold) { + return domain.OUTCOME_STATUS_WIN, nil + } + + default: + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) + } + + return domain.OUTCOME_STATUS_LOSS, nil +} + +// Money Line 3 Way betting is a type of bet where the bettor predicts the outcome of a match with three possible outcomes: home win, away win, or draw. +func evaluateMoneyLine3Way(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + switch outcome.OddName { + case "1": // Home win + if score.Home > score.Away { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case "Tie": + if score.Home == score.Away { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case "2": // Away win + if score.Away > score.Home { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + default: + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) + } +} + +func evaluateDoubleResult(outcome domain.BetOutcome, firstHalfScore struct{ Home, Away int }, fullTimeScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { + halfWins := strings.Split(outcome.OddName, "-") + if len(halfWins) != 2 { + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName) + } + firstHalfWinner := strings.TrimSpace(halfWins[0]) + fullTimeWinner := strings.TrimSpace(halfWins[1]) + + if firstHalfWinner != outcome.HomeTeamName && firstHalfWinner != outcome.AwayTeamName && firstHalfWinner != "Tie" { + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", firstHalfWinner) + } + if fullTimeWinner != outcome.HomeTeamName && fullTimeWinner != outcome.AwayTeamName && fullTimeWinner != "Tie" { + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", firstHalfWinner) + } + + switch { + case firstHalfWinner == outcome.HomeTeamName && firstHalfScore.Home < firstHalfScore.Away: + return domain.OUTCOME_STATUS_LOSS, nil + case firstHalfWinner == outcome.AwayTeamName && firstHalfScore.Away < firstHalfScore.Home: + return domain.OUTCOME_STATUS_LOSS, nil + case firstHalfWinner == "Tie" && firstHalfScore.Home != firstHalfScore.Away: return domain.OUTCOME_STATUS_LOSS, nil } - // Team With Highest Scoring Quarter betting is a type of bet where the bettor predicts which team will have the highest score in a specific quarter. - func evaluateTeamWithHighestScoringQuarter(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }, thirdScore struct{ Home, Away int }, fourthScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { - homeTeamHighestQuarter := max(firstScore.Home, secondScore.Home, thirdScore.Home, fourthScore.Home) - awayTeamHighestQuarter := max(firstScore.Away, secondScore.Away, thirdScore.Away, fourthScore.Away) - - switch outcome.OddName { - case "1": - if homeTeamHighestQuarter > awayTeamHighestQuarter { - return domain.OUTCOME_STATUS_WIN, nil - } - case "2": - if awayTeamHighestQuarter > homeTeamHighestQuarter { - return domain.OUTCOME_STATUS_WIN, nil - } - case "Tie": - if homeTeamHighestQuarter == awayTeamHighestQuarter { - return domain.OUTCOME_STATUS_WIN, nil - } - default: - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) - } + switch { + case fullTimeWinner == outcome.HomeTeamName && fullTimeScore.Home < fullTimeScore.Away: + return domain.OUTCOME_STATUS_LOSS, nil + case fullTimeWinner == outcome.AwayTeamName && fullTimeScore.Away < fullTimeScore.Home: + return domain.OUTCOME_STATUS_LOSS, nil + case fullTimeWinner == "Tie" && fullTimeScore.Home != fullTimeScore.Away: return domain.OUTCOME_STATUS_LOSS, nil } - // Handicap and Total betting is a combination of spread betting and total points betting - // where the bettor predicts the outcome of a match with a point spread and the total number of points scored is over or under a specified number. - func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + return domain.OUTCOME_STATUS_WIN, nil +} - nameSplit := strings.Split(outcome.OddName, " ") - // Evaluate from bottom to get the threshold and find out if its over or under - threshold, err := strconv.ParseFloat(nameSplit[len(nameSplit)-1], 10) - if err != nil { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing threshold: %s", outcome.OddName) +// Highest Scoring Half betting is a type of bet where the bettor predicts which half of the match will have the highest total score. +func evaluateHighestScoringHalf(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { + firstHalfTotal := firstScore.Home + firstScore.Away + secondHalfTotal := secondScore.Home + secondScore.Away + switch outcome.OddName { + case "1st Half": + if firstHalfTotal > secondHalfTotal { + return domain.OUTCOME_STATUS_WIN, nil } - total := float64(score.Home + score.Away) - overUnder := nameSplit[len(nameSplit)-2] - if overUnder == "Over" { - if total < threshold { - return domain.OUTCOME_STATUS_LOSS, nil - } - } else if overUnder == "Under" { - if total > threshold { - return domain.OUTCOME_STATUS_LOSS, nil - } - } else { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over and under: %s", outcome.OddName) + case "2nd Half": + if firstHalfTotal < secondHalfTotal { + return domain.OUTCOME_STATUS_WIN, nil } - - handicap, err := strconv.ParseFloat(nameSplit[len(nameSplit)-4], 10) - if err != nil { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing handicap: %s", outcome.OddName) + case "Tie": + if firstHalfTotal == secondHalfTotal { + return domain.OUTCOME_STATUS_WIN, nil } - - teamName := strings.TrimSpace(strings.Join(nameSplit[:len(nameSplit)-4], "")) - - adjustedHomeScore := float64(score.Home) - adjustedAwayScore := float64(score.Away) - - switch teamName { - case outcome.HomeTeamName: - adjustedHomeScore += handicap - if adjustedHomeScore > adjustedAwayScore { - return domain.OUTCOME_STATUS_WIN, nil - } - return domain.OUTCOME_STATUS_LOSS, nil - case outcome.AwayTeamName: - adjustedAwayScore += handicap - if adjustedAwayScore > adjustedHomeScore { - return domain.OUTCOME_STATUS_WIN, nil - } - return domain.OUTCOME_STATUS_LOSS, nil - default: - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing team name: %s", outcome.OddName) - } - - } - - // Winning Margin betting is a type of bet where the bettor predicts the margin of victory in a match. - func evaluateWinningMargin(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { - - marginSplit := strings.Split(outcome.OddName, "") - if len(marginSplit) < 1 { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) - } - - margin, err := strconv.ParseInt(marginSplit[0], 10, 64) - if err != nil { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) - } - - isGtr := false - if len(marginSplit) == 2 { - isGtr = marginSplit[1] == "+" - } - switch outcome.OddHeader { - case "1": - if score.Home == (score.Away + int(margin)) { - return domain.OUTCOME_STATUS_WIN, nil - } else if isGtr && score.Home > (score.Away+int(margin)) { - return domain.OUTCOME_STATUS_WIN, nil - } - return domain.OUTCOME_STATUS_LOSS, nil - case "2": - if (score.Home + int(margin)) == score.Away { - return domain.OUTCOME_STATUS_WIN, nil - } else if isGtr && (score.Home+int(margin)) > score.Away { - return domain.OUTCOME_STATUS_WIN, nil - } - return domain.OUTCOME_STATUS_LOSS, nil - } - - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddheader: %s", outcome.OddHeader) - } - - // Highest Scoring Period betting is a type of bet where the bettor predicts which period of the match will have the highest total score. - func evaluateHighestScoringPeriod(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }, thirdScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { - firstPeriodTotal := firstScore.Home + firstScore.Away - secondPeriodTotal := secondScore.Home + secondScore.Away - thirdPeriodTotal := thirdScore.Home + thirdScore.Away - - switch outcome.OddName { - case "Period 1": - if firstPeriodTotal > secondPeriodTotal && firstPeriodTotal > thirdPeriodTotal { - return domain.OUTCOME_STATUS_WIN, nil - } - case "Period 2": - if secondPeriodTotal > firstPeriodTotal && secondPeriodTotal > thirdPeriodTotal { - return domain.OUTCOME_STATUS_WIN, nil - } - case "Period 3": - if thirdPeriodTotal > firstPeriodTotal && thirdPeriodTotal > secondPeriodTotal { - return domain.OUTCOME_STATUS_WIN, nil - } - case "Tie": - if firstPeriodTotal == secondPeriodTotal || secondPeriodTotal == thirdPeriodTotal { - return domain.OUTCOME_STATUS_WIN, nil - } - default: - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) - } - return domain.OUTCOME_STATUS_LOSS, nil - } - - // Tied After Regulation is a type of bet where the bettor predicts whether the match will end in a tie after regulation time. - func evaluateTiedAfterRegulation(outcome domain.BetOutcome, scores []struct{ Home, Away int }) (domain.OutcomeStatus, error) { - totalScore := struct{ Home, Away int }{0, 0} - for _, score := range scores { - totalScore.Home += score.Home - totalScore.Away += score.Away - } - switch outcome.OddName { - case "Yes": - if totalScore.Home == totalScore.Away { - return domain.OUTCOME_STATUS_WIN, nil - } - return domain.OUTCOME_STATUS_LOSS, nil - case "No": - if totalScore.Home != totalScore.Away { - return domain.OUTCOME_STATUS_WIN, nil - } - return domain.OUTCOME_STATUS_LOSS, nil - } - + default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) } + return domain.OUTCOME_STATUS_LOSS, nil +} - // evaluateRugbyOutcome evaluates the outcome of a Rugby bet - func evaluateRugbyOutcome(outcome domain.BetOutcome, result *domain.RugbyResultResponse) (domain.OutcomeStatus, error) { - finalScore := parseSS(result.SS) +// Highest Scoring Quarter betting is a type of bet where the bettor predicts which quarter of the match will have the highest score. +func evaluateHighestScoringQuarter(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }, thirdScore struct{ Home, Away int }, fourthScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { + firstQuarterTotal := firstScore.Home + firstScore.Away + secondQuarterTotal := secondScore.Home + secondScore.Away + thirdQuarterTotal := thirdScore.Home + thirdScore.Away + fourthQuarterTotal := fourthScore.Home + fourthScore.Away - switch outcome.MarketName { - case "Money Line": - return evaluateRugbyMoneyLine(outcome, finalScore) - case "Spread": - return evaluateRugbySpread(outcome, finalScore) - case "Total Points": - return evaluateRugbyTotalPoints(outcome, finalScore) - default: - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported rugby market: %s", outcome.MarketName) + switch outcome.OddName { + case "1st Quarter": + if firstQuarterTotal > secondQuarterTotal && firstQuarterTotal > thirdQuarterTotal && firstQuarterTotal > fourthQuarterTotal { + return domain.OUTCOME_STATUS_WIN, nil } + case "2nd Quarter": + if secondQuarterTotal > firstQuarterTotal && secondQuarterTotal > thirdQuarterTotal && secondQuarterTotal > fourthQuarterTotal { + return domain.OUTCOME_STATUS_WIN, nil + } + case "3rd Quarter": + if thirdQuarterTotal > firstQuarterTotal && thirdQuarterTotal > secondQuarterTotal && thirdQuarterTotal > fourthQuarterTotal { + return domain.OUTCOME_STATUS_WIN, nil + } + case "4th Quarter": + if fourthQuarterTotal > firstQuarterTotal && fourthQuarterTotal > secondQuarterTotal && fourthQuarterTotal > thirdQuarterTotal { + return domain.OUTCOME_STATUS_WIN, nil + } + case "Tie": + if firstQuarterTotal == secondQuarterTotal || secondQuarterTotal == thirdQuarterTotal || thirdQuarterTotal == fourthQuarterTotal { + return domain.OUTCOME_STATUS_WIN, nil + } + default: + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) + } + return domain.OUTCOME_STATUS_LOSS, nil +} + +// Team With Highest Scoring Quarter betting is a type of bet where the bettor predicts which team will have the highest score in a specific quarter. +func evaluateTeamWithHighestScoringQuarter(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }, thirdScore struct{ Home, Away int }, fourthScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { + homeTeamHighestQuarter := max(firstScore.Home, secondScore.Home, thirdScore.Home, fourthScore.Home) + awayTeamHighestQuarter := max(firstScore.Away, secondScore.Away, thirdScore.Away, fourthScore.Away) + + switch outcome.OddName { + case "1": + if homeTeamHighestQuarter > awayTeamHighestQuarter { + return domain.OUTCOME_STATUS_WIN, nil + } + case "2": + if awayTeamHighestQuarter > homeTeamHighestQuarter { + return domain.OUTCOME_STATUS_WIN, nil + } + case "Tie": + if homeTeamHighestQuarter == awayTeamHighestQuarter { + return domain.OUTCOME_STATUS_WIN, nil + } + default: + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) + } + return domain.OUTCOME_STATUS_LOSS, nil +} + +// Handicap and Total betting is a combination of spread betting and total points betting +// where the bettor predicts the outcome of a match with a point spread and the total number of points scored is over or under a specified number. +func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + + nameSplit := strings.Split(outcome.OddName, " ") + // Evaluate from bottom to get the threshold and find out if its over or under + threshold, err := strconv.ParseFloat(nameSplit[len(nameSplit)-1], 10) + if err != nil { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing threshold: %s", outcome.OddName) + } + total := float64(score.Home + score.Away) + overUnder := nameSplit[len(nameSplit)-2] + if overUnder == "Over" { + if total < threshold { + return domain.OUTCOME_STATUS_LOSS, nil + } + } else if overUnder == "Under" { + if total > threshold { + return domain.OUTCOME_STATUS_LOSS, nil + } + } else { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over and under: %s", outcome.OddName) } - // evaluateBaseballOutcome evaluates the outcome of a Baseball bet - func evaluateBaseballOutcome(outcome domain.BetOutcome, result *domain.BaseballResultResponse) (domain.OutcomeStatus, error) { - finalScore := parseSS(result.SS) - - switch outcome.MarketName { - case "Money Line": - return evaluateBaseballMoneyLine(outcome, finalScore) - case "Spread": - return evaluateBaseballSpread(outcome, finalScore) - case "Total Runs": - return evaluateBaseballTotalRuns(outcome, finalScore) - default: - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported baseball market: %s", outcome.MarketName) - } + handicap, err := strconv.ParseFloat(nameSplit[len(nameSplit)-4], 10) + if err != nil { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing handicap: %s", outcome.OddName) } + + teamName := strings.TrimSpace(strings.Join(nameSplit[:len(nameSplit)-4], "")) + adjustedHomeScore := float64(score.Home) + adjustedAwayScore := float64(score.Away) + + outcome.HomeTeamName = strings.Join(strings.Split(outcome.HomeTeamName, " "), "") + outcome.AwayTeamName = strings.Join(strings.Split(outcome.AwayTeamName, " "), "") + + switch teamName { + case outcome.HomeTeamName: + adjustedHomeScore += handicap + if adjustedHomeScore > adjustedAwayScore { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case outcome.AwayTeamName: + adjustedAwayScore += handicap + if adjustedAwayScore > adjustedHomeScore { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + default: + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing team name: %s", outcome.OddName) + } + +} + +func evaluateWinningMargin(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + if len(outcome.OddName) < 1 { + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) + } + + isGtr := false + idx := len(outcome.OddName) + if outcome.OddName[idx-1] == '+' { + isGtr = true + idx-- + } + + margin, err := strconv.ParseInt(outcome.OddName[:idx], 10, 64) + if err != nil { + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) + } + + switch outcome.OddHeader { + case "1": + if score.Home == (score.Away + int(margin)) { + return domain.OUTCOME_STATUS_WIN, nil + } else if isGtr && score.Home > (score.Away+int(margin)) { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case "2": + if score.Away == (score.Home + int(margin)) { + return domain.OUTCOME_STATUS_WIN, nil + } else if isGtr && score.Away > (score.Home+int(margin)) { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + } + + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddheader: %s", outcome.OddHeader) +} + +// Highest Scoring Period betting is a type of bet where the bettor predicts which period of the match will have the highest total score. +func evaluateHighestScoringPeriod(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }, thirdScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { + firstPeriodTotal := firstScore.Home + firstScore.Away + secondPeriodTotal := secondScore.Home + secondScore.Away + thirdPeriodTotal := thirdScore.Home + thirdScore.Away + + switch outcome.OddName { + case "Period 1": + if firstPeriodTotal > secondPeriodTotal && firstPeriodTotal > thirdPeriodTotal { + return domain.OUTCOME_STATUS_WIN, nil + } + case "Period 2": + if secondPeriodTotal > firstPeriodTotal && secondPeriodTotal > thirdPeriodTotal { + return domain.OUTCOME_STATUS_WIN, nil + } + case "Period 3": + if thirdPeriodTotal > firstPeriodTotal && thirdPeriodTotal > secondPeriodTotal { + return domain.OUTCOME_STATUS_WIN, nil + } + case "Tie": + if firstPeriodTotal == secondPeriodTotal || secondPeriodTotal == thirdPeriodTotal { + return domain.OUTCOME_STATUS_WIN, nil + } + default: + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) + } + return domain.OUTCOME_STATUS_LOSS, nil +} + +// Tied After Regulation is a type of bet where the bettor predicts whether the match will end in a tie after regulation time. +func evaluateTiedAfterRegulation(outcome domain.BetOutcome, scores []struct{ Home, Away int }) (domain.OutcomeStatus, error) { + totalScore := struct{ Home, Away int }{0, 0} + for _, score := range scores { + totalScore.Home += score.Home + totalScore.Away += score.Away + } + switch outcome.OddName { + case "Yes": + if totalScore.Home == totalScore.Away { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case "No": + if totalScore.Home != totalScore.Away { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + } + + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) +} + +func evaluateRugbyOutcome(outcome domain.BetOutcome, result *domain.RugbyResultResponse) (domain.OutcomeStatus, error) { + finalScore := parseSS(result.SS) + + switch outcome.MarketName { + case "Money Line": + return evaluateRugbyMoneyLine(outcome, finalScore) + case "Spread": + return evaluateRugbySpread(outcome, finalScore) + case "Total Points": + return evaluateRugbyTotalPoints(outcome, finalScore) + default: + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported rugby market: %s", outcome.MarketName) + } +} + +func evaluateBaseballOutcome(outcome domain.BetOutcome, result *domain.BaseballResultResponse) (domain.OutcomeStatus, error) { + finalScore := parseSS(result.SS) + + switch outcome.MarketName { + case "Money Line": + return evaluateBaseballMoneyLine(outcome, finalScore) + case "Spread": + return evaluateBaseballSpread(outcome, finalScore) + case "Total Runs": + return evaluateBaseballTotalRuns(outcome, finalScore) + default: + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported baseball market: %s", outcome.MarketName) + } +} + +func evaluateVolleyballGamelines(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + switch outcome.OddName { + case "Total": + return evaluateTotalOverUnder(outcome, score) + default: + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName) + } +} diff --git a/internal/services/result/service.go b/internal/services/result/service.go index 4c3fa81..bc73535 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -16,7 +16,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" ) -type Service struct { +type Service struct { repo *repository.Store config *config.Config logger *slog.Logger @@ -227,7 +227,9 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { // } func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID, sportID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { - url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%d", s.config.Bet365Token, eventID) + // url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%d", s.config.Bet365Token, eventID) + url := fmt.Sprintf("https://api.b365api.com/v1/event/view?token=%s&event_id=%d", s.config.Bet365Token, eventID) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { s.logger.Error("Failed to create request", "event_id", eventID, "error", err) @@ -261,18 +263,70 @@ func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID, spo switch sportID { case domain.FOOTBALL: result, err = s.parseFootball(resultResp.Results[0], eventID, oddID, marketID, outcome) + if err != nil { + s.logger.Error("Failed to parse football", "event id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } case domain.BASKETBALL: result, err = s.parseBasketball(resultResp.Results[0], eventID, oddID, marketID, outcome) + if err != nil { + s.logger.Error("Failed to parse basketball", "event id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } case domain.ICE_HOCKEY: result, err = s.parseIceHockey(resultResp.Results[0], eventID, oddID, marketID, outcome) + if err != nil { + s.logger.Error("Failed to parse ice hockey", "event id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } + case domain.CRICKET: + result, err = s.parseCricket(resultResp.Results[0], eventID, oddID, marketID, outcome) + if err != nil { + s.logger.Error("Failed to parse cricket", "event id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } + case domain.VOLLEYBALL: + result, err = s.parseVolleyball(resultResp.Results[0], eventID, oddID, marketID, outcome) + if err != nil { + s.logger.Error("Failed to parse volleyball", "event id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } + case domain.DARTS: + result, err = s.parseDarts(resultResp.Results[0], eventID, oddID, marketID, outcome) + if err != nil { + s.logger.Error("Failed to parse darts", "event id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } + case domain.FUTSAL: + result, err = s.parseFutsal(resultResp.Results[0], eventID, oddID, marketID, outcome) + if err != nil { + s.logger.Error("Failed to parse futsal", "event id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } case domain.AMERICAN_FOOTBALL: result, err = s.parseNFL(resultResp.Results[0], eventID, oddID, marketID, outcome) + if err != nil { + s.logger.Error("Failed to parse american football", "event id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } case domain.RUGBY_UNION: result, err = s.parseRugbyUnion(resultResp.Results[0], eventID, oddID, marketID, outcome) + if err != nil { + s.logger.Error("Failed to parse rugby_union", "event id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } case domain.RUGBY_LEAGUE: result, err = s.parseRugbyLeague(resultResp.Results[0], eventID, oddID, marketID, outcome) + if err != nil { + s.logger.Error("Failed to parse rugby_league", "event id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } case domain.BASEBALL: result, err = s.parseBaseball(resultResp.Results[0], eventID, oddID, marketID, outcome) + if err != nil { + s.logger.Error("Failed to parse baseball", "event id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } default: s.logger.Error("Unsupported sport", "sport", sportID) return domain.CreateResult{}, fmt.Errorf("unsupported sport: %v", sportID) @@ -352,6 +406,7 @@ func (s *Service) parseFootball(resultRes json.RawMessage, eventID, oddID, marke func (s *Service) parseBasketball(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { var basketBallRes domain.BasketballResultResponse + if err := json.Unmarshal(response, &basketBallRes); err != nil { s.logger.Error("Failed to unmarshal basketball result", "event_id", eventID, "error", err) return domain.CreateResult{}, err @@ -416,6 +471,121 @@ func (s *Service) parseIceHockey(response json.RawMessage, eventID, oddID, marke } +func (s *Service) parseCricket(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { + var cricketRes domain.CricketResultResponse + + if err := json.Unmarshal(response, &cricketRes); err != nil { + s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) + return domain.CreateResult{}, err + } + if cricketRes.TimeStatus != "3" { + s.logger.Warn("Match not yet completed", "event_id", eventID) + return domain.CreateResult{}, fmt.Errorf("match not yet completed") + } + + status, err := s.evaluateCricketOutcome(outcome, cricketRes) + if err != nil { + s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } + + return domain.CreateResult{ + BetOutcomeID: 0, + EventID: eventID, + OddID: oddID, + MarketID: marketID, + Status: status, + }, nil +} + +func (s *Service) parseVolleyball(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { + var volleyballRes domain.VolleyballResultResponse + + if err := json.Unmarshal(response, &volleyballRes); err != nil { + s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) + return domain.CreateResult{}, err + } + if volleyballRes.TimeStatus != "3" { + s.logger.Warn("Match not yet completed", "event_id", eventID) + return domain.CreateResult{}, fmt.Errorf("match not yet completed") + } + + status, err := s.evaluateVolleyballOutcome(outcome, volleyballRes) + if err != nil { + s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } + + return domain.CreateResult{ + BetOutcomeID: 0, + EventID: eventID, + OddID: oddID, + MarketID: marketID, + Status: status, + }, nil + +} + +func (s *Service) parseDarts(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { + var dartsRes domain.DartsResultResponse + + if err := json.Unmarshal(response, &dartsRes); err != nil { + s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) + return domain.CreateResult{}, err + } + + if dartsRes.TimeStatus != "3" { + s.logger.Warn("Match not yet completed", "event_id", eventID) + return domain.CreateResult{}, fmt.Errorf("match not yet completed") + } + + // result for dart is limited + // only ss is given, format with 2-4 + status, err := s.evaluateDartsOutcome(outcome, dartsRes) + if err != nil { + s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } + + return domain.CreateResult{ + BetOutcomeID: 0, + EventID: eventID, + OddID: oddID, + MarketID: marketID, + Status: status, + }, nil + +} + +func (s *Service) parseFutsal(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { + var futsalRes domain.FutsalResultResponse + + if err := json.Unmarshal(response, &futsalRes); err != nil { + s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) + return domain.CreateResult{}, err + } + + if futsalRes.TimeStatus != "3" { + s.logger.Warn("Match not yet completed", "event_id", eventID) + return domain.CreateResult{}, fmt.Errorf("match not yet completed") + } + + status, err := s.evaluateFutsalOutcome(outcome, futsalRes) + if err != nil { + s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } + + return domain.CreateResult{ + BetOutcomeID: 0, + EventID: eventID, + OddID: oddID, + MarketID: marketID, + Status: status, + }, nil + +} + func (s *Service) parseNFL(resultRes json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { var nflResp domain.NFLResultResponse if err := json.Unmarshal(resultRes, &nflResp); err != nil { @@ -730,6 +900,92 @@ func (s *Service) evaluateIceHockeyOutcome(outcome domain.BetOutcome, res domain return domain.OUTCOME_STATUS_PENDING, nil } +func (s *Service) evaluateCricketOutcome(outcome domain.BetOutcome, res domain.CricketResultResponse) (domain.OutcomeStatus, error) { + if !domain.SupportedMarkets[outcome.MarketID] { + s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) + } + + score := parseSS(res.SS) + + switch outcome.MarketID { + case int64(domain.CRICKET_TO_WIN_THE_MATCH): + return evaluateFullTimeResult(outcome, score) + } + + return domain.OUTCOME_STATUS_PENDING, nil +} + +func (s *Service) evaluateVolleyballOutcome(outcome domain.BetOutcome, res domain.VolleyballResultResponse) (domain.OutcomeStatus, error) { + if !domain.SupportedMarkets[outcome.MarketID] { + s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) + } + + score := parseSS(res.SS) + + // res.SS example: { 2-3 } is the win count not actuall score of sets + // for total score we need every set's score + firstSet := parseScore(res.Scores.FirstSet.Home, res.Scores.FirstSet.Away) + secondSet := parseScore(res.Scores.SecondSet.Home, res.Scores.SecondSet.Away) + thirdSet := parseScore(res.Scores.ThirdSet.Home, res.Scores.ThirdSet.Away) + fourthSet := parseScore(res.Scores.FourthSet.Home, res.Scores.FourthSet.Away) + fivethSet := parseScore(res.Scores.FivethSet.Home, res.Scores.FivethSet.Away) + + totalScore := struct{ Home, Away int }{Home: 0, Away: 0} + totalScore.Home = firstSet.Home + secondSet.Home + thirdSet.Home + fourthSet.Home + fivethSet.Home + totalScore.Away = firstSet.Away + secondSet.Away + thirdSet.Away + fourthSet.Away + fivethSet.Away + + switch outcome.MarketID { + case int64(domain.VOLLEYBALL_GAME_LINES): + return evaluateVolleyballGamelines(outcome, totalScore) + case int64(domain.VOLLEYBALL_MATCH_TOTAL_ODD_EVEN): + return evaluateGoalsOddEven(outcome, totalScore) + case int64(domain.VOLLEYBALL_CORRECT_SET_SCORE): + return evaluateCorrectScore(outcome, score) + } + + return domain.OUTCOME_STATUS_PENDING, nil +} + +func (s *Service) evaluateDartsOutcome(outcome domain.BetOutcome, res domain.DartsResultResponse) (domain.OutcomeStatus, error) { + if !domain.SupportedMarkets[outcome.MarketID] { + s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) + } + + score := parseSS(res.SS) + + switch outcome.MarketID { + case int64(domain.DARTS_MATCH_WINNER): + return evaluateFullTimeResult(outcome, score) + case int64(domain.DARTS_TOTAL_LEGS): + return evaluateTotalLegs(outcome, score) + } + + return domain.OUTCOME_STATUS_PENDING, nil +} + +func (s *Service) evaluateFutsalOutcome(outcome domain.BetOutcome, res domain.FutsalResultResponse) (domain.OutcomeStatus, error) { + if !domain.SupportedMarkets[outcome.MarketID] { + s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) + } + + score := parseSS(res.SS) + + switch outcome.MarketID { + case int64(domain.FUTSAL_GAME_LINES): + return evaluateGameLines(outcome, score) + case int64(domain.FUTSAL_MONEY_LINE): + return evaluateMoneyLine(outcome, score) + case int64(domain.FUTSAL_TEAM_TO_SCORE_FIRST): + return evaluateFirstTeamToScore(outcome, res.Events) + } + + return domain.OUTCOME_STATUS_PENDING, nil +} + func (s *Service) evaluateNFLOutcome(outcome domain.BetOutcome, finalScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { if !domain.SupportedMarkets[outcome.MarketID] { s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) diff --git a/internal/services/result/service_test.go b/internal/services/result/service_test.go index c92f2a4..2705049 100644 --- a/internal/services/result/service_test.go +++ b/internal/services/result/service_test.go @@ -1,49 +1 @@ package result - -import ( - "fmt" - "testing" - "time" - - "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" -) - -func TestEvaluateFootballOutcome(t *testing.T) { - service := &Service{} // or your real logger - - // Mock outcome - outcome := domain.BetOutcome{ - ID: 1, - BetID: 1, - EventID: 1001, - OddID: 2001, - SportID: 1, // Assuming 1 = Football - HomeTeamName: "Manchester", - AwayTeamName: "Liverpool", - MarketID: int64(domain.FOOTBALL_FULL_TIME_RESULT), - MarketName: "Full Time Result", - Odd: 1.75, - OddName: "2", // Home win - OddHeader: "1", - OddHandicap: "", - Status: domain.OUTCOME_STATUS_PENDING, // Initial status - Expires: time.Now().Add(24 * time.Hour), - } - - // Parsed result (simulate Bet365 JSON) - finalScore := struct{ Home, Away int }{Home: 2, Away: 1} - firstHalfScore := struct{ Home, Away int }{Home: 1, Away: 1} - secondHalfScore := struct{ Home, Away int }{Home: 1, Away: 0} - corners := struct{ Home, Away int }{Home: 5, Away: 3} - halfTimeCorners := struct{ Home, Away int }{Home: 2, Away: 2} - events := []map[string]string{ - {"type": "goal", "team": "home", "minute": "23"}, - {"type": "goal", "team": "away", "minute": "34"}, - } - - // Act - status, _ := service.evaluateFootballOutcome(outcome, finalScore, firstHalfScore, secondHalfScore, corners, halfTimeCorners, events) - - fmt.Printf("\n\nBet Outcome: %v\n\n", &status) - -} diff --git a/internal/services/result/sports_eval_test.go b/internal/services/result/sports_eval_test.go index f300879..4810385 100644 --- a/internal/services/result/sports_eval_test.go +++ b/internal/services/result/sports_eval_test.go @@ -1,7 +1,9 @@ package result import ( + "fmt" "testing" + "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/stretchr/testify/assert" @@ -301,3 +303,678 @@ func TestBaseballMarkets(t *testing.T) { }) } } + +func TestEvaluateFootballOutcome(t *testing.T) { + service := &Service{} // or your real logger + + // Mock outcome + outcome := domain.BetOutcome{ + ID: 1, + BetID: 1, + EventID: 1001, + OddID: 2001, + SportID: 1, // Assuming 1 = Football + HomeTeamName: "Manchester", + AwayTeamName: "Liverpool", + MarketID: int64(domain.FOOTBALL_FULL_TIME_RESULT), + MarketName: "Full Time Result", + Odd: 1.75, + OddName: "2", // Home win + OddHeader: "1", + OddHandicap: "", + Status: domain.OUTCOME_STATUS_PENDING, // Initial status + Expires: time.Now().Add(24 * time.Hour), + } + + // Parsed result (simulate Bet365 JSON) + finalScore := struct{ Home, Away int }{Home: 2, Away: 1} + firstHalfScore := struct{ Home, Away int }{Home: 1, Away: 1} + secondHalfScore := struct{ Home, Away int }{Home: 1, Away: 0} + corners := struct{ Home, Away int }{Home: 5, Away: 3} + halfTimeCorners := struct{ Home, Away int }{Home: 2, Away: 2} + events := []map[string]string{ + {"type": "goal", "team": "home", "minute": "23"}, + {"type": "goal", "team": "away", "minute": "34"}, + } + + // Act + status, _ := service.evaluateFootballOutcome(outcome, finalScore, firstHalfScore, secondHalfScore, corners, halfTimeCorners, events) + + fmt.Printf("\n\nBet Outcome: %v\n\n", &status) + +} +func TestEvaluateTotalLegs(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"OverTotalLegs", domain.BetOutcome{OddName: "3", OddHeader: "Over"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_WIN}, + {"OverTotalLegs", domain.BetOutcome{OddName: "3", OddHeader: "Under"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_LOSS}, + {"UnderTotalLegs", domain.BetOutcome{OddName: "7", OddHeader: "Under"}, struct{ Home, Away int }{2, 3}, domain.OUTCOME_STATUS_WIN}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateTotalLegs(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateGameLines(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"GameLines - Total", domain.BetOutcome{OddName: "Total", OddHandicap: "O 5.5"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_WIN}, + {"GameLines - Total", domain.BetOutcome{OddName: "Total", OddHandicap: "O 5.5"}, struct{ Home, Away int }{2, 3}, domain.OUTCOME_STATUS_LOSS}, + {"GameLines - Money Line", domain.BetOutcome{OddName: "Money Line", OddHeader: "1"}, struct{ Home, Away int }{2, 3}, domain.OUTCOME_STATUS_LOSS}, + {"GameLines - Money Line", domain.BetOutcome{OddName: "Money Line", OddHeader: "1"}, struct{ Home, Away int }{3, 2}, domain.OUTCOME_STATUS_WIN}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateGameLines(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateFirstTeamToScore(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + events []map[string]string + expected domain.OutcomeStatus + }{ + {"HomeScoreFirst", domain.BetOutcome{OddName: "1", HomeTeamName: "Team A", AwayTeamName: "Team B"}, []map[string]string{ + {"text": "1st Goal - Team A"}, + }, domain.OUTCOME_STATUS_WIN}, + {"AwayScoreFirst", domain.BetOutcome{OddName: "2", HomeTeamName: "Team A", AwayTeamName: "Team B"}, []map[string]string{ + {"text": "1st Goal - Team A"}, + }, domain.OUTCOME_STATUS_LOSS}, + {"AwayScoreFirst", domain.BetOutcome{OddName: "2", HomeTeamName: "Team A", AwayTeamName: "Team B"}, []map[string]string{ + {"text": "1st Goal - Team B"}, + }, domain.OUTCOME_STATUS_WIN}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateFirstTeamToScore(tt.outcome, tt.events) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateGoalsOverUnder(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"LosingGoalsOver", domain.BetOutcome{OddHeader: "Over", OddName: "13"}, struct{ Home, Away int }{7, 5}, domain.OUTCOME_STATUS_LOSS}, + {"WinningGoalsOver", domain.BetOutcome{OddHeader: "Over", OddName: "11"}, struct{ Home, Away int }{7, 5}, domain.OUTCOME_STATUS_WIN}, + {"WinningGoalsUnder", domain.BetOutcome{OddHeader: "Under", OddName: "12"}, struct{ Home, Away int }{6, 5}, domain.OUTCOME_STATUS_WIN}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateGoalsOverUnder(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateGoalsOddEven(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningOddGoals", domain.BetOutcome{OddName: "Odd"}, struct{ Home, Away int }{7, 4}, domain.OUTCOME_STATUS_WIN}, + {"LosingEvenGoals", domain.BetOutcome{OddName: "Even"}, struct{ Home, Away int }{7, 4}, domain.OUTCOME_STATUS_LOSS}, + {"WinningEvenGoals", domain.BetOutcome{OddName: "Even"}, struct{ Home, Away int }{6, 6}, domain.OUTCOME_STATUS_WIN}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateGoalsOddEven(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateCorrectScore(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"CorrectScore", domain.BetOutcome{OddName: "7-4"}, struct{ Home, Away int }{7, 4}, domain.OUTCOME_STATUS_WIN}, + {"CorrectScore", domain.BetOutcome{OddName: "6-6"}, struct{ Home, Away int }{6, 6}, domain.OUTCOME_STATUS_WIN}, + {"IncorrectScore", domain.BetOutcome{OddName: "2-3"}, struct{ Home, Away int }{7, 4}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateCorrectScore(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateHighestScoringHalf(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + firstScore struct{ Home, Away int } + secondScore struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"Winning1stHalf", domain.BetOutcome{OddName: "1st Half"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_WIN}, + {"Losing1stHalf", domain.BetOutcome{OddName: "1st Half"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_LOSS}, + {"Losing2ndHalf", domain.BetOutcome{OddName: "2nd Half"}, struct{ Home, Away int }{0, 0}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_LOSS}, + {"Winning2ndHalf", domain.BetOutcome{OddName: "2nd Half"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_WIN}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateHighestScoringHalf(tt.outcome, tt.firstScore, tt.secondScore) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } + +} + +func TestEvaluateHighestScoringQuarter(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + firstScore struct{ Home, Away int } + secondScore struct{ Home, Away int } + thirdScore struct{ Home, Away int } + fourthScore struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"Winning1stQuarter", domain.BetOutcome{OddName: "1st Quarter"}, + struct{ Home, Away int }{1, 1}, + struct{ Home, Away int }{0, 0}, + struct{ Home, Away int }{1, 0}, + struct{ Home, Away int }{0, 0}, + domain.OUTCOME_STATUS_WIN}, + {"Losing1stQuarter", domain.BetOutcome{OddName: "1st Quarter"}, + struct{ Home, Away int }{1, 1}, + struct{ Home, Away int }{0, 0}, + struct{ Home, Away int }{1, 1}, + struct{ Home, Away int }{0, 0}, + domain.OUTCOME_STATUS_LOSS}, + {"Losing2ndQuarter", domain.BetOutcome{OddName: "2nd Quarter"}, + struct{ Home, Away int }{1, 1}, + struct{ Home, Away int }{0, 0}, + struct{ Home, Away int }{1, 1}, + struct{ Home, Away int }{0, 0}, + domain.OUTCOME_STATUS_LOSS}, + {"Winning3rdQuarter", domain.BetOutcome{OddName: "3rd Quarter"}, + struct{ Home, Away int }{1, 0}, + struct{ Home, Away int }{0, 0}, + struct{ Home, Away int }{1, 1}, + struct{ Home, Away int }{0, 0}, + domain.OUTCOME_STATUS_WIN}, + {"Wining4thQuarter", domain.BetOutcome{OddName: "4th Quarter"}, + struct{ Home, Away int }{1, 1}, + struct{ Home, Away int }{0, 0}, + struct{ Home, Away int }{1, 1}, + struct{ Home, Away int }{2, 2}, + domain.OUTCOME_STATUS_WIN}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateHighestScoringQuarter(tt.outcome, tt.firstScore, tt.secondScore, tt.thirdScore, tt.fourthScore) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } + +} + +func TestEvaluateWinningMargin(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningMargin", domain.BetOutcome{OddHeader: "1", OddName: "12"}, struct{ Home, Away int }{12, 0}, domain.OUTCOME_STATUS_WIN}, + {"WinningMargin", domain.BetOutcome{OddHeader: "2", OddName: "3"}, struct{ Home, Away int }{1, 4}, domain.OUTCOME_STATUS_WIN}, + {"WinningMargin", domain.BetOutcome{OddHeader: "1", OddName: "3+"}, struct{ Home, Away int }{4, 0}, domain.OUTCOME_STATUS_WIN}, + {"WinningMargin", domain.BetOutcome{OddHeader: "2", OddName: "12+"}, struct{ Home, Away int }{0, 13}, domain.OUTCOME_STATUS_WIN}, + {"LosingMargin", domain.BetOutcome{OddHeader: "2", OddName: "3"}, struct{ Home, Away int }{0, 4}, domain.OUTCOME_STATUS_LOSS}, + {"LosingMargin", domain.BetOutcome{OddHeader: "2", OddName: "3+"}, struct{ Home, Away int }{1, 3}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateWinningMargin(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateDoubleResult(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + firstHalfScore struct{ Home, Away int } + fullTimeScore struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningHomeAway", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A - Team B"}, struct{ Home, Away int }{1, 0}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, + {"WinningAwayHome", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B - Team A"}, struct{ Home, Away int }{0, 1}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_WIN}, + {"WinningTie", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Tie - Tie"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_WIN}, + {"WinningTieAway", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Tie - Team B"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, + {"LosingHomeAway", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A - Team B"}, struct{ Home, Away int }{1, 0}, struct{ Home, Away int }{2, 0}, domain.OUTCOME_STATUS_LOSS}, + {"LosingTie", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Tie - Tie"}, struct{ Home, Away int }{1, 0}, struct{ Home, Away int }{1, 1}, domain.OUTCOME_STATUS_LOSS}, + {"LosingTieAway", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Tie - Team A"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, + {"BadInput", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A - "}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_PENDING}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateDoubleResult(tt.outcome, tt.firstHalfScore, tt.fullTimeScore) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateHighestScoringPeriod(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + firstScore struct{ Home, Away int } + secondScore struct{ Home, Away int } + thirdScore struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"Winning1stPeriod", domain.BetOutcome{OddName: "Period 1"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_WIN}, + {"Winning2ndPeriod", domain.BetOutcome{OddName: "Period 2"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{2, 3}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_WIN}, + {"Winning3rdPeriod", domain.BetOutcome{OddName: "Period 3"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{2, 3}, struct{ Home, Away int }{3, 3}, domain.OUTCOME_STATUS_WIN}, + {"WinningTie", domain.BetOutcome{OddName: "Tie"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_WIN}, + {"Losing1stPeriod", domain.BetOutcome{OddName: "Period 1"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{2, 3}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_LOSS}, + {"Losing3rdPeriod", domain.BetOutcome{OddName: "Period 3"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{2, 3}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateHighestScoringPeriod(tt.outcome, tt.firstScore, tt.secondScore, tt.thirdScore) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvalauteTiedAfterRegulation(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score []struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningTied", domain.BetOutcome{OddName: "Yes"}, []struct{ Home, Away int }{{1, 0}, {0, 1}, {2, 2}}, domain.OUTCOME_STATUS_WIN}, + {"WinningTied", domain.BetOutcome{OddName: "Yes"}, []struct{ Home, Away int }{{1, 1}, {0, 1}, {2, 2}, {2, 1}}, domain.OUTCOME_STATUS_WIN}, + {"WinningNotTied", domain.BetOutcome{OddName: "No"}, []struct{ Home, Away int }{{0, 0}, {0, 0}, {0, 0}, {1, 0}}, domain.OUTCOME_STATUS_WIN}, + {"LosingTied", domain.BetOutcome{OddName: "Yes"}, []struct{ Home, Away int }{{0, 2}, {0, 0}, {0, 0}, {0, 0}}, domain.OUTCOME_STATUS_LOSS}, + {"LosingNotTied", domain.BetOutcome{OddName: "No"}, []struct{ Home, Away int }{{0, 0}, {0, 0}, {0, 0}, {0, 0}}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateTiedAfterRegulation(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateTeamTotal(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningHomeUnder", domain.BetOutcome{OddHandicap: "Under 3", OddHeader: "1"}, struct{ Home, Away int }{2, 0}, domain.OUTCOME_STATUS_WIN}, + {"WinningHomeOver", domain.BetOutcome{OddHandicap: "Over 2", OddHeader: "1"}, struct{ Home, Away int }{3, 1}, domain.OUTCOME_STATUS_WIN}, + {"WinningAwayOver", domain.BetOutcome{OddHandicap: "Over 2", OddHeader: "2"}, struct{ Home, Away int }{1, 3}, domain.OUTCOME_STATUS_WIN}, + {"LosingHomeOver", domain.BetOutcome{OddHandicap: "Over 2", OddHeader: "1"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, + {"LosingAwayOver", domain.BetOutcome{OddHandicap: "Over 2", OddHeader: "2"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateTeamTotal(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestDrawNoBet(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningHome", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 0}, domain.OUTCOME_STATUS_WIN}, + {"WinningAway", domain.BetOutcome{OddName: "2"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, + {"LosingHome", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, + {"Tie", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 1}, domain.OUTCOME_STATUS_VOID}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateDrawNoBet(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateMoneyLine(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningHome", domain.BetOutcome{OddHeader: "1"}, struct{ Home, Away int }{1, 0}, domain.OUTCOME_STATUS_WIN}, + {"WinningAway", domain.BetOutcome{OddHeader: "2"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, + {"WinningTie", domain.BetOutcome{OddHeader: "Tie"}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_WIN}, + {"LosingTie", domain.BetOutcome{OddHeader: "1"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, + {"LosingAway", domain.BetOutcome{OddHeader: "2"}, struct{ Home, Away int }{3, 2}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateMoneyLine(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateDoubleChance(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningHomeOrDraw", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "1 or Draw"}, struct{ Home, Away int }{1, 0}, domain.OUTCOME_STATUS_WIN}, + {"WinningHomeOrDraw", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A or Draw"}, struct{ Home, Away int }{1, 0}, domain.OUTCOME_STATUS_WIN}, + {"WinningAwayOrDraw", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Draw or Team B"}, struct{ Home, Away int }{0, 1}, domain.OUTCOME_STATUS_WIN}, + {"LosingHomeorAway", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "1 or 2"}, struct{ Home, Away int }{1, 1}, domain.OUTCOME_STATUS_LOSS}, + {"LosingAwayOrDraw", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Draw or 2"}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateDoubleChance(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateResultAndTotal(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningHomeOver", domain.BetOutcome{OddHeader: "1", OddHandicap: "Over 4"}, struct{ Home, Away int }{3, 2}, domain.OUTCOME_STATUS_WIN}, + {"WinningHomeUnder", domain.BetOutcome{OddHeader: "1", OddHandicap: "Under 4"}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_WIN}, + {"WinningAwayUnder", domain.BetOutcome{OddHeader: "2", OddHandicap: "Under 4"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, + {"LosingHomeOver", domain.BetOutcome{OddHeader: "1", OddHandicap: "Under 4"}, struct{ Home, Away int }{3, 2}, domain.OUTCOME_STATUS_LOSS}, + {"LosingAwayUnder", domain.BetOutcome{OddHeader: "2", OddHandicap: "Under 4"}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateResultAndTotal(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestCheckMultiOutcome(t *testing.T) { + tests := []struct { + name string + outcome domain.OutcomeStatus + secondOutcome domain.OutcomeStatus + expected domain.OutcomeStatus + }{ + {"Win-Win", domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_WIN}, + {"Win-Void", domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_HALF}, + {"Win-Loss", domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_LOSS}, + {"Loss-Loss", domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_LOSS}, + {"Loss-Void", domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_VOID}, + {"Loss-Win", domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_LOSS}, + {"Void-Win", domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_VOID}, + {"Void-Loss", domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_VOID}, + {"Void-Void", domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_VOID}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := checkMultiOutcome(tt.outcome, tt.secondOutcome) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateBTTSX(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningBothScoreX", domain.BetOutcome{OddName: "3", OddHeader: "Yes"}, struct{ Home, Away int }{3, 4}, domain.OUTCOME_STATUS_WIN}, + {"WinningBothScoreLess", domain.BetOutcome{OddName: "3", OddHeader: "No"}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_WIN}, + {"LosingBothScoreX", domain.BetOutcome{OddName: "3", OddHeader: "Yes"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_LOSS}, + {"LosingBothScoreLess", domain.BetOutcome{OddName: "3", OddHeader: "No"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateBTTSX(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateResultAndBTTSX(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningHomeAndBothScoreX", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A and Yes", OddHeader: "3"}, struct{ Home, Away int }{4, 3}, domain.OUTCOME_STATUS_WIN}, + {"WinningHomeAndBothScoreLess", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A and No", OddHeader: "3"}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_WIN}, + {"WinningAwayAndBothScoreX", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B and Yes", OddHeader: "3"}, struct{ Home, Away int }{3, 4}, domain.OUTCOME_STATUS_WIN}, + {"WinningAwayAndBothScoreLess", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B and No", OddHeader: "3"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, + {"LosingHomeAndBothScoreX", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A and Yes", OddHeader: "3"}, struct{ Home, Away int }{3, 4}, domain.OUTCOME_STATUS_LOSS}, + {"LosingHomeAndBothScoreX", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B and Yes", OddHeader: "3"}, struct{ Home, Away int }{4, 2}, domain.OUTCOME_STATUS_LOSS}, + {"LosingAwayAndBothScoreX", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B and Yes", OddHeader: "3"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateResultAndBTTSX(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateMoneyLine3Way(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningHome", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 0}, domain.OUTCOME_STATUS_WIN}, + {"WinningAway", domain.BetOutcome{OddName: "2"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, + {"WinningTie", domain.BetOutcome{OddName: "Tie"}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_WIN}, + {"LosingTie", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, + {"LosingAway", domain.BetOutcome{OddName: "2"}, struct{ Home, Away int }{3, 2}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateMoneyLine3Way(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateAsianHandicap(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + { + name: "Home -1 Win", + outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "-1"}, + score: struct{ Home, Away int }{Home: 2, Away: 0}, + expected: domain.OUTCOME_STATUS_WIN, + }, + { + name: "Home -0.5 Win", + outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "-0.5"}, + score: struct{ Home, Away int }{Home: 1, Away: 0}, + expected: domain.OUTCOME_STATUS_WIN, + }, + { + name: "Home -1 Void", + outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "-1"}, + score: struct{ Home, Away int }{Home: 1, Away: 0}, + expected: domain.OUTCOME_STATUS_VOID, + }, + { + name: "Away +3 Win", + outcome: domain.BetOutcome{OddHeader: "2", OddHandicap: "3"}, + score: struct{ Home, Away int }{Home: 1, Away: 2}, + expected: domain.OUTCOME_STATUS_WIN, + }, + { + name: "Split Handicap Home -0.5,-1 Win/Win", + outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "-0.5,-1"}, + score: struct{ Home, Away int }{Home: 2, Away: 0}, + expected: domain.OUTCOME_STATUS_WIN, + }, + { + name: "Split Handicap Home -0.5,-1 Win/Void", + outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "-0.5,-1"}, + score: struct{ Home, Away int }{Home: 1, Away: 0}, + expected: domain.OUTCOME_STATUS_WIN, + }, + { + name: "Invalid Handicap", + outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "invalid"}, + score: struct{ Home, Away int }{Home: 1, Away: 0}, + expected: domain.OUTCOME_STATUS_ERROR, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateAsianHandicap(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateHandicapAndTotal(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"Home +2.5 Over 3", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A +2.5 & Over 3"}, + struct{ Home, Away int }{4, 0}, + domain.OUTCOME_STATUS_WIN}, + {"Away +2.5 Over 4", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B +2.5 & Over 4"}, + struct{ Home, Away int }{1, 5}, + domain.OUTCOME_STATUS_WIN}, + {"Home +2.5 Over 3", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A +2.5 & Over 3"}, + struct{ Home, Away int }{2, 0}, + domain.OUTCOME_STATUS_LOSS}, + {"Home -3.5 Over 3", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A -2.5 & Over 3"}, + struct{ Home, Away int }{4, 3}, + domain.OUTCOME_STATUS_LOSS}, + {"Away -3 Over 4", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B -3 & Over 4"}, + struct{ Home, Away int }{3, 5}, + domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateHandicapAndTotal(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} diff --git a/makefile b/makefile index 2a55cc9..b49fd95 100644 --- a/makefile +++ b/makefile @@ -39,6 +39,11 @@ migrations/up: @echo 'Running up migrations...' @docker compose up migrate +.PHONY: postgres +postgres: + @echo 'Running postgres db...' + docker compose -f docker-compose.yml exec postgres psql -U root -d gh + .PHONY: swagger swagger: @swag init -g cmd/main.go