From b4e20a274dc50da3012c9f80150c77e5b1177d57 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Sat, 3 May 2025 20:22:57 +0300 Subject: [PATCH] basketball and fixes --- db/migrations/000001_fortune.up.sql | 1 + db/query/bet.sql | 9 +- db/query/events.sql | 5 +- docs/docs.go | 59 +- docs/swagger.json | 59 +- docs/swagger.yaml | 43 +- gen/db/bet.sql.go | 43 ++ gen/db/copyfrom.go | 3 +- gen/db/events.sql.go | 10 + gen/db/models.go | 1 + gen/db/result.sql.go | 3 +- internal/domain/bet.go | 3 +- internal/domain/league.go | 30 + internal/domain/result.go | 175 ++++-- internal/domain/sport.go | 34 ++ internal/domain/sportmarket.go | 147 +++++ internal/repository/bet.go | 61 +- internal/repository/event.go | 8 + internal/services/event/service.go | 26 +- internal/services/odds/service.go | 4 +- internal/services/result/eval.go | 614 ++++++++++++++++++++ internal/services/result/service.go | 541 +++++++++-------- internal/services/result/service_test.go | 1 + internal/web_server/cron.go | 13 +- internal/web_server/handlers/bet_handler.go | 9 + 25 files changed, 1528 insertions(+), 374 deletions(-) create mode 100644 internal/domain/league.go create mode 100644 internal/domain/sport.go create mode 100644 internal/domain/sportmarket.go create mode 100644 internal/services/result/eval.go create mode 100644 internal/services/result/service_test.go diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index c5eb97a..3cf6d3e 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -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, diff --git a/db/query/bet.sql b/db/query/bet.sql index 9ebbb30..42db5a7 100644 --- a/db/query/bet.sql +++ b/db/query/bet.sql @@ -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, diff --git a/db/query/events.sql b/db/query/events.sql index 09ecdf5..4109c44 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -232,4 +232,7 @@ UPDATE events SET score = $1, status = $2, fetched_at = NOW() -WHERE id = $3; \ No newline at end of file +WHERE id = $3; +-- name: DeleteEvent :exec +DELETE FROM events +WHERE id = $1; \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 22547e0..6aaf02d 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -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": { diff --git a/docs/swagger.json b/docs/swagger.json index 04fa088..ab1ec85 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -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": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 5c7a188..043dc82 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -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: diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index 11a44ad..e236690 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -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 diff --git a/gen/db/copyfrom.go b/gen/db/copyfrom.go index 54dbb8b..900af58 100644 --- a/gen/db/copyfrom.go +++ b/gen/db/copyfrom.go @@ -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. diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index ed9dedf..94315a7 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -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, diff --git a/gen/db/models.go b/gen/db/models.go index 42c5185..0cc5956 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -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"` diff --git a/gen/db/result.sql.go b/gen/db/result.sql.go index 0659af6..cb3fdd8 100644 --- a/gen/db/result.sql.go +++ b/gen/db/result.sql.go @@ -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, diff --git a/internal/domain/bet.go b/internal/domain/bet.go index af9f03b..6e2d81a 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -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 } - diff --git a/internal/domain/league.go b/internal/domain/league.go new file mode 100644 index 0000000..f05914a --- /dev/null +++ b/internal/domain/league.go @@ -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 + +} diff --git a/internal/domain/result.go b/internal/domain/result.go index 2c625f3..ded6edd 100644 --- a/internal/domain/result.go +++ b/internal/domain/result.go @@ -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 ) diff --git a/internal/domain/sport.go b/internal/domain/sport.go new file mode 100644 index 0000000..fadcfe9 --- /dev/null +++ b/internal/domain/sport.go @@ -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 +) diff --git a/internal/domain/sportmarket.go b/internal/domain/sportmarket.go new file mode 100644 index 0000000..7d04741 --- /dev/null +++ b/internal/domain/sportmarket.go @@ -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, + }, + }, +} diff --git a/internal/repository/bet.go b/internal/repository/bet.go index c19db94..5ff779f 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -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), diff --git a/internal/repository/event.go b/internal/repository/event.go index 2e2824a..630cd39 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -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 +} diff --git a/internal/services/event/service.go b/internal/services/event/service.go index d5e5aa3..3a98371 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -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) } } diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index f509836..81f5bd7 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -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) } diff --git a/internal/services/result/eval.go b/internal/services/result/eval.go new file mode 100644 index 0000000..0e4775f --- /dev/null +++ b/internal/services/result/eval.go @@ -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) + } + +} diff --git a/internal/services/result/service.go b/internal/services/result/service.go index 5d25c60..4c122ea 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -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 -} diff --git a/internal/services/result/service_test.go b/internal/services/result/service_test.go new file mode 100644 index 0000000..2705049 --- /dev/null +++ b/internal/services/result/service_test.go @@ -0,0 +1 @@ +package result diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 74d3c7c..d4edd3c 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -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 { diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index 5019602..268fbb3 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -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,