diff --git a/db/query/odds.sql b/db/query/odds.sql index a508511..c44a691 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -1,43 +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 (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, + 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, - name = EXCLUDED.name, - handicap = EXCLUDED.handicap, - 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, @@ -53,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, @@ -73,23 +81,20 @@ SELECT source, is_active FROM odds -WHERE is_active = true AND source = 'b365api'; - +WHERE is_active = true + AND source = 'b365api'; -- name: GetRawOddsByMarketID :many -SELECT - id, - raw_odds, +SELECT id, + raw_odds, fetched_at FROM odds -WHERE - market_id = $1 AND - is_active = true AND - source = 'b365api' -LIMIT $2 OFFSET $3; - +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, @@ -105,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 57af447..0c5cb37 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1354,53 +1354,6 @@ const docTemplate = `{ } } }, - "/prematch/odds/raw/{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": "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/upcoming/{upcoming_id}": { "get": { "description": "Retrieve prematch odds by upcoming event ID (FI from Bet365) with optional pagination", @@ -1460,6 +1413,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", diff --git a/docs/swagger.json b/docs/swagger.json index 55f1352..60327a4 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1346,53 +1346,6 @@ } } }, - "/prematch/odds/raw/{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": "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/upcoming/{upcoming_id}": { "get": { "description": "Retrieve prematch odds by upcoming event ID (FI from Bet365) with optional pagination", @@ -1452,6 +1405,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", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index b27f68c..7256661 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1704,37 +1704,6 @@ paths: summary: Retrieve prematch odds for an event tags: - prematch - /prematch/odds/raw/{market_id}: - get: - consumes: - - application/json - description: Retrieve raw odds records using a Market ID - parameters: - - 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 /prematch/odds/upcoming/{upcoming_id}: get: consumes: @@ -1775,6 +1744,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/models.go b/gen/db/models.go index 9a9993c..72f7dfc 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -148,7 +148,6 @@ type Odd struct { MarketCategory pgtype.Text `json:"market_category"` MarketID pgtype.Text `json:"market_id"` Name pgtype.Text `json:"name"` - Header pgtype.Text `json:"header"` Handicap pgtype.Text `json:"handicap"` OddsValue pgtype.Float8 `json:"odds_value"` Section string `json:"section"` diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index d4c31c2..846494f 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -253,15 +253,17 @@ SELECT id, fetched_at FROM odds WHERE market_id = $1 + AND fi = $2 AND is_active = true AND source = 'b365api' -LIMIT $2 OFFSET $3 +LIMIT $3 OFFSET $4 ` type GetRawOddsByMarketIDParams struct { - MarketID pgtype.Text - Limit int32 - Offset int32 + MarketID pgtype.Text `json:"market_id"` + Fi pgtype.Text `json:"fi"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` } type GetRawOddsByMarketIDRow struct { @@ -271,7 +273,12 @@ type GetRawOddsByMarketIDRow struct { } func (q *Queries) GetRawOddsByMarketID(ctx context.Context, arg GetRawOddsByMarketIDParams) ([]GetRawOddsByMarketIDRow, error) { - rows, err := q.db.Query(ctx, GetRawOddsByMarketID, arg.MarketID, arg.Limit, arg.Offset) + rows, err := q.db.Query(ctx, GetRawOddsByMarketID, + arg.MarketID, + arg.Fi, + arg.Limit, + arg.Offset, + ) if err != nil { return nil, err } @@ -324,37 +331,15 @@ VALUES ( $13, $14, $15 - ) ON CONFLICT (market_id, name, handicap) DO + ) 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, - 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, + name = EXCLUDED.name, + handicap = EXCLUDED.handicap, fetched_at = EXCLUDED.fetched_at, is_active = EXCLUDED.is_active, source = EXCLUDED.source, diff --git a/internal/repository/odds.go b/internal/repository/odds.go index bb41387..72f2c93 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -14,242 +14,243 @@ import ( ) 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) GetRawOddsByMarketID(ctx context.Context, rawOddsID string) (domain.RawOddsByMarketID, error) { - params := dbgen.GetRawOddsByMarketIDParams{ - MarketID: pgtype.Text{String: rawOddsID, Valid: true}, - Limit: 1, - Offset: 0, - } +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, + } - rows, err := s.queries.GetRawOddsByMarketID(ctx, params) - if err != nil { - return domain.RawOddsByMarketID{}, err - } + rows, err := s.queries.GetRawOddsByMarketID(ctx, params) + if err != nil { + return domain.RawOddsByMarketID{}, err + } - if len(rows) == 0 { - return domain.RawOddsByMarketID{}, fmt.Errorf("no raw odds found for market_id: %s", rawOddsID) - } + if len(rows) == 0 { + return domain.RawOddsByMarketID{}, fmt.Errorf("no raw odds found for market_id: %s", rawOddsID) + } - row := rows[0] + row := rows[0] - var rawOdds []json.RawMessage - if err := json.Unmarshal(row.RawOdds, &rawOdds); err != nil { - return domain.RawOddsByMarketID{}, err - } + 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 + 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) { - params := dbgen.GetPrematchOddsByUpcomingIDParams{ - ID: upcomingID, - Limit: limit, - Offset: offset, - } + params := dbgen.GetPrematchOddsByUpcomingIDParams{ + ID: upcomingID, + Limit: limit, + Offset: offset, + } - 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 2472e99..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) - GetRawOddsByMarketID(ctx context.Context, marketID string) ([]domain.RawOddsByMarketID, 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 d8bce34..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,8 +119,8 @@ func (s *ServiceImpl) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, err return s.store.GetALLPrematchOdds(ctx) } -func (s *ServiceImpl) GetRawOddsByMarketID(ctx context.Context, marketID string) ([]domain.RawOddsByMarketID, error) { - rows, err := s.store.GetRawOddsByMarketID(ctx, marketID) +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 } @@ -132,5 +129,5 @@ func (s *ServiceImpl) GetRawOddsByMarketID(ctx context.Context, marketID string) } 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 2ee4e69..0eeb4ac 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -1,61 +1,58 @@ package httpserver import ( - // "context" - "log" + // "context" - eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" - oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" - "github.com/robfig/cron/v3" + "log" + + eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" + "github.com/robfig/cron/v3" ) func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.Service) { - c := cron.New(cron.WithSeconds()) + c := cron.New(cron.WithSeconds()) - schedule := []struct { - spec string - task func() - }{ + schedule := []struct { + spec string + task func() + }{ - // { - // spec: "*/30 * * * * *", // Every 30 seconds - // task: func() { - // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { - // log.Printf("FetchUpcomingEvents error: %v", err) - // } - // }, - // }, - - - // { - // spec: "*/5 * * * * *", // Every 5 seconds - // task: func() { - // if err := eventService.FetchLiveEvents(context.Background()); err != nil { - // log.Printf("FetchLiveEvents error: %v", err) - // } - // }, - // }, + // { + // spec: "*/30 * * * * *", // Every 30 seconds + // task: func() { + // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { + // log.Printf("FetchUpcomingEvents error: %v", err) + // } + // }, + // }, + // { + // spec: "*/5 * * * * *", // Every 5 seconds + // task: func() { + // if err := eventService.FetchLiveEvents(context.Background()); err != nil { + // log.Printf("FetchLiveEvents error: %v", err) + // } + // }, + // }, - // { - // spec: "*/5 * * * * *", // Every 5 seconds - // task: func() { - // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { - // log.Printf("FetchNonLiveOdds error: %v", err) - // } - // }, - // }, - - - } + // { + // spec: "*/5 * * * * *", // Every 5 seconds + // task: func() { + // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { + // log.Printf("FetchNonLiveOdds error: %v", err) + // } + // }, + // }, + } - - for _, job := range schedule { - if _, err := c.AddFunc(job.spec, job.task); err != nil { - log.Fatalf("Failed to schedule cron job: %v", err) - } - } + 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 event and odds services") -} \ No newline at end of file + c.Start() + log.Println("Cron jobs started for event and odds services") +} diff --git a/internal/web_server/handlers/prematch.go b/internal/web_server/handlers/prematch.go index ea1ac59..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,41 +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) + } } + // GetRawOddsByMarketID // @Summary Retrieve raw odds by Market ID // @Description Retrieve raw odds records using a Market ID // @Tags prematch // @Accept json // @Produce json +// @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/{market_id} [get] +// @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") - if marketID == "" { - return response.WriteJSON(c, fiber.StatusBadRequest, "Missing market_id", nil, nil) - } + 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.GetRawOddsByMarketID(c.Context(), marketID) - if err != nil { - logger.Error("failed to fetch raw odds", "error", err) - 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 @@ -92,15 +100,16 @@ func GetRawOddsByMarketID(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fi // @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 @@ -112,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 @@ -139,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 c8d75a5..7e00e4c 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/:market_id", handlers.GetRawOddsByMarketID(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))