diff --git a/cmd/main.go b/cmd/main.go index 364cb70..fa4b9fc 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -86,6 +86,7 @@ func main() { virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger) httpserver.StartDataFetchingCrons(eventSvc, oddsSvc, resultSvc) + httpserver.StartTicketCrons(*ticketSvc) app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{ JwtAccessKey: cfg.JwtKey, diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 3cf6d3e..c351619 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -340,15 +340,43 @@ INSERT INTO users ( suspended_at, suspended ) +VALUES ( + 'Test', + 'Admin', + 'test.admin@gmail.com', + '0911111111', + crypt('password123', gen_salt('bf'))::bytea, + 'admin', + TRUE, + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + NULL, + FALSE + ); +INSERT INTO users ( + first_name, + last_name, + email, + phone_number, + password, + role, + email_verified, + phone_verified, + created_at, + updated_at, + suspended_at, + suspended + ) VALUES ( 'Samuel', 'Tariku', 'cybersamt@gmail.com', - NULL, + '0911111111', crypt('password@123', gen_salt('bf'))::bytea, 'super_admin', TRUE, - FALSE, + TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, @@ -372,11 +400,11 @@ VALUES ( 'Kirubel', 'Kibru', 'kirubeljkl679 @gmail.com', - NULL, + '0911111111', crypt('password@123', gen_salt('bf'))::bytea, 'super_admin', TRUE, - FALSE, + TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, diff --git a/db/query/bet.sql b/db/query/bet.sql index aed3aa4..61a3d02 100644 --- a/db/query/bet.sql +++ b/db/query/bet.sql @@ -62,6 +62,10 @@ WHERE branch_id = $1; SELECT * FROM bet_outcomes WHERE event_id = $1; +-- name: GetBetOutcomeByBetID :many +SELECT * +FROM bet_outcomes +WHERE bet_id = $1; -- name: UpdateCashOut :exec UPDATE bets SET cashed_out = $2, @@ -74,9 +78,9 @@ WHERE id = $2 RETURNING *; -- name: UpdateStatus :exec UPDATE bets -SET status = $2, +SET status = $1, updated_at = CURRENT_TIMESTAMP -WHERE id = $1; +WHERE id = $2; -- name: DeleteBet :exec DELETE FROM bets WHERE id = $1; diff --git a/db/query/events.sql b/db/query/events.sql index 4109c44..1e40107 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -196,15 +196,23 @@ FROM events WHERE is_live = false AND status = 'upcoming' AND ( - league_id = $3 - OR $3 IS NULL + league_id = sqlc.narg('league_id') + OR sqlc.narg('league_id') IS NULL ) AND ( - sport_id = $4 - OR $4 IS NULL + sport_id = sqlc.narg('sport_id') + OR sqlc.narg('sport_id') IS NULL + ) + AND ( + start_time < sqlc.narg('last_start_time') + OR sqlc.narg('last_start_time') IS NULL + ) + AND ( + start_time > sqlc.narg('first_start_time') + OR sqlc.narg('first_start_time') IS NULL ) ORDER BY start_time ASC -LIMIT $1 OFFSET $2; +LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- name: GetUpcomingByID :one SELECT id, sport_id, diff --git a/db/query/odds.sql b/db/query/odds.sql index 908a445..9de17b3 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -94,23 +94,17 @@ WHERE market_id = $1 AND fi = $2 AND is_active = true AND source = 'b365api'; - -- name: GetPrematchOddsByUpcomingID :many -SELECT o.event_id, - o.fi, - o.market_type, - o.market_name, - o.market_category, - o.market_id, - o.name, - o.handicap, - o.odds_value, - o.section, - o.category, - o.raw_odds, - o.fetched_at, - o.source, - o.is_active +SELECT o.* +FROM odds o + JOIN events e ON o.fi = e.id +WHERE e.id = $1 + AND e.is_live = false + AND e.status = 'upcoming' + AND o.is_active = true + AND o.source = 'b365api'; +-- name: GetPaginatedPrematchOddsByUpcomingID :many +SELECT o.* FROM odds o JOIN events e ON o.fi = e.id WHERE e.id = $1 @@ -118,4 +112,4 @@ WHERE e.id = $1 AND e.status = 'upcoming' AND o.is_active = true AND o.source = 'b365api' -LIMIT $2 OFFSET $3; \ No newline at end of file +LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 72da21c..2a56dac 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -304,7 +304,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/handlers.BetRes" + "$ref": "#/definitions/domain.BetRes" } } }, @@ -341,7 +341,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handlers.CreateBetReq" + "$ref": "#/definitions/domain.CreateBetReq" } } ], @@ -349,7 +349,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.BetRes" + "$ref": "#/definitions/domain.BetRes" } }, "400": { @@ -393,7 +393,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.BetRes" + "$ref": "#/definitions/domain.BetRes" } }, "400": { @@ -437,7 +437,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.BetRes" + "$ref": "#/definitions/domain.BetRes" } }, "400": { @@ -786,7 +786,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/handlers.BetRes" + "$ref": "#/definitions/domain.BetRes" } } }, @@ -1915,6 +1915,52 @@ const docTemplate = `{ } } }, + "/random/bet": { + "post": { + "description": "Generate a random bet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Generate a random bet", + "parameters": [ + { + "description": "Create Random bet", + "name": "createBet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.RandomBetReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BetRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/referral/settings": { "get": { "security": [ @@ -3386,6 +3432,117 @@ const docTemplate = `{ } } }, + "domain.BetRes": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "example": 100 + }, + "branch_id": { + "type": "integer", + "example": 2 + }, + "cashed_id": { + "type": "string", + "example": "21234" + }, + "cashed_out": { + "type": "boolean", + "example": false + }, + "full_name": { + "type": "string", + "example": "John" + }, + "id": { + "type": "integer", + "example": 1 + }, + "is_shop_bet": { + "type": "boolean", + "example": false + }, + "outcomes": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.BetOutcome" + } + }, + "phone_number": { + "type": "string", + "example": "1234567890" + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/domain.OutcomeStatus" + } + ], + "example": 1 + }, + "total_odds": { + "type": "number", + "example": 4.22 + }, + "user_id": { + "type": "integer", + "example": 2 + } + } + }, + "domain.CreateBetOutcomeReq": { + "type": "object", + "properties": { + "event_id": { + "type": "integer", + "example": 1 + }, + "market_id": { + "type": "integer", + "example": 1 + }, + "odd_id": { + "type": "integer", + "example": 1 + } + } + }, + "domain.CreateBetReq": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "example": 100 + }, + "branch_id": { + "type": "integer", + "example": 1 + }, + "full_name": { + "type": "string", + "example": "John" + }, + "outcomes": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.CreateBetOutcomeReq" + } + }, + "phone_number": { + "type": "string", + "example": "1234567890" + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/domain.OutcomeStatus" + } + ], + "example": 1 + } + } + }, "domain.Odd": { "type": "object", "properties": { @@ -3501,6 +3658,15 @@ const docTemplate = `{ } } }, + "domain.RandomBetReq": { + "type": "object", + "properties": { + "branch_id": { + "type": "integer", + "example": 1 + } + } + }, "domain.RawOddsByMarketID": { "type": "object", "properties": { @@ -3757,65 +3923,6 @@ const docTemplate = `{ } } }, - "handlers.BetRes": { - "type": "object", - "properties": { - "amount": { - "type": "number", - "example": 100 - }, - "branch_id": { - "type": "integer", - "example": 2 - }, - "cashed_id": { - "type": "string", - "example": "21234" - }, - "cashed_out": { - "type": "boolean", - "example": false - }, - "full_name": { - "type": "string", - "example": "John" - }, - "id": { - "type": "integer", - "example": 1 - }, - "is_shop_bet": { - "type": "boolean", - "example": false - }, - "outcomes": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.BetOutcome" - } - }, - "phone_number": { - "type": "string", - "example": "1234567890" - }, - "status": { - "allOf": [ - { - "$ref": "#/definitions/domain.OutcomeStatus" - } - ], - "example": 1 - }, - "total_odds": { - "type": "number", - "example": 4.22 - }, - "user_id": { - "type": "integer", - "example": 2 - } - } - }, "handlers.BranchDetailRes": { "type": "object", "properties": { @@ -3977,58 +4084,6 @@ const docTemplate = `{ } } }, - "handlers.CreateBetOutcomeReq": { - "type": "object", - "properties": { - "event_id": { - "type": "integer", - "example": 1 - }, - "market_id": { - "type": "integer", - "example": 1 - }, - "odd_id": { - "type": "integer", - "example": 1 - } - } - }, - "handlers.CreateBetReq": { - "type": "object", - "properties": { - "amount": { - "type": "number", - "example": 100 - }, - "branch_id": { - "type": "integer", - "example": 1 - }, - "full_name": { - "type": "string", - "example": "John" - }, - "outcomes": { - "type": "array", - "items": { - "$ref": "#/definitions/handlers.CreateBetOutcomeReq" - } - }, - "phone_number": { - "type": "string", - "example": "1234567890" - }, - "status": { - "allOf": [ - { - "$ref": "#/definitions/domain.OutcomeStatus" - } - ], - "example": 1 - } - } - }, "handlers.CreateBranchOperationReq": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 123e78f..66fa0cd 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -296,7 +296,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/handlers.BetRes" + "$ref": "#/definitions/domain.BetRes" } } }, @@ -333,7 +333,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handlers.CreateBetReq" + "$ref": "#/definitions/domain.CreateBetReq" } } ], @@ -341,7 +341,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.BetRes" + "$ref": "#/definitions/domain.BetRes" } }, "400": { @@ -385,7 +385,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.BetRes" + "$ref": "#/definitions/domain.BetRes" } }, "400": { @@ -429,7 +429,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.BetRes" + "$ref": "#/definitions/domain.BetRes" } }, "400": { @@ -778,7 +778,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/handlers.BetRes" + "$ref": "#/definitions/domain.BetRes" } } }, @@ -1907,6 +1907,52 @@ } } }, + "/random/bet": { + "post": { + "description": "Generate a random bet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Generate a random bet", + "parameters": [ + { + "description": "Create Random bet", + "name": "createBet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.RandomBetReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BetRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/referral/settings": { "get": { "security": [ @@ -3378,6 +3424,117 @@ } } }, + "domain.BetRes": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "example": 100 + }, + "branch_id": { + "type": "integer", + "example": 2 + }, + "cashed_id": { + "type": "string", + "example": "21234" + }, + "cashed_out": { + "type": "boolean", + "example": false + }, + "full_name": { + "type": "string", + "example": "John" + }, + "id": { + "type": "integer", + "example": 1 + }, + "is_shop_bet": { + "type": "boolean", + "example": false + }, + "outcomes": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.BetOutcome" + } + }, + "phone_number": { + "type": "string", + "example": "1234567890" + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/domain.OutcomeStatus" + } + ], + "example": 1 + }, + "total_odds": { + "type": "number", + "example": 4.22 + }, + "user_id": { + "type": "integer", + "example": 2 + } + } + }, + "domain.CreateBetOutcomeReq": { + "type": "object", + "properties": { + "event_id": { + "type": "integer", + "example": 1 + }, + "market_id": { + "type": "integer", + "example": 1 + }, + "odd_id": { + "type": "integer", + "example": 1 + } + } + }, + "domain.CreateBetReq": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "example": 100 + }, + "branch_id": { + "type": "integer", + "example": 1 + }, + "full_name": { + "type": "string", + "example": "John" + }, + "outcomes": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.CreateBetOutcomeReq" + } + }, + "phone_number": { + "type": "string", + "example": "1234567890" + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/domain.OutcomeStatus" + } + ], + "example": 1 + } + } + }, "domain.Odd": { "type": "object", "properties": { @@ -3493,6 +3650,15 @@ } } }, + "domain.RandomBetReq": { + "type": "object", + "properties": { + "branch_id": { + "type": "integer", + "example": 1 + } + } + }, "domain.RawOddsByMarketID": { "type": "object", "properties": { @@ -3749,65 +3915,6 @@ } } }, - "handlers.BetRes": { - "type": "object", - "properties": { - "amount": { - "type": "number", - "example": 100 - }, - "branch_id": { - "type": "integer", - "example": 2 - }, - "cashed_id": { - "type": "string", - "example": "21234" - }, - "cashed_out": { - "type": "boolean", - "example": false - }, - "full_name": { - "type": "string", - "example": "John" - }, - "id": { - "type": "integer", - "example": 1 - }, - "is_shop_bet": { - "type": "boolean", - "example": false - }, - "outcomes": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.BetOutcome" - } - }, - "phone_number": { - "type": "string", - "example": "1234567890" - }, - "status": { - "allOf": [ - { - "$ref": "#/definitions/domain.OutcomeStatus" - } - ], - "example": 1 - }, - "total_odds": { - "type": "number", - "example": 4.22 - }, - "user_id": { - "type": "integer", - "example": 2 - } - } - }, "handlers.BranchDetailRes": { "type": "object", "properties": { @@ -3969,58 +4076,6 @@ } } }, - "handlers.CreateBetOutcomeReq": { - "type": "object", - "properties": { - "event_id": { - "type": "integer", - "example": 1 - }, - "market_id": { - "type": "integer", - "example": 1 - }, - "odd_id": { - "type": "integer", - "example": 1 - } - } - }, - "handlers.CreateBetReq": { - "type": "object", - "properties": { - "amount": { - "type": "number", - "example": 100 - }, - "branch_id": { - "type": "integer", - "example": 1 - }, - "full_name": { - "type": "string", - "example": "John" - }, - "outcomes": { - "type": "array", - "items": { - "$ref": "#/definitions/handlers.CreateBetOutcomeReq" - } - }, - "phone_number": { - "type": "string", - "example": "1234567890" - }, - "status": { - "allOf": [ - { - "$ref": "#/definitions/domain.OutcomeStatus" - } - ], - "example": 1 - } - } - }, "handlers.CreateBranchOperationReq": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e76a984..fee0fad 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -48,6 +48,82 @@ definitions: - $ref: '#/definitions/domain.OutcomeStatus' example: 1 type: object + domain.BetRes: + properties: + amount: + example: 100 + type: number + branch_id: + example: 2 + type: integer + cashed_id: + example: "21234" + type: string + cashed_out: + example: false + type: boolean + full_name: + example: John + type: string + id: + example: 1 + type: integer + is_shop_bet: + example: false + type: boolean + outcomes: + items: + $ref: '#/definitions/domain.BetOutcome' + type: array + phone_number: + example: "1234567890" + type: string + status: + allOf: + - $ref: '#/definitions/domain.OutcomeStatus' + example: 1 + total_odds: + example: 4.22 + type: number + user_id: + example: 2 + type: integer + type: object + domain.CreateBetOutcomeReq: + properties: + event_id: + example: 1 + type: integer + market_id: + example: 1 + type: integer + odd_id: + example: 1 + type: integer + type: object + domain.CreateBetReq: + properties: + amount: + example: 100 + type: number + branch_id: + example: 1 + type: integer + full_name: + example: John + type: string + outcomes: + items: + $ref: '#/definitions/domain.CreateBetOutcomeReq' + type: array + phone_number: + example: "1234567890" + type: string + status: + allOf: + - $ref: '#/definitions/domain.OutcomeStatus' + example: 1 + type: object domain.Odd: properties: category: @@ -130,6 +206,12 @@ definitions: description: BET, WIN, REFUND, JACKPOT_WIN type: string type: object + domain.RandomBetReq: + properties: + branch_id: + example: 1 + type: integer + type: object domain.RawOddsByMarketID: properties: fetched_at: @@ -309,47 +391,6 @@ definitions: updated_at: type: string type: object - handlers.BetRes: - properties: - amount: - example: 100 - type: number - branch_id: - example: 2 - type: integer - cashed_id: - example: "21234" - type: string - cashed_out: - example: false - type: boolean - full_name: - example: John - type: string - id: - example: 1 - type: integer - is_shop_bet: - example: false - type: boolean - outcomes: - items: - $ref: '#/definitions/domain.BetOutcome' - type: array - phone_number: - example: "1234567890" - type: string - status: - allOf: - - $ref: '#/definitions/domain.OutcomeStatus' - example: 1 - total_odds: - example: 4.22 - type: number - user_id: - example: 2 - type: integer - type: object handlers.BranchDetailRes: properties: branch_manager_id: @@ -465,41 +506,6 @@ definitions: example: "1234567890" type: string type: object - handlers.CreateBetOutcomeReq: - properties: - event_id: - example: 1 - type: integer - market_id: - example: 1 - type: integer - odd_id: - example: 1 - type: integer - type: object - handlers.CreateBetReq: - properties: - amount: - example: 100 - type: number - branch_id: - example: 1 - type: integer - full_name: - example: John - type: string - outcomes: - items: - $ref: '#/definitions/handlers.CreateBetOutcomeReq' - type: array - phone_number: - example: "1234567890" - type: string - status: - allOf: - - $ref: '#/definitions/domain.OutcomeStatus' - example: 1 - type: object handlers.CreateBranchOperationReq: properties: branch_id: @@ -1320,7 +1326,7 @@ paths: description: OK schema: items: - $ref: '#/definitions/handlers.BetRes' + $ref: '#/definitions/domain.BetRes' type: array "400": description: Bad Request @@ -1343,14 +1349,14 @@ paths: name: createBet required: true schema: - $ref: '#/definitions/handlers.CreateBetReq' + $ref: '#/definitions/domain.CreateBetReq' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/handlers.BetRes' + $ref: '#/definitions/domain.BetRes' "400": description: Bad Request schema: @@ -1407,7 +1413,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/handlers.BetRes' + $ref: '#/definitions/domain.BetRes' "400": description: Bad Request schema: @@ -1470,7 +1476,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/handlers.BetRes' + $ref: '#/definitions/domain.BetRes' "400": description: Bad Request schema: @@ -1639,7 +1645,7 @@ paths: description: OK schema: items: - $ref: '#/definitions/handlers.BetRes' + $ref: '#/definitions/domain.BetRes' type: array "400": description: Bad Request @@ -2385,6 +2391,36 @@ paths: summary: Retrieve raw odds by Market ID tags: - prematch + /random/bet: + post: + consumes: + - application/json + description: Generate a random bet + parameters: + - description: Create Random bet + in: body + name: createBet + required: true + schema: + $ref: '#/definitions/domain.RandomBetReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.BetRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Generate a random bet + tags: + - bet /referral/settings: get: consumes: diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index 0f10df6..823fb43 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -243,6 +243,48 @@ func (q *Queries) GetBetByID(ctx context.Context, id int64) (BetWithOutcome, err return i, err } +const GetBetOutcomeByBetID = `-- name: GetBetOutcomeByBetID :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 bet_id = $1 +` + +func (q *Queries) GetBetOutcomeByBetID(ctx context.Context, betID int64) ([]BetOutcome, error) { + rows, err := q.db.Query(ctx, GetBetOutcomeByBetID, betID) + 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 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 @@ -339,17 +381,17 @@ func (q *Queries) UpdateCashOut(ctx context.Context, arg UpdateCashOutParams) er const UpdateStatus = `-- name: UpdateStatus :exec UPDATE bets -SET status = $2, +SET status = $1, updated_at = CURRENT_TIMESTAMP -WHERE id = $1 +WHERE id = $2 ` type UpdateStatusParams struct { - ID int64 `json:"id"` Status int32 `json:"status"` + ID int64 `json:"id"` } func (q *Queries) UpdateStatus(ctx context.Context, arg UpdateStatusParams) error { - _, err := q.db.Exec(ctx, UpdateStatus, arg.ID, arg.Status) + _, err := q.db.Exec(ctx, UpdateStatus, arg.Status, arg.ID) return err } diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index 94315a7..6c1e083 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -201,22 +201,32 @@ FROM events WHERE is_live = false AND status = 'upcoming' AND ( - league_id = $3 + league_id = $1 + OR $1 IS NULL + ) + AND ( + sport_id = $2 + OR $2 IS NULL + ) + AND ( + start_time < $3 OR $3 IS NULL ) AND ( - sport_id = $4 + start_time > $4 OR $4 IS NULL ) ORDER BY start_time ASC -LIMIT $1 OFFSET $2 +LIMIT $6 OFFSET $5 ` type GetPaginatedUpcomingEventsParams struct { - Limit int32 `json:"limit"` - Offset int32 `json:"offset"` - LeagueID pgtype.Text `json:"league_id"` - SportID pgtype.Text `json:"sport_id"` + LeagueID pgtype.Text `json:"league_id"` + SportID pgtype.Text `json:"sport_id"` + LastStartTime pgtype.Timestamp `json:"last_start_time"` + FirstStartTime pgtype.Timestamp `json:"first_start_time"` + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` } type GetPaginatedUpcomingEventsRow struct { @@ -240,10 +250,12 @@ type GetPaginatedUpcomingEventsRow struct { func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginatedUpcomingEventsParams) ([]GetPaginatedUpcomingEventsRow, error) { rows, err := q.db.Query(ctx, GetPaginatedUpcomingEvents, - arg.Limit, - arg.Offset, arg.LeagueID, arg.SportID, + arg.LastStartTime, + arg.FirstStartTime, + arg.Offset, + arg.Limit, ) if err != nil { return nil, err diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index 3f920f4..3d92299 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -86,6 +86,61 @@ func (q *Queries) GetALLPrematchOdds(ctx context.Context) ([]GetALLPrematchOddsR return items, nil } +const GetPaginatedPrematchOddsByUpcomingID = `-- name: GetPaginatedPrematchOddsByUpcomingID :many +SELECT o.id, o.event_id, o.fi, o.market_type, o.market_name, o.market_category, o.market_id, o.name, o.handicap, o.odds_value, o.section, o.category, o.raw_odds, o.fetched_at, o.source, o.is_active +FROM odds o + JOIN events e ON o.fi = e.id +WHERE e.id = $1 + AND e.is_live = false + AND e.status = 'upcoming' + AND o.is_active = true + AND o.source = 'b365api' +LIMIT $3 OFFSET $2 +` + +type GetPaginatedPrematchOddsByUpcomingIDParams struct { + ID string `json:"id"` + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` +} + +func (q *Queries) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, arg GetPaginatedPrematchOddsByUpcomingIDParams) ([]Odd, error) { + rows, err := q.db.Query(ctx, GetPaginatedPrematchOddsByUpcomingID, arg.ID, arg.Offset, arg.Limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Odd + for rows.Next() { + var i Odd + if err := rows.Scan( + &i.ID, + &i.EventID, + &i.Fi, + &i.MarketType, + &i.MarketName, + &i.MarketCategory, + &i.MarketID, + &i.Name, + &i.Handicap, + &i.OddsValue, + &i.Section, + &i.Category, + &i.RawOdds, + &i.FetchedAt, + &i.Source, + &i.IsActive, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const GetPrematchOdds = `-- name: GetPrematchOdds :many SELECT event_id, fi, @@ -162,21 +217,7 @@ func (q *Queries) GetPrematchOdds(ctx context.Context) ([]GetPrematchOddsRow, er } const GetPrematchOddsByUpcomingID = `-- name: GetPrematchOddsByUpcomingID :many -SELECT o.event_id, - o.fi, - o.market_type, - o.market_name, - o.market_category, - o.market_id, - o.name, - o.handicap, - o.odds_value, - o.section, - o.category, - o.raw_odds, - o.fetched_at, - o.source, - o.is_active +SELECT o.id, o.event_id, o.fi, o.market_type, o.market_name, o.market_category, o.market_id, o.name, o.handicap, o.odds_value, o.section, o.category, o.raw_odds, o.fetched_at, o.source, o.is_active FROM odds o JOIN events e ON o.fi = e.id WHERE e.id = $1 @@ -184,43 +225,19 @@ WHERE e.id = $1 AND e.status = 'upcoming' AND o.is_active = true AND o.source = 'b365api' -LIMIT $2 OFFSET $3 ` -type GetPrematchOddsByUpcomingIDParams struct { - ID string `json:"id"` - Limit int32 `json:"limit"` - Offset int32 `json:"offset"` -} - -type GetPrematchOddsByUpcomingIDRow struct { - EventID pgtype.Text `json:"event_id"` - Fi pgtype.Text `json:"fi"` - MarketType string `json:"market_type"` - MarketName pgtype.Text `json:"market_name"` - MarketCategory pgtype.Text `json:"market_category"` - MarketID pgtype.Text `json:"market_id"` - Name pgtype.Text `json:"name"` - Handicap pgtype.Text `json:"handicap"` - OddsValue pgtype.Float8 `json:"odds_value"` - Section string `json:"section"` - Category pgtype.Text `json:"category"` - RawOdds []byte `json:"raw_odds"` - FetchedAt pgtype.Timestamp `json:"fetched_at"` - Source pgtype.Text `json:"source"` - IsActive pgtype.Bool `json:"is_active"` -} - -func (q *Queries) GetPrematchOddsByUpcomingID(ctx context.Context, arg GetPrematchOddsByUpcomingIDParams) ([]GetPrematchOddsByUpcomingIDRow, error) { - rows, err := q.db.Query(ctx, GetPrematchOddsByUpcomingID, arg.ID, arg.Limit, arg.Offset) +func (q *Queries) GetPrematchOddsByUpcomingID(ctx context.Context, id string) ([]Odd, error) { + rows, err := q.db.Query(ctx, GetPrematchOddsByUpcomingID, id) if err != nil { return nil, err } defer rows.Close() - var items []GetPrematchOddsByUpcomingIDRow + var items []Odd for rows.Next() { - var i GetPrematchOddsByUpcomingIDRow + var i Odd if err := rows.Scan( + &i.ID, &i.EventID, &i.Fi, &i.MarketType, diff --git a/internal/domain/bet.go b/internal/domain/bet.go index 93392cf..e8f4ee2 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -97,7 +97,7 @@ type CreateBetReq struct { } type RandomBetReq struct { - BranchID int64 `json:"branch_id,omitempty" example:"1"` + BranchID int64 `json:"branch_id" validate:"required" example:"1"` } type CreateBetRes struct { diff --git a/internal/domain/common.go b/internal/domain/common.go index f5969d9..14323a4 100644 --- a/internal/domain/common.go +++ b/internal/domain/common.go @@ -1,6 +1,9 @@ package domain -import "fmt" +import ( + "fmt" + "time" +) type ValidInt64 struct { Value int64 @@ -11,6 +14,10 @@ type ValidString struct { Value string Valid bool } +type ValidTime struct { + Value time.Time + Valid bool +} type ValidBool struct { Value bool Valid bool diff --git a/internal/domain/league.go b/internal/domain/league.go index f05914a..8f63445 100644 --- a/internal/domain/league.go +++ b/internal/domain/league.go @@ -12,9 +12,8 @@ var SupportedLeagues = []int64{ 10041957, //UEFA Europa League 10079560, //UEFA Conference League 10047168, // US MLS - + 10044469, // Ethiopian Premier League 10050282, //UEFA Nations League - 10040795, //EuroLeague 10043156, //England FA Cup 10042103, //France Cup @@ -26,5 +25,12 @@ var SupportedLeagues = []int64{ // Basketball 173998768, //NBA + 10041830, //NBA + + // Ice Hockey + 10037477, //NHL + 10037447, //AHL + 10069385, //IIHF World Championship + 10040795, //EuroLeague } diff --git a/internal/domain/oddres.go b/internal/domain/oddres.go index 8c2707d..48540f0 100644 --- a/internal/domain/oddres.go +++ b/internal/domain/oddres.go @@ -12,8 +12,9 @@ type OddsSection struct { Sp map[string]OddsMarket `json:"sp"` } +// The Market ID for the json data can be either string / int which is causing problems when UnMarshalling type OddsMarket struct { - ID json.Number `json:"id"` + ID json.RawMessage `json:"id"` Name string `json:"name"` Odds []json.RawMessage `json:"odds"` Header string `json:"header,omitempty"` diff --git a/internal/domain/result.go b/internal/domain/result.go index 44861d2..fc3a621 100644 --- a/internal/domain/result.go +++ b/internal/domain/result.go @@ -43,4 +43,24 @@ const ( OUTCOME_STATUS_LOSS OutcomeStatus = 2 OUTCOME_STATUS_VOID OutcomeStatus = 3 //Give Back OUTCOME_STATUS_HALF OutcomeStatus = 4 //Half Win and Half Given Back + OUTCOME_STATUS_ERROR OutcomeStatus = 5 //Half Win and Half Given Back ) + +func (o *OutcomeStatus) String() string { + switch *o { + case OUTCOME_STATUS_PENDING: + return "PENDING" + case OUTCOME_STATUS_WIN: + return "WIN" + case OUTCOME_STATUS_LOSS: + return "LOSS" + case OUTCOME_STATUS_VOID: + return "VOID" + case OUTCOME_STATUS_HALF: + return "HALF" + case OUTCOME_STATUS_ERROR: + return "ERROR" + default: + return "UNKNOWN" + } +} diff --git a/internal/domain/sportmarket.go b/internal/domain/sportmarket.go index b6fde09..360afee 100644 --- a/internal/domain/sportmarket.go +++ b/internal/domain/sportmarket.go @@ -9,12 +9,15 @@ const ( 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 diff --git a/internal/repository/bet.go b/internal/repository/bet.go index 6788a27..81a501c 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -2,6 +2,7 @@ package repository import ( "context" + // "fmt" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" @@ -225,6 +226,19 @@ func (s *Store) GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]do } return result, nil } + +func (s *Store) GetBetOutcomeByBetID(ctx context.Context, betID int64) ([]domain.BetOutcome, error) { + outcomes, err := s.queries.GetBetOutcomeByBetID(ctx, betID) + 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) (domain.BetOutcome, error) { update, err := s.queries.UpdateBetOutcomeStatus(ctx, dbgen.UpdateBetOutcomeStatusParams{ Status: int32(status), diff --git a/internal/repository/event.go b/internal/repository/event.go index 630cd39..895a963 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -117,7 +117,8 @@ func (s *Store) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.Upcoming return upcomingEvents, nil } -func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, offset int32, leagueID domain.ValidString, sportID domain.ValidString) ([]domain.UpcomingEvent, int64, error) { +func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit domain.ValidInt64, offset domain.ValidInt64, leagueID domain.ValidString, sportID domain.ValidString, firstStartTime domain.ValidTime, lastStartTime domain.ValidTime) ([]domain.UpcomingEvent, int64, error) { + events, err := s.queries.GetPaginatedUpcomingEvents(ctx, dbgen.GetPaginatedUpcomingEventsParams{ LeagueID: pgtype.Text{ String: leagueID.Value, @@ -127,8 +128,22 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, off String: sportID.Value, Valid: sportID.Valid, }, - Limit: limit, - Offset: offset * limit, + Limit: pgtype.Int4{ + Int32: int32(limit.Value), + Valid: limit.Valid, + }, + Offset: pgtype.Int4{ + Int32: int32(offset.Value), + Valid: offset.Valid, + }, + FirstStartTime: pgtype.Timestamp{ + Time: firstStartTime.Value.UTC(), + Valid: firstStartTime.Valid, + }, + LastStartTime: pgtype.Timestamp{ + Time: lastStartTime.Value.UTC(), + Valid: lastStartTime.Valid, + }, }) if err != nil { @@ -167,7 +182,7 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, off return nil, 0, err } - numberOfPages := math.Ceil(float64(totalCount) / float64(limit)) + numberOfPages := math.Ceil(float64(totalCount) / float64(limit.Value)) return upcomingEvents, int64(numberOfPages), nil } func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) { diff --git a/internal/repository/odds.go b/internal/repository/odds.go index 31810f5..fd20d1c 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -205,15 +205,54 @@ func (s *Store) GetRawOddsByMarketID(ctx context.Context, rawOddsID string, upco FetchedAt: odds.FetchedAt.Time, }, nil } +func (s *Store) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit domain.ValidInt64, offset domain.ValidInt64) ([]domain.Odd, error) { + odds, err := s.queries.GetPaginatedPrematchOddsByUpcomingID(ctx, dbgen.GetPaginatedPrematchOddsByUpcomingIDParams{ + ID: upcomingID, + Limit: pgtype.Int4{ + Int32: int32(limit.Value), + Valid: limit.Valid, + }, + Offset: pgtype.Int4{ + Int32: int32(offset.Value), + Valid: offset.Valid, + }, + }) + if err != nil { + return nil, err + } + // Map the results to domain.Odd + domainOdds := make([]domain.Odd, len(odds)) + for i, odd := range odds { + var rawOdds []domain.RawMessage + if err := json.Unmarshal(odd.RawOdds, &rawOdds); err != nil { + rawOdds = nil + } -func (s *Store) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset int32) ([]domain.Odd, error) { - params := dbgen.GetPrematchOddsByUpcomingIDParams{ - ID: upcomingID, - Limit: limit, - Offset: offset, + domainOdds[i] = domain.Odd{ + EventID: odd.EventID.String, + Fi: odd.Fi.String, + MarketType: odd.MarketType, + MarketName: odd.MarketName.String, + MarketCategory: odd.MarketCategory.String, + MarketID: odd.MarketID.String, + Name: odd.Name.String, + Handicap: odd.Handicap.String, + OddsValue: odd.OddsValue.Float64, + Section: odd.Section, + Category: odd.Category.String, + RawOdds: rawOdds, + FetchedAt: odd.FetchedAt.Time, + Source: odd.Source.String, + IsActive: odd.IsActive.Bool, + } } - odds, err := s.queries.GetPrematchOddsByUpcomingID(ctx, params) + return domainOdds, nil +} + +func (s *Store) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string) ([]domain.Odd, error) { + + odds, err := s.queries.GetPrematchOddsByUpcomingID(ctx, upcomingID) if err != nil { return nil, err } diff --git a/internal/services/bet/port.go b/internal/services/bet/port.go index 2e8cf24..cdd1ea0 100644 --- a/internal/services/bet/port.go +++ b/internal/services/bet/port.go @@ -14,9 +14,9 @@ type BetStore interface { GetAllBets(ctx context.Context) ([]domain.GetBet, error) GetBetByBranchID(ctx context.Context, BranchID int64) ([]domain.GetBet, error) GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]domain.BetOutcome, error) + GetBetOutcomeByBetID(ctx context.Context, betID int64) ([]domain.BetOutcome, error) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) DeleteBet(ctx context.Context, id int64) error } - diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index 793618d..5bc392d 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -9,7 +9,6 @@ import ( "log/slog" "math/big" random "math/rand" - "slices" "strconv" "time" @@ -20,6 +19,12 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" ) +var ( + ErrNoEventsAvailable = errors.New("Not enough events available with the given filters") + ErrGenerateRandomOutcome = errors.New("Failed to generate any random outcome for events") + ErrOutcomesNotCompleted = errors.New("Some bet outcomes are still pending") +) + type Service struct { betStore BetStore eventSvc event.Service @@ -239,12 +244,12 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID return res, nil } -func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID, sportID, HomeTeam, AwayTeam string, StartTime time.Time) ([]domain.CreateBetOutcome, float32, error) { +func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID, sportID, HomeTeam, AwayTeam string, StartTime time.Time, numMarkets int) ([]domain.CreateBetOutcome, float32, error) { var newOdds []domain.CreateBetOutcome var totalOdds float32 = 1 - markets, err := s.prematchSvc.GetPrematchOdds(ctx, eventID) + markets, err := s.prematchSvc.GetPrematchOddsByUpcomingID(ctx, eventID) if err != nil { s.logger.Error("failed to get odds for event", "event id", eventID, "error", err) @@ -253,32 +258,20 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID, sportI if len(markets) == 0 { s.logger.Error("empty odds for event", "event id", eventID) - return nil, 0, fmt.Errorf("empty odds or event", "event id", eventID) + return nil, 0, fmt.Errorf("empty odds or event %v", eventID) } - var numMarkets = min(5, len(markets)) - var randIndex []int = make([]int, numMarkets) + var selectedMarkets []domain.Odd + numMarkets = min(numMarkets, len(markets)) for i := 0; i < numMarkets; i++ { - // Guarantee that the odd is unique - var newRandMarket int - count := 0 - for { - newRandMarket = random.Intn(len(markets)) - if !slices.Contains(randIndex, newRandMarket) { - break - } - // just in case - if count >= 5 { - s.logger.Warn("market overload", "event id", eventID) - break - } - count++ - } + randomIndex := random.Intn(len(markets)) + selectedMarkets = append(selectedMarkets, markets[randomIndex]) + markets = append(markets[:randomIndex], markets[randomIndex+1:]...) + } - randIndex[i] = newRandMarket + for _, market := range selectedMarkets { - rawOdds := markets[i].RawOdds - randomRawOdd := rawOdds[random.Intn(len(rawOdds))] + randomRawOdd := market.RawOdds[random.Intn(len(market.RawOdds))] type rawOddType struct { ID string @@ -317,13 +310,13 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID, sportI continue } - marketID, err := strconv.ParseInt(markets[i].MarketID, 10, 64) + marketID, err := strconv.ParseInt(market.MarketID, 10, 64) if err != nil { s.logger.Error("Failed to get odd id", "error", err) continue } - marketName := markets[i].MarketName + marketName := market.MarketName newOdds = append(newOdds, domain.CreateBetOutcome{ EventID: eventID, @@ -345,28 +338,48 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID, sportI } if len(newOdds) == 0 { - s.logger.Error("Failed to generate random outcomes") - return nil, 0, nil + s.logger.Error("Bet Outcomes is empty for market", "selectedMarket", selectedMarkets[0].MarketName) + return nil, 0, ErrGenerateRandomOutcome } return newOdds, totalOdds, nil } -func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64) (domain.CreateBetRes, error) { +func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, leagueID, sportID domain.ValidString, firstStartTime, lastStartTime domain.ValidTime) (domain.CreateBetRes, error) { // Get a unexpired event id - events, _, err := s.eventSvc.GetPaginatedUpcomingEvents(ctx, 5, 0, domain.ValidString{}, domain.ValidString{}) + + events, _, err := s.eventSvc.GetPaginatedUpcomingEvents(ctx, + domain.ValidInt64{}, domain.ValidInt64{}, leagueID, sportID, firstStartTime, lastStartTime) if err != nil { return domain.CreateBetRes{}, err } + if len(events) == 0 { + return domain.CreateBetRes{}, ErrNoEventsAvailable + } + + // TODO: Add the option of passing number of created events + var selectedUpcomingEvents []domain.UpcomingEvent + numEventsPerBet := random.Intn(4) + 1 //Eliminate the option of 0 + + for i := 0; i < int(numEventsPerBet); i++ { + randomIndex := random.Intn(len(events)) + selectedUpcomingEvents = append(selectedUpcomingEvents, events[randomIndex]) + events = append(events[:randomIndex], events[randomIndex+1:]...) + + } + + s.logger.Info("Generating random bet events", "selectedUpcomingEvents", len(selectedUpcomingEvents)) + // Get market and odds for that var randomOdds []domain.CreateBetOutcome var totalOdds float32 = 1 - for _, event := range events { + numMarketsPerBet := random.Intn(2) + 1 + for _, event := range selectedUpcomingEvents { - newOdds, total, err := s.GenerateRandomBetOutcomes(ctx, event.ID, event.SportID, event.HomeTeam, event.AwayTeam, event.StartTime) + newOdds, total, err := s.GenerateRandomBetOutcomes(ctx, event.ID, event.SportID, event.HomeTeam, event.AwayTeam, event.StartTime, numMarketsPerBet) if err != nil { s.logger.Error("failed to generate random bet outcome", "event id", event.ID, "error", err) @@ -378,10 +391,12 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64) (d } if len(randomOdds) == 0 { - s.logger.Error("Failed to generate random outcomes") - return domain.CreateBetRes{}, nil + s.logger.Error("Failed to generate random any outcomes for all events") + return domain.CreateBetRes{}, ErrGenerateRandomOutcome } + s.logger.Info("Generated Random bet Outcome", "randomOdds", len(randomOdds)) + var cashoutID string cashoutID, err = s.GenerateCashoutID() @@ -389,13 +404,13 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64) (d return domain.CreateBetRes{}, err } - randomNumber := strconv.FormatInt(int64(random.Intn(10)), 10) + randomNumber := strconv.FormatInt(int64(random.Intn(100000000000)), 10) newBet := domain.CreateBet{ - Amount: 123, + Amount: domain.ToCurrency(123.5), TotalOdds: totalOdds, Status: domain.OUTCOME_STATUS_PENDING, FullName: "test" + randomNumber, - PhoneNumber: randomNumber, + PhoneNumber: "0900000000", CashoutID: cashoutID, BranchID: domain.ValidInt64{Valid: true, Value: branchID}, UserID: domain.ValidInt64{Valid: true, Value: userID}, @@ -450,42 +465,97 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc return s.betStore.UpdateStatus(ctx, id, status) } -func (s *Service) checkBetOutcomeForBet(ctx context.Context, eventID int64) error { - betOutcomes, err := s.betStore.GetBetOutcomeByEventID(ctx, eventID) +func (s *Service) CheckBetOutcomeForBet(ctx context.Context, betID int64) (domain.OutcomeStatus, error) { + betOutcomes, err := s.betStore.GetBetOutcomeByBetID(ctx, betID) if err != nil { - return err + return domain.OUTCOME_STATUS_PENDING, err } status := domain.OUTCOME_STATUS_PENDING for _, betOutcome := range betOutcomes { - // Check if any of them are pending + // If any of the bet outcomes are pending return if betOutcome.Status == domain.OUTCOME_STATUS_PENDING { - return nil + return domain.OUTCOME_STATUS_PENDING, ErrOutcomesNotCompleted } - if status == domain.OUTCOME_STATUS_PENDING { + if betOutcome.Status == domain.OUTCOME_STATUS_ERROR { + return domain.OUTCOME_STATUS_ERROR, nil + } + + // The bet status can only be updated if its not lost or error + // If all the bet outcomes are a win, then set the bet status to win + // If even one of the bet outcomes is a loss then set the bet status to loss + // If even one of the bet outcomes is an error, then set the bet status to error + switch status { + case domain.OUTCOME_STATUS_PENDING: status = betOutcome.Status - } else if status == domain.OUTCOME_STATUS_WIN { - status = betOutcome.Status - } else if status == domain.OUTCOME_STATUS_LOSS { - continue + case domain.OUTCOME_STATUS_WIN: + if betOutcome.Status == domain.OUTCOME_STATUS_LOSS { + status = domain.OUTCOME_STATUS_HALF + } else if betOutcome.Status == domain.OUTCOME_STATUS_HALF { + status = domain.OUTCOME_STATUS_VOID + } else if betOutcome.Status == domain.OUTCOME_STATUS_WIN { + status = domain.OUTCOME_STATUS_WIN + } else if betOutcome.Status == domain.OUTCOME_STATUS_VOID { + status = domain.OUTCOME_STATUS_VOID + } else { + status = domain.OUTCOME_STATUS_ERROR + } + case domain.OUTCOME_STATUS_LOSS: + if betOutcome.Status == domain.OUTCOME_STATUS_LOSS { + status = domain.OUTCOME_STATUS_LOSS + } else if betOutcome.Status == domain.OUTCOME_STATUS_HALF { + status = domain.OUTCOME_STATUS_LOSS + } else if betOutcome.Status == domain.OUTCOME_STATUS_WIN { + status = domain.OUTCOME_STATUS_LOSS + } else if betOutcome.Status == domain.OUTCOME_STATUS_VOID { + status = domain.OUTCOME_STATUS_VOID + } else { + status = domain.OUTCOME_STATUS_ERROR + } + case domain.OUTCOME_STATUS_VOID: + if betOutcome.Status == domain.OUTCOME_STATUS_VOID || + betOutcome.Status == domain.OUTCOME_STATUS_WIN || + betOutcome.Status == domain.OUTCOME_STATUS_LOSS || + betOutcome.Status == domain.OUTCOME_STATUS_HALF { + status = domain.OUTCOME_STATUS_VOID + } else { + status = domain.OUTCOME_STATUS_ERROR + } + case domain.OUTCOME_STATUS_HALF: + if betOutcome.Status == domain.OUTCOME_STATUS_HALF || + betOutcome.Status == domain.OUTCOME_STATUS_WIN { + status = domain.OUTCOME_STATUS_HALF + } else if betOutcome.Status == domain.OUTCOME_STATUS_LOSS { + status = domain.OUTCOME_STATUS_LOSS + } else if betOutcome.Status == domain.OUTCOME_STATUS_VOID { + status = domain.OUTCOME_STATUS_VOID + } else { + status = domain.OUTCOME_STATUS_ERROR + } + default: + // If the status is not pending, win, loss or error, then set the status to error + status = domain.OUTCOME_STATUS_ERROR } } - if status != domain.OUTCOME_STATUS_PENDING { - return nil + if status == domain.OUTCOME_STATUS_PENDING || status == domain.OUTCOME_STATUS_ERROR { + // If the status is pending or error, then we don't need to update the bet + s.logger.Info("bet not updated", "bet id", betID, "status", status) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("Error when processing bet outcomes") } - return s.UpdateStatus(ctx, eventID, status) + return status, nil } -func (s *Service) UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error { +func (s *Service) UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) { betOutcome, err := s.betStore.UpdateBetOutcomeStatus(ctx, id, status) if err != nil { - return err + return domain.BetOutcome{}, err } - return s.checkBetOutcomeForBet(ctx, betOutcome.EventID) + + return betOutcome, err } diff --git a/internal/services/event/port.go b/internal/services/event/port.go index 05fd33b..94f4313 100644 --- a/internal/services/event/port.go +++ b/internal/services/event/port.go @@ -11,7 +11,7 @@ type Service interface { FetchUpcomingEvents(ctx context.Context) error GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) - GetPaginatedUpcomingEvents(ctx context.Context, limit int32, offset int32, leagueID domain.ValidString, sportID domain.ValidString) ([]domain.UpcomingEvent, int64, error) + GetPaginatedUpcomingEvents(ctx context.Context, limit domain.ValidInt64, offset domain.ValidInt64, leagueID domain.ValidString, sportID domain.ValidString, firstStartTime domain.ValidTime, lastStartTime domain.ValidTime) ([]domain.UpcomingEvent, int64, error) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) // GetAndStoreMatchResult(ctx context.Context, eventID string) error diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 70b4f98..f344e2c 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -99,18 +99,18 @@ func (s *service) FetchLiveEvents(ctx context.Context) error { } func (s *service) FetchUpcomingEvents(ctx context.Context) error { - sportIDs := []int{1, 18} - var totalPages int = 1 - var page int = 0 - var limit int = 100 - var count int = 0 - for _, sportID := range sportIDs { - for page != totalPages { - time.Sleep(3 * time.Second) //This will restrict the fetching to 1200 requests per hour + // sportIDs := []int{1, 18, 17} + sportIDs := []int{18} + for _, sportID := range sportIDs { + var totalPages int = 1 + var page int = 0 + var limit int = 10 + var count int = 0 + for page <= totalPages { page = page + 1 url := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s&page=%d", sportID, s.token, page) - log.Printf("๐Ÿ“ก Fetching data for event data page %d", page) + log.Printf("๐Ÿ“ก Fetching data for sport %d event data page %d/%d", sportID, page, min(limit, totalPages)) resp, err := http.Get(url) if err != nil { log.Printf("โŒ Failed to fetch event data for page %d: %v", page, err) @@ -145,9 +145,10 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { } `json:"results"` } if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { + log.Printf("โŒ Failed to parse json data") continue } - skippedLeague := 0 + var skippedLeague []string for _, ev := range data.Results { startUnix, _ := strconv.ParseInt(ev.Time, 10, 64) // eventID, err := strconv.ParseInt(ev.ID, 10, 64) @@ -163,7 +164,8 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { } if !slices.Contains(domain.SupportedLeagues, leagueID) { - skippedLeague++ + + skippedLeague = append(skippedLeague, ev.League.Name) continue } @@ -188,11 +190,20 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { event.AwayTeamID = ev.Away.ID } - _ = s.store.SaveUpcomingEvent(ctx, event) + err = s.store.SaveUpcomingEvent(ctx, event) + if err != nil { + log.Printf("โŒ Failed to save upcoming event %s", event.ID) + } } - totalPages = data.Pager.Total - if count > limit { + log.Printf("โš ๏ธ Skipped leagues %v", len(skippedLeague)) + // log.Printf("โš ๏ธ Total pages %v", data.Pager.Total) + totalPages = data.Pager.Total / data.Pager.PerPage + + if count >= limit { + break + } + if page > totalPages { break } count++ @@ -223,8 +234,8 @@ func (s *service) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.Upcomi return s.store.GetExpiredUpcomingEvents(ctx) } -func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, offset int32, leagueID domain.ValidString, sportID domain.ValidString) ([]domain.UpcomingEvent, int64, error) { - return s.store.GetPaginatedUpcomingEvents(ctx, limit, offset, leagueID, sportID) +func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, limit domain.ValidInt64, offset domain.ValidInt64, leagueID domain.ValidString, sportID domain.ValidString, firstStartTime domain.ValidTime, lastStartTime domain.ValidTime) ([]domain.UpcomingEvent, int64, error){ + return s.store.GetPaginatedUpcomingEvents(ctx, limit, offset, leagueID, sportID, firstStartTime, lastStartTime) } func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) { diff --git a/internal/services/odds/port.go b/internal/services/odds/port.go index 69fd5ee..50275b2 100644 --- a/internal/services/odds/port.go +++ b/internal/services/odds/port.go @@ -9,6 +9,8 @@ import ( type Service interface { FetchNonLiveOdds(ctx context.Context) error GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) + GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string) ([]domain.Odd, error) + GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit domain.ValidInt64, offset domain.ValidInt64) ([]domain.Odd, error) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error) GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) (domain.RawOddsByMarketID, error) } diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index c42bab7..5d6f1d0 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -43,8 +43,7 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { var errs []error - for _, event := range eventIDs { - // time.Sleep(3 * time.Second) //This will restrict the fetching to 1200 requests per hour + for index, event := range eventIDs { eventID, err := strconv.ParseInt(event.ID, 10, 64) if err != nil { @@ -54,17 +53,26 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { url := fmt.Sprintf("https://api.b365api.com/v3/bet365/prematch?token=%s&FI=%d", s.config.Bet365Token, eventID) - log.Printf("๐Ÿ“ก Fetching prematch odds for event ID: %d", eventID) + log.Printf("๐Ÿ“ก Fetching prematch odds for event ID: %d (%d/%d) ", eventID, index, len(eventIDs)) - resp, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + log.Printf("โŒ Failed to create request for event %d: %v", eventID, err) + continue + } + + resp, err := s.client.Do(req) if err != nil { log.Printf("โŒ Failed to fetch prematch odds for event %d: %v", eventID, err) continue } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("โŒ Failed to read response body for event %d: %v", eventID, err) + continue + } var oddsData domain.BaseNonLiveOddResponse if err := json.Unmarshal(body, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { @@ -77,17 +85,17 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { switch sportID { case domain.FOOTBALL: if err := s.parseFootball(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Failed to insert football odd") + s.logger.Error("Error while inserting football odd") errs = append(errs, err) } case domain.BASKETBALL: if err := s.parseBasketball(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Failed to insert basketball odd") + s.logger.Error("Error while inserting basketball odd") errs = append(errs, err) } case domain.ICE_HOCKEY: if err := s.parseIceHockey(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Failed to insert ice hockey odd") + s.logger.Error("Error while inserting ice hockey odd") errs = append(errs, err) } @@ -107,8 +115,8 @@ func (s *ServiceImpl) parseFootball(ctx context.Context, res json.RawMessage) er return err } if footballRes.EventID == "" && footballRes.FI == "" { - s.logger.Error("Skipping result with no valid Event ID") - return fmt.Errorf("Skipping result with no valid Event ID") + s.logger.Error("Skipping football result with no valid Event ID", "eventID", footballRes.EventID, "fi", footballRes.FI) + return fmt.Errorf("Skipping football result with no valid Event ID Event ID %v", footballRes.EventID) } sections := map[string]domain.OddsSection{ "main": footballRes.Main, @@ -121,7 +129,8 @@ func (s *ServiceImpl) parseFootball(ctx context.Context, res json.RawMessage) er for oddCategory, section := range sections { if err := s.storeSection(ctx, footballRes.EventID, footballRes.FI, oddCategory, section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") + s.logger.Error("Error storing football section", "eventID", footballRes.FI, "odd", oddCategory) + log.Printf("โš ๏ธ Error when storing football %v", err) errs = append(errs, err) } } @@ -136,12 +145,12 @@ func (s *ServiceImpl) parseFootball(ctx context.Context, res json.RawMessage) er func (s *ServiceImpl) parseBasketball(ctx context.Context, res json.RawMessage) error { var basketballRes domain.BasketballOddsResponse if err := json.Unmarshal(res, &basketballRes); err != nil { - s.logger.Error("Failed to unmarshal football result", "error", err) + s.logger.Error("Failed to unmarshal basketball result", "error", err) return err } if basketballRes.EventID == "" && basketballRes.FI == "" { - s.logger.Error("Skipping result with no valid Event ID") - return fmt.Errorf("Skipping result with no valid Event ID") + s.logger.Error("Skipping basketball result with no valid Event ID") + return fmt.Errorf("Skipping basketball result with no valid Event ID") } sections := map[string]domain.OddsSection{ "main": basketballRes.Main, @@ -177,7 +186,7 @@ func (s *ServiceImpl) parseBasketball(ctx context.Context, res json.RawMessage) func (s *ServiceImpl) parseIceHockey(ctx context.Context, res json.RawMessage) error { var iceHockeyRes domain.IceHockeyOddsResponse if err := json.Unmarshal(res, &iceHockeyRes); err != nil { - s.logger.Error("Failed to unmarshal football result", "error", err) + s.logger.Error("Failed to unmarshal ice hockey result", "error", err) return err } if iceHockeyRes.EventID == "" && iceHockeyRes.FI == "" { @@ -229,17 +238,30 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName continue } - marketID, err := market.ID.Int64() + // Check if the market id is a string + var marketIDstr string + err := json.Unmarshal(market.ID, &marketIDstr) if err != nil { - s.logger.Error("Invalid market id", "marketID", marketID) + // check if its int + var marketIDint int + err := json.Unmarshal(market.ID, &marketIDint) + if err != nil { + s.logger.Error("Invalid market id") + errs = append(errs, err) + } + } + + marketIDint, err := strconv.ParseInt(marketIDstr, 10, 64) + if err != nil { + s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name) errs = append(errs, err) continue } - isSupported, ok := domain.SupportedMarkets[marketID] + isSupported, ok := domain.SupportedMarkets[marketIDint] if !ok || !isSupported { - s.logger.Info("Unsupported market_id", "marketID", marketID) + // s.logger.Info("Unsupported market_id", "marketID", marketIDint, "marketName", market.Name) continue } @@ -249,7 +271,7 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName MarketCategory: sectionName, MarketType: marketType, MarketName: market.Name, - MarketID: market.ID.String(), + MarketID: marketIDstr, UpdatedAt: updatedAt, Odds: market.Odds, } @@ -285,6 +307,10 @@ func (s *ServiceImpl) GetRawOddsByMarketID(ctx context.Context, marketID string, return rows, nil } -func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset int32) ([]domain.Odd, error) { - return s.store.GetPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset) +func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string) ([]domain.Odd, error) { + return s.store.GetPrematchOddsByUpcomingID(ctx, upcomingID) +} + +func (s *ServiceImpl) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset domain.ValidInt64) ([]domain.Odd, error) { + return s.store.GetPaginatedPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset) } diff --git a/internal/services/result/eval.go b/internal/services/result/eval.go index 8096e3a..93f3d64 100644 --- a/internal/services/result/eval.go +++ b/internal/services/result/eval.go @@ -9,6 +9,8 @@ import ( ) // Football evaluations + +// Full Time Result betting is a type of bet where the bettor predicts the outcome of a match at the end of the full 90 minutes of play. func evaluateFullTimeResult(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { switch outcome.OddName { case "1": // Home win @@ -27,15 +29,16 @@ func evaluateFullTimeResult(outcome domain.BetOutcome, score struct{ Home, Away } return domain.OUTCOME_STATUS_LOSS, nil default: - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) } } +// Over/Under betting is a type of bet where the bettor predicts whether the total number of goals scored in a match will be over or under a specified number. 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) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName) } if outcome.OddHeader == "Over" { @@ -53,9 +56,10 @@ func evaluateGoalsOverUnder(outcome domain.BetOutcome, score struct{ Home, Away } return domain.OUTCOME_STATUS_LOSS, nil } - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) } +// Correct Score betting is a type of bet where the bettor predicts the exact final score of a match. 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 { @@ -64,6 +68,8 @@ func evaluateCorrectScore(outcome domain.BetOutcome, score struct{ Home, Away in return domain.OUTCOME_STATUS_LOSS, nil } +// Half Time Result betting is a type of bet where the bettor predicts the outcome of a match at the end of the first half. +// This is the same as the full time result but only for the first half of the game func evaluateHalfTimeResult(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { return evaluateFullTimeResult(outcome, score) } @@ -71,43 +77,90 @@ func evaluateHalfTimeResult(outcome domain.BetOutcome, score struct{ Home, Away // 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) { + if secondOutcome == domain.OUTCOME_STATUS_PENDING { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("cannot check pending outcome") + } + + if outcome == domain.OUTCOME_STATUS_ERROR || secondOutcome == domain.OUTCOME_STATUS_ERROR { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("โŒ mutli outcome: %v -> %v \n", outcome.String(), secondOutcome.String()) + } + 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_LOSS { + return domain.OUTCOME_STATUS_LOSS, nil + } else if secondOutcome == domain.OUTCOME_STATUS_HALF { + return domain.OUTCOME_STATUS_HALF, 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") + fmt.Printf("โŒ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String()) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome") } case domain.OUTCOME_STATUS_LOSS: - if secondOutcome == domain.OUTCOME_STATUS_LOSS { + if secondOutcome == domain.OUTCOME_STATUS_LOSS || + secondOutcome == domain.OUTCOME_STATUS_WIN || + secondOutcome == domain.OUTCOME_STATUS_HALF { 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") + fmt.Printf("โŒ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String()) + return domain.OUTCOME_STATUS_ERROR, 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 if secondOutcome == domain.OUTCOME_STATUS_VOID || secondOutcome == domain.OUTCOME_STATUS_HALF { + return domain.OUTCOME_STATUS_VOID, nil } else { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid multi outcome") + fmt.Printf("โŒ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String()) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome") + } + case domain.OUTCOME_STATUS_HALF: + if secondOutcome == domain.OUTCOME_STATUS_WIN || secondOutcome == domain.OUTCOME_STATUS_HALF { + return domain.OUTCOME_STATUS_HALF, nil + } else if secondOutcome == domain.OUTCOME_STATUS_LOSS { + return domain.OUTCOME_STATUS_LOSS, nil + } else if secondOutcome == domain.OUTCOME_STATUS_VOID { + return domain.OUTCOME_STATUS_VOID, nil + } else { + fmt.Printf("โŒ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String()) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome") } default: - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid multi outcome") + fmt.Printf("โŒ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String()) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome") } } +// Asian Handicap betting is a type of betting that eliminates the possibility of a draw by giving one team a virtual advantage or disadvantage. +// +// { +// "id": "548319135", +// "odds": "1.750", +// "header": "1", +// "handicap": "+0.5, +1.0" +// }, +// +// { +// "id": "548319139", +// "odds": "1.950", +// "header": "2", +// "handicap": "-0.5, -1.0" +// } 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 { + handicapStr = strings.TrimSpace(handicapStr) handicap, err := strconv.ParseFloat(handicapStr, 64) if err != nil { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid handicap: %s", outcome.OddHandicap) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid handicap: %s", outcome.OddHandicap) } adjustedHomeScore := float64(score.Home) adjustedAwayScore := float64(score.Away) @@ -116,49 +169,117 @@ func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away i } else if outcome.OddHeader == "2" { // Away team adjustedAwayScore += handicap } else { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) + return domain.OUTCOME_STATUS_ERROR, 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 + return domain.OUTCOME_STATUS_ERROR, err } } newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) if err != nil { - fmt.Printf("multi outcome check error") - return domain.OUTCOME_STATUS_PENDING, err + return domain.OUTCOME_STATUS_ERROR, 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 + return domain.OUTCOME_STATUS_ERROR, err } } newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) if err != nil { - fmt.Printf("multi outcome check error") - return domain.OUTCOME_STATUS_PENDING, err + return domain.OUTCOME_STATUS_ERROR, 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 domain.OUTCOME_STATUS_ERROR, err } } return newOutcome, nil } +// Goal Line betting, also known as Over/Under betting, +// involves predicting the total number of goals scored in a match, regardless of which team wins. +// +// { +// "id": "548319141", +// "odds": "1.800", +// "header": "Over", +// "name": "1.5, 2.0" +// }, +// +// { +// "id": "548319146", +// "odds": "1.900", +// "header": "Under", +// "name": "1.5, 2.0" +// } func evaluateGoalLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { - return evaluateGoalsOverUnder(outcome, score) + + totalGoals := float64(score.Home + score.Away) + thresholdList := strings.Split(outcome.OddName, ",") + + newOutcome := domain.OUTCOME_STATUS_PENDING + for _, thresholdStr := range thresholdList { + thresholdStr = strings.TrimSpace(thresholdStr) + threshold, err := strconv.ParseFloat(thresholdStr, 64) + if err != nil { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: '%s', %v", thresholdStr, err) + } + + oddHeader := strings.TrimSpace(outcome.OddHeader) + if oddHeader == "Over" { + if totalGoals > threshold { + newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN) + if err != nil { + return domain.OUTCOME_STATUS_ERROR, err + } + } else if totalGoals == threshold { + newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID) + if err != nil { + + return domain.OUTCOME_STATUS_ERROR, err + } + } + newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) + if err != nil { + return domain.OUTCOME_STATUS_ERROR, err + } + } else if oddHeader == "Under" { + if totalGoals < threshold { + newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN) + if err != nil { + + return domain.OUTCOME_STATUS_ERROR, err + } + } else if totalGoals == threshold { + newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID) + if err != nil { + + return domain.OUTCOME_STATUS_ERROR, err + } + } + newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) + if err != nil { + return domain.OUTCOME_STATUS_ERROR, err + } + + } else { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: '%s'", oddHeader) + } + + } + + return newOutcome, nil } +// First Team To Score betting is a type of bet where the bettor predicts which team will score first in a match. +// We can get this from the "events" field on the result json func evaluateFirstTeamToScore(outcome domain.BetOutcome, events []map[string]string) (domain.OutcomeStatus, error) { for _, event := range events { if strings.Contains(event["text"], "1st Goal") || strings.Contains(event["text"], "Goal 1") { @@ -173,6 +294,7 @@ func evaluateFirstTeamToScore(outcome domain.BetOutcome, events []map[string]str return domain.OUTCOME_STATUS_VOID, nil // No goals scored } +// Goals Odd/Even betting is a type of bet where the bettor predicts whether the total number of goals scored in a match will be odd or even. func evaluateGoalsOddEven(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { totalGoals := score.Home + score.Away isOdd := totalGoals%2 == 1 @@ -184,6 +306,7 @@ func evaluateGoalsOddEven(outcome domain.BetOutcome, score struct{ Home, Away in return domain.OUTCOME_STATUS_LOSS, nil } +// Double Chance betting is a type of bet where the bettor predicts two of the three possible outcomes of a match. func evaluateDoubleChance(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { isHomeWin := score.Home > score.Away isDraw := score.Home == score.Away @@ -206,10 +329,11 @@ func evaluateDoubleChance(outcome domain.BetOutcome, score struct{ Home, Away in } return domain.OUTCOME_STATUS_LOSS, nil default: - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) } } +// Draw No Bet betting is a type of bet where the bettor predicts the outcome of a match, but if the match ends in a draw, the bet is voided. func evaluateDrawNoBet(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { if score.Home == score.Away { return domain.OUTCOME_STATUS_VOID, nil @@ -222,8 +346,9 @@ func evaluateDrawNoBet(outcome domain.BetOutcome, score struct{ Home, Away int } return domain.OUTCOME_STATUS_LOSS, nil } -// basketball evaluations +// Basketball evaluations +// Game Lines is an aggregate of money line, spread and total betting markets in one func evaluateGameLines(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { switch outcome.OddName { case "Money Line": @@ -235,10 +360,11 @@ func evaluateGameLines(outcome domain.BetOutcome, score struct{ Home, Away int } case "Total": return evaluateTotalOverUnder(outcome, score) default: - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) } } +// Money Line betting is a type of bet where the bettor predicts the outcome of a match without any point spread. func evaluateMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { switch outcome.OddHeader { case "1": @@ -258,21 +384,22 @@ func evaluateMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int } } return domain.OUTCOME_STATUS_LOSS, nil default: - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) } } +// Total Over/Under betting is a type of bet where the bettor predicts whether the total number of points scored in a match will be over or under a specified number. 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) + return domain.OUTCOME_STATUS_ERROR, 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) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName) } // Since the threshold will come in a xx.5 format, there is no VOID for this kind of bet @@ -294,26 +421,28 @@ func evaluateTotalOverUnder(outcome domain.BetOutcome, score struct{ Home, Away } return domain.OUTCOME_STATUS_LOSS, nil } - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) } +// Result and Total betting is a type of bet where the bettor predicts +// the outcome of a match and whether the total number of points scored will be over or under a specified number. func evaluateResultAndTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { // The handicap will be in the format "U {float}" or "O {float}" // U and O denoting over and under for this case overUnderStr := strings.Split(outcome.OddHandicap, " ") if len(overUnderStr) != 2 { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, 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) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over under: %s", outcome.OddHeader) } threshold, err := strconv.ParseFloat(overUnderStr[1], 64) if err != nil { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName) } // Since the threshold will come in a xx.5 format, there is no VOID for this kind of bet @@ -321,6 +450,10 @@ func evaluateResultAndTotal(outcome domain.BetOutcome, score struct{ Home, Away switch outcome.OddHeader { case "1": + if score.Home < score.Away { + return domain.OUTCOME_STATUS_LOSS, nil + } + if overUnder == "Over" && totalScore > threshold { return domain.OUTCOME_STATUS_WIN, nil } else if overUnder == "Under" && totalScore < threshold { @@ -328,6 +461,9 @@ func evaluateResultAndTotal(outcome domain.BetOutcome, score struct{ Home, Away } return domain.OUTCOME_STATUS_LOSS, nil case "2": + if score.Away < score.Home { + return domain.OUTCOME_STATUS_LOSS, nil + } if overUnder == "Over" && totalScore > threshold { return domain.OUTCOME_STATUS_WIN, nil } else if overUnder == "Under" && totalScore < threshold { @@ -336,27 +472,29 @@ func evaluateResultAndTotal(outcome domain.BetOutcome, score struct{ Home, Away return domain.OUTCOME_STATUS_LOSS, nil default: - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("failed to parse over and under: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed to parse over and under: %s", outcome.OddName) } } +// Team Total betting is a type of bet where the bettor predicts the total number of points scored by a specific team in a match +// is over or under a specified number. func evaluateTeamTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { // The handicap will be in the format "U {float}" or "O {float}" // U and O denoting over and under for this case overUnderStr := strings.Split(outcome.OddHandicap, " ") if len(overUnderStr) != 2 { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddHandicap) + return domain.OUTCOME_STATUS_ERROR, 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) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over under: %s", outcome.OddHeader) } threshold, err := strconv.ParseFloat(overUnderStr[1], 64) if err != nil { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddHandicap) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddHandicap) } // Since the threshold will come in a xx.5 format, there is no VOID for this kind of bet @@ -380,11 +518,12 @@ func evaluateTeamTotal(outcome domain.BetOutcome, score struct{ Home, Away int } return domain.OUTCOME_STATUS_LOSS, nil default: - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("failed to parse over and under: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed to parse over and under: %s", outcome.OddName) } } -// Evaluate Result and Both Teams To Score X Points +// Result and Both Teams To Score X Points is a type of bet where the bettor predicts whether both teams will score a certain number of points +// and also the result fo the match func evaluateResultAndBTTSX(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { // The name parameter will hold value "name": "{team_name} and {Yes | No}" @@ -400,14 +539,14 @@ func evaluateResultAndBTTSX(outcome domain.BetOutcome, score struct{ Home, Away } else if scoreCheckSplit == "No" { isScorePoints = false } else { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) } teamName := strings.TrimSpace(strings.Join(oddNameSplit[:len(oddNameSplit)-2], "")) threshold, err := strconv.ParseInt(outcome.OddHeader, 10, 64) if err != nil { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddHeader) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddHeader) } switch teamName { @@ -428,18 +567,18 @@ func evaluateResultAndBTTSX(outcome domain.BetOutcome, score struct{ Home, Away } } default: - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("team name error: %s", teamName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("team name error: %s", teamName) } return domain.OUTCOME_STATUS_LOSS, nil } -// Both Teams To Score X Points +// Both Teams To Score X Points is a type of bet where the bettor predicts whether both teams will score a certain number of points. func evaluateBTTSX(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { threshold, err := strconv.ParseInt(outcome.OddName, 10, 64) if err != nil { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName) } switch outcome.OddHeader { @@ -453,12 +592,13 @@ func evaluateBTTSX(outcome domain.BetOutcome, score struct{ Home, Away int }) (d } default: - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) } return domain.OUTCOME_STATUS_LOSS, nil } +// Money Line 3 Way betting is a type of bet where the bettor predicts the outcome of a match with three possible outcomes: home win, away win, or draw. func evaluateMoneyLine3Way(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { switch outcome.OddName { case "1": // Home win @@ -477,23 +617,24 @@ func evaluateMoneyLine3Way(outcome domain.BetOutcome, score struct{ Home, Away i } return domain.OUTCOME_STATUS_LOSS, nil default: - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) } } +// Double Result betting is a type of bet where the bettor predicts the outcome of a match at both half-time and full-time. func evaluateDoubleResult(outcome domain.BetOutcome, firstHalfScore struct{ Home, Away int }, secondHalfScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { halfWins := strings.Split(outcome.OddName, "-") if len(halfWins) != 2 { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) } firstHalfWinner := strings.TrimSpace(halfWins[0]) secondHalfWinner := strings.TrimSpace(halfWins[1]) if firstHalfWinner != outcome.HomeTeamName && firstHalfWinner != outcome.AwayTeamName && firstHalfWinner != "Tie" { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", firstHalfWinner) + return domain.OUTCOME_STATUS_ERROR, 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) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", firstHalfWinner) } switch { @@ -517,6 +658,7 @@ func evaluateDoubleResult(outcome domain.BetOutcome, firstHalfScore struct{ Home return domain.OUTCOME_STATUS_WIN, nil } +// Highest Scoring Half betting is a type of bet where the bettor predicts which half of the match will have the highest total score. func evaluateHighestScoringHalf(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { firstHalfTotal := firstScore.Home + firstScore.Away secondHalfTotal := secondScore.Home + secondScore.Away @@ -534,11 +676,12 @@ func evaluateHighestScoringHalf(outcome domain.BetOutcome, firstScore struct{ Ho return domain.OUTCOME_STATUS_WIN, nil } default: - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) } return domain.OUTCOME_STATUS_LOSS, nil } +// Highest Scoring Quarter betting is a type of bet where the bettor predicts which quarter of the match will have the highest score. func evaluateHighestScoringQuarter(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }, thirdScore struct{ Home, Away int }, fourthScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { firstQuarterTotal := firstScore.Home + firstScore.Away secondQuarterTotal := secondScore.Home + secondScore.Away @@ -567,18 +710,20 @@ func evaluateHighestScoringQuarter(outcome domain.BetOutcome, firstScore struct{ return domain.OUTCOME_STATUS_WIN, nil } default: - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) } return domain.OUTCOME_STATUS_LOSS, nil } +// Handicap and Total betting is a combination of spread betting and total points betting +// where the bettor predicts the outcome of a match with a point spread and the total number of points scored is over or under a specified number. func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { nameSplit := strings.Split(outcome.OddName, " ") // Evaluate from bottom to get the threshold and find out if its over or under threshold, err := strconv.ParseFloat(nameSplit[len(nameSplit)-1], 10) if err != nil { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("failed parsing threshold: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing threshold: %s", outcome.OddName) } total := float64(score.Home + score.Away) overUnder := nameSplit[len(nameSplit)-2] @@ -591,12 +736,12 @@ func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Awa return domain.OUTCOME_STATUS_LOSS, nil } } else { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("failed parsing over and under: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, 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) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing handicap: %s", outcome.OddName) } teamName := strings.TrimSpace(strings.Join(nameSplit[:len(nameSplit)-4], "")) @@ -618,21 +763,22 @@ func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Awa } return domain.OUTCOME_STATUS_LOSS, nil default: - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("failed parsing team name: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing team name: %s", outcome.OddName) } } +// Winning Margin betting is a type of bet where the bettor predicts the margin of victory in a match. func evaluateWinningMargin(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { marginSplit := strings.Split(outcome.OddName, "") if len(marginSplit) < 1 { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) } margin, err := strconv.ParseInt(marginSplit[0], 10, 64) if err != nil { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) } isGtr := false @@ -656,9 +802,10 @@ func evaluateWinningMargin(outcome domain.BetOutcome, score struct{ Home, Away i return domain.OUTCOME_STATUS_LOSS, nil } - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddheader: %s", outcome.OddHeader) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddheader: %s", outcome.OddHeader) } +// Highest Scoring Period betting is a type of bet where the bettor predicts which period of the match will have the highest total score. func evaluateHighestScoringPeriod(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }, thirdScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { firstPeriodTotal := firstScore.Home + firstScore.Away secondPeriodTotal := secondScore.Home + secondScore.Away @@ -682,11 +829,12 @@ func evaluateHighestScoringPeriod(outcome domain.BetOutcome, firstScore struct{ return domain.OUTCOME_STATUS_WIN, nil } default: - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) } return domain.OUTCOME_STATUS_LOSS, nil } +// Tied After Regulation is a type of bet where the bettor predicts whether the match will end in a tie after regulation time. func evaluateTiedAfterRegulation(outcome domain.BetOutcome, scores []struct{ Home, Away int }) (domain.OutcomeStatus, error) { totalScore := struct{ Home, Away int }{0, 0} for _, score := range scores { @@ -706,6 +854,5 @@ func evaluateTiedAfterRegulation(outcome domain.BetOutcome, scores []struct{ Hom return domain.OUTCOME_STATUS_LOSS, nil } - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) } - diff --git a/internal/services/result/football_test.go b/internal/services/result/football_test.go new file mode 100644 index 0000000..0130cf6 --- /dev/null +++ b/internal/services/result/football_test.go @@ -0,0 +1,30 @@ +package result + +import ( + "testing" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +func TestEvaluateFullTimeResult(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"Home win", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_WIN}, + {"Away win", domain.BetOutcome{OddName: "2"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, + {"Draw", domain.BetOutcome{OddName: "Draw"}, struct{ Home, Away int }{1, 1}, domain.OUTCOME_STATUS_WIN}, + {"Home selected, but Draw", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 1}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateFullTimeResult(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} diff --git a/internal/services/result/service.go b/internal/services/result/service.go index c2ef4b1..d7cc80a 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -45,9 +45,9 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { s.logger.Error("Failed to fetch events") return err } - fmt.Printf("Expired Events: %d \n", len(events)) - - for _, event := range events { + fmt.Printf("โš ๏ธ Expired Events: %d \n", len(events)) + for i, event := range events { + fmt.Printf("๐Ÿ•› Checking if event has bets placed on it %v (%d/%d) \n", event.ID, i+1, len(events)) eventID, err := strconv.ParseInt(event.ID, 10, 64) if err != nil { s.logger.Error("Failed to parse event id") @@ -59,46 +59,89 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { return err } - for _, outcome := range outcomes { + if len(outcomes) == 0 { + continue + } + + isDeleted := true + for j, outcome := range outcomes { + fmt.Printf("โš™๏ธ Processing ๐ŸŽฒ outcomes '%v' for event %v(%v) (%d/%d) \n", + outcome.MarketName, + event.HomeTeam+" "+event.AwayTeam, event.ID, + j+1, len(outcomes)) + if outcome.Expires.After(time.Now()) { + isDeleted = false + s.logger.Info("Outcome is not expired yet", "event_id", event.ID, "outcome_id", outcome.ID) 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) + isDeleted = false continue } + // TODO: optimize this because the result is being fetched for each outcome which will have the same event id but different market id 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) + fmt.Printf("โŒ failed to parse ๐ŸŽฒ outcomes '%v' for event %v(%v) (%d/%d) \n", + outcome.MarketName, + event.HomeTeam+" "+event.AwayTeam, event.ID, + j+1, len(outcomes)) + s.logger.Error("Failed to fetch result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "market_id", outcome.MarketID, "market", outcome.MarketName, "error", err) + isDeleted = false 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) + outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, result.Status) if err != nil { + isDeleted = false s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err) continue } + if outcome.Status == domain.OUTCOME_STATUS_ERROR || outcome.Status == domain.OUTCOME_STATUS_PENDING { + fmt.Printf("โŒ Error while updating ๐ŸŽฒ outcomes '%v' for event %v(%v) (%d/%d) \n", + outcome.MarketName, + event.HomeTeam+" "+event.AwayTeam, event.ID, + j+1, len(outcomes)) + + s.logger.Error("Outcome is pending or error", "event_id", outcome.EventID, "outcome_id", outcome.ID) + isDeleted = false + continue + } + + fmt.Printf("โœ… Successfully updated ๐ŸŽฒ outcomes '%v' for event %v(%v) (%d/%d) \n", + outcome.MarketName, + event.HomeTeam+" "+event.AwayTeam, event.ID, + j+1, len(outcomes)) + + status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID) + if err != nil { + if err != bet.ErrOutcomesNotCompleted { + s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err) + } + continue + } + fmt.Printf("๐Ÿงพ Updating bet status for event %v (%d/%d) to %v\n", event.ID, j+1, len(outcomes), status.String()) + err = s.betSvc.UpdateStatus(ctx, outcome.BetID, status) + if err != nil { + s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err) + continue + } + fmt.Printf("โœ… Successfully updated ๐ŸŽซ Bet for event %v(%v) (%d/%d) \n", + event.HomeTeam+" "+event.AwayTeam, event.ID, + j+1, len(outcomes)) + } - err = s.repo.DeleteEvent(ctx, event.ID) - if err != nil { - s.logger.Error("Failed to remove event", "event_id", event.ID, "error", err) - return err + if isDeleted { + // err = s.repo.DeleteEvent(ctx, event.ID) + // if err != nil { + // s.logger.Error("Failed to remove event", "event_id", event.ID, "error", err) + // return err + // } } + } return nil @@ -248,7 +291,7 @@ func (s *Service) parseFootball(resultRes json.RawMessage, eventID, oddID, marke corners := parseStats(result.Stats.Corners) 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) + s.logger.Error("Failed to evaluate football outcome", "event_id", eventID, "market_id", marketID, "error", err) return domain.CreateResult{}, err } diff --git a/internal/services/result/service_test.go b/internal/services/result/service_test.go deleted file mode 100644 index 2705049..0000000 --- a/internal/services/result/service_test.go +++ /dev/null @@ -1 +0,0 @@ -package result diff --git a/internal/services/ticket/service.go b/internal/services/ticket/service.go index 1d86313..509f353 100644 --- a/internal/services/ticket/service.go +++ b/internal/services/ticket/service.go @@ -37,3 +37,7 @@ func (s *Service) UpdateTicketOutcomeStatus(ctx context.Context, id int64, statu func (s *Service) DeleteTicket(ctx context.Context, id int64) error { return s.ticketStore.DeleteTicket(ctx, id) } + +func (s *Service) DeleteOldTickets(ctx context.Context) error { + return s.ticketStore.DeleteOldTickets(ctx) +} diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 4dccd22..e9bca42 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -1,8 +1,8 @@ package httpserver import ( - // "context" "context" + "log" // "time" @@ -10,6 +10,7 @@ import ( eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" resultsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/robfig/cron/v3" ) @@ -20,14 +21,14 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S spec string task func() }{ - { - spec: "0 0 * * * *", // Every 1 hour - task: func() { - if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { - log.Printf("FetchUpcomingEvents error: %v", err) - } - }, - }, + // { + // spec: "0 0 * * * *", // Every 1 hour + // task: func() { + // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { + // log.Printf("FetchUpcomingEvents error: %v", err) + // } + // }, + // }, // { // spec: "*/5 * * * * *", // Every 5 seconds @@ -37,14 +38,14 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S // } // }, // }, - { - spec: "0 */15 * * * *", // Every 15 minutes - task: func() { - if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { - log.Printf("FetchNonLiveOdds error: %v", err) - } - }, - }, + // { + // spec: "0 */15 * * * *", // Every 15 minutes + // task: func() { + // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { + // log.Printf("FetchNonLiveOdds error: %v", err) + // } + // }, + // }, // { // spec: "0 */15 * * * *", // task: func() { @@ -80,6 +81,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S } for _, job := range schedule { + job.task() if _, err := c.AddFunc(job.spec, job.task); err != nil { log.Fatalf("Failed to schedule cron job: %v", err) } @@ -88,3 +90,34 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S c.Start() log.Println("Cron jobs started for event and odds services") } + +func StartTicketCrons(ticketService ticket.Service) { + c := cron.New(cron.WithSeconds()) + + schedule := []struct { + spec string + task func() + }{ + { + spec: "0 0 * * * *", // Every hour + task: func() { + log.Println("Deleting old tickets...") + if err := ticketService.DeleteOldTickets(context.Background()); err != nil { + log.Printf("Failed to remove old ticket: %v", err) + } else { + log.Printf("Successfully deleted old tickets") + } + }, + }, + } + + for _, job := range schedule { + job.task() + if _, err := c.AddFunc(job.spec, job.task); err != nil { + log.Fatalf("Failed to schedule cron job: %v", err) + } + } + + c.Start() + log.Println("Cron jobs started for ticket service") +} diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index d6952a1..860d128 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -2,8 +2,10 @@ package handlers import ( "strconv" + "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" ) @@ -15,7 +17,7 @@ import ( // @Accept json // @Produce json // @Param createBet body domain.CreateBetReq true "Creates bet" -// @Success 200 {object} BetRes +// @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /bet [post] @@ -54,7 +56,7 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param createBet body domain.RandomBetReq true "Create Random bet" -// @Success 200 {object} BetRes +// @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /random/bet [post] @@ -64,6 +66,45 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) // role := c.Locals("role").(domain.Role) + leagueIDQuery := c.Query("league_id") + sportIDQuery := c.Query("sport_id") + firstStartTimeQuery := c.Query("first_start_time") + lastStartTimeQuery := c.Query("last_start_time") + + leagueID := domain.ValidString{ + Value: leagueIDQuery, + Valid: leagueIDQuery != "", + } + sportID := domain.ValidString{ + Value: sportIDQuery, + Valid: sportIDQuery != "", + } + + var firstStartTime domain.ValidTime + if firstStartTimeQuery != "" { + firstStartTimeParsed, err := time.Parse(time.RFC3339, firstStartTimeQuery) + if err != nil { + h.logger.Error("invalid start_time format", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil) + } + firstStartTime = domain.ValidTime{ + Value: firstStartTimeParsed, + Valid: true, + } + } + var lastStartTime domain.ValidTime + if lastStartTimeQuery != "" { + lastStartTimeParsed, err := time.Parse(time.RFC3339, lastStartTimeQuery) + if err != nil { + h.logger.Error("invalid start_time format", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil) + } + lastStartTime = domain.ValidTime{ + Value: lastStartTimeParsed, + Valid: true, + } + } + var req domain.RandomBetReq if err := c.BodyParser(&req); err != nil { h.logger.Error("Failed to parse RandomBet request", "error", err) @@ -75,10 +116,14 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } - res, err := h.betSvc.PlaceRandomBet(c.Context(), userID, req.BranchID) + res, err := h.betSvc.PlaceRandomBet(c.Context(), userID, req.BranchID, leagueID, sportID, firstStartTime, lastStartTime) if err != nil { h.logger.Error("Random Bet failed", "error", err) + switch err { + case bet.ErrNoEventsAvailable: + return fiber.NewError(fiber.StatusBadRequest, "No events found") + } return fiber.NewError(fiber.StatusInternalServerError, "Unable to create random bet") } @@ -92,7 +137,7 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error { // @Tags bet // @Accept json // @Produce json -// @Success 200 {array} BetRes +// @Success 200 {array} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /bet [get] @@ -118,7 +163,7 @@ func (h *Handler) GetAllBet(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param id path int true "Bet ID" -// @Success 200 {object} BetRes +// @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /bet/{id} [get] @@ -149,7 +194,7 @@ func (h *Handler) GetBetByID(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param id path string true "cashout ID" -// @Success 200 {object} BetRes +// @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /bet/cashout/{id} [get] diff --git a/internal/web_server/handlers/branch_handler.go b/internal/web_server/handlers/branch_handler.go index 905da0b..4ec72e7 100644 --- a/internal/web_server/handlers/branch_handler.go +++ b/internal/web_server/handlers/branch_handler.go @@ -498,7 +498,7 @@ func (h *Handler) GetBranchOperations(c *fiber.Ctx) error { // @Tags branch // @Accept json // @Produce json -// @Success 200 {array} BetRes +// @Success 200 {array} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /branch/{id}/bets [get] diff --git a/internal/web_server/handlers/prematch.go b/internal/web_server/handlers/prematch.go index e4790e8..52e3780 100644 --- a/internal/web_server/handlers/prematch.go +++ b/internal/web_server/handlers/prematch.go @@ -2,6 +2,7 @@ package handlers import ( "strconv" + "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" @@ -106,6 +107,8 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { pageSize := c.QueryInt("page_size", 10) leagueIDQuery := c.Query("league_id") sportIDQuery := c.Query("sport_id") + firstStartTimeQuery := c.Query("first_start_time") + lastStartTimeQuery := c.Query("last_start_time") leagueID := domain.ValidString{ Value: leagueIDQuery, @@ -116,7 +119,41 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { Valid: sportIDQuery != "", } - events, total, err := h.eventSvc.GetPaginatedUpcomingEvents(c.Context(), int32(pageSize), int32(page)-1, leagueID, sportID) + var firstStartTime domain.ValidTime + if firstStartTimeQuery != "" { + firstStartTimeParsed, err := time.Parse(time.RFC3339, firstStartTimeQuery) + if err != nil { + h.logger.Error("invalid start_time format", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil) + } + firstStartTime = domain.ValidTime{ + Value: firstStartTimeParsed, + Valid: true, + } + } + var lastStartTime domain.ValidTime + if lastStartTimeQuery != "" { + lastStartTimeParsed, err := time.Parse(time.RFC3339, lastStartTimeQuery) + if err != nil { + h.logger.Error("invalid start_time format", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil) + } + lastStartTime = domain.ValidTime{ + Value: lastStartTimeParsed, + Valid: true, + } + } + + limit := domain.ValidInt64{ + Value: int64(pageSize), + Valid: true, + } + offset := domain.ValidInt64{ + Value: int64(page - 1), + Valid: true, + } + events, total, err := h.eventSvc.GetPaginatedUpcomingEvents( + c.Context(), limit, offset, leagueID, sportID, firstStartTime, lastStartTime) // fmt.Printf("League ID: %v", leagueID) if err != nil { @@ -183,7 +220,7 @@ func (h *Handler) GetPrematchOddsByUpcomingID(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid offset value", nil, nil) } - odds, err := h.prematchSvc.GetPrematchOddsByUpcomingID(c.Context(), upcomingID, int32(limit), int32(offset)) + odds, err := h.prematchSvc.GetPrematchOddsByUpcomingID(c.Context(), upcomingID) if err != nil { return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve prematch odds", nil, nil) } diff --git a/makefile b/makefile index 79017cf..15d4368 100644 --- a/makefile +++ b/makefile @@ -1,7 +1,7 @@ include .env .PHONY: test test: - @go test ./app + @go test ./... .PHONY: coverage coverage: @mkdir -p coverage