fix: result service evaluation issues

This commit is contained in:
Samuel Tariku 2025-05-15 01:01:09 +03:00
parent b7b17fa8d2
commit 95fb33c9d4
36 changed files with 1425 additions and 621 deletions

View File

@ -86,6 +86,7 @@ func main() {
virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger) virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger)
httpserver.StartDataFetchingCrons(eventSvc, oddsSvc, resultSvc) httpserver.StartDataFetchingCrons(eventSvc, oddsSvc, resultSvc)
httpserver.StartTicketCrons(*ticketSvc)
app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{ app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{
JwtAccessKey: cfg.JwtKey, JwtAccessKey: cfg.JwtKey,

View File

@ -340,15 +340,43 @@ INSERT INTO users (
suspended_at, suspended_at,
suspended 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 ( VALUES (
'Samuel', 'Samuel',
'Tariku', 'Tariku',
'cybersamt@gmail.com', 'cybersamt@gmail.com',
NULL, '0911111111',
crypt('password@123', gen_salt('bf'))::bytea, crypt('password@123', gen_salt('bf'))::bytea,
'super_admin', 'super_admin',
TRUE, TRUE,
FALSE, TRUE,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP,
NULL, NULL,
@ -372,11 +400,11 @@ VALUES (
'Kirubel', 'Kirubel',
'Kibru', 'Kibru',
'kirubeljkl679 @gmail.com', 'kirubeljkl679 @gmail.com',
NULL, '0911111111',
crypt('password@123', gen_salt('bf'))::bytea, crypt('password@123', gen_salt('bf'))::bytea,
'super_admin', 'super_admin',
TRUE, TRUE,
FALSE, TRUE,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP,
NULL, NULL,

View File

@ -62,6 +62,10 @@ WHERE branch_id = $1;
SELECT * SELECT *
FROM bet_outcomes FROM bet_outcomes
WHERE event_id = $1; WHERE event_id = $1;
-- name: GetBetOutcomeByBetID :many
SELECT *
FROM bet_outcomes
WHERE bet_id = $1;
-- name: UpdateCashOut :exec -- name: UpdateCashOut :exec
UPDATE bets UPDATE bets
SET cashed_out = $2, SET cashed_out = $2,
@ -74,9 +78,9 @@ WHERE id = $2
RETURNING *; RETURNING *;
-- name: UpdateStatus :exec -- name: UpdateStatus :exec
UPDATE bets UPDATE bets
SET status = $2, SET status = $1,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $1; WHERE id = $2;
-- name: DeleteBet :exec -- name: DeleteBet :exec
DELETE FROM bets DELETE FROM bets
WHERE id = $1; WHERE id = $1;

View File

@ -196,15 +196,23 @@ FROM events
WHERE is_live = false WHERE is_live = false
AND status = 'upcoming' AND status = 'upcoming'
AND ( AND (
league_id = $3 league_id = sqlc.narg('league_id')
OR $3 IS NULL OR sqlc.narg('league_id') IS NULL
) )
AND ( AND (
sport_id = $4 sport_id = sqlc.narg('sport_id')
OR $4 IS NULL 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 ORDER BY start_time ASC
LIMIT $1 OFFSET $2; LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
-- name: GetUpcomingByID :one -- name: GetUpcomingByID :one
SELECT id, SELECT id,
sport_id, sport_id,

View File

@ -94,23 +94,17 @@ WHERE market_id = $1
AND fi = $2 AND fi = $2
AND is_active = true AND is_active = true
AND source = 'b365api'; AND source = 'b365api';
-- name: GetPrematchOddsByUpcomingID :many -- name: GetPrematchOddsByUpcomingID :many
SELECT o.event_id, SELECT o.*
o.fi, FROM odds o
o.market_type, JOIN events e ON o.fi = e.id
o.market_name, WHERE e.id = $1
o.market_category, AND e.is_live = false
o.market_id, AND e.status = 'upcoming'
o.name, AND o.is_active = true
o.handicap, AND o.source = 'b365api';
o.odds_value, -- name: GetPaginatedPrematchOddsByUpcomingID :many
o.section, SELECT o.*
o.category,
o.raw_odds,
o.fetched_at,
o.source,
o.is_active
FROM odds o FROM odds o
JOIN events e ON o.fi = e.id JOIN events e ON o.fi = e.id
WHERE e.id = $1 WHERE e.id = $1
@ -118,4 +112,4 @@ WHERE e.id = $1
AND e.status = 'upcoming' AND e.status = 'upcoming'
AND o.is_active = true AND o.is_active = true
AND o.source = 'b365api' AND o.source = 'b365api'
LIMIT $2 OFFSET $3; LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');

View File

@ -304,7 +304,7 @@ const docTemplate = `{
"schema": { "schema": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/handlers.BetRes" "$ref": "#/definitions/domain.BetRes"
} }
} }
}, },
@ -341,7 +341,7 @@ const docTemplate = `{
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/handlers.CreateBetReq" "$ref": "#/definitions/domain.CreateBetReq"
} }
} }
], ],
@ -349,7 +349,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.BetRes" "$ref": "#/definitions/domain.BetRes"
} }
}, },
"400": { "400": {
@ -393,7 +393,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.BetRes" "$ref": "#/definitions/domain.BetRes"
} }
}, },
"400": { "400": {
@ -437,7 +437,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.BetRes" "$ref": "#/definitions/domain.BetRes"
} }
}, },
"400": { "400": {
@ -786,7 +786,7 @@ const docTemplate = `{
"schema": { "schema": {
"type": "array", "type": "array",
"items": { "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": { "/referral/settings": {
"get": { "get": {
"security": [ "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": { "domain.Odd": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -3501,6 +3658,15 @@ const docTemplate = `{
} }
} }
}, },
"domain.RandomBetReq": {
"type": "object",
"properties": {
"branch_id": {
"type": "integer",
"example": 1
}
}
},
"domain.RawOddsByMarketID": { "domain.RawOddsByMarketID": {
"type": "object", "type": "object",
"properties": { "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": { "handlers.BranchDetailRes": {
"type": "object", "type": "object",
"properties": { "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": { "handlers.CreateBranchOperationReq": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -296,7 +296,7 @@
"schema": { "schema": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/handlers.BetRes" "$ref": "#/definitions/domain.BetRes"
} }
} }
}, },
@ -333,7 +333,7 @@
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/handlers.CreateBetReq" "$ref": "#/definitions/domain.CreateBetReq"
} }
} }
], ],
@ -341,7 +341,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.BetRes" "$ref": "#/definitions/domain.BetRes"
} }
}, },
"400": { "400": {
@ -385,7 +385,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.BetRes" "$ref": "#/definitions/domain.BetRes"
} }
}, },
"400": { "400": {
@ -429,7 +429,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.BetRes" "$ref": "#/definitions/domain.BetRes"
} }
}, },
"400": { "400": {
@ -778,7 +778,7 @@
"schema": { "schema": {
"type": "array", "type": "array",
"items": { "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": { "/referral/settings": {
"get": { "get": {
"security": [ "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": { "domain.Odd": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -3493,6 +3650,15 @@
} }
} }
}, },
"domain.RandomBetReq": {
"type": "object",
"properties": {
"branch_id": {
"type": "integer",
"example": 1
}
}
},
"domain.RawOddsByMarketID": { "domain.RawOddsByMarketID": {
"type": "object", "type": "object",
"properties": { "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": { "handlers.BranchDetailRes": {
"type": "object", "type": "object",
"properties": { "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": { "handlers.CreateBranchOperationReq": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -48,6 +48,82 @@ definitions:
- $ref: '#/definitions/domain.OutcomeStatus' - $ref: '#/definitions/domain.OutcomeStatus'
example: 1 example: 1
type: object 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: domain.Odd:
properties: properties:
category: category:
@ -130,6 +206,12 @@ definitions:
description: BET, WIN, REFUND, JACKPOT_WIN description: BET, WIN, REFUND, JACKPOT_WIN
type: string type: string
type: object type: object
domain.RandomBetReq:
properties:
branch_id:
example: 1
type: integer
type: object
domain.RawOddsByMarketID: domain.RawOddsByMarketID:
properties: properties:
fetched_at: fetched_at:
@ -309,47 +391,6 @@ definitions:
updated_at: updated_at:
type: string type: string
type: object 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: handlers.BranchDetailRes:
properties: properties:
branch_manager_id: branch_manager_id:
@ -465,41 +506,6 @@ definitions:
example: "1234567890" example: "1234567890"
type: string type: string
type: object 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: handlers.CreateBranchOperationReq:
properties: properties:
branch_id: branch_id:
@ -1320,7 +1326,7 @@ paths:
description: OK description: OK
schema: schema:
items: items:
$ref: '#/definitions/handlers.BetRes' $ref: '#/definitions/domain.BetRes'
type: array type: array
"400": "400":
description: Bad Request description: Bad Request
@ -1343,14 +1349,14 @@ paths:
name: createBet name: createBet
required: true required: true
schema: schema:
$ref: '#/definitions/handlers.CreateBetReq' $ref: '#/definitions/domain.CreateBetReq'
produces: produces:
- application/json - application/json
responses: responses:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/handlers.BetRes' $ref: '#/definitions/domain.BetRes'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
@ -1407,7 +1413,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/handlers.BetRes' $ref: '#/definitions/domain.BetRes'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
@ -1470,7 +1476,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/handlers.BetRes' $ref: '#/definitions/domain.BetRes'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
@ -1639,7 +1645,7 @@ paths:
description: OK description: OK
schema: schema:
items: items:
$ref: '#/definitions/handlers.BetRes' $ref: '#/definitions/domain.BetRes'
type: array type: array
"400": "400":
description: Bad Request description: Bad Request
@ -2385,6 +2391,36 @@ paths:
summary: Retrieve raw odds by Market ID summary: Retrieve raw odds by Market ID
tags: tags:
- prematch - 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: /referral/settings:
get: get:
consumes: consumes:

View File

@ -243,6 +243,48 @@ func (q *Queries) GetBetByID(ctx context.Context, id int64) (BetWithOutcome, err
return i, 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 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 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 FROM bet_outcomes
@ -339,17 +381,17 @@ func (q *Queries) UpdateCashOut(ctx context.Context, arg UpdateCashOutParams) er
const UpdateStatus = `-- name: UpdateStatus :exec const UpdateStatus = `-- name: UpdateStatus :exec
UPDATE bets UPDATE bets
SET status = $2, SET status = $1,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $1 WHERE id = $2
` `
type UpdateStatusParams struct { type UpdateStatusParams struct {
ID int64 `json:"id"`
Status int32 `json:"status"` Status int32 `json:"status"`
ID int64 `json:"id"`
} }
func (q *Queries) UpdateStatus(ctx context.Context, arg UpdateStatusParams) error { 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 return err
} }

View File

@ -201,22 +201,32 @@ FROM events
WHERE is_live = false WHERE is_live = false
AND status = 'upcoming' AND status = 'upcoming'
AND ( 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 OR $3 IS NULL
) )
AND ( AND (
sport_id = $4 start_time > $4
OR $4 IS NULL OR $4 IS NULL
) )
ORDER BY start_time ASC ORDER BY start_time ASC
LIMIT $1 OFFSET $2 LIMIT $6 OFFSET $5
` `
type GetPaginatedUpcomingEventsParams struct { type GetPaginatedUpcomingEventsParams struct {
Limit int32 `json:"limit"` LeagueID pgtype.Text `json:"league_id"`
Offset int32 `json:"offset"` SportID pgtype.Text `json:"sport_id"`
LeagueID pgtype.Text `json:"league_id"` LastStartTime pgtype.Timestamp `json:"last_start_time"`
SportID pgtype.Text `json:"sport_id"` FirstStartTime pgtype.Timestamp `json:"first_start_time"`
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
} }
type GetPaginatedUpcomingEventsRow struct { type GetPaginatedUpcomingEventsRow struct {
@ -240,10 +250,12 @@ type GetPaginatedUpcomingEventsRow struct {
func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginatedUpcomingEventsParams) ([]GetPaginatedUpcomingEventsRow, error) { func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginatedUpcomingEventsParams) ([]GetPaginatedUpcomingEventsRow, error) {
rows, err := q.db.Query(ctx, GetPaginatedUpcomingEvents, rows, err := q.db.Query(ctx, GetPaginatedUpcomingEvents,
arg.Limit,
arg.Offset,
arg.LeagueID, arg.LeagueID,
arg.SportID, arg.SportID,
arg.LastStartTime,
arg.FirstStartTime,
arg.Offset,
arg.Limit,
) )
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -86,6 +86,61 @@ func (q *Queries) GetALLPrematchOdds(ctx context.Context) ([]GetALLPrematchOddsR
return items, nil 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 const GetPrematchOdds = `-- name: GetPrematchOdds :many
SELECT event_id, SELECT event_id,
fi, fi,
@ -162,21 +217,7 @@ func (q *Queries) GetPrematchOdds(ctx context.Context) ([]GetPrematchOddsRow, er
} }
const GetPrematchOddsByUpcomingID = `-- name: GetPrematchOddsByUpcomingID :many const GetPrematchOddsByUpcomingID = `-- name: GetPrematchOddsByUpcomingID :many
SELECT o.event_id, 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
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 FROM odds o
JOIN events e ON o.fi = e.id JOIN events e ON o.fi = e.id
WHERE e.id = $1 WHERE e.id = $1
@ -184,43 +225,19 @@ WHERE e.id = $1
AND e.status = 'upcoming' AND e.status = 'upcoming'
AND o.is_active = true AND o.is_active = true
AND o.source = 'b365api' AND o.source = 'b365api'
LIMIT $2 OFFSET $3
` `
type GetPrematchOddsByUpcomingIDParams struct { func (q *Queries) GetPrematchOddsByUpcomingID(ctx context.Context, id string) ([]Odd, error) {
ID string `json:"id"` rows, err := q.db.Query(ctx, GetPrematchOddsByUpcomingID, 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)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var items []GetPrematchOddsByUpcomingIDRow var items []Odd
for rows.Next() { for rows.Next() {
var i GetPrematchOddsByUpcomingIDRow var i Odd
if err := rows.Scan( if err := rows.Scan(
&i.ID,
&i.EventID, &i.EventID,
&i.Fi, &i.Fi,
&i.MarketType, &i.MarketType,

View File

@ -97,7 +97,7 @@ type CreateBetReq struct {
} }
type RandomBetReq struct { type RandomBetReq struct {
BranchID int64 `json:"branch_id,omitempty" example:"1"` BranchID int64 `json:"branch_id" validate:"required" example:"1"`
} }
type CreateBetRes struct { type CreateBetRes struct {

View File

@ -1,6 +1,9 @@
package domain package domain
import "fmt" import (
"fmt"
"time"
)
type ValidInt64 struct { type ValidInt64 struct {
Value int64 Value int64
@ -11,6 +14,10 @@ type ValidString struct {
Value string Value string
Valid bool Valid bool
} }
type ValidTime struct {
Value time.Time
Valid bool
}
type ValidBool struct { type ValidBool struct {
Value bool Value bool
Valid bool Valid bool

View File

@ -12,9 +12,8 @@ var SupportedLeagues = []int64{
10041957, //UEFA Europa League 10041957, //UEFA Europa League
10079560, //UEFA Conference League 10079560, //UEFA Conference League
10047168, // US MLS 10047168, // US MLS
10044469, // Ethiopian Premier League
10050282, //UEFA Nations League 10050282, //UEFA Nations League
10040795, //EuroLeague
10043156, //England FA Cup 10043156, //England FA Cup
10042103, //France Cup 10042103, //France Cup
@ -26,5 +25,12 @@ var SupportedLeagues = []int64{
// Basketball // Basketball
173998768, //NBA 173998768, //NBA
10041830, //NBA
// Ice Hockey
10037477, //NHL
10037447, //AHL
10069385, //IIHF World Championship
10040795, //EuroLeague
} }

View File

@ -12,8 +12,9 @@ type OddsSection struct {
Sp map[string]OddsMarket `json:"sp"` 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 { type OddsMarket struct {
ID json.Number `json:"id"` ID json.RawMessage `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Odds []json.RawMessage `json:"odds"` Odds []json.RawMessage `json:"odds"`
Header string `json:"header,omitempty"` Header string `json:"header,omitempty"`

View File

@ -43,4 +43,24 @@ const (
OUTCOME_STATUS_LOSS OutcomeStatus = 2 OUTCOME_STATUS_LOSS OutcomeStatus = 2
OUTCOME_STATUS_VOID OutcomeStatus = 3 //Give Back OUTCOME_STATUS_VOID OutcomeStatus = 3 //Give Back
OUTCOME_STATUS_HALF OutcomeStatus = 4 //Half Win and Half Given 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"
}
}

View File

@ -9,12 +9,15 @@ const (
FOOTBALL_CORRECT_SCORE FootballMarket = 43 //"correct_score" FOOTBALL_CORRECT_SCORE FootballMarket = 43 //"correct_score"
FOOTBALL_ASIAN_HANDICAP FootballMarket = 938 //"asian_handicap" FOOTBALL_ASIAN_HANDICAP FootballMarket = 938 //"asian_handicap"
FOOTBALL_GOAL_LINE FootballMarket = 10143 //"goal_line" FOOTBALL_GOAL_LINE FootballMarket = 10143 //"goal_line"
FOOTBALL_HALF_TIME_RESULT FootballMarket = 1579 //"half_time_result" FOOTBALL_HALF_TIME_RESULT FootballMarket = 1579 //"half_time_result"
FOOTBALL_FIRST_HALF_ASIAN_HANDICAP FootballMarket = 50137 //"1st_half_asian_handicap" FOOTBALL_FIRST_HALF_ASIAN_HANDICAP FootballMarket = 50137 //"1st_half_asian_handicap"
FOOTBALL_FIRST_HALF_GOAL_LINE FootballMarket = 50136 //"1st_half_goal_line" FOOTBALL_FIRST_HALF_GOAL_LINE FootballMarket = 50136 //"1st_half_goal_line"
FOOTBALL_FIRST_TEAM_TO_SCORE FootballMarket = 1178 //"first_team_to_score" FOOTBALL_FIRST_TEAM_TO_SCORE FootballMarket = 1178 //"first_team_to_score"
FOOTBALL_GOALS_ODD_EVEN FootballMarket = 10111 //"goals_odd_even" FOOTBALL_GOALS_ODD_EVEN FootballMarket = 10111 //"goals_odd_even"
FOOTBALL_DRAW_NO_BET FootballMarket = 10544 //"draw_no_bet" FOOTBALL_DRAW_NO_BET FootballMarket = 10544 //"draw_no_bet"
) )
type BasketBallMarket int64 type BasketBallMarket int64

View File

@ -2,6 +2,7 @@ package repository
import ( import (
"context" "context"
// "fmt" // "fmt"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" 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 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) { func (s *Store) UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) {
update, err := s.queries.UpdateBetOutcomeStatus(ctx, dbgen.UpdateBetOutcomeStatusParams{ update, err := s.queries.UpdateBetOutcomeStatus(ctx, dbgen.UpdateBetOutcomeStatusParams{
Status: int32(status), Status: int32(status),

View File

@ -117,7 +117,8 @@ func (s *Store) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.Upcoming
return upcomingEvents, nil 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{ events, err := s.queries.GetPaginatedUpcomingEvents(ctx, dbgen.GetPaginatedUpcomingEventsParams{
LeagueID: pgtype.Text{ LeagueID: pgtype.Text{
String: leagueID.Value, String: leagueID.Value,
@ -127,8 +128,22 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, off
String: sportID.Value, String: sportID.Value,
Valid: sportID.Valid, Valid: sportID.Valid,
}, },
Limit: limit, Limit: pgtype.Int4{
Offset: offset * limit, 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 { if err != nil {
@ -167,7 +182,7 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, off
return nil, 0, err return nil, 0, err
} }
numberOfPages := math.Ceil(float64(totalCount) / float64(limit)) numberOfPages := math.Ceil(float64(totalCount) / float64(limit.Value))
return upcomingEvents, int64(numberOfPages), nil return upcomingEvents, int64(numberOfPages), nil
} }
func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) { func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) {

View File

@ -205,15 +205,54 @@ func (s *Store) GetRawOddsByMarketID(ctx context.Context, rawOddsID string, upco
FetchedAt: odds.FetchedAt.Time, FetchedAt: odds.FetchedAt.Time,
}, nil }, 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) { domainOdds[i] = domain.Odd{
params := dbgen.GetPrematchOddsByUpcomingIDParams{ EventID: odd.EventID.String,
ID: upcomingID, Fi: odd.Fi.String,
Limit: limit, MarketType: odd.MarketType,
Offset: offset, 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -14,9 +14,9 @@ type BetStore interface {
GetAllBets(ctx context.Context) ([]domain.GetBet, error) GetAllBets(ctx context.Context) ([]domain.GetBet, error)
GetBetByBranchID(ctx context.Context, BranchID int64) ([]domain.GetBet, error) GetBetByBranchID(ctx context.Context, BranchID int64) ([]domain.GetBet, error)
GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]domain.BetOutcome, 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 UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error
UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error
UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error)
DeleteBet(ctx context.Context, id int64) error DeleteBet(ctx context.Context, id int64) error
} }

View File

@ -9,7 +9,6 @@ import (
"log/slog" "log/slog"
"math/big" "math/big"
random "math/rand" random "math/rand"
"slices"
"strconv" "strconv"
"time" "time"
@ -20,6 +19,12 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "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 { type Service struct {
betStore BetStore betStore BetStore
eventSvc event.Service eventSvc event.Service
@ -239,12 +244,12 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
return res, nil 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 newOdds []domain.CreateBetOutcome
var totalOdds float32 = 1 var totalOdds float32 = 1
markets, err := s.prematchSvc.GetPrematchOdds(ctx, eventID) markets, err := s.prematchSvc.GetPrematchOddsByUpcomingID(ctx, eventID)
if err != nil { if err != nil {
s.logger.Error("failed to get odds for event", "event id", eventID, "error", err) 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 { if len(markets) == 0 {
s.logger.Error("empty odds for event", "event id", eventID) 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 selectedMarkets []domain.Odd
var randIndex []int = make([]int, numMarkets) numMarkets = min(numMarkets, len(markets))
for i := 0; i < numMarkets; i++ { for i := 0; i < numMarkets; i++ {
// Guarantee that the odd is unique randomIndex := random.Intn(len(markets))
var newRandMarket int selectedMarkets = append(selectedMarkets, markets[randomIndex])
count := 0 markets = append(markets[:randomIndex], markets[randomIndex+1:]...)
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++
}
randIndex[i] = newRandMarket for _, market := range selectedMarkets {
rawOdds := markets[i].RawOdds randomRawOdd := market.RawOdds[random.Intn(len(market.RawOdds))]
randomRawOdd := rawOdds[random.Intn(len(rawOdds))]
type rawOddType struct { type rawOddType struct {
ID string ID string
@ -317,13 +310,13 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID, sportI
continue continue
} }
marketID, err := strconv.ParseInt(markets[i].MarketID, 10, 64) marketID, err := strconv.ParseInt(market.MarketID, 10, 64)
if err != nil { if err != nil {
s.logger.Error("Failed to get odd id", "error", err) s.logger.Error("Failed to get odd id", "error", err)
continue continue
} }
marketName := markets[i].MarketName marketName := market.MarketName
newOdds = append(newOdds, domain.CreateBetOutcome{ newOdds = append(newOdds, domain.CreateBetOutcome{
EventID: eventID, EventID: eventID,
@ -345,28 +338,48 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID, sportI
} }
if len(newOdds) == 0 { if len(newOdds) == 0 {
s.logger.Error("Failed to generate random outcomes") s.logger.Error("Bet Outcomes is empty for market", "selectedMarket", selectedMarkets[0].MarketName)
return nil, 0, nil return nil, 0, ErrGenerateRandomOutcome
} }
return newOdds, totalOdds, nil 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 // 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 { if err != nil {
return domain.CreateBetRes{}, err 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 // Get market and odds for that
var randomOdds []domain.CreateBetOutcome var randomOdds []domain.CreateBetOutcome
var totalOdds float32 = 1 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 { if err != nil {
s.logger.Error("failed to generate random bet outcome", "event id", event.ID, "error", err) 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 { if len(randomOdds) == 0 {
s.logger.Error("Failed to generate random outcomes") s.logger.Error("Failed to generate random any outcomes for all events")
return domain.CreateBetRes{}, nil return domain.CreateBetRes{}, ErrGenerateRandomOutcome
} }
s.logger.Info("Generated Random bet Outcome", "randomOdds", len(randomOdds))
var cashoutID string var cashoutID string
cashoutID, err = s.GenerateCashoutID() cashoutID, err = s.GenerateCashoutID()
@ -389,13 +404,13 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64) (d
return domain.CreateBetRes{}, err return domain.CreateBetRes{}, err
} }
randomNumber := strconv.FormatInt(int64(random.Intn(10)), 10) randomNumber := strconv.FormatInt(int64(random.Intn(100000000000)), 10)
newBet := domain.CreateBet{ newBet := domain.CreateBet{
Amount: 123, Amount: domain.ToCurrency(123.5),
TotalOdds: totalOdds, TotalOdds: totalOdds,
Status: domain.OUTCOME_STATUS_PENDING, Status: domain.OUTCOME_STATUS_PENDING,
FullName: "test" + randomNumber, FullName: "test" + randomNumber,
PhoneNumber: randomNumber, PhoneNumber: "0900000000",
CashoutID: cashoutID, CashoutID: cashoutID,
BranchID: domain.ValidInt64{Valid: true, Value: branchID}, BranchID: domain.ValidInt64{Valid: true, Value: branchID},
UserID: domain.ValidInt64{Valid: true, Value: userID}, 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) return s.betStore.UpdateStatus(ctx, id, status)
} }
func (s *Service) checkBetOutcomeForBet(ctx context.Context, eventID int64) error { func (s *Service) CheckBetOutcomeForBet(ctx context.Context, betID int64) (domain.OutcomeStatus, error) {
betOutcomes, err := s.betStore.GetBetOutcomeByEventID(ctx, eventID) betOutcomes, err := s.betStore.GetBetOutcomeByBetID(ctx, betID)
if err != nil { if err != nil {
return err return domain.OUTCOME_STATUS_PENDING, err
} }
status := domain.OUTCOME_STATUS_PENDING status := domain.OUTCOME_STATUS_PENDING
for _, betOutcome := range betOutcomes { 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 { 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 status = betOutcome.Status
} else if status == domain.OUTCOME_STATUS_WIN { case domain.OUTCOME_STATUS_WIN:
status = betOutcome.Status if betOutcome.Status == domain.OUTCOME_STATUS_LOSS {
} else if status == domain.OUTCOME_STATUS_LOSS { status = domain.OUTCOME_STATUS_HALF
continue } 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 { if status == domain.OUTCOME_STATUS_PENDING || status == domain.OUTCOME_STATUS_ERROR {
return nil // 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) betOutcome, err := s.betStore.UpdateBetOutcomeStatus(ctx, id, status)
if err != nil { if err != nil {
return err return domain.BetOutcome{}, err
} }
return s.checkBetOutcomeForBet(ctx, betOutcome.EventID)
return betOutcome, err
} }

View File

@ -11,7 +11,7 @@ type Service interface {
FetchUpcomingEvents(ctx context.Context) error FetchUpcomingEvents(ctx context.Context) error
GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error)
GetExpiredUpcomingEvents(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) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error)
// GetAndStoreMatchResult(ctx context.Context, eventID string) error // GetAndStoreMatchResult(ctx context.Context, eventID string) error

View File

@ -99,18 +99,18 @@ func (s *service) FetchLiveEvents(ctx context.Context) error {
} }
func (s *service) FetchUpcomingEvents(ctx context.Context) error { func (s *service) FetchUpcomingEvents(ctx context.Context) error {
sportIDs := []int{1, 18} // sportIDs := []int{1, 18, 17}
var totalPages int = 1 sportIDs := []int{18}
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
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 page = page + 1
url := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s&page=%d", sportID, s.token, page) 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) resp, err := http.Get(url)
if err != nil { if err != nil {
log.Printf("❌ Failed to fetch event data for page %d: %v", page, err) 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"` } `json:"results"`
} }
if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 {
log.Printf("❌ Failed to parse json data")
continue continue
} }
skippedLeague := 0 var skippedLeague []string
for _, ev := range data.Results { for _, ev := range data.Results {
startUnix, _ := strconv.ParseInt(ev.Time, 10, 64) startUnix, _ := strconv.ParseInt(ev.Time, 10, 64)
// eventID, err := strconv.ParseInt(ev.ID, 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) { if !slices.Contains(domain.SupportedLeagues, leagueID) {
skippedLeague++
skippedLeague = append(skippedLeague, ev.League.Name)
continue continue
} }
@ -188,11 +190,20 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error {
event.AwayTeamID = ev.Away.ID 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 break
} }
count++ count++
@ -223,8 +234,8 @@ func (s *service) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.Upcomi
return s.store.GetExpiredUpcomingEvents(ctx) 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) { 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) return s.store.GetPaginatedUpcomingEvents(ctx, limit, offset, leagueID, sportID, firstStartTime, lastStartTime)
} }
func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) { func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) {

View File

@ -9,6 +9,8 @@ import (
type Service interface { type Service interface {
FetchNonLiveOdds(ctx context.Context) error FetchNonLiveOdds(ctx context.Context) error
GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, 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) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error)
GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) (domain.RawOddsByMarketID, error) GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) (domain.RawOddsByMarketID, error)
} }

View File

@ -43,8 +43,7 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
var errs []error var errs []error
for _, event := range eventIDs { for index, event := range eventIDs {
// time.Sleep(3 * time.Second) //This will restrict the fetching to 1200 requests per hour
eventID, err := strconv.ParseInt(event.ID, 10, 64) eventID, err := strconv.ParseInt(event.ID, 10, 64)
if err != nil { 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) 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 { if err != nil {
log.Printf("❌ Failed to fetch prematch odds for event %d: %v", eventID, err) log.Printf("❌ Failed to fetch prematch odds for event %d: %v", eventID, err)
continue continue
} }
defer resp.Body.Close() 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 var oddsData domain.BaseNonLiveOddResponse
if err := json.Unmarshal(body, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { 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 { switch sportID {
case domain.FOOTBALL: case domain.FOOTBALL:
if err := s.parseFootball(ctx, oddsData.Results[0]); err != nil { 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) errs = append(errs, err)
} }
case domain.BASKETBALL: case domain.BASKETBALL:
if err := s.parseBasketball(ctx, oddsData.Results[0]); err != nil { 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) errs = append(errs, err)
} }
case domain.ICE_HOCKEY: case domain.ICE_HOCKEY:
if err := s.parseIceHockey(ctx, oddsData.Results[0]); err != nil { 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) errs = append(errs, err)
} }
@ -107,8 +115,8 @@ func (s *ServiceImpl) parseFootball(ctx context.Context, res json.RawMessage) er
return err return err
} }
if footballRes.EventID == "" && footballRes.FI == "" { if footballRes.EventID == "" && footballRes.FI == "" {
s.logger.Error("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 result with no valid Event ID") return fmt.Errorf("Skipping football result with no valid Event ID Event ID %v", footballRes.EventID)
} }
sections := map[string]domain.OddsSection{ sections := map[string]domain.OddsSection{
"main": footballRes.Main, "main": footballRes.Main,
@ -121,7 +129,8 @@ func (s *ServiceImpl) parseFootball(ctx context.Context, res json.RawMessage) er
for oddCategory, section := range sections { for oddCategory, section := range sections {
if err := s.storeSection(ctx, footballRes.EventID, footballRes.FI, oddCategory, section); err != nil { 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) 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 { func (s *ServiceImpl) parseBasketball(ctx context.Context, res json.RawMessage) error {
var basketballRes domain.BasketballOddsResponse var basketballRes domain.BasketballOddsResponse
if err := json.Unmarshal(res, &basketballRes); err != nil { 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 return err
} }
if basketballRes.EventID == "" && basketballRes.FI == "" { if basketballRes.EventID == "" && basketballRes.FI == "" {
s.logger.Error("Skipping result with no valid Event ID") s.logger.Error("Skipping basketball result with no valid Event ID")
return fmt.Errorf("Skipping result with no valid Event ID") return fmt.Errorf("Skipping basketball result with no valid Event ID")
} }
sections := map[string]domain.OddsSection{ sections := map[string]domain.OddsSection{
"main": basketballRes.Main, "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 { func (s *ServiceImpl) parseIceHockey(ctx context.Context, res json.RawMessage) error {
var iceHockeyRes domain.IceHockeyOddsResponse var iceHockeyRes domain.IceHockeyOddsResponse
if err := json.Unmarshal(res, &iceHockeyRes); err != nil { 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 return err
} }
if iceHockeyRes.EventID == "" && iceHockeyRes.FI == "" { if iceHockeyRes.EventID == "" && iceHockeyRes.FI == "" {
@ -229,17 +238,30 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName
continue 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 { 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) errs = append(errs, err)
continue continue
} }
isSupported, ok := domain.SupportedMarkets[marketID] isSupported, ok := domain.SupportedMarkets[marketIDint]
if !ok || !isSupported { if !ok || !isSupported {
s.logger.Info("Unsupported market_id", "marketID", marketID) // s.logger.Info("Unsupported market_id", "marketID", marketIDint, "marketName", market.Name)
continue continue
} }
@ -249,7 +271,7 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName
MarketCategory: sectionName, MarketCategory: sectionName,
MarketType: marketType, MarketType: marketType,
MarketName: market.Name, MarketName: market.Name,
MarketID: market.ID.String(), MarketID: marketIDstr,
UpdatedAt: updatedAt, UpdatedAt: updatedAt,
Odds: market.Odds, Odds: market.Odds,
} }
@ -285,6 +307,10 @@ func (s *ServiceImpl) GetRawOddsByMarketID(ctx context.Context, marketID string,
return rows, nil return rows, nil
} }
func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset int32) ([]domain.Odd, error) { func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string) ([]domain.Odd, error) {
return s.store.GetPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset) 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)
} }

View File

@ -9,6 +9,8 @@ import (
) )
// Football evaluations // 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) { func evaluateFullTimeResult(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddName { switch outcome.OddName {
case "1": // Home win case "1": // Home win
@ -27,15 +29,16 @@ func evaluateFullTimeResult(outcome domain.BetOutcome, score struct{ Home, Away
} }
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
default: 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) { func evaluateGoalsOverUnder(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalGoals := float64(score.Home + score.Away) totalGoals := float64(score.Home + score.Away)
threshold, err := strconv.ParseFloat(outcome.OddName, 64) threshold, err := strconv.ParseFloat(outcome.OddName, 64)
if err != nil { 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" { 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_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) { func evaluateCorrectScore(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
expectedScore := fmt.Sprintf("%d-%d", score.Home, score.Away) expectedScore := fmt.Sprintf("%d-%d", score.Home, score.Away)
if outcome.OddName == expectedScore { if outcome.OddName == expectedScore {
@ -64,6 +68,8 @@ func evaluateCorrectScore(outcome domain.BetOutcome, score struct{ Home, Away in
return domain.OUTCOME_STATUS_LOSS, nil 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) { func evaluateHalfTimeResult(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
return evaluateFullTimeResult(outcome, score) 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 // 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" // 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) { 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 { switch outcome {
case domain.OUTCOME_STATUS_PENDING: case domain.OUTCOME_STATUS_PENDING:
return secondOutcome, nil return secondOutcome, nil
case domain.OUTCOME_STATUS_WIN: case domain.OUTCOME_STATUS_WIN:
if secondOutcome == domain.OUTCOME_STATUS_WIN { if secondOutcome == domain.OUTCOME_STATUS_WIN {
return domain.OUTCOME_STATUS_WIN, nil 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 { } else if secondOutcome == domain.OUTCOME_STATUS_VOID {
return domain.OUTCOME_STATUS_HALF, nil return domain.OUTCOME_STATUS_HALF, nil
} else { } 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: 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 return domain.OUTCOME_STATUS_LOSS, nil
} else if secondOutcome == domain.OUTCOME_STATUS_VOID { } else if secondOutcome == domain.OUTCOME_STATUS_VOID {
return domain.OUTCOME_STATUS_HALF, nil return domain.OUTCOME_STATUS_HALF, nil
} else { } 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: case domain.OUTCOME_STATUS_VOID:
if secondOutcome == domain.OUTCOME_STATUS_WIN || secondOutcome == domain.OUTCOME_STATUS_LOSS { if secondOutcome == domain.OUTCOME_STATUS_WIN || secondOutcome == domain.OUTCOME_STATUS_LOSS {
return domain.OUTCOME_STATUS_HALF, nil 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 { } 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: 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) { func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
handicapList := strings.Split(outcome.OddHandicap, ",") handicapList := strings.Split(outcome.OddHandicap, ",")
newOutcome := domain.OUTCOME_STATUS_PENDING newOutcome := domain.OUTCOME_STATUS_PENDING
for _, handicapStr := range handicapList { for _, handicapStr := range handicapList {
handicapStr = strings.TrimSpace(handicapStr)
handicap, err := strconv.ParseFloat(handicapStr, 64) handicap, err := strconv.ParseFloat(handicapStr, 64)
if err != nil { 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) adjustedHomeScore := float64(score.Home)
adjustedAwayScore := float64(score.Away) 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 } else if outcome.OddHeader == "2" { // Away team
adjustedAwayScore += handicap adjustedAwayScore += handicap
} else { } 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 adjustedHomeScore > adjustedAwayScore {
if outcome.OddHeader == "1" { if outcome.OddHeader == "1" {
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN) newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN)
if err != nil { if err != nil {
fmt.Printf("multi outcome check error") return domain.OUTCOME_STATUS_ERROR, err
return domain.OUTCOME_STATUS_PENDING, err
} }
} }
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS)
if err != nil { if err != nil {
fmt.Printf("multi outcome check error") return domain.OUTCOME_STATUS_ERROR, err
return domain.OUTCOME_STATUS_PENDING, err
} }
} else if adjustedHomeScore < adjustedAwayScore { } else if adjustedHomeScore < adjustedAwayScore {
if outcome.OddHeader == "2" { if outcome.OddHeader == "2" {
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN) newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN)
if err != nil { if err != nil {
fmt.Printf("multi outcome check error") return domain.OUTCOME_STATUS_ERROR, err
return domain.OUTCOME_STATUS_PENDING, err
} }
} }
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS)
if err != nil { if err != nil {
fmt.Printf("multi outcome check error") return domain.OUTCOME_STATUS_ERROR, err
return domain.OUTCOME_STATUS_PENDING, err
} }
} }
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID) newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID)
if err != nil { if err != nil {
fmt.Printf("multi outcome check error") return domain.OUTCOME_STATUS_ERROR, err
return domain.OUTCOME_STATUS_PENDING, err
} }
} }
return newOutcome, nil 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) { 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) { func evaluateFirstTeamToScore(outcome domain.BetOutcome, events []map[string]string) (domain.OutcomeStatus, error) {
for _, event := range events { for _, event := range events {
if strings.Contains(event["text"], "1st Goal") || strings.Contains(event["text"], "Goal 1") { 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 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) { func evaluateGoalsOddEven(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalGoals := score.Home + score.Away totalGoals := score.Home + score.Away
isOdd := totalGoals%2 == 1 isOdd := totalGoals%2 == 1
@ -184,6 +306,7 @@ func evaluateGoalsOddEven(outcome domain.BetOutcome, score struct{ Home, Away in
return domain.OUTCOME_STATUS_LOSS, nil 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) { func evaluateDoubleChance(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
isHomeWin := score.Home > score.Away isHomeWin := score.Home > score.Away
isDraw := 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 return domain.OUTCOME_STATUS_LOSS, nil
default: 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) { func evaluateDrawNoBet(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
if score.Home == score.Away { if score.Home == score.Away {
return domain.OUTCOME_STATUS_VOID, nil 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 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) { func evaluateGameLines(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddName { switch outcome.OddName {
case "Money Line": case "Money Line":
@ -235,10 +360,11 @@ func evaluateGameLines(outcome domain.BetOutcome, score struct{ Home, Away int }
case "Total": case "Total":
return evaluateTotalOverUnder(outcome, score) return evaluateTotalOverUnder(outcome, score)
default: 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) { func evaluateMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddHeader { switch outcome.OddHeader {
case "1": case "1":
@ -258,21 +384,22 @@ func evaluateMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }
} }
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
default: 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) { 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}" // The handicap will be in the format "U {float}" or "O {float}"
// U and O denoting over and under for this case // U and O denoting over and under for this case
overUnderStr := strings.Split(outcome.OddHandicap, " ") overUnderStr := strings.Split(outcome.OddHandicap, " ")
if len(overUnderStr) != 2 { 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) threshold, err := strconv.ParseFloat(overUnderStr[1], 64)
if err != nil { 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 // 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_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) { 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}" // The handicap will be in the format "U {float}" or "O {float}"
// U and O denoting over and under for this case // U and O denoting over and under for this case
overUnderStr := strings.Split(outcome.OddHandicap, " ") overUnderStr := strings.Split(outcome.OddHandicap, " ")
if len(overUnderStr) != 2 { 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] overUnder := overUnderStr[0]
if overUnder != "Over" && overUnder != "Under" { 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) threshold, err := strconv.ParseFloat(overUnderStr[1], 64)
if err != nil { 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 // 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 { switch outcome.OddHeader {
case "1": case "1":
if score.Home < score.Away {
return domain.OUTCOME_STATUS_LOSS, nil
}
if overUnder == "Over" && totalScore > threshold { if overUnder == "Over" && totalScore > threshold {
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} else if overUnder == "Under" && totalScore < threshold { } 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 return domain.OUTCOME_STATUS_LOSS, nil
case "2": case "2":
if score.Away < score.Home {
return domain.OUTCOME_STATUS_LOSS, nil
}
if overUnder == "Over" && totalScore > threshold { if overUnder == "Over" && totalScore > threshold {
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} else if overUnder == "Under" && totalScore < threshold { } 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 return domain.OUTCOME_STATUS_LOSS, nil
default: 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) { 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}" // The handicap will be in the format "U {float}" or "O {float}"
// U and O denoting over and under for this case // U and O denoting over and under for this case
overUnderStr := strings.Split(outcome.OddHandicap, " ") overUnderStr := strings.Split(outcome.OddHandicap, " ")
if len(overUnderStr) != 2 { 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] overUnder := overUnderStr[0]
if overUnder != "Over" && overUnder != "Under" { 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) threshold, err := strconv.ParseFloat(overUnderStr[1], 64)
if err != nil { 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 // 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 return domain.OUTCOME_STATUS_LOSS, nil
default: 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) { func evaluateResultAndBTTSX(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
// The name parameter will hold value "name": "{team_name} and {Yes | No}" // The 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" { } else if scoreCheckSplit == "No" {
isScorePoints = false isScorePoints = false
} else { } 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], "")) teamName := strings.TrimSpace(strings.Join(oddNameSplit[:len(oddNameSplit)-2], ""))
threshold, err := strconv.ParseInt(outcome.OddHeader, 10, 64) threshold, err := strconv.ParseInt(outcome.OddHeader, 10, 64)
if err != nil { 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 { switch teamName {
@ -428,18 +567,18 @@ func evaluateResultAndBTTSX(outcome domain.BetOutcome, score struct{ Home, Away
} }
} }
default: 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 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) { func evaluateBTTSX(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
threshold, err := strconv.ParseInt(outcome.OddName, 10, 64) threshold, err := strconv.ParseInt(outcome.OddName, 10, 64)
if err != nil { 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 { switch outcome.OddHeader {
@ -453,12 +592,13 @@ func evaluateBTTSX(outcome domain.BetOutcome, score struct{ Home, Away int }) (d
} }
default: 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 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) { func evaluateMoneyLine3Way(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddName { switch outcome.OddName {
case "1": // Home win case "1": // Home win
@ -477,23 +617,24 @@ func evaluateMoneyLine3Way(outcome domain.BetOutcome, score struct{ Home, Away i
} }
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
default: 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) { func evaluateDoubleResult(outcome domain.BetOutcome, firstHalfScore struct{ Home, Away int }, secondHalfScore struct{ Home, Away int }) (domain.OutcomeStatus, error) {
halfWins := strings.Split(outcome.OddName, "-") halfWins := strings.Split(outcome.OddName, "-")
if len(halfWins) != 2 { 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]) firstHalfWinner := strings.TrimSpace(halfWins[0])
secondHalfWinner := strings.TrimSpace(halfWins[1]) secondHalfWinner := strings.TrimSpace(halfWins[1])
if firstHalfWinner != outcome.HomeTeamName && firstHalfWinner != outcome.AwayTeamName && firstHalfWinner != "Tie" { 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" { 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 { switch {
@ -517,6 +658,7 @@ func evaluateDoubleResult(outcome domain.BetOutcome, firstHalfScore struct{ Home
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} }
// Highest Scoring Half betting is a type of bet where the bettor predicts which half of the match will have the highest total score.
func evaluateHighestScoringHalf(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { func evaluateHighestScoringHalf(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }) (domain.OutcomeStatus, error) {
firstHalfTotal := firstScore.Home + firstScore.Away firstHalfTotal := firstScore.Home + firstScore.Away
secondHalfTotal := secondScore.Home + secondScore.Away secondHalfTotal := secondScore.Home + secondScore.Away
@ -534,11 +676,12 @@ func evaluateHighestScoringHalf(outcome domain.BetOutcome, firstScore struct{ Ho
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} }
default: 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 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) { 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 firstQuarterTotal := firstScore.Home + firstScore.Away
secondQuarterTotal := secondScore.Home + secondScore.Away secondQuarterTotal := secondScore.Home + secondScore.Away
@ -567,18 +710,20 @@ func evaluateHighestScoringQuarter(outcome domain.BetOutcome, firstScore struct{
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} }
default: 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 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) { func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
nameSplit := strings.Split(outcome.OddName, " ") nameSplit := strings.Split(outcome.OddName, " ")
// Evaluate from bottom to get the threshold and find out if its over or under // Evaluate from bottom to get the threshold and find out if its over or under
threshold, err := strconv.ParseFloat(nameSplit[len(nameSplit)-1], 10) threshold, err := strconv.ParseFloat(nameSplit[len(nameSplit)-1], 10)
if err != nil { 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) total := float64(score.Home + score.Away)
overUnder := nameSplit[len(nameSplit)-2] overUnder := nameSplit[len(nameSplit)-2]
@ -591,12 +736,12 @@ func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Awa
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
} }
} else { } 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) handicap, err := strconv.ParseFloat(nameSplit[len(nameSplit)-4], 10)
if err != nil { 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], "")) 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 return domain.OUTCOME_STATUS_LOSS, nil
default: 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) { func evaluateWinningMargin(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
marginSplit := strings.Split(outcome.OddName, "") marginSplit := strings.Split(outcome.OddName, "")
if len(marginSplit) < 1 { 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) margin, err := strconv.ParseInt(marginSplit[0], 10, 64)
if err != nil { 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 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_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) { 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 firstPeriodTotal := firstScore.Home + firstScore.Away
secondPeriodTotal := secondScore.Home + secondScore.Away secondPeriodTotal := secondScore.Home + secondScore.Away
@ -682,11 +829,12 @@ func evaluateHighestScoringPeriod(outcome domain.BetOutcome, firstScore struct{
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} }
default: 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 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) { func evaluateTiedAfterRegulation(outcome domain.BetOutcome, scores []struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalScore := struct{ Home, Away int }{0, 0} totalScore := struct{ Home, Away int }{0, 0}
for _, score := range scores { 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_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)
} }

View File

@ -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)
}
})
}
}

View File

@ -45,9 +45,9 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error {
s.logger.Error("Failed to fetch events") s.logger.Error("Failed to fetch events")
return err return err
} }
fmt.Printf("Expired Events: %d \n", len(events)) fmt.Printf("⚠️ Expired Events: %d \n", len(events))
for i, event := range events {
for _, 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) eventID, err := strconv.ParseInt(event.ID, 10, 64)
if err != nil { if err != nil {
s.logger.Error("Failed to parse event id") s.logger.Error("Failed to parse event id")
@ -59,46 +59,89 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error {
return err 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()) { 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 continue
} }
sportID, err := strconv.ParseInt(event.SportID, 10, 64) sportID, err := strconv.ParseInt(event.SportID, 10, 64)
if err != nil { if err != nil {
s.logger.Error("Sport ID is invalid", "event_id", outcome.EventID, "error", err) s.logger.Error("Sport ID is invalid", "event_id", outcome.EventID, "error", err)
isDeleted = false
continue 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) result, err := s.fetchResult(ctx, outcome.EventID, outcome.OddID, outcome.MarketID, sportID, outcome)
if err != nil { 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 continue
} }
// _, err = s.repo.CreateResult(ctx, domain.CreateResult{ outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, result.Status)
// BetOutcomeID: outcome.ID,
// EventID: outcome.EventID,
// OddID: outcome.OddID,
// MarketID: outcome.MarketID,
// Status: result.Status,
// Score: result.Score,
// })
// if err != nil {
// s.logger.Error("Failed to store result", "bet_outcome_id", outcome.ID, "error", err)
// continue
// }
_, err = s.repo.UpdateBetOutcomeStatus(ctx, outcome.ID, result.Status)
if err != nil { if err != nil {
isDeleted = false
s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err) s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err)
continue 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 isDeleted {
if err != nil { // err = s.repo.DeleteEvent(ctx, event.ID)
s.logger.Error("Failed to remove event", "event_id", event.ID, "error", err) // if err != nil {
return err // s.logger.Error("Failed to remove event", "event_id", event.ID, "error", err)
// return err
// }
} }
} }
return nil return nil
@ -248,7 +291,7 @@ func (s *Service) parseFootball(resultRes json.RawMessage, eventID, oddID, marke
corners := parseStats(result.Stats.Corners) corners := parseStats(result.Stats.Corners)
status, err := s.evaluateFootballOutcome(outcome, finalScore, firstHalfScore, corners, result.Events) status, err := s.evaluateFootballOutcome(outcome, finalScore, firstHalfScore, corners, result.Events)
if err != nil { 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 return domain.CreateResult{}, err
} }

View File

@ -1 +0,0 @@
package result

View File

@ -37,3 +37,7 @@ func (s *Service) UpdateTicketOutcomeStatus(ctx context.Context, id int64, statu
func (s *Service) DeleteTicket(ctx context.Context, id int64) error { func (s *Service) DeleteTicket(ctx context.Context, id int64) error {
return s.ticketStore.DeleteTicket(ctx, id) return s.ticketStore.DeleteTicket(ctx, id)
} }
func (s *Service) DeleteOldTickets(ctx context.Context) error {
return s.ticketStore.DeleteOldTickets(ctx)
}

View File

@ -1,8 +1,8 @@
package httpserver package httpserver
import ( import (
// "context"
"context" "context"
"log" "log"
// "time" // "time"
@ -10,6 +10,7 @@ import (
eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
resultsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" resultsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
) )
@ -20,14 +21,14 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
spec string spec string
task func() task func()
}{ }{
{ // {
spec: "0 0 * * * *", // Every 1 hour // spec: "0 0 * * * *", // Every 1 hour
task: func() { // task: func() {
if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
log.Printf("FetchUpcomingEvents error: %v", err) // log.Printf("FetchUpcomingEvents error: %v", err)
} // }
}, // },
}, // },
// { // {
// spec: "*/5 * * * * *", // Every 5 seconds // spec: "*/5 * * * * *", // Every 5 seconds
@ -37,14 +38,14 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
// } // }
// }, // },
// }, // },
{ // {
spec: "0 */15 * * * *", // Every 15 minutes // spec: "0 */15 * * * *", // Every 15 minutes
task: func() { // task: func() {
if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
log.Printf("FetchNonLiveOdds error: %v", err) // log.Printf("FetchNonLiveOdds error: %v", err)
} // }
}, // },
}, // },
// { // {
// spec: "0 */15 * * * *", // spec: "0 */15 * * * *",
// task: func() { // task: func() {
@ -80,6 +81,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
} }
for _, job := range schedule { for _, job := range schedule {
job.task()
if _, err := c.AddFunc(job.spec, job.task); err != nil { if _, err := c.AddFunc(job.spec, job.task); err != nil {
log.Fatalf("Failed to schedule cron job: %v", err) log.Fatalf("Failed to schedule cron job: %v", err)
} }
@ -88,3 +90,34 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
c.Start() c.Start()
log.Println("Cron jobs started for event and odds services") 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")
}

View File

@ -2,8 +2,10 @@ package handlers
import ( import (
"strconv" "strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "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/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
@ -15,7 +17,7 @@ import (
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param createBet body domain.CreateBetReq true "Creates bet" // @Param createBet body domain.CreateBetReq true "Creates bet"
// @Success 200 {object} BetRes // @Success 200 {object} domain.BetRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet [post] // @Router /bet [post]
@ -54,7 +56,7 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param createBet body domain.RandomBetReq true "Create Random bet" // @Param createBet body domain.RandomBetReq true "Create Random bet"
// @Success 200 {object} BetRes // @Success 200 {object} domain.BetRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /random/bet [post] // @Router /random/bet [post]
@ -64,6 +66,45 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64) userID := c.Locals("user_id").(int64)
// role := c.Locals("role").(domain.Role) // 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 var req domain.RandomBetReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
h.logger.Error("Failed to parse RandomBet request", "error", err) 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) 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 { if err != nil {
h.logger.Error("Random Bet failed", "error", err) 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") return fiber.NewError(fiber.StatusInternalServerError, "Unable to create random bet")
} }
@ -92,7 +137,7 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error {
// @Tags bet // @Tags bet
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {array} BetRes // @Success 200 {array} domain.BetRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet [get] // @Router /bet [get]
@ -118,7 +163,7 @@ func (h *Handler) GetAllBet(c *fiber.Ctx) error {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param id path int true "Bet ID" // @Param id path int true "Bet ID"
// @Success 200 {object} BetRes // @Success 200 {object} domain.BetRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet/{id} [get] // @Router /bet/{id} [get]
@ -149,7 +194,7 @@ func (h *Handler) GetBetByID(c *fiber.Ctx) error {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param id path string true "cashout ID" // @Param id path string true "cashout ID"
// @Success 200 {object} BetRes // @Success 200 {object} domain.BetRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet/cashout/{id} [get] // @Router /bet/cashout/{id} [get]

View File

@ -498,7 +498,7 @@ func (h *Handler) GetBranchOperations(c *fiber.Ctx) error {
// @Tags branch // @Tags branch
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {array} BetRes // @Success 200 {array} domain.BetRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /branch/{id}/bets [get] // @Router /branch/{id}/bets [get]

View File

@ -2,6 +2,7 @@ package handlers
import ( import (
"strconv" "strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "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) pageSize := c.QueryInt("page_size", 10)
leagueIDQuery := c.Query("league_id") leagueIDQuery := c.Query("league_id")
sportIDQuery := c.Query("sport_id") sportIDQuery := c.Query("sport_id")
firstStartTimeQuery := c.Query("first_start_time")
lastStartTimeQuery := c.Query("last_start_time")
leagueID := domain.ValidString{ leagueID := domain.ValidString{
Value: leagueIDQuery, Value: leagueIDQuery,
@ -116,7 +119,41 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error {
Valid: sportIDQuery != "", 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) // fmt.Printf("League ID: %v", leagueID)
if err != nil { 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) 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 { if err != nil {
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve prematch odds", nil, nil) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve prematch odds", nil, nil)
} }

View File

@ -1,7 +1,7 @@
include .env include .env
.PHONY: test .PHONY: test
test: test:
@go test ./app @go test ./...
.PHONY: coverage .PHONY: coverage
coverage: coverage:
@mkdir -p coverage @mkdir -p coverage