basketball and fixes

This commit is contained in:
Samuel Tariku 2025-05-03 20:22:57 +03:00
parent ae571d51a6
commit b4e20a274d
25 changed files with 1528 additions and 374 deletions

View File

@ -70,6 +70,7 @@ CREATE TABLE IF NOT EXISTS tickets (
CREATE TABLE IF NOT EXISTS bet_outcomes (
id BIGSERIAL PRIMARY KEY,
bet_id BIGINT NOT NULL,
sport_id BIGINT NOT NULL,
event_id BIGINT NOT null,
odd_id BIGINT NOT NULL,
home_team_name VARCHAR(255) NOT NULL,

View File

@ -15,6 +15,7 @@ RETURNING *;
-- name: CreateBetOutcome :copyfrom
INSERT INTO bet_outcomes (
bet_id,
sport_id,
event_id,
odd_id,
home_team_name,
@ -39,7 +40,8 @@ VALUES (
$9,
$10,
$11,
$12
$12,
$13
);
-- name: GetAllBets :many
SELECT *
@ -56,6 +58,11 @@ WHERE cashout_id = $1;
SELECT *
FROM bet_with_outcomes
WHERE branch_id = $1;
-- name: GetBetOutcomeByEventID :many
SELECT *
FROM bet_outcomes
WHERE event_id = $1;
-- name: UpdateCashOut :exec
UPDATE bets
SET cashed_out = $2,

View File

@ -232,4 +232,7 @@ UPDATE events
SET score = $1,
status = $2,
fetched_at = NOW()
WHERE id = $3;
WHERE id = $3;
-- name: DeleteEvent :exec
DELETE FROM events
WHERE id = $1;

View File

@ -1256,7 +1256,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.CreateCompanyReq"
"$ref": "#/definitions/handlers.UpdateCompanyReq"
}
}
],
@ -3372,6 +3372,10 @@ const docTemplate = `{
"type": "string",
"example": "1"
},
"sport_id": {
"type": "integer",
"example": 1
},
"status": {
"allOf": [
{
@ -3439,13 +3443,19 @@ const docTemplate = `{
0,
1,
2,
3
3,
4
],
"x-enum-comments": {
"OUTCOME_STATUS_HALF": "Half Win and Half Given Back",
"OUTCOME_STATUS_VOID": "Give Back"
},
"x-enum-varnames": [
"OUTCOME_STATUS_PENDING",
"OUTCOME_STATUS_WIN",
"OUTCOME_STATUS_LOSS",
"OUTCOME_STATUS_VOID"
"OUTCOME_STATUS_VOID",
"OUTCOME_STATUS_HALF"
]
},
"domain.PaymentOption": {
@ -4034,6 +4044,12 @@ const docTemplate = `{
},
"handlers.CreateBranchReq": {
"type": "object",
"required": [
"branch_manager_id",
"location",
"name",
"operations"
],
"properties": {
"branch_manager_id": {
"type": "integer",
@ -4049,10 +4065,14 @@ const docTemplate = `{
},
"location": {
"type": "string",
"maxLength": 100,
"minLength": 3,
"example": "Addis Ababa"
},
"name": {
"type": "string",
"maxLength": 100,
"minLength": 3,
"example": "4-kilo Branch"
},
"operations": {
@ -4502,6 +4522,10 @@ const docTemplate = `{
"type": "integer",
"example": 1
},
"approver_name": {
"type": "string",
"example": "John Smith"
},
"bank_code": {
"type": "string"
},
@ -4516,10 +4540,26 @@ const docTemplate = `{
"type": "integer",
"example": 1
},
"branch_location": {
"type": "string",
"example": "Branch Location"
},
"branch_name": {
"type": "string",
"example": "Branch Name"
},
"cashier_id": {
"type": "integer",
"example": 1
},
"cashier_name": {
"type": "string",
"example": "John Smith"
},
"company_id": {
"type": "integer",
"example": 1
},
"created_at": {
"type": "string"
},
@ -4616,6 +4656,19 @@ const docTemplate = `{
}
}
},
"handlers.UpdateCompanyReq": {
"type": "object",
"properties": {
"admin_id": {
"type": "integer",
"example": 1
},
"name": {
"type": "string",
"example": "CompanyName"
}
}
},
"handlers.UpdateTransactionVerifiedReq": {
"type": "object",
"properties": {

View File

@ -1248,7 +1248,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.CreateCompanyReq"
"$ref": "#/definitions/handlers.UpdateCompanyReq"
}
}
],
@ -3364,6 +3364,10 @@
"type": "string",
"example": "1"
},
"sport_id": {
"type": "integer",
"example": 1
},
"status": {
"allOf": [
{
@ -3431,13 +3435,19 @@
0,
1,
2,
3
3,
4
],
"x-enum-comments": {
"OUTCOME_STATUS_HALF": "Half Win and Half Given Back",
"OUTCOME_STATUS_VOID": "Give Back"
},
"x-enum-varnames": [
"OUTCOME_STATUS_PENDING",
"OUTCOME_STATUS_WIN",
"OUTCOME_STATUS_LOSS",
"OUTCOME_STATUS_VOID"
"OUTCOME_STATUS_VOID",
"OUTCOME_STATUS_HALF"
]
},
"domain.PaymentOption": {
@ -4026,6 +4036,12 @@
},
"handlers.CreateBranchReq": {
"type": "object",
"required": [
"branch_manager_id",
"location",
"name",
"operations"
],
"properties": {
"branch_manager_id": {
"type": "integer",
@ -4041,10 +4057,14 @@
},
"location": {
"type": "string",
"maxLength": 100,
"minLength": 3,
"example": "Addis Ababa"
},
"name": {
"type": "string",
"maxLength": 100,
"minLength": 3,
"example": "4-kilo Branch"
},
"operations": {
@ -4494,6 +4514,10 @@
"type": "integer",
"example": 1
},
"approver_name": {
"type": "string",
"example": "John Smith"
},
"bank_code": {
"type": "string"
},
@ -4508,10 +4532,26 @@
"type": "integer",
"example": 1
},
"branch_location": {
"type": "string",
"example": "Branch Location"
},
"branch_name": {
"type": "string",
"example": "Branch Name"
},
"cashier_id": {
"type": "integer",
"example": 1
},
"cashier_name": {
"type": "string",
"example": "John Smith"
},
"company_id": {
"type": "integer",
"example": 1
},
"created_at": {
"type": "string"
},
@ -4608,6 +4648,19 @@
}
}
},
"handlers.UpdateCompanyReq": {
"type": "object",
"properties": {
"admin_id": {
"type": "integer",
"example": 1
},
"name": {
"type": "string",
"example": "CompanyName"
}
}
},
"handlers.UpdateTransactionVerifiedReq": {
"type": "object",
"properties": {

View File

@ -40,6 +40,9 @@ definitions:
odd_name:
example: "1"
type: string
sport_id:
example: 1
type: integer
status:
allOf:
- $ref: '#/definitions/domain.OutcomeStatus'
@ -85,12 +88,17 @@ definitions:
- 1
- 2
- 3
- 4
type: integer
x-enum-comments:
OUTCOME_STATUS_HALF: Half Win and Half Given Back
OUTCOME_STATUS_VOID: Give Back
x-enum-varnames:
- OUTCOME_STATUS_PENDING
- OUTCOME_STATUS_WIN
- OUTCOME_STATUS_LOSS
- OUTCOME_STATUS_VOID
- OUTCOME_STATUS_HALF
domain.PaymentOption:
enum:
- 0
@ -514,14 +522,23 @@ definitions:
type: boolean
location:
example: Addis Ababa
maxLength: 100
minLength: 3
type: string
name:
example: 4-kilo Branch
maxLength: 100
minLength: 3
type: string
operations:
items:
type: integer
type: array
required:
- branch_manager_id
- location
- name
- operations
type: object
handlers.CreateCashierReq:
properties:
@ -830,6 +847,9 @@ definitions:
approved_by:
example: 1
type: integer
approver_name:
example: John Smith
type: string
bank_code:
type: string
beneficiary_name:
@ -840,9 +860,21 @@ definitions:
branch_id:
example: 1
type: integer
branch_location:
example: Branch Location
type: string
branch_name:
example: Branch Name
type: string
cashier_id:
example: 1
type: integer
cashier_name:
example: John Smith
type: string
company_id:
example: 1
type: integer
created_at:
type: string
full_name:
@ -910,6 +942,15 @@ definitions:
cashedOut:
type: boolean
type: object
handlers.UpdateCompanyReq:
properties:
admin_id:
example: 1
type: integer
name:
example: CompanyName
type: string
type: object
handlers.UpdateTransactionVerifiedReq:
properties:
verified:
@ -1935,7 +1976,7 @@ paths:
name: updateCompany
required: true
schema:
$ref: '#/definitions/handlers.CreateCompanyReq'
$ref: '#/definitions/handlers.UpdateCompanyReq'
produces:
- application/json
responses:

View File

@ -72,6 +72,7 @@ func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, erro
type CreateBetOutcomeParams struct {
BetID int64 `json:"bet_id"`
SportID int64 `json:"sport_id"`
EventID int64 `json:"event_id"`
OddID int64 `json:"odd_id"`
HomeTeamName string `json:"home_team_name"`
@ -242,6 +243,48 @@ func (q *Queries) GetBetByID(ctx context.Context, id int64) (BetWithOutcome, err
return i, err
}
const GetBetOutcomeByEventID = `-- name: GetBetOutcomeByEventID :many
SELECT id, bet_id, sport_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, status, expires
FROM bet_outcomes
WHERE event_id = $1
`
func (q *Queries) GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]BetOutcome, error) {
rows, err := q.db.Query(ctx, GetBetOutcomeByEventID, eventID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []BetOutcome
for rows.Next() {
var i BetOutcome
if err := rows.Scan(
&i.ID,
&i.BetID,
&i.SportID,
&i.EventID,
&i.OddID,
&i.HomeTeamName,
&i.AwayTeamName,
&i.MarketID,
&i.MarketName,
&i.Odd,
&i.OddName,
&i.OddHeader,
&i.OddHandicap,
&i.Status,
&i.Expires,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const UpdateBetOutcomeStatus = `-- name: UpdateBetOutcomeStatus :exec
UPDATE bet_outcomes
SET status = $1

View File

@ -30,6 +30,7 @@ func (r *iteratorForCreateBetOutcome) Next() bool {
func (r iteratorForCreateBetOutcome) Values() ([]interface{}, error) {
return []interface{}{
r.rows[0].BetID,
r.rows[0].SportID,
r.rows[0].EventID,
r.rows[0].OddID,
r.rows[0].HomeTeamName,
@ -49,7 +50,7 @@ func (r iteratorForCreateBetOutcome) Err() error {
}
func (q *Queries) CreateBetOutcome(ctx context.Context, arg []CreateBetOutcomeParams) (int64, error) {
return q.db.CopyFrom(ctx, []string{"bet_outcomes"}, []string{"bet_id", "event_id", "odd_id", "home_team_name", "away_team_name", "market_id", "market_name", "odd", "odd_name", "odd_header", "odd_handicap", "expires"}, &iteratorForCreateBetOutcome{rows: arg})
return q.db.CopyFrom(ctx, []string{"bet_outcomes"}, []string{"bet_id", "sport_id", "event_id", "odd_id", "home_team_name", "away_team_name", "market_id", "market_name", "odd", "odd_name", "odd_header", "odd_handicap", "expires"}, &iteratorForCreateBetOutcome{rows: arg})
}
// iteratorForCreateTicketOutcome implements pgx.CopyFromSource.

View File

@ -11,6 +11,16 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
const DeleteEvent = `-- name: DeleteEvent :exec
DELETE FROM events
WHERE id = $1
`
func (q *Queries) DeleteEvent(ctx context.Context, id string) error {
_, err := q.db.Exec(ctx, DeleteEvent, id)
return err
}
const GetAllUpcomingEvents = `-- name: GetAllUpcomingEvents :many
SELECT id,
sport_id,

View File

@ -74,6 +74,7 @@ type Bet struct {
type BetOutcome struct {
ID int64 `json:"id"`
BetID int64 `json:"bet_id"`
SportID int64 `json:"sport_id"`
EventID int64 `json:"event_id"`
OddID int64 `json:"odd_id"`
HomeTeamName string `json:"home_team_name"`

View File

@ -70,7 +70,7 @@ func (q *Queries) CreateResult(ctx context.Context, arg CreateResultParams) (Res
}
const GetPendingBetOutcomes = `-- name: GetPendingBetOutcomes :many
SELECT id, bet_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, status, expires FROM bet_outcomes WHERE status = 0 AND expires <= CURRENT_TIMESTAMP
SELECT id, bet_id, sport_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, status, expires FROM bet_outcomes WHERE status = 0 AND expires <= CURRENT_TIMESTAMP
`
func (q *Queries) GetPendingBetOutcomes(ctx context.Context) ([]BetOutcome, error) {
@ -85,6 +85,7 @@ func (q *Queries) GetPendingBetOutcomes(ctx context.Context) ([]BetOutcome, erro
if err := rows.Scan(
&i.ID,
&i.BetID,
&i.SportID,
&i.EventID,
&i.OddID,
&i.HomeTeamName,

View File

@ -9,6 +9,7 @@ type BetOutcome struct {
BetID int64 `json:"bet_id" example:"1"`
EventID int64 `json:"event_id" example:"1"`
OddID int64 `json:"odd_id" example:"1"`
SportID int64 `json:"sport_id" example:"1"`
HomeTeamName string `json:"home_team_name" example:"Manchester"`
AwayTeamName string `json:"away_team_name" example:"Liverpool"`
MarketID int64 `json:"market_id" example:"1"`
@ -25,6 +26,7 @@ type CreateBetOutcome struct {
BetID int64 `json:"bet_id" example:"1"`
EventID int64 `json:"event_id" example:"1"`
OddID int64 `json:"odd_id" example:"1"`
SportID int64 `json:"sport_id" example:"1"`
HomeTeamName string `json:"home_team_name" example:"Manchester"`
AwayTeamName string `json:"away_team_name" example:"Liverpool"`
MarketID int64 `json:"market_id" example:"1"`
@ -78,4 +80,3 @@ type CreateBet struct {
IsShopBet bool
CashoutID string
}

30
internal/domain/league.go Normal file
View File

@ -0,0 +1,30 @@
package domain
// TODO Will make this dynamic by moving into the database
var SupportedLeagues = []int64{
// Football
10041282, //Premier League
10083364, //La Liga
10041095, //German Bundesliga
10041100, //Ligue 1
10041809, //UEFA Champions League
10041957, //UEFA Europa League
10079560, //UEFA Conference League
10047168, // US MLS
10050282, //UEFA Nations League
10040795, //EuroLeague
10043156, //England FA Cup
10042103, //France Cup
10041088, //Premier League 2
10084250, //Turkiye Super League
10041187, //Kenya Super League
10041315, //Italian Serie A
10041391, //Netherlands Eredivisie
// Basketball
173998768, //NBA
}

View File

@ -1,62 +1,126 @@
package domain
import (
"encoding/json"
"time"
)
type ResultResponse struct {
Success int `json:"success"`
Results []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 {
FirstHalf Score `json:"1"`
SecondHalf Score `json:"2"`
} `json:"scores"`
Stats struct {
Attacks []string `json:"attacks"`
Corners []string `json:"corners"`
DangerousAttacks []string `json:"dangerous_attacks"`
Goals []string `json:"goals"`
OffTarget []string `json:"off_target"`
OnTarget []string `json:"on_target"`
Penalties []string `json:"penalties"`
PossessionRT []string `json:"possession_rt"`
RedCards []string `json:"redcards"`
Substitutions []string `json:"substitutions"`
YellowCards []string `json:"yellowcards"`
} `json:"stats"`
Extra struct {
HomePos string `json:"home_pos"`
AwayPos string `json:"away_pos"`
StadiumData map[string]string `json:"stadium_data"`
Round string `json:"round"`
} `json:"extra"`
Events []map[string]string `json:"events"`
HasLineup int `json:"has_lineup"`
ConfirmedAt string `json:"confirmed_at"`
Bet365ID string `json:"bet365_id"`
} `json:"results"`
type BaseResultResponse struct {
Success int `json:"success"`
Results []json.RawMessage `json:"results"`
}
type FootballResultResponse 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 {
FirstHalf Score `json:"1"`
SecondHalf Score `json:"2"`
} `json:"scores"`
Stats struct {
Attacks []string `json:"attacks"`
Corners []string `json:"corners"`
DangerousAttacks []string `json:"dangerous_attacks"`
Goals []string `json:"goals"`
OffTarget []string `json:"off_target"`
OnTarget []string `json:"on_target"`
Penalties []string `json:"penalties"`
PossessionRT []string `json:"possession_rt"`
RedCards []string `json:"redcards"`
Substitutions []string `json:"substitutions"`
YellowCards []string `json:"yellowcards"`
} `json:"stats"`
Extra struct {
HomePos string `json:"home_pos"`
AwayPos string `json:"away_pos"`
StadiumData map[string]string `json:"stadium_data"`
Round string `json:"round"`
} `json:"extra"`
Events []map[string]string `json:"events"`
HasLineup int `json:"has_lineup"`
ConfirmedAt string `json:"confirmed_at"`
Bet365ID string `json:"bet365_id"`
}
type BasketballResultResponse 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 {
FirstQuarter Score `json:"1"`
SecondQuarter Score `json:"2"`
FirstHalf Score `json:"3"`
ThirdQuarter Score `json:"4"`
FourthQuarter Score `json:"5"`
TotalScore Score `json:"7"`
} `json:"scores"`
Stats struct {
TwoPoints []string `json:"2points"`
ThreePoints []string `json:"3points"`
BiggestLead []string `json:"biggest_lead"`
Fouls []string `json:"fouls"`
FreeThrows []string `json:"free_throws"`
FreeThrowRate []string `json:"free_throws_rate"`
LeadChanges []string `json:"lead_changes"`
MaxpointsInarow []string `json:"maxpoints_inarow"`
Possession []string `json:"possession"`
SuccessAttempts []string `json:"success_attempts"`
TimeSpendInLead []string `json:"timespent_inlead"`
Timeuts []string `json:"time_outs"`
} `json:"stats"`
Extra struct {
HomePos string `json:"home_pos"`
AwayPos string `json:"away_pos"`
AwayManager map[string]string `json:"away_manager"`
HomeManager map[string]string `json:"home_manager"`
NumberOfPeriods string `json:"numberofperiods"`
PeriodLength string `json:"periodlength"`
StadiumData map[string]string `json:"stadium_data"`
Length string `json:"length"`
Round string `json:"round"`
} `json:"extra"`
Events []map[string]string `json:"events"`
HasLineup int `json:"has_lineup"`
ConfirmedAt string `json:"confirmed_at"`
Bet365ID string `json:"bet365_id"`
}
type Score struct {
@ -67,7 +131,7 @@ type Score struct {
type MarketConfig struct {
Sport string
MarketCategories map[string]bool
MarketTypes map[string]bool
MarketTypes map[int64]bool
}
type Result struct {
@ -101,5 +165,6 @@ const (
OUTCOME_STATUS_PENDING OutcomeStatus = 0
OUTCOME_STATUS_WIN OutcomeStatus = 1
OUTCOME_STATUS_LOSS OutcomeStatus = 2
OUTCOME_STATUS_VOID OutcomeStatus = 3
OUTCOME_STATUS_VOID OutcomeStatus = 3 //Give Back
OUTCOME_STATUS_HALF OutcomeStatus = 4 //Half Win and Half Given Back
)

34
internal/domain/sport.go Normal file
View File

@ -0,0 +1,34 @@
package domain
type Sport int64
const (
FOOTBALL = 1
BASKETBALL = 18
VOLLEYBALL = 91
HANDBALL = 78
BASEBALL = 16
HORSE_RACING = 2
GREYHOUNDS = 4
ICE_HOCKEY = 17
SNOOKER = 14
AMERICAN_FOOTBALL = 12
CRICKET = 3
FUTSAL = 83
DARTS = 15
TABLE_TENNIS = 92
BADMINTON = 94
RUGBY_UNION = 8
RUGBY_LEAGUE = 19
AUSTRALIAN_RULES = 36
BOWLS = 66
BOXING = 9
GAELIC_SPORTS = 75
FLOORBALL = 90
BEACH_VOLLEYBALL = 95
WATER_POLO = 110
SQUASH = 107
E_SPORTS = 151
MMA = 162
SURFING = 148
)

View File

@ -0,0 +1,147 @@
package domain
type FootballMarket int64
const (
FOOTBALL_FULL_TIME_RESULT FootballMarket = 40 //"full_time_result"
FOOTBALL_DOUBLE_CHANCE FootballMarket = 10114 //"double_chance"
FOOTBALL_GOALS_OVER_UNDER FootballMarket = 981 //"goals_over_under"
FOOTBALL_CORRECT_SCORE FootballMarket = 43 //"correct_score"
FOOTBALL_ASIAN_HANDICAP FootballMarket = 938 //"asian_handicap"
FOOTBALL_GOAL_LINE FootballMarket = 10143 //"goal_line"
FOOTBALL_HALF_TIME_RESULT FootballMarket = 1579 //"half_time_result"
FOOTBALL_FIRST_HALF_ASIAN_HANDICAP FootballMarket = 50137 //"1st_half_asian_handicap"
FOOTBALL_FIRST_HALF_GOAL_LINE FootballMarket = 50136 //"1st_half_goal_line"
FOOTBALL_FIRST_TEAM_TO_SCORE FootballMarket = 1178 //"first_team_to_score"
FOOTBALL_GOALS_ODD_EVEN FootballMarket = 10111 //"goals_odd_even"
FOOTBALL_DRAW_NO_BET FootballMarket = 10544 //"draw_no_bet"
)
type BasketBallMarket int64
const (
// Main
BASKETBALL_GAME_LINES BasketBallMarket = 1453 //"game_lines"
BASKETBALL_FIRST_HALF BasketBallMarket = 928 //"1st_half"
BASKETBALL_FIRST_QUARTER BasketBallMarket = 941 //"1st_quarter"
// Main Props
BASKETBALL_RESULT_AND_BOTH_TEAMS_TO_SCORE_X_POINTS BasketBallMarket = 181273 //"result_and_both_teams_to_score_'x'_points"
BASKETBALL_DOUBLE_RESULT BasketBallMarket = 1517 //"double_result"
BASKETBALL_MATCH_RESULT_AND_TOTAL BasketBallMarket = 181125 //"match_result_and_total"
BASKETBALL_MATCH_HANDICAP_AND_TOTAL BasketBallMarket = 181126 //"match_handicap_and_total"
// Half Props
BASKETBALL_FIRST_HALF_TEAM_TOTALS BasketBallMarket = 181159 //"1st_half_team_totals"
BASKETBALL_FIRST_HALF_WINNING_MARGIN BasketBallMarket = 181185 //"1st_half_winning_margin"
BASKETBALL_FIRST_HALF_HANDICAP_AND_TOTAL BasketBallMarket = 181182 //"1st_half_handicap_and_total"
BASKETBALL_FIRST_HALF_BOTH_TEAMS_TO_SCORE_X_POINTS BasketBallMarket = 181195 //"1st_half_both_teams_to_score_x_points"
BASKETBALL_FIRST_HALF_MONEY_LINE_3_WAY BasketBallMarket = 181183 //"1st_half_money_line_3_way"
// Others
BASKETBALL_GAME_TOTAL_ODD_EVEN BasketBallMarket = 180013 //"game_total_odd_even"
BASKETBALL_FIRST_QUARTER_TOTAL_ODD_EVEN BasketBallMarket = 180170 //"1st_quarter_total_odd_even"
BASKETBALL_HIGHEST_SCORING_HALF BasketBallMarket = 181131 //"highest_scoring_half"
BASKETBALL_HIGHEST_SCORING_QUARTER BasketBallMarket = 181132 //"highest_scoring_quarter"
BASKETBALL_FIRST_HALF_DOUBLE_CHANCE BasketBallMarket = 181184 //"1st_half_double_chance"
BASKETBALL_FIRST_HALF_TOTAL_ODD_EVEN BasketBallMarket = 181204 //"1st_half_total_odd_even"
BASKETBALL_FIRST_QUARTER_HANDICAP_AND_TOTAL BasketBallMarket = 181243 //"1st_quarter_handicap_and_total"
BASKETBALL_FIRST_QUARTER_DOUBLE_CHANCE BasketBallMarket = 181245 //"1st_quarter_double_chance"
// Quarter Props
BASKETBALL_FIRST_QUARTER_TEAM_TOTALS BasketBallMarket = 181220 //"1st_quarter_team_totals"
BASKETBALL_FIRST_QUARTER_WINNING_MARGIN BasketBallMarket = 181247 //"1st_quarter_winning_margin"
// Team Props
BASKETBALL_TEAM_WITH_HIGHEST_SCORING_QUARTER BasketBallMarket = 181377 //"team_with_highest_scoring_quarter"
BASKETBALL_TEAM_TOTALS BasketBallMarket = 181335 //"team_totals"
BASKETBALL_TEAM_TOTAL_ODD_EVEN BasketBallMarket = 1731 //"team_total_odd_even"
)
type IceHockeyMarket int64
const (
ICE_HOCKEY_FIRST_PERIOD IceHockeyMarket = 1531
ICE_HOCKEY_GAME_LINES IceHockeyMarket = 972
ICE_HOCKEY_THREE_WAY IceHockeyMarket = 170008
ICE_HOCKEY_DRAW_NO_BET IceHockeyMarket = 170447
ICE_HOCKEY_DOUBLE_CHANCE IceHockeyMarket = 170038
ICE_HOCKEY_WINNING_MARGIN IceHockeyMarket = 1556
ICE_HOCKEY_HIGHEST_SCORING_PERIOD IceHockeyMarket = 1557
ICE_HOCKEY_TIED_AFTER_REGULATION IceHockeyMarket = 170479
ICE_HOCKEY_WHEN_WILL_MATCH_END IceHockeyMarket = 170481
ICE_HOCKEY_GAME_TOTAL_ODD_EVEN IceHockeyMarket = 170013
ICE_HOCKEY_ALTERNATIVE_PUCK_LINE_TWO_WAY IceHockeyMarket = 170226
ICE_HOCKEY_ALTERNATIVE_TOTAL_TWO_WAY IceHockeyMarket = 170240
)
// TODO: Move this into the database so that it can be modified dynamically
var SupportedMarkets = map[string]MarketConfig{
"football": {
Sport: "football",
MarketCategories: map[string]bool{
"main": true,
"asian_lines": true,
"goals": true,
"half": true,
},
MarketTypes: map[int64]bool{
int64(FOOTBALL_FULL_TIME_RESULT): true, //"full_time_result"
int64(FOOTBALL_DOUBLE_CHANCE): true, //"double_chance"
int64(FOOTBALL_GOALS_OVER_UNDER): true, //"goals_over_under"
int64(FOOTBALL_CORRECT_SCORE): true, //"correct_score"
int64(FOOTBALL_ASIAN_HANDICAP): true, //"asian_handicap"
int64(FOOTBALL_GOAL_LINE): true, //"goal_line"
int64(FOOTBALL_HALF_TIME_RESULT): true, //"half_time_result"
int64(FOOTBALL_FIRST_HALF_ASIAN_HANDICAP): true, //"1st_half_asian_handicap"
int64(FOOTBALL_FIRST_HALF_GOAL_LINE): true, //"1st_half_goal_line"
int64(FOOTBALL_FIRST_TEAM_TO_SCORE): true, //"first_team_to_score"
int64(FOOTBALL_GOALS_ODD_EVEN): true, //"goals_odd_even"
int64(FOOTBALL_DRAW_NO_BET): true, //"draw_no_bet"
},
},
"basketball": {
Sport: "basketball",
MarketCategories: map[string]bool{
"main": true,
"main_props": true,
"others": true,
"quarter_props": true,
"team_props": true,
"half_props": true,
},
MarketTypes: map[int64]bool{
int64(BASKETBALL_GAME_LINES): true,
int64(BASKETBALL_FIRST_HALF): true,
int64(BASKETBALL_FIRST_QUARTER): true,
int64(BASKETBALL_RESULT_AND_BOTH_TEAMS_TO_SCORE_X_POINTS): true,
int64(BASKETBALL_DOUBLE_RESULT): true,
int64(BASKETBALL_MATCH_RESULT_AND_TOTAL): true,
int64(BASKETBALL_MATCH_HANDICAP_AND_TOTAL): false,
int64(BASKETBALL_GAME_TOTAL_ODD_EVEN): true,
int64(BASKETBALL_TEAM_TOTALS): true,
int64(BASKETBALL_TEAM_TOTAL_ODD_EVEN): true,
int64(BASKETBALL_FIRST_HALF_TEAM_TOTALS): true,
int64(BASKETBALL_FIRST_HALF_WINNING_MARGIN): false,
int64(BASKETBALL_FIRST_HALF_HANDICAP_AND_TOTAL): true,
int64(BASKETBALL_FIRST_HALF_BOTH_TEAMS_TO_SCORE_X_POINTS): true,
int64(BASKETBALL_FIRST_HALF_MONEY_LINE_3_WAY): true,
int64(BASKETBALL_FIRST_HALF_DOUBLE_CHANCE): true,
int64(BASKETBALL_FIRST_HALF_TOTAL_ODD_EVEN): true,
int64(BASKETBALL_HIGHEST_SCORING_HALF): true,
int64(BASKETBALL_FIRST_QUARTER_HANDICAP_AND_TOTAL): false,
int64(BASKETBALL_FIRST_QUARTER_DOUBLE_CHANCE): true,
int64(BASKETBALL_FIRST_QUARTER_TEAM_TOTALS): true,
int64(BASKETBALL_FIRST_QUARTER_WINNING_MARGIN): true,
int64(BASKETBALL_FIRST_QUARTER_TOTAL_ODD_EVEN): true,
int64(BASKETBALL_HIGHEST_SCORING_QUARTER): true,
int64(BASKETBALL_TEAM_WITH_HIGHEST_SCORING_QUARTER): true,
},
},
}

View File

@ -31,27 +31,33 @@ func convertDBBet(bet dbgen.Bet) domain.Bet {
}
}
func convertDBBetOutcomes(bet dbgen.BetWithOutcome) domain.GetBet {
func convertDBBetOutcomes(outcome dbgen.BetOutcome) domain.BetOutcome {
return domain.BetOutcome{
ID: outcome.ID,
BetID: outcome.BetID,
SportID: outcome.SportID,
EventID: outcome.EventID,
OddID: outcome.OddID,
HomeTeamName: outcome.HomeTeamName,
AwayTeamName: outcome.AwayTeamName,
MarketID: outcome.MarketID,
MarketName: outcome.MarketName,
Odd: outcome.Odd,
OddName: outcome.OddName,
OddHeader: outcome.OddHeader,
OddHandicap: outcome.OddHandicap,
Status: domain.OutcomeStatus(outcome.Status),
Expires: outcome.Expires.Time,
}
}
func convertDBBetWithOutcomes(bet dbgen.BetWithOutcome) domain.GetBet {
var outcomes []domain.BetOutcome = make([]domain.BetOutcome, 0, len(bet.Outcomes))
for _, outcome := range bet.Outcomes {
outcomes = append(outcomes, domain.BetOutcome{
ID: outcome.ID,
BetID: outcome.BetID,
EventID: outcome.EventID,
OddID: outcome.OddID,
HomeTeamName: outcome.HomeTeamName,
AwayTeamName: outcome.AwayTeamName,
MarketID: outcome.MarketID,
MarketName: outcome.MarketName,
Odd: outcome.Odd,
OddName: outcome.OddName,
OddHeader: outcome.OddHeader,
OddHandicap: outcome.OddHandicap,
Status: domain.OutcomeStatus(outcome.Status),
Expires: outcome.Expires.Time,
})
outcomes = append(outcomes, convertDBBetOutcomes(outcome))
}
return domain.GetBet{
ID: bet.ID,
Amount: domain.Currency(bet.Amount),
@ -78,6 +84,7 @@ func convertDBCreateBetOutcome(betOutcome domain.CreateBetOutcome) dbgen.CreateB
return dbgen.CreateBetOutcomeParams{
BetID: betOutcome.BetID,
EventID: betOutcome.EventID,
SportID: betOutcome.SportID,
OddID: betOutcome.OddID,
HomeTeamName: betOutcome.HomeTeamName,
AwayTeamName: betOutcome.AwayTeamName,
@ -145,7 +152,7 @@ func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error)
return domain.GetBet{}, err
}
return convertDBBetOutcomes(bet), nil
return convertDBBetWithOutcomes(bet), nil
}
func (s *Store) GetBetByCashoutID(ctx context.Context, id string) (domain.GetBet, error) {
@ -155,7 +162,7 @@ func (s *Store) GetBetByCashoutID(ctx context.Context, id string) (domain.GetBet
return domain.GetBet{}, err
}
return convertDBBetOutcomes(bet), nil
return convertDBBetWithOutcomes(bet), nil
}
func (s *Store) GetAllBets(ctx context.Context) ([]domain.GetBet, error) {
@ -166,7 +173,7 @@ func (s *Store) GetAllBets(ctx context.Context) ([]domain.GetBet, error) {
var result []domain.GetBet = make([]domain.GetBet, 0, len(bets))
for _, bet := range bets {
result = append(result, convertDBBetOutcomes(bet))
result = append(result, convertDBBetWithOutcomes(bet))
}
return result, nil
@ -184,7 +191,7 @@ func (s *Store) GetBetByBranchID(ctx context.Context, BranchID int64) ([]domain.
var result []domain.GetBet = make([]domain.GetBet, 0, len(bets))
for _, bet := range bets {
result = append(result, convertDBBetOutcomes(bet))
result = append(result, convertDBBetWithOutcomes(bet))
}
return result, nil
@ -206,6 +213,18 @@ func (s *Store) UpdateStatus(ctx context.Context, id int64, status domain.Outcom
return err
}
func (s *Store) GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]domain.BetOutcome, error) {
outcomes, err := s.queries.GetBetOutcomeByEventID(ctx, eventID)
if err != nil {
return nil, nil
}
var result []domain.BetOutcome = make([]domain.BetOutcome, 0, len(outcomes))
for _, outcome := range outcomes {
result = append(result, convertDBBetOutcomes(outcome))
}
return result, nil
}
func (s *Store) UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error {
err := s.queries.UpdateBetOutcomeStatus(ctx, dbgen.UpdateBetOutcomeStatusParams{
Status: int32(status),

View File

@ -206,3 +206,11 @@ func (s *Store) UpdateFinalScore(ctx context.Context, eventID, fullScore, status
return nil
}
func (s *Store) DeleteEvent(ctx context.Context, eventID string) error {
err := s.queries.DeleteEvent(ctx, eventID)
if err != nil {
return err
}
return nil
}

View File

@ -7,6 +7,7 @@ import (
"io"
"log"
"net/http"
"slices"
"strconv"
"sync"
"time"
@ -98,7 +99,7 @@ func (s *service) FetchLiveEvents(ctx context.Context) error {
}
func (s *service) FetchUpcomingEvents(ctx context.Context) error {
sportIDs := []int{1}
sportIDs := []int{1, 18}
for _, sportID := range sportIDs {
var totalPages int = 1
@ -106,7 +107,7 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error {
var limit int = 100
var count int = 0
for page != totalPages {
time.Sleep(3 * time.Second) //This will restrict the fetching to 1200 requests per hour
// time.Sleep(1 * time.Second) //This will restrict the fetching to 1200 requests per hour
page = page + 1
url := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s&page=%d", sportID, s.token, page)
@ -147,9 +148,27 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error {
if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 {
continue
}
skippedLeague := 0
for _, ev := range data.Results {
startUnix, _ := strconv.ParseInt(ev.Time, 10, 64)
// eventID, err := strconv.ParseInt(ev.ID, 10, 64)
// if err != nil {
// log.Panicf("❌ Invalid event id, eventID %v", ev.ID)
// continue
// }
leagueID, err := strconv.ParseInt(ev.League.ID, 10, 64)
if err != nil {
log.Printf("❌ Invalid league id, leagueID %v", ev.League.ID)
continue
}
if !slices.Contains(domain.SupportedLeagues, leagueID) {
skippedLeague++
continue
}
event := domain.UpcomingEvent{
ID: ev.ID,
SportID: ev.SportID,
@ -179,6 +198,7 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error {
break
}
count++
log.Printf("Skipped leagues %d", skippedLeague)
}
}

View File

@ -22,6 +22,7 @@ func New(token string, store *repository.Store) *ServiceImpl {
return &ServiceImpl{token: token, store: store}
}
// TODO this is only getting the main odds, this must be fixed
func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
eventIDs, err := s.store.GetAllUpcomingEvents(ctx)
if err != nil {
@ -30,7 +31,7 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
}
for _, event := range eventIDs {
time.Sleep(3 * time.Second) //This will restrict the fetching to 1200 requests per hour
// time.Sleep(3 * time.Second) //This will restrict the fetching to 1200 requests per hour
eventID := event.ID
prematchURL := "https://api.b365api.com/v3/bet365/prematch?token=" + s.token + "&FI=" + eventID
@ -65,7 +66,6 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
log.Printf("⚠️ Skipping event %s with no valid ID", eventID)
continue
}
s.storeSection(ctx, finalID, result.FI, "main", result.Main)
}

View File

@ -0,0 +1,614 @@
package result
import (
"fmt"
"strconv"
"strings"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
// Football evaluations
func evaluateFullTimeResult(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 "Draw":
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_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName)
}
}
func evaluateGoalsOverUnder(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalGoals := float64(score.Home + score.Away)
threshold, err := strconv.ParseFloat(outcome.OddName, 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
}
if outcome.OddHeader == "Over" {
if totalGoals > threshold {
return domain.OUTCOME_STATUS_WIN, nil
} else if totalGoals == threshold {
return domain.OUTCOME_STATUS_VOID, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else if outcome.OddHeader == "Under" {
if totalGoals < threshold {
return domain.OUTCOME_STATUS_WIN, nil
} else if totalGoals == threshold {
return domain.OUTCOME_STATUS_VOID, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
func evaluateCorrectScore(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
expectedScore := fmt.Sprintf("%d-%d", score.Home, score.Away)
if outcome.OddName == expectedScore {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}
func evaluateHalfTimeResult(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
return evaluateFullTimeResult(outcome, score)
}
// This is a multiple outcome checker for the asian handicap and other kinds of bets
// The only outcome that are allowed are "Both Bets win", "Both Bets Lose", "Half Win and Half Void"
func checkMultiOutcome(outcome domain.OutcomeStatus, secondOutcome domain.OutcomeStatus) (domain.OutcomeStatus, error) {
switch outcome {
case domain.OUTCOME_STATUS_PENDING:
return secondOutcome, nil
case domain.OUTCOME_STATUS_WIN:
if secondOutcome == domain.OUTCOME_STATUS_WIN {
return domain.OUTCOME_STATUS_WIN, nil
} else if secondOutcome == domain.OUTCOME_STATUS_VOID {
return domain.OUTCOME_STATUS_HALF, nil
} else {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid multi outcome")
}
case domain.OUTCOME_STATUS_LOSS:
if secondOutcome == domain.OUTCOME_STATUS_LOSS {
return domain.OUTCOME_STATUS_LOSS, nil
} else if secondOutcome == domain.OUTCOME_STATUS_VOID {
return domain.OUTCOME_STATUS_HALF, nil
} else {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid multi outcome")
}
case domain.OUTCOME_STATUS_VOID:
if secondOutcome == domain.OUTCOME_STATUS_WIN || secondOutcome == domain.OUTCOME_STATUS_LOSS {
return domain.OUTCOME_STATUS_HALF, nil
} else {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid multi outcome")
}
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid multi outcome")
}
}
func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
handicapList := strings.Split(outcome.OddHandicap, ",")
newOutcome := domain.OUTCOME_STATUS_PENDING
for _, handicapStr := range handicapList {
handicap, err := strconv.ParseFloat(handicapStr, 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid handicap: %s", outcome.OddHandicap)
}
adjustedHomeScore := float64(score.Home)
adjustedAwayScore := float64(score.Away)
if outcome.OddHeader == "1" { // Home team
adjustedHomeScore += handicap
} else if outcome.OddHeader == "2" { // Away team
adjustedAwayScore += handicap
} else {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
if adjustedHomeScore > adjustedAwayScore {
if outcome.OddHeader == "1" {
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN)
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 {
fmt.Printf("multi outcome check error")
return domain.OUTCOME_STATUS_PENDING, err
}
} else if adjustedHomeScore < adjustedAwayScore {
if outcome.OddHeader == "2" {
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN)
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 {
fmt.Printf("multi outcome check error")
return domain.OUTCOME_STATUS_PENDING, err
}
}
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID)
if err != nil {
fmt.Printf("multi outcome check error")
return domain.OUTCOME_STATUS_PENDING, err
}
}
return newOutcome, nil
}
func evaluateGoalLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
return evaluateGoalsOverUnder(outcome, score)
}
func evaluateFirstTeamToScore(outcome domain.BetOutcome, events []map[string]string) (domain.OutcomeStatus, error) {
for _, event := range events {
if strings.Contains(event["text"], "1st Goal") {
if strings.Contains(event["text"], outcome.HomeTeamName) && outcome.OddName == "1" {
return domain.OUTCOME_STATUS_WIN, nil
} else if strings.Contains(event["text"], outcome.AwayTeamName) && outcome.OddName == "2" {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}
}
return domain.OUTCOME_STATUS_VOID, nil // No goals scored
}
func evaluateGoalsOddEven(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalGoals := score.Home + score.Away
isOdd := totalGoals%2 == 1
if outcome.OddName == "Odd" && isOdd {
return domain.OUTCOME_STATUS_WIN, nil
} else if outcome.OddName == "Even" && !isOdd {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}
func evaluateDoubleChance(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
isHomeWin := score.Home > score.Away
isDraw := score.Home == score.Away
isAwayWin := score.Away > score.Home
switch outcome.OddName {
case "1 or Draw", (outcome.HomeTeamName + " or " + "Draw"):
if isHomeWin || isDraw {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
case "Draw or 2", ("Draw" + " or " + outcome.AwayTeamName):
if isDraw || isAwayWin {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
case "1 or 2", (outcome.HomeTeamName + " or " + outcome.AwayTeamName):
if isHomeWin || isAwayWin {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName)
}
}
func evaluateDrawNoBet(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
if score.Home == score.Away {
return domain.OUTCOME_STATUS_VOID, nil
}
if outcome.OddName == "1" && score.Home > score.Away {
return domain.OUTCOME_STATUS_WIN, nil
} else if outcome.OddName == "2" && score.Away > score.Home {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}
// basketball evaluations
func evaluateGameLines(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddName {
case "Money Line":
return evaluateMoneyLine(outcome, score)
case "Spread":
// Since Spread betting is essentially the same thing
return evaluateAsianHandicap(outcome, score)
case "Total":
return evaluateTotalOverUnder(outcome, score)
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName)
}
}
func evaluateMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddHeader {
case "1":
if score.Home > score.Away {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
case "2":
if score.Home < score.Away {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName)
}
}
func evaluateTotalOverUnder(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_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
}
threshold, err := strconv.ParseFloat(overUnderStr[1], 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, 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)
if overUnderStr[0] == "O" {
if totalScore > threshold {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else if overUnderStr[0] == "U" {
if totalScore < threshold {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
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_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
}
overUnder := overUnderStr[0]
if overUnder != "Over" && overUnder != "Under" {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("failed parsing over under: %s", outcome.OddHeader)
}
threshold, err := strconv.ParseFloat(overUnderStr[1], 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, 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 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 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_PENDING, fmt.Errorf("failed to parse over and under: %s", outcome.OddName)
}
}
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_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddHandicap)
}
overUnder := overUnderStr[0]
if overUnder != "Over" && overUnder != "Under" {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("failed parsing over under: %s", outcome.OddHeader)
}
threshold, err := strconv.ParseFloat(overUnderStr[1], 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, 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_PENDING, fmt.Errorf("failed to parse over and under: %s", outcome.OddName)
}
}
// Evaluate Result and Both Teams To Score X Points
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_PENDING, 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_PENDING, 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_PENDING, fmt.Errorf("team name error: %s", teamName)
}
return domain.OUTCOME_STATUS_LOSS, nil
}
// Both Teams To Score X 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_PENDING, 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_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
return domain.OUTCOME_STATUS_LOSS, nil
}
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_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName)
}
}
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_PENDING, 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_PENDING, fmt.Errorf("invalid oddname: %s", firstHalfWinner)
}
if secondHalfWinner != outcome.HomeTeamName && secondHalfWinner != outcome.AwayTeamName && secondHalfWinner != "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
}
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
}
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_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName)
}
return domain.OUTCOME_STATUS_LOSS, nil
}
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.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 fourthQuarterTotal == firstQuarterTotal && fourthQuarterTotal == secondQuarterTotal && fourthQuarterTotal == thirdQuarterTotal {
return domain.OUTCOME_STATUS_WIN, nil
}
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName)
}
return domain.OUTCOME_STATUS_LOSS, nil
}
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_PENDING, 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_PENDING, fmt.Errorf("failed parsing over and under: %s", outcome.OddName)
}
handicap, err := strconv.ParseFloat(nameSplit[len(nameSplit)-4], 10)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, 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)
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_PENDING, fmt.Errorf("failed parsing team name: %s", outcome.OddName)
}
}

View File

@ -31,139 +31,142 @@ func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger)
}
}
var supportedMarkets = map[string]domain.MarketConfig{
"football": {
Sport: "football",
MarketCategories: map[string]bool{
"main": true,
"asian_lines": true,
"goals": true,
"half": true,
},
MarketTypes: map[string]bool{
"full_time_result": true,
"double_chance": true,
"goals_over_under": true,
"correct_score": true,
"asian_handicap": true,
"goal_line": true,
"half_time_result": true,
"1st_half_asian_handicap": true,
"1st_half_goal_line": true,
"first_team_to_score": true,
"goals_odd_even": true,
"draw_no_bet": true,
},
},
type ResultCheck struct {
}
func (s *Service) FetchAndProcessResults(ctx context.Context) error {
outcomes, err := s.repo.GetPendingBetOutcomes(ctx)
// TODO: Optimize this because there could be many bet outcomes for the same odd
// Take market id and match result as param and update all the bet outcomes at the same time
events, err := s.repo.GetExpiredUpcomingEvents(ctx)
if err != nil {
s.logger.Error("Failed to get pending bet outcomes", "error", err)
s.logger.Error("Failed to fetch events")
return err
}
for _, outcome := range outcomes {
if outcome.Expires.After(time.Now()) {
continue
for _, event := range events {
eventID, err := strconv.ParseInt(event.ID, 10, 64)
if err != nil {
s.logger.Error("Failed to parse event id")
return err
}
outcomes, err := s.repo.GetBetOutcomeByEventID(ctx, eventID)
if err != nil {
s.logger.Error("Failed to get pending bet outcomes", "error", err)
return err
}
result, err := s.fetchResult(ctx, outcome.EventID, outcome.OddID, outcome.MarketID, outcome)
if err != nil {
s.logger.Error("Failed to fetch result", "event_id", outcome.EventID, "error", err)
continue
}
for _, outcome := range outcomes {
if outcome.Expires.After(time.Now()) {
continue
}
_, err = s.repo.CreateResult(ctx, domain.CreateResult{
BetOutcomeID: outcome.ID,
EventID: outcome.EventID,
OddID: outcome.OddID,
MarketID: outcome.MarketID,
Status: result.Status,
Score: result.Score,
})
if err != nil {
s.logger.Error("Failed to store result", "bet_outcome_id", outcome.ID, "error", err)
continue
}
sportID, err := strconv.ParseInt(event.SportID, 10, 64)
if err != nil {
s.logger.Error("Sport ID is invalid", "event_id", outcome.EventID, "error", err)
continue
}
err = s.repo.UpdateBetOutcomeStatus(ctx, outcome.ID, result.Status)
result, err := s.fetchResult(ctx, outcome.EventID, outcome.OddID, outcome.MarketID, sportID, outcome)
if err != nil {
s.logger.Error("Failed to fetch result", "event_id", outcome.EventID, "error", err)
continue
}
// _, err = s.repo.CreateResult(ctx, domain.CreateResult{
// BetOutcomeID: outcome.ID,
// EventID: outcome.EventID,
// OddID: outcome.OddID,
// MarketID: outcome.MarketID,
// Status: result.Status,
// Score: result.Score,
// })
// if err != nil {
// s.logger.Error("Failed to store result", "bet_outcome_id", outcome.ID, "error", err)
// continue
// }
err = s.repo.UpdateBetOutcomeStatus(ctx, outcome.ID, result.Status)
if err != nil {
s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err)
continue
}
}
err = s.repo.DeleteEvent(ctx, event.ID)
if err != nil {
s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err)
continue
s.logger.Error("Failed to remove event", "event_id", event.ID, "error", err)
return err
}
}
return nil
}
func (s *Service) FetchAndStoreResult(ctx context.Context, eventID string) error {
url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%s", s.config.Bet365Token, eventID)
// func (s *Service) FetchAndStoreResult(ctx context.Context, eventID string) error {
// url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%s", s.config.Bet365Token, eventID)
res, err := s.client.Get(url)
if err != nil {
s.logger.Error("Failed to fetch result", "event_id", eventID, "error", err)
return fmt.Errorf("failed to fetch result: %w", err)
}
defer res.Body.Close()
// res, err := s.client.Get(url)
// if err != nil {
// s.logger.Error("Failed to fetch result", "event_id", eventID, "error", err)
// return fmt.Errorf("failed to fetch result: %w", err)
// }
// defer res.Body.Close()
if res.StatusCode != http.StatusOK {
s.logger.Error("Unexpected status code", "event_id", eventID, "status_code", res.StatusCode)
return fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
// if res.StatusCode != http.StatusOK {
// s.logger.Error("Unexpected status code", "event_id", eventID, "status_code", res.StatusCode)
// return fmt.Errorf("unexpected status code: %d", res.StatusCode)
// }
var apiResp domain.ResultResponse
if err := json.NewDecoder(res.Body).Decode(&apiResp); err != nil {
s.logger.Error("Failed to decode result", "event_id", eventID, "error", err)
return fmt.Errorf("failed to decode result: %w", err)
}
// var apiResp domain.BaseResultResponse
// if err := json.NewDecoder(res.Body).Decode(&apiResp); err != nil {
// s.logger.Error("Failed to decode result", "event_id", eventID, "error", err)
// return fmt.Errorf("failed to decode result: %w", err)
// }
if apiResp.Success != 1 || len(apiResp.Results) == 0 {
s.logger.Error("Invalid API response", "event_id", eventID)
return fmt.Errorf("no result returned from API")
}
// if apiResp.Success != 1 || len(apiResp.Results) == 0 {
// s.logger.Error("Invalid API response", "event_id", eventID)
// return fmt.Errorf("no result returned from API")
// }
r := apiResp.Results[0]
if r.TimeStatus != "3" {
s.logger.Warn("Match not yet completed", "event_id", eventID)
return fmt.Errorf("match not yet completed")
}
// r := apiResp.Results[0]
// if r.TimeStatus != "3" {
// s.logger.Warn("Match not yet completed", "event_id", eventID)
// return fmt.Errorf("match not yet completed")
// }
eventIDInt, err := strconv.ParseInt(eventID, 10, 64)
if err != nil {
s.logger.Error("Failed to parse event_id", "event_id", eventID, "error", err)
return fmt.Errorf("failed to parse event_id: %w", err)
}
// eventIDInt, err := strconv.ParseInt(eventID, 10, 64)
// if err != nil {
// s.logger.Error("Failed to parse event_id", "event_id", eventID, "error", err)
// return fmt.Errorf("failed to parse event_id: %w", err)
// }
halfScore := ""
if r.Scores.FirstHalf.Home != "" {
halfScore = fmt.Sprintf("%s-%s", r.Scores.FirstHalf.Home, r.Scores.FirstHalf.Away)
}
// halfScore := ""
// if r.Scores.FirstHalf.Home != "" {
// halfScore = fmt.Sprintf("%s-%s", r.Scores.FirstHalf.Home, r.Scores.FirstHalf.Away)
// }
result := domain.Result{
EventID: eventIDInt,
Status: domain.OUTCOME_STATUS_PENDING,
Score: r.SS,
FullTimeScore: r.SS,
HalfTimeScore: halfScore,
SS: r.SS,
Scores: make(map[string]domain.Score),
}
for k, v := range map[string]domain.Score{
"1": r.Scores.FirstHalf,
"2": r.Scores.SecondHalf,
} {
result.Scores[k] = domain.Score{
Home: v.Home,
Away: v.Away,
}
}
// result := domain.Result{
// EventID: eventIDInt,
// Status: domain.OUTCOME_STATUS_PENDING,
// Score: r.SS,
// FullTimeScore: r.SS,
// HalfTimeScore: halfScore,
// SS: r.SS,
// Scores: make(map[string]domain.Score),
// }
// for k, v := range map[string]domain.Score{
// "1": r.Scores.FirstHalf,
// "2": r.Scores.SecondHalf,
// } {
// result.Scores[k] = domain.Score{
// Home: v.Home,
// Away: v.Away,
// }
// }
return s.repo.InsertResult(ctx, result)
}
// return s.repo.InsertResult(ctx, result)
// }
func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, 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)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
@ -183,7 +186,7 @@ func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID int6
return domain.CreateResult{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var resultResp domain.ResultResponse
var resultResp domain.BaseResultResponse
if err := json.NewDecoder(resp.Body).Decode(&resultResp); err != nil {
s.logger.Error("Failed to decode result", "event_id", eventID, "error", err)
return domain.CreateResult{}, err
@ -194,17 +197,47 @@ func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID int6
return domain.CreateResult{}, fmt.Errorf("invalid API response")
}
result := resultResp.Results[0]
var result domain.CreateResult
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
}
default:
s.logger.Error("Unsupported sport", "sport", sportID)
return domain.CreateResult{}, fmt.Errorf("unsupported sport: %v", sportID)
}
return result, nil
}
func (s *Service) parseFootball(resultRes json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) {
var fbResp domain.FootballResultResponse
if err := json.Unmarshal(resultRes, &fbResp); err != nil {
s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err)
return domain.CreateResult{}, err
}
result := fbResp
if result.TimeStatus != "3" {
s.logger.Warn("Match not yet completed", "event_id", eventID)
return domain.CreateResult{}, fmt.Errorf("match not yet completed")
}
finalScore := parseScore(result.SS)
firstHalfScore := parseScore(fmt.Sprintf("%s-%s", result.Scores.FirstHalf.Home, result.Scores.FirstHalf.Away))
finalScore := parseSS(result.SS)
firstHalfScore := parseSS(fmt.Sprintf("%s-%s", result.Scores.FirstHalf.Home, result.Scores.FirstHalf.Away))
corners := parseStats(result.Stats.Corners)
status, err := s.evaluateOutcome(outcome, finalScore, firstHalfScore, corners, result.Events)
status, err := s.evaluateFootballOutcome(outcome, finalScore, firstHalfScore, corners, result.Events)
if err != nil {
s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err)
return domain.CreateResult{}, err
@ -218,9 +251,44 @@ func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID int6
Status: status,
Score: result.SS,
}, nil
}
func parseScore(scoreStr string) struct{ Home, Away int } {
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 football result", "event_id", eventID, "error", err)
return domain.CreateResult{}, err
}
if basketBallRes.TimeStatus != "3" {
s.logger.Warn("Match not yet completed", "event_id", eventID)
return domain.CreateResult{}, fmt.Errorf("match not yet completed")
}
status, err := s.evaluateBasketballOutcome(outcome, basketBallRes)
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,
Score: basketBallRes.SS,
}, nil
}
func parseScore(home string, away string) struct{ Home, Away int } {
homeVal, _ := strconv.Atoi(strings.TrimSpace(home))
awaVal, _ := strconv.Atoi(strings.TrimSpace(away))
return struct{ Home, Away int }{Home: homeVal, Away: awaVal}
}
func parseSS(scoreStr string) struct{ Home, Away int } {
parts := strings.Split(scoreStr, "-")
if len(parts) != 2 {
return struct{ Home, Away int }{0, 0}
@ -240,37 +308,38 @@ func parseStats(stats []string) struct{ Home, Away int } {
}
// evaluateOutcome determines the outcome status based on market type and odd
func (s *Service) evaluateOutcome(outcome domain.BetOutcome, finalScore, firstHalfScore struct{ Home, Away int }, corners struct{ Home, Away int }, events []map[string]string) (domain.OutcomeStatus, error) {
marketConfig := supportedMarkets["football"]
if !marketConfig.MarketTypes[outcome.MarketName] {
func (s *Service) evaluateFootballOutcome(outcome domain.BetOutcome, finalScore, firstHalfScore struct{ Home, Away int }, corners struct{ Home, Away int }, events []map[string]string) (domain.OutcomeStatus, error) {
marketConfig := domain.SupportedMarkets["football"]
if !marketConfig.MarketTypes[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)
}
switch outcome.MarketName {
case "full_time_result":
switch outcome.MarketID {
case int64(domain.FOOTBALL_FULL_TIME_RESULT):
return evaluateFullTimeResult(outcome, finalScore)
case "goals_over_under":
case int64(domain.FOOTBALL_GOALS_OVER_UNDER):
return evaluateGoalsOverUnder(outcome, finalScore)
case "correct_score":
case int64(domain.FOOTBALL_CORRECT_SCORE):
return evaluateCorrectScore(outcome, finalScore)
case "half_time_result":
case int64(domain.FOOTBALL_HALF_TIME_RESULT):
return evaluateHalfTimeResult(outcome, firstHalfScore)
case "asian_handicap":
case int64(domain.FOOTBALL_ASIAN_HANDICAP):
return evaluateAsianHandicap(outcome, finalScore)
case "goal_line":
case int64(domain.FOOTBALL_GOAL_LINE):
return evaluateGoalLine(outcome, finalScore)
case "1st_half_asian_handicap":
case int64(domain.FOOTBALL_FIRST_HALF_ASIAN_HANDICAP):
return evaluateAsianHandicap(outcome, firstHalfScore)
case "1st_half_goal_line":
case int64(domain.FOOTBALL_FIRST_HALF_GOAL_LINE):
return evaluateGoalLine(outcome, firstHalfScore)
case "first_team_to_score":
case int64(domain.FOOTBALL_FIRST_TEAM_TO_SCORE):
return evaluateFirstTeamToScore(outcome, events)
case "goals_odd_even":
case int64(domain.FOOTBALL_GOALS_ODD_EVEN):
return evaluateGoalsOddEven(outcome, finalScore)
case "double_chance":
case int64(domain.FOOTBALL_DOUBLE_CHANCE):
return evaluateDoubleChance(outcome, finalScore)
case "draw_no_bet":
case int64(domain.FOOTBALL_DRAW_NO_BET):
return evaluateDrawNoBet(outcome, finalScore)
default:
s.logger.Warn("Market type not implemented", "market_name", outcome.MarketName)
@ -278,159 +347,71 @@ func (s *Service) evaluateOutcome(outcome domain.BetOutcome, finalScore, firstHa
}
}
func evaluateFullTimeResult(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 "Draw":
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
func (s *Service) evaluateBasketballOutcome(outcome domain.BetOutcome, res domain.BasketballResultResponse) (domain.OutcomeStatus, error) {
marketConfig := domain.SupportedMarkets["basketball"]
if !marketConfig.MarketTypes[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)
}
finalScore := parseSS(res.SS)
firstHalfScore := parseScore(res.Scores.FirstHalf.Home, res.Scores.FirstHalf.Away)
secondHalfScore := struct{ Home, Away int }{Home: finalScore.Home - firstHalfScore.Home, Away: finalScore.Away - firstHalfScore.Away}
firstQuarter := parseScore(res.Scores.FirstQuarter.Home, res.Scores.FirstQuarter.Away)
secondQuarter := parseScore(res.Scores.SecondQuarter.Home, res.Scores.SecondQuarter.Away)
thirdQuarter := parseScore(res.Scores.ThirdQuarter.Home, res.Scores.ThirdQuarter.Away)
fourthQuarter := parseScore(res.Scores.FourthQuarter.Home, res.Scores.FourthQuarter.Away)
switch outcome.MarketID {
case int64(domain.BASKETBALL_GAME_LINES):
return evaluateGameLines(outcome, finalScore)
case int64(domain.BASKETBALL_RESULT_AND_BOTH_TEAMS_TO_SCORE_X_POINTS):
return evaluateResultAndBTTSX(outcome, finalScore)
case int64(domain.BASKETBALL_DOUBLE_RESULT):
return evaluateDoubleResult(outcome, firstHalfScore, secondHalfScore)
case int64(domain.BASKETBALL_MATCH_RESULT_AND_TOTAL):
return evaluateResultAndTotal(outcome, finalScore)
case int64(domain.BASKETBALL_MATCH_HANDICAP_AND_TOTAL):
return evaluateHandicapAndTotal(outcome, finalScore)
case int64(domain.BASKETBALL_GAME_TOTAL_ODD_EVEN):
return evaluateGoalsOddEven(outcome, finalScore)
case int64(domain.BASKETBALL_TEAM_TOTALS):
return evaluateGoalsOddEven(outcome, finalScore)
case int64(domain.BASKETBALL_FIRST_HALF):
return evaluateGameLines(outcome, firstHalfScore)
case int64(domain.BASKETBALL_FIRST_HALF_TEAM_TOTALS):
return evaluateTeamTotal(outcome, firstHalfScore)
case int64(domain.BASKETBALL_FIRST_HALF_HANDICAP_AND_TOTAL):
return evaluateHandicapAndTotal(outcome, firstHalfScore)
case int64(domain.BASKETBALL_FIRST_HALF_BOTH_TEAMS_TO_SCORE_X_POINTS):
return evaluateBTTSX(outcome, firstHalfScore)
case int64(domain.BASKETBALL_FIRST_HALF_DOUBLE_CHANCE):
return evaluateDoubleChance(outcome, firstHalfScore)
case int64(domain.BASKETBALL_FIRST_HALF_TOTAL_ODD_EVEN):
return evaluateGoalsOddEven(outcome, firstHalfScore)
case int64(domain.BASKETBALL_FIRST_HALF_MONEY_LINE_3_WAY):
return evaluateMoneyLine3Way(outcome, firstHalfScore)
case int64(domain.BASKETBALL_HIGHEST_SCORING_HALF):
return evaluateHighestScoringHalf(outcome, firstHalfScore, secondHalfScore)
case int64(domain.BASKETBALL_FIRST_QUARTER):
return evaluateGameLines(outcome, firstQuarter)
case int64(domain.BASKETBALL_FIRST_QUARTER_TEAM_TOTALS):
return evaluateTeamTotal(outcome, firstQuarter)
case int64(domain.BASKETBALL_FIRST_QUARTER_TOTAL_ODD_EVEN):
return evaluateGoalsOddEven(outcome, firstQuarter)
case int64(domain.BASKETBALL_FIRST_QUARTER_HANDICAP_AND_TOTAL):
return evaluateHandicapAndTotal(outcome, firstQuarter)
case int64(domain.BASKETBALL_FIRST_QUARTER_DOUBLE_CHANCE):
return evaluateDoubleChance(outcome, firstQuarter)
case int64(domain.BASKETBALL_HIGHEST_SCORING_QUARTER):
return evaluateHighestScoringQuarter(outcome, firstQuarter, secondQuarter, thirdQuarter, fourthQuarter)
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName)
s.logger.Warn("Market type not implemented", "market_name", outcome.MarketName)
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("market type not implemented: %s", outcome.MarketName)
}
}
func evaluateGoalsOverUnder(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalGoals := float64(score.Home + score.Away)
threshold, err := strconv.ParseFloat(outcome.OddName, 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
}
if outcome.OddHeader == "Over" {
if totalGoals > threshold {
return domain.OUTCOME_STATUS_WIN, nil
} else if totalGoals == threshold {
return domain.OUTCOME_STATUS_VOID, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else if outcome.OddHeader == "Under" {
if totalGoals < threshold {
return domain.OUTCOME_STATUS_WIN, nil
} else if totalGoals == threshold {
return domain.OUTCOME_STATUS_VOID, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
func evaluateCorrectScore(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
expectedScore := fmt.Sprintf("%d-%d", score.Home, score.Away)
if outcome.OddName == expectedScore {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}
func evaluateHalfTimeResult(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
return evaluateFullTimeResult(outcome, score)
}
func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
handicap, err := strconv.ParseFloat(outcome.OddHandicap, 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid handicap: %s", outcome.OddHandicap)
}
adjustedHomeScore := float64(score.Home)
adjustedAwayScore := float64(score.Away)
if outcome.OddHeader == "1" { // Home team
adjustedHomeScore += handicap
} else if outcome.OddHeader == "2" { // Away team
adjustedAwayScore += handicap
} else {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
if adjustedHomeScore > adjustedAwayScore {
if outcome.OddHeader == "1" {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else if adjustedHomeScore < adjustedAwayScore {
if outcome.OddHeader == "2" {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}
return domain.OUTCOME_STATUS_VOID, nil
}
func evaluateGoalLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
return evaluateGoalsOverUnder(outcome, score)
}
func evaluateFirstTeamToScore(outcome domain.BetOutcome, events []map[string]string) (domain.OutcomeStatus, error) {
for _, event := range events {
if strings.Contains(event["text"], "1st Goal") {
if strings.Contains(event["text"], outcome.HomeTeamName) && outcome.OddName == "1" {
return domain.OUTCOME_STATUS_WIN, nil
} else if strings.Contains(event["text"], outcome.AwayTeamName) && outcome.OddName == "2" {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}
}
return domain.OUTCOME_STATUS_VOID, nil // No goals scored
}
func evaluateGoalsOddEven(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalGoals := score.Home + score.Away
isOdd := totalGoals%2 == 1
if outcome.OddName == "Odd" && isOdd {
return domain.OUTCOME_STATUS_WIN, nil
} else if outcome.OddName == "Even" && !isOdd {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}
func evaluateDoubleChance(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
isHomeWin := score.Home > score.Away
isDraw := score.Home == score.Away
isAwayWin := score.Away > score.Home
switch outcome.OddName {
case "1 or Draw":
if isHomeWin || isDraw {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
case "Draw or 2":
if isDraw || isAwayWin {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
case "1 or 2":
if isHomeWin || isAwayWin {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName)
}
}
func evaluateDrawNoBet(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
if score.Home == score.Away {
return domain.OUTCOME_STATUS_VOID, nil
}
if outcome.OddName == "1" && score.Home > score.Away {
return domain.OUTCOME_STATUS_WIN, nil
} else if outcome.OddName == "2" && score.Away > score.Home {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}

View File

@ -0,0 +1 @@
package result

View File

@ -2,7 +2,6 @@ package httpserver
import (
// "context"
"context"
"log"
@ -67,6 +66,18 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
// }
// },
// },
{
spec: "0 */15 * * * *",
task: func() {
log.Println("Fetching results for upcoming events...")
if err := resultService.FetchAndProcessResults(context.Background()); err != nil {
log.Printf("Failed to process result: %v", err)
} else {
log.Printf("Successfully processed all outcomes")
}
},
},
}
for _, job := range schedule {

View File

@ -2,6 +2,7 @@ package handlers
import (
"encoding/json"
"log/slog"
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
@ -169,10 +170,18 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error {
parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32)
totalOdds = totalOdds * float32(parsedOdd)
sportID, err := strconv.ParseInt(event.SportID, 10, 64)
if err != nil {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid sport id", nil, nil)
}
h.logger.Info("Create Bet", slog.Int64("sportId", sportID))
outcomes = append(outcomes, domain.CreateBetOutcome{
EventID: outcome.EventID,
OddID: outcome.OddID,
MarketID: outcome.MarketID,
SportID: sportID,
HomeTeamName: event.HomeTeam,
AwayTeamName: event.AwayTeam,
MarketName: odds.MarketName,