diff --git a/db/query/odds.sql b/db/query/odds.sql index 912b1f9..c44a691 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -1,38 +1,52 @@ -- name: InsertNonLiveOdd :exec INSERT INTO odds ( - event_id, - fi, - market_type, - market_name, - market_category, - market_id, - name, - handicap, - odds_value, - section, - category, - raw_odds, - is_active, - source, - fetched_at -) VALUES ( - $1, $2, $3, $4, $5, $6, $7, - $8, $9, $10, $11, $12, $13, $14, $15 -) -ON CONFLICT (market_id, name, handicap) DO UPDATE SET - odds_value = EXCLUDED.odds_value, - raw_odds = EXCLUDED.raw_odds, - market_type = EXCLUDED.market_type, - market_name = EXCLUDED.market_name, + event_id, + fi, + market_type, + market_name, + market_category, + market_id, + name, + handicap, + odds_value, + section, + category, + raw_odds, + is_active, + source, + fetched_at + ) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15 + ) ON CONFLICT (event_id, market_id) DO +UPDATE +SET odds_value = EXCLUDED.odds_value, + raw_odds = EXCLUDED.raw_odds, + market_type = EXCLUDED.market_type, + market_name = EXCLUDED.market_name, market_category = EXCLUDED.market_category, - fetched_at = EXCLUDED.fetched_at, - is_active = EXCLUDED.is_active, - source = EXCLUDED.source, - fi = EXCLUDED.fi; - + name = EXCLUDED.name, + handicap = EXCLUDED.handicap, + fetched_at = EXCLUDED.fetched_at, + is_active = EXCLUDED.is_active, + source = EXCLUDED.source, + fi = EXCLUDED.fi; -- name: GetPrematchOdds :many -SELECT - event_id, +SELECT event_id, fi, market_type, market_name, @@ -48,11 +62,10 @@ SELECT source, is_active FROM odds -WHERE is_active = true AND source = 'b365api'; - +WHERE is_active = true + AND source = 'b365api'; -- name: GetALLPrematchOdds :many -SELECT - event_id, +SELECT event_id, fi, market_type, market_name, @@ -68,23 +81,20 @@ SELECT source, is_active FROM odds -WHERE is_active = true AND source = 'b365api'; - --- name: GetRawOddsByID :one -SELECT - id, - raw_odds, +WHERE is_active = true + AND source = 'b365api'; +-- name: GetRawOddsByMarketID :many +SELECT id, + raw_odds, fetched_at FROM odds -WHERE - raw_odds @> $1::jsonb AND - is_active = true AND - source = 'b365api' -LIMIT 1; - +WHERE market_id = $1 + AND fi = $2 + AND is_active = true + AND source = 'b365api' +LIMIT $3 OFFSET $4; -- name: GetPrematchOddsByUpcomingID :many -SELECT - o.event_id, +SELECT o.event_id, o.fi, o.market_type, o.market_name, @@ -100,10 +110,10 @@ SELECT o.source, o.is_active FROM odds o -JOIN events e ON o.fi = e.id + 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' + AND e.is_live = false + AND e.status = 'upcoming' + AND o.is_active = true + AND o.source = 'b365api' LIMIT $2 OFFSET $3; \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 4e4fd35..919bb51 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1398,50 +1398,6 @@ const docTemplate = `{ } } }, - "/prematch/odds/raw/{raw_odds_id}": { - "get": { - "description": "Retrieve raw odds by raw odds ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "prematch" - ], - "summary": "Retrieve raw odds by ID", - "parameters": [ - { - "type": "string", - "description": "Raw Odds ID", - "name": "raw_odds_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/domain.RawOddsByID" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/prematch/odds/upcoming/{upcoming_id}": { "get": { "description": "Retrieve prematch odds by upcoming event ID (FI from Bet365) with optional pagination", @@ -1501,6 +1457,60 @@ const docTemplate = `{ } } }, + "/prematch/odds/upcoming/{upcoming_id}/market/{market_id}": { + "get": { + "description": "Retrieve raw odds records using a Market ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve raw odds by Market ID", + "parameters": [ + { + "type": "string", + "description": "Upcoming ID", + "name": "upcoming_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Market ID", + "name": "market_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.RawOddsByMarketID" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/prematch/odds/{event_id}": { "get": { "description": "Retrieve prematch odds for a specific event by event ID", @@ -2748,12 +2758,9 @@ const docTemplate = `{ "BANK" ] }, - "domain.RawOddsByID": { + "domain.RawOddsByMarketID": { "type": "object", "properties": { - "event_id": { - "type": "string" - }, "fetched_at": { "type": "string" }, diff --git a/docs/swagger.json b/docs/swagger.json index e20677f..5ea7099 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1390,50 +1390,6 @@ } } }, - "/prematch/odds/raw/{raw_odds_id}": { - "get": { - "description": "Retrieve raw odds by raw odds ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "prematch" - ], - "summary": "Retrieve raw odds by ID", - "parameters": [ - { - "type": "string", - "description": "Raw Odds ID", - "name": "raw_odds_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/domain.RawOddsByID" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/prematch/odds/upcoming/{upcoming_id}": { "get": { "description": "Retrieve prematch odds by upcoming event ID (FI from Bet365) with optional pagination", @@ -1493,6 +1449,60 @@ } } }, + "/prematch/odds/upcoming/{upcoming_id}/market/{market_id}": { + "get": { + "description": "Retrieve raw odds records using a Market ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve raw odds by Market ID", + "parameters": [ + { + "type": "string", + "description": "Upcoming ID", + "name": "upcoming_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Market ID", + "name": "market_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.RawOddsByMarketID" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/prematch/odds/{event_id}": { "get": { "description": "Retrieve prematch odds for a specific event by event ID", @@ -2740,12 +2750,9 @@ "BANK" ] }, - "domain.RawOddsByID": { + "domain.RawOddsByMarketID": { "type": "object", "properties": { - "event_id": { - "type": "string" - }, "fetched_at": { "type": "string" }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 306272a..f03a301 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -93,10 +93,8 @@ definitions: - TELEBIRR_TRANSACTION - ARIFPAY_TRANSACTION - BANK - domain.RawOddsByID: + domain.RawOddsByMarketID: properties: - event_id: - type: string fetched_at: type: string id: @@ -1829,35 +1827,6 @@ paths: summary: Retrieve prematch odds for an event tags: - prematch - /prematch/odds/raw/{raw_odds_id}: - get: - consumes: - - application/json - description: Retrieve raw odds by raw odds ID - parameters: - - description: Raw Odds ID - in: path - name: raw_odds_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/domain.RawOddsByID' - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Retrieve raw odds by ID - tags: - - prematch /prematch/odds/upcoming/{upcoming_id}: get: consumes: @@ -1898,6 +1867,42 @@ paths: summary: Retrieve prematch odds by upcoming ID (FI) tags: - prematch + /prematch/odds/upcoming/{upcoming_id}/market/{market_id}: + get: + consumes: + - application/json + description: Retrieve raw odds records using a Market ID + parameters: + - description: Upcoming ID + in: path + name: upcoming_id + required: true + type: string + - description: Market ID + in: path + name: market_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.RawOddsByMarketID' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Retrieve raw odds by Market ID + tags: + - prematch /search/branch: get: consumes: diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index f7e88ab..846494f 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -12,8 +12,7 @@ import ( ) const GetALLPrematchOdds = `-- name: GetALLPrematchOdds :many -SELECT - event_id, +SELECT event_id, fi, market_type, market_name, @@ -29,7 +28,8 @@ SELECT source, is_active FROM odds -WHERE is_active = true AND source = 'b365api' +WHERE is_active = true + AND source = 'b365api' ` type GetALLPrematchOddsRow struct { @@ -87,8 +87,7 @@ func (q *Queries) GetALLPrematchOdds(ctx context.Context) ([]GetALLPrematchOddsR } const GetPrematchOdds = `-- name: GetPrematchOdds :many -SELECT - event_id, +SELECT event_id, fi, market_type, market_name, @@ -104,7 +103,8 @@ SELECT source, is_active FROM odds -WHERE is_active = true AND source = 'b365api' +WHERE is_active = true + AND source = 'b365api' ` type GetPrematchOddsRow struct { @@ -162,8 +162,7 @@ func (q *Queries) GetPrematchOdds(ctx context.Context) ([]GetPrematchOddsRow, er } const GetPrematchOddsByUpcomingID = `-- name: GetPrematchOddsByUpcomingID :many -SELECT - o.event_id, +SELECT o.event_id, o.fi, o.market_type, o.market_name, @@ -179,12 +178,12 @@ SELECT o.source, o.is_active FROM odds o -JOIN events e ON o.fi = e.id + 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' + AND e.is_live = false + AND e.status = 'upcoming' + AND o.is_active = true + AND o.source = 'b365api' LIMIT $2 OFFSET $3 ` @@ -248,63 +247,103 @@ func (q *Queries) GetPrematchOddsByUpcomingID(ctx context.Context, arg GetPremat return items, nil } -const GetRawOddsByID = `-- name: GetRawOddsByID :one -SELECT - id, - raw_odds, +const GetRawOddsByMarketID = `-- name: GetRawOddsByMarketID :many +SELECT id, + raw_odds, fetched_at FROM odds -WHERE - raw_odds @> $1::jsonb AND - is_active = true AND - source = 'b365api' -LIMIT 1 +WHERE market_id = $1 + AND fi = $2 + AND is_active = true + AND source = 'b365api' +LIMIT $3 OFFSET $4 ` -type GetRawOddsByIDRow struct { +type GetRawOddsByMarketIDParams struct { + MarketID pgtype.Text `json:"market_id"` + Fi pgtype.Text `json:"fi"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +type GetRawOddsByMarketIDRow struct { ID int32 `json:"id"` RawOdds []byte `json:"raw_odds"` FetchedAt pgtype.Timestamp `json:"fetched_at"` } -func (q *Queries) GetRawOddsByID(ctx context.Context, dollar_1 []byte) (GetRawOddsByIDRow, error) { - row := q.db.QueryRow(ctx, GetRawOddsByID, dollar_1) - var i GetRawOddsByIDRow - err := row.Scan(&i.ID, &i.RawOdds, &i.FetchedAt) - return i, err +func (q *Queries) GetRawOddsByMarketID(ctx context.Context, arg GetRawOddsByMarketIDParams) ([]GetRawOddsByMarketIDRow, error) { + rows, err := q.db.Query(ctx, GetRawOddsByMarketID, + arg.MarketID, + arg.Fi, + arg.Limit, + arg.Offset, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetRawOddsByMarketIDRow + for rows.Next() { + var i GetRawOddsByMarketIDRow + if err := rows.Scan(&i.ID, &i.RawOdds, &i.FetchedAt); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil } const InsertNonLiveOdd = `-- name: InsertNonLiveOdd :exec INSERT INTO odds ( - event_id, - fi, - market_type, - market_name, - market_category, - market_id, - name, - handicap, - odds_value, - section, - category, - raw_odds, - is_active, - source, - fetched_at -) VALUES ( - $1, $2, $3, $4, $5, $6, $7, - $8, $9, $10, $11, $12, $13, $14, $15 -) -ON CONFLICT (market_id, name, handicap) DO UPDATE SET - odds_value = EXCLUDED.odds_value, - raw_odds = EXCLUDED.raw_odds, - market_type = EXCLUDED.market_type, - market_name = EXCLUDED.market_name, + event_id, + fi, + market_type, + market_name, + market_category, + market_id, + name, + handicap, + odds_value, + section, + category, + raw_odds, + is_active, + source, + fetched_at + ) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15 + ) ON CONFLICT (event_id, market_id) DO +UPDATE +SET odds_value = EXCLUDED.odds_value, + raw_odds = EXCLUDED.raw_odds, + market_type = EXCLUDED.market_type, + market_name = EXCLUDED.market_name, market_category = EXCLUDED.market_category, - fetched_at = EXCLUDED.fetched_at, - is_active = EXCLUDED.is_active, - source = EXCLUDED.source, - fi = EXCLUDED.fi + name = EXCLUDED.name, + handicap = EXCLUDED.handicap, + fetched_at = EXCLUDED.fetched_at, + is_active = EXCLUDED.is_active, + source = EXCLUDED.source, + fi = EXCLUDED.fi ` type InsertNonLiveOddParams struct { diff --git a/internal/domain/odds.go b/internal/domain/odds.go index df2de7e..9992490 100644 --- a/internal/domain/odds.go +++ b/internal/domain/odds.go @@ -38,9 +38,8 @@ type Odd struct { Source string `json:"source"` IsActive bool `json:"is_active"` } -type RawOddsByID struct { - ID int64 `json:"id"` - EventID string `json:"event_id"` - RawOdds []RawMessage `json:"raw_odds"` - FetchedAt time.Time `json:"fetched_at"` +type RawOddsByMarketID struct { + ID int64 `json:"id"` + RawOdds []RawMessage `json:"raw_odds"` + FetchedAt time.Time `json:"fetched_at"` } \ No newline at end of file diff --git a/internal/repository/odds.go b/internal/repository/odds.go index a8573a3..72f2c93 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -1,246 +1,256 @@ package repository import ( - "context" - "encoding/json" - "os" - "strconv" - "time" + "context" + "encoding/json" + "fmt" + "os" + "strconv" + "time" - dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" - "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - "github.com/jackc/pgx/v5/pgtype" + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/jackc/pgx/v5/pgtype" ) func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error { - if len(m.Odds) == 0 { - return nil - } + if len(m.Odds) == 0 { + return nil + } - for _, raw := range m.Odds { - var item map[string]interface{} - if err := json.Unmarshal(raw, &item); err != nil { - continue - } + for _, raw := range m.Odds { + var item map[string]interface{} + if err := json.Unmarshal(raw, &item); err != nil { + continue + } - name := getString(item["name"]) - handicap := getString(item["handicap"]) - oddsVal := getFloat(item["odds"]) + name := getString(item["name"]) + handicap := getString(item["handicap"]) + oddsVal := getFloat(item["odds"]) - rawOddsBytes, _ := json.Marshal(m.Odds) + rawOddsBytes, _ := json.Marshal(m.Odds) - params := dbgen.InsertNonLiveOddParams{ - EventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""}, - Fi: pgtype.Text{String: m.FI, Valid: m.FI != ""}, - MarketType: m.MarketType, - MarketName: pgtype.Text{String: m.MarketName, Valid: m.MarketName != ""}, - MarketCategory: pgtype.Text{String: m.MarketCategory, Valid: m.MarketCategory != ""}, - MarketID: pgtype.Text{String: m.MarketID, Valid: m.MarketID != ""}, - Name: pgtype.Text{String: name, Valid: name != ""}, - Handicap: pgtype.Text{String: handicap, Valid: handicap != ""}, - OddsValue: pgtype.Float8{Float64: oddsVal, Valid: oddsVal != 0}, - Section: m.MarketCategory, - Category: pgtype.Text{Valid: false}, - RawOdds: rawOddsBytes, - IsActive: pgtype.Bool{Bool: true, Valid: true}, - Source: pgtype.Text{String: "b365api", Valid: true}, - FetchedAt: pgtype.Timestamp{Time: time.Now(), Valid: true}, - } + params := dbgen.InsertNonLiveOddParams{ + EventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""}, + Fi: pgtype.Text{String: m.FI, Valid: m.FI != ""}, + MarketType: m.MarketType, + MarketName: pgtype.Text{String: m.MarketName, Valid: m.MarketName != ""}, + MarketCategory: pgtype.Text{String: m.MarketCategory, Valid: m.MarketCategory != ""}, + MarketID: pgtype.Text{String: m.MarketID, Valid: m.MarketID != ""}, + Name: pgtype.Text{String: name, Valid: name != ""}, + Handicap: pgtype.Text{String: handicap, Valid: handicap != ""}, + OddsValue: pgtype.Float8{Float64: oddsVal, Valid: oddsVal != 0}, + Section: m.MarketCategory, + Category: pgtype.Text{Valid: false}, + RawOdds: rawOddsBytes, + IsActive: pgtype.Bool{Bool: true, Valid: true}, + Source: pgtype.Text{String: "b365api", Valid: true}, + FetchedAt: pgtype.Timestamp{Time: time.Now(), Valid: true}, + } - err := s.queries.InsertNonLiveOdd(ctx, params) - if err != nil { - _ = writeFailedMarketLog(m, err) - continue - } - } - return nil + err := s.queries.InsertNonLiveOdd(ctx, params) + if err != nil { + _ = writeFailedMarketLog(m, err) + continue + } + } + return nil } func writeFailedMarketLog(m domain.Market, err error) error { - logDir := "logs" - logFile := logDir + "/failed_markets.log" + logDir := "logs" + logFile := logDir + "/failed_markets.log" - if mkErr := os.MkdirAll(logDir, 0755); mkErr != nil { - return mkErr - } + if mkErr := os.MkdirAll(logDir, 0755); mkErr != nil { + return mkErr + } - f, fileErr := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if fileErr != nil { - return fileErr - } - defer f.Close() + f, fileErr := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if fileErr != nil { + return fileErr + } + defer f.Close() - entry := struct { - Time string `json:"time"` - Error string `json:"error"` - Record domain.Market `json:"record"` - }{ - Time: time.Now().Format(time.RFC3339), - Error: err.Error(), - Record: m, - } + entry := struct { + Time string `json:"time"` + Error string `json:"error"` + Record domain.Market `json:"record"` + }{ + Time: time.Now().Format(time.RFC3339), + Error: err.Error(), + Record: m, + } - jsonData, _ := json.MarshalIndent(entry, "", " ") - _, writeErr := f.WriteString(string(jsonData) + "\n\n") - return writeErr + jsonData, _ := json.MarshalIndent(entry, "", " ") + _, writeErr := f.WriteString(string(jsonData) + "\n\n") + return writeErr } func getString(v interface{}) string { - if s, ok := v.(string); ok { - return s - } - return "" + if s, ok := v.(string); ok { + return s + } + return "" } func getFloat(v interface{}) float64 { - if s, ok := v.(string); ok { - f, err := strconv.ParseFloat(s, 64) - if err == nil { - return f - } - } - return 0 + if s, ok := v.(string); ok { + f, err := strconv.ParseFloat(s, 64) + if err == nil { + return f + } + } + return 0 } func (s *Store) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) { - odds, err := s.queries.GetPrematchOdds(ctx) - if err != nil { - return nil, err - } + odds, err := s.queries.GetPrematchOdds(ctx) + if err != nil { + return nil, err + } - domainOdds := make([]domain.Odd, len(odds)) - for i, odd := range odds { - domainOdds[i] = domain.Odd{ - EventID: odd.EventID.String, - Fi: odd.Fi.String, - MarketType: odd.MarketType, - MarketName: odd.MarketName.String, - MarketCategory: odd.MarketCategory.String, - MarketID: odd.MarketID.String, - Name: odd.Name.String, - Handicap: odd.Handicap.String, - OddsValue: odd.OddsValue.Float64, - Section: odd.Section, - Category: odd.Category.String, - RawOdds: func() []domain.RawMessage { - var rawOdds []domain.RawMessage - if err := json.Unmarshal(odd.RawOdds, &rawOdds); err != nil { - rawOdds = nil - } - return rawOdds - }(), - FetchedAt: odd.FetchedAt.Time, - Source: odd.Source.String, - IsActive: odd.IsActive.Bool, - } - } + domainOdds := make([]domain.Odd, len(odds)) + for i, odd := range odds { + domainOdds[i] = domain.Odd{ + EventID: odd.EventID.String, + Fi: odd.Fi.String, + MarketType: odd.MarketType, + MarketName: odd.MarketName.String, + MarketCategory: odd.MarketCategory.String, + MarketID: odd.MarketID.String, + Name: odd.Name.String, + Handicap: odd.Handicap.String, + OddsValue: odd.OddsValue.Float64, + Section: odd.Section, + Category: odd.Category.String, + RawOdds: func() []domain.RawMessage { + var rawOdds []domain.RawMessage + if err := json.Unmarshal(odd.RawOdds, &rawOdds); err != nil { + rawOdds = nil + } + return rawOdds + }(), + FetchedAt: odd.FetchedAt.Time, + Source: odd.Source.String, + IsActive: odd.IsActive.Bool, + } + } - return domainOdds, nil + return domainOdds, nil } func (s *Store) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error) { - rows, err := s.queries.GetALLPrematchOdds(ctx) - if err != nil { - return nil, err - } + rows, err := s.queries.GetALLPrematchOdds(ctx) + if err != nil { + return nil, err + } - domainOdds := make([]domain.Odd, len(rows)) - for i, row := range rows { - domainOdds[i] = domain.Odd{ - // ID: int64(row.ID), - EventID: row.EventID.String, - Fi: row.Fi.String, - MarketType: row.MarketType, - MarketName: row.MarketName.String, - MarketCategory: row.MarketCategory.String, - MarketID: row.MarketID.String, - Name: row.Name.String, - Handicap: row.Handicap.String, - OddsValue: row.OddsValue.Float64, - Section: row.Section, - Category: row.Category.String, - RawOdds: func() []domain.RawMessage { - var rawOdds []domain.RawMessage - if err := json.Unmarshal(row.RawOdds, &rawOdds); err != nil { - rawOdds = nil - } - return rawOdds - }(), - FetchedAt: row.FetchedAt.Time, - Source: row.Source.String, - IsActive: row.IsActive.Bool, - } - } + domainOdds := make([]domain.Odd, len(rows)) + for i, row := range rows { + domainOdds[i] = domain.Odd{ + // ID: int64(row.ID), + EventID: row.EventID.String, + Fi: row.Fi.String, + MarketType: row.MarketType, + MarketName: row.MarketName.String, + MarketCategory: row.MarketCategory.String, + MarketID: row.MarketID.String, + Name: row.Name.String, + Handicap: row.Handicap.String, + OddsValue: row.OddsValue.Float64, + Section: row.Section, + Category: row.Category.String, + RawOdds: func() []domain.RawMessage { + var rawOdds []domain.RawMessage + if err := json.Unmarshal(row.RawOdds, &rawOdds); err != nil { + rawOdds = nil + } + return rawOdds + }(), + FetchedAt: row.FetchedAt.Time, + Source: row.Source.String, + IsActive: row.IsActive.Bool, + } + } - return domainOdds, nil + return domainOdds, nil } -func (s *Store) GetRawOddsByID(ctx context.Context, rawOddsID string) (domain.RawOddsByID, error) { - jsonFilter := `[{"id":"` + rawOddsID + `"}]` +func (s *Store) GetRawOddsByMarketID(ctx context.Context, rawOddsID string, upcomingID string) (domain.RawOddsByMarketID, error) { + params := dbgen.GetRawOddsByMarketIDParams{ + MarketID: pgtype.Text{String: rawOddsID, Valid: true}, + Fi: pgtype.Text{String: upcomingID, Valid: true}, + Limit: 1, + Offset: 0, + } - odd, err := s.queries.GetRawOddsByID(ctx, []byte(jsonFilter)) - if err != nil { - return domain.RawOddsByID{}, err - } + rows, err := s.queries.GetRawOddsByMarketID(ctx, params) + if err != nil { + return domain.RawOddsByMarketID{}, err + } - var rawOdds []json.RawMessage - if err := json.Unmarshal(odd.RawOdds, &rawOdds); err != nil { - return domain.RawOddsByID{}, err - } + if len(rows) == 0 { + return domain.RawOddsByMarketID{}, fmt.Errorf("no raw odds found for market_id: %s", rawOddsID) + } - return domain.RawOddsByID{ - ID: int64(odd.ID), - RawOdds: func() []domain.RawMessage { - converted := make([]domain.RawMessage, len(rawOdds)) - for i, r := range rawOdds { - converted[i] = domain.RawMessage(r) - } - return converted - }(), - FetchedAt: odd.FetchedAt.Time, - }, nil + row := rows[0] + + var rawOdds []json.RawMessage + if err := json.Unmarshal(row.RawOdds, &rawOdds); err != nil { + return domain.RawOddsByMarketID{}, err + } + + return domain.RawOddsByMarketID{ + ID: int64(row.ID), + RawOdds: func() []domain.RawMessage { + converted := make([]domain.RawMessage, len(rawOdds)) + for i, r := range rawOdds { + converted[i] = domain.RawMessage(r) + } + return converted + }(), + FetchedAt: row.FetchedAt.Time, + }, nil } func (s *Store) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset int32) ([]domain.Odd, error) { - // Prepare query parameters - params := dbgen.GetPrematchOddsByUpcomingIDParams{ - ID: upcomingID, - Limit: limit, - Offset: offset, - } + params := dbgen.GetPrematchOddsByUpcomingIDParams{ + ID: upcomingID, + Limit: limit, + Offset: offset, + } - // Execute the query - odds, err := s.queries.GetPrematchOddsByUpcomingID(ctx, params) - if err != nil { - return nil, err - } + odds, err := s.queries.GetPrematchOddsByUpcomingID(ctx, params) + 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 - } + // 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 + } - domainOdds[i] = domain.Odd{ - EventID: odd.EventID.String, - Fi: odd.Fi.String, - MarketType: odd.MarketType, - MarketName: odd.MarketName.String, - MarketCategory: odd.MarketCategory.String, - MarketID: odd.MarketID.String, - Name: odd.Name.String, - Handicap: odd.Handicap.String, - OddsValue: odd.OddsValue.Float64, - Section: odd.Section, - Category: odd.Category.String, - RawOdds: rawOdds, - FetchedAt: odd.FetchedAt.Time, - Source: odd.Source.String, - IsActive: odd.IsActive.Bool, - } - } + domainOdds[i] = domain.Odd{ + EventID: odd.EventID.String, + Fi: odd.Fi.String, + MarketType: odd.MarketType, + MarketName: odd.MarketName.String, + MarketCategory: odd.MarketCategory.String, + MarketID: odd.MarketID.String, + Name: odd.Name.String, + Handicap: odd.Handicap.String, + OddsValue: odd.OddsValue.Float64, + Section: odd.Section, + Category: odd.Category.String, + RawOdds: rawOdds, + FetchedAt: odd.FetchedAt.Time, + Source: odd.Source.String, + IsActive: odd.IsActive.Bool, + } + } - return domainOdds, nil -} \ No newline at end of file + return domainOdds, nil +} diff --git a/internal/services/odds/port.go b/internal/services/odds/port.go index 8805a66..eb3d3e6 100644 --- a/internal/services/odds/port.go +++ b/internal/services/odds/port.go @@ -10,7 +10,5 @@ type Service interface { FetchNonLiveOdds(ctx context.Context) error GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error) - GetRawOddsByID(ctx context.Context, rawOddsID string) ([]domain.RawOddsByID, error) - - + GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) ([]domain.RawOddsByMarketID, error) } diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 9b31a94..2ae8e4d 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -22,7 +22,6 @@ func New(token string, store *repository.Store) *ServiceImpl { return &ServiceImpl{token: token, store: store} } - func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { eventIDs, err := s.store.GetAllUpcomingEvents(ctx) if err != nil { @@ -30,43 +29,43 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { return err } - for _, event := range eventIDs { - eventID := event.ID - prematchURL := "https://api.b365api.com/v3/bet365/prematch?token=" + s.token + "&FI=" + eventID - log.Printf("📡 Fetching prematch odds for event ID: %s", eventID) - resp, err := http.Get(prematchURL) - if err != nil { - log.Printf("❌ Failed to fetch prematch odds for event %s: %v", eventID, err) - continue - } - defer resp.Body.Close() - - body, _ := io.ReadAll(resp.Body) - var oddsData struct { - Success int `json:"success"` - Results []struct { - EventID string `json:"event_id"` - FI string `json:"FI"` - Main OddsSection `json:"main"` - } `json:"results"` - } - if err := json.Unmarshal(body, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { - log.Printf("❌ Invalid prematch data for event %s", eventID) - continue - } - - result := oddsData.Results[0] - finalID := result.EventID - if finalID == "" { - finalID = result.FI - } - if finalID == "" { - log.Printf("⚠️ Skipping event %s with no valid ID", eventID) - continue - } - - s.storeSection(ctx, finalID, result.FI, "main", result.Main) - } + for _, event := range eventIDs { + eventID := event.ID + prematchURL := "https://api.b365api.com/v3/bet365/prematch?token=" + s.token + "&FI=" + eventID + log.Printf("📡 Fetching prematch odds for event ID: %s", eventID) + resp, err := http.Get(prematchURL) + if err != nil { + log.Printf("❌ Failed to fetch prematch odds for event %s: %v", eventID, err) + continue + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + var oddsData struct { + Success int `json:"success"` + Results []struct { + EventID string `json:"event_id"` + FI string `json:"FI"` + Main OddsSection `json:"main"` + } `json:"results"` + } + if err := json.Unmarshal(body, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { + log.Printf("❌ Invalid prematch data for event %s", eventID) + continue + } + + result := oddsData.Results[0] + finalID := result.EventID + if finalID == "" { + finalID = result.FI + } + if finalID == "" { + log.Printf("⚠️ Skipping event %s with no valid ID", eventID) + continue + } + + s.storeSection(ctx, finalID, result.FI, "main", result.Main) + } return nil } @@ -108,12 +107,10 @@ type OddsMarket struct { } type OddsSection struct { - UpdatedAt string `json:"updated_at"` + UpdatedAt string `json:"updated_at"` Sp map[string]OddsMarket `json:"sp"` } - - func (s *ServiceImpl) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) { return s.store.GetPrematchOdds(ctx, eventID) } @@ -122,13 +119,15 @@ func (s *ServiceImpl) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, err return s.store.GetALLPrematchOdds(ctx) } -func (s *ServiceImpl) GetRawOddsByID(ctx context.Context, rawOddsID string) ([]domain.RawOddsByID, error) { - rawOdds, err := s.store.GetRawOddsByID(ctx, rawOddsID) +func (s *ServiceImpl) GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) ([]domain.RawOddsByMarketID, error) { + rows, err := s.store.GetRawOddsByMarketID(ctx, marketID, upcomingID) if err != nil { return nil, err } - return []domain.RawOddsByID{rawOdds}, nil + + return []domain.RawOddsByMarketID{rows}, nil } + func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset int32) ([]domain.Odd, error) { - return s.store.GetPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset) + return s.store.GetPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset) } diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 461c0cb..0eeb4ac 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -1,6 +1,8 @@ package httpserver import ( + // "context" + "log" eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" @@ -35,7 +37,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S // }, // { - // spec: "*/30 * * * * *", // Every 30 seconds + // spec: "*/5 * * * * *", // Every 5 seconds // task: func() { // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { // log.Printf("FetchNonLiveOdds error: %v", err) diff --git a/internal/web_server/handlers/prematch.go b/internal/web_server/handlers/prematch.go index 17e4de4..efa3b00 100644 --- a/internal/web_server/handlers/prematch.go +++ b/internal/web_server/handlers/prematch.go @@ -22,21 +22,22 @@ import ( // @Failure 500 {object} response.APIResponse // @Router /prematch/odds/{event_id} [get] func GetPrematchOdds(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.Handler { - return func(c *fiber.Ctx) error { - eventID := c.Params("event_id") - if eventID == "" { - return response.WriteJSON(c, fiber.StatusBadRequest, "Missing event_id", nil, nil) - } + return func(c *fiber.Ctx) error { + eventID := c.Params("event_id") + if eventID == "" { + return response.WriteJSON(c, fiber.StatusBadRequest, "Missing event_id", nil, nil) + } - odds, err := prematchSvc.GetPrematchOdds(c.Context(), eventID) - if err != nil { - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve odds", nil, nil) - } + odds, err := prematchSvc.GetPrematchOdds(c.Context(), eventID) + if err != nil { + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve odds", nil, nil) + } - return response.WriteJSON(c, fiber.StatusOK, "Prematch odds retrieved successfully", odds, nil) - } + return response.WriteJSON(c, fiber.StatusOK, "Prematch odds retrieved successfully", odds, nil) + } } -//GetALLPrematchOdds + +// GetALLPrematchOdds // @Summary Retrieve all prematch odds // @Description Retrieve all prematch odds from the database // @Tags prematch @@ -46,40 +47,48 @@ func GetPrematchOdds(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.H // @Failure 500 {object} response.APIResponse // @Router /prematch/odds [get] func GetALLPrematchOdds(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.Handler { - return func(c *fiber.Ctx) error { - odds, err := prematchSvc.GetALLPrematchOdds(c.Context()) - if err != nil { - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve all prematch odds", nil, nil) - } + return func(c *fiber.Ctx) error { + odds, err := prematchSvc.GetALLPrematchOdds(c.Context()) + if err != nil { + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve all prematch odds", nil, nil) + } - return response.WriteJSON(c, fiber.StatusOK, "All prematch odds retrieved successfully", odds, nil) - } + return response.WriteJSON(c, fiber.StatusOK, "All prematch odds retrieved successfully", odds, nil) + } } -// GetRawOddsByID -// @Summary Retrieve raw odds by ID -// @Description Retrieve raw odds by raw odds ID + +// GetRawOddsByMarketID +// @Summary Retrieve raw odds by Market ID +// @Description Retrieve raw odds records using a Market ID // @Tags prematch // @Accept json // @Produce json -// @Param raw_odds_id path string true "Raw Odds ID" -// @Success 200 {object} domain.RawOddsByID +// @Param upcoming_id path string true "Upcoming ID" +// @Param market_id path string true "Market ID" +// @Success 200 {array} domain.RawOddsByMarketID // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /prematch/odds/raw/{raw_odds_id} [get] -func GetRawOddsByID(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.Handler { - return func(c *fiber.Ctx) error { - rawOddsID := c.Params("raw_odds_id") - if rawOddsID == "" { - return response.WriteJSON(c, fiber.StatusBadRequest, "Missing raw_odds_id", nil, nil) - } +// @Router /prematch/odds/upcoming/{upcoming_id}/market/{market_id} [get] +func GetRawOddsByMarketID(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.Handler { + return func(c *fiber.Ctx) error { + marketID := c.Params("market_id") + upcomingID := c.Params("upcoming_id") + if marketID == "" { + return response.WriteJSON(c, fiber.StatusBadRequest, "Missing market_id", nil, nil) + } - rawOdds, err := prematchSvc.GetRawOddsByID(c.Context(), rawOddsID) - if err != nil { - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve raw odds", nil, nil) - } + if upcomingID == "" { + return response.WriteJSON(c, fiber.StatusBadRequest, "Missing upcoming_id", nil, nil) + } - return response.WriteJSON(c, fiber.StatusOK, "Raw odds retrieved successfully", rawOdds, nil) - } + rawOdds, err := prematchSvc.GetRawOddsByMarketID(c.Context(), marketID, upcomingID) + if err != nil { + logger.Error("failed to fetch raw odds", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve raw odds", err, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "Raw odds retrieved successfully", rawOdds, nil) + } } // @Summary Retrieve all upcoming events @@ -91,15 +100,16 @@ func GetRawOddsByID(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.Ha // @Failure 500 {object} response.APIResponse // @Router /prematch/events [get] func GetAllUpcomingEvents(logger *slog.Logger, eventSvc event.Service) fiber.Handler { - return func(c *fiber.Ctx) error { - events, err := eventSvc.GetAllUpcomingEvents(c.Context()) - if err != nil { - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve all upcoming events", nil, nil) - } + return func(c *fiber.Ctx) error { + events, err := eventSvc.GetAllUpcomingEvents(c.Context()) + if err != nil { + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve all upcoming events", nil, nil) + } - return response.WriteJSON(c, fiber.StatusOK, "All upcoming events retrieved successfully", events, nil) - } + return response.WriteJSON(c, fiber.StatusOK, "All upcoming events retrieved successfully", events, nil) + } } + // @Summary Retrieve an upcoming by ID // @Description Retrieve an upcoming event by ID // @Tags prematch @@ -111,20 +121,21 @@ func GetAllUpcomingEvents(logger *slog.Logger, eventSvc event.Service) fiber.Han // @Failure 500 {object} response.APIResponse // @Router /prematch/events/{id} [get] func GetUpcomingEventByID(logger *slog.Logger, eventSvc event.Service) fiber.Handler { - return func(c *fiber.Ctx) error { - id := c.Params("id") - if id == "" { - return response.WriteJSON(c, fiber.StatusBadRequest, "Missing id", nil, nil) - } + return func(c *fiber.Ctx) error { + id := c.Params("id") + if id == "" { + return response.WriteJSON(c, fiber.StatusBadRequest, "Missing id", nil, nil) + } - event, err := eventSvc.GetUpcomingEventByID(c.Context(), id) - if err != nil { - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve upcoming event", nil, nil) - } + event, err := eventSvc.GetUpcomingEventByID(c.Context(), id) + if err != nil { + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve upcoming event", nil, nil) + } - return response.WriteJSON(c, fiber.StatusOK, "Upcoming event retrieved successfully", event, nil) - } + return response.WriteJSON(c, fiber.StatusOK, "Upcoming event retrieved successfully", event, nil) + } } + // @Summary Retrieve prematch odds by upcoming ID (FI) // @Description Retrieve prematch odds by upcoming event ID (FI from Bet365) with optional pagination // @Tags prematch @@ -138,28 +149,27 @@ func GetUpcomingEventByID(logger *slog.Logger, eventSvc event.Service) fiber.Han // @Failure 500 {object} response.APIResponse // @Router /prematch/odds/upcoming/{upcoming_id} [get] func GetPrematchOddsByUpcomingID(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.Handler { - return func(c *fiber.Ctx) error { - upcomingID := c.Params("upcoming_id") - if upcomingID == "" { - return response.WriteJSON(c, fiber.StatusBadRequest, "Missing upcoming_id", nil, nil) - } + return func(c *fiber.Ctx) error { + upcomingID := c.Params("upcoming_id") + if upcomingID == "" { + return response.WriteJSON(c, fiber.StatusBadRequest, "Missing upcoming_id", nil, nil) + } - limit, err := strconv.Atoi(c.Query("limit", "10")) // Default limit is 10 - if err != nil || limit <= 0 { - return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid limit value", nil, nil) - } + limit, err := strconv.Atoi(c.Query("limit", "10")) // Default limit is 10 + if err != nil || limit <= 0 { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid limit value", nil, nil) + } - offset, err := strconv.Atoi(c.Query("offset", "0")) // Default offset is 0 - if err != nil || offset < 0 { - return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid offset value", nil, nil) - } + offset, err := strconv.Atoi(c.Query("offset", "0")) // Default offset is 0 + if err != nil || offset < 0 { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid offset value", nil, nil) + } - odds, err := prematchSvc.GetPrematchOddsByUpcomingID(c.Context(), upcomingID, int32(limit), int32(offset)) - if err != nil { - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve prematch odds", nil, nil) - } + odds, err := prematchSvc.GetPrematchOddsByUpcomingID(c.Context(), upcomingID, int32(limit), int32(offset)) + if err != nil { + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve prematch odds", nil, nil) + } - return response.WriteJSON(c, fiber.StatusOK, "Prematch odds retrieved successfully", odds, nil) - } + return response.WriteJSON(c, fiber.StatusOK, "Prematch odds retrieved successfully", odds, nil) + } } - diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 5de11e4..7547aa9 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -59,7 +59,7 @@ func (a *App) initAppRoutes() { a.fiber.Get("/prematch/odds/:event_id", handlers.GetPrematchOdds(a.logger, a.prematchSvc)) a.fiber.Get("/prematch/odds", handlers.GetALLPrematchOdds(a.logger, a.prematchSvc)) - a.fiber.Get("/prematch/odds/raw/:raw_odds_id", handlers.GetRawOddsByID(a.logger, a.prematchSvc)) + a.fiber.Get("/prematch/odds/upcoming/:upcoming_id/market/:market_id", handlers.GetRawOddsByMarketID(a.logger, a.prematchSvc)) a.fiber.Get("/prematch/events/:id", handlers.GetUpcomingEventByID(a.logger, a.eventSvc)) a.fiber.Get("/prematch/events", handlers.GetAllUpcomingEvents(a.logger, a.eventSvc))