Merge branch 'auth'
This commit is contained in:
commit
06c6325175
27
cmd/main.go
27
cmd/main.go
|
|
@ -1,10 +1,13 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
// "context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
|
||||
mockemail "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_email"
|
||||
|
|
@ -16,12 +19,13 @@ import (
|
|||
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||
httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server"
|
||||
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
// @title FortuneBet API
|
||||
|
|
@ -40,24 +44,30 @@ import (
|
|||
func main() {
|
||||
cfg, err := config.NewConfig()
|
||||
if err != nil {
|
||||
slog.Error(err.Error())
|
||||
slog.Error("❌ Config error:", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
db, _, err := repository.OpenDB(cfg.DbUrl)
|
||||
if err != nil {
|
||||
fmt.Print("db", err)
|
||||
fmt.Println("❌ Database error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel)
|
||||
store := repository.NewStore(db)
|
||||
v := customvalidator.NewCustomValidator(validator.New())
|
||||
|
||||
authSvc := authentication.NewService(store, store, cfg.RefreshExpiry)
|
||||
mockSms := mocksms.NewMockSMS()
|
||||
mockemail := mockemail.NewMockEmail()
|
||||
mockEmail := mockemail.NewMockEmail()
|
||||
|
||||
userSvc := user.NewService(store, store, mockSms, mockEmail)
|
||||
|
||||
eventSvc := event.New(cfg.Bet365Token, store)
|
||||
oddsSvc := odds.New(cfg.Bet365Token, store)
|
||||
|
||||
|
||||
userSvc := user.NewService(store, store, mockSms, mockemail)
|
||||
ticketSvc := ticket.NewService(store)
|
||||
betSvc := bet.NewService(store)
|
||||
walletSvc := wallet.NewService(store, store)
|
||||
|
|
@ -67,16 +77,17 @@ func main() {
|
|||
notificationRepo := repository.NewNotificationRepository(store)
|
||||
notificationSvc := notificationservice.New(notificationRepo, logger, cfg)
|
||||
|
||||
httpserver.StartDataFetchingCrons(eventSvc, oddsSvc)
|
||||
|
||||
app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{
|
||||
JwtAccessKey: cfg.JwtKey,
|
||||
JwtAccessExpiry: cfg.AccessExpiry,
|
||||
}, userSvc, ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, notificationSvc,
|
||||
)
|
||||
}, userSvc, ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, notificationSvc, oddsSvc)
|
||||
|
||||
logger.Info("Starting server", "port", cfg.Port)
|
||||
|
||||
if err := app.Run(); err != nil {
|
||||
logger.Error("Failed to start server", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,3 +88,8 @@ DROP TABLE IF EXISTS refresh_tokens;
|
|||
DROP TABLE IF EXISTS otps;
|
||||
|
||||
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS odds;
|
||||
DROP TABLE IF EXISTS events;
|
||||
|
||||
|
|
|
|||
|
|
@ -176,6 +176,54 @@ CREATE TABLE IF NOT EXISTS branch_cashiers (
|
|||
branch_id BIGINT NOT NULL,
|
||||
UNIQUE(user_id, branch_id)
|
||||
);
|
||||
|
||||
CREATE TABLE events (
|
||||
id TEXT PRIMARY KEY,
|
||||
sport_id TEXT,
|
||||
match_name TEXT,
|
||||
home_team TEXT,
|
||||
away_team TEXT,
|
||||
home_team_id TEXT,
|
||||
away_team_id TEXT,
|
||||
home_kit_image TEXT,
|
||||
away_kit_image TEXT,
|
||||
league_id TEXT,
|
||||
league_name TEXT,
|
||||
league_cc TEXT,
|
||||
start_time TIMESTAMP,
|
||||
score TEXT,
|
||||
match_minute INT,
|
||||
timer_status TEXT,
|
||||
added_time INT,
|
||||
match_period INT,
|
||||
is_live BOOLEAN,
|
||||
status TEXT,
|
||||
fetched_at TIMESTAMP DEFAULT now()
|
||||
);
|
||||
CREATE TABLE odds (
|
||||
id SERIAL PRIMARY KEY,
|
||||
event_id TEXT,
|
||||
fi TEXT,
|
||||
raw_event_id TEXT,
|
||||
market_type TEXT NOT NULL,
|
||||
market_name TEXT,
|
||||
market_category TEXT,
|
||||
market_id TEXT,
|
||||
header TEXT,
|
||||
name TEXT,
|
||||
handicap TEXT,
|
||||
odds_value DOUBLE PRECISION,
|
||||
section TEXT NOT NULL,
|
||||
category TEXT,
|
||||
raw_odds JSONB,
|
||||
fetched_at TIMESTAMP DEFAULT now(),
|
||||
source TEXT DEFAULT 'b365api',
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
UNIQUE (event_id, market_id, header, name, handicap)
|
||||
);
|
||||
|
||||
|
||||
|
||||
ALTER TABLE refresh_tokens
|
||||
ADD CONSTRAINT fk_refresh_tokens_users FOREIGN KEY (user_id) REFERENCES users(id);
|
||||
ALTER TABLE bets
|
||||
|
|
@ -295,3 +343,5 @@ VALUES (
|
|||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
--------------------------------------------------Bet365 Data Fetching + Event Managment------------------------------------------------
|
||||
|
|
|
|||
38
db/query/events.sql
Normal file
38
db/query/events.sql
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
-- name: InsertEvent :exec
|
||||
INSERT INTO events (
|
||||
id, sport_id, match_name, home_team, away_team,
|
||||
home_team_id, away_team_id, home_kit_image, away_kit_image,
|
||||
league_id, league_name, league_cc, start_time, score,
|
||||
match_minute, timer_status, added_time, match_period,
|
||||
is_live, status
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
$6, $7, $8, $9,
|
||||
$10, $11, $12, $13, $14,
|
||||
$15, $16, $17, $18,
|
||||
$19, $20
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
sport_id = EXCLUDED.sport_id,
|
||||
match_name = EXCLUDED.match_name,
|
||||
home_team = EXCLUDED.home_team,
|
||||
away_team = EXCLUDED.away_team,
|
||||
home_team_id = EXCLUDED.home_team_id,
|
||||
away_team_id = EXCLUDED.away_team_id,
|
||||
home_kit_image = EXCLUDED.home_kit_image,
|
||||
away_kit_image = EXCLUDED.away_kit_image,
|
||||
league_id = EXCLUDED.league_id,
|
||||
league_name = EXCLUDED.league_name,
|
||||
league_cc = EXCLUDED.league_cc,
|
||||
start_time = EXCLUDED.start_time,
|
||||
score = EXCLUDED.score,
|
||||
match_minute = EXCLUDED.match_minute,
|
||||
timer_status = EXCLUDED.timer_status,
|
||||
added_time = EXCLUDED.added_time,
|
||||
match_period = EXCLUDED.match_period,
|
||||
is_live = EXCLUDED.is_live,
|
||||
status = EXCLUDED.status,
|
||||
fetched_at = now();
|
||||
|
||||
-- name: ListLiveEvents :many
|
||||
SELECT id FROM events WHERE is_live = true;
|
||||
59
db/query/odds.sql
Normal file
59
db/query/odds.sql
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
-- name: InsertNonLiveOdd :exec
|
||||
INSERT INTO odds (
|
||||
event_id,
|
||||
fi,
|
||||
raw_event_id,
|
||||
market_type,
|
||||
market_name,
|
||||
market_category,
|
||||
market_id,
|
||||
header,
|
||||
name,
|
||||
handicap,
|
||||
odds_value,
|
||||
section,
|
||||
category,
|
||||
raw_odds,
|
||||
is_active,
|
||||
source,
|
||||
fetched_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7,
|
||||
$8, $9, $10, $11, $12, $13, $14,
|
||||
true, 'b365api', now()
|
||||
)
|
||||
ON CONFLICT (event_id, market_id, header, name, handicap) DO UPDATE SET
|
||||
odds_value = EXCLUDED.odds_value,
|
||||
raw_odds = EXCLUDED.raw_odds,
|
||||
market_type = EXCLUDED.market_type,
|
||||
market_name = EXCLUDED.market_name,
|
||||
market_category = EXCLUDED.market_category,
|
||||
fetched_at = now(),
|
||||
is_active = true,
|
||||
source = 'b365api',
|
||||
fi = EXCLUDED.fi,
|
||||
raw_event_id = EXCLUDED.raw_event_id;
|
||||
|
||||
|
||||
-- name: GetPrematchOdds :many
|
||||
SELECT
|
||||
id,
|
||||
event_id,
|
||||
fi,
|
||||
raw_event_id,
|
||||
market_type,
|
||||
market_name,
|
||||
market_category,
|
||||
market_id,
|
||||
header,
|
||||
name,
|
||||
handicap,
|
||||
odds_value,
|
||||
section,
|
||||
category,
|
||||
raw_odds,
|
||||
fetched_at,
|
||||
source,
|
||||
is_active
|
||||
FROM odds
|
||||
WHERE event_id = $1 AND is_active = true AND source = 'b365api';
|
||||
107
docs/docs.go
107
docs/docs.go
|
|
@ -1246,6 +1246,53 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/prematch/odds/{event_id}": {
|
||||
"get": {
|
||||
"description": "Retrieve prematch odds for a specific event by event ID",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"prematch"
|
||||
],
|
||||
"summary": "Retrieve prematch odds for an event",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Event ID",
|
||||
"name": "event_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/domain.Odd"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/search/branch": {
|
||||
"get": {
|
||||
"description": "Search branches by name or location",
|
||||
|
|
@ -2348,6 +2395,66 @@ const docTemplate = `{
|
|||
"BET_STATUS_ERROR"
|
||||
]
|
||||
},
|
||||
"domain.Odd": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"event_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"fetched_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"fi": {
|
||||
"type": "string"
|
||||
},
|
||||
"handicap": {
|
||||
"type": "string"
|
||||
},
|
||||
"header": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"is_active": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"market_category": {
|
||||
"type": "string"
|
||||
},
|
||||
"market_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"market_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"market_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"odds_value": {
|
||||
"type": "number"
|
||||
},
|
||||
"raw_event_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"raw_odds": {
|
||||
"type": "array",
|
||||
"items": {}
|
||||
},
|
||||
"section": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.PaymentOption": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
|
|
|
|||
|
|
@ -1238,6 +1238,53 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/prematch/odds/{event_id}": {
|
||||
"get": {
|
||||
"description": "Retrieve prematch odds for a specific event by event ID",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"prematch"
|
||||
],
|
||||
"summary": "Retrieve prematch odds for an event",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Event ID",
|
||||
"name": "event_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/domain.Odd"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/search/branch": {
|
||||
"get": {
|
||||
"description": "Search branches by name or location",
|
||||
|
|
@ -2340,6 +2387,66 @@
|
|||
"BET_STATUS_ERROR"
|
||||
]
|
||||
},
|
||||
"domain.Odd": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"event_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"fetched_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"fi": {
|
||||
"type": "string"
|
||||
},
|
||||
"handicap": {
|
||||
"type": "string"
|
||||
},
|
||||
"header": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"is_active": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"market_category": {
|
||||
"type": "string"
|
||||
},
|
||||
"market_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"market_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"market_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"odds_value": {
|
||||
"type": "number"
|
||||
},
|
||||
"raw_event_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"raw_odds": {
|
||||
"type": "array",
|
||||
"items": {}
|
||||
},
|
||||
"section": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.PaymentOption": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
|
|
|
|||
|
|
@ -22,6 +22,46 @@ definitions:
|
|||
- BET_STATUS_WIN
|
||||
- BET_STATUS_LOSS
|
||||
- BET_STATUS_ERROR
|
||||
domain.Odd:
|
||||
properties:
|
||||
category:
|
||||
type: string
|
||||
event_id:
|
||||
type: string
|
||||
fetched_at:
|
||||
type: string
|
||||
fi:
|
||||
type: string
|
||||
handicap:
|
||||
type: string
|
||||
header:
|
||||
type: string
|
||||
id:
|
||||
type: integer
|
||||
is_active:
|
||||
type: boolean
|
||||
market_category:
|
||||
type: string
|
||||
market_id:
|
||||
type: string
|
||||
market_name:
|
||||
type: string
|
||||
market_type:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
odds_value:
|
||||
type: number
|
||||
raw_event_id:
|
||||
type: string
|
||||
raw_odds:
|
||||
items: {}
|
||||
type: array
|
||||
section:
|
||||
type: string
|
||||
source:
|
||||
type: string
|
||||
type: object
|
||||
domain.PaymentOption:
|
||||
enum:
|
||||
- 0
|
||||
|
|
@ -1495,6 +1535,37 @@ paths:
|
|||
summary: Create a operation
|
||||
tags:
|
||||
- branch
|
||||
/prematch/odds/{event_id}:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Retrieve prematch odds for a specific event by event ID
|
||||
parameters:
|
||||
- description: Event ID
|
||||
in: path
|
||||
name: event_id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/domain.Odd'
|
||||
type: array
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
summary: Retrieve prematch odds for an event
|
||||
tags:
|
||||
- prematch
|
||||
/search/branch:
|
||||
get:
|
||||
consumes:
|
||||
|
|
|
|||
122
gen/db/events.sql.go
Normal file
122
gen/db/events.sql.go
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// source: events.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const InsertEvent = `-- name: InsertEvent :exec
|
||||
INSERT INTO events (
|
||||
id, sport_id, match_name, home_team, away_team,
|
||||
home_team_id, away_team_id, home_kit_image, away_kit_image,
|
||||
league_id, league_name, league_cc, start_time, score,
|
||||
match_minute, timer_status, added_time, match_period,
|
||||
is_live, status
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
$6, $7, $8, $9,
|
||||
$10, $11, $12, $13, $14,
|
||||
$15, $16, $17, $18,
|
||||
$19, $20
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
sport_id = EXCLUDED.sport_id,
|
||||
match_name = EXCLUDED.match_name,
|
||||
home_team = EXCLUDED.home_team,
|
||||
away_team = EXCLUDED.away_team,
|
||||
home_team_id = EXCLUDED.home_team_id,
|
||||
away_team_id = EXCLUDED.away_team_id,
|
||||
home_kit_image = EXCLUDED.home_kit_image,
|
||||
away_kit_image = EXCLUDED.away_kit_image,
|
||||
league_id = EXCLUDED.league_id,
|
||||
league_name = EXCLUDED.league_name,
|
||||
league_cc = EXCLUDED.league_cc,
|
||||
start_time = EXCLUDED.start_time,
|
||||
score = EXCLUDED.score,
|
||||
match_minute = EXCLUDED.match_minute,
|
||||
timer_status = EXCLUDED.timer_status,
|
||||
added_time = EXCLUDED.added_time,
|
||||
match_period = EXCLUDED.match_period,
|
||||
is_live = EXCLUDED.is_live,
|
||||
status = EXCLUDED.status,
|
||||
fetched_at = now()
|
||||
`
|
||||
|
||||
type InsertEventParams struct {
|
||||
ID string
|
||||
SportID pgtype.Text
|
||||
MatchName pgtype.Text
|
||||
HomeTeam pgtype.Text
|
||||
AwayTeam pgtype.Text
|
||||
HomeTeamID pgtype.Text
|
||||
AwayTeamID pgtype.Text
|
||||
HomeKitImage pgtype.Text
|
||||
AwayKitImage pgtype.Text
|
||||
LeagueID pgtype.Text
|
||||
LeagueName pgtype.Text
|
||||
LeagueCc pgtype.Text
|
||||
StartTime pgtype.Timestamp
|
||||
Score pgtype.Text
|
||||
MatchMinute pgtype.Int4
|
||||
TimerStatus pgtype.Text
|
||||
AddedTime pgtype.Int4
|
||||
MatchPeriod pgtype.Int4
|
||||
IsLive pgtype.Bool
|
||||
Status pgtype.Text
|
||||
}
|
||||
|
||||
func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error {
|
||||
_, err := q.db.Exec(ctx, InsertEvent,
|
||||
arg.ID,
|
||||
arg.SportID,
|
||||
arg.MatchName,
|
||||
arg.HomeTeam,
|
||||
arg.AwayTeam,
|
||||
arg.HomeTeamID,
|
||||
arg.AwayTeamID,
|
||||
arg.HomeKitImage,
|
||||
arg.AwayKitImage,
|
||||
arg.LeagueID,
|
||||
arg.LeagueName,
|
||||
arg.LeagueCc,
|
||||
arg.StartTime,
|
||||
arg.Score,
|
||||
arg.MatchMinute,
|
||||
arg.TimerStatus,
|
||||
arg.AddedTime,
|
||||
arg.MatchPeriod,
|
||||
arg.IsLive,
|
||||
arg.Status,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const ListLiveEvents = `-- name: ListLiveEvents :many
|
||||
SELECT id FROM events WHERE is_live = true
|
||||
`
|
||||
|
||||
func (q *Queries) ListLiveEvents(ctx context.Context) ([]string, error) {
|
||||
rows, err := q.db.Query(ctx, ListLiveEvents)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []string
|
||||
for rows.Next() {
|
||||
var id string
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, id)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
|
@ -98,6 +98,30 @@ type CustomerWallet struct {
|
|||
UpdatedAt pgtype.Timestamp
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
ID string
|
||||
SportID pgtype.Text
|
||||
MatchName pgtype.Text
|
||||
HomeTeam pgtype.Text
|
||||
AwayTeam pgtype.Text
|
||||
HomeTeamID pgtype.Text
|
||||
AwayTeamID pgtype.Text
|
||||
HomeKitImage pgtype.Text
|
||||
AwayKitImage pgtype.Text
|
||||
LeagueID pgtype.Text
|
||||
LeagueName pgtype.Text
|
||||
LeagueCc pgtype.Text
|
||||
StartTime pgtype.Timestamp
|
||||
Score pgtype.Text
|
||||
MatchMinute pgtype.Int4
|
||||
TimerStatus pgtype.Text
|
||||
AddedTime pgtype.Int4
|
||||
MatchPeriod pgtype.Int4
|
||||
IsLive pgtype.Bool
|
||||
Status pgtype.Text
|
||||
FetchedAt pgtype.Timestamp
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
ID string
|
||||
RecipientID int64
|
||||
|
|
@ -115,6 +139,27 @@ type Notification struct {
|
|||
Metadata []byte
|
||||
}
|
||||
|
||||
type Odd struct {
|
||||
ID int32
|
||||
EventID pgtype.Text
|
||||
Fi pgtype.Text
|
||||
RawEventID pgtype.Text
|
||||
MarketType string
|
||||
MarketName pgtype.Text
|
||||
MarketCategory pgtype.Text
|
||||
MarketID pgtype.Text
|
||||
Header pgtype.Text
|
||||
Name pgtype.Text
|
||||
Handicap pgtype.Text
|
||||
OddsValue pgtype.Float8
|
||||
Section string
|
||||
Category pgtype.Text
|
||||
RawOdds []byte
|
||||
FetchedAt pgtype.Timestamp
|
||||
Source pgtype.Text
|
||||
IsActive pgtype.Bool
|
||||
}
|
||||
|
||||
type Otp struct {
|
||||
ID int64
|
||||
SentTo string
|
||||
|
|
|
|||
149
gen/db/odds.sql.go
Normal file
149
gen/db/odds.sql.go
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// source: odds.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const GetPrematchOdds = `-- name: GetPrematchOdds :many
|
||||
SELECT
|
||||
id,
|
||||
event_id,
|
||||
fi,
|
||||
raw_event_id,
|
||||
market_type,
|
||||
market_name,
|
||||
market_category,
|
||||
market_id,
|
||||
header,
|
||||
name,
|
||||
handicap,
|
||||
odds_value,
|
||||
section,
|
||||
category,
|
||||
raw_odds,
|
||||
fetched_at,
|
||||
source,
|
||||
is_active
|
||||
FROM odds
|
||||
WHERE event_id = $1 AND is_active = true AND source = 'b365api'
|
||||
`
|
||||
|
||||
func (q *Queries) GetPrematchOdds(ctx context.Context, eventID pgtype.Text) ([]Odd, error) {
|
||||
rows, err := q.db.Query(ctx, GetPrematchOdds, eventID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Odd
|
||||
for rows.Next() {
|
||||
var i Odd
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.EventID,
|
||||
&i.Fi,
|
||||
&i.RawEventID,
|
||||
&i.MarketType,
|
||||
&i.MarketName,
|
||||
&i.MarketCategory,
|
||||
&i.MarketID,
|
||||
&i.Header,
|
||||
&i.Name,
|
||||
&i.Handicap,
|
||||
&i.OddsValue,
|
||||
&i.Section,
|
||||
&i.Category,
|
||||
&i.RawOdds,
|
||||
&i.FetchedAt,
|
||||
&i.Source,
|
||||
&i.IsActive,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const InsertNonLiveOdd = `-- name: InsertNonLiveOdd :exec
|
||||
INSERT INTO odds (
|
||||
event_id,
|
||||
fi,
|
||||
raw_event_id,
|
||||
market_type,
|
||||
market_name,
|
||||
market_category,
|
||||
market_id,
|
||||
header,
|
||||
name,
|
||||
handicap,
|
||||
odds_value,
|
||||
section,
|
||||
category,
|
||||
raw_odds,
|
||||
is_active,
|
||||
source,
|
||||
fetched_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7,
|
||||
$8, $9, $10, $11, $12, $13, $14,
|
||||
true, 'b365api', now()
|
||||
)
|
||||
ON CONFLICT (event_id, market_id, header, name, handicap) DO UPDATE SET
|
||||
odds_value = EXCLUDED.odds_value,
|
||||
raw_odds = EXCLUDED.raw_odds,
|
||||
market_type = EXCLUDED.market_type,
|
||||
market_name = EXCLUDED.market_name,
|
||||
market_category = EXCLUDED.market_category,
|
||||
fetched_at = now(),
|
||||
is_active = true,
|
||||
source = 'b365api',
|
||||
fi = EXCLUDED.fi,
|
||||
raw_event_id = EXCLUDED.raw_event_id
|
||||
`
|
||||
|
||||
type InsertNonLiveOddParams struct {
|
||||
EventID pgtype.Text
|
||||
Fi pgtype.Text
|
||||
RawEventID pgtype.Text
|
||||
MarketType string
|
||||
MarketName pgtype.Text
|
||||
MarketCategory pgtype.Text
|
||||
MarketID pgtype.Text
|
||||
Header pgtype.Text
|
||||
Name pgtype.Text
|
||||
Handicap pgtype.Text
|
||||
OddsValue pgtype.Float8
|
||||
Section string
|
||||
Category pgtype.Text
|
||||
RawOdds []byte
|
||||
}
|
||||
|
||||
func (q *Queries) InsertNonLiveOdd(ctx context.Context, arg InsertNonLiveOddParams) error {
|
||||
_, err := q.db.Exec(ctx, InsertNonLiveOdd,
|
||||
arg.EventID,
|
||||
arg.Fi,
|
||||
arg.RawEventID,
|
||||
arg.MarketType,
|
||||
arg.MarketName,
|
||||
arg.MarketCategory,
|
||||
arg.MarketID,
|
||||
arg.Header,
|
||||
arg.Name,
|
||||
arg.Handicap,
|
||||
arg.OddsValue,
|
||||
arg.Section,
|
||||
arg.Category,
|
||||
arg.RawOdds,
|
||||
)
|
||||
return err
|
||||
}
|
||||
1
go.mod
1
go.mod
|
|
@ -11,6 +11,7 @@ require (
|
|||
github.com/google/uuid v1.6.0
|
||||
github.com/jackc/pgx/v5 v5.7.4
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/swaggo/fiber-swagger v1.3.0
|
||||
github.com/swaggo/swag v1.16.4
|
||||
golang.org/x/crypto v0.36.0
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -109,6 +109,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
|||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ var (
|
|||
ErrInvalidLevel = errors.New("invalid log level")
|
||||
ErrInvalidEnv = errors.New("env not set or invalid")
|
||||
ErrInvalidSMSAPIKey = errors.New("SMS API key is invalid")
|
||||
ErrMissingBetToken = errors.New("missing BET365_TOKEN in .env")
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
|
|
@ -34,6 +35,7 @@ type Config struct {
|
|||
AFRO_SMS_SENDER_NAME string
|
||||
AFRO_SMS_RECEIVER_PHONE_NUMBER string
|
||||
ADRO_SMS_HOST_URL string
|
||||
Bet365Token string
|
||||
}
|
||||
|
||||
func NewConfig() (*Config, error) {
|
||||
|
|
@ -126,5 +128,10 @@ func (c *Config) loadEnv() error {
|
|||
c.ADRO_SMS_HOST_URL = "https://api.afrosms.com"
|
||||
}
|
||||
|
||||
betToken := os.Getenv("BET365_TOKEN")
|
||||
if betToken == "" {
|
||||
return ErrMissingBetToken
|
||||
}
|
||||
c.Bet365Token = betToken
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,23 @@
|
|||
package domain
|
||||
|
||||
type Event struct {}
|
||||
|
||||
type Outcome struct {
|
||||
|
||||
type Event struct {
|
||||
ID string
|
||||
SportID string
|
||||
MatchName string
|
||||
HomeTeam string
|
||||
AwayTeam string
|
||||
HomeTeamID string
|
||||
AwayTeamID string
|
||||
HomeKitImage string
|
||||
AwayKitImage string
|
||||
LeagueID string
|
||||
LeagueName string
|
||||
LeagueCC string
|
||||
StartTime string
|
||||
Score string
|
||||
MatchMinute int
|
||||
TimerStatus string
|
||||
AddedTime int
|
||||
MatchPeriod int
|
||||
IsLive bool
|
||||
Status string
|
||||
}
|
||||
|
||||
|
|
|
|||
45
internal/domain/odds.go
Normal file
45
internal/domain/odds.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
)
|
||||
type RawMessage interface{} // Change from json.RawMessage to interface{}
|
||||
|
||||
type Market struct {
|
||||
EventID string
|
||||
FI string
|
||||
MarketCategory string
|
||||
MarketType string
|
||||
MarketName string
|
||||
MarketID string
|
||||
UpdatedAt time.Time
|
||||
Odds []json.RawMessage
|
||||
|
||||
Header string
|
||||
Name string
|
||||
Handicap string
|
||||
OddsVal float64
|
||||
}
|
||||
|
||||
type Odd struct {
|
||||
ID int64 `json:"id"`
|
||||
EventID string `json:"event_id"`
|
||||
Fi string `json:"fi"`
|
||||
RawEventID string `json:"raw_event_id"`
|
||||
MarketType string `json:"market_type"`
|
||||
MarketName string `json:"market_name"`
|
||||
MarketCategory string `json:"market_category"`
|
||||
MarketID string `json:"market_id"`
|
||||
Header string `json:"header"`
|
||||
Name string `json:"name"`
|
||||
Handicap string `json:"handicap"`
|
||||
OddsValue float64 `json:"odds_value"`
|
||||
Section string `json:"section"`
|
||||
Category string `json:"category"`
|
||||
RawOdds []RawMessage `json:"raw_odds"`
|
||||
FetchedAt time.Time `json:"fetched_at"`
|
||||
Source string `json:"source"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
44
internal/repository/event.go
Normal file
44
internal/repository/event.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func (s *Store) SaveEvent(ctx context.Context, e domain.Event) error {
|
||||
parsedTime, err := time.Parse(time.RFC3339, e.StartTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.queries.InsertEvent(ctx, dbgen.InsertEventParams{
|
||||
ID: e.ID,
|
||||
SportID: pgtype.Text{String: 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},
|
||||
HomeKitImage: pgtype.Text{String: e.HomeKitImage, Valid: true},
|
||||
AwayKitImage: pgtype.Text{String: e.AwayKitImage, Valid: true},
|
||||
LeagueID: pgtype.Text{String: 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},
|
||||
Score: pgtype.Text{String: e.Score, Valid: true},
|
||||
MatchMinute: pgtype.Int4{Int32: int32(e.MatchMinute), Valid: true},
|
||||
TimerStatus: pgtype.Text{String: e.TimerStatus, Valid: true},
|
||||
AddedTime: pgtype.Int4{Int32: int32(e.AddedTime), Valid: true},
|
||||
MatchPeriod: pgtype.Int4{Int32: int32(e.MatchPeriod), Valid: true},
|
||||
IsLive: pgtype.Bool{Bool: e.IsLive, Valid: true},
|
||||
Status: pgtype.Text{String: e.Status, Valid: true},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) GetLiveEventIDs(ctx context.Context) ([]string, error) {
|
||||
return s.queries.ListLiveEvents(ctx)
|
||||
}
|
||||
151
internal/repository/odds.go
Normal file
151
internal/repository/odds.go
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error {
|
||||
if len(m.Odds) == 0 {
|
||||
fmt.Printf(" Market has no odds: %s (%s)\n", m.MarketType, m.EventID)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, raw := range m.Odds {
|
||||
var item map[string]interface{}
|
||||
if err := json.Unmarshal(raw, &item); err != nil {
|
||||
fmt.Printf(" Invalid odd JSON for %s (%s): %v\n", m.MarketType, m.EventID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
header := getString(item["header"])
|
||||
name := getString(item["name"])
|
||||
handicap := getString(item["handicap"])
|
||||
oddsVal := getFloat(item["odds"])
|
||||
|
||||
rawOddsBytes, _ := json.Marshal(m.Odds)
|
||||
|
||||
params := dbgen.InsertNonLiveOddParams{
|
||||
EventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""},
|
||||
Fi: pgtype.Text{String: m.FI, Valid: m.FI != ""},
|
||||
RawEventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""},
|
||||
MarketType: m.MarketType,
|
||||
MarketName: pgtype.Text{String: m.MarketName, Valid: m.MarketName != ""},
|
||||
MarketCategory: pgtype.Text{String: m.MarketCategory, Valid: m.MarketCategory != ""},
|
||||
MarketID: pgtype.Text{String: m.MarketID, Valid: m.MarketID != ""},
|
||||
Header: pgtype.Text{String: header, Valid: header != ""},
|
||||
Name: pgtype.Text{String: name, Valid: name != ""},
|
||||
Handicap: pgtype.Text{String: handicap, Valid: handicap != ""},
|
||||
OddsValue: pgtype.Float8{Float64: oddsVal, Valid: oddsVal != 0},
|
||||
Section: m.MarketCategory,
|
||||
Category: pgtype.Text{Valid: false},
|
||||
RawOdds: rawOddsBytes,
|
||||
}
|
||||
|
||||
err := s.queries.InsertNonLiveOdd(ctx, params)
|
||||
if err != nil {
|
||||
fmt.Printf(" Failed to insert odd for market %s (%s): %v\n", m.MarketType, m.EventID, err)
|
||||
_ = writeFailedMarketLog(m, err)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("Inserted odd: %s | type=%s | header=%s | name=%s\n", m.EventID, m.MarketType, header, name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func writeFailedMarketLog(m domain.Market, err error) error {
|
||||
logDir := "logs"
|
||||
logFile := logDir + "/failed_markets.log"
|
||||
|
||||
if mkErr := os.MkdirAll(logDir, 0755); mkErr != nil {
|
||||
return mkErr
|
||||
}
|
||||
|
||||
f, fileErr := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if fileErr != nil {
|
||||
return fileErr
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
entry := struct {
|
||||
Time string `json:"time"`
|
||||
Error string `json:"error"`
|
||||
Record domain.Market `json:"record"`
|
||||
}{
|
||||
Time: time.Now().Format(time.RFC3339),
|
||||
Error: err.Error(),
|
||||
Record: m,
|
||||
}
|
||||
|
||||
jsonData, _ := json.MarshalIndent(entry, "", " ")
|
||||
_, writeErr := f.WriteString(string(jsonData) + "\n\n")
|
||||
return writeErr
|
||||
}
|
||||
|
||||
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) {
|
||||
eventIDParam := pgtype.Text{String: eventID, Valid: eventID != ""}
|
||||
|
||||
odds, err := s.queries.GetPrematchOdds(ctx, eventIDParam)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
domainOdds := make([]domain.Odd, len(odds))
|
||||
for i, odd := range odds {
|
||||
domainOdds[i] = domain.Odd{
|
||||
ID: int64(odd.ID), // Cast int32 to int64
|
||||
EventID: odd.EventID.String, // Extract the String value
|
||||
Fi: odd.Fi.String, // Extract the String value
|
||||
RawEventID: odd.RawEventID.String, // Extract the String value
|
||||
MarketType: odd.MarketType, // Direct assignment
|
||||
MarketName: odd.MarketName.String, // Extract the String value
|
||||
MarketCategory: odd.MarketCategory.String, // Extract the String value
|
||||
MarketID: odd.MarketID.String, // Extract the String value
|
||||
Header: odd.Header.String, // Extract the String value
|
||||
Name: odd.Name.String, // Extract the String value
|
||||
Handicap: odd.Handicap.String, // Extract the String value
|
||||
OddsValue: odd.OddsValue.Float64, // Extract the Float64 value
|
||||
Section: odd.Section, // Direct assignment
|
||||
Category: odd.Category.String, // Extract the String value
|
||||
RawOdds: func() []domain.RawMessage {
|
||||
var rawOdds []domain.RawMessage
|
||||
if err := json.Unmarshal(odd.RawOdds, &rawOdds); err != nil {
|
||||
rawOdds = nil
|
||||
}
|
||||
return rawOdds
|
||||
}(),
|
||||
FetchedAt: odd.FetchedAt.Time, // Extract the Time value
|
||||
Source: odd.Source.String, // Extract the String value
|
||||
IsActive: odd.IsActive.Bool, // Extract the Bool value
|
||||
}
|
||||
}
|
||||
|
||||
return domainOdds, nil
|
||||
}
|
||||
8
internal/services/event/port.go
Normal file
8
internal/services/event/port.go
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package event
|
||||
|
||||
import "context"
|
||||
|
||||
type Service interface {
|
||||
FetchLiveEvents(ctx context.Context) error
|
||||
FetchUpcomingEvents(ctx context.Context) error
|
||||
}
|
||||
173
internal/services/event/service.go
Normal file
173
internal/services/event/service.go
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
package event
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
token string
|
||||
store *repository.Store
|
||||
}
|
||||
|
||||
func New(token string, store *repository.Store) Service {
|
||||
return &service{
|
||||
token: token,
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) FetchLiveEvents(ctx context.Context) error {
|
||||
sportIDs := []int{1, 13, 78, 18, 91, 16, 17, 14, 12, 3, 2, 4, 83, 15, 92, 94, 8, 19, 36, 66, 9, 75, 90, 95, 110, 107, 151, 162, 148}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, sportID := range sportIDs {
|
||||
wg.Add(1)
|
||||
go func(sportID int) {
|
||||
defer wg.Done()
|
||||
|
||||
url := fmt.Sprintf("https://api.b365api.com/v1/bet365/inplay?sport_id=%d&token=%s", sportID, s.token)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
fmt.Printf(" Failed request for sport_id=%d: %v\n", sportID, err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
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))
|
||||
return
|
||||
}
|
||||
|
||||
for _, group := range data.Results {
|
||||
for _, ev := range group {
|
||||
if getString(ev["type"]) != "EV" {
|
||||
continue
|
||||
}
|
||||
|
||||
event := domain.Event{
|
||||
ID: getString(ev["ID"]),
|
||||
SportID: fmt.Sprintf("%d", 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"]),
|
||||
HomeKitImage: getString(ev["K1"]),
|
||||
AwayKitImage: getString(ev["K2"]),
|
||||
LeagueName: getString(ev["CT"]),
|
||||
LeagueID: getString(ev["C2"]),
|
||||
LeagueCC: getString(ev["CB"]),
|
||||
StartTime: time.Now().UTC().Format(time.RFC3339),
|
||||
IsLive: true,
|
||||
Status: "live",
|
||||
MatchPeriod: getInt(ev["MD"]),
|
||||
AddedTime: getInt(ev["TA"]),
|
||||
}
|
||||
|
||||
if err := s.store.SaveEvent(ctx, event); err != nil {
|
||||
fmt.Printf("Could not store live event [id=%s]: %v\n", event.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}(sportID)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
fmt.Println("All live events fetched and stored.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) FetchUpcomingEvents(ctx context.Context) error {
|
||||
sportIDs := []int{1, 13, 78, 18, 91, 16, 17, 14, 12, 3, 2, 4, 83, 15, 92, 94, 8, 19, 36, 66, 9, 75, 90, 95, 110, 107, 151, 162, 148}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, sportID := range sportIDs {
|
||||
wg.Add(1)
|
||||
go func(sportID int) {
|
||||
defer wg.Done()
|
||||
|
||||
url := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s", sportID, s.token)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
fmt.Printf(" Failed request for upcoming sport_id=%d: %v\n", sportID, err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
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 upcoming sport_id=%d\nRaw: %s\n", sportID, string(body))
|
||||
return
|
||||
}
|
||||
|
||||
for _, group := range data.Results {
|
||||
for _, ev := range group {
|
||||
if getString(ev["type"]) != "EV" {
|
||||
continue
|
||||
}
|
||||
|
||||
event := domain.Event{
|
||||
ID: getString(ev["ID"]),
|
||||
SportID: fmt.Sprintf("%d", sportID),
|
||||
MatchName: getString(ev["NA"]),
|
||||
HomeTeamID: getString(ev["HT"]),
|
||||
AwayTeamID: getString(ev["AT"]),
|
||||
HomeKitImage: getString(ev["K1"]),
|
||||
AwayKitImage: getString(ev["K2"]),
|
||||
LeagueID: getString(ev["C2"]),
|
||||
LeagueName: getString(ev["CT"]),
|
||||
LeagueCC: getString(ev["CB"]),
|
||||
StartTime: time.Now().UTC().Format(time.RFC3339),
|
||||
IsLive: false,
|
||||
Status: "upcoming",
|
||||
}
|
||||
|
||||
if err := s.store.SaveEvent(ctx, event); err != nil {
|
||||
fmt.Printf(" Could not store upcoming event [id=%s]: %v\n", event.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}(sportID)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
fmt.Println(" All upcoming events fetched and stored.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func getString(v interface{}) string {
|
||||
if str, ok := v.(string); ok {
|
||||
return str
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getInt(v interface{}) int {
|
||||
if f, ok := v.(float64); ok {
|
||||
return int(f)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
14
internal/services/odds/port.go
Normal file
14
internal/services/odds/port.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package odds
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
FetchNonLiveOdds(ctx context.Context) error
|
||||
GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error)
|
||||
|
||||
|
||||
}
|
||||
165
internal/services/odds/service.go
Normal file
165
internal/services/odds/service.go
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
package odds
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
||||
)
|
||||
|
||||
type ServiceImpl struct {
|
||||
token string
|
||||
store *repository.Store
|
||||
}
|
||||
|
||||
func New(token string, store *repository.Store) *ServiceImpl {
|
||||
return &ServiceImpl{token: token, store: store}
|
||||
}
|
||||
|
||||
func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
|
||||
fmt.Println("Starting FetchNonLiveOdds...")
|
||||
|
||||
sportID := 1
|
||||
upcomingURL := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s", sportID, s.token)
|
||||
resp, err := http.Get(upcomingURL)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to fetch upcoming: %v\n", err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var upcomingData struct {
|
||||
Success int `json:"success"`
|
||||
Results []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"results"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &upcomingData); err != nil || upcomingData.Success != 1 {
|
||||
fmt.Printf("Failed to decode upcoming response\nRaw: %s\n", string(body))
|
||||
return err
|
||||
}
|
||||
|
||||
for _, ev := range upcomingData.Results {
|
||||
eventID := ev.ID
|
||||
fmt.Printf("Fetching prematch odds for event_id=%s\n", eventID)
|
||||
prematchURL := fmt.Sprintf("https://api.b365api.com/v3/bet365/prematch?token=%s&FI=%s", s.token, eventID)
|
||||
oddsResp, err := http.Get(prematchURL)
|
||||
if err != nil {
|
||||
fmt.Printf(" Odds fetch failed for event_id=%s: %v\n", eventID, err)
|
||||
continue
|
||||
}
|
||||
defer oddsResp.Body.Close()
|
||||
|
||||
oddsBody, _ := io.ReadAll(oddsResp.Body)
|
||||
fmt.Printf(" Raw odds response for event_id=%s: %.300s...\n", eventID, string(oddsBody))
|
||||
|
||||
var oddsData struct {
|
||||
Success int `json:"success"`
|
||||
Results []struct {
|
||||
EventID string `json:"event_id"`
|
||||
FI string `json:"FI"`
|
||||
Main OddsSection `json:"main"`
|
||||
} `json:"results"`
|
||||
}
|
||||
if err := json.Unmarshal(oddsBody, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 {
|
||||
fmt.Printf(" Failed odds decode for event_id=%s\nRaw: %s\n", eventID, string(oddsBody))
|
||||
continue
|
||||
}
|
||||
|
||||
result := oddsData.Results[0]
|
||||
finalID := result.EventID
|
||||
if finalID == "" {
|
||||
finalID = result.FI
|
||||
}
|
||||
if finalID == "" {
|
||||
fmt.Println(" Skipping event with missing final ID.")
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("🗂 Saving prematch odds for event_id=%s\n", finalID)
|
||||
s.storeSection(ctx, finalID, result.FI, "main", result.Main)
|
||||
fmt.Printf(" Finished storing prematch odds for event_id=%s\n", finalID)
|
||||
}
|
||||
|
||||
fmt.Println(" All prematch odds fetched and stored.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName string, section OddsSection) {
|
||||
fmt.Printf(" Processing section '%s' for event_id=%s\n", sectionName, eventID)
|
||||
if len(section.Sp) == 0 {
|
||||
fmt.Printf(" No odds in section '%s' for event_id=%s\n", sectionName, eventID)
|
||||
return
|
||||
}
|
||||
|
||||
updatedAtUnix, _ := strconv.ParseInt(section.UpdatedAt, 10, 64)
|
||||
updatedAt := time.Unix(updatedAtUnix, 0)
|
||||
|
||||
for marketType, market := range section.Sp {
|
||||
fmt.Printf(" Processing market: %s (%s)\n", marketType, market.ID)
|
||||
if len(market.Odds) == 0 {
|
||||
fmt.Printf(" Empty odds for marketType=%s in section=%s\n", marketType, sectionName)
|
||||
continue
|
||||
}
|
||||
|
||||
marketRecord := domain.Market{
|
||||
EventID: eventID,
|
||||
FI: fi,
|
||||
MarketCategory: sectionName,
|
||||
MarketType: marketType,
|
||||
MarketName: market.Name,
|
||||
MarketID: market.ID,
|
||||
UpdatedAt: updatedAt,
|
||||
Odds: market.Odds,
|
||||
}
|
||||
|
||||
fmt.Printf(" Saving market to DB: %s (%s)\n", marketType, market.ID)
|
||||
err := s.store.SaveNonLiveMarket(ctx, marketRecord)
|
||||
if err != nil {
|
||||
fmt.Printf(" Save failed for market %s (%s): %v\n", marketType, eventID, err)
|
||||
} else {
|
||||
fmt.Printf(" Successfully stored market: %s (%s)\n", marketType, eventID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type OddsMarket struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Odds []json.RawMessage `json:"odds"`
|
||||
Header string `json:"header,omitempty"`
|
||||
Handicap string `json:"handicap,omitempty"`
|
||||
}
|
||||
|
||||
type OddsSection struct {
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Sp map[string]OddsMarket `json:"sp"`
|
||||
}
|
||||
|
||||
func getString(v interface{}) string {
|
||||
if str, ok := v.(string); ok {
|
||||
return str
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *ServiceImpl) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) {
|
||||
fmt.Printf("Retrieving prematch odds for event_id=%s\n", eventID)
|
||||
|
||||
odds, err := s.store.GetPrematchOdds(ctx, eventID)
|
||||
if err != nil {
|
||||
fmt.Printf(" Failed to retrieve odds for event_id=%s: %v\n", eventID, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Printf(" Retrieved %d odds entries for event_id=%s\n", len(odds), eventID)
|
||||
return odds, nil
|
||||
}
|
||||
|
|
@ -1,22 +1,24 @@
|
|||
package httpserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
||||
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
||||
|
||||
|
||||
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
)
|
||||
|
||||
|
|
@ -35,27 +37,29 @@ type App struct {
|
|||
validator *customvalidator.CustomValidator
|
||||
JwtConfig jwtutil.JwtConfig
|
||||
Logger *slog.Logger
|
||||
prematchSvc *odds.ServiceImpl
|
||||
}
|
||||
|
||||
func NewApp(
|
||||
port int, validator *customvalidator.CustomValidator,
|
||||
authSvc *authentication.Service,
|
||||
logger *slog.Logger,
|
||||
JwtConfig jwtutil.JwtConfig,
|
||||
userSvc *user.Service,
|
||||
port int, validator *customvalidator.CustomValidator,
|
||||
authSvc *authentication.Service,
|
||||
logger *slog.Logger,
|
||||
JwtConfig jwtutil.JwtConfig,
|
||||
userSvc *user.Service,
|
||||
ticketSvc *ticket.Service,
|
||||
betSvc *bet.Service,
|
||||
walletSvc *wallet.Service,
|
||||
transactionSvc *transaction.Service,
|
||||
branchSvc *branch.Service,
|
||||
notidicationStore notificationservice.NotificationStore,
|
||||
prematchSvc *odds.ServiceImpl,
|
||||
) *App {
|
||||
app := fiber.New(fiber.Config{
|
||||
CaseSensitive: true,
|
||||
DisableHeaderNormalizing: true,
|
||||
JSONEncoder: sonic.Marshal,
|
||||
JSONDecoder: sonic.Unmarshal,
|
||||
})
|
||||
app := fiber.New(fiber.Config{
|
||||
CaseSensitive: true,
|
||||
DisableHeaderNormalizing: true,
|
||||
JSONEncoder: sonic.Marshal,
|
||||
JSONDecoder: sonic.Unmarshal,
|
||||
})
|
||||
|
||||
app.Use(cors.New(cors.Config{
|
||||
AllowOrigins: "http://localhost:5173", // Specify your frontend's origin
|
||||
|
|
@ -63,14 +67,14 @@ func NewApp(
|
|||
AllowHeaders: "Content-Type,Authorization", // Specify the allowed headers
|
||||
}))
|
||||
|
||||
s := &App{
|
||||
fiber: app,
|
||||
port: port,
|
||||
authSvc: authSvc,
|
||||
validator: validator,
|
||||
logger: logger,
|
||||
JwtConfig: JwtConfig,
|
||||
userSvc: userSvc,
|
||||
s := &App{
|
||||
fiber: app,
|
||||
port: port,
|
||||
authSvc: authSvc,
|
||||
validator: validator,
|
||||
logger: logger,
|
||||
JwtConfig: JwtConfig,
|
||||
userSvc: userSvc,
|
||||
ticketSvc: ticketSvc,
|
||||
betSvc: betSvc,
|
||||
walletSvc: walletSvc,
|
||||
|
|
@ -78,13 +82,14 @@ func NewApp(
|
|||
branchSvc: branchSvc,
|
||||
NotidicationStore: notidicationStore,
|
||||
Logger: logger,
|
||||
}
|
||||
prematchSvc: prematchSvc,
|
||||
}
|
||||
|
||||
s.initAppRoutes()
|
||||
s.initAppRoutes()
|
||||
|
||||
return s
|
||||
return s
|
||||
}
|
||||
|
||||
func (a *App) Run() error {
|
||||
return a.fiber.Listen(fmt.Sprintf(":%d", a.port))
|
||||
return a.fiber.Listen(fmt.Sprintf(":%d", a.port))
|
||||
}
|
||||
54
internal/web_server/cron.go
Normal file
54
internal/web_server/cron.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package httpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
|
||||
oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.Service) {
|
||||
c := cron.New(cron.WithSeconds())
|
||||
|
||||
schedule := []struct {
|
||||
spec string
|
||||
task func()
|
||||
}{
|
||||
{
|
||||
spec: "0 0 * * * *", // Every hour
|
||||
task: func() {
|
||||
if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
|
||||
log.Printf(" FetchUpcomingEvents error: %v", err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
spec: "*/5 * * * * *", // Every 5 seconds
|
||||
task: func() {
|
||||
if err := eventService.FetchLiveEvents(context.Background()); err != nil {
|
||||
log.Printf(" FetchLiveEvents error: %v", err)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
spec: "*/5 * * * * *", // Every 5 seconds
|
||||
task: func() {
|
||||
if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
|
||||
log.Printf(" FetchNonLiveOdds error: %v", err)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
for _, job := range schedule {
|
||||
if _, err := c.AddFunc(job.spec, job.task); err != nil {
|
||||
log.Fatalf(" Failed to schedule cron job: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
c.Start()
|
||||
log.Println(" Cron jobs started for event and odds services")
|
||||
}
|
||||
37
internal/web_server/handlers/prematch.go
Normal file
37
internal/web_server/handlers/prematch.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// GetPrematchOdds godoc
|
||||
// @Summary Retrieve prematch odds for an event
|
||||
// @Description Retrieve prematch odds for a specific event by event ID
|
||||
// @Tags prematch
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param event_id path string true "Event ID"
|
||||
// @Success 200 {array} domain.Odd
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /prematch/odds/{event_id} [get]
|
||||
func GetPrematchOdds(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
eventID := c.Params("event_id")
|
||||
if eventID == "" {
|
||||
logger.Error("GetPrematchOdds failed: missing event_id")
|
||||
return response.WriteJSON(c, fiber.StatusBadRequest, "Missing event_id", nil, nil)
|
||||
}
|
||||
|
||||
odds, err := prematchSvc.GetPrematchOdds(c.Context(), eventID)
|
||||
if err != nil {
|
||||
logger.Error("GetPrematchOdds failed", "error", err)
|
||||
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve odds", nil, nil)
|
||||
}
|
||||
|
||||
return response.WriteJSON(c, fiber.StatusOK, "Prematch odds retrieved successfully", odds, nil)
|
||||
}
|
||||
}
|
||||
|
|
@ -57,6 +57,9 @@ func (a *App) initAppRoutes() {
|
|||
|
||||
a.fiber.Get("/company/:id/branch", handlers.GetBranchByCompanyID(a.logger, a.branchSvc, a.validator))
|
||||
|
||||
|
||||
a.fiber.Get("/prematch/odds/:event_id", handlers.GetPrematchOdds(a.logger, a.prematchSvc))
|
||||
|
||||
// Swagger
|
||||
a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler())
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user