fix: result and event service fixes

This commit is contained in:
Samuel Tariku 2025-06-07 07:58:39 +03:00
parent 2586f3752d
commit efc51e3b72
41 changed files with 956 additions and 737 deletions

View File

@ -86,7 +86,7 @@ func main() {
branchSvc := branch.NewService(store) branchSvc := branch.NewService(store)
companySvc := company.NewService(store) companySvc := company.NewService(store)
betSvc := bet.NewService(store, eventSvc, oddsSvc, *walletSvc, *branchSvc, logger) betSvc := bet.NewService(store, eventSvc, oddsSvc, *walletSvc, *branchSvc, logger)
resultSvc := result.NewService(store, cfg, logger, *betSvc) resultSvc := result.NewService(store, cfg, logger, *betSvc, oddsSvc, eventSvc)
notificationRepo := repository.NewNotificationRepository(store) notificationRepo := repository.NewNotificationRepository(store)
referalRepo := repository.NewReferralRepository(store) referalRepo := repository.NewReferralRepository(store)
vitualGameRepo := repository.NewVirtualGameRepository(store) vitualGameRepo := repository.NewVirtualGameRepository(store)

View File

@ -167,6 +167,10 @@ SELECT id,
fetched_at fetched_at
FROM events FROM events
WHERE start_time < now() WHERE start_time < now()
and (
status = sqlc.narg('status')
OR sqlc.narg('status') IS NULL
)
ORDER BY start_time ASC; ORDER BY start_time ASC;
-- name: GetTotalEvents :one -- name: GetTotalEvents :one
SELECT COUNT(*) SELECT COUNT(*)

View File

@ -2780,6 +2780,53 @@ const docTemplate = `{
} }
} }
}, },
"/result/{id}": {
"get": {
"description": "Get results for an event",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"result"
],
"summary": "Get results for an event",
"parameters": [
{
"type": "string",
"description": "Event ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.BetOutcome"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/search/branch": { "/search/branch": {
"get": { "get": {
"description": "Search branches by name or location", "description": "Search branches by name or location",
@ -5041,6 +5088,10 @@ const docTemplate = `{
"description": "Match or event name", "description": "Match or event name",
"type": "string" "type": "string"
}, },
"source": {
"description": "bet api provider (bet365, betfair)",
"type": "string"
},
"sportID": { "sportID": {
"description": "Sport ID", "description": "Sport ID",
"type": "string" "type": "string"

View File

@ -2772,6 +2772,53 @@
} }
} }
}, },
"/result/{id}": {
"get": {
"description": "Get results for an event",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"result"
],
"summary": "Get results for an event",
"parameters": [
{
"type": "string",
"description": "Event ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.BetOutcome"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/search/branch": { "/search/branch": {
"get": { "get": {
"description": "Search branches by name or location", "description": "Search branches by name or location",
@ -5033,6 +5080,10 @@
"description": "Match or event name", "description": "Match or event name",
"type": "string" "type": "string"
}, },
"source": {
"description": "bet api provider (bet365, betfair)",
"type": "string"
},
"sportID": { "sportID": {
"description": "Sport ID", "description": "Sport ID",
"type": "string" "type": "string"

View File

@ -555,6 +555,9 @@ definitions:
matchName: matchName:
description: Match or event name description: Match or event name
type: string type: string
source:
description: bet api provider (bet365, betfair)
type: string
sportID: sportID:
description: Sport ID description: Sport ID
type: string type: string
@ -3310,6 +3313,37 @@ paths:
summary: Get referral statistics summary: Get referral statistics
tags: tags:
- referral - referral
/result/{id}:
get:
consumes:
- application/json
description: Get results for an event
parameters:
- description: Event ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/domain.BetOutcome'
type: array
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Get results for an event
tags:
- result
/search/branch: /search/branch:
get: get:
consumes: consumes:

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: auth.sql // source: auth.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: bet.sql // source: bet.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: branch.sql // source: branch.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: company.sql // source: company.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: copyfrom.go // source: copyfrom.go
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: events.sql // source: events.sql
package dbgen package dbgen
@ -123,6 +123,10 @@ SELECT id,
fetched_at fetched_at
FROM events FROM events
WHERE start_time < now() WHERE start_time < now()
and (
status = $1
OR $1 IS NULL
)
ORDER BY start_time ASC ORDER BY start_time ASC
` `
@ -146,8 +150,8 @@ type GetExpiredUpcomingEventsRow struct {
FetchedAt pgtype.Timestamp `json:"fetched_at"` FetchedAt pgtype.Timestamp `json:"fetched_at"`
} }
func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context) ([]GetExpiredUpcomingEventsRow, error) { func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context, status pgtype.Text) ([]GetExpiredUpcomingEventsRow, error) {
rows, err := q.db.Query(ctx, GetExpiredUpcomingEvents) rows, err := q.db.Query(ctx, GetExpiredUpcomingEvents, status)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: notification.sql // source: notification.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: odds.sql // source: odds.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: otp.sql // source: otp.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: referal.sql // source: referal.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: result.sql // source: result.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: ticket.sql // source: ticket.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: transactions.sql // source: transactions.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: transfer.sql // source: transfer.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: user.sql // source: user.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: virtual_games.sql // source: virtual_games.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.28.0
// source: wallet.sql // source: wallet.sql
package dbgen package dbgen

View File

@ -2,6 +2,39 @@ package domain
import "time" import "time"
// TODO: turn status into an enum
// Status represents the status of an event.
// 0 Not Started
// 1 InPlay
// 2 TO BE FIXED
// 3 Ended
// 4 Postponed
// 5 Cancelled
// 6 Walkover
// 7 Interrupted
// 8 Abandoned
// 9 Retired
// 10 Suspended
// 11 Decided by FA
// 99 Removed
type EventStatus string
const (
STATUS_PENDING EventStatus = "upcoming"
STATUS_IN_PLAY EventStatus = "in_play"
STATUS_TO_BE_FIXED EventStatus = "to_be_fixed"
STATUS_ENDED EventStatus = "ended"
STATUS_POSTPONED EventStatus = "postponed"
STATUS_CANCELLED EventStatus = "cancelled"
STATUS_WALKOVER EventStatus = "walkover"
STATUS_INTERRUPTED EventStatus = "interrupted"
STATUS_ABANDONED EventStatus = "abandoned"
STATUS_RETIRED EventStatus = "retired"
STATUS_SUSPENDED EventStatus = "suspended"
STATUS_DECIDED_BY_FA EventStatus = "decided_by_fa"
STATUS_REMOVED EventStatus = "removed"
)
type Event struct { type Event struct {
ID string ID string
SportID string SportID string
@ -83,3 +116,13 @@ type Odds struct {
Name string `json:"name"` Name string `json:"name"`
HitStatus string `json:"hit_status"` HitStatus string `json:"hit_status"`
} }
type EventFilter struct {
SportID ValidString
LeagueID ValidString
FirstStartTime ValidTime
LastStartTime ValidTime
Limit ValidInt64
Offset ValidInt64
MatchStatus ValidString // e.g., "upcoming", "in_play", "ended"
}

View File

@ -12,6 +12,19 @@ type OddsSection struct {
Sp map[string]OddsMarket `json:"sp"` Sp map[string]OddsMarket `json:"sp"`
} }
type ParseOddSectionsRes struct {
Sections map[string]OddsSection
OtherRes []OddsSection
EventFI string
}
type RawOdd struct {
ID string `json:"id"`
Name string `json:"name"`
Header string `json:"header,omitempty"`
Handicap string `json:"handicap,omitempty"`
Odds string `json:"odds"`
}
// The Market ID for the json data can be either string / int which is causing problems when UnMarshalling // 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.RawMessage `json:"id"` ID json.RawMessage `json:"id"`

View File

@ -45,3 +45,5 @@ type RawOddsByMarketID struct {
RawOdds []RawMessage `json:"raw_odds"` RawOdds []RawMessage `json:"raw_odds"`
FetchedAt time.Time `json:"fetched_at"` FetchedAt time.Time `json:"fetched_at"`
} }

View File

@ -27,6 +27,17 @@ type Score struct {
Away string `json:"away"` Away string `json:"away"`
} }
type CommonResultResponse struct {
ID string `json:"id"`
SportID string `json:"sport_id"`
Time string `json:"time"`
TimeStatus string `json:"time_status"`
League League `json:"league"`
Home Team `json:"home"`
Away Team `json:"away"`
SS string `json:"ss"`
}
type FootballResultResponse struct { type FootballResultResponse struct {
ID string `json:"id"` ID string `json:"id"`
SportID string `json:"sport_id"` SportID string `json:"sport_id"`

View File

@ -93,8 +93,11 @@ func (s *Store) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEven
return upcomingEvents, nil return upcomingEvents, nil
} }
func (s *Store) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) { func (s *Store) GetExpiredUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, error) {
events, err := s.queries.GetExpiredUpcomingEvents(ctx) events, err := s.queries.GetExpiredUpcomingEvents(ctx, pgtype.Text{
String: filter.MatchStatus.Value,
Valid: filter.MatchStatus.Valid,
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -121,32 +124,32 @@ func (s *Store) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.Upcoming
return upcomingEvents, nil return upcomingEvents, nil
} }
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) { func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]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: filter.LeagueID.Value,
Valid: leagueID.Valid, Valid: filter.LeagueID.Valid,
}, },
SportID: pgtype.Text{ SportID: pgtype.Text{
String: sportID.Value, String: filter.SportID.Value,
Valid: sportID.Valid, Valid: filter.SportID.Valid,
}, },
Limit: pgtype.Int4{ Limit: pgtype.Int4{
Int32: int32(limit.Value), Int32: int32(filter.Limit.Value),
Valid: limit.Valid, Valid: filter.Limit.Valid,
}, },
Offset: pgtype.Int4{ Offset: pgtype.Int4{
Int32: int32(offset.Value * limit.Value), Int32: int32(filter.Offset.Value * filter.Limit.Value),
Valid: offset.Valid, Valid: filter.Offset.Valid,
}, },
FirstStartTime: pgtype.Timestamp{ FirstStartTime: pgtype.Timestamp{
Time: firstStartTime.Value.UTC(), Time: filter.FirstStartTime.Value.UTC(),
Valid: firstStartTime.Valid, Valid: filter.FirstStartTime.Valid,
}, },
LastStartTime: pgtype.Timestamp{ LastStartTime: pgtype.Timestamp{
Time: lastStartTime.Value.UTC(), Time: filter.LastStartTime.Value.UTC(),
Valid: lastStartTime.Valid, Valid: filter.LastStartTime.Valid,
}, },
}) })
@ -174,27 +177,27 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit domain.Val
} }
totalCount, err := s.queries.GetTotalEvents(ctx, dbgen.GetTotalEventsParams{ totalCount, err := s.queries.GetTotalEvents(ctx, dbgen.GetTotalEventsParams{
LeagueID: pgtype.Text{ LeagueID: pgtype.Text{
String: leagueID.Value, String: filter.LeagueID.Value,
Valid: leagueID.Valid, Valid: filter.LeagueID.Valid,
}, },
SportID: pgtype.Text{ SportID: pgtype.Text{
String: sportID.Value, String: filter.SportID.Value,
Valid: sportID.Valid, Valid: filter.SportID.Valid,
}, },
FirstStartTime: pgtype.Timestamp{ FirstStartTime: pgtype.Timestamp{
Time: firstStartTime.Value.UTC(), Time: filter.FirstStartTime.Value.UTC(),
Valid: firstStartTime.Valid, Valid: filter.FirstStartTime.Valid,
}, },
LastStartTime: pgtype.Timestamp{ LastStartTime: pgtype.Timestamp{
Time: lastStartTime.Value.UTC(), Time: filter.LastStartTime.Value.UTC(),
Valid: lastStartTime.Valid, Valid: filter.LastStartTime.Valid,
}, },
}) })
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
numberOfPages := math.Ceil(float64(totalCount) / float64(limit.Value)) numberOfPages := math.Ceil(float64(totalCount) / float64(filter.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) {
@ -220,10 +223,10 @@ func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.Upc
Source: event.Source.String, Source: event.Source.String,
}, nil }, nil
} }
func (s *Store) UpdateFinalScore(ctx context.Context, eventID, fullScore, status string) error { func (s *Store) UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error {
params := dbgen.UpdateMatchResultParams{ params := dbgen.UpdateMatchResultParams{
Score: pgtype.Text{String: fullScore, Valid: true}, Score: pgtype.Text{String: fullScore, Valid: true},
Status: pgtype.Text{String: status, Valid: true}, Status: pgtype.Text{String: string(status), Valid: true},
ID: eventID, ID: eventID,
} }

View File

@ -393,7 +393,12 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, le
// Get a unexpired event id // Get a unexpired event id
events, _, err := s.eventSvc.GetPaginatedUpcomingEvents(ctx, events, _, err := s.eventSvc.GetPaginatedUpcomingEvents(ctx,
domain.ValidInt64{}, domain.ValidInt64{}, leagueID, sportID, firstStartTime, lastStartTime) domain.EventFilter{
SportID: sportID,
LeagueID: leagueID,
FirstStartTime: firstStartTime,
LastStartTime: lastStartTime,
})
if err != nil { if err != nil {
return domain.CreateBetRes{}, err return domain.CreateBetRes{}, err

View File

@ -10,9 +10,9 @@ type Service interface {
FetchLiveEvents(ctx context.Context) error FetchLiveEvents(ctx context.Context) error
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, filter domain.EventFilter) ([]domain.UpcomingEvent, 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) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]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
UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error
} }

View File

@ -323,18 +323,22 @@ func (s *service) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEv
return s.store.GetAllUpcomingEvents(ctx) return s.store.GetAllUpcomingEvents(ctx)
} }
func (s *service) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) { func (s *service) GetExpiredUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, error) {
return s.store.GetExpiredUpcomingEvents(ctx) return s.store.GetExpiredUpcomingEvents(ctx, filter)
} }
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) { func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, int64, error){
return s.store.GetPaginatedUpcomingEvents(ctx, limit, offset, leagueID, sportID, firstStartTime, lastStartTime) return s.store.GetPaginatedUpcomingEvents(ctx, filter)
} }
func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) { func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) {
return s.store.GetUpcomingEventByID(ctx, ID) return s.store.GetUpcomingEventByID(ctx, ID)
} }
func (s *service) UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error {
return s.store.UpdateFinalScore(ctx, eventID, fullScore, status)
}
// func (s *service) GetAndStoreMatchResult(ctx context.Context, eventID string) error { // func (s *service) GetAndStoreMatchResult(ctx context.Context, eventID string) error {
// url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%s", s.token, eventID) // url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%s", s.token, eventID)

View File

@ -2,12 +2,15 @@ package odds
import ( import (
"context" "context"
"encoding/json"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
) )
type Service interface { type Service interface {
FetchNonLiveOdds(ctx context.Context) error FetchNonLiveOdds(ctx context.Context) error
FetchNonLiveOddsByEventID(ctx context.Context, eventIDStr string) (domain.BaseNonLiveOddResponse, error)
ParseOddSections(ctx context.Context, res json.RawMessage, sportID int64) (domain.ParseOddSectionsRes, 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) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string) ([]domain.Odd, error)
GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit domain.ValidInt64, offset domain.ValidInt64) ([]domain.Odd, error) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit domain.ValidInt64, offset domain.ValidInt64) ([]domain.Odd, error)

View File

@ -44,99 +44,47 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
var errs []error var errs []error
for index, event := range eventIDs { for index, event := range eventIDs {
log.Printf("📡 Fetching prematch odds for event ID: %v (%d/%d) ", event.ID, index, len(eventIDs))
eventID, err := strconv.ParseInt(event.ID, 10, 64)
if err != nil {
s.logger.Error("Failed to parse event id")
return err
}
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 (%d/%d) ", eventID, index, len(eventIDs))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
log.Printf("❌ Failed to create request for event %d: %v", eventID, err)
continue
}
resp, err := s.client.Do(req)
if err != nil {
log.Printf("❌ Failed to fetch prematch odds for event %d: %v", eventID, err)
continue
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("❌ Failed to read response body for event %d: %v", eventID, err)
continue
}
var oddsData domain.BaseNonLiveOddResponse
if err := json.Unmarshal(body, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 {
log.Printf("❌ Invalid prematch data for event %d", eventID)
continue
}
sportID, err := strconv.ParseInt(event.SportID, 10, 64) sportID, err := strconv.ParseInt(event.SportID, 10, 64)
if err != nil {
s.logger.Error("Failed to parse sport id", "error", err)
errs = append(errs, fmt.Errorf("failed to parse sport id %s: %w", event.SportID, err))
continue
}
switch sportID { oddsData, err := s.FetchNonLiveOddsByEventID(ctx, event.ID)
case domain.FOOTBALL: if err != nil {
if err := s.parseFootball(ctx, oddsData.Results[0]); err != nil { s.logger.Error("Failed to fetch prematch odds", "eventID", event.ID, "error", err)
s.logger.Error("Error while inserting football odd") errs = append(errs, fmt.Errorf("failed to fetch prematch odds for event %v: %w", event.ID, err))
continue
}
parsedOddSections, err := s.ParseOddSections(ctx, oddsData.Results[0], sportID)
if err != nil {
s.logger.Error("Failed to parse odd section", "error", err)
errs = append(errs, fmt.Errorf("failed to parse odd section for event %v: %w", event.ID, err))
continue
}
if parsedOddSections.EventFI == "" {
s.logger.Error("Skipping result with no valid Event FI field", "fi", parsedOddSections.EventFI)
errs = append(errs, errors.New("event FI is empty"))
continue
}
for oddCategory, section := range parsedOddSections.Sections {
if err := s.storeSection(ctx, event.ID, parsedOddSections.EventFI, oddCategory, section); err != nil {
s.logger.Error("Error storing odd section", "eventID", event.ID, "odd", oddCategory)
log.Printf("⚠️ Error when storing %v", err)
errs = append(errs, err) errs = append(errs, err)
} }
case domain.BASKETBALL:
if err := s.parseBasketball(ctx, oddsData.Results[0]); err != nil {
s.logger.Error("Error while inserting basketball odd")
errs = append(errs, err)
} }
case domain.ICE_HOCKEY: for _, section := range parsedOddSections.OtherRes {
if err := s.parseIceHockey(ctx, oddsData.Results[0]); err != nil { if err := s.storeSection(ctx, event.ID, parsedOddSections.EventFI, "others", section); err != nil {
s.logger.Error("Error while inserting ice hockey odd") s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
}
case domain.CRICKET:
if err := s.parseCricket(ctx, oddsData.Results[0]); err != nil {
s.logger.Error("Error while inserting cricket odd")
errs = append(errs, err)
}
case domain.VOLLEYBALL:
if err := s.parseVolleyball(ctx, oddsData.Results[0]); err != nil {
s.logger.Error("Error while inserting volleyball odd")
errs = append(errs, err)
}
case domain.DARTS:
if err := s.parseDarts(ctx, oddsData.Results[0]); err != nil {
s.logger.Error("Error while inserting darts odd")
errs = append(errs, err)
}
case domain.FUTSAL:
if err := s.parseFutsal(ctx, oddsData.Results[0]); err != nil {
s.logger.Error("Error while inserting futsal odd")
errs = append(errs, err)
}
case domain.AMERICAN_FOOTBALL:
if err := s.parseAmericanFootball(ctx, oddsData.Results[0]); err != nil {
s.logger.Error("Error while inserting american football odd")
errs = append(errs, err)
}
case domain.RUGBY_LEAGUE:
if err := s.parseRugbyLeague(ctx, oddsData.Results[0]); err != nil {
s.logger.Error("Error while inserting rugby league odd")
errs = append(errs, err)
}
case domain.RUGBY_UNION:
if err := s.parseRugbyUnion(ctx, oddsData.Results[0]); err != nil {
s.logger.Error("Error while inserting rugby union odd")
errs = append(errs, err)
}
case domain.BASEBALL:
if err := s.parseBaseball(ctx, oddsData.Results[0]); err != nil {
s.logger.Error("Error while inserting baseball odd")
errs = append(errs, err) errs = append(errs, err)
continue
} }
} }
@ -144,137 +92,102 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
} }
return nil return errors.Join(errs...)
} }
func (s *ServiceImpl) parseFootball(ctx context.Context, res json.RawMessage) error { func (s *ServiceImpl) FetchNonLiveOddsByEventID(ctx context.Context, eventIDStr string) (domain.BaseNonLiveOddResponse, error) {
eventID, err := strconv.ParseInt(eventIDStr, 10, 64)
if err != nil {
s.logger.Error("Failed to parse event id")
return domain.BaseNonLiveOddResponse{}, err
}
url := fmt.Sprintf("https://api.b365api.com/v3/bet365/prematch?token=%s&FI=%d", s.config.Bet365Token, eventID)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
log.Printf("❌ Failed to create request for event %d: %v", eventID, err)
}
resp, err := s.client.Do(req)
if err != nil {
log.Printf("❌ Failed to fetch prematch odds for event %d: %v", eventID, err)
return domain.BaseNonLiveOddResponse{}, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("❌ Failed to read response body for event %d: %v", eventID, err)
return domain.BaseNonLiveOddResponse{}, err
}
var oddsData domain.BaseNonLiveOddResponse
if err := json.Unmarshal(body, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 {
log.Printf("❌ Invalid prematch data for event %d", eventID)
return domain.BaseNonLiveOddResponse{}, err
}
return oddsData, nil
}
func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage, sportID int64) (domain.ParseOddSectionsRes, error) {
var sections map[string]domain.OddsSection
var OtherRes []domain.OddsSection
var eventFI string
switch sportID {
case domain.FOOTBALL:
var footballRes domain.FootballOddsResponse var footballRes domain.FootballOddsResponse
if err := json.Unmarshal(res, &footballRes); err != nil { if err := json.Unmarshal(res, &footballRes); err != nil {
s.logger.Error("Failed to unmarshal football result", "error", err) s.logger.Error("Failed to unmarshal football result", "error", err)
return err return domain.ParseOddSectionsRes{}, err
} }
if footballRes.EventID == "" && footballRes.FI == "" { eventFI = footballRes.FI
s.logger.Error("Skipping football result with no valid Event ID", "eventID", footballRes.EventID, "fi", footballRes.FI) sections = map[string]domain.OddsSection{
return fmt.Errorf("Skipping football result with no valid Event ID Event ID %v", footballRes.EventID)
}
sections := map[string]domain.OddsSection{
"main": footballRes.Main, "main": footballRes.Main,
"asian_lines": footballRes.AsianLines, "asian_lines": footballRes.AsianLines,
"goals": footballRes.Goals, "goals": footballRes.Goals,
"half": footballRes.Half, "half": footballRes.Half,
} }
case domain.BASKETBALL:
var errs []error
for oddCategory, section := range sections {
if err := s.storeSection(ctx, footballRes.EventID, footballRes.FI, oddCategory, section); err != nil {
s.logger.Error("Error storing football section", "eventID", footballRes.FI, "odd", oddCategory)
log.Printf("⚠️ Error when storing football %v", err)
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
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 basketball result", "error", err) s.logger.Error("Failed to unmarshal basketball result", "error", err)
return err return domain.ParseOddSectionsRes{}, err
} }
if basketballRes.EventID == "" && basketballRes.FI == "" { eventFI = basketballRes.FI
s.logger.Error("Skipping basketball result with no valid Event ID") OtherRes = basketballRes.Others
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,
"half_props": basketballRes.HalfProps, "half_props": basketballRes.HalfProps,
"quarter_props": basketballRes.QuarterProps, "quarter_props": basketballRes.QuarterProps,
"team_props": basketballRes.TeamProps, "team_props": basketballRes.TeamProps,
} }
var errs []error case domain.ICE_HOCKEY:
for oddCategory, section := range sections {
if err := s.storeSection(ctx, basketballRes.EventID, basketballRes.FI, oddCategory, section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
for _, section := range basketballRes.Others {
if err := s.storeSection(ctx, basketballRes.EventID, basketballRes.FI, "others", section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
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 ice hockey result", "error", err) s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
return err return domain.ParseOddSectionsRes{}, err
} }
if iceHockeyRes.EventID == "" && iceHockeyRes.FI == "" { eventFI = iceHockeyRes.FI
s.logger.Error("Skipping result with no valid Event ID") OtherRes = iceHockeyRes.Others
return fmt.Errorf("Skipping result with no valid Event ID") sections = map[string]domain.OddsSection{
}
sections := map[string]domain.OddsSection{
"main": iceHockeyRes.Main, "main": iceHockeyRes.Main,
"main_2": iceHockeyRes.Main2, "main_2": iceHockeyRes.Main2,
"1st_period": iceHockeyRes.FirstPeriod, "1st_period": iceHockeyRes.FirstPeriod,
} }
case domain.CRICKET:
var errs []error
for oddCategory, section := range sections {
if err := s.storeSection(ctx, iceHockeyRes.EventID, iceHockeyRes.FI, oddCategory, section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
for _, section := range iceHockeyRes.Others {
if err := s.storeSection(ctx, iceHockeyRes.EventID, iceHockeyRes.FI, "others", section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (s *ServiceImpl) parseCricket(ctx context.Context, res json.RawMessage) error {
var cricketRes domain.CricketOddsResponse var cricketRes domain.CricketOddsResponse
if err := json.Unmarshal(res, &cricketRes); err != nil { if err := json.Unmarshal(res, &cricketRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err) s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
return err return domain.ParseOddSectionsRes{}, err
} }
if cricketRes.EventID == "" && cricketRes.FI == "" { eventFI = cricketRes.FI
s.logger.Error("Skipping result with no valid Event ID") OtherRes = cricketRes.Others
return fmt.Errorf("Skipping result with no valid Event ID") sections = map[string]domain.OddsSection{
}
sections := map[string]domain.OddsSection{
"1st_over": cricketRes.Main, "1st_over": cricketRes.Main,
"innings_1": cricketRes.First_Innings, "innings_1": cricketRes.First_Innings,
"main": cricketRes.Main, "main": cricketRes.Main,
@ -282,205 +195,65 @@ func (s *ServiceImpl) parseCricket(ctx context.Context, res json.RawMessage) err
"player": cricketRes.Player, "player": cricketRes.Player,
"team": cricketRes.Team, "team": cricketRes.Team,
} }
case domain.VOLLEYBALL:
var errs []error
for oddCategory, section := range sections {
if err := s.storeSection(ctx, cricketRes.EventID, cricketRes.FI, oddCategory, section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
for _, section := range cricketRes.Others {
if err := s.storeSection(ctx, cricketRes.EventID, cricketRes.FI, "others", section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (s *ServiceImpl) parseVolleyball(ctx context.Context, res json.RawMessage) error {
var volleyballRes domain.VolleyballOddsResponse var volleyballRes domain.VolleyballOddsResponse
if err := json.Unmarshal(res, &volleyballRes); err != nil { if err := json.Unmarshal(res, &volleyballRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err) s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
return err return domain.ParseOddSectionsRes{}, err
} }
if volleyballRes.EventID == "" && volleyballRes.FI == "" { eventFI = volleyballRes.FI
s.logger.Error("Skipping result with no valid Event ID") OtherRes = volleyballRes.Others
return fmt.Errorf("Skipping result with no valid Event ID") sections = map[string]domain.OddsSection{
}
sections := map[string]domain.OddsSection{
"main": volleyballRes.Main, "main": volleyballRes.Main,
} }
case domain.DARTS:
var errs []error
for oddCategory, section := range sections {
if err := s.storeSection(ctx, volleyballRes.EventID, volleyballRes.FI, oddCategory, section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
for _, section := range volleyballRes.Others {
if err := s.storeSection(ctx, volleyballRes.EventID, volleyballRes.FI, "others", section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (s *ServiceImpl) parseDarts(ctx context.Context, res json.RawMessage) error {
var dartsRes domain.DartsOddsResponse var dartsRes domain.DartsOddsResponse
if err := json.Unmarshal(res, &dartsRes); err != nil { if err := json.Unmarshal(res, &dartsRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err) s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
return err return domain.ParseOddSectionsRes{}, err
} }
if dartsRes.EventID == "" && dartsRes.FI == "" { eventFI = dartsRes.FI
s.logger.Error("Skipping result with no valid Event ID") OtherRes = dartsRes.Others
return fmt.Errorf("Skipping result with no valid Event ID") sections = map[string]domain.OddsSection{
}
sections := map[string]domain.OddsSection{
"180s": dartsRes.OneEightys, "180s": dartsRes.OneEightys,
"extra": dartsRes.Extra, "extra": dartsRes.Extra,
"leg": dartsRes.Leg, "leg": dartsRes.Leg,
"main": dartsRes.Main, "main": dartsRes.Main,
} }
case domain.FUTSAL:
var errs []error
for oddCategory, section := range sections {
if err := s.storeSection(ctx, dartsRes.EventID, dartsRes.FI, oddCategory, section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
for _, section := range dartsRes.Others {
if err := s.storeSection(ctx, dartsRes.EventID, dartsRes.FI, "others", section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (s *ServiceImpl) parseFutsal(ctx context.Context, res json.RawMessage) error {
var futsalRes domain.FutsalOddsResponse var futsalRes domain.FutsalOddsResponse
if err := json.Unmarshal(res, &futsalRes); err != nil { if err := json.Unmarshal(res, &futsalRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err) s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
return err return domain.ParseOddSectionsRes{}, err
} }
if futsalRes.EventID == "" && futsalRes.FI == "" { eventFI = futsalRes.FI
s.logger.Error("Skipping result with no valid Event ID") OtherRes = futsalRes.Others
return fmt.Errorf("Skipping result with no valid Event ID") sections = map[string]domain.OddsSection{
}
sections := map[string]domain.OddsSection{
"main": futsalRes.Main, "main": futsalRes.Main,
"score": futsalRes.Score, "score": futsalRes.Score,
} }
case domain.AMERICAN_FOOTBALL:
var errs []error
for oddCategory, section := range sections {
if err := s.storeSection(ctx, futsalRes.EventID, futsalRes.FI, oddCategory, section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
for _, section := range futsalRes.Others {
if err := s.storeSection(ctx, futsalRes.EventID, futsalRes.FI, "others", section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (s *ServiceImpl) parseAmericanFootball(ctx context.Context, res json.RawMessage) error {
var americanFootballRes domain.AmericanFootballOddsResponse var americanFootballRes domain.AmericanFootballOddsResponse
if err := json.Unmarshal(res, &americanFootballRes); err != nil { if err := json.Unmarshal(res, &americanFootballRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err) s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
return err return domain.ParseOddSectionsRes{}, err
} }
if americanFootballRes.EventID == "" && americanFootballRes.FI == "" { eventFI = americanFootballRes.FI
s.logger.Error("Skipping result with no valid Event ID") OtherRes = americanFootballRes.Others
return fmt.Errorf("Skipping result with no valid Event ID") sections = map[string]domain.OddsSection{
}
sections := map[string]domain.OddsSection{
"half_props": americanFootballRes.HalfProps, "half_props": americanFootballRes.HalfProps,
"main": americanFootballRes.Main, "main": americanFootballRes.Main,
"quarter_props": americanFootballRes.QuarterProps, "quarter_props": americanFootballRes.QuarterProps,
} }
case domain.RUGBY_LEAGUE:
var errs []error
for oddCategory, section := range sections {
if err := s.storeSection(ctx, americanFootballRes.EventID, americanFootballRes.FI, oddCategory, section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
for _, section := range americanFootballRes.Others {
if err := s.storeSection(ctx, americanFootballRes.EventID, americanFootballRes.FI, "others", section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (s *ServiceImpl) parseRugbyLeague(ctx context.Context, res json.RawMessage) error {
var rugbyLeagueRes domain.RugbyLeagueOddsResponse var rugbyLeagueRes domain.RugbyLeagueOddsResponse
if err := json.Unmarshal(res, &rugbyLeagueRes); err != nil { if err := json.Unmarshal(res, &rugbyLeagueRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err) s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
return err return domain.ParseOddSectionsRes{}, err
} }
if rugbyLeagueRes.EventID == "" && rugbyLeagueRes.FI == "" { eventFI = rugbyLeagueRes.FI
s.logger.Error("Skipping result with no valid Event ID") OtherRes = rugbyLeagueRes.Others
return fmt.Errorf("Skipping result with no valid Event ID") sections = map[string]domain.OddsSection{
}
sections := map[string]domain.OddsSection{
"10minute": rugbyLeagueRes.TenMinute, "10minute": rugbyLeagueRes.TenMinute,
"main": rugbyLeagueRes.Main, "main": rugbyLeagueRes.Main,
"main_2": rugbyLeagueRes.Main2, "main_2": rugbyLeagueRes.Main2,
@ -488,105 +261,39 @@ func (s *ServiceImpl) parseRugbyLeague(ctx context.Context, res json.RawMessage)
"Score": rugbyLeagueRes.Score, "Score": rugbyLeagueRes.Score,
"Team": rugbyLeagueRes.Team, "Team": rugbyLeagueRes.Team,
} }
case domain.RUGBY_UNION:
var errs []error
for oddCategory, section := range sections {
if err := s.storeSection(ctx, rugbyLeagueRes.EventID, rugbyLeagueRes.FI, oddCategory, section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
for _, section := range rugbyLeagueRes.Others {
if err := s.storeSection(ctx, rugbyLeagueRes.EventID, rugbyLeagueRes.FI, "others", section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (s *ServiceImpl) parseRugbyUnion(ctx context.Context, res json.RawMessage) error {
var rugbyUnionRes domain.RugbyUnionOddsResponse var rugbyUnionRes domain.RugbyUnionOddsResponse
if err := json.Unmarshal(res, &rugbyUnionRes); err != nil { if err := json.Unmarshal(res, &rugbyUnionRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err) s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
return err return domain.ParseOddSectionsRes{}, err
} }
if rugbyUnionRes.EventID == "" && rugbyUnionRes.FI == "" { eventFI = rugbyUnionRes.FI
s.logger.Error("Skipping result with no valid Event ID") OtherRes = rugbyUnionRes.Others
return fmt.Errorf("Skipping result with no valid Event ID") sections = map[string]domain.OddsSection{
}
sections := map[string]domain.OddsSection{
"main": rugbyUnionRes.Main, "main": rugbyUnionRes.Main,
"main_2": rugbyUnionRes.Main2, "main_2": rugbyUnionRes.Main2,
"player": rugbyUnionRes.Player, "player": rugbyUnionRes.Player,
"Score": rugbyUnionRes.Score, "Score": rugbyUnionRes.Score,
"Team": rugbyUnionRes.Team, "Team": rugbyUnionRes.Team,
} }
case domain.BASEBALL:
var errs []error
for oddCategory, section := range sections {
if err := s.storeSection(ctx, rugbyUnionRes.EventID, rugbyUnionRes.FI, oddCategory, section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
for _, section := range rugbyUnionRes.Others {
if err := s.storeSection(ctx, rugbyUnionRes.EventID, rugbyUnionRes.FI, "others", section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (s *ServiceImpl) parseBaseball(ctx context.Context, res json.RawMessage) error {
var baseballRes domain.BaseballOddsResponse var baseballRes domain.BaseballOddsResponse
if err := json.Unmarshal(res, &baseballRes); err != nil { if err := json.Unmarshal(res, &baseballRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err) s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
return err return domain.ParseOddSectionsRes{}, err
} }
if baseballRes.EventID == "" && baseballRes.FI == "" { eventFI = baseballRes.FI
s.logger.Error("Skipping result with no valid Event ID") sections = map[string]domain.OddsSection{
return fmt.Errorf("Skipping result with no valid Event ID")
}
sections := map[string]domain.OddsSection{
"main": baseballRes.Main, "main": baseballRes.Main,
"mani_props": baseballRes.MainProps, "mani_props": baseballRes.MainProps,
} }
var errs []error
for oddCategory, section := range sections {
if err := s.storeSection(ctx, baseballRes.EventID, baseballRes.FI, oddCategory, section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
} }
if len(errs) > 0 { return domain.ParseOddSectionsRes{
return errors.Join(errs...) Sections: sections,
} OtherRes: OtherRes,
EventFI: eventFI,
return nil }, nil
} }
func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName string, section domain.OddsSection) error { func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName string, section domain.OddsSection) error {
@ -606,22 +313,21 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName
// Check if the market id is a string // Check if the market id is a string
var marketIDstr string var marketIDstr string
err := json.Unmarshal(market.ID, &marketIDstr) err := json.Unmarshal(market.ID, &marketIDstr)
var marketIDint int64
if err != nil { if err != nil {
// check if its int // check if its int
var marketIDint int
err := json.Unmarshal(market.ID, &marketIDint) err := json.Unmarshal(market.ID, &marketIDint)
if err != nil { if err != nil {
s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name) s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name)
errs = append(errs, err) continue
} }
} } else {
marketIDint, err = strconv.ParseInt(marketIDstr, 10, 64)
marketIDint, err := strconv.ParseInt(marketIDstr, 10, 64)
if err != nil { if err != nil {
s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name) s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name)
errs = append(errs, err)
continue continue
} }
}
isSupported, ok := domain.SupportedMarkets[marketIDint] isSupported, ok := domain.SupportedMarkets[marketIDint]

View File

@ -14,6 +14,8 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
) )
type Service struct { type Service struct {
@ -22,43 +24,50 @@ type Service struct {
logger *slog.Logger logger *slog.Logger
client *http.Client client *http.Client
betSvc bet.Service betSvc bet.Service
oddSvc odds.Service
eventSvc event.Service
} }
func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, betSvc bet.Service) *Service { func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, betSvc bet.Service, oddSvc odds.Service, eventSvc event.Service) *Service {
return &Service{ return &Service{
repo: repo, repo: repo,
config: cfg, config: cfg,
logger: logger, logger: logger,
client: &http.Client{Timeout: 10 * time.Second}, client: &http.Client{Timeout: 10 * time.Second},
betSvc: betSvc, betSvc: betSvc,
oddSvc: oddSvc,
eventSvc: eventSvc,
} }
} }
var ( var (
ErrEventIsNotActive = fmt.Errorf("Event has been cancelled or postponed") ErrEventIsNotActive = fmt.Errorf("event has been cancelled or postponed")
) )
func (s *Service) FetchAndProcessResults(ctx context.Context) error { func (s *Service) FetchAndProcessResults(ctx context.Context) error {
// TODO: Optimize this because there could be many bet outcomes for the same odd // TODO: Optimize this because there could be many bet outcomes for the same odd
// Take market id and match result as param and update all the bet outcomes at the same time // Take market id and match result as param and update all the bet outcomes at the same time
events, err := s.repo.GetExpiredUpcomingEvents(ctx) events, err := s.repo.GetExpiredUpcomingEvents(ctx, domain.EventFilter{})
if err != nil { if err != nil {
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))
removed := 0 removed := 0
errs := make([]error, 0, len(events))
for i, event := range events { for i, event := range 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")
return err errs = append(errs, fmt.Errorf("failed to parse event id %s: %w", event.ID, err))
continue
} }
outcomes, err := s.repo.GetBetOutcomeByEventID(ctx, eventID) outcomes, err := s.repo.GetBetOutcomeByEventID(ctx, eventID)
if err != nil { if err != nil {
s.logger.Error("Failed to get pending bet outcomes", "error", err) s.logger.Error("Failed to get pending bet outcomes", "error", err)
return err errs = append(errs, fmt.Errorf("failed to get pending bet outcomes for event %d: %w", eventID, err))
continue
} }
if len(outcomes) == 0 { if len(outcomes) == 0 {
@ -68,6 +77,40 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error {
} }
isDeleted := true isDeleted := true
result, err := s.fetchResult(ctx, eventID)
if err != nil {
if err == ErrEventIsNotActive {
s.logger.Warn("Event is not active", "event_id", eventID, "error", err)
continue
}
s.logger.Error("Failed to fetch result", "event_id", eventID, "error", err)
continue
}
var commonResp domain.CommonResultResponse
if err := json.Unmarshal(result.Results[0], &commonResp); err != nil {
s.logger.Error("Failed to unmarshal common result", "event_id", eventID, "error", err)
continue
}
sportID, err := strconv.ParseInt(commonResp.SportID, 10, 64)
if err != nil {
s.logger.Error("Failed to parse sport id", "event_id", eventID, "error", err)
continue
}
timeStatusParsed, err := strconv.ParseInt(strings.TrimSpace(commonResp.TimeStatus), 10, 64)
if err != nil {
s.logger.Error("Failed to parse time status", "time_status", commonResp.TimeStatus, "error", err)
continue
}
// TODO: Figure out what to do with the events that have been cancelled or postponed, etc...
if timeStatusParsed != int64(domain.TIME_STATUS_ENDED) {
s.logger.Warn("Event is not ended yet", "event_id", eventID, "time_status", commonResp.TimeStatus)
fmt.Printf("⚠️ Event %v is not ended yet (%d/%d) \n", event.ID, i+1, len(events))
isDeleted = false
continue
}
for j, outcome := range outcomes { for j, outcome := range outcomes {
fmt.Printf("⚙️ Processing 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n", fmt.Printf("⚙️ Processing 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n",
outcome.MarketName, outcome.MarketName,
@ -80,29 +123,14 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error {
continue continue
} }
sportID, err := strconv.ParseInt(event.SportID, 10, 64) parseResult, err := s.parseResult(ctx, result.Results[0], outcome, sportID)
if err != nil { if err != nil {
s.logger.Error("Sport ID is invalid", "event_id", outcome.EventID, "error", err)
isDeleted = false isDeleted = false
s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err)
errs = append(errs, fmt.Errorf("failed to parse result for event %d: %w", outcome.EventID, err))
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 outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, parseResult.Status)
result, err := s.fetchResult(ctx, outcome.EventID, outcome.OddID, outcome.MarketID, sportID, outcome)
if err != nil {
if err == ErrEventIsNotActive {
s.logger.Warn("Event is not active", "event_id", outcome.EventID, "error", err)
continue
}
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
}
outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, result.Status)
if err != nil { if err != nil {
isDeleted = false 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)
@ -157,109 +185,348 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error {
} }
fmt.Printf("🗑️ Removed Events: %d \n", removed) fmt.Printf("🗑️ Removed Events: %d \n", removed)
if len(errs) > 0 {
s.logger.Error("Errors occurred while processing results", "errors", errs)
for _, err := range errs {
fmt.Println("Error:", err)
}
return fmt.Errorf("errors occurred while processing results: %v", errs)
}
s.logger.Info("Successfully processed results", "removed_events", removed, "total_events", len(events))
return nil return nil
} }
func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID, sportID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error) {
// url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%d", s.config.Bet365Token, eventID) events, err := s.repo.GetExpiredUpcomingEvents(ctx, domain.EventFilter{})
url := fmt.Sprintf("https://api.b365api.com/v1/event/view?token=%s&event_id=%d", s.config.Bet365Token, eventID) if err != nil {
s.logger.Error("Failed to fetch events")
return 0, err
}
fmt.Printf("⚠️ Expired Events: %d \n", len(events))
updated := 0
for i, event := range events {
fmt.Printf("⚙️ Processing event %v (%d/%d) \n", event.ID, i+1, len(events))
eventID, err := strconv.ParseInt(event.ID, 10, 64)
if err != nil {
s.logger.Error("Failed to parse event id")
continue
}
result, err := s.fetchResult(ctx, eventID)
if err != nil {
s.logger.Error("Failed to fetch result", "event_id", eventID, "error", err)
continue
}
if result.Success != 1 || len(result.Results) == 0 {
s.logger.Error("Invalid API response", "event_id", eventID)
fmt.Printf("⚠️ Invalid API response for event %v \n", result)
continue
}
var commonResp domain.CommonResultResponse
if err := json.Unmarshal(result.Results[0], &commonResp); err != nil {
s.logger.Error("Failed to unmarshal common result", "event_id", eventID, "error", err)
continue
}
var eventStatus domain.EventStatus
// TODO Change event status to int64 enum
timeStatus, err := strconv.ParseInt(strings.TrimSpace(commonResp.TimeStatus), 10, 64)
switch timeStatus {
case int64(domain.TIME_STATUS_NOT_STARTED):
eventStatus = domain.STATUS_PENDING
case int64(domain.TIME_STATUS_IN_PLAY):
eventStatus = domain.STATUS_IN_PLAY
case int64(domain.TIME_STATUS_TO_BE_FIXED):
eventStatus = domain.STATUS_TO_BE_FIXED
case int64(domain.TIME_STATUS_ENDED):
eventStatus = domain.STATUS_ENDED
case int64(domain.TIME_STATUS_POSTPONED):
eventStatus = domain.STATUS_POSTPONED
case int64(domain.TIME_STATUS_CANCELLED):
eventStatus = domain.STATUS_CANCELLED
case int64(domain.TIME_STATUS_WALKOVER):
eventStatus = domain.STATUS_WALKOVER
case int64(domain.TIME_STATUS_INTERRUPTED):
eventStatus = domain.STATUS_INTERRUPTED
case int64(domain.TIME_STATUS_ABANDONED):
eventStatus = domain.STATUS_ABANDONED
case int64(domain.TIME_STATUS_RETIRED):
eventStatus = domain.STATUS_RETIRED
case int64(domain.TIME_STATUS_SUSPENDED):
eventStatus = domain.STATUS_SUSPENDED
case int64(domain.TIME_STATUS_DECIDED_BY_FA):
eventStatus = domain.STATUS_DECIDED_BY_FA
case int64(domain.TIME_STATUS_REMOVED):
eventStatus = domain.STATUS_REMOVED
default:
s.logger.Error("Invalid time status", "time_status", commonResp.TimeStatus, "event_id", eventID)
}
err = s.eventSvc.UpdateFinalScore(ctx, strconv.FormatInt(eventID, 10), commonResp.SS, eventStatus)
if err != nil {
s.logger.Error("Failed to update final score", "event_id", eventID, "error", err)
continue
}
updated++
fmt.Printf("✅ Successfully updated event %v to %v (%d/%d) \n", event.ID, eventStatus, i+1, len(events))
}
if updated == 0 {
s.logger.Info("No events were updated")
return 0, nil
}
s.logger.Info("Successfully updated live events", "updated_events", updated, "total_events", len(events))
return int64(updated), nil
}
func (s *Service) GetResultsForEvent(ctx context.Context, eventID string) (json.RawMessage, []domain.BetOutcome, error) {
id, err := strconv.ParseInt(eventID, 10, 64)
if err != nil {
s.logger.Error("Failed to parse event id")
return json.RawMessage{}, nil, err
}
result, err := s.fetchResult(ctx, id)
if err != nil {
s.logger.Error("Failed to fetch result", "event_id", id, "error", err)
}
if result.Success != 1 || len(result.Results) == 0 {
fmt.Printf("⚠️ Invalid API response for event %v \n", result)
s.logger.Error("Invalid API response", "event_id", id)
return json.RawMessage{}, nil, fmt.Errorf("invalid API response for event %d", id)
}
var commonResp domain.CommonResultResponse
if err := json.Unmarshal(result.Results[0], &commonResp); err != nil {
s.logger.Error("Failed to unmarshal common result", "event_id", eventID, "error", err)
return json.RawMessage{}, nil, err
}
sportID, err := strconv.ParseInt(commonResp.SportID, 10, 64)
if err != nil {
s.logger.Error("Failed to parse sport id", "event_id", eventID, "error", err)
return json.RawMessage{}, nil, fmt.Errorf("failed to parse sport id: %w", err)
}
expireUnix, err := strconv.ParseInt(commonResp.Time, 10, 64)
if err != nil {
s.logger.Error("Failed to parse expire time", "event_id", eventID, "error", err)
return json.RawMessage{}, nil, fmt.Errorf("Failed to parse expire time for event %s: %w", eventID, err)
}
expires := time.Unix(expireUnix, 0)
odds, err := s.oddSvc.FetchNonLiveOddsByEventID(ctx, eventID)
if err != nil {
s.logger.Error("Failed to fetch non-live odds by event ID", "event_id", eventID, "error", err)
return json.RawMessage{}, nil, fmt.Errorf("failed to fetch non-live odds for event %s: %w", eventID, err)
}
parsedOddSections, err := s.oddSvc.ParseOddSections(ctx, odds.Results[0], sportID)
if err != nil {
s.logger.Error("Failed to parse odd section", "error", err)
return json.RawMessage{}, nil, fmt.Errorf("failed to parse odd section for event %v: %w", eventID, err)
}
outcomes := make([]domain.BetOutcome, 0)
for _, section := range parsedOddSections.Sections {
// TODO: Remove repeat code here, same as in odds service
for _, market := range section.Sp {
var marketIDstr string
err := json.Unmarshal(market.ID, &marketIDstr)
var marketIDint int64
if err != nil {
// check if its int
err := json.Unmarshal(market.ID, &marketIDint)
if err != nil {
s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name)
continue
}
} else {
marketIDint, err = strconv.ParseInt(marketIDstr, 10, 64)
if err != nil {
s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name)
continue
}
}
isSupported, ok := domain.SupportedMarkets[marketIDint]
if !ok || !isSupported {
// s.logger.Info("Unsupported market_id", "marketID", marketIDint, "marketName", market.Name)
continue
}
for _, oddRes := range market.Odds {
var odd domain.RawOdd
if err := json.Unmarshal(oddRes, &odd); err != nil {
s.logger.Error("Failed to unmarshal odd", "error", err)
continue
}
oddID, err := strconv.ParseInt(odd.ID, 10, 64)
if err != nil {
s.logger.Error("Failed to parse odd id", "odd_id", odd.ID, "error", err)
continue
}
oddValue, err := strconv.ParseFloat(odd.Odds, 64)
if err != nil {
s.logger.Error("Failed to parse odd value", "odd_value", odd.Odds, "error", err)
continue
}
outcome := domain.BetOutcome{
EventID: id,
MarketID: marketIDint,
OddID: oddID,
MarketName: market.Name,
OddHeader: odd.Header,
OddHandicap: odd.Handicap,
OddName: odd.Name,
Odd: float32(oddValue),
SportID: sportID,
HomeTeamName: commonResp.Home.Name,
AwayTeamName: commonResp.Away.Name,
Status: domain.OUTCOME_STATUS_PENDING,
Expires: expires,
BetID: 0, // This won't be set
}
outcomes = append(outcomes, outcome)
}
}
}
if len(outcomes) == 0 {
s.logger.Warn("No outcomes found for event", "event_id", eventID)
return json.RawMessage{}, nil, fmt.Errorf("no outcomes found for event %s", eventID)
}
s.logger.Info("Successfully fetched outcomes for event", "event_id", eventID, "outcomes_count", len(outcomes))
// Get results for outcome
for i, outcome := range outcomes {
// Parse the result based on sport type
parsedResult, err := s.parseResult(ctx, result.Results[0], outcome, sportID)
if err != nil {
s.logger.Error("Failed to parse result for outcome", "event_id", outcome.EventID, "error", err)
return json.RawMessage{}, nil, fmt.Errorf("failed to parse result for outcome %d: %w", i, err)
}
outcomes[i].Status = parsedResult.Status
}
return result.Results[0], outcomes, err
}
func (s *Service) fetchResult(ctx context.Context, eventID int64) (domain.BaseResultResponse, error) {
url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%d", s.config.Bet365Token, eventID)
// url := fmt.Sprintf("https://api.b365api.com/v1/event/view?token=%s&event_id=%d", s.config.Bet365Token, eventID)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil { if err != nil {
s.logger.Error("Failed to create request", "event_id", eventID, "error", err) s.logger.Error("Failed to create request", "event_id", eventID, "error", err)
return domain.CreateResult{}, err return domain.BaseResultResponse{}, err
} }
resp, err := s.client.Do(req) resp, err := s.client.Do(req)
if err != nil { if err != nil {
s.logger.Error("Failed to fetch result", "event_id", eventID, "error", err) s.logger.Error("Failed to fetch result", "event_id", eventID, "error", err)
return domain.CreateResult{}, err return domain.BaseResultResponse{}, err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
s.logger.Error("Unexpected status code", "event_id", eventID, "status_code", resp.StatusCode) s.logger.Error("Unexpected status code", "event_id", eventID, "status_code", resp.StatusCode)
return domain.CreateResult{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode) return domain.BaseResultResponse{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
} }
var resultResp domain.BaseResultResponse var resultResp domain.BaseResultResponse
if err := json.NewDecoder(resp.Body).Decode(&resultResp); err != nil { if err := json.NewDecoder(resp.Body).Decode(&resultResp); err != nil {
s.logger.Error("Failed to decode result", "event_id", eventID, "error", err) s.logger.Error("Failed to decode result", "event_id", eventID, "error", err)
return domain.CreateResult{}, err return domain.BaseResultResponse{}, err
} }
if resultResp.Success != 1 || len(resultResp.Results) == 0 { if resultResp.Success != 1 || len(resultResp.Results) == 0 {
s.logger.Error("Invalid API response", "event_id", eventID) s.logger.Error("Invalid API response", "event_id", eventID)
return domain.CreateResult{}, fmt.Errorf("invalid API response") fmt.Printf("⚠️ Invalid API response for event %v \n", resultResp)
return domain.BaseResultResponse{}, fmt.Errorf("invalid API response")
} }
return resultResp, nil
}
func (s *Service) parseResult(ctx context.Context, resultResp json.RawMessage, outcome domain.BetOutcome, sportID int64) (domain.CreateResult, error) {
var result domain.CreateResult var result domain.CreateResult
var err error
switch sportID { switch sportID {
case domain.FOOTBALL: case domain.FOOTBALL:
result, err = s.parseFootball(resultResp.Results[0], eventID, oddID, marketID, outcome) result, err = s.parseFootball(resultResp, outcome)
if err != nil { if err != nil {
s.logger.Error("Failed to parse football", "event id", eventID, "market_id", marketID, "error", err) s.logger.Error("Failed to parse football", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err)
return domain.CreateResult{}, err return domain.CreateResult{}, err
} }
case domain.BASKETBALL: case domain.BASKETBALL:
result, err = s.parseBasketball(resultResp.Results[0], eventID, oddID, marketID, outcome) result, err = s.parseBasketball(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome)
if err != nil { if err != nil {
s.logger.Error("Failed to parse basketball", "event id", eventID, "market_id", marketID, "error", err) s.logger.Error("Failed to parse basketball", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err)
return domain.CreateResult{}, err return domain.CreateResult{}, err
} }
case domain.ICE_HOCKEY: case domain.ICE_HOCKEY:
result, err = s.parseIceHockey(resultResp.Results[0], eventID, oddID, marketID, outcome) result, err = s.parseIceHockey(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome)
if err != nil { if err != nil {
s.logger.Error("Failed to parse ice hockey", "event id", eventID, "market_id", marketID, "error", err) s.logger.Error("Failed to parse ice hockey", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err)
return domain.CreateResult{}, err return domain.CreateResult{}, err
} }
case domain.CRICKET: case domain.CRICKET:
result, err = s.parseCricket(resultResp.Results[0], eventID, oddID, marketID, outcome) result, err = s.parseCricket(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome)
if err != nil { if err != nil {
s.logger.Error("Failed to parse cricket", "event id", eventID, "market_id", marketID, "error", err) s.logger.Error("Failed to parse cricket", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err)
return domain.CreateResult{}, err return domain.CreateResult{}, err
} }
case domain.VOLLEYBALL: case domain.VOLLEYBALL:
result, err = s.parseVolleyball(resultResp.Results[0], eventID, oddID, marketID, outcome) result, err = s.parseVolleyball(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome)
if err != nil { if err != nil {
s.logger.Error("Failed to parse volleyball", "event id", eventID, "market_id", marketID, "error", err) s.logger.Error("Failed to parse volleyball", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err)
return domain.CreateResult{}, err return domain.CreateResult{}, err
} }
case domain.DARTS: case domain.DARTS:
result, err = s.parseDarts(resultResp.Results[0], eventID, oddID, marketID, outcome) result, err = s.parseDarts(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome)
if err != nil { if err != nil {
s.logger.Error("Failed to parse darts", "event id", eventID, "market_id", marketID, "error", err) s.logger.Error("Failed to parse darts", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err)
return domain.CreateResult{}, err return domain.CreateResult{}, err
} }
case domain.FUTSAL: case domain.FUTSAL:
result, err = s.parseFutsal(resultResp.Results[0], eventID, oddID, marketID, outcome) result, err = s.parseFutsal(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome)
if err != nil { if err != nil {
s.logger.Error("Failed to parse futsal", "event id", eventID, "market_id", marketID, "error", err) s.logger.Error("Failed to parse futsal", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err)
return domain.CreateResult{}, err return domain.CreateResult{}, err
} }
case domain.AMERICAN_FOOTBALL: case domain.AMERICAN_FOOTBALL:
result, err = s.parseNFL(resultResp.Results[0], eventID, oddID, marketID, outcome) result, err = s.parseNFL(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome)
if err != nil { if err != nil {
s.logger.Error("Failed to parse american football", "event id", eventID, "market_id", marketID, "error", err) s.logger.Error("Failed to parse american football", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err)
return domain.CreateResult{}, err return domain.CreateResult{}, err
} }
case domain.RUGBY_UNION: case domain.RUGBY_UNION:
result, err = s.parseRugbyUnion(resultResp.Results[0], eventID, oddID, marketID, outcome) result, err = s.parseRugbyUnion(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome)
if err != nil { if err != nil {
s.logger.Error("Failed to parse rugby_union", "event id", eventID, "market_id", marketID, "error", err) s.logger.Error("Failed to parse rugby_union", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err)
return domain.CreateResult{}, err return domain.CreateResult{}, err
} }
case domain.RUGBY_LEAGUE: case domain.RUGBY_LEAGUE:
result, err = s.parseRugbyLeague(resultResp.Results[0], eventID, oddID, marketID, outcome) result, err = s.parseRugbyLeague(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome)
if err != nil { if err != nil {
s.logger.Error("Failed to parse rugby_league", "event id", eventID, "market_id", marketID, "error", err) s.logger.Error("Failed to parse rugby_league", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err)
return domain.CreateResult{}, err return domain.CreateResult{}, err
} }
case domain.BASEBALL: case domain.BASEBALL:
result, err = s.parseBaseball(resultResp.Results[0], eventID, oddID, marketID, outcome) result, err = s.parseBaseball(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome)
if err != nil { if err != nil {
s.logger.Error("Failed to parse baseball", "event id", eventID, "market_id", marketID, "error", err) s.logger.Error("Failed to parse baseball", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err)
return domain.CreateResult{}, err return domain.CreateResult{}, err
} }
default: default:
@ -270,52 +537,14 @@ func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID, spo
return result, nil return result, nil
} }
func (s *Service) parseTimeStatus(timeStatusStr string) (bool, error) { func (s *Service) parseFootball(resultRes json.RawMessage, outcome domain.BetOutcome) (domain.CreateResult, error) {
timeStatusParsed, err := strconv.ParseInt(strings.TrimSpace(timeStatusStr), 10, 64)
if err != nil {
s.logger.Error("Failed to parse time status", "time_status", timeStatusStr, "error", err)
return false, fmt.Errorf("failed to parse time status: %w", err)
}
timeStatus := domain.TimeStatus(timeStatusParsed)
switch timeStatus {
case domain.TIME_STATUS_NOT_STARTED, domain.TIME_STATUS_IN_PLAY, domain.TIME_STATUS_TO_BE_FIXED, domain.TIME_STATUS_ENDED:
return true, nil
case domain.TIME_STATUS_POSTPONED,
domain.TIME_STATUS_CANCELLED,
domain.TIME_STATUS_WALKOVER,
domain.TIME_STATUS_INTERRUPTED,
domain.TIME_STATUS_ABANDONED,
domain.TIME_STATUS_RETIRED,
domain.TIME_STATUS_SUSPENDED,
domain.TIME_STATUS_DECIDED_BY_FA,
domain.TIME_STATUS_REMOVED:
return false, nil
default:
s.logger.Error("Invalid time status", "time_status", timeStatus)
return false, fmt.Errorf("invalid time status: %d", timeStatus)
}
}
func (s *Service) parseFootball(resultRes json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) {
var fbResp domain.FootballResultResponse var fbResp domain.FootballResultResponse
if err := json.Unmarshal(resultRes, &fbResp); err != nil { if err := json.Unmarshal(resultRes, &fbResp); err != nil {
s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) s.logger.Error("Failed to unmarshal football result", "event_id", outcome.EventID, "error", err)
return domain.CreateResult{}, err return domain.CreateResult{}, err
} }
result := fbResp result := fbResp
isEventActive, err := s.parseTimeStatus(result.TimeStatus)
if err != nil {
s.logger.Error("Failed to parse time status", "event_id", eventID, "error", err)
return domain.CreateResult{}, err
}
if !isEventActive {
s.logger.Warn("Event is not active", "event_id", eventID)
return domain.CreateResult{}, ErrEventIsNotActive
}
finalScore := parseSS(result.SS) finalScore := parseSS(result.SS)
firstHalfScore := parseScore(result.Scores.FirstHalf.Home, result.Scores.FirstHalf.Away) firstHalfScore := parseScore(result.Scores.FirstHalf.Home, result.Scores.FirstHalf.Away)
secondHalfScore := parseScore(result.Scores.SecondHalf.Home, result.Scores.SecondHalf.Away) secondHalfScore := parseScore(result.Scores.SecondHalf.Home, result.Scores.SecondHalf.Away)
@ -324,15 +553,15 @@ func (s *Service) parseFootball(resultRes json.RawMessage, eventID, oddID, marke
halfTimeCorners := parseStats(result.Stats.HalfTimeCorners) halfTimeCorners := parseStats(result.Stats.HalfTimeCorners)
status, err := s.evaluateFootballOutcome(outcome, finalScore, firstHalfScore, secondHalfScore, corners, halfTimeCorners, result.Events) status, err := s.evaluateFootballOutcome(outcome, finalScore, firstHalfScore, secondHalfScore, corners, halfTimeCorners, result.Events)
if err != nil { if err != nil {
s.logger.Error("Failed to evaluate football outcome", "event_id", eventID, "market_id", marketID, "error", err) s.logger.Error("Failed to evaluate football outcome", "event_id", outcome.EventID, "market_id", outcome.MarketID, "error", err)
return domain.CreateResult{}, err return domain.CreateResult{}, err
} }
return domain.CreateResult{ return domain.CreateResult{
BetOutcomeID: 0, BetOutcomeID: 0,
EventID: eventID, EventID: outcome.EventID,
OddID: oddID, OddID: outcome.OddID,
MarketID: marketID, MarketID: outcome.MarketID,
Status: status, Status: status,
Score: result.SS, Score: result.SS,
}, nil }, nil
@ -346,15 +575,6 @@ func (s *Service) parseBasketball(response json.RawMessage, eventID, oddID, mark
s.logger.Error("Failed to unmarshal basketball result", "event_id", eventID, "error", err) s.logger.Error("Failed to unmarshal basketball result", "event_id", eventID, "error", err)
return domain.CreateResult{}, err return domain.CreateResult{}, err
} }
isEventActive, err := s.parseTimeStatus(basketBallRes.TimeStatus)
if err != nil {
s.logger.Error("Failed to parse time status", "event_id", eventID, "error", err)
return domain.CreateResult{}, err
}
if !isEventActive {
s.logger.Warn("Event is not active", "event_id", eventID)
return domain.CreateResult{}, ErrEventIsNotActive
}
status, err := s.evaluateBasketballOutcome(outcome, basketBallRes) status, err := s.evaluateBasketballOutcome(outcome, basketBallRes)
if err != nil { if err != nil {
@ -379,15 +599,6 @@ func (s *Service) parseIceHockey(response json.RawMessage, eventID, oddID, marke
s.logger.Error("Failed to unmarshal ice hockey result", "event_id", eventID, "error", err) s.logger.Error("Failed to unmarshal ice hockey result", "event_id", eventID, "error", err)
return domain.CreateResult{}, err return domain.CreateResult{}, err
} }
isEventActive, err := s.parseTimeStatus(iceHockeyRes.TimeStatus)
if err != nil {
s.logger.Error("Failed to parse time status", "event_id", eventID, "error", err)
return domain.CreateResult{}, err
}
if !isEventActive {
s.logger.Warn("Event is not active", "event_id", eventID)
return domain.CreateResult{}, ErrEventIsNotActive
}
status, err := s.evaluateIceHockeyOutcome(outcome, iceHockeyRes) status, err := s.evaluateIceHockeyOutcome(outcome, iceHockeyRes)
if err != nil { if err != nil {

View File

@ -21,38 +21,50 @@ 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: "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 * * * *", // Every 15 Minutes // spec: "0 */5 * * * *", // Every 5 Minutes
task: func() { // task: func() {
log.Println("Fetching results for upcoming events...") // log.Println("Updating expired events...")
if err := resultService.FetchAndProcessResults(context.Background()); err != nil { // if _, err := resultService.CheckAndUpdateExpiredEvents(context.Background()); err != nil {
log.Printf("Failed to process result: %v", err) // log.Printf("Failed to update events: %v", err)
} else { // } else {
log.Printf("Successfully processed all outcomes") // log.Printf("Successfully updated expired events")
} // }
}, // },
}, // },
// {
// spec: "0 */15 * * * *", // Every 15 Minutes
// task: func() {
// log.Println("Fetching results for upcoming events...")
// if err := resultService.FetchAndProcessResults(context.Background()); err != nil {
// log.Printf("Failed to process result: %v", err)
// } else {
// log.Printf("Successfully processed all outcomes")
// }
// },
// },
} }
for _, job := range schedule { for _, job := range schedule {
// job.task() 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)
} }

View File

@ -39,7 +39,8 @@ func (h *Handler) CreateBet(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.PlaceBet(c.Context(), req, userID, role) res, err := h.betSvc.
PlaceBet(c.Context(), req, userID, role)
if err != nil { if err != nil {
h.logger.Error("PlaceBet failed", "error", err) h.logger.Error("PlaceBet failed", "error", err)

View File

@ -13,6 +13,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation"
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
@ -42,6 +43,7 @@ type Handler struct {
veliVirtualGameSvc veli.VeliVirtualGameService veliVirtualGameSvc veli.VeliVirtualGameService
recommendationSvc recommendation.RecommendationService recommendationSvc recommendation.RecommendationService
authSvc *authentication.Service authSvc *authentication.Service
resultSvc result.Service
jwtConfig jwtutil.JwtConfig jwtConfig jwtutil.JwtConfig
validator *customvalidator.CustomValidator validator *customvalidator.CustomValidator
Cfg *config.Config Cfg *config.Config
@ -67,6 +69,7 @@ func New(
companySvc *company.Service, companySvc *company.Service,
prematchSvc *odds.ServiceImpl, prematchSvc *odds.ServiceImpl,
eventSvc event.Service, eventSvc event.Service,
resultSvc result.Service,
cfg *config.Config, cfg *config.Config,
) *Handler { ) *Handler {
return &Handler{ return &Handler{
@ -88,6 +91,7 @@ func New(
veliVirtualGameSvc: veliVirtualGameSvc, veliVirtualGameSvc: veliVirtualGameSvc,
recommendationSvc: recommendationSvc, recommendationSvc: recommendationSvc,
authSvc: authSvc, authSvc: authSvc,
resultSvc: resultSvc,
jwtConfig: jwtConfig, jwtConfig: jwtConfig,
Cfg: cfg, Cfg: cfg,
} }

View File

@ -156,7 +156,14 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error {
} }
events, total, err := h.eventSvc.GetPaginatedUpcomingEvents( events, total, err := h.eventSvc.GetPaginatedUpcomingEvents(
c.Context(), limit, offset, leagueID, sportID, firstStartTime, lastStartTime) c.Context(), domain.EventFilter{
SportID: sportID,
LeagueID: leagueID,
FirstStartTime: firstStartTime,
LastStartTime: lastStartTime,
Limit: limit,
Offset: offset,
})
// fmt.Printf("League ID: %v", leagueID) // fmt.Printf("League ID: %v", leagueID)
if err != nil { if err != nil {

View File

@ -0,0 +1,47 @@
package handlers
import (
"encoding/json"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2"
)
type ResultRes struct {
ResultData json.RawMessage `json:"result_data"`
Outcomes []domain.BetOutcome `json:"outcomes"`
}
// This will take an event ID and return the success results for
// all the odds for that event.
// @Summary Get results for an event
// @Description Get results for an event
// @Tags result
// @Accept json
// @Produce json
// @Param id path string true "Event ID"
// @Success 200 {array} ResultRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /result/{id} [get]
func (h *Handler) GetResultsByEventID(c *fiber.Ctx) error {
eventID := c.Params("id")
if eventID == "" {
h.logger.Error("Event ID is required")
return fiber.NewError(fiber.StatusBadRequest, "Event ID is required")
}
results, outcomes, err := h.resultSvc.GetResultsForEvent(c.Context(), eventID)
if err != nil {
h.logger.Error("Failed to get results by Event ID", "eventID", eventID, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve results")
}
resultRes := ResultRes{
ResultData: results,
Outcomes: outcomes,
}
return response.WriteJSON(c, fiber.StatusOK, "Results retrieved successfully", resultRes, nil)
}

View File

@ -34,6 +34,7 @@ func (a *App) initAppRoutes() {
a.companySvc, a.companySvc,
a.prematchSvc, a.prematchSvc,
a.eventSvc, a.eventSvc,
*a.resultSvc,
a.cfg, a.cfg,
) )
@ -121,6 +122,8 @@ func (a *App) initAppRoutes() {
a.fiber.Get("/prematch/events", h.GetAllUpcomingEvents) a.fiber.Get("/prematch/events", h.GetAllUpcomingEvents)
a.fiber.Get("/prematch/odds/upcoming/:upcoming_id", h.GetPrematchOddsByUpcomingID) a.fiber.Get("/prematch/odds/upcoming/:upcoming_id", h.GetPrematchOddsByUpcomingID)
a.fiber.Get("/result/:id", h.GetResultsByEventID)
// Swagger // Swagger
a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler()) a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler())