From ec02497f9790bee0141423eef1f865154e199243 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Sat, 31 May 2025 18:14:46 +0300 Subject: [PATCH 1/5] event fetch for bwin --- internal/services/event/service.go | 57 +++++++++++++++++++++++++----- makefile | 2 -- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/internal/services/event/service.go b/internal/services/event/service.go index b24a8ec..1fc2f12 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -35,9 +35,10 @@ func (s *service) FetchLiveEvents(ctx context.Context) error { name string source string }{ - {"https://api.b365api.com/v1/bet365/inplay?sport_id=%d&token=%s", "bet365"}, + {"https://api.b365api.com/v1/bet365/inplay?sport_id=%d&&token=%s", "bet365"}, {"https://api.b365api.com/v1/betfair/sb/inplay?sport_id=%d&token=%s", "betfair"}, {"https://api.b365api.com/v1/1xbet/inplay?sport_id=%d&token=%s", "1xbet"}, + {"https://api.b365api.com/v1/bwin/inplay?sport_id=%d&token=%s", "bwin"}, } for _, url := range urls { @@ -76,12 +77,14 @@ func (s *service) fetchLiveEvents(ctx context.Context, url, source string) error events := []domain.Event{} switch source { case "bet365": - events = handleBet365prematch(body, sportID) + events = handleBet365prematch(body, sportID, source) case "betfair": events = handleBetfairprematch(body, sportID, source) case "1xbet": // betfair and 1xbet have the same result structure events = handleBetfairprematch(body, sportID, source) + case "bwin": + events = handleBwinprematch(body, sportID, source) } for _, event := range events { @@ -98,7 +101,7 @@ func (s *service) fetchLiveEvents(ctx context.Context, url, source string) error } -func handleBet365prematch(body []byte, sportID int) []domain.Event { +func handleBet365prematch(body []byte, sportID int, source string) []domain.Event { var data struct { Success int `json:"success"` Results [][]map[string]interface{} `json:"results"` @@ -106,7 +109,7 @@ func handleBet365prematch(body []byte, sportID int) []domain.Event { events := []domain.Event{} if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { - fmt.Printf(" Decode failed for sport_id=%d\nRaw: %s\n", sportID, string(body)) + fmt.Printf("%s: Decode failed for sport_id=%d\nRaw: %s\n", source, sportID, string(body)) return events } @@ -135,7 +138,7 @@ func handleBet365prematch(body []byte, sportID int) []domain.Event { Status: "live", MatchPeriod: getInt(ev["MD"]), AddedTime: getInt(ev["TA"]), - Source: "bet365", + Source: source, } events = append(events, event) @@ -153,7 +156,7 @@ func handleBetfairprematch(body []byte, sportID int, source string) []domain.Eve events := []domain.Event{} if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { - fmt.Printf(" Decode failed for sport_id=%d\nRaw: %s\n", sportID, string(body)) + fmt.Printf("%s: Decode failed for sport_id=%d\nRaw: %s\n", source, sportID, string(body)) return events } @@ -182,6 +185,42 @@ func handleBetfairprematch(body []byte, sportID int, source string) []domain.Eve return events } +func handleBwinprematch(body []byte, sportID int, source string) []domain.Event { + var data struct { + Success int `json:"success"` + Results []map[string]interface{} `json:"results"` + } + + events := []domain.Event{} + if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { + fmt.Printf("%s: Decode failed for sport_id=%d\nRaw: %s\n", source, sportID, string(body)) + return events + } + + for _, ev := range data.Results { + homeTeam := getString(ev["HomeTeam"]) + awayTeam := getString(ev["HomeTeam"]) + + event := domain.Event{ + ID: getString(ev["Id"]), + SportID: fmt.Sprintf("%d", sportID), + TimerStatus: "1", + HomeTeam: homeTeam, + AwayTeam: awayTeam, + StartTime: time.Now().UTC().Format(time.RFC3339), + LeagueID: getString(ev["LeagueId"]), + LeagueName: getString(ev["LeagueName"]), + IsLive: true, + Status: "live", + Source: source, + } + + events = append(events, event) + } + + return events +} + func (s *service) FetchUpcomingEvents(ctx context.Context) error { var wg sync.WaitGroup urls := []struct { @@ -215,8 +254,8 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour for _, sportID := range sportIDs { for page <= totalPages { page = page + 1 - url := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s&page=%d", sportID, s.token, page) - log.Printf("📡 Fetching data for event data page %d", page) + url := fmt.Sprintf(url, sportID, s.token, page) + log.Printf("📡 Fetching data from %s, for event data page %d", source, page) resp, err := http.Get(url) if err != nil { log.Printf("❌ Failed to fetch event data for page %d: %v", page, err) @@ -249,7 +288,7 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour if !slices.Contains(domain.SupportedLeagues, leagueID) { // fmt.Printf("⚠️ Skipping league %s (%d) as it is not supported\n", ev.League.Name, leagueID) skippedLeague = append(skippedLeague, ev.League.Name) - continue + // continue } event := domain.UpcomingEvent{ diff --git a/makefile b/makefile index 5e85c50..a149170 100644 --- a/makefile +++ b/makefile @@ -55,8 +55,6 @@ db-up: .PHONY: db-down db-down: @docker compose down -postgres: - @docker exec -it fortunebet-backend-postgres-1 psql -U root -d gh .PHONY: sqlc-gen sqlc-gen: From 84bbe53bb796a4652f16143225757afd2220d4d6 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Mon, 2 Jun 2025 00:59:22 +0300 Subject: [PATCH 2/5] odds and events fetch for bwin (together) --- internal/domain/odds.go | 4 +- internal/repository/odds.go | 68 ++++++----- internal/services/event/service.go | 41 +------ internal/services/odds/service.go | 174 ++++++++++++++++++++++++++++- 4 files changed, 219 insertions(+), 68 deletions(-) diff --git a/internal/domain/odds.go b/internal/domain/odds.go index 990c6a0..b617079 100644 --- a/internal/domain/odds.go +++ b/internal/domain/odds.go @@ -1,7 +1,6 @@ package domain import ( - "encoding/json" "time" ) @@ -15,10 +14,11 @@ type Market struct { MarketName string MarketID string UpdatedAt time.Time - Odds []json.RawMessage + Odds []map[string]interface{} Name string Handicap string OddsVal float64 + Source string } type Odd struct { diff --git a/internal/repository/odds.go b/internal/repository/odds.go index fd20d1c..875ea97 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -17,15 +17,19 @@ func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error { return nil } - for _, raw := range m.Odds { - var item map[string]interface{} - if err := json.Unmarshal(raw, &item); err != nil { - continue - } + for _, item := range m.Odds { + var name string + var oddsVal float64 - name := getString(item["name"]) + if m.Source == "bwin" { + nameValue := getMap(item["name"]) + name = getString(nameValue["value"]) + oddsVal = getFloat(item["odds"]) + } else { + name = getString(item["name"]) + oddsVal = getConvertedFloat(item["odds"]) + } handicap := getString(item["handicap"]) - oddsVal := getFloat(item["odds"]) rawOddsBytes, _ := json.Marshal(m.Odds) @@ -43,7 +47,7 @@ func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error { Category: pgtype.Text{Valid: false}, RawOdds: rawOddsBytes, IsActive: pgtype.Bool{Bool: true, Valid: true}, - Source: pgtype.Text{String: "b365api", Valid: true}, + Source: pgtype.Text{String: m.Source, Valid: true}, FetchedAt: pgtype.Timestamp{Time: time.Now(), Valid: true}, } @@ -85,23 +89,6 @@ func writeFailedMarketLog(m domain.Market, err error) error { return writeErr } -func getString(v interface{}) string { - 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 -} - func (s *Store) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) { odds, err := s.queries.GetPrematchOdds(ctx) if err != nil { @@ -286,3 +273,34 @@ func (s *Store) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID stri return domainOdds, nil } + +func getString(v interface{}) string { + if s, ok := v.(string); ok { + return s + } + return "" +} + +func getConvertedFloat(v interface{}) float64 { + if s, ok := v.(string); ok { + f, err := strconv.ParseFloat(s, 64) + if err == nil { + return f + } + } + return 0 +} + +func getFloat(v interface{}) float64 { + if n, ok := v.(float64); ok { + return n + } + return 0 +} + +func getMap(v interface{}) map[string]interface{} { + if m, ok := v.(map[string]interface{}); ok { + return m + } + return nil +} diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 1fc2f12..1ad4310 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -38,7 +38,6 @@ func (s *service) FetchLiveEvents(ctx context.Context) error { {"https://api.b365api.com/v1/bet365/inplay?sport_id=%d&&token=%s", "bet365"}, {"https://api.b365api.com/v1/betfair/sb/inplay?sport_id=%d&token=%s", "betfair"}, {"https://api.b365api.com/v1/1xbet/inplay?sport_id=%d&token=%s", "1xbet"}, - {"https://api.b365api.com/v1/bwin/inplay?sport_id=%d&token=%s", "bwin"}, } for _, url := range urls { @@ -83,8 +82,6 @@ func (s *service) fetchLiveEvents(ctx context.Context, url, source string) error case "1xbet": // betfair and 1xbet have the same result structure events = handleBetfairprematch(body, sportID, source) - case "bwin": - events = handleBwinprematch(body, sportID, source) } for _, event := range events { @@ -185,42 +182,6 @@ func handleBetfairprematch(body []byte, sportID int, source string) []domain.Eve return events } -func handleBwinprematch(body []byte, sportID int, source string) []domain.Event { - var data struct { - Success int `json:"success"` - Results []map[string]interface{} `json:"results"` - } - - events := []domain.Event{} - if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { - fmt.Printf("%s: Decode failed for sport_id=%d\nRaw: %s\n", source, sportID, string(body)) - return events - } - - for _, ev := range data.Results { - homeTeam := getString(ev["HomeTeam"]) - awayTeam := getString(ev["HomeTeam"]) - - event := domain.Event{ - ID: getString(ev["Id"]), - SportID: fmt.Sprintf("%d", sportID), - TimerStatus: "1", - HomeTeam: homeTeam, - AwayTeam: awayTeam, - StartTime: time.Now().UTC().Format(time.RFC3339), - LeagueID: getString(ev["LeagueId"]), - LeagueName: getString(ev["LeagueName"]), - IsLive: true, - Status: "live", - Source: source, - } - - events = append(events, event) - } - - return events -} - func (s *service) FetchUpcomingEvents(ctx context.Context) error { var wg sync.WaitGroup urls := []struct { @@ -288,7 +249,7 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour if !slices.Contains(domain.SupportedLeagues, leagueID) { // fmt.Printf("⚠️ Skipping league %s (%d) as it is not supported\n", ev.League.Name, leagueID) skippedLeague = append(skippedLeague, ev.League.Name) - // continue + continue } event := domain.UpcomingEvent{ diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 36f3a8a..85ca2f7 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -10,6 +10,7 @@ import ( "log/slog" "net/http" "strconv" + "sync" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" @@ -35,6 +36,37 @@ func New(store *repository.Store, cfg *config.Config, logger *slog.Logger) *Serv // TODO Add the optimization to get 10 events at the same time func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { + var wg sync.WaitGroup + errChan := make(chan error, 2) + wg.Add(2) + + go func() { + defer wg.Done() + if err := s.fetchBet365Odds(ctx); err != nil { + errChan <- fmt.Errorf("bet365 odds fetching error: %w", err) + } + }() + + go func() { + defer wg.Done() + if err := s.fetchBwinOdds(ctx); err != nil { + errChan <- fmt.Errorf("bwin odds fetching error: %w", err) + } + }() + + var errs []error + for err := range errChan { + errs = append(errs, err) + } + + if len(errs) > 0 { + return errors.Join(errs...) + } + + return nil +} + +func (s *ServiceImpl) fetchBet365Odds(ctx context.Context) error { eventIDs, err := s.store.GetAllUpcomingEvents(ctx) if err != nil { log.Printf("❌ Failed to fetch upcoming event IDs: %v", err) @@ -107,6 +139,91 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { return nil } +func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error { + // getting odds for a specific event is not possible for bwin, most specific we can get is fetch odds on a single sport + // so instead of having event and odds fetched separetly event will also be fetched along with the odds + sportIds := []int{12, 7} + for _, sportId := range sportIds { + url := fmt.Sprintf("https://api.b365api.com/v1/bwin/prematch?sport_id=%d&token=%s", sportId, s.config.Bet365Token) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + log.Printf("❌ Failed to create request for sportId %d: %v", sportId, err) + continue + } + + resp, err := s.client.Do(req) + if err != nil { + log.Printf("❌ Failed to fetch request for sportId %d: %v", sportId, err) + continue + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("❌ Failed to read response body for sportId %d: %v", sportId, err) + continue + } + + var data struct { + Success int `json:"success"` + Results []map[string]interface{} `json:"results"` + } + + if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { + fmt.Printf("Decode failed for sport_id=%d\nRaw: %s\n", sportId, string(body)) + continue + } + + for _, res := range data.Results { + if getInt(res["Id"]) == -1 { + continue + } + + event := domain.Event{ + ID: strconv.Itoa(getInt(res["Id"])), + SportID: strconv.Itoa(getInt(res["SportId"])), + LeagueID: strconv.Itoa(getInt(res["LeagueId"])), + LeagueName: getString(res["Leaguename"]), + HomeTeam: getString(res["HomeTeam"]), + HomeTeamID: strconv.Itoa(getInt(res["HomeTeamId"])), + AwayTeam: getString(res["AwayTeam"]), + AwayTeamID: strconv.Itoa(getInt(res["AwayTeamId"])), + StartTime: time.Now().UTC().Format(time.RFC3339), + TimerStatus: "1", + IsLive: true, + Status: "live", + Source: "bwin", + } + + if err := s.store.SaveEvent(ctx, event); err != nil { + fmt.Printf("Could not store live event [id=%s]: %v\n", event.ID, err) + continue + } + + for _, m := range getMapArray(res["Markets"]) { + name := getMap(m["name"]) + marketName := getString(name["value"]) + + market := domain.Market{ + EventID: event.ID, + MarketID: getString(m["id"]), + MarketCategory: getString(m["category"]), + MarketName: marketName, + Source: "bwin", + } + + results := getMapArray(m["results"]) + market.Odds = results + + s.store.SaveNonLiveMarket(ctx, market) + + } + } + + } + return nil +} + func (s *ServiceImpl) parseFootball(ctx context.Context, res json.RawMessage) error { var footballRes domain.FootballOddsResponse if err := json.Unmarshal(res, &footballRes); err != nil { @@ -264,6 +381,13 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName continue } + marketOdds, err := convertRawMessage(market.Odds) + if err != nil { + s.logger.Error("failed to conver json.RawMessage to []map[string]interface{} for market_id: ", market.ID) + errs = append(errs, err) + continue + } + marketRecord := domain.Market{ EventID: eventID, FI: fi, @@ -272,7 +396,9 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName MarketName: market.Name, MarketID: marketIDstr, UpdatedAt: updatedAt, - Odds: market.Odds, + Odds: marketOdds, + // bwin won't reach this code so bet365 is hardcoded for now + Source: "bet365", } err = s.store.SaveNonLiveMarket(ctx, marketRecord) @@ -313,3 +439,49 @@ func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingI func (s *ServiceImpl) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset domain.ValidInt64) ([]domain.Odd, error) { return s.store.GetPaginatedPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset) } + +func getString(v interface{}) string { + if str, ok := v.(string); ok { + return str + } + return "" +} + +func getInt(v interface{}) int { + if n, ok := v.(float64); ok { + return int(n) + } + return -1 +} + +func getMap(v interface{}) map[string]interface{} { + if m, ok := v.(map[string]interface{}); ok { + return m + } + return nil +} + +func getMapArray(v interface{}) []map[string]interface{} { + result := []map[string]interface{}{} + if arr, ok := v.([]interface{}); ok { + for _, item := range arr { + if m, ok := item.(map[string]interface{}); ok { + result = append(result, m) + } + } + } + return result +} + +func convertRawMessage(rawMessages []json.RawMessage) ([]map[string]interface{}, error) { + var result []map[string]interface{} + for _, raw := range rawMessages { + var m map[string]interface{} + if err := json.Unmarshal(raw, &m); err != nil { + return nil, err + } + result = append(result, m) + } + + return result, nil +} From eb4f2671420d1019303873dfa49e3f6c54d834b3 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Thu, 5 Jun 2025 01:56:40 +0300 Subject: [PATCH 3/5] refactor events table --- db/migrations/000001_fortune.up.sql | 8 +-- gen/db/cashier.sql.go | 2 +- gen/db/events.sql.go | 56 ++++++++-------- gen/db/models.go | 34 ++++++++-- internal/domain/common.go | 4 ++ internal/domain/event.go | 16 ++--- internal/repository/event.go | 74 ++++++++++----------- internal/services/bet/service.go | 17 ++--- internal/services/event/port.go | 2 +- internal/services/event/service.go | 49 ++++++++------ internal/services/odds/service.go | 61 ++++++++--------- internal/services/result/service.go | 8 +-- internal/web_server/handlers/bet_handler.go | 27 +++++--- internal/web_server/handlers/prematch.go | 25 ++++--- 14 files changed, 214 insertions(+), 169 deletions(-) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 56f3d51..50a4b75 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -183,15 +183,15 @@ CREATE TABLE IF NOT EXISTS branch_cashiers ( ); CREATE TABLE events ( id TEXT PRIMARY KEY, - sport_id TEXT, + sport_id INT, match_name TEXT, home_team TEXT, away_team TEXT, - home_team_id TEXT, - away_team_id TEXT, + home_team_id INT, + away_team_id INT, home_kit_image TEXT, away_kit_image TEXT, - league_id TEXT, + league_id INT, league_name TEXT, league_cc TEXT, start_time TIMESTAMP, diff --git a/gen/db/cashier.sql.go b/gen/db/cashier.sql.go index d0f6768..bb71cb2 100644 --- a/gen/db/cashier.sql.go +++ b/gen/db/cashier.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: cashier.sql package dbgen diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index d7c6824..a2a53d6 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -47,15 +47,15 @@ ORDER BY start_time ASC type GetAllUpcomingEventsRow struct { ID string `json:"id"` - SportID pgtype.Text `json:"sport_id"` + SportID pgtype.Int4 `json:"sport_id"` MatchName pgtype.Text `json:"match_name"` HomeTeam pgtype.Text `json:"home_team"` AwayTeam pgtype.Text `json:"away_team"` - HomeTeamID pgtype.Text `json:"home_team_id"` - AwayTeamID pgtype.Text `json:"away_team_id"` + HomeTeamID pgtype.Int4 `json:"home_team_id"` + AwayTeamID pgtype.Int4 `json:"away_team_id"` HomeKitImage pgtype.Text `json:"home_kit_image"` AwayKitImage pgtype.Text `json:"away_kit_image"` - LeagueID pgtype.Text `json:"league_id"` + LeagueID pgtype.Int4 `json:"league_id"` LeagueName pgtype.Text `json:"league_name"` LeagueCc pgtype.Text `json:"league_cc"` StartTime pgtype.Timestamp `json:"start_time"` @@ -128,15 +128,15 @@ ORDER BY start_time ASC type GetExpiredUpcomingEventsRow struct { ID string `json:"id"` - SportID pgtype.Text `json:"sport_id"` + SportID pgtype.Int4 `json:"sport_id"` MatchName pgtype.Text `json:"match_name"` HomeTeam pgtype.Text `json:"home_team"` AwayTeam pgtype.Text `json:"away_team"` - HomeTeamID pgtype.Text `json:"home_team_id"` - AwayTeamID pgtype.Text `json:"away_team_id"` + HomeTeamID pgtype.Int4 `json:"home_team_id"` + AwayTeamID pgtype.Int4 `json:"away_team_id"` HomeKitImage pgtype.Text `json:"home_kit_image"` AwayKitImage pgtype.Text `json:"away_kit_image"` - LeagueID pgtype.Text `json:"league_id"` + LeagueID pgtype.Int4 `json:"league_id"` LeagueName pgtype.Text `json:"league_name"` LeagueCc pgtype.Text `json:"league_cc"` StartTime pgtype.Timestamp `json:"start_time"` @@ -226,8 +226,8 @@ LIMIT $6 OFFSET $5 ` type GetPaginatedUpcomingEventsParams struct { - LeagueID pgtype.Text `json:"league_id"` - SportID pgtype.Text `json:"sport_id"` + LeagueID pgtype.Int4 `json:"league_id"` + SportID pgtype.Int4 `json:"sport_id"` LastStartTime pgtype.Timestamp `json:"last_start_time"` FirstStartTime pgtype.Timestamp `json:"first_start_time"` Offset pgtype.Int4 `json:"offset"` @@ -236,15 +236,15 @@ type GetPaginatedUpcomingEventsParams struct { type GetPaginatedUpcomingEventsRow struct { ID string `json:"id"` - SportID pgtype.Text `json:"sport_id"` + SportID pgtype.Int4 `json:"sport_id"` MatchName pgtype.Text `json:"match_name"` HomeTeam pgtype.Text `json:"home_team"` AwayTeam pgtype.Text `json:"away_team"` - HomeTeamID pgtype.Text `json:"home_team_id"` - AwayTeamID pgtype.Text `json:"away_team_id"` + HomeTeamID pgtype.Int4 `json:"home_team_id"` + AwayTeamID pgtype.Int4 `json:"away_team_id"` HomeKitImage pgtype.Text `json:"home_kit_image"` AwayKitImage pgtype.Text `json:"away_kit_image"` - LeagueID pgtype.Text `json:"league_id"` + LeagueID pgtype.Int4 `json:"league_id"` LeagueName pgtype.Text `json:"league_name"` LeagueCc pgtype.Text `json:"league_cc"` StartTime pgtype.Timestamp `json:"start_time"` @@ -323,8 +323,8 @@ WHERE is_live = false ` type GetTotalEventsParams struct { - LeagueID pgtype.Text `json:"league_id"` - SportID pgtype.Text `json:"sport_id"` + LeagueID pgtype.Int4 `json:"league_id"` + SportID pgtype.Int4 `json:"sport_id"` LastStartTime pgtype.Timestamp `json:"last_start_time"` FirstStartTime pgtype.Timestamp `json:"first_start_time"` } @@ -368,15 +368,15 @@ LIMIT 1 type GetUpcomingByIDRow struct { ID string `json:"id"` - SportID pgtype.Text `json:"sport_id"` + SportID pgtype.Int4 `json:"sport_id"` MatchName pgtype.Text `json:"match_name"` HomeTeam pgtype.Text `json:"home_team"` AwayTeam pgtype.Text `json:"away_team"` - HomeTeamID pgtype.Text `json:"home_team_id"` - AwayTeamID pgtype.Text `json:"away_team_id"` + HomeTeamID pgtype.Int4 `json:"home_team_id"` + AwayTeamID pgtype.Int4 `json:"away_team_id"` HomeKitImage pgtype.Text `json:"home_kit_image"` AwayKitImage pgtype.Text `json:"away_kit_image"` - LeagueID pgtype.Text `json:"league_id"` + LeagueID pgtype.Int4 `json:"league_id"` LeagueName pgtype.Text `json:"league_name"` LeagueCc pgtype.Text `json:"league_cc"` StartTime pgtype.Timestamp `json:"start_time"` @@ -484,15 +484,15 @@ SET sport_id = EXCLUDED.sport_id, type InsertEventParams struct { ID string `json:"id"` - SportID pgtype.Text `json:"sport_id"` + SportID pgtype.Int4 `json:"sport_id"` MatchName pgtype.Text `json:"match_name"` HomeTeam pgtype.Text `json:"home_team"` AwayTeam pgtype.Text `json:"away_team"` - HomeTeamID pgtype.Text `json:"home_team_id"` - AwayTeamID pgtype.Text `json:"away_team_id"` + HomeTeamID pgtype.Int4 `json:"home_team_id"` + AwayTeamID pgtype.Int4 `json:"away_team_id"` HomeKitImage pgtype.Text `json:"home_kit_image"` AwayKitImage pgtype.Text `json:"away_kit_image"` - LeagueID pgtype.Text `json:"league_id"` + LeagueID pgtype.Int4 `json:"league_id"` LeagueName pgtype.Text `json:"league_name"` LeagueCc pgtype.Text `json:"league_cc"` StartTime pgtype.Timestamp `json:"start_time"` @@ -591,15 +591,15 @@ SET sport_id = EXCLUDED.sport_id, type InsertUpcomingEventParams struct { ID string `json:"id"` - SportID pgtype.Text `json:"sport_id"` + SportID pgtype.Int4 `json:"sport_id"` MatchName pgtype.Text `json:"match_name"` HomeTeam pgtype.Text `json:"home_team"` AwayTeam pgtype.Text `json:"away_team"` - HomeTeamID pgtype.Text `json:"home_team_id"` - AwayTeamID pgtype.Text `json:"away_team_id"` + HomeTeamID pgtype.Int4 `json:"home_team_id"` + AwayTeamID pgtype.Int4 `json:"away_team_id"` HomeKitImage pgtype.Text `json:"home_kit_image"` AwayKitImage pgtype.Text `json:"away_kit_image"` - LeagueID pgtype.Text `json:"league_id"` + LeagueID pgtype.Int4 `json:"league_id"` LeagueName pgtype.Text `json:"league_name"` LeagueCc pgtype.Text `json:"league_cc"` StartTime pgtype.Timestamp `json:"start_time"` diff --git a/gen/db/models.go b/gen/db/models.go index 7c65cfc..75c6af8 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -176,15 +176,15 @@ type CustomerWallet struct { type Event struct { ID string `json:"id"` - SportID pgtype.Text `json:"sport_id"` + SportID pgtype.Int4 `json:"sport_id"` MatchName pgtype.Text `json:"match_name"` HomeTeam pgtype.Text `json:"home_team"` AwayTeam pgtype.Text `json:"away_team"` - HomeTeamID pgtype.Text `json:"home_team_id"` - AwayTeamID pgtype.Text `json:"away_team_id"` + HomeTeamID pgtype.Int4 `json:"home_team_id"` + AwayTeamID pgtype.Int4 `json:"away_team_id"` HomeKitImage pgtype.Text `json:"home_kit_image"` AwayKitImage pgtype.Text `json:"away_kit_image"` - LeagueID pgtype.Text `json:"league_id"` + LeagueID pgtype.Int4 `json:"league_id"` LeagueName pgtype.Text `json:"league_name"` LeagueCc pgtype.Text `json:"league_cc"` StartTime pgtype.Timestamp `json:"start_time"` @@ -383,6 +383,32 @@ type User struct { ReferredBy pgtype.Text `json:"referred_by"` } +type UserGameInteraction struct { + ID int64 `json:"id"` + UserID int64 `json:"user_id"` + GameID int64 `json:"game_id"` + InteractionType string `json:"interaction_type"` + Amount pgtype.Numeric `json:"amount"` + DurationSeconds pgtype.Int4 `json:"duration_seconds"` + CreatedAt pgtype.Timestamptz `json:"created_at"` +} + +type VirtualGame struct { + ID int64 `json:"id"` + Name string `json:"name"` + Provider string `json:"provider"` + Category string `json:"category"` + MinBet pgtype.Numeric `json:"min_bet"` + MaxBet pgtype.Numeric `json:"max_bet"` + Volatility string `json:"volatility"` + Rtp pgtype.Numeric `json:"rtp"` + IsFeatured pgtype.Bool `json:"is_featured"` + PopularityScore pgtype.Int4 `json:"popularity_score"` + ThumbnailUrl pgtype.Text `json:"thumbnail_url"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + type VirtualGameSession struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` diff --git a/internal/domain/common.go b/internal/domain/common.go index fc652d1..43c5ed0 100644 --- a/internal/domain/common.go +++ b/internal/domain/common.go @@ -13,6 +13,10 @@ type ValidInt struct { Value int Valid bool } +type ValidInt32 struct { + Value int32 + Valid bool +} type ValidString struct { Value string diff --git a/internal/domain/event.go b/internal/domain/event.go index 9a463ca..a1c2a6f 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -4,15 +4,15 @@ import "time" type Event struct { ID string - SportID string + SportID int32 MatchName string HomeTeam string AwayTeam string - HomeTeamID string - AwayTeamID string + HomeTeamID int32 + AwayTeamID int32 HomeKitImage string AwayKitImage string - LeagueID string + LeagueID int32 LeagueName string LeagueCC string StartTime string @@ -54,15 +54,15 @@ type BetResult struct { type UpcomingEvent struct { ID string // Event ID - SportID string // Sport ID + SportID int32 // Sport ID MatchName string // Match or event name HomeTeam string // Home team name (if available) AwayTeam string // Away team name (can be empty/null) - HomeTeamID string // Home team ID - AwayTeamID string // Away team ID (can be empty/null) + HomeTeamID int32 // Home team ID + AwayTeamID int32 // Away team ID (can be empty/null) HomeKitImage string // Kit or image for home team (optional) AwayKitImage string // Kit or image for away team (optional) - LeagueID string // League ID + LeagueID int32 // League ID LeagueName string // League name LeagueCC string // League country code StartTime time.Time // Converted from "time" field in UNIX format diff --git a/internal/repository/event.go b/internal/repository/event.go index 8f2ade8..2366e75 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -21,15 +21,15 @@ func (s *Store) SaveEvent(ctx context.Context, e domain.Event) error { return s.queries.InsertEvent(ctx, dbgen.InsertEventParams{ ID: e.ID, - SportID: pgtype.Text{String: e.SportID, Valid: true}, + SportID: pgtype.Int4{Int32: e.SportID, Valid: true}, MatchName: pgtype.Text{String: e.MatchName, Valid: true}, HomeTeam: pgtype.Text{String: e.HomeTeam, Valid: true}, AwayTeam: pgtype.Text{String: e.AwayTeam, Valid: true}, - HomeTeamID: pgtype.Text{String: e.HomeTeamID, Valid: true}, - AwayTeamID: pgtype.Text{String: e.AwayTeamID, Valid: true}, + HomeTeamID: pgtype.Int4{Int32: e.HomeTeamID, Valid: true}, + AwayTeamID: pgtype.Int4{Int32: e.AwayTeamID, Valid: true}, HomeKitImage: pgtype.Text{String: e.HomeKitImage, Valid: true}, AwayKitImage: pgtype.Text{String: e.AwayKitImage, Valid: true}, - LeagueID: pgtype.Text{String: e.LeagueID, Valid: true}, + LeagueID: pgtype.Int4{Int32: e.LeagueID, Valid: true}, LeagueName: pgtype.Text{String: e.LeagueName, Valid: true}, LeagueCc: pgtype.Text{String: e.LeagueCC, Valid: true}, StartTime: pgtype.Timestamp{Time: parsedTime, Valid: true}, @@ -46,15 +46,15 @@ func (s *Store) SaveEvent(ctx context.Context, e domain.Event) error { func (s *Store) SaveUpcomingEvent(ctx context.Context, e domain.UpcomingEvent) error { return s.queries.InsertUpcomingEvent(ctx, dbgen.InsertUpcomingEventParams{ ID: e.ID, - SportID: pgtype.Text{String: e.SportID, Valid: true}, + SportID: pgtype.Int4{Int32: e.SportID, Valid: true}, MatchName: pgtype.Text{String: e.MatchName, Valid: true}, HomeTeam: pgtype.Text{String: e.HomeTeam, Valid: true}, AwayTeam: pgtype.Text{String: e.AwayTeam, Valid: true}, - HomeTeamID: pgtype.Text{String: e.HomeTeamID, Valid: true}, - AwayTeamID: pgtype.Text{String: e.AwayTeamID, Valid: true}, + HomeTeamID: pgtype.Int4{Int32: e.HomeTeamID, Valid: true}, + AwayTeamID: pgtype.Int4{Int32: e.AwayTeamID, Valid: true}, HomeKitImage: pgtype.Text{String: e.HomeKitImage, Valid: true}, AwayKitImage: pgtype.Text{String: e.AwayKitImage, Valid: true}, - LeagueID: pgtype.Text{String: e.LeagueID, Valid: true}, + LeagueID: pgtype.Int4{Int32: e.LeagueID, Valid: true}, LeagueName: pgtype.Text{String: e.LeagueName, Valid: true}, LeagueCc: pgtype.Text{String: e.LeagueCC, Valid: true}, StartTime: pgtype.Timestamp{Time: e.StartTime, Valid: true}, @@ -75,15 +75,15 @@ func (s *Store) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEven for i, e := range events { upcomingEvents[i] = domain.UpcomingEvent{ ID: e.ID, - SportID: e.SportID.String, + SportID: e.SportID.Int32, MatchName: e.MatchName.String, HomeTeam: e.HomeTeam.String, AwayTeam: e.AwayTeam.String, - HomeTeamID: e.HomeTeamID.String, - AwayTeamID: e.AwayTeamID.String, + HomeTeamID: e.HomeTeamID.Int32, + AwayTeamID: e.AwayTeamID.Int32, HomeKitImage: e.HomeKitImage.String, AwayKitImage: e.AwayKitImage.String, - LeagueID: e.LeagueID.String, + LeagueID: e.LeagueID.Int32, LeagueName: e.LeagueName.String, LeagueCC: e.LeagueCc.String, StartTime: e.StartTime.Time.UTC(), @@ -103,15 +103,15 @@ func (s *Store) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.Upcoming for i, e := range events { upcomingEvents[i] = domain.UpcomingEvent{ ID: e.ID, - SportID: e.SportID.String, + SportID: e.SportID.Int32, MatchName: e.MatchName.String, HomeTeam: e.HomeTeam.String, AwayTeam: e.AwayTeam.String, - HomeTeamID: e.HomeTeamID.String, - AwayTeamID: e.AwayTeamID.String, + HomeTeamID: e.HomeTeamID.Int32, + AwayTeamID: e.AwayTeamID.Int32, HomeKitImage: e.HomeKitImage.String, AwayKitImage: e.AwayKitImage.String, - LeagueID: e.LeagueID.String, + LeagueID: e.LeagueID.Int32, LeagueName: e.LeagueName.String, LeagueCC: e.LeagueCc.String, StartTime: e.StartTime.Time.UTC(), @@ -121,16 +121,16 @@ func (s *Store) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.Upcoming 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, limit domain.ValidInt64, offset domain.ValidInt64, leagueID domain.ValidInt32, sportID domain.ValidInt32, firstStartTime domain.ValidTime, lastStartTime domain.ValidTime) ([]domain.UpcomingEvent, int64, error) { events, err := s.queries.GetPaginatedUpcomingEvents(ctx, dbgen.GetPaginatedUpcomingEventsParams{ - LeagueID: pgtype.Text{ - String: leagueID.Value, - Valid: leagueID.Valid, + LeagueID: pgtype.Int4{ + Int32: leagueID.Value, + Valid: leagueID.Valid, }, - SportID: pgtype.Text{ - String: sportID.Value, - Valid: sportID.Valid, + SportID: pgtype.Int4{ + Int32: sportID.Value, + Valid: sportID.Valid, }, Limit: pgtype.Int4{ Int32: int32(limit.Value), @@ -157,15 +157,15 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit domain.Val for i, e := range events { upcomingEvents[i] = domain.UpcomingEvent{ ID: e.ID, - SportID: e.SportID.String, + SportID: e.SportID.Int32, MatchName: e.MatchName.String, HomeTeam: e.HomeTeam.String, AwayTeam: e.AwayTeam.String, - HomeTeamID: e.HomeTeamID.String, - AwayTeamID: e.AwayTeamID.String, + HomeTeamID: e.HomeTeamID.Int32, + AwayTeamID: e.AwayTeamID.Int32, HomeKitImage: e.HomeKitImage.String, AwayKitImage: e.AwayKitImage.String, - LeagueID: e.LeagueID.String, + LeagueID: e.LeagueID.Int32, LeagueName: e.LeagueName.String, LeagueCC: e.LeagueCc.String, StartTime: e.StartTime.Time.UTC(), @@ -173,13 +173,13 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit domain.Val } } totalCount, err := s.queries.GetTotalEvents(ctx, dbgen.GetTotalEventsParams{ - LeagueID: pgtype.Text{ - String: leagueID.Value, - Valid: leagueID.Valid, + LeagueID: pgtype.Int4{ + Int32: leagueID.Value, + Valid: leagueID.Valid, }, - SportID: pgtype.Text{ - String: sportID.Value, - Valid: sportID.Valid, + SportID: pgtype.Int4{ + Int32: sportID.Value, + Valid: sportID.Valid, }, FirstStartTime: pgtype.Timestamp{ Time: firstStartTime.Value.UTC(), @@ -205,15 +205,15 @@ func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.Upc return domain.UpcomingEvent{ ID: event.ID, - SportID: event.SportID.String, + SportID: event.SportID.Int32, MatchName: event.MatchName.String, HomeTeam: event.HomeTeam.String, AwayTeam: event.AwayTeam.String, - HomeTeamID: event.HomeTeamID.String, - AwayTeamID: event.AwayTeamID.String, + HomeTeamID: event.HomeTeamID.Int32, + AwayTeamID: event.AwayTeamID.Int32, HomeKitImage: event.HomeKitImage.String, AwayKitImage: event.AwayKitImage.String, - LeagueID: event.LeagueID.String, + LeagueID: event.LeagueID.Int32, LeagueName: event.LeagueName.String, LeagueCC: event.LeagueCc.String, StartTime: event.StartTime.Time.UTC(), diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index 65af3d7..cb65c9d 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -121,15 +121,11 @@ func (s *Service) GenerateBetOutcome(ctx context.Context, eventID int64, marketI if err != nil { return domain.CreateBetOutcome{}, err } - sportID, err := strconv.ParseInt(event.SportID, 10, 64) - if err != nil { - return domain.CreateBetOutcome{}, err - } newOutcome := domain.CreateBetOutcome{ EventID: eventID, OddID: oddID, MarketID: marketID, - SportID: sportID, + SportID: int64(event.SportID), HomeTeamName: event.HomeTeam, AwayTeamName: event.AwayTeam, MarketName: odds.MarketName, @@ -278,7 +274,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID return res, nil } -func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID, sportID, HomeTeam, AwayTeam string, StartTime time.Time, numMarkets int) ([]domain.CreateBetOutcome, float32, error) { +func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string, sportID int32, HomeTeam, AwayTeam string, StartTime time.Time, numMarkets int) ([]domain.CreateBetOutcome, float32, error) { var newOdds []domain.CreateBetOutcome var totalOdds float32 = 1 @@ -328,11 +324,6 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID, sportI s.logger.Error("Failed to parse odd", "error", err) continue } - sportID, err := strconv.ParseInt(sportID, 10, 64) - if err != nil { - s.logger.Error("Failed to get sport id", "error", err) - continue - } eventID, err := strconv.ParseInt(eventID, 10, 64) if err != nil { s.logger.Error("Failed to get event id", "error", err) @@ -356,7 +347,7 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID, sportI EventID: eventID, OddID: oddID, MarketID: marketID, - SportID: sportID, + SportID: int64(sportID), HomeTeamName: HomeTeam, AwayTeamName: AwayTeam, MarketName: marketName, @@ -379,7 +370,7 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID, sportI return newOdds, totalOdds, nil } -func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, leagueID, sportID domain.ValidString, firstStartTime, lastStartTime domain.ValidTime) (domain.CreateBetRes, error) { +func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, leagueID, sportID domain.ValidInt32, firstStartTime, lastStartTime domain.ValidTime) (domain.CreateBetRes, error) { // Get a unexpired event id diff --git a/internal/services/event/port.go b/internal/services/event/port.go index 94f4313..af8397e 100644 --- a/internal/services/event/port.go +++ b/internal/services/event/port.go @@ -11,7 +11,7 @@ type Service interface { FetchUpcomingEvents(ctx context.Context) error GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) - GetPaginatedUpcomingEvents(ctx context.Context, limit 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, limit domain.ValidInt64, offset domain.ValidInt64, leagueID domain.ValidInt32, sportID domain.ValidInt32, firstStartTime domain.ValidTime, lastStartTime domain.ValidTime) ([]domain.UpcomingEvent, int64, error) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) // GetAndStoreMatchResult(ctx context.Context, eventID string) error diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 1ad4310..2c6bc52 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -35,7 +35,7 @@ func (s *service) FetchLiveEvents(ctx context.Context) error { name string source string }{ - {"https://api.b365api.com/v1/bet365/inplay?sport_id=%d&&token=%s", "bet365"}, + {"https://api.b365api.com/v1/bet365/inplay?sport_id=%d&token=%s", "bet365"}, {"https://api.b365api.com/v1/betfair/sb/inplay?sport_id=%d&token=%s", "betfair"}, {"https://api.b365api.com/v1/1xbet/inplay?sport_id=%d&token=%s", "1xbet"}, } @@ -48,7 +48,6 @@ func (s *service) FetchLiveEvents(ctx context.Context) error { s.fetchLiveEvents(ctx, url.name, url.source) }() } - wg.Wait() return nil } @@ -118,17 +117,17 @@ func handleBet365prematch(body []byte, sportID int, source string) []domain.Even event := domain.Event{ ID: getString(ev["ID"]), - SportID: fmt.Sprintf("%d", sportID), + SportID: int32(sportID), MatchName: getString(ev["NA"]), Score: getString(ev["SS"]), MatchMinute: getInt(ev["TM"]), TimerStatus: getString(ev["TT"]), - HomeTeamID: getString(ev["HT"]), - AwayTeamID: getString(ev["AT"]), + HomeTeamID: getInt32(ev["HT"]), + AwayTeamID: getInt32(ev["AT"]), HomeKitImage: getString(ev["K1"]), AwayKitImage: getString(ev["K2"]), LeagueName: getString(ev["CT"]), - LeagueID: getString(ev["C2"]), + LeagueID: getInt32(ev["C2"]), LeagueCC: getString(ev["CB"]), StartTime: time.Now().UTC().Format(time.RFC3339), IsLive: true, @@ -159,17 +158,14 @@ func handleBetfairprematch(body []byte, sportID int, source string) []domain.Eve for _, ev := range data.Results { homeRaw, _ := ev["home"].(map[string]interface{}) - homeId, _ := homeRaw["id"].(string) - awayRaw, _ := ev["home"].(map[string]interface{}) - awayId, _ := awayRaw["id"].(string) event := domain.Event{ ID: getString(ev["id"]), - SportID: fmt.Sprintf("%d", sportID), + SportID: int32(sportID), TimerStatus: getString(ev["time_status"]), - HomeTeamID: homeId, - AwayTeamID: awayId, + HomeTeamID: getInt32(homeRaw["id"]), + AwayTeamID: getInt32(awayRaw["id"]), StartTime: time.Now().UTC().Format(time.RFC3339), IsLive: true, Status: "live", @@ -249,20 +245,21 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour if !slices.Contains(domain.SupportedLeagues, leagueID) { // fmt.Printf("⚠️ Skipping league %s (%d) as it is not supported\n", ev.League.Name, leagueID) skippedLeague = append(skippedLeague, ev.League.Name) - continue + // ! for now + // continue } event := domain.UpcomingEvent{ ID: ev.ID, - SportID: ev.SportID, + SportID: convertInt32(ev.SportID), MatchName: "", HomeTeam: ev.Home.Name, AwayTeam: "", // handle nil safely - HomeTeamID: ev.Home.ID, - AwayTeamID: "", + HomeTeamID: convertInt32(ev.Home.ID), + AwayTeamID: 0, HomeKitImage: "", AwayKitImage: "", - LeagueID: ev.League.ID, + LeagueID: convertInt32(ev.League.ID), LeagueName: ev.League.Name, LeagueCC: "", StartTime: time.Unix(startUnix, 0).UTC(), @@ -271,7 +268,7 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour if ev.Away != nil { event.AwayTeam = ev.Away.Name - event.AwayTeamID = ev.Away.ID + event.AwayTeamID = convertInt32(ev.Away.ID) event.MatchName = ev.Home.Name + " vs " + ev.Away.Name } @@ -309,6 +306,20 @@ func getInt(v interface{}) int { } return 0 } + +func getInt32(v interface{}) int32 { + if n, err := strconv.Atoi(getString(v)); err == nil { + return int32(n) + } + return 0 +} + +func convertInt32(num string) int32 { + if n, err := strconv.Atoi(num); err == nil { + return int32(n) + } + return 0 +} func (s *service) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) { return s.store.GetAllUpcomingEvents(ctx) } @@ -317,7 +328,7 @@ func (s *service) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.Upcomi return s.store.GetExpiredUpcomingEvents(ctx) } -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, limit domain.ValidInt64, offset domain.ValidInt64, leagueID domain.ValidInt32, sportID domain.ValidInt32, firstStartTime domain.ValidTime, lastStartTime domain.ValidTime) ([]domain.UpcomingEvent, int64, error) { return s.store.GetPaginatedUpcomingEvents(ctx, limit, offset, leagueID, sportID, firstStartTime, lastStartTime) } diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 85ca2f7..488c9bb 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -38,14 +38,15 @@ func New(store *repository.Store, cfg *config.Config, logger *slog.Logger) *Serv func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { var wg sync.WaitGroup errChan := make(chan error, 2) - wg.Add(2) + // wg.Add(2) + wg.Add(1) - go func() { - defer wg.Done() - if err := s.fetchBet365Odds(ctx); err != nil { - errChan <- fmt.Errorf("bet365 odds fetching error: %w", err) - } - }() + // go func() { + // defer wg.Done() + // if err := s.fetchBet365Odds(ctx); err != nil { + // errChan <- fmt.Errorf("bet365 odds fetching error: %w", err) + // } + // }() go func() { defer wg.Done() @@ -112,9 +113,7 @@ func (s *ServiceImpl) fetchBet365Odds(ctx context.Context) error { continue } - sportID, err := strconv.ParseInt(event.SportID, 10, 64) - - switch sportID { + switch event.SportID { case domain.FOOTBALL: if err := s.parseFootball(ctx, oddsData.Results[0]); err != nil { s.logger.Error("Error while inserting football odd") @@ -142,7 +141,7 @@ func (s *ServiceImpl) fetchBet365Odds(ctx context.Context) error { func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error { // getting odds for a specific event is not possible for bwin, most specific we can get is fetch odds on a single sport // so instead of having event and odds fetched separetly event will also be fetched along with the odds - sportIds := []int{12, 7} + sportIds := []int{4, 12, 7} for _, sportId := range sportIds { url := fmt.Sprintf("https://api.b365api.com/v1/bwin/prematch?sport_id=%d&token=%s", sportId, s.config.Bet365Token) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) @@ -181,13 +180,13 @@ func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error { event := domain.Event{ ID: strconv.Itoa(getInt(res["Id"])), - SportID: strconv.Itoa(getInt(res["SportId"])), - LeagueID: strconv.Itoa(getInt(res["LeagueId"])), + SportID: int32(getInt(res["SportId"])), + LeagueID: int32(getInt(res["LeagueId"])), LeagueName: getString(res["Leaguename"]), HomeTeam: getString(res["HomeTeam"]), - HomeTeamID: strconv.Itoa(getInt(res["HomeTeamId"])), + HomeTeamID: int32(getInt(res["HomeTeamId"])), AwayTeam: getString(res["AwayTeam"]), - AwayTeamID: strconv.Itoa(getInt(res["AwayTeamId"])), + AwayTeamID: int32(getInt(res["AwayTeamId"])), StartTime: time.Now().UTC().Format(time.RFC3339), TimerStatus: "1", IsLive: true, @@ -200,23 +199,25 @@ func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error { continue } - for _, m := range getMapArray(res["Markets"]) { - name := getMap(m["name"]) - marketName := getString(name["value"]) + for _, market := range []string{"Markets, optionMarkets"} { + for _, m := range getMapArray(res[market]) { + name := getMap(m["name"]) + marketName := getString(name["value"]) + + market := domain.Market{ + EventID: event.ID, + MarketID: getString(m["id"]), + MarketCategory: getString(m["category"]), + MarketName: marketName, + Source: "bwin", + } + + results := getMapArray(m["results"]) + market.Odds = results + + s.store.SaveNonLiveMarket(ctx, market) - market := domain.Market{ - EventID: event.ID, - MarketID: getString(m["id"]), - MarketCategory: getString(m["category"]), - MarketName: marketName, - Source: "bwin", } - - results := getMapArray(m["results"]) - market.Odds = results - - s.store.SaveNonLiveMarket(ctx, market) - } } diff --git a/internal/services/result/service.go b/internal/services/result/service.go index bc73535..b3f285d 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -80,14 +80,8 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { continue } - sportID, err := strconv.ParseInt(event.SportID, 10, 64) - if err != nil { - s.logger.Error("Sport ID is invalid", "event_id", outcome.EventID, "error", err) - isDeleted = false - continue - } // TODO: optimize this because the result is being fetched for each outcome which will have the same event id but different market id - result, err := s.fetchResult(ctx, outcome.EventID, outcome.OddID, outcome.MarketID, sportID, outcome) + result, err := s.fetchResult(ctx, outcome.EventID, outcome.OddID, outcome.MarketID, int64(event.SportID), outcome) if err != nil { if err == ErrEventIsNotActive { s.logger.Warn("Event is not active", "event_id", outcome.EventID, "error", err) diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index b5f87ec..6070740 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -71,18 +71,28 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) // role := c.Locals("role").(domain.Role) - leagueIDQuery := c.Query("league_id") - sportIDQuery := c.Query("sport_id") + leagueIDQuery, err := strconv.Atoi(c.Query("league_id")) + if err != nil { + h.logger.Error("invalid league id", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, nil) + } + + sportIDQuery, err := strconv.Atoi(c.Query("sport_id")) + if err != nil { + h.logger.Error("invalid sport id", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "invalid sport id", nil, nil) + } + firstStartTimeQuery := c.Query("first_start_time") lastStartTimeQuery := c.Query("last_start_time") - leagueID := domain.ValidString{ - Value: leagueIDQuery, - Valid: leagueIDQuery != "", + leagueID := domain.ValidInt32{ + Value: int32(leagueIDQuery), + Valid: leagueIDQuery != 0, } - sportID := domain.ValidString{ - Value: sportIDQuery, - Valid: sportIDQuery != "", + sportID := domain.ValidInt32{ + Value: int32(sportIDQuery), + Valid: sportIDQuery != 0, } var firstStartTime domain.ValidTime @@ -122,7 +132,6 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error { } var res domain.CreateBetRes - var err error for i := 0; i < int(req.NumberOfBets); i++ { res, err = h.betSvc.PlaceRandomBet(c.Context(), userID, req.BranchID, leagueID, sportID, firstStartTime, lastStartTime) diff --git a/internal/web_server/handlers/prematch.go b/internal/web_server/handlers/prematch.go index b8d3778..f070d5e 100644 --- a/internal/web_server/handlers/prematch.go +++ b/internal/web_server/handlers/prematch.go @@ -107,18 +107,27 @@ func (h *Handler) GetRawOddsByMarketID(c *fiber.Ctx) error { func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { page := c.QueryInt("page", 1) pageSize := c.QueryInt("page_size", 10) - leagueIDQuery := c.Query("league_id") - sportIDQuery := c.Query("sport_id") + leagueIDQuery, err := strconv.Atoi(c.Query("league_id")) + if err != nil { + h.logger.Error("invalid league id", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, nil) + } + + sportIDQuery, err := strconv.Atoi(c.Query("sport_id")) + if err != nil { + h.logger.Error("invalid sport id", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "invalid sport id", nil, nil) + } firstStartTimeQuery := c.Query("first_start_time") lastStartTimeQuery := c.Query("last_start_time") - leagueID := domain.ValidString{ - Value: leagueIDQuery, - Valid: leagueIDQuery != "", + leagueID := domain.ValidInt32{ + Value: int32(leagueIDQuery), + Valid: leagueIDQuery != 0, } - sportID := domain.ValidString{ - Value: sportIDQuery, - Valid: sportIDQuery != "", + sportID := domain.ValidInt32{ + Value: int32(sportIDQuery), + Valid: sportIDQuery != 0, } var firstStartTime domain.ValidTime From aedefbdb0b36afcb100db732cf84a66582fb9ff4 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Fri, 6 Jun 2025 03:36:15 +0300 Subject: [PATCH 4/5] league support check from db --- db/migrations/000001_fortune.down.sql | 3 +- db/migrations/000001_fortune.up.sql | 7 ++ db/query/leagues.sql | 37 ++++++++ gen/db/leagues.sql.go | 128 ++++++++++++++++++++++++++ gen/db/models.go | 8 ++ internal/domain/league.go | 42 ++------- internal/domain/resultres.go | 50 +++++----- internal/repository/league.go | 61 ++++++++++++ internal/services/event/service.go | 14 ++- 9 files changed, 283 insertions(+), 67 deletions(-) create mode 100644 db/query/leagues.sql create mode 100644 gen/db/leagues.sql.go create mode 100644 internal/repository/league.go diff --git a/db/migrations/000001_fortune.down.sql b/db/migrations/000001_fortune.down.sql index 82d488d..2724f06 100644 --- a/db/migrations/000001_fortune.down.sql +++ b/db/migrations/000001_fortune.down.sql @@ -75,4 +75,5 @@ DROP TABLE IF EXISTS supported_operations; DROP TABLE IF EXISTS refresh_tokens; DROP TABLE IF EXISTS otps; DROP TABLE IF EXISTS odds; -DROP TABLE IF EXISTS events; \ No newline at end of file +DROP TABLE IF EXISTS events; +DROP TABLE IF EXISTS leagues; \ No newline at end of file diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 50a4b75..25f23f3 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -232,6 +232,13 @@ CREATE TABLE companies ( admin_id BIGINT NOT NULL, wallet_id BIGINT NOT NULL ); +CREATE TABLE leagues ( + id BIGINT PRIMARY KEY, + name TEXT NOT NULL, + country_code TEXT, + bet365_id INT, + is_active BOOLEAN DEFAULT true +); -- Views CREATE VIEW companies_details AS SELECT companies.*, diff --git a/db/query/leagues.sql b/db/query/leagues.sql new file mode 100644 index 0000000..b4905c8 --- /dev/null +++ b/db/query/leagues.sql @@ -0,0 +1,37 @@ +-- name: InsertLeague :exec +INSERT INTO leagues ( + id, + name, + country_code, + bet365_id, + is_active +) VALUES ( + $1, $2, $3, $4, $5 +) +ON CONFLICT (id) DO UPDATE +SET name = EXCLUDED.name, + country_code = EXCLUDED.country_code, + bet365_id = EXCLUDED.bet365_id, + is_active = EXCLUDED.is_active; +-- name: GetSupportedLeagues :many +SELECT id, + name, + country_code, + bet365_id, + is_active +FROM leagues +WHERE is_active = true; +-- name: CheckLeagueSupport :one +SELECT EXISTS( + SELECT 1 + FROM leagues + WHERE id = $1 + AND is_active = true +); +-- name: UpdateLeague :exec +UPDATE leagues +SET name = $1, + country_code = $2, + bet365_id = $3, + is_active = $4 +WHERE id = $5; \ No newline at end of file diff --git a/gen/db/leagues.sql.go b/gen/db/leagues.sql.go new file mode 100644 index 0000000..e8589dd --- /dev/null +++ b/gen/db/leagues.sql.go @@ -0,0 +1,128 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: leagues.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const CheckLeagueSupport = `-- name: CheckLeagueSupport :one +SELECT EXISTS( + SELECT 1 + FROM leagues + WHERE id = $1 + AND is_active = true +) +` + +func (q *Queries) CheckLeagueSupport(ctx context.Context, id int64) (bool, error) { + row := q.db.QueryRow(ctx, CheckLeagueSupport, id) + var exists bool + err := row.Scan(&exists) + return exists, err +} + +const GetSupportedLeagues = `-- name: GetSupportedLeagues :many +SELECT id, + name, + country_code, + bet365_id, + is_active +FROM leagues +WHERE is_active = true +` + +func (q *Queries) GetSupportedLeagues(ctx context.Context) ([]League, error) { + rows, err := q.db.Query(ctx, GetSupportedLeagues) + if err != nil { + return nil, err + } + defer rows.Close() + var items []League + for rows.Next() { + var i League + if err := rows.Scan( + &i.ID, + &i.Name, + &i.CountryCode, + &i.Bet365ID, + &i.IsActive, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const InsertLeague = `-- name: InsertLeague :exec +INSERT INTO leagues ( + id, + name, + country_code, + bet365_id, + is_active +) VALUES ( + $1, $2, $3, $4, $5 +) +ON CONFLICT (id) DO UPDATE +SET name = EXCLUDED.name, + country_code = EXCLUDED.country_code, + bet365_id = EXCLUDED.bet365_id, + is_active = EXCLUDED.is_active +` + +type InsertLeagueParams struct { + ID int64 `json:"id"` + Name string `json:"name"` + CountryCode pgtype.Text `json:"country_code"` + Bet365ID pgtype.Int4 `json:"bet365_id"` + IsActive pgtype.Bool `json:"is_active"` +} + +func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) error { + _, err := q.db.Exec(ctx, InsertLeague, + arg.ID, + arg.Name, + arg.CountryCode, + arg.Bet365ID, + arg.IsActive, + ) + return err +} + +const UpdateLeague = `-- name: UpdateLeague :exec +UPDATE leagues +SET name = $1, + country_code = $2, + bet365_id = $3, + is_active = $4 +WHERE id = $5 +` + +type UpdateLeagueParams struct { + Name string `json:"name"` + CountryCode pgtype.Text `json:"country_code"` + Bet365ID pgtype.Int4 `json:"bet365_id"` + IsActive pgtype.Bool `json:"is_active"` + ID int64 `json:"id"` +} + +func (q *Queries) UpdateLeague(ctx context.Context, arg UpdateLeagueParams) error { + _, err := q.db.Exec(ctx, UpdateLeague, + arg.Name, + arg.CountryCode, + arg.Bet365ID, + arg.IsActive, + arg.ID, + ) + return err +} diff --git a/gen/db/models.go b/gen/db/models.go index 75c6af8..d45d0a3 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -199,6 +199,14 @@ type Event struct { Source pgtype.Text `json:"source"` } +type League struct { + ID int64 `json:"id"` + Name string `json:"name"` + CountryCode pgtype.Text `json:"country_code"` + Bet365ID pgtype.Int4 `json:"bet365_id"` + IsActive pgtype.Bool `json:"is_active"` +} + type Notification struct { ID string `json:"id"` RecipientID int64 `json:"recipient_id"` diff --git a/internal/domain/league.go b/internal/domain/league.go index a4a9cc2..f5ac35e 100644 --- a/internal/domain/league.go +++ b/internal/domain/league.go @@ -1,39 +1,9 @@ package domain -// TODO Will make this dynamic by moving into the database - -var SupportedLeagues = []int64{ - // Football - 10041282, //Premier League - 10083364, //La Liga - 10041095, //German Bundesliga - 10041100, //Ligue 1 - 10041809, //UEFA Champions League - 10041957, //UEFA Europa League - 10079560, //UEFA Conference League - 10047168, // US MLS - 10044469, // Ethiopian Premier League - 10050282, //UEFA Nations League - - 10043156, //England FA Cup - 10042103, //France Cup - 10041088, //Premier League 2 - 10084250, //Turkiye Super League - 10041187, //Kenya Super League - 10041315, //Italian Serie A - 10041391, //Netherlands Eredivisie - - // Basketball - 173998768, //NBA - 10041830, //NBA - 10049984, //WNBA - 10037165, //German Bundesliga - 10036608, //Italian Lega 1 - 10040795, //EuroLeague - - // Ice Hockey - 10037477, //NHL - 10037447, //AHL - 10069385, //IIHF World Championship - +type League struct { + ID int64 + Name string + CountryCode string + Bet365ID int32 + IsActive bool } diff --git a/internal/domain/resultres.go b/internal/domain/resultres.go index 493c0c9..3e53ce6 100644 --- a/internal/domain/resultres.go +++ b/internal/domain/resultres.go @@ -9,7 +9,7 @@ type BaseResultResponse struct { Results []json.RawMessage `json:"results"` } -type League struct { +type LeagueRes struct { ID string `json:"id"` Name string `json:"name"` CC string `json:"cc"` @@ -28,14 +28,14 @@ type Score struct { } type FootballResultResponse 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"` + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + TimeStatus string `json:"time_status"` + League LeagueRes `json:"league"` + Home Team `json:"home"` + Away Team `json:"away"` + SS string `json:"ss"` Scores struct { FirstHalf Score `json:"1"` SecondHalf Score `json:"2"` @@ -67,14 +67,14 @@ type FootballResultResponse struct { } type BasketballResultResponse 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"` + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + TimeStatus string `json:"time_status"` + League LeagueRes `json:"league"` + Home Team `json:"home"` + Away Team `json:"away"` + SS string `json:"ss"` Scores struct { FirstQuarter Score `json:"1"` SecondQuarter Score `json:"2"` @@ -114,14 +114,14 @@ type BasketballResultResponse struct { Bet365ID string `json:"bet365_id"` } type IceHockeyResultResponse 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"` + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + TimeStatus string `json:"time_status"` + League LeagueRes `json:"league"` + Home Team `json:"home"` + Away Team `json:"away"` + SS string `json:"ss"` Scores struct { FirstPeriod Score `json:"1"` SecondPeriod Score `json:"2"` diff --git a/internal/repository/league.go b/internal/repository/league.go new file mode 100644 index 0000000..1bbec9c --- /dev/null +++ b/internal/repository/league.go @@ -0,0 +1,61 @@ +package repository + +import ( + "context" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/jackc/pgx/v5/pgtype" +) + +func (s *Store) SaveLeague(ctx context.Context, l domain.League) error { + return s.queries.InsertLeague(ctx, dbgen.InsertLeagueParams{ + ID: l.ID, + Name: l.Name, + CountryCode: pgtype.Text{String: l.CountryCode, Valid: true}, + Bet365ID: pgtype.Int4{Int32: l.Bet365ID, Valid: true}, + IsActive: pgtype.Bool{Bool: l.IsActive, Valid: true}, + }) +} + +func (s *Store) GetSupportedLeagues(ctx context.Context) ([]domain.League, error) { + leagues, err := s.queries.GetSupportedLeagues(ctx) + if err != nil { + return nil, err + } + + supportedLeagues := make([]domain.League, len(leagues)) + for i, league := range leagues { + supportedLeagues[i] = domain.League{ + ID: league.ID, + Name: league.Name, + CountryCode: league.CountryCode.String, + Bet365ID: league.Bet365ID.Int32, + IsActive: league.IsActive.Bool, + } + } + return supportedLeagues, nil +} + +func (s *Store) CheckLeagueSupport(ctx context.Context, leagueID int64) (bool, error) { + return s.queries.CheckLeagueSupport(ctx, leagueID) +} + +// TODO: change to only take league id instad of the whole league +func (s *Store) SetLeagueActive(ctx context.Context, l domain.League) error { + return s.queries.UpdateLeague(ctx, dbgen.UpdateLeagueParams{ + Name: l.Name, + CountryCode: pgtype.Text{String: l.CountryCode, Valid: true}, + Bet365ID: pgtype.Int4{Int32: l.Bet365ID, Valid: true}, + IsActive: pgtype.Bool{Bool: true, Valid: true}, + }) +} + +func (s *Store) SetLeagueInActive(ctx context.Context, l domain.League) error { + return s.queries.UpdateLeague(ctx, dbgen.UpdateLeagueParams{ + Name: l.Name, + CountryCode: pgtype.Text{String: l.CountryCode, Valid: true}, + Bet365ID: pgtype.Int4{Int32: l.Bet365ID, Valid: true}, + IsActive: pgtype.Bool{Bool: false, Valid: true}, + }) +} diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 2c6bc52..2382091 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -7,7 +7,6 @@ import ( "io" "log" "net/http" - "slices" "strconv" "sync" "time" @@ -242,11 +241,16 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour continue } - if !slices.Contains(domain.SupportedLeagues, leagueID) { - // fmt.Printf("⚠️ Skipping league %s (%d) as it is not supported\n", ev.League.Name, leagueID) + // doesn't make sense to save and check back to back, but for now it can be here + s.store.SaveLeague(ctx, domain.League{ + ID: leagueID, + Name: ev.League.Name, + IsActive: true, + }) + + if supported, err := s.store.CheckLeagueSupport(ctx, leagueID); !supported || err != nil { skippedLeague = append(skippedLeague, ev.League.Name) - // ! for now - // continue + continue } event := domain.UpcomingEvent{ From 9807e8ed14e2da1a93dc9674a8c5c8c2ae78edf2 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Fri, 6 Jun 2025 15:19:42 +0300 Subject: [PATCH 5/5] handler for fetching leagues and update league status --- cmd/main.go | 4 ++- db/migrations/000001_fortune.up.sql | 7 ++++ db/query/leagues.sql | 13 ++++++- gen/db/leagues.sql.go | 46 ++++++++++++++++++++++++ internal/repository/league.go | 30 +++++++++++----- internal/services/league/port.go | 12 +++++++ internal/services/league/service.go | 26 ++++++++++++++ internal/web_server/app.go | 4 +++ internal/web_server/handlers/handlers.go | 4 +++ internal/web_server/handlers/leagues.go | 34 ++++++++++++++++++ internal/web_server/routes.go | 17 +++++---- makefile | 2 -- 12 files changed, 181 insertions(+), 18 deletions(-) create mode 100644 internal/services/league/port.go create mode 100644 internal/services/league/service.go create mode 100644 internal/web_server/handlers/leagues.go diff --git a/cmd/main.go b/cmd/main.go index cd98778..c271ec9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -23,6 +23,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation" @@ -86,6 +87,7 @@ func main() { transactionSvc := transaction.NewService(store) branchSvc := branch.NewService(store) companySvc := company.NewService(store) + leagueSvc := league.New(store) betSvc := bet.NewService(store, eventSvc, oddsSvc, *walletSvc, *branchSvc, logger) resultSvc := result.NewService(store, cfg, logger, *betSvc) notificationRepo := repository.NewNotificationRepository(store) @@ -128,7 +130,7 @@ func main() { JwtAccessKey: cfg.JwtKey, JwtAccessExpiry: cfg.AccessExpiry, }, userSvc, - ticketSvc, betSvc, chapaSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc, referalSvc, virtualGameSvc, aleaService, veliService, recommendationSvc, resultSvc, cfg) + ticketSvc, betSvc, chapaSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc, leagueSvc, referalSvc, virtualGameSvc, aleaService, veliService, recommendationSvc, resultSvc, cfg) logger.Info("Starting server", "port", cfg.Port) if err := app.Run(); err != nil { diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index b2cb0aa..30a006b 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -240,6 +240,13 @@ CREATE TABLE leagues ( bet365_id INT, is_active BOOLEAN DEFAULT true ); +CREATE TABLE teams ( + id TEXT PRIMARY KEY, + team_name TEXT NOT NULL, + country TEXT, + bet365_id INT, + logo_url TEXT +); -- Views CREATE VIEW companies_details AS SELECT companies.*, diff --git a/db/query/leagues.sql b/db/query/leagues.sql index b4905c8..b9c0e02 100644 --- a/db/query/leagues.sql +++ b/db/query/leagues.sql @@ -21,6 +21,13 @@ SELECT id, is_active FROM leagues WHERE is_active = true; +-- name: GetAllLeagues :many +SELECT id, + name, + country_code, + bet365_id, + is_active +FROM leagues; -- name: CheckLeagueSupport :one SELECT EXISTS( SELECT 1 @@ -34,4 +41,8 @@ SET name = $1, country_code = $2, bet365_id = $3, is_active = $4 -WHERE id = $5; \ No newline at end of file +WHERE id = $5; +-- name: SetLeagueActive :exec +UPDATE leagues +SET is_active = true +WHERE id = $1; \ No newline at end of file diff --git a/gen/db/leagues.sql.go b/gen/db/leagues.sql.go index e8589dd..49c1555 100644 --- a/gen/db/leagues.sql.go +++ b/gen/db/leagues.sql.go @@ -27,6 +27,41 @@ func (q *Queries) CheckLeagueSupport(ctx context.Context, id int64) (bool, error return exists, err } +const GetAllLeagues = `-- name: GetAllLeagues :many +SELECT id, + name, + country_code, + bet365_id, + is_active +FROM leagues +` + +func (q *Queries) GetAllLeagues(ctx context.Context) ([]League, error) { + rows, err := q.db.Query(ctx, GetAllLeagues) + if err != nil { + return nil, err + } + defer rows.Close() + var items []League + for rows.Next() { + var i League + if err := rows.Scan( + &i.ID, + &i.Name, + &i.CountryCode, + &i.Bet365ID, + &i.IsActive, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const GetSupportedLeagues = `-- name: GetSupportedLeagues :many SELECT id, name, @@ -99,6 +134,17 @@ func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) erro return err } +const SetLeagueActive = `-- name: SetLeagueActive :exec +UPDATE leagues +SET is_active = true +WHERE id = $1 +` + +func (q *Queries) SetLeagueActive(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, SetLeagueActive, id) + return err +} + const UpdateLeague = `-- name: UpdateLeague :exec UPDATE leagues SET name = $1, diff --git a/internal/repository/league.go b/internal/repository/league.go index 1bbec9c..7e5205f 100644 --- a/internal/repository/league.go +++ b/internal/repository/league.go @@ -37,20 +37,34 @@ func (s *Store) GetSupportedLeagues(ctx context.Context) ([]domain.League, error return supportedLeagues, nil } +func (s *Store) GetAllLeagues(ctx context.Context) ([]domain.League, error) { + l, err := s.queries.GetAllLeagues(ctx) + if err != nil { + return nil, err + } + + leagues := make([]domain.League, len(l)) + for i, league := range l { + leagues[i] = domain.League{ + ID: league.ID, + Name: league.Name, + CountryCode: league.CountryCode.String, + Bet365ID: league.Bet365ID.Int32, + IsActive: league.IsActive.Bool, + } + } + return leagues, nil +} + func (s *Store) CheckLeagueSupport(ctx context.Context, leagueID int64) (bool, error) { return s.queries.CheckLeagueSupport(ctx, leagueID) } -// TODO: change to only take league id instad of the whole league -func (s *Store) SetLeagueActive(ctx context.Context, l domain.League) error { - return s.queries.UpdateLeague(ctx, dbgen.UpdateLeagueParams{ - Name: l.Name, - CountryCode: pgtype.Text{String: l.CountryCode, Valid: true}, - Bet365ID: pgtype.Int4{Int32: l.Bet365ID, Valid: true}, - IsActive: pgtype.Bool{Bool: true, Valid: true}, - }) +func (s *Store) SetLeagueActive(ctx context.Context, leagueId int64) error { + return s.queries.SetLeagueActive(ctx, leagueId) } +// TODO: update based on id, no need for the entire league (same as the set active one) func (s *Store) SetLeagueInActive(ctx context.Context, l domain.League) error { return s.queries.UpdateLeague(ctx, dbgen.UpdateLeagueParams{ Name: l.Name, diff --git a/internal/services/league/port.go b/internal/services/league/port.go new file mode 100644 index 0000000..7b71a48 --- /dev/null +++ b/internal/services/league/port.go @@ -0,0 +1,12 @@ +package league + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type Service interface { + GetAllLeagues(ctx context.Context) ([]domain.League, error) + SetLeagueActive(ctx context.Context, leagueId int64) error +} diff --git a/internal/services/league/service.go b/internal/services/league/service.go new file mode 100644 index 0000000..b1f05ed --- /dev/null +++ b/internal/services/league/service.go @@ -0,0 +1,26 @@ +package league + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" +) + +type service struct { + store *repository.Store +} + +func New(store *repository.Store) Service { + return &service{ + store: store, + } +} + +func (s *service) GetAllLeagues(ctx context.Context) ([]domain.League, error) { + return s.store.GetAllLeagues(ctx) +} + +func (s *service) SetLeagueActive(ctx context.Context, leagueId int64) error { + return s.store.SetLeagueActive(ctx, leagueId) +} diff --git a/internal/web_server/app.go b/internal/web_server/app.go index d7c0b46..0a50ef7 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -11,6 +11,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation" referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" @@ -56,6 +57,7 @@ type App struct { Logger *slog.Logger prematchSvc *odds.ServiceImpl eventSvc event.Service + leagueSvc league.Service resultSvc *result.Service } @@ -75,6 +77,7 @@ func NewApp( notidicationStore *notificationservice.Service, prematchSvc *odds.ServiceImpl, eventSvc event.Service, + leagueSvc league.Service, referralSvc referralservice.ReferralStore, virtualGameSvc virtualgameservice.VirtualGameService, aleaVirtualGameService alea.AleaVirtualGameService, @@ -117,6 +120,7 @@ func NewApp( Logger: logger, prematchSvc: prematchSvc, eventSvc: eventSvc, + leagueSvc: leagueSvc, virtualGameSvc: virtualGameSvc, aleaVirtualGameService: aleaVirtualGameService, veliVirtualGameService: veliVirtualGameService, diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index 6c42024..f6665d7 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -10,6 +10,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation" @@ -39,6 +40,7 @@ type Handler struct { companySvc *company.Service prematchSvc *odds.ServiceImpl eventSvc event.Service + leagueSvc league.Service virtualGameSvc virtualgameservice.VirtualGameService aleaVirtualGameSvc alea.AleaVirtualGameService veliVirtualGameSvc veli.VeliVirtualGameService @@ -70,6 +72,7 @@ func New( companySvc *company.Service, prematchSvc *odds.ServiceImpl, eventSvc event.Service, + leagueSvc league.Service, cfg *config.Config, ) *Handler { return &Handler{ @@ -87,6 +90,7 @@ func New( companySvc: companySvc, prematchSvc: prematchSvc, eventSvc: eventSvc, + leagueSvc: leagueSvc, virtualGameSvc: virtualGameSvc, aleaVirtualGameSvc: aleaVirtualGameSvc, veliVirtualGameSvc: veliVirtualGameSvc, diff --git a/internal/web_server/handlers/leagues.go b/internal/web_server/handlers/leagues.go new file mode 100644 index 0000000..d4f78ee --- /dev/null +++ b/internal/web_server/handlers/leagues.go @@ -0,0 +1,34 @@ +package handlers + +import ( + "strconv" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" + "github.com/gofiber/fiber/v2" +) + +func (h *Handler) GetAllLeagues(c *fiber.Ctx) error { + leagues, err := h.leagueSvc.GetAllLeagues(c.Context()) + if err != nil { + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get leagues", err, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "All leagues retrived", leagues, nil) +} + +func (h *Handler) SetLeagueActive(c *fiber.Ctx) error { + leagueIdStr := c.Params("id") + if leagueIdStr == "" { + response.WriteJSON(c, fiber.StatusBadRequest, "Missing league id", nil, nil) + } + leagueId, err := strconv.Atoi(leagueIdStr) + if err != nil { + response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, nil) + } + + if err := h.leagueSvc.SetLeagueActive(c.Context(), int64(leagueId)); err != nil { + response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update league", err, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil) +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 88e8a2f..eaa2939 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -35,6 +35,7 @@ func (a *App) initAppRoutes() { a.companySvc, a.prematchSvc, a.eventSvc, + a.leagueSvc, a.cfg, ) @@ -114,13 +115,17 @@ func (a *App) initAppRoutes() { a.fiber.Put("/managers/:id", a.authMiddleware, h.UpdateManagers) a.fiber.Get("/manager/:id/branch", a.authMiddleware, h.GetBranchByManagerID) - a.fiber.Get("/prematch/odds/:event_id", h.GetPrematchOdds) - a.fiber.Get("/prematch/odds", h.GetALLPrematchOdds) - a.fiber.Get("/prematch/odds/upcoming/:upcoming_id/market/:market_id", h.GetRawOddsByMarketID) + a.fiber.Get("/events/odds/:event_id", h.GetPrematchOdds) + a.fiber.Get("/events/odds", h.GetALLPrematchOdds) + a.fiber.Get("/events/odds/upcoming/:upcoming_id/market/:market_id", h.GetRawOddsByMarketID) - a.fiber.Get("/prematch/events/:id", h.GetUpcomingEventByID) - a.fiber.Get("/prematch/events", h.GetAllUpcomingEvents) - a.fiber.Get("/prematch/odds/upcoming/:upcoming_id", h.GetPrematchOddsByUpcomingID) + a.fiber.Get("/events/:id", h.GetUpcomingEventByID) + a.fiber.Get("/events", h.GetAllUpcomingEvents) + a.fiber.Get("/events/odds/upcoming/:upcoming_id", h.GetPrematchOddsByUpcomingID) + + // Leagues + a.fiber.Get("/leagues", h.GetAllLeagues) + a.fiber.Get("/leagues/:id/set-active", h.SetLeagueActive) // Swagger a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler()) diff --git a/makefile b/makefile index 303a8cc..a40a255 100644 --- a/makefile +++ b/makefile @@ -56,8 +56,6 @@ db-up: db-down: @docker compose down @docker volume rm fortunebet-backend_postgres_data -postgres: - @docker exec -it fortunebet-backend-postgres-1 psql -U root -d gh .PHONY: sqlc-gen sqlc-gen: @sqlc generate \ No newline at end of file