feat: result checker fixed for market flags

This commit is contained in:
dawitel 2025-04-26 06:07:05 +03:00
parent 90aee0c470
commit acf54d4de7
16 changed files with 949 additions and 226 deletions

View File

@ -2,6 +2,8 @@
# Directory Structure
├── .vscode
│ ├── settings.json
├── cmd
│ ├── main.go
├── db
@ -12,16 +14,24 @@
│ │ ├── 000002_notification.up.sql
│ │ ├── 000003_referal.down.sql
│ │ ├── 000003_referal.up.sql
│ │ ├── 000004_virtual_game_Sessios.down.sql
│ │ ├── 000004_virtual_game_Sessios.up.sql
│ └── query
│ ├── auth.sql
│ ├── bet.sql
│ ├── branch.sql
│ ├── company.sql
│ ├── events.sql
│ ├── notification.sql
│ ├── odds.sql
│ ├── otp.sql
│ ├── referal.sql
│ ├── result.sql
│ ├── ticket.sql
│ ├── transactions.sql
│ ├── transfer.sql
│ ├── user.sql
│ ├── virtual_games.sql
│ ├── wallet.sql
├── docs
│ ├── docs.go
@ -31,26 +41,37 @@
│ └── db
│ ├── auth.sql.go
│ ├── bet.sql.go
│ ├── branch.sql.go
│ ├── company.sql.go
│ ├── copyfrom.go
│ ├── db.go
│ ├── events.sql.go
│ ├── models.go
│ ├── notification.sql.go
│ ├── odds.sql.go
│ ├── otp.sql.go
│ ├── referal.sql.go
│ ├── result.sql.go
│ ├── ticket.sql.go
│ ├── transactions.sql.go
│ ├── transfer.sql.go
│ ├── user.sql.go
│ ├── virtual_games.sql.go
│ ├── wallet.sql.go
└── internal
├── config
│ ├── config.go
├── domain
│ ├── auth.go
│ ├── bank.go
│ ├── bet.go
│ ├── branch.go
│ ├── chapa.go
│ ├── common.go
│ ├── company.go
│ ├── event.go
│ ├── notification.go
│ ├── odds.go
│ ├── otp.go
│ ├── referal.go
│ ├── role.go
@ -58,6 +79,7 @@
│ ├── transaction.go
│ ├── transfer.go
│ ├── user.go
│ ├── virtual_game.go
│ ├── wallet.go
├── logger
│ ├── logger.go
@ -72,14 +94,20 @@
├── repository
│ ├── auth.go
│ ├── bet.go
│ ├── branch.go
│ ├── company.go
│ ├── event.go
│ ├── notification.go
│ ├── odds.go
│ ├── otp.go
│ ├── referal.go
│ ├── result.go
│ ├── store.go
│ ├── ticket.go
│ ├── transaction.go
│ ├── transfer.go
│ ├── user.go
│ ├── virtual_game.go
│ ├── wallet.go
├── services
│ ├── authentication
@ -89,12 +117,27 @@
│ ├── bet
│ │ ├── port.go
│ │ ├── service.go
│ ├── branch
│ │ ├── port.go
│ │ ├── service.go
│ ├── company
│ │ ├── port.go
│ │ ├── service.go
│ ├── event
│ │ ├── port.go
│ │ ├── service.go
│ ├── notfication
│ │ ├── port.go
│ │ ├── service.go
│ ├── odds
│ │ ├── port.go
│ │ ├── service.go
│ ├── referal
│ │ ├── port.go
│ │ ├── service.go
│ ├── result
│ │ ├── port.go
│ │ ├── service.go
│ ├── sportsbook
│ │ ├── events.go
│ │ ├── odds.go
@ -105,30 +148,40 @@
│ ├── transaction
│ │ ├── port.go
│ │ ├── service.go
│ ├── transfer
│ │ ├── chapa.go
│ │ ├── port.go
│ │ ├── service.go
│ ├── user
│ │ ├── common.go
│ │ ├── direct.go
│ │ ├── port.go
│ │ ├── register.go
│ │ ├── reset.go
│ │ ├── service.go
│ │ ├── user.go
│ ├── virtualGame
│ │ ├── port.go
│ │ ├── service.go
│ └── wallet
│ ├── chapa.go
│ ├── port.go
│ ├── service.go
│ ├── transfer.go
│ ├── wallet.go
└── web_server
├── handlers
│ ├── auth_handler.go
│ ├── bet_handler.go
│ ├── branch_handler.go
│ ├── cashier.go
│ ├── company_handler.go
│ ├── handlers.go
│ ├── manager.go
│ ├── notification_handler.go
│ ├── prematch.go
│ ├── referal_handlers.go
│ ├── ticket_handler.go
│ ├── transaction_handler.go
│ ├── transfer_handler.go
│ ├── user.go
│ ├── virtual_games_hadlers.go
│ ├── wallet_handler.go
├── jwt
│ ├── jwt.go
@ -138,10 +191,10 @@
└── validator
├── validatord.go
├── app.go
├── cron.go
├── middleware.go
├── routes.go
├── .air.toml
├── .env
├── .gitignore
├── README.md
├── compose.db.yaml

View File

@ -70,7 +70,7 @@ func main() {
eventSvc := event.New(cfg.Bet365Token, store)
oddsSvc := odds.New(cfg.Bet365Token, store)
resultSvc := result.NewService(cfg.Bet365Token,store)
resultSvc := result.NewService(store, cfg, logger)
ticketSvc := ticket.NewService(store)
betSvc := bet.NewService(store)
walletSvc := wallet.NewService(store, store)
@ -92,7 +92,7 @@ func main() {
JwtAccessKey: cfg.JwtKey,
JwtAccessExpiry: cfg.AccessExpiry,
}, userSvc,
ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc, referalSvc, virtualGameSvc)
ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc, referalSvc, virtualGameSvc, resultSvc)
logger.Info("Starting server", "port", cfg.Port)
if err := app.Run(); err != nil {

View File

@ -386,13 +386,3 @@ VALUES (
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
);
--------------------------------------------------Bet365 Data Fetching + Event Managment------------------------------------------------
CREATE TABLE results (
id SERIAL PRIMARY KEY,
event_id TEXT UNIQUE,
full_time_score TEXT,
half_time_score TEXT,
ss TEXT,
scores JSONB,
fetched_at TIMESTAMPTZ DEFAULT now()
);

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS results;

View File

@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS results (
id BIGSERIAL PRIMARY KEY,
bet_outcome_id BIGINT NOT NULL,
event_id BIGINT NOT NULL,
odd_id BIGINT NOT NULL,
market_id BIGINT NOT NULL,
status INT NOT NULL,
score VARCHAR(255),
full_time_score VARCHAR(255),
half_time_score VARCHAR(255),
ss VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (bet_outcome_id) REFERENCES bet_outcomes (id)
);

View File

@ -1,8 +1,53 @@
-- name: InsertResult :one
INSERT INTO results (event_id, full_time_score, half_time_score, ss, scores)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, event_id, full_time_score, half_time_score, ss, scores, fetched_at;
-- name: CreateResult :one
INSERT INTO results (
bet_outcome_id,
event_id,
odd_id,
market_id,
status,
score,
created_at,
updated_at
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
) RETURNING *;
-- name: InsertResult :exec
INSERT INTO results (
bet_outcome_id,
event_id,
odd_id,
market_id,
status,
score,
full_time_score,
half_time_score,
ss,
created_at,
updated_at
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
);
-- name: GetResultByEventID :one
SELECT * FROM results WHERE event_id = $1;
-- name: GetResultByBetOutcomeID :one
SELECT * FROM results WHERE bet_outcome_id = $1;
-- name: GetPendingBetOutcomes :many
SELECT * FROM bet_outcomes WHERE status = 0 AND expires <= CURRENT_TIMESTAMP;

View File

@ -270,13 +270,18 @@ type RefreshToken struct {
}
type Result struct {
ID int32 `json:"id"`
EventID pgtype.Text `json:"event_id"`
FullTimeScore pgtype.Text `json:"full_time_score"`
HalfTimeScore pgtype.Text `json:"half_time_score"`
Ss pgtype.Text `json:"ss"`
Scores []byte `json:"scores"`
FetchedAt pgtype.Timestamptz `json:"fetched_at"`
ID int64 `json:"id"`
BetOutcomeID int64 `json:"bet_outcome_id"`
EventID int64 `json:"event_id"`
OddID int64 `json:"odd_id"`
MarketID int64 `json:"market_id"`
Status int32 `json:"status"`
Score pgtype.Text `json:"score"`
FullTimeScore pgtype.Text `json:"full_time_score"`
HalfTimeScore pgtype.Text `json:"half_time_score"`
Ss pgtype.Text `json:"ss"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
}
type SupportedOperation struct {

View File

@ -11,56 +11,178 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
const GetResultByEventID = `-- name: GetResultByEventID :one
SELECT id, event_id, full_time_score, half_time_score, ss, scores, fetched_at FROM results WHERE event_id = $1
const CreateResult = `-- name: CreateResult :one
INSERT INTO results (
bet_outcome_id,
event_id,
odd_id,
market_id,
status,
score,
created_at,
updated_at
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
) RETURNING id, bet_outcome_id, event_id, odd_id, market_id, status, score, full_time_score, half_time_score, ss, created_at, updated_at
`
func (q *Queries) GetResultByEventID(ctx context.Context, eventID pgtype.Text) (Result, error) {
row := q.db.QueryRow(ctx, GetResultByEventID, eventID)
type CreateResultParams struct {
BetOutcomeID int64 `json:"bet_outcome_id"`
EventID int64 `json:"event_id"`
OddID int64 `json:"odd_id"`
MarketID int64 `json:"market_id"`
Status int32 `json:"status"`
Score pgtype.Text `json:"score"`
}
func (q *Queries) CreateResult(ctx context.Context, arg CreateResultParams) (Result, error) {
row := q.db.QueryRow(ctx, CreateResult,
arg.BetOutcomeID,
arg.EventID,
arg.OddID,
arg.MarketID,
arg.Status,
arg.Score,
)
var i Result
err := row.Scan(
&i.ID,
&i.BetOutcomeID,
&i.EventID,
&i.OddID,
&i.MarketID,
&i.Status,
&i.Score,
&i.FullTimeScore,
&i.HalfTimeScore,
&i.Ss,
&i.Scores,
&i.FetchedAt,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const InsertResult = `-- name: InsertResult :one
INSERT INTO results (event_id, full_time_score, half_time_score, ss, scores)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, event_id, full_time_score, half_time_score, ss, scores, fetched_at
const GetPendingBetOutcomes = `-- name: GetPendingBetOutcomes :many
SELECT id, bet_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, status, expires FROM bet_outcomes WHERE status = 0 AND expires <= CURRENT_TIMESTAMP
`
func (q *Queries) GetPendingBetOutcomes(ctx context.Context) ([]BetOutcome, error) {
rows, err := q.db.Query(ctx, GetPendingBetOutcomes)
if err != nil {
return nil, err
}
defer rows.Close()
var items []BetOutcome
for rows.Next() {
var i BetOutcome
if err := rows.Scan(
&i.ID,
&i.BetID,
&i.EventID,
&i.OddID,
&i.HomeTeamName,
&i.AwayTeamName,
&i.MarketID,
&i.MarketName,
&i.Odd,
&i.OddName,
&i.OddHeader,
&i.OddHandicap,
&i.Status,
&i.Expires,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetResultByBetOutcomeID = `-- name: GetResultByBetOutcomeID :one
SELECT id, bet_outcome_id, event_id, odd_id, market_id, status, score, full_time_score, half_time_score, ss, created_at, updated_at FROM results WHERE bet_outcome_id = $1
`
func (q *Queries) GetResultByBetOutcomeID(ctx context.Context, betOutcomeID int64) (Result, error) {
row := q.db.QueryRow(ctx, GetResultByBetOutcomeID, betOutcomeID)
var i Result
err := row.Scan(
&i.ID,
&i.BetOutcomeID,
&i.EventID,
&i.OddID,
&i.MarketID,
&i.Status,
&i.Score,
&i.FullTimeScore,
&i.HalfTimeScore,
&i.Ss,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const InsertResult = `-- name: InsertResult :exec
INSERT INTO results (
bet_outcome_id,
event_id,
odd_id,
market_id,
status,
score,
full_time_score,
half_time_score,
ss,
created_at,
updated_at
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
)
`
type InsertResultParams struct {
EventID pgtype.Text `json:"event_id"`
BetOutcomeID int64 `json:"bet_outcome_id"`
EventID int64 `json:"event_id"`
OddID int64 `json:"odd_id"`
MarketID int64 `json:"market_id"`
Status int32 `json:"status"`
Score pgtype.Text `json:"score"`
FullTimeScore pgtype.Text `json:"full_time_score"`
HalfTimeScore pgtype.Text `json:"half_time_score"`
Ss pgtype.Text `json:"ss"`
Scores []byte `json:"scores"`
}
func (q *Queries) InsertResult(ctx context.Context, arg InsertResultParams) (Result, error) {
row := q.db.QueryRow(ctx, InsertResult,
func (q *Queries) InsertResult(ctx context.Context, arg InsertResultParams) error {
_, err := q.db.Exec(ctx, InsertResult,
arg.BetOutcomeID,
arg.EventID,
arg.OddID,
arg.MarketID,
arg.Status,
arg.Score,
arg.FullTimeScore,
arg.HalfTimeScore,
arg.Ss,
arg.Scores,
)
var i Result
err := row.Scan(
&i.ID,
&i.EventID,
&i.FullTimeScore,
&i.HalfTimeScore,
&i.Ss,
&i.Scores,
&i.FetchedAt,
)
return i, err
return err
}

View File

@ -36,16 +36,3 @@ func (m Currency) String() string {
x = x / 100
return fmt.Sprintf("$%.2f", x)
}
type OutcomeStatus int
const (
OUTCOME_STATUS_PENDING OutcomeStatus = iota
OUTCOME_STATUS_WIN
OUTCOME_STATUS_LOSS
OUTCOME_STATUS_ERROR
)
func (b OutcomeStatus) String() string {
return []string{"Pending", "Win", "Loss", "Error"}[b]
}

View File

@ -46,21 +46,11 @@ type MatchResult struct {
Status string
Scores map[string]map[string]string
}
type Result struct {
EventID string
FullTimeScore string
HalfTimeScore string
SS string
Scores map[string]Score // "1": {"home": "0", "away": "1"}
}
type Score struct {
Home string `json:"home"`
Away string `json:"away"`
}
type Odds struct {
ID int64 `json:"id"`
EventID string `json:"event_id"`
MarketType string `json:"market_type"`
Name string `json:"name"`
HitStatus string `json:"hit_status"`
}
ID int64 `json:"id"`
EventID string `json:"event_id"`
MarketType string `json:"market_type"`
Name string `json:"name"`
HitStatus string `json:"hit_status"`
}

105
internal/domain/result.go Normal file
View File

@ -0,0 +1,105 @@
package domain
import (
"time"
)
type ResultResponse struct {
Success int `json:"success"`
Results []struct {
ID string `json:"id"`
SportID string `json:"sport_id"`
Time string `json:"time"`
TimeStatus string `json:"time_status"`
League struct {
ID string `json:"id"`
Name string `json:"name"`
CC string `json:"cc"`
} `json:"league"`
Home struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
CC string `json:"cc"`
} `json:"home"`
Away struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
CC string `json:"cc"`
} `json:"away"`
SS string `json:"ss"`
Scores struct {
FirstHalf Score `json:"1"`
SecondHalf Score `json:"2"`
} `json:"scores"`
Stats struct {
Attacks []string `json:"attacks"`
Corners []string `json:"corners"`
DangerousAttacks []string `json:"dangerous_attacks"`
Goals []string `json:"goals"`
OffTarget []string `json:"off_target"`
OnTarget []string `json:"on_target"`
Penalties []string `json:"penalties"`
PossessionRT []string `json:"possession_rt"`
RedCards []string `json:"redcards"`
Substitutions []string `json:"substitutions"`
YellowCards []string `json:"yellowcards"`
} `json:"stats"`
Extra struct {
HomePos string `json:"home_pos"`
AwayPos string `json:"away_pos"`
StadiumData map[string]string `json:"stadium_data"`
Round string `json:"round"`
} `json:"extra"`
Events []map[string]string `json:"events"`
HasLineup int `json:"has_lineup"`
ConfirmedAt string `json:"confirmed_at"`
Bet365ID string `json:"bet365_id"`
} `json:"results"`
}
type Score struct {
Home string `json:"home"`
Away string `json:"away"`
}
type MarketConfig struct {
Sport string
MarketCategories map[string]bool
MarketTypes map[string]bool
}
type Result struct {
ID int64
BetOutcomeID int64
EventID int64
OddID int64
MarketID int64
Status OutcomeStatus
Score string
FullTimeScore string
HalfTimeScore string
SS string
Scores map[string]Score
CreatedAt time.Time
UpdatedAt time.Time
}
type CreateResult struct {
BetOutcomeID int64
EventID int64
OddID int64
MarketID int64
Status OutcomeStatus
Score string
}
type OutcomeStatus int32
const (
OUTCOME_STATUS_PENDING OutcomeStatus = 0
OUTCOME_STATUS_WIN OutcomeStatus = 1
OUTCOME_STATUS_LOSS OutcomeStatus = 2
OUTCOME_STATUS_VOID OutcomeStatus = 3
)

View File

@ -2,60 +2,99 @@ package repository
import (
"context"
"encoding/json"
"fmt"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/jackc/pgx/v5/pgtype"
)
func (s *Store) InsertResult(ctx context.Context, result domain.Result) error {
scoresJSON, err := json.Marshal(result.Scores)
if err != nil {
return fmt.Errorf("failed to marshal Scores: %w", err)
}
_, err = s.queries.InsertResult(ctx, dbgen.InsertResultParams{
EventID: pgtype.Text{String: result.EventID, Valid: true},
FullTimeScore: pgtype.Text{String: result.FullTimeScore, Valid: true},
HalfTimeScore: pgtype.Text{String: result.HalfTimeScore, Valid: true},
Ss: pgtype.Text{String: result.SS, Valid: true},
Scores: scoresJSON,
})
if err != nil {
return fmt.Errorf("failed to insert result: %w", err)
}
return nil
func convertDBResult(result dbgen.Result) domain.Result {
scores := make(map[string]domain.Score)
return domain.Result{
ID: result.ID,
BetOutcomeID: result.BetOutcomeID,
EventID: result.EventID,
OddID: result.OddID,
MarketID: result.MarketID,
Status: domain.OutcomeStatus(result.Status),
Score: result.Score.String,
FullTimeScore: result.FullTimeScore.String,
HalfTimeScore: result.HalfTimeScore.String,
SS: result.Ss.String,
Scores: scores,
CreatedAt: result.CreatedAt.Time,
UpdatedAt: result.UpdatedAt.Time,
}
}
func (s *Store) GetResultByEventID(ctx context.Context, eventID string) (domain.Result, error) {
eventIDText := pgtype.Text{String: eventID, Valid: true}
func convertCreateResult(result domain.CreateResult) dbgen.CreateResultParams {
return dbgen.CreateResultParams{
BetOutcomeID: result.BetOutcomeID,
EventID: result.EventID,
OddID: result.OddID,
MarketID: result.MarketID,
Status: int32(result.Status),
Score: pgtype.Text{String: result.Score},
}
}
result, err := s.queries.GetResultByEventID(ctx, eventIDText)
if err != nil {
return domain.Result{}, fmt.Errorf("failed to get result by event ID: %w", err)
}
func convertResult(result domain.Result) dbgen.InsertResultParams {
return dbgen.InsertResultParams{
BetOutcomeID: result.BetOutcomeID,
EventID: result.EventID,
OddID: result.OddID,
MarketID: result.MarketID,
Status: int32(result.Status),
Score: pgtype.Text{String: result.Score},
FullTimeScore: pgtype.Text{String: result.FullTimeScore},
HalfTimeScore: pgtype.Text{String: result.HalfTimeScore},
Ss: pgtype.Text{String: result.SS},
}
}
var rawScores map[string]map[string]string
if err := json.Unmarshal(result.Scores, &rawScores); err != nil {
return domain.Result{}, fmt.Errorf("failed to unmarshal scores: %w", err)
}
func (s *Store) CreateResult(ctx context.Context, result domain.CreateResult) (domain.Result, error) {
dbResult, err := s.queries.CreateResult(ctx, convertCreateResult(result))
if err != nil {
return domain.Result{}, err
}
return convertDBResult(dbResult), nil
}
scores := make(map[string]domain.Score)
for key, value := range rawScores {
scores[key] = domain.Score{
Home: value["home"],
Away: value["away"],
}
}
func (s *Store) InsertResult(ctx context.Context, result domain.Result) error {
return s.queries.InsertResult(ctx, convertResult(result))
}
return domain.Result{
EventID: result.EventID.String,
FullTimeScore: result.FullTimeScore.String,
HalfTimeScore: result.HalfTimeScore.String,
SS: result.Ss.String,
Scores: scores,
}, nil
}
func (s *Store) GetResultByBetOutcomeID(ctx context.Context, betOutcomeID int64) (domain.Result, error) {
dbResult, err := s.queries.GetResultByBetOutcomeID(ctx, betOutcomeID)
if err != nil {
return domain.Result{}, err
}
return convertDBResult(dbResult), nil
}
func (s *Store) GetPendingBetOutcomes(ctx context.Context) ([]domain.BetOutcome, error) {
dbOutcomes, err := s.queries.GetPendingBetOutcomes(ctx)
if err != nil {
return nil, err
}
outcomes := make([]domain.BetOutcome, 0, len(dbOutcomes))
for _, dbOutcome := range dbOutcomes {
outcomes = append(outcomes, domain.BetOutcome{
ID: dbOutcome.ID,
BetID: dbOutcome.BetID,
EventID: dbOutcome.EventID,
OddID: dbOutcome.OddID,
HomeTeamName: dbOutcome.HomeTeamName,
AwayTeamName: dbOutcome.AwayTeamName,
MarketID: dbOutcome.MarketID,
MarketName: dbOutcome.MarketName,
Odd: dbOutcome.Odd,
OddName: dbOutcome.OddName,
OddHeader: dbOutcome.OddHeader,
OddHandicap: dbOutcome.OddHandicap,
Status: domain.OutcomeStatus(dbOutcome.Status),
Expires: dbOutcome.Expires.Time,
})
}
return outcomes, nil
}

View File

@ -2,10 +2,9 @@ package result
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type Store interface {
InsertResult(ctx context.Context, result domain.Result) error
GetResultByEventID(ctx context.Context, eventID string) (domain.Result, error)
type ResultService interface {
FetchAndProcessResults(ctx context.Context) error
FetchAndStoreResult(ctx context.Context, eventID string) error
}

View File

@ -1,68 +1,436 @@
package result
import (
"context"
"encoding/json"
"fmt"
"net/http"
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"strconv"
"strings"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
)
type Service interface {
FetchAndStoreResult(ctx context.Context, eventID string) error
type Service struct {
repo *repository.Store
config *config.Config
logger *slog.Logger
client *http.Client
}
type service struct {
token string
store *repository.Store
func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger) *Service {
return &Service{
repo: repo,
config: cfg,
logger: logger,
client: &http.Client{Timeout: 10 * time.Second},
}
}
func NewService(token string, store *repository.Store) Service {
return &service{
token: token,
store: store,
}
var supportedMarkets = map[string]domain.MarketConfig{
"football": {
Sport: "football",
MarketCategories: map[string]bool{
"main": true,
"asian_lines": true,
"goals": true,
"half": true,
},
MarketTypes: map[string]bool{
"full_time_result": true,
"double_chance": true,
"goals_over_under": true,
"correct_score": true,
"asian_handicap": true,
"goal_line": true,
"half_time_result": true,
"1st_half_asian_handicap": true,
"1st_half_goal_line": true,
"first_team_to_score": true,
"goals_odd_even": true,
"draw_no_bet": true,
},
},
}
func (s *service) FetchAndStoreResult(ctx context.Context, eventID string) error {
url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%s", s.token, eventID)
func (s *Service) FetchAndProcessResults(ctx context.Context) error {
outcomes, err := s.repo.GetPendingBetOutcomes(ctx)
if err != nil {
s.logger.Error("Failed to get pending bet outcomes", "error", err)
return err
}
res, err := http.Get(url)
if err != nil {
return fmt.Errorf("failed to fetch result: %w", err)
}
defer res.Body.Close()
for _, outcome := range outcomes {
if outcome.Expires.After(time.Now()) {
continue
}
var apiResp struct {
Results []struct {
SS string
Scores map[string]domain.Score `json:"scores"`
} `json:"results"`
}
result, err := s.fetchResult(ctx, outcome.EventID, outcome.OddID, outcome.MarketID, outcome)
if err != nil {
s.logger.Error("Failed to fetch result", "event_id", outcome.EventID, "error", err)
continue
}
if err := json.NewDecoder(res.Body).Decode(&apiResp); err != nil {
return fmt.Errorf("failed to decode result: %w", err)
}
if len(apiResp.Results) == 0 {
return fmt.Errorf("no result returned from API")
}
_, err = s.repo.CreateResult(ctx, domain.CreateResult{
BetOutcomeID: outcome.ID,
EventID: outcome.EventID,
OddID: outcome.OddID,
MarketID: outcome.MarketID,
Status: result.Status,
Score: result.Score,
})
if err != nil {
s.logger.Error("Failed to store result", "bet_outcome_id", outcome.ID, "error", err)
continue
}
r := apiResp.Results[0]
err = s.repo.UpdateBetOutcomeStatus(ctx, outcome.ID, result.Status)
if err != nil {
s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err)
continue
}
}
halfScore := ""
if s1, ok := r.Scores["1"]; ok {
halfScore = fmt.Sprintf("%s-%s", s1.Home, s1.Away)
}
return nil
}
result := domain.Result{
EventID: eventID,
FullTimeScore: r.SS,
HalfTimeScore: halfScore,
SS: r.SS,
Scores: r.Scores,
}
func (s *Service) FetchAndStoreResult(ctx context.Context, eventID string) error {
url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%s", s.config.Bet365Token, eventID)
return s.store.InsertResult(ctx, result)
}
res, err := s.client.Get(url)
if err != nil {
s.logger.Error("Failed to fetch result", "event_id", eventID, "error", err)
return fmt.Errorf("failed to fetch result: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
s.logger.Error("Unexpected status code", "event_id", eventID, "status_code", res.StatusCode)
return fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
var apiResp domain.ResultResponse
if err := json.NewDecoder(res.Body).Decode(&apiResp); err != nil {
s.logger.Error("Failed to decode result", "event_id", eventID, "error", err)
return fmt.Errorf("failed to decode result: %w", err)
}
if apiResp.Success != 1 || len(apiResp.Results) == 0 {
s.logger.Error("Invalid API response", "event_id", eventID)
return fmt.Errorf("no result returned from API")
}
r := apiResp.Results[0]
if r.TimeStatus != "3" {
s.logger.Warn("Match not yet completed", "event_id", eventID)
return fmt.Errorf("match not yet completed")
}
eventIDInt, err := strconv.ParseInt(eventID, 10, 64)
if err != nil {
s.logger.Error("Failed to parse event_id", "event_id", eventID, "error", err)
return fmt.Errorf("failed to parse event_id: %w", err)
}
halfScore := ""
if r.Scores.FirstHalf.Home != "" {
halfScore = fmt.Sprintf("%s-%s", r.Scores.FirstHalf.Home, r.Scores.FirstHalf.Away)
}
result := domain.Result{
EventID: eventIDInt,
Status: domain.OUTCOME_STATUS_PENDING,
Score: r.SS,
FullTimeScore: r.SS,
HalfTimeScore: halfScore,
SS: r.SS,
Scores: make(map[string]domain.Score),
}
for k, v := range map[string]domain.Score{
"1": r.Scores.FirstHalf,
"2": r.Scores.SecondHalf,
} {
result.Scores[k] = domain.Score{
Home: v.Home,
Away: v.Away,
}
}
return s.repo.InsertResult(ctx, result)
}
func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) {
url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%d", s.config.Bet365Token, eventID)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
s.logger.Error("Failed to create request", "event_id", eventID, "error", err)
return domain.CreateResult{}, err
}
resp, err := s.client.Do(req)
if err != nil {
s.logger.Error("Failed to fetch result", "event_id", eventID, "error", err)
return domain.CreateResult{}, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
s.logger.Error("Unexpected status code", "event_id", eventID, "status_code", resp.StatusCode)
return domain.CreateResult{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var resultResp domain.ResultResponse
if err := json.NewDecoder(resp.Body).Decode(&resultResp); err != nil {
s.logger.Error("Failed to decode result", "event_id", eventID, "error", err)
return domain.CreateResult{}, err
}
if resultResp.Success != 1 || len(resultResp.Results) == 0 {
s.logger.Error("Invalid API response", "event_id", eventID)
return domain.CreateResult{}, fmt.Errorf("invalid API response")
}
result := resultResp.Results[0]
if result.TimeStatus != "3" {
s.logger.Warn("Match not yet completed", "event_id", eventID)
return domain.CreateResult{}, fmt.Errorf("match not yet completed")
}
finalScore := parseScore(result.SS)
firstHalfScore := parseScore(fmt.Sprintf("%s-%s", result.Scores.FirstHalf.Home, result.Scores.FirstHalf.Away))
corners := parseStats(result.Stats.Corners)
status, err := s.evaluateOutcome(outcome, finalScore, firstHalfScore, corners, result.Events)
if err != nil {
s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err)
return domain.CreateResult{}, err
}
return domain.CreateResult{
BetOutcomeID: 0,
EventID: eventID,
OddID: oddID,
MarketID: marketID,
Status: status,
Score: result.SS,
}, nil
}
func parseScore(scoreStr string) struct{ Home, Away int } {
parts := strings.Split(scoreStr, "-")
if len(parts) != 2 {
return struct{ Home, Away int }{0, 0}
}
home, _ := strconv.Atoi(strings.TrimSpace(parts[0]))
away, _ := strconv.Atoi(strings.TrimSpace(parts[1]))
return struct{ Home, Away int }{Home: home, Away: away}
}
func parseStats(stats []string) struct{ Home, Away int } {
if len(stats) != 2 {
return struct{ Home, Away int }{0, 0}
}
home, _ := strconv.Atoi(stats[0])
away, _ := strconv.Atoi(stats[1])
return struct{ Home, Away int }{Home: home, Away: away}
}
// evaluateOutcome determines the outcome status based on market type and odd
func (s *Service) evaluateOutcome(outcome domain.BetOutcome, finalScore, firstHalfScore struct{ Home, Away int }, corners struct{ Home, Away int }, events []map[string]string) (domain.OutcomeStatus, error) {
marketConfig := supportedMarkets["football"]
if !marketConfig.MarketTypes[outcome.MarketName] {
s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName)
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName)
}
switch outcome.MarketName {
case "full_time_result":
return evaluateFullTimeResult(outcome, finalScore)
case "goals_over_under":
return evaluateGoalsOverUnder(outcome, finalScore)
case "correct_score":
return evaluateCorrectScore(outcome, finalScore)
case "half_time_result":
return evaluateHalfTimeResult(outcome, firstHalfScore)
case "asian_handicap":
return evaluateAsianHandicap(outcome, finalScore)
case "goal_line":
return evaluateGoalLine(outcome, finalScore)
case "1st_half_asian_handicap":
return evaluateAsianHandicap(outcome, firstHalfScore)
case "1st_half_goal_line":
return evaluateGoalLine(outcome, firstHalfScore)
case "first_team_to_score":
return evaluateFirstTeamToScore(outcome, events)
case "goals_odd_even":
return evaluateGoalsOddEven(outcome, finalScore)
case "double_chance":
return evaluateDoubleChance(outcome, finalScore)
case "draw_no_bet":
return evaluateDrawNoBet(outcome, finalScore)
default:
s.logger.Warn("Market type not implemented", "market_name", outcome.MarketName)
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("market type not implemented: %s", outcome.MarketName)
}
}
func evaluateFullTimeResult(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddName {
case "1": // Home win
if score.Home > score.Away {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
case "Draw":
if score.Home == score.Away {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
case "2": // Away win
if score.Away > score.Home {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName)
}
}
func evaluateGoalsOverUnder(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalGoals := float64(score.Home + score.Away)
threshold, err := strconv.ParseFloat(outcome.OddName, 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
}
if outcome.OddHeader == "Over" {
if totalGoals > threshold {
return domain.OUTCOME_STATUS_WIN, nil
} else if totalGoals == threshold {
return domain.OUTCOME_STATUS_VOID, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else if outcome.OddHeader == "Under" {
if totalGoals < threshold {
return domain.OUTCOME_STATUS_WIN, nil
} else if totalGoals == threshold {
return domain.OUTCOME_STATUS_VOID, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
func evaluateCorrectScore(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
expectedScore := fmt.Sprintf("%d-%d", score.Home, score.Away)
if outcome.OddName == expectedScore {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}
func evaluateHalfTimeResult(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
return evaluateFullTimeResult(outcome, score)
}
func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
handicap, err := strconv.ParseFloat(outcome.OddHandicap, 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid handicap: %s", outcome.OddHandicap)
}
adjustedHomeScore := float64(score.Home)
adjustedAwayScore := float64(score.Away)
if outcome.OddHeader == "1" { // Home team
adjustedHomeScore += handicap
} else if outcome.OddHeader == "2" { // Away team
adjustedAwayScore += handicap
} else {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
if adjustedHomeScore > adjustedAwayScore {
if outcome.OddHeader == "1" {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else if adjustedHomeScore < adjustedAwayScore {
if outcome.OddHeader == "2" {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}
return domain.OUTCOME_STATUS_VOID, nil
}
func evaluateGoalLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
return evaluateGoalsOverUnder(outcome, score)
}
func evaluateFirstTeamToScore(outcome domain.BetOutcome, events []map[string]string) (domain.OutcomeStatus, error) {
for _, event := range events {
if strings.Contains(event["text"], "1st Goal") {
if strings.Contains(event["text"], outcome.HomeTeamName) && outcome.OddName == "1" {
return domain.OUTCOME_STATUS_WIN, nil
} else if strings.Contains(event["text"], outcome.AwayTeamName) && outcome.OddName == "2" {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}
}
return domain.OUTCOME_STATUS_VOID, nil // No goals scored
}
func evaluateGoalsOddEven(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalGoals := score.Home + score.Away
isOdd := totalGoals%2 == 1
if outcome.OddName == "Odd" && isOdd {
return domain.OUTCOME_STATUS_WIN, nil
} else if outcome.OddName == "Even" && !isOdd {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}
func evaluateDoubleChance(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
isHomeWin := score.Home > score.Away
isDraw := score.Home == score.Away
isAwayWin := score.Away > score.Home
switch outcome.OddName {
case "1 or Draw":
if isHomeWin || isDraw {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
case "Draw or 2":
if isDraw || isAwayWin {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
case "1 or 2":
if isHomeWin || isAwayWin {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName)
}
}
func evaluateDrawNoBet(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
if score.Home == score.Away {
return domain.OUTCOME_STATUS_VOID, nil
}
if outcome.OddName == "1" && score.Home > score.Away {
return domain.OUTCOME_STATUS_WIN, nil
} else if outcome.OddName == "2" && score.Away > score.Home {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}

View File

@ -6,11 +6,12 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
@ -45,6 +46,7 @@ type App struct {
Logger *slog.Logger
prematchSvc *odds.ServiceImpl
eventSvc event.Service
resultSvc *result.Service
}
func NewApp(
@ -64,6 +66,7 @@ func NewApp(
eventSvc event.Service,
referralSvc referralservice.ReferralStore,
virtualGameSvc virtualgameservice.VirtualGameService,
resultSvc *result.Service,
) *App {
app := fiber.New(fiber.Config{
CaseSensitive: true,
@ -99,6 +102,7 @@ func NewApp(
prematchSvc: prematchSvc,
eventSvc: eventSvc,
virtualGameSvc: virtualGameSvc,
resultSvc: resultSvc,
}
s.initAppRoutes()

View File

@ -1,23 +1,23 @@
package httpserver
import (
"context"
"log"
"context"
"log"
eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
resultsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
"github.com/robfig/cron/v3"
eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
resultsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
"github.com/robfig/cron/v3"
)
func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.Service, resultService resultsvc.Service) {
c := cron.New(cron.WithSeconds())
func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.Service, resultService *resultsvc.Service) {
c := cron.New(cron.WithSeconds())
schedule := []struct {
spec string
task func()
}{
// {
schedule := []struct {
spec string
task func()
}{
// {
// spec: "*/5 * * * * *", // Every 5 seconds
// task: func() {
// if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
@ -25,34 +25,34 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
// }
// },
// },
// {
// spec: "*/5 * * * * *", // Every 5 seconds
// task: func() {
// if err := eventService.FetchLiveEvents(context.Background()); err != nil {
// log.Printf("FetchLiveEvents error: %v", err)
// }
// },
// },
// {
// spec: "0 */15 * * * *", // Every 15 minutes
// task: func() {
// if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
// log.Printf("FetchNonLiveOdds error: %v", err)
// }
// },
// },
{
// {
// spec: "*/5 * * * * *", // Every 5 seconds
// task: func() {
// if err := eventService.FetchLiveEvents(context.Background()); err != nil {
// log.Printf("FetchLiveEvents error: %v", err)
// }
// },
// },
// {
// spec: "0 */15 * * * *", // Every 15 minutes
// task: func() {
// if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
// log.Printf("FetchNonLiveOdds error: %v", err)
// }
// },
// },
{
spec: "*/10 * * * * *",
task: func() {
log.Println("Fetching results for upcoming events...")
upcomingEvents, err := eventService.GetAllUpcomingEvents(context.Background())
if err != nil {
log.Printf("Failed to fetch upcoming events: %v", err)
return
}
for _, event := range upcomingEvents {
if err := resultService.FetchAndStoreResult(context.Background(), event.ID); err != nil {
log.Printf(" Failed to fetch/store result for event %s: %v", event.ID, err)
@ -64,12 +64,12 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
},
}
for _, job := range schedule {
if _, err := c.AddFunc(job.spec, job.task); err != nil {
log.Fatalf("Failed to schedule cron job: %v", err)
}
}
for _, job := range schedule {
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")
}
c.Start()
log.Println("Cron jobs started for event and odds services")
}