From 3fb3da6cc8bbcc19978f00b3af92d26d87ea85aa Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 30 Jul 2025 16:55:57 +0300 Subject: [PATCH 01/10] fix: result log and notification --- .gitignore | 1 + db/migrations/000001_fortune.up.sql | 17 +++ db/query/events_stat.sql | 54 ++++++++- db/query/result_log.sql | 28 +++++ docker-compose.yml | 1 + gen/db/events.sql.go | 14 ++- gen/db/events_stat.sql.go | 109 +++++++++++++++++ gen/db/models.go | 18 +++ gen/db/result_log.sql.go | 132 +++++++++++++++++++++ internal/domain/bet.go | 4 +- internal/domain/result.go | 90 +++++++++++--- internal/repository/result.go | 97 +++------------ internal/services/bet/service.go | 14 ++- internal/services/result/port.go | 7 ++ internal/services/result/service.go | 178 +++++++++++++++++++++------- internal/web_server/cron.go | 39 ++++-- 16 files changed, 644 insertions(+), 159 deletions(-) create mode 100644 db/query/result_log.sql create mode 100644 gen/db/result_log.sql.go diff --git a/.gitignore b/.gitignore index d96cd29..995b5a8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ logs/ app_logs/ backup/ reports/ +exports/ \ No newline at end of file diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 6892944..d7fec37 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -264,6 +264,7 @@ CREATE TABLE events ( fetched_at TIMESTAMP DEFAULT now(), source TEXT DEFAULT 'b365api', is_featured BOOLEAN NOT NULL DEFAULT FALSE, + is_monitorred BOOLEAN NOT NULL DEFAULT FALSE, is_active BOOLEAN NOT NULL DEFAULT TRUE ); CREATE TABLE odds ( @@ -287,6 +288,22 @@ CREATE TABLE odds ( UNIQUE (event_id, market_id, name, handicap), UNIQUE (event_id, market_id) ); +CREATE TABLE result_log ( + id BIGSERIAL PRIMARY KEY, + status_not_finished_count INT NOT NULL, + status_not_finished_bets INT NOT NULL, + status_to_be_fixed_count INT NOT NULL, + status_to_be_fixed_bets INT NOT NULL, + status_postponed_count INT NOT NULL, + status_postponed_bets INT NOT NULL, + status_ended_count INT NOT NULL, + status_ended_bets INT NOT NULL, + status_removed_count INT NOT NULL, + status_removed_bets INT NOT NULL, + removed_count INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); CREATE TABLE companies ( id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL, diff --git a/db/query/events_stat.sql b/db/query/events_stat.sql index 96e2100..733969e 100644 --- a/db/query/events_stat.sql +++ b/db/query/events_stat.sql @@ -16,4 +16,56 @@ WHERE ( OR sqlc.narg('league_id') IS NULL ) GROUP BY month -ORDER BY month; \ No newline at end of file +ORDER BY month; + +-- name: GetLeagueEventStat :many +SELECT leagues.id, + leagues.name, + COUNT(*) AS total_events, + COUNT(*) FILTER ( + WHERE events.status = 'pending' + ) AS pending, + COUNT(*) FILTER ( + WHERE events.status = 'in_play' + ) AS in_play, + COUNT(*) FILTER ( + WHERE events.status = 'to_be_fixed' + ) AS to_be_fixed, + COUNT(*) FILTER ( + WHERE events.status = 'ended' + ) AS ended, + COUNT(*) FILTER ( + WHERE events.status = 'postponed' + ) AS postponed, + COUNT(*) FILTER ( + WHERE events.status = 'cancelled' + ) AS cancelled, + COUNT(*) FILTER ( + WHERE events.status = 'walkover' + ) AS walkover, + COUNT(*) FILTER ( + WHERE events.status = 'interrupted' + ) AS interrupted, + COUNT(*) FILTER ( + WHERE events.status = 'abandoned' + ) AS abandoned, + COUNT(*) FILTER ( + WHERE events.status = 'retired' + ) AS retired, + COUNT(*) FILTER ( + WHERE events.status = 'suspended' + ) AS suspended, + COUNT(*) FILTER ( + WHERE events.status = 'decided_by_fa' + ) AS decided_by_fa, + COUNT(*) FILTER ( + WHERE events.status = 'removed' + ) AS removed +FROM leagues + JOIN events ON leagues.id = events.league_id +WHERE ( + leagues.is_featured = sqlc.narg('is_league_featured') + OR sqlc.narg('is_league_featured') IS NULL + ) +GROUP BY leagues.id, + leagues.name; \ No newline at end of file diff --git a/db/query/result_log.sql b/db/query/result_log.sql new file mode 100644 index 0000000..b20d71e --- /dev/null +++ b/db/query/result_log.sql @@ -0,0 +1,28 @@ +-- name: CreateResultLog :one +INSERT INTO result_log ( + status_not_finished_count, + status_not_finished_bets, + status_to_be_fixed_count, + status_to_be_fixed_bets, + status_postponed_count, + status_postponed_bets, + status_ended_count, + status_ended_bets, + status_removed_count, + status_removed_bets, + removed_count + ) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) +RETURNING *; +-- name: GetAllResultLog :many +SELECT * +FROM result_log +WHERE ( + created_at < sqlc.narg('created_before') + OR sqlc.narg('created_before') IS NULL + ) + AND ( + created_at > sqlc.narg('created_after') + OR sqlc.narg('created_after') IS NULL + ) +ORDER BY created_at DESC; diff --git a/docker-compose.yml b/docker-compose.yml index 7df9ea8..3a5d5aa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,7 @@ services: retries: 5 volumes: - postgres_data:/var/lib/postgresql/data + - ./exports:/exports mongo: container_name: fortunebet-mongo diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index 101c705..da93861 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -22,7 +22,7 @@ func (q *Queries) DeleteEvent(ctx context.Context, id string) error { } const GetAllUpcomingEvents = `-- name: GetAllUpcomingEvents :many -SELECT 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, fetched_at, source, is_featured, is_active +SELECT 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, fetched_at, source, is_featured, is_monitorred, is_active FROM events WHERE start_time > now() AND is_live = false @@ -63,6 +63,7 @@ func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]Event, error) { &i.FetchedAt, &i.Source, &i.IsFeatured, + &i.IsMonitorred, &i.IsActive, ); err != nil { return nil, err @@ -76,7 +77,7 @@ func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]Event, error) { } const GetExpiredUpcomingEvents = `-- name: GetExpiredUpcomingEvents :many -SELECT events.id, events.sport_id, events.match_name, events.home_team, events.away_team, events.home_team_id, events.away_team_id, events.home_kit_image, events.away_kit_image, events.league_id, events.league_name, events.league_cc, events.start_time, events.score, events.match_minute, events.timer_status, events.added_time, events.match_period, events.is_live, events.status, events.fetched_at, events.source, events.is_featured, events.is_active, +SELECT events.id, events.sport_id, events.match_name, events.home_team, events.away_team, events.home_team_id, events.away_team_id, events.home_kit_image, events.away_kit_image, events.league_id, events.league_name, events.league_cc, events.start_time, events.score, events.match_minute, events.timer_status, events.added_time, events.match_period, events.is_live, events.status, events.fetched_at, events.source, events.is_featured, events.is_monitorred, events.is_active, leagues.country_code as league_cc FROM events LEFT JOIN leagues ON leagues.id = league_id @@ -112,6 +113,7 @@ type GetExpiredUpcomingEventsRow struct { FetchedAt pgtype.Timestamp `json:"fetched_at"` Source pgtype.Text `json:"source"` IsFeatured bool `json:"is_featured"` + IsMonitorred bool `json:"is_monitorred"` IsActive bool `json:"is_active"` LeagueCc_2 pgtype.Text `json:"league_cc_2"` } @@ -149,6 +151,7 @@ func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context, status pgtype.Te &i.FetchedAt, &i.Source, &i.IsFeatured, + &i.IsMonitorred, &i.IsActive, &i.LeagueCc_2, ); err != nil { @@ -163,7 +166,7 @@ func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context, status pgtype.Te } const GetPaginatedUpcomingEvents = `-- name: GetPaginatedUpcomingEvents :many -SELECT events.id, events.sport_id, events.match_name, events.home_team, events.away_team, events.home_team_id, events.away_team_id, events.home_kit_image, events.away_kit_image, events.league_id, events.league_name, events.league_cc, events.start_time, events.score, events.match_minute, events.timer_status, events.added_time, events.match_period, events.is_live, events.status, events.fetched_at, events.source, events.is_featured, events.is_active, +SELECT events.id, events.sport_id, events.match_name, events.home_team, events.away_team, events.home_team_id, events.away_team_id, events.home_kit_image, events.away_kit_image, events.league_id, events.league_name, events.league_cc, events.start_time, events.score, events.match_minute, events.timer_status, events.added_time, events.match_period, events.is_live, events.status, events.fetched_at, events.source, events.is_featured, events.is_monitorred, events.is_active, leagues.country_code as league_cc FROM events LEFT JOIN leagues ON leagues.id = league_id @@ -239,6 +242,7 @@ type GetPaginatedUpcomingEventsRow struct { FetchedAt pgtype.Timestamp `json:"fetched_at"` Source pgtype.Text `json:"source"` IsFeatured bool `json:"is_featured"` + IsMonitorred bool `json:"is_monitorred"` IsActive bool `json:"is_active"` LeagueCc_2 pgtype.Text `json:"league_cc_2"` } @@ -286,6 +290,7 @@ func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginat &i.FetchedAt, &i.Source, &i.IsFeatured, + &i.IsMonitorred, &i.IsActive, &i.LeagueCc_2, ); err != nil { @@ -362,7 +367,7 @@ func (q *Queries) GetTotalEvents(ctx context.Context, arg GetTotalEventsParams) } const GetUpcomingByID = `-- name: GetUpcomingByID :one -SELECT 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, fetched_at, source, is_featured, is_active +SELECT 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, fetched_at, source, is_featured, is_monitorred, is_active FROM events WHERE id = $1 AND is_live = false @@ -397,6 +402,7 @@ func (q *Queries) GetUpcomingByID(ctx context.Context, id string) (Event, error) &i.FetchedAt, &i.Source, &i.IsFeatured, + &i.IsMonitorred, &i.IsActive, ) return i, err diff --git a/gen/db/events_stat.sql.go b/gen/db/events_stat.sql.go index 35087e1..2380d54 100644 --- a/gen/db/events_stat.sql.go +++ b/gen/db/events_stat.sql.go @@ -11,6 +11,115 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const GetLeagueEventStat = `-- name: GetLeagueEventStat :many +SELECT leagues.id, + leagues.name, + COUNT(*) AS total_events, + COUNT(*) FILTER ( + WHERE events.status = 'pending' + ) AS pending, + COUNT(*) FILTER ( + WHERE events.status = 'in_play' + ) AS in_play, + COUNT(*) FILTER ( + WHERE events.status = 'to_be_fixed' + ) AS to_be_fixed, + COUNT(*) FILTER ( + WHERE events.status = 'ended' + ) AS ended, + COUNT(*) FILTER ( + WHERE events.status = 'postponed' + ) AS postponed, + COUNT(*) FILTER ( + WHERE events.status = 'cancelled' + ) AS cancelled, + COUNT(*) FILTER ( + WHERE events.status = 'walkover' + ) AS walkover, + COUNT(*) FILTER ( + WHERE events.status = 'interrupted' + ) AS interrupted, + COUNT(*) FILTER ( + WHERE events.status = 'abandoned' + ) AS abandoned, + COUNT(*) FILTER ( + WHERE events.status = 'retired' + ) AS retired, + COUNT(*) FILTER ( + WHERE events.status = 'suspended' + ) AS suspended, + COUNT(*) FILTER ( + WHERE events.status = 'decided_by_fa' + ) AS decided_by_fa, + COUNT(*) FILTER ( + WHERE events.status = 'removed' + ) AS removed +FROM leagues + JOIN events ON leagues.id = events.league_id +WHERE ( + leagues.is_featured = $1 + OR $1 IS NULL + ) +GROUP BY leagues.id, + leagues.name +` + +type GetLeagueEventStatRow struct { + ID int64 `json:"id"` + Name string `json:"name"` + TotalEvents int64 `json:"total_events"` + Pending int64 `json:"pending"` + InPlay int64 `json:"in_play"` + ToBeFixed int64 `json:"to_be_fixed"` + Ended int64 `json:"ended"` + Postponed int64 `json:"postponed"` + Cancelled int64 `json:"cancelled"` + Walkover int64 `json:"walkover"` + Interrupted int64 `json:"interrupted"` + Abandoned int64 `json:"abandoned"` + Retired int64 `json:"retired"` + Suspended int64 `json:"suspended"` + DecidedByFa int64 `json:"decided_by_fa"` + Removed int64 `json:"removed"` +} + +func (q *Queries) GetLeagueEventStat(ctx context.Context, isLeagueFeatured pgtype.Bool) ([]GetLeagueEventStatRow, error) { + rows, err := q.db.Query(ctx, GetLeagueEventStat, isLeagueFeatured) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetLeagueEventStatRow + for rows.Next() { + var i GetLeagueEventStatRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.TotalEvents, + &i.Pending, + &i.InPlay, + &i.ToBeFixed, + &i.Ended, + &i.Postponed, + &i.Cancelled, + &i.Walkover, + &i.Interrupted, + &i.Abandoned, + &i.Retired, + &i.Suspended, + &i.DecidedByFa, + &i.Removed, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const GetTotalMontlyEventStat = `-- name: GetTotalMontlyEventStat :many SELECT DATE_TRUNC('month', start_time) AS month, COUNT(*) AS event_count diff --git a/gen/db/models.go b/gen/db/models.go index 575526a..8cad748 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -258,6 +258,7 @@ type Event struct { FetchedAt pgtype.Timestamp `json:"fetched_at"` Source pgtype.Text `json:"source"` IsFeatured bool `json:"is_featured"` + IsMonitorred bool `json:"is_monitorred"` IsActive bool `json:"is_active"` } @@ -408,6 +409,23 @@ type Result struct { UpdatedAt pgtype.Timestamp `json:"updated_at"` } +type ResultLog struct { + ID int64 `json:"id"` + StatusNotFinishedCount int32 `json:"status_not_finished_count"` + StatusNotFinishedBets int32 `json:"status_not_finished_bets"` + StatusToBeFixedCount int32 `json:"status_to_be_fixed_count"` + StatusToBeFixedBets int32 `json:"status_to_be_fixed_bets"` + StatusPostponedCount int32 `json:"status_postponed_count"` + StatusPostponedBets int32 `json:"status_postponed_bets"` + StatusEndedCount int32 `json:"status_ended_count"` + StatusEndedBets int32 `json:"status_ended_bets"` + StatusRemovedCount int32 `json:"status_removed_count"` + StatusRemovedBets int32 `json:"status_removed_bets"` + RemovedCount int32 `json:"removed_count"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` +} + type Setting struct { Key string `json:"key"` Value string `json:"value"` diff --git a/gen/db/result_log.sql.go b/gen/db/result_log.sql.go new file mode 100644 index 0000000..468795e --- /dev/null +++ b/gen/db/result_log.sql.go @@ -0,0 +1,132 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: result_log.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const CreateResultLog = `-- name: CreateResultLog :one +INSERT INTO result_log ( + status_not_finished_count, + status_not_finished_bets, + status_to_be_fixed_count, + status_to_be_fixed_bets, + status_postponed_count, + status_postponed_bets, + status_ended_count, + status_ended_bets, + status_removed_count, + status_removed_bets, + removed_count + ) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) +RETURNING id, status_not_finished_count, status_not_finished_bets, status_to_be_fixed_count, status_to_be_fixed_bets, status_postponed_count, status_postponed_bets, status_ended_count, status_ended_bets, status_removed_count, status_removed_bets, removed_count, created_at, updated_at +` + +type CreateResultLogParams struct { + StatusNotFinishedCount int32 `json:"status_not_finished_count"` + StatusNotFinishedBets int32 `json:"status_not_finished_bets"` + StatusToBeFixedCount int32 `json:"status_to_be_fixed_count"` + StatusToBeFixedBets int32 `json:"status_to_be_fixed_bets"` + StatusPostponedCount int32 `json:"status_postponed_count"` + StatusPostponedBets int32 `json:"status_postponed_bets"` + StatusEndedCount int32 `json:"status_ended_count"` + StatusEndedBets int32 `json:"status_ended_bets"` + StatusRemovedCount int32 `json:"status_removed_count"` + StatusRemovedBets int32 `json:"status_removed_bets"` + RemovedCount int32 `json:"removed_count"` +} + +func (q *Queries) CreateResultLog(ctx context.Context, arg CreateResultLogParams) (ResultLog, error) { + row := q.db.QueryRow(ctx, CreateResultLog, + arg.StatusNotFinishedCount, + arg.StatusNotFinishedBets, + arg.StatusToBeFixedCount, + arg.StatusToBeFixedBets, + arg.StatusPostponedCount, + arg.StatusPostponedBets, + arg.StatusEndedCount, + arg.StatusEndedBets, + arg.StatusRemovedCount, + arg.StatusRemovedBets, + arg.RemovedCount, + ) + var i ResultLog + err := row.Scan( + &i.ID, + &i.StatusNotFinishedCount, + &i.StatusNotFinishedBets, + &i.StatusToBeFixedCount, + &i.StatusToBeFixedBets, + &i.StatusPostponedCount, + &i.StatusPostponedBets, + &i.StatusEndedCount, + &i.StatusEndedBets, + &i.StatusRemovedCount, + &i.StatusRemovedBets, + &i.RemovedCount, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const GetAllResultLog = `-- name: GetAllResultLog :many +SELECT id, status_not_finished_count, status_not_finished_bets, status_to_be_fixed_count, status_to_be_fixed_bets, status_postponed_count, status_postponed_bets, status_ended_count, status_ended_bets, status_removed_count, status_removed_bets, removed_count, created_at, updated_at +FROM result_log +WHERE ( + created_at < $1 + OR $1 IS NULL + ) + AND ( + created_at > $2 + OR $2 IS NULL + ) +ORDER BY created_at DESC +` + +type GetAllResultLogParams struct { + CreatedBefore pgtype.Timestamp `json:"created_before"` + CreatedAfter pgtype.Timestamp `json:"created_after"` +} + +func (q *Queries) GetAllResultLog(ctx context.Context, arg GetAllResultLogParams) ([]ResultLog, error) { + rows, err := q.db.Query(ctx, GetAllResultLog, arg.CreatedBefore, arg.CreatedAfter) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ResultLog + for rows.Next() { + var i ResultLog + if err := rows.Scan( + &i.ID, + &i.StatusNotFinishedCount, + &i.StatusNotFinishedBets, + &i.StatusToBeFixedCount, + &i.StatusToBeFixedBets, + &i.StatusPostponedCount, + &i.StatusPostponedBets, + &i.StatusEndedCount, + &i.StatusEndedBets, + &i.StatusRemovedCount, + &i.StatusRemovedBets, + &i.RemovedCount, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/domain/bet.go b/internal/domain/bet.go index bc6aae0..b5272ec 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -57,8 +57,8 @@ type BetFilter struct { CashedOut ValidBool IsShopBet ValidBool Query ValidString - CreatedBefore ValidTime - CreatedAfter ValidTime + CreatedBefore ValidTime + CreatedAfter ValidTime } type Flag struct { diff --git a/internal/domain/result.go b/internal/domain/result.go index cd36655..69f74fc 100644 --- a/internal/domain/result.go +++ b/internal/domain/result.go @@ -2,6 +2,8 @@ package domain import ( "time" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" ) type MarketConfig struct { @@ -83,22 +85,78 @@ const ( TIME_STATUS_REMOVED TimeStatus = 99 ) -type ResultStatusCounts struct { - IsNotFinished int `json:"is_not_finished"` - IsNotFinishedBets int `json:"is_not_finished_bets"` - IsToBeFixed int `json:"is_to_be_fixed"` - IsToBeFixedBets int `json:"is_to_be_fixed_bets"` - IsPostponed int `json:"is_postponed"` - IsPostponedBets int `json:"is_postponed_bets"` - IsEnded int `json:"is_ended"` - IsEndedBets int `json:"is_ended_bets"` - IsRemoved int `json:"is_removed"` - IsRemovedBets int `json:"is_removed_bets"` +type ResultLog struct { + ID int64 `json:"id"` + StatusNotFinishedCount int `json:"status_not_finished_count"` + StatusNotFinishedBets int `json:"status_not_finished_bets"` + StatusToBeFixedCount int `json:"status_to_be_fixed_count"` + StatusToBeFixedBets int `json:"status_to_be_fixed_bets"` + StatusPostponedCount int `json:"status_postponed_count"` + StatusPostponedBets int `json:"status_postponed_bets"` + StatusEndedCount int `json:"status_ended_count"` + StatusEndedBets int `json:"status_ended_bets"` + StatusRemovedCount int `json:"status_removed_count"` + StatusRemovedBets int `json:"status_removed_bets"` + RemovedCount int `json:"removed"` + CreatedAt time.Time `json:"created_at"` +} + +type CreateResultLog struct { + StatusNotFinishedCount int `json:"status_not_finished_count"` + StatusNotFinishedBets int `json:"status_not_finished_bets"` + StatusToBeFixedCount int `json:"status_to_be_fixed_count"` + StatusToBeFixedBets int `json:"status_to_be_fixed_bets"` + StatusPostponedCount int `json:"status_postponed_count"` + StatusPostponedBets int `json:"status_postponed_bets"` + StatusEndedCount int `json:"status_ended_count"` + StatusEndedBets int `json:"status_ended_bets"` + StatusRemovedCount int `json:"status_removed_count"` + StatusRemovedBets int `json:"status_removed_bets"` + RemovedCount int `json:"removed"` +} + +type ResultFilter struct { + CreatedBefore ValidTime + CreatedAfter ValidTime } type ResultStatusBets struct { - IsNotFinished []int64 `json:"is_not_finished"` - IsToBeFixed []int64 `json:"is_to_be_fixed"` - IsPostponed []int64 `json:"is_postponed"` - IsEnded []int64 `json:"is_ended"` - IsRemoved []int64 `json:"is_removed"` + StatusNotFinished []int64 `json:"status_not_finished"` + StatusToBeFixed []int64 `json:"status_to_be_fixed"` + StatusPostponed []int64 `json:"status_postponed"` + StatusEnded []int64 `json:"status_ended"` + StatusRemoved []int64 `json:"status_removed"` +} + +func ConvertDBResultLog(result dbgen.ResultLog) ResultLog { + return ResultLog{ + ID: result.ID, + StatusNotFinishedCount: int(result.StatusNotFinishedCount), + StatusNotFinishedBets: int(result.StatusNotFinishedBets), + StatusToBeFixedCount: int(result.StatusToBeFixedCount), + StatusToBeFixedBets: int(result.StatusToBeFixedBets), + StatusPostponedCount: int(result.StatusPostponedCount), + StatusPostponedBets: int(result.StatusPostponedBets), + StatusEndedCount: int(result.StatusEndedCount), + StatusEndedBets: int(result.StatusEndedBets), + StatusRemovedCount: int(result.StatusRemovedCount), + StatusRemovedBets: int(result.StatusRemovedBets), + RemovedCount: int(result.RemovedCount), + CreatedAt: result.CreatedAt.Time, + } +} + +func ConvertCreateResultLog(result CreateResultLog) dbgen.CreateResultLogParams { + return dbgen.CreateResultLogParams{ + StatusNotFinishedCount: int32(result.StatusNotFinishedCount), + StatusNotFinishedBets: int32(result.StatusNotFinishedBets), + StatusToBeFixedCount: int32(result.StatusToBeFixedCount), + StatusToBeFixedBets: int32(result.StatusToBeFixedBets), + StatusPostponedCount: int32(result.StatusPostponedCount), + StatusPostponedBets: int32(result.StatusPostponedBets), + StatusEndedCount: int32(result.StatusEndedCount), + StatusEndedBets: int32(result.StatusEndedBets), + StatusRemovedCount: int32(result.StatusRemovedCount), + StatusRemovedBets: int32(result.StatusRemovedBets), + RemovedCount: int32(result.RemovedCount), + } } diff --git a/internal/repository/result.go b/internal/repository/result.go index c7c7685..ed4aa89 100644 --- a/internal/repository/result.go +++ b/internal/repository/result.go @@ -8,93 +8,34 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -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 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}, - } -} -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}, - } -} -func (s *Store) CreateResult(ctx context.Context, result domain.CreateResult) (domain.Result, error) { - dbResult, err := s.queries.CreateResult(ctx, convertCreateResult(result)) +func (s *Store) CreateResultLog(ctx context.Context, result domain.CreateResultLog) (domain.ResultLog, error) { + dbResult, err := s.queries.CreateResultLog(ctx, domain.ConvertCreateResultLog(result)) if err != nil { - return domain.Result{}, err + return domain.ResultLog{}, err } - return convertDBResult(dbResult), nil + return domain.ConvertDBResultLog(dbResult), nil } -func (s *Store) InsertResult(ctx context.Context, result domain.Result) error { - return s.queries.InsertResult(ctx, convertResult(result)) -} - -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) +func (s *Store) GetAllResultLog(ctx context.Context, filter domain.ResultFilter) ([]domain.ResultLog, error) { + dbResultLogs, err := s.queries.GetAllResultLog(ctx, dbgen.GetAllResultLogParams{ + CreatedBefore: pgtype.Timestamp{ + Time: filter.CreatedBefore.Value, + Valid: filter.CreatedBefore.Valid, + }, + CreatedAfter: pgtype.Timestamp{ + Time: filter.CreatedAfter.Value, + Valid: filter.CreatedAfter.Valid, + }, + }) 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, - }) + result := make([]domain.ResultLog, 0, len(dbResultLogs)) + for _, dbResultLog := range dbResultLogs { + result = append(result, domain.ConvertDBResultLog(dbResultLog)) } - return outcomes, nil + return result, nil } diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index 573c207..1ac84bf 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -987,6 +987,8 @@ func (s *Service) SendWinningStatusNotification(ctx context.Context, status doma betNotification := &domain.Notification{ RecipientID: userID, + DeliveryStatus: domain.DeliveryStatusPending, + IsRead: false, Type: domain.NOTIFICATION_TYPE_BET_RESULT, Level: domain.NotificationLevelSuccess, Reciever: domain.NotificationRecieverSideCustomer, @@ -1028,6 +1030,8 @@ func (s *Service) SendLosingStatusNotification(ctx context.Context, status domai betNotification := &domain.Notification{ RecipientID: userID, + DeliveryStatus: domain.DeliveryStatusPending, + IsRead: false, Type: domain.NOTIFICATION_TYPE_BET_RESULT, Level: domain.NotificationLevelSuccess, Reciever: domain.NotificationRecieverSideCustomer, @@ -1070,6 +1074,8 @@ func (s *Service) SendErrorStatusNotification(ctx context.Context, status domain betNotification := &domain.Notification{ RecipientID: userID, + DeliveryStatus: domain.DeliveryStatusPending, + IsRead: false, Type: domain.NOTIFICATION_TYPE_BET_RESULT, Level: domain.NotificationLevelSuccess, Reciever: domain.NotificationRecieverSideCustomer, @@ -1104,11 +1110,15 @@ func (s *Service) SendAdminErrorAlertNotification(ctx context.Context, status do switch status { case domain.OUTCOME_STATUS_ERROR, domain.OUTCOME_STATUS_PENDING: - headline = "There was an error with your bet" - message = "We have encounter an error with your bet. We will fix it as soon as we can" + headline = "There was an error processing bet" + message = "We have encounter an error with bet. We will fix it as soon as we can" } + errorSeverity := domain.NotificationErrorSeverityHigh betNotification := &domain.Notification{ + ErrorSeverity: &errorSeverity, + DeliveryStatus: domain.DeliveryStatusPending, + IsRead: false, Type: domain.NOTIFICATION_TYPE_BET_RESULT, Level: domain.NotificationLevelSuccess, Reciever: domain.NotificationRecieverSideCustomer, diff --git a/internal/services/result/port.go b/internal/services/result/port.go index 4035319..8890385 100644 --- a/internal/services/result/port.go +++ b/internal/services/result/port.go @@ -2,9 +2,16 @@ package result import ( "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) type ResultService interface { FetchAndProcessResults(ctx context.Context) error FetchAndStoreResult(ctx context.Context, eventID string) error } + +type ResultLogStore interface { + CreateResultLog(ctx context.Context, result domain.CreateResultLog) (domain.ResultLog, error) + GetAllResultLog(ctx context.Context, filter domain.ResultFilter) ([]domain.ResultLog, error) +} diff --git a/internal/services/result/service.go b/internal/services/result/service.go index a3db262..cc2138a 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -264,9 +264,8 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { return err } - removed := 0 empty_sport_id := make([]int64, 0) - var resultStatusCounts domain.ResultStatusCounts + var resultLog domain.CreateResultLog var resultStatusBets domain.ResultStatusBets for _, event := range events { @@ -283,7 +282,6 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { result, err := s.fetchResult(ctx, eventID) if err != nil { if err == ErrEventIsNotActive { - s.logger.Warn("Event is not active", "event_id", eventID, "error", err) s.mongoLogger.Warn( "Event is not active", zap.Int64("eventID", eventID), @@ -329,25 +327,42 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { // Admin users will be able to review the events switch timeStatusParsed { case int64(domain.TIME_STATUS_NOT_STARTED), int64(domain.TIME_STATUS_IN_PLAY): - resultStatusCounts.IsNotFinished += 1 + resultLog.StatusNotFinishedCount += 1 bets, err := s.GetTotalBetsForEvents(ctx, eventID) if err != nil { continue } - resultStatusCounts.IsNotFinishedBets = len(bets) + resultLog.StatusNotFinishedBets = len(bets) for k := range bets { - resultStatusBets.IsNotFinished = append(resultStatusBets.IsNotFinished, k) + resultStatusBets.StatusNotFinished = append(resultStatusBets.StatusNotFinished, k) } case int64(domain.TIME_STATUS_TO_BE_FIXED): - resultStatusCounts.IsToBeFixed += 1 - bets, err := s.GetTotalBetsForEvents(ctx, eventID) + totalBetsRefunded, err := s.RefundAllOutcomes(ctx, eventID) + + err = s.repo.DeleteEvent(ctx, event.ID) if err != nil { + s.mongoLogger.Error( + "Failed to remove event", + zap.Int64("eventID", eventID), + zap.Error(err), + ) continue } - resultStatusCounts.IsToBeFixedBets = len(bets) - for k := range bets { - resultStatusBets.IsNotFinished = append(resultStatusBets.IsNotFinished, k) + err = s.repo.DeleteOddsForEvent(ctx, event.ID) + if err != nil { + s.mongoLogger.Error( + "Failed to remove odds for event", + zap.Int64("eventID", eventID), + zap.Error(err), + ) + continue + } + resultLog.RemovedCount += 1 + resultLog.StatusToBeFixedCount += 1 + resultLog.StatusToBeFixedBets = len(totalBetsRefunded) + for k := range totalBetsRefunded { + resultStatusBets.StatusToBeFixed = append(resultStatusBets.StatusToBeFixed, k) } // s.mongoLogger.Warn( // "Event needs to be rescheduled or corrected", @@ -355,14 +370,16 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { // zap.Error(err), // ) case int64(domain.TIME_STATUS_POSTPONED), int64(domain.TIME_STATUS_SUSPENDED): - resultStatusCounts.IsPostponed += 1 + bets, err := s.GetTotalBetsForEvents(ctx, eventID) if err != nil { continue } - resultStatusCounts.IsPostponed = len(bets) + + resultLog.StatusPostponedCount += 1 + resultLog.StatusPostponedBets = len(bets) for k := range bets { - resultStatusBets.IsNotFinished = append(resultStatusBets.IsNotFinished, k) + resultStatusBets.StatusPostponed = append(resultStatusBets.StatusPostponed, k) } // s.mongoLogger.Warn( // "Event has been temporarily postponed", @@ -409,15 +426,15 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { ) continue } - removed += 1 - resultStatusCounts.IsEnded += 1 + resultLog.RemovedCount += 1 + resultLog.StatusEndedCount += 1 bets, err := s.GetTotalBetsForEvents(ctx, eventID) if err != nil { continue } - resultStatusCounts.IsEndedBets = len(bets) + resultLog.StatusEndedBets = len(bets) for k := range bets { - resultStatusBets.IsNotFinished = append(resultStatusBets.IsNotFinished, k) + resultStatusBets.StatusEnded = append(resultStatusBets.StatusEnded, k) } case int64(domain.TIME_STATUS_ABANDONED), int64(domain.TIME_STATUS_CANCELLED), int64(domain.TIME_STATUS_REMOVED): // s.SendAdminResultStatusErrorNotification( @@ -451,59 +468,126 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { ) continue } - removed += 1 - resultStatusCounts.IsRemoved += 1 - resultStatusCounts.IsRemovedBets = len(totalBetsRefunded) + resultLog.RemovedCount += 1 + resultLog.StatusRemovedCount += 1 + resultLog.StatusRemovedBets = len(totalBetsRefunded) for k := range totalBetsRefunded { - resultStatusBets.IsNotFinished = append(resultStatusBets.IsNotFinished, k) + resultStatusBets.StatusRemoved = append(resultStatusBets.StatusRemoved, k) } } } - s.SendAdminResultStatusErrorNotification( - ctx, - resultStatusCounts, - ) + // This will be used to send daily notifications, since events will be removed + _, err = s.repo.CreateResultLog(ctx, resultLog) + if err != nil { + s.mongoLogger.Warn( + "Failed to store result log", + zap.Error(err), + ) + } var logMessage string - if resultStatusCounts.IsNotFinished != 0 || resultStatusCounts.IsPostponed != 0 || - resultStatusCounts.IsRemoved != 0 || resultStatusCounts.IsToBeFixed != 0 { - logMessage = "Completed processed results with issues" + if resultLog.StatusNotFinishedCount != 0 || resultLog.StatusPostponedCount != 0 || + resultLog.StatusRemovedCount != 0 || resultLog.StatusToBeFixedCount != 0 { + logMessage = "Completed processing results with issues" } else { logMessage = "Successfully processed results with no issues" } s.mongoLogger.Info( logMessage, - zap.Int("number_of_removed_events", removed), + zap.Int("number_of_removed_events", resultLog.RemovedCount), zap.Int("total_expired_events", len(events)), zap.Any("events_with_empty_sport_id", empty_sport_id), - zap.Any("result status counts", resultStatusCounts), + zap.Any("result status counts", resultLog), zap.Any("bets by event status", resultStatusBets), ) return nil } -func buildHeadlineAndMessage(counts domain.ResultStatusCounts) (string, string) { - totalIssues := counts.IsNotFinished + counts.IsToBeFixed + counts.IsPostponed + counts.IsRemoved +func (s *Service) CheckAndSendResultNotifications(ctx context.Context, createdAfter time.Time) error { + + resultLog, err := s.repo.GetAllResultLog(ctx, domain.ResultFilter{ + CreatedAfter: domain.ValidTime{ + Value: createdAfter, + Valid: true, + }, + }) + + if err != nil { + s.mongoLogger.Error( + "Failed to get result log", + zap.Time("CreatedAfter", createdAfter), + zap.Error(err), + ) + return err + } + + if len(resultLog) == 0 { + s.mongoLogger.Info( + "No results found for check and send result notification", + zap.Time("CreatedAfter", createdAfter), + ) + return nil + } + + totalResultLog := domain.ResultLog{ + StatusNotFinishedCount: resultLog[0].StatusNotFinishedCount, + StatusPostponedCount: resultLog[0].StatusPostponedCount, + } + for _, log := range resultLog { + // Add all the bets + totalResultLog.StatusNotFinishedBets += log.StatusNotFinishedBets + totalResultLog.StatusPostponedBets += log.StatusPostponedBets + totalResultLog.StatusToBeFixedBets += log.StatusToBeFixedBets + totalResultLog.StatusRemovedBets += log.StatusRemovedBets + totalResultLog.StatusEndedBets += log.StatusEndedBets + + totalResultLog.StatusToBeFixedCount += log.StatusToBeFixedCount + totalResultLog.StatusRemovedCount += log.StatusRemovedCount + totalResultLog.StatusEndedCount += log.StatusEndedCount + totalResultLog.RemovedCount += log.RemovedCount + } + + err = s.SendAdminResultStatusErrorNotification(ctx, totalResultLog) + if err != nil { + s.mongoLogger.Error( + "Failed to send admin result status notification", + zap.Time("CreatedAfter", createdAfter), + zap.Error(err), + ) + return err + } + + return nil +} + +func buildHeadlineAndMessage(counts domain.ResultLog) (string, string) { + totalIssues := counts.StatusNotFinishedCount + counts.StatusToBeFixedCount + counts.StatusPostponedCount + counts.StatusRemovedCount + totalBets := counts.StatusEndedBets + counts.StatusNotFinishedBets + counts.StatusPostponedBets + counts.StatusRemovedBets + counts.StatusToBeFixedBets if totalIssues == 0 { - return "✅ Event Results Processed", "All event results were processed successfully. No issues detected." + return "✅ Successfully Processed Event Results", fmt.Sprintf( + "%d total ended events with %d total bets. No issues detected", counts.StatusEndedCount, totalBets, + ) } parts := []string{} - if counts.IsNotFinished > 0 { - parts = append(parts, fmt.Sprintf("%d unfinished", counts.IsNotFinished)) + if counts.StatusNotFinishedCount > 0 { + parts = append(parts, fmt.Sprintf("%d unfinished with %d bets", counts.StatusNotFinishedCount, counts.StatusNotFinishedBets)) } - if counts.IsToBeFixed > 0 { - parts = append(parts, fmt.Sprintf("%d to-fix", counts.IsToBeFixed)) + if counts.StatusToBeFixedCount > 0 { + parts = append(parts, fmt.Sprintf("%d to-fix with %d bets", counts.StatusToBeFixedCount, counts.StatusToBeFixedBets)) } - if counts.IsPostponed > 0 { - parts = append(parts, fmt.Sprintf("%d postponed", counts.IsPostponed)) + if counts.StatusPostponedCount > 0 { + parts = append(parts, fmt.Sprintf("%d postponed with %d bets", counts.StatusPostponedCount, counts.StatusPostponedBets)) } - if counts.IsRemoved > 0 { - parts = append(parts, fmt.Sprintf("%d removed", counts.IsRemoved)) + if counts.StatusRemovedCount > 0 { + parts = append(parts, fmt.Sprintf("%d removed with %d bets", counts.StatusRemovedCount, counts.StatusRemovedBets)) + } + if counts.StatusEndedCount > 0 { + parts = append(parts, fmt.Sprintf("%d ended with %d bets", counts.StatusEndedCount, counts.StatusEndedBets)) } headline := "⚠️ Issues Found Processing Event Results" @@ -513,7 +597,7 @@ func buildHeadlineAndMessage(counts domain.ResultStatusCounts) (string, string) func (s *Service) SendAdminResultStatusErrorNotification( ctx context.Context, - counts domain.ResultStatusCounts, + counts domain.ResultLog, ) error { superAdmins, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{ @@ -558,6 +642,14 @@ func (s *Service) SendAdminResultStatusErrorNotification( ) sendErrors = append(sendErrors, err) } + notification.DeliveryChannel = domain.DeliveryChannelEmail + if err := s.notificationSvc.SendNotification(ctx, notification); err != nil { + s.mongoLogger.Error("failed to send admin email notification", + zap.Int64("admin_id", user.ID), + zap.Error(err), + ) + sendErrors = append(sendErrors, err) + } } if len(sendErrors) > 0 { diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index bbb8f67..3dba6e9 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -51,19 +51,19 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S // } // }, // }, - // { - // spec: "0 */5 * * * *", // Every 5 Minutes - // task: func() { - // mongoLogger.Info("Began updating all expired events status") - // if _, err := resultService.CheckAndUpdateExpiredEvents(context.Background()); err != nil { - // mongoLogger.Error("Failed to update expired events status", - // zap.Error(err), - // ) - // } else { - // mongoLogger.Info("Successfully updated expired events") - // } - // }, - // }, + { + spec: "0 */5 * * * *", // Every 5 Minutes + task: func() { + mongoLogger.Info("Began updating all expired events status") + if _, err := resultService.CheckAndUpdateExpiredEvents(context.Background()); err != nil { + mongoLogger.Error("Failed to update expired events status", + zap.Error(err), + ) + } else { + mongoLogger.Info("Successfully updated expired events") + } + }, + }, { spec: "0 */15 * * * *", // Every 15 Minutes task: func() { @@ -77,6 +77,19 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S } }, }, + { + spec: "0 0 * * * *", // Every Day + task: func() { + mongoLogger.Info("Send daily result notification") + if err := resultService.CheckAndSendResultNotifications(context.Background(), time.Now().Add(-24*time.Hour)); err != nil { + mongoLogger.Error("Failed to process result", + zap.Error(err), + ) + } else { + mongoLogger.Info("Successfully processed all event result outcomes") + } + }, + }, } for _, job := range schedule { From c08b7868033657aa69c0bef30200f31090c94e22 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Thu, 7 Aug 2025 19:04:54 +0300 Subject: [PATCH 02/10] feat: odd and event history --- .vscode/settings.json | 3 + cmd/main.go | 2 +- db/migrations/000001_fortune.up.sql | 19 +- db/query/event_history.sql | 36 ++++ db/query/events.sql | 10 +- db/query/odd_history.sql | 67 ++++++ db/query/odds.sql | 40 +--- gen/db/event_history.sql.go | 133 ++++++++++++ gen/db/events.sql.go | 57 +++-- gen/db/models.go | 21 +- gen/db/odd_history.sql.go | 201 ++++++++++++++++++ gen/db/odds.sql.go | 159 +++++--------- internal/domain/bet.go | 4 +- internal/domain/common.go | 101 +++++++-- internal/domain/event.go | 11 +- internal/domain/event_history.go | 41 ++++ internal/domain/odds.go | 46 ++-- internal/domain/odds_history.go | 56 +++++ internal/repository/branch.go | 26 +-- internal/repository/event.go | 26 ++- internal/repository/event_history.go | 55 +++++ internal/repository/odd_history.go | 60 ++++++ internal/repository/odds.go | 43 ++-- internal/services/event/port.go | 4 +- internal/services/event/service.go | 94 +++++++- internal/services/odds/port.go | 3 + internal/services/odds/service.go | 121 ++++++++++- internal/web_server/handlers/event_handler.go | 2 +- internal/web_server/routes.go | 2 +- 29 files changed, 1160 insertions(+), 283 deletions(-) create mode 100644 db/query/event_history.sql create mode 100644 db/query/odd_history.sql create mode 100644 gen/db/event_history.sql.go create mode 100644 gen/db/odd_history.sql.go create mode 100644 internal/domain/event_history.go create mode 100644 internal/domain/odds_history.go create mode 100644 internal/repository/event_history.go create mode 100644 internal/repository/odd_history.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 2e648fd..9228e0e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,11 @@ { "cSpell.words": [ "Cashout", + "dbgen", + "jackc", "narg", "notificationservice", + "pgtype", "sqlc" ], "cSpell.enabledFileTypes": { diff --git a/cmd/main.go b/cmd/main.go index a39974d..07fe744 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -110,7 +110,7 @@ func main() { authSvc := authentication.NewService(store, store, cfg.RefreshExpiry) userSvc := user.NewService(store, store, messengerSvc, cfg) eventSvc := event.New(cfg.Bet365Token, store, domain.MongoDBLogger) - oddsSvc := odds.New(store, cfg, logger, domain.MongoDBLogger) + oddsSvc := odds.New(store, cfg, eventSvc, logger, domain.MongoDBLogger) notificationRepo := repository.NewNotificationRepository(store) virtuaGamesRepo := repository.NewVirtualGameRepository(store) notificationSvc := notificationservice.New(notificationRepo, domain.MongoDBLogger, logger, cfg, messengerSvc, userSvc) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index d7fec37..28ce27c 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -264,11 +264,17 @@ CREATE TABLE events ( fetched_at TIMESTAMP DEFAULT now(), source TEXT DEFAULT 'b365api', is_featured BOOLEAN NOT NULL DEFAULT FALSE, - is_monitorred BOOLEAN NOT NULL DEFAULT FALSE, + is_monitored BOOLEAN NOT NULL DEFAULT FALSE, is_active BOOLEAN NOT NULL DEFAULT TRUE ); +CREATE TABLE event_history ( + id BIGSERIAL PRIMARY KEY, + event_id TEXT NOT NULL, + status TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); CREATE TABLE odds ( - id SERIAL PRIMARY KEY, + id BIGSERIAL PRIMARY KEY, event_id TEXT, fi TEXT, market_type TEXT NOT NULL, @@ -288,6 +294,15 @@ CREATE TABLE odds ( UNIQUE (event_id, market_id, name, handicap), UNIQUE (event_id, market_id) ); +CREATE TABLE odd_history ( + id BIGSERIAL PRIMARY KEY, + odd_id BIGINT NOT NULL, + raw_odd_id BIGINT NOT NULL, + market_id TEXT NOT NULL, + event_id TEXT NOT NULL, + odd_value DOUBLE PRECISION NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); CREATE TABLE result_log ( id BIGSERIAL PRIMARY KEY, status_not_finished_count INT NOT NULL, diff --git a/db/query/event_history.sql b/db/query/event_history.sql new file mode 100644 index 0000000..3739838 --- /dev/null +++ b/db/query/event_history.sql @@ -0,0 +1,36 @@ +-- name: InsertEventHistory :one +INSERT INTO event_history (event_id, status) +VALUES ($1, $2) +RETURNING *; +-- name: GetAllEventHistory :many +SELECT * +FROM event_history +WHERE ( + event_id = sqlc.narg('event_id') + OR sqlc.narg('event_id') IS NULL + ) + AND ( + created_at > sqlc.narg('created_before') + OR sqlc.narg('created_before') IS NULL + ) + AND ( + created_at < sqlc.narg('created_after') + OR sqlc.narg('created_after') IS NULL + ); +-- name: GetInitialEventPerDay :many +SELECT DISTINCT ON (DATE_TRUNC('day', created_at)) * +FROM event_history +WHERE ( + event_id = sqlc.narg('event_id') + OR sqlc.narg('event_id') IS NULL + ) + AND ( + created_at > sqlc.narg('created_before') + OR sqlc.narg('created_before') IS NULL + ) + AND ( + created_at < sqlc.narg('created_after') + OR sqlc.narg('created_after') IS NULL + ) +ORDER BY DATE_TRUNC('day', created_at), + created_at ASC; \ No newline at end of file diff --git a/db/query/events.sql b/db/query/events.sql index b88f6d8..893081e 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -229,10 +229,18 @@ UPDATE events SET score = $1, status = $2 WHERE id = $3; --- name: UpdateFeatured :exec +-- name: UpdateEventFeatured :exec UPDATE events SET is_featured = $1 WHERE id = $2; +-- name: IsEventMonitored :one +SELECT is_monitored +FROM events +WHERE id = $1; +-- name: UpdateEventMonitored :exec +UPDATE events +SET is_monitored = $1 +WHERE id = $2; -- name: DeleteEvent :exec DELETE FROM events WHERE id = $1; \ No newline at end of file diff --git a/db/query/odd_history.sql b/db/query/odd_history.sql new file mode 100644 index 0000000..fb9284f --- /dev/null +++ b/db/query/odd_history.sql @@ -0,0 +1,67 @@ +-- name: InsertOddHistory :one +INSERT INTO odd_history ( + odd_id, + market_id, + raw_odd_id, + event_id, + odd_value + ) +VALUES ($1, $2, $3, $4, $5) +RETURNING *; +-- name: GetAllOddHistory :many +SELECT * +FROM odd_history +WHERE ( + odd_id = sqlc.narg('odd_id') + OR sqlc.narg('odd_id') IS NULL + ) + AND ( + market_id = sqlc.narg('market_id') + OR sqlc.narg('market_id') IS NULL + ) + AND ( + raw_odd_id = sqlc.narg('raw_odd_id') + OR sqlc.narg('raw_odd_id') IS NULL + ) + AND ( + event_id = sqlc.narg('event_id') + OR sqlc.narg('event_id') IS NULL + ) + AND ( + created_at > sqlc.narg('created_before') + OR sqlc.narg('created_before') IS NULL + ) + AND ( + created_at < sqlc.narg('created_after') + OR sqlc.narg('created_after') IS NULL + ); + +-- name: GetInitialOddPerDay :many +SELECT DISTINCT ON (DATE_TRUNC('day', created_at)) * +FROM odd_history +WHERE ( + odd_id = sqlc.narg('odd_id') + OR sqlc.narg('odd_id') IS NULL + ) + AND ( + market_id = sqlc.narg('market_id') + OR sqlc.narg('market_id') IS NULL + ) + AND ( + raw_odd_id = sqlc.narg('raw_odd_id') + OR sqlc.narg('raw_odd_id') IS NULL + ) + AND ( + event_id = sqlc.narg('event_id') + OR sqlc.narg('event_id') IS NULL + ) + AND ( + created_at > sqlc.narg('created_before') + OR sqlc.narg('created_before') IS NULL + ) + AND ( + created_at < sqlc.narg('created_after') + OR sqlc.narg('created_after') IS NULL + ) +ORDER BY DATE_TRUNC('day', created_at), + created_at ASC; \ No newline at end of file diff --git a/db/query/odds.sql b/db/query/odds.sql index bdabb44..c48097b 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -46,49 +46,17 @@ SET odds_value = EXCLUDED.odds_value, source = EXCLUDED.source, fi = EXCLUDED.fi; -- name: GetPrematchOdds :many -SELECT event_id, - fi, - market_type, - market_name, - market_category, - market_id, - name, - handicap, - odds_value, - section, - category, - raw_odds, - fetched_at, - source, - is_active +SELECT * FROM odds WHERE is_active = true AND source = 'bet365'; -- name: GetALLPrematchOdds :many -SELECT event_id, - fi, - market_type, - market_name, - market_category, - market_id, - name, - handicap, - odds_value, - section, - category, - raw_odds, - fetched_at, - source, - is_active +SELECT * FROM odds WHERE is_active = true AND source = 'bet365'; --- name: GetRawOddsByMarketID :one -SELECT id, - market_name, - handicap, - raw_odds, - fetched_at +-- name: GetOddsByMarketID :one +SELECT * FROM odds WHERE market_id = $1 AND fi = $2 diff --git a/gen/db/event_history.sql.go b/gen/db/event_history.sql.go new file mode 100644 index 0000000..ab29359 --- /dev/null +++ b/gen/db/event_history.sql.go @@ -0,0 +1,133 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: event_history.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const GetAllEventHistory = `-- name: GetAllEventHistory :many +SELECT id, event_id, status, created_at +FROM event_history +WHERE ( + event_id = $1 + OR $1 IS NULL + ) + AND ( + created_at > $2 + OR $2 IS NULL + ) + AND ( + created_at < $3 + OR $3 IS NULL + ) +` + +type GetAllEventHistoryParams struct { + EventID pgtype.Text `json:"event_id"` + CreatedBefore pgtype.Timestamp `json:"created_before"` + CreatedAfter pgtype.Timestamp `json:"created_after"` +} + +func (q *Queries) GetAllEventHistory(ctx context.Context, arg GetAllEventHistoryParams) ([]EventHistory, error) { + rows, err := q.db.Query(ctx, GetAllEventHistory, arg.EventID, arg.CreatedBefore, arg.CreatedAfter) + if err != nil { + return nil, err + } + defer rows.Close() + var items []EventHistory + for rows.Next() { + var i EventHistory + if err := rows.Scan( + &i.ID, + &i.EventID, + &i.Status, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetInitialEventPerDay = `-- name: GetInitialEventPerDay :many +SELECT DISTINCT ON (DATE_TRUNC('day', created_at)) id, event_id, status, created_at +FROM event_history +WHERE ( + event_id = $1 + OR $1 IS NULL + ) + AND ( + created_at > $2 + OR $2 IS NULL + ) + AND ( + created_at < $3 + OR $3 IS NULL + ) +ORDER BY DATE_TRUNC('day', created_at), + created_at ASC +` + +type GetInitialEventPerDayParams struct { + EventID pgtype.Text `json:"event_id"` + CreatedBefore pgtype.Timestamp `json:"created_before"` + CreatedAfter pgtype.Timestamp `json:"created_after"` +} + +func (q *Queries) GetInitialEventPerDay(ctx context.Context, arg GetInitialEventPerDayParams) ([]EventHistory, error) { + rows, err := q.db.Query(ctx, GetInitialEventPerDay, arg.EventID, arg.CreatedBefore, arg.CreatedAfter) + if err != nil { + return nil, err + } + defer rows.Close() + var items []EventHistory + for rows.Next() { + var i EventHistory + if err := rows.Scan( + &i.ID, + &i.EventID, + &i.Status, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const InsertEventHistory = `-- name: InsertEventHistory :one +INSERT INTO event_history (event_id, status) +VALUES ($1, $2) +RETURNING id, event_id, status, created_at +` + +type InsertEventHistoryParams struct { + EventID string `json:"event_id"` + Status string `json:"status"` +} + +func (q *Queries) InsertEventHistory(ctx context.Context, arg InsertEventHistoryParams) (EventHistory, error) { + row := q.db.QueryRow(ctx, InsertEventHistory, arg.EventID, arg.Status) + var i EventHistory + err := row.Scan( + &i.ID, + &i.EventID, + &i.Status, + &i.CreatedAt, + ) + return i, err +} diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index da93861..423354b 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -22,7 +22,7 @@ func (q *Queries) DeleteEvent(ctx context.Context, id string) error { } const GetAllUpcomingEvents = `-- name: GetAllUpcomingEvents :many -SELECT 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, fetched_at, source, is_featured, is_monitorred, is_active +SELECT 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, fetched_at, source, is_featured, is_monitored, is_active FROM events WHERE start_time > now() AND is_live = false @@ -63,7 +63,7 @@ func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]Event, error) { &i.FetchedAt, &i.Source, &i.IsFeatured, - &i.IsMonitorred, + &i.IsMonitored, &i.IsActive, ); err != nil { return nil, err @@ -77,7 +77,7 @@ func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]Event, error) { } const GetExpiredUpcomingEvents = `-- name: GetExpiredUpcomingEvents :many -SELECT events.id, events.sport_id, events.match_name, events.home_team, events.away_team, events.home_team_id, events.away_team_id, events.home_kit_image, events.away_kit_image, events.league_id, events.league_name, events.league_cc, events.start_time, events.score, events.match_minute, events.timer_status, events.added_time, events.match_period, events.is_live, events.status, events.fetched_at, events.source, events.is_featured, events.is_monitorred, events.is_active, +SELECT events.id, events.sport_id, events.match_name, events.home_team, events.away_team, events.home_team_id, events.away_team_id, events.home_kit_image, events.away_kit_image, events.league_id, events.league_name, events.league_cc, events.start_time, events.score, events.match_minute, events.timer_status, events.added_time, events.match_period, events.is_live, events.status, events.fetched_at, events.source, events.is_featured, events.is_monitored, events.is_active, leagues.country_code as league_cc FROM events LEFT JOIN leagues ON leagues.id = league_id @@ -113,7 +113,7 @@ type GetExpiredUpcomingEventsRow struct { FetchedAt pgtype.Timestamp `json:"fetched_at"` Source pgtype.Text `json:"source"` IsFeatured bool `json:"is_featured"` - IsMonitorred bool `json:"is_monitorred"` + IsMonitored bool `json:"is_monitored"` IsActive bool `json:"is_active"` LeagueCc_2 pgtype.Text `json:"league_cc_2"` } @@ -151,7 +151,7 @@ func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context, status pgtype.Te &i.FetchedAt, &i.Source, &i.IsFeatured, - &i.IsMonitorred, + &i.IsMonitored, &i.IsActive, &i.LeagueCc_2, ); err != nil { @@ -166,7 +166,7 @@ func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context, status pgtype.Te } const GetPaginatedUpcomingEvents = `-- name: GetPaginatedUpcomingEvents :many -SELECT events.id, events.sport_id, events.match_name, events.home_team, events.away_team, events.home_team_id, events.away_team_id, events.home_kit_image, events.away_kit_image, events.league_id, events.league_name, events.league_cc, events.start_time, events.score, events.match_minute, events.timer_status, events.added_time, events.match_period, events.is_live, events.status, events.fetched_at, events.source, events.is_featured, events.is_monitorred, events.is_active, +SELECT events.id, events.sport_id, events.match_name, events.home_team, events.away_team, events.home_team_id, events.away_team_id, events.home_kit_image, events.away_kit_image, events.league_id, events.league_name, events.league_cc, events.start_time, events.score, events.match_minute, events.timer_status, events.added_time, events.match_period, events.is_live, events.status, events.fetched_at, events.source, events.is_featured, events.is_monitored, events.is_active, leagues.country_code as league_cc FROM events LEFT JOIN leagues ON leagues.id = league_id @@ -242,7 +242,7 @@ type GetPaginatedUpcomingEventsRow struct { FetchedAt pgtype.Timestamp `json:"fetched_at"` Source pgtype.Text `json:"source"` IsFeatured bool `json:"is_featured"` - IsMonitorred bool `json:"is_monitorred"` + IsMonitored bool `json:"is_monitored"` IsActive bool `json:"is_active"` LeagueCc_2 pgtype.Text `json:"league_cc_2"` } @@ -290,7 +290,7 @@ func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginat &i.FetchedAt, &i.Source, &i.IsFeatured, - &i.IsMonitorred, + &i.IsMonitored, &i.IsActive, &i.LeagueCc_2, ); err != nil { @@ -367,7 +367,7 @@ func (q *Queries) GetTotalEvents(ctx context.Context, arg GetTotalEventsParams) } const GetUpcomingByID = `-- name: GetUpcomingByID :one -SELECT 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, fetched_at, source, is_featured, is_monitorred, is_active +SELECT 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, fetched_at, source, is_featured, is_monitored, is_active FROM events WHERE id = $1 AND is_live = false @@ -402,7 +402,7 @@ func (q *Queries) GetUpcomingByID(ctx context.Context, id string) (Event, error) &i.FetchedAt, &i.Source, &i.IsFeatured, - &i.IsMonitorred, + &i.IsMonitored, &i.IsActive, ) return i, err @@ -623,6 +623,19 @@ func (q *Queries) InsertUpcomingEvent(ctx context.Context, arg InsertUpcomingEve return err } +const IsEventMonitored = `-- name: IsEventMonitored :one +SELECT is_monitored +FROM events +WHERE id = $1 +` + +func (q *Queries) IsEventMonitored(ctx context.Context, id string) (bool, error) { + row := q.db.QueryRow(ctx, IsEventMonitored, id) + var is_monitored bool + err := row.Scan(&is_monitored) + return is_monitored, err +} + const ListLiveEvents = `-- name: ListLiveEvents :many SELECT id FROM events @@ -649,19 +662,35 @@ func (q *Queries) ListLiveEvents(ctx context.Context) ([]string, error) { return items, nil } -const UpdateFeatured = `-- name: UpdateFeatured :exec +const UpdateEventFeatured = `-- name: UpdateEventFeatured :exec UPDATE events SET is_featured = $1 WHERE id = $2 ` -type UpdateFeaturedParams struct { +type UpdateEventFeaturedParams struct { IsFeatured bool `json:"is_featured"` ID string `json:"id"` } -func (q *Queries) UpdateFeatured(ctx context.Context, arg UpdateFeaturedParams) error { - _, err := q.db.Exec(ctx, UpdateFeatured, arg.IsFeatured, arg.ID) +func (q *Queries) UpdateEventFeatured(ctx context.Context, arg UpdateEventFeaturedParams) error { + _, err := q.db.Exec(ctx, UpdateEventFeatured, arg.IsFeatured, arg.ID) + return err +} + +const UpdateEventMonitored = `-- name: UpdateEventMonitored :exec +UPDATE events +SET is_monitored = $1 +WHERE id = $2 +` + +type UpdateEventMonitoredParams struct { + IsMonitored bool `json:"is_monitored"` + ID string `json:"id"` +} + +func (q *Queries) UpdateEventMonitored(ctx context.Context, arg UpdateEventMonitoredParams) error { + _, err := q.db.Exec(ctx, UpdateEventMonitored, arg.IsMonitored, arg.ID) return err } diff --git a/gen/db/models.go b/gen/db/models.go index 8cad748..7435b97 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -258,10 +258,17 @@ type Event struct { FetchedAt pgtype.Timestamp `json:"fetched_at"` Source pgtype.Text `json:"source"` IsFeatured bool `json:"is_featured"` - IsMonitorred bool `json:"is_monitorred"` + IsMonitored bool `json:"is_monitored"` IsActive bool `json:"is_active"` } +type EventHistory struct { + ID int64 `json:"id"` + EventID string `json:"event_id"` + Status string `json:"status"` + CreatedAt pgtype.Timestamp `json:"created_at"` +} + type ExchangeRate struct { ID int32 `json:"id"` FromCurrency string `json:"from_currency"` @@ -316,7 +323,7 @@ type Notification struct { } type Odd struct { - ID int32 `json:"id"` + ID int64 `json:"id"` EventID pgtype.Text `json:"event_id"` Fi pgtype.Text `json:"fi"` MarketType string `json:"market_type"` @@ -334,6 +341,16 @@ type Odd struct { IsActive pgtype.Bool `json:"is_active"` } +type OddHistory struct { + ID int64 `json:"id"` + OddID int64 `json:"odd_id"` + RawOddID int64 `json:"raw_odd_id"` + MarketID string `json:"market_id"` + EventID string `json:"event_id"` + OddValue float64 `json:"odd_value"` + CreatedAt pgtype.Timestamp `json:"created_at"` +} + type Otp struct { ID int64 `json:"id"` SentTo string `json:"sent_to"` diff --git a/gen/db/odd_history.sql.go b/gen/db/odd_history.sql.go new file mode 100644 index 0000000..ecb35d4 --- /dev/null +++ b/gen/db/odd_history.sql.go @@ -0,0 +1,201 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: odd_history.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const GetAllOddHistory = `-- name: GetAllOddHistory :many +SELECT id, odd_id, raw_odd_id, market_id, event_id, odd_value, created_at +FROM odd_history +WHERE ( + odd_id = $1 + OR $1 IS NULL + ) + AND ( + market_id = $2 + OR $2 IS NULL + ) + AND ( + raw_odd_id = $3 + OR $3 IS NULL + ) + AND ( + event_id = $4 + OR $4 IS NULL + ) + AND ( + created_at > $5 + OR $5 IS NULL + ) + AND ( + created_at < $6 + OR $6 IS NULL + ) +` + +type GetAllOddHistoryParams struct { + OddID pgtype.Int8 `json:"odd_id"` + MarketID pgtype.Text `json:"market_id"` + RawOddID pgtype.Int8 `json:"raw_odd_id"` + EventID pgtype.Text `json:"event_id"` + CreatedBefore pgtype.Timestamp `json:"created_before"` + CreatedAfter pgtype.Timestamp `json:"created_after"` +} + +func (q *Queries) GetAllOddHistory(ctx context.Context, arg GetAllOddHistoryParams) ([]OddHistory, error) { + rows, err := q.db.Query(ctx, GetAllOddHistory, + arg.OddID, + arg.MarketID, + arg.RawOddID, + arg.EventID, + arg.CreatedBefore, + arg.CreatedAfter, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []OddHistory + for rows.Next() { + var i OddHistory + if err := rows.Scan( + &i.ID, + &i.OddID, + &i.RawOddID, + &i.MarketID, + &i.EventID, + &i.OddValue, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetInitialOddPerDay = `-- name: GetInitialOddPerDay :many +SELECT DISTINCT ON (DATE_TRUNC('day', created_at)) id, odd_id, raw_odd_id, market_id, event_id, odd_value, created_at +FROM odd_history +WHERE ( + odd_id = $1 + OR $1 IS NULL + ) + AND ( + market_id = $2 + OR $2 IS NULL + ) + AND ( + raw_odd_id = $3 + OR $3 IS NULL + ) + AND ( + event_id = $4 + OR $4 IS NULL + ) + AND ( + created_at > $5 + OR $5 IS NULL + ) + AND ( + created_at < $6 + OR $6 IS NULL + ) +ORDER BY DATE_TRUNC('day', created_at), + created_at ASC +` + +type GetInitialOddPerDayParams struct { + OddID pgtype.Int8 `json:"odd_id"` + MarketID pgtype.Text `json:"market_id"` + RawOddID pgtype.Int8 `json:"raw_odd_id"` + EventID pgtype.Text `json:"event_id"` + CreatedBefore pgtype.Timestamp `json:"created_before"` + CreatedAfter pgtype.Timestamp `json:"created_after"` +} + +func (q *Queries) GetInitialOddPerDay(ctx context.Context, arg GetInitialOddPerDayParams) ([]OddHistory, error) { + rows, err := q.db.Query(ctx, GetInitialOddPerDay, + arg.OddID, + arg.MarketID, + arg.RawOddID, + arg.EventID, + arg.CreatedBefore, + arg.CreatedAfter, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []OddHistory + for rows.Next() { + var i OddHistory + if err := rows.Scan( + &i.ID, + &i.OddID, + &i.RawOddID, + &i.MarketID, + &i.EventID, + &i.OddValue, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const InsertOddHistory = `-- name: InsertOddHistory :one +INSERT INTO odd_history ( + odd_id, + market_id, + raw_odd_id, + event_id, + odd_value + ) +VALUES ($1, $2, $3, $4, $5) +RETURNING id, odd_id, raw_odd_id, market_id, event_id, odd_value, created_at +` + +type InsertOddHistoryParams struct { + OddID int64 `json:"odd_id"` + MarketID string `json:"market_id"` + RawOddID int64 `json:"raw_odd_id"` + EventID string `json:"event_id"` + OddValue float64 `json:"odd_value"` +} + +func (q *Queries) InsertOddHistory(ctx context.Context, arg InsertOddHistoryParams) (OddHistory, error) { + row := q.db.QueryRow(ctx, InsertOddHistory, + arg.OddID, + arg.MarketID, + arg.RawOddID, + arg.EventID, + arg.OddValue, + ) + var i OddHistory + err := row.Scan( + &i.ID, + &i.OddID, + &i.RawOddID, + &i.MarketID, + &i.EventID, + &i.OddValue, + &i.CreatedAt, + ) + return i, err +} diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index cb30007..f0aa0fa 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -22,54 +22,23 @@ func (q *Queries) DeleteOddsForEvent(ctx context.Context, fi pgtype.Text) error } const GetALLPrematchOdds = `-- name: GetALLPrematchOdds :many -SELECT event_id, - fi, - market_type, - market_name, - market_category, - market_id, - name, - handicap, - odds_value, - section, - category, - raw_odds, - fetched_at, - source, - is_active +SELECT id, event_id, fi, market_type, market_name, market_category, market_id, name, handicap, odds_value, section, category, raw_odds, fetched_at, source, is_active FROM odds WHERE is_active = true AND source = 'bet365' ` -type GetALLPrematchOddsRow struct { - EventID pgtype.Text `json:"event_id"` - Fi pgtype.Text `json:"fi"` - MarketType string `json:"market_type"` - MarketName pgtype.Text `json:"market_name"` - MarketCategory pgtype.Text `json:"market_category"` - MarketID pgtype.Text `json:"market_id"` - Name pgtype.Text `json:"name"` - Handicap pgtype.Text `json:"handicap"` - OddsValue pgtype.Float8 `json:"odds_value"` - Section string `json:"section"` - Category pgtype.Text `json:"category"` - RawOdds []byte `json:"raw_odds"` - FetchedAt pgtype.Timestamp `json:"fetched_at"` - Source pgtype.Text `json:"source"` - IsActive pgtype.Bool `json:"is_active"` -} - -func (q *Queries) GetALLPrematchOdds(ctx context.Context) ([]GetALLPrematchOddsRow, error) { +func (q *Queries) GetALLPrematchOdds(ctx context.Context) ([]Odd, error) { rows, err := q.db.Query(ctx, GetALLPrematchOdds) if err != nil { return nil, err } defer rows.Close() - var items []GetALLPrematchOddsRow + var items []Odd for rows.Next() { - var i GetALLPrematchOddsRow + var i Odd if err := rows.Scan( + &i.ID, &i.EventID, &i.Fi, &i.MarketType, @@ -96,6 +65,44 @@ func (q *Queries) GetALLPrematchOdds(ctx context.Context) ([]GetALLPrematchOddsR return items, nil } +const GetOddsByMarketID = `-- name: GetOddsByMarketID :one +SELECT id, event_id, fi, market_type, market_name, market_category, market_id, name, handicap, odds_value, section, category, raw_odds, fetched_at, source, is_active +FROM odds +WHERE market_id = $1 + AND fi = $2 + AND is_active = true + AND source = 'bet365' +` + +type GetOddsByMarketIDParams struct { + MarketID pgtype.Text `json:"market_id"` + Fi pgtype.Text `json:"fi"` +} + +func (q *Queries) GetOddsByMarketID(ctx context.Context, arg GetOddsByMarketIDParams) (Odd, error) { + row := q.db.QueryRow(ctx, GetOddsByMarketID, arg.MarketID, arg.Fi) + var i Odd + err := row.Scan( + &i.ID, + &i.EventID, + &i.Fi, + &i.MarketType, + &i.MarketName, + &i.MarketCategory, + &i.MarketID, + &i.Name, + &i.Handicap, + &i.OddsValue, + &i.Section, + &i.Category, + &i.RawOdds, + &i.FetchedAt, + &i.Source, + &i.IsActive, + ) + return i, err +} + const GetPaginatedPrematchOddsByUpcomingID = `-- name: GetPaginatedPrematchOddsByUpcomingID :many SELECT o.id, o.event_id, o.fi, o.market_type, o.market_name, o.market_category, o.market_id, o.name, o.handicap, o.odds_value, o.section, o.category, o.raw_odds, o.fetched_at, o.source, o.is_active FROM odds o @@ -152,54 +159,23 @@ func (q *Queries) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, arg } const GetPrematchOdds = `-- name: GetPrematchOdds :many -SELECT event_id, - fi, - market_type, - market_name, - market_category, - market_id, - name, - handicap, - odds_value, - section, - category, - raw_odds, - fetched_at, - source, - is_active +SELECT id, event_id, fi, market_type, market_name, market_category, market_id, name, handicap, odds_value, section, category, raw_odds, fetched_at, source, is_active FROM odds WHERE is_active = true AND source = 'bet365' ` -type GetPrematchOddsRow struct { - EventID pgtype.Text `json:"event_id"` - Fi pgtype.Text `json:"fi"` - MarketType string `json:"market_type"` - MarketName pgtype.Text `json:"market_name"` - MarketCategory pgtype.Text `json:"market_category"` - MarketID pgtype.Text `json:"market_id"` - Name pgtype.Text `json:"name"` - Handicap pgtype.Text `json:"handicap"` - OddsValue pgtype.Float8 `json:"odds_value"` - Section string `json:"section"` - Category pgtype.Text `json:"category"` - RawOdds []byte `json:"raw_odds"` - FetchedAt pgtype.Timestamp `json:"fetched_at"` - Source pgtype.Text `json:"source"` - IsActive pgtype.Bool `json:"is_active"` -} - -func (q *Queries) GetPrematchOdds(ctx context.Context) ([]GetPrematchOddsRow, error) { +func (q *Queries) GetPrematchOdds(ctx context.Context) ([]Odd, error) { rows, err := q.db.Query(ctx, GetPrematchOdds) if err != nil { return nil, err } defer rows.Close() - var items []GetPrematchOddsRow + var items []Odd for rows.Next() { - var i GetPrematchOddsRow + var i Odd if err := rows.Scan( + &i.ID, &i.EventID, &i.Fi, &i.MarketType, @@ -274,45 +250,6 @@ func (q *Queries) GetPrematchOddsByUpcomingID(ctx context.Context, id string) ([ return items, nil } -const GetRawOddsByMarketID = `-- name: GetRawOddsByMarketID :one -SELECT id, - market_name, - handicap, - raw_odds, - fetched_at -FROM odds -WHERE market_id = $1 - AND fi = $2 - AND is_active = true - AND source = 'bet365' -` - -type GetRawOddsByMarketIDParams struct { - MarketID pgtype.Text `json:"market_id"` - Fi pgtype.Text `json:"fi"` -} - -type GetRawOddsByMarketIDRow struct { - ID int32 `json:"id"` - MarketName pgtype.Text `json:"market_name"` - Handicap pgtype.Text `json:"handicap"` - RawOdds []byte `json:"raw_odds"` - FetchedAt pgtype.Timestamp `json:"fetched_at"` -} - -func (q *Queries) GetRawOddsByMarketID(ctx context.Context, arg GetRawOddsByMarketIDParams) (GetRawOddsByMarketIDRow, error) { - row := q.db.QueryRow(ctx, GetRawOddsByMarketID, arg.MarketID, arg.Fi) - var i GetRawOddsByMarketIDRow - err := row.Scan( - &i.ID, - &i.MarketName, - &i.Handicap, - &i.RawOdds, - &i.FetchedAt, - ) - return i, err -} - const InsertNonLiveOdd = `-- name: InsertNonLiveOdd :exec INSERT INTO odds ( event_id, diff --git a/internal/domain/bet.go b/internal/domain/bet.go index b5272ec..bc6aae0 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -57,8 +57,8 @@ type BetFilter struct { CashedOut ValidBool IsShopBet ValidBool Query ValidString - CreatedBefore ValidTime - CreatedAfter ValidTime + CreatedBefore ValidTime + CreatedAfter ValidTime } type Flag struct { diff --git a/internal/domain/common.go b/internal/domain/common.go index 28910a2..88c5852 100644 --- a/internal/domain/common.go +++ b/internal/domain/common.go @@ -6,6 +6,7 @@ import ( "strconv" "time" + "github.com/jackc/pgx/v5/pgtype" "go.uber.org/zap" ) @@ -42,6 +43,62 @@ type ValidBool struct { Valid bool } +// ValidInt64 → pgtype.Int8 +func (v ValidInt64) ToPG() pgtype.Int8 { + return pgtype.Int8{ + Int64: v.Value, + Valid: v.Valid, + } +} + +// ValidInt32 → pgtype.Int4 +func (v ValidInt32) ToPG() pgtype.Int4 { + return pgtype.Int4{ + Int32: v.Value, + Valid: v.Valid, + } +} + +// ValidInt → pgtype.Int4 (Go int mapped to int32 for pg compatibility) +func (v ValidInt) ToPG() pgtype.Int4 { + return pgtype.Int4{ + Int32: int32(v.Value), + Valid: v.Valid, + } +} + +// ValidFloat32 → pgtype.Float4 +func (v ValidFloat32) ToPG() pgtype.Float4 { + return pgtype.Float4{ + Float32: v.Value, + Valid: v.Valid, + } +} + +// ValidString → pgtype.Text +func (v ValidString) ToPG() pgtype.Text { + return pgtype.Text{ + String: v.Value, + Valid: v.Valid, + } +} + +// ValidTime → pgtype.Timestamp +func (v ValidTime) ToPG() pgtype.Timestamp { + return pgtype.Timestamp{ + Time: v.Value, + Valid: v.Valid, + } +} + +// ValidBool → pgtype.Bool +func (v ValidBool) ToPG() pgtype.Bool { + return pgtype.Bool{ + Bool: v.Value, + Valid: v.Valid, + } +} + type Currency int64 // ToCurrency converts a float32 to Currency @@ -136,32 +193,30 @@ func (i *Int64JSON) UnmarshalJSON(data []byte) error { } type NullableInt64JSON struct { - Int64 int64 - Valid bool + Int64 int64 + Valid bool } func (n *NullableInt64JSON) UnmarshalJSON(data []byte) error { - var s string - if err := json.Unmarshal(data, &s); err == nil { - if s == "" { - n.Valid = false - return nil - } - v, err := strconv.ParseInt(s, 10, 64) - if err != nil { - return err - } - n.Int64, n.Valid = v, true - return nil - } + var s string + if err := json.Unmarshal(data, &s); err == nil { + if s == "" { + n.Valid = false + return nil + } + v, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return err + } + n.Int64, n.Valid = v, true + return nil + } - var v int64 - if err := json.Unmarshal(data, &v); err == nil { - n.Int64, n.Valid = v, true - return nil - } + var v int64 + if err := json.Unmarshal(data, &v); err == nil { + n.Int64, n.Valid = v, true + return nil + } - return fmt.Errorf("invalid int64 value: %s", string(data)) + return fmt.Errorf("invalid int64 value: %s", string(data)) } - - diff --git a/internal/domain/event.go b/internal/domain/event.go index 7e4e43f..f241566 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -102,7 +102,8 @@ type UpcomingEvent struct { Source string `json:"source"` // bet api provider (bet365, betfair) Status EventStatus `json:"status"` //Match Status for event IsFeatured bool `json:"is_featured"` //Whether the event is featured or not - IsActive bool `json:"is_active"` //Whether the event is featured or not + IsMonitored bool `json:"is_monitored"` //Whether the event is monitored or not + IsActive bool `json:"is_active"` //Whether the event is active or not } type MatchResult struct { EventID string @@ -112,14 +113,6 @@ type MatchResult struct { Scores map[string]map[string]string } -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"` -} - type EventFilter struct { Query ValidString SportID ValidInt32 diff --git a/internal/domain/event_history.go b/internal/domain/event_history.go new file mode 100644 index 0000000..9a5460d --- /dev/null +++ b/internal/domain/event_history.go @@ -0,0 +1,41 @@ +package domain + +import ( + "time" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" +) + +type EventHistory struct { + ID int64 + EventID string + Status string + CreatedAt time.Time +} + +type CreateEventHistory struct { + EventID string + Status string +} + +type EventHistoryFilter struct { + EventID ValidString + CreatedBefore ValidTime + CreatedAfter ValidTime +} + +func ConvertCreateEventHistory(eventHistory CreateEventHistory) dbgen.InsertEventHistoryParams { + return dbgen.InsertEventHistoryParams{ + EventID: eventHistory.EventID, + Status: eventHistory.Status, + } +} + +func ConvertDBEventHistory(eventHistory dbgen.EventHistory) EventHistory { + return EventHistory{ + ID: eventHistory.ID, + EventID: eventHistory.EventID, + Status: eventHistory.Status, + CreatedAt: eventHistory.CreatedAt.Time, + } +} diff --git a/internal/domain/odds.go b/internal/domain/odds.go index ea5a3e9..bb1cdc5 100644 --- a/internal/domain/odds.go +++ b/internal/domain/odds.go @@ -1,11 +1,10 @@ package domain import ( + "encoding/json" "time" ) -type RawMessage interface{} - type Market struct { EventID string FI string @@ -22,28 +21,27 @@ type Market struct { } type Odd struct { - EventID string `json:"event_id"` - Fi string `json:"fi"` - MarketType string `json:"market_type"` - MarketName string `json:"market_name"` - MarketCategory string `json:"market_category"` - MarketID string `json:"market_id"` - 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"` + ID int64 `json:"id"` + EventID string `json:"event_id"` + Fi string `json:"fi"` + MarketType string `json:"market_type"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID string `json:"market_id"` + Name string `json:"name"` + Handicap string `json:"handicap"` + OddsValue float64 `json:"odds_value"` + Section string `json:"section"` + Category string `json:"category"` + RawOdds []json.RawMessage `json:"raw_odds"` + FetchedAt time.Time `json:"fetched_at"` + Source string `json:"source"` + IsActive bool `json:"is_active"` } type RawOddsByMarketID struct { - ID int64 `json:"id"` - MarketName string `json:"market_name"` - Handicap string `json:"handicap"` - RawOdds []RawMessage `json:"raw_odds"` - FetchedAt time.Time `json:"fetched_at"` + ID int64 `json:"id"` + MarketName string `json:"market_name"` + Handicap string `json:"handicap"` + RawOdds []json.RawMessage `json:"raw_odds"` + FetchedAt time.Time `json:"fetched_at"` } - - diff --git a/internal/domain/odds_history.go b/internal/domain/odds_history.go new file mode 100644 index 0000000..58b8320 --- /dev/null +++ b/internal/domain/odds_history.go @@ -0,0 +1,56 @@ +package domain + +import ( + "time" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" +) + +type OddHistory struct { + ID int64 + OddID int64 + MarketID string + RawOddID int64 + EventID string + OddValue float64 + CreatedAt time.Time +} + +type CreateOddHistory struct { + OddID int64 + MarketID string + RawOddID int64 + EventID string + OddValue float64 +} + +type OddHistoryFilter struct { + OddID ValidInt64 + MarketID ValidString + RawOddID ValidInt64 + EventID ValidString + CreatedBefore ValidTime + CreatedAfter ValidTime +} + +func ConvertCreateOddHistory(odd CreateOddHistory) dbgen.InsertOddHistoryParams { + return dbgen.InsertOddHistoryParams{ + OddID: odd.OddID, + MarketID: odd.MarketID, + RawOddID: odd.RawOddID, + EventID: odd.EventID, + OddValue: odd.OddValue, + } +} + +func ConvertDBOddHistory(dbOddHistory dbgen.OddHistory) OddHistory { + return OddHistory{ + ID: dbOddHistory.ID, + OddID: dbOddHistory.OddID, + MarketID: dbOddHistory.MarketID, + RawOddID: dbOddHistory.RawOddID, + EventID: dbOddHistory.EventID, + OddValue: dbOddHistory.OddValue, + CreatedAt: dbOddHistory.CreatedAt.Time, + } +} diff --git a/internal/repository/branch.go b/internal/repository/branch.go index f7a4f7a..89b329e 100644 --- a/internal/repository/branch.go +++ b/internal/repository/branch.go @@ -9,7 +9,6 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) - func (s *Store) CreateBranch(ctx context.Context, branch domain.CreateBranch) (domain.Branch, error) { dbBranch, err := s.queries.CreateBranch(ctx, domain.ConvertCreateBranch(branch)) @@ -53,26 +52,11 @@ func (s *Store) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]do func (s *Store) GetAllBranches(ctx context.Context, filter domain.BranchFilter) ([]domain.BranchDetail, error) { dbBranches, err := s.queries.GetAllBranches(ctx, dbgen.GetAllBranchesParams{ - CompanyID: pgtype.Int8{ - Int64: filter.CompanyID.Value, - Valid: filter.CompanyID.Valid, - }, - BranchManagerID: pgtype.Int8{ - Int64: filter.BranchManagerID.Value, - Valid: filter.BranchManagerID.Valid, - }, - Query: pgtype.Text{ - String: filter.Query.Value, - Valid: filter.Query.Valid, - }, - CreatedBefore: pgtype.Timestamp{ - Time: filter.CreatedBefore.Value, - Valid: filter.CreatedBefore.Valid, - }, - CreatedAfter: pgtype.Timestamp{ - Time: filter.CreatedAfter.Value, - Valid: filter.CreatedAfter.Valid, - }, + CompanyID: filter.CompanyID.ToPG(), + BranchManagerID: filter.BranchManagerID.ToPG(), + Query: filter.Query.ToPG(), + CreatedBefore: filter.CreatedBefore.ToPG(), + CreatedAfter: filter.CreatedAfter.ToPG(), }) if err != nil { return nil, err diff --git a/internal/repository/event.go b/internal/repository/event.go index 58951ad..fb43d79 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -90,6 +90,8 @@ func (s *Store) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEven Source: e.Source.String, Status: domain.EventStatus(e.Status.String), IsFeatured: e.IsFeatured, + IsMonitored: e.IsMonitored, + IsActive: e.IsActive, } } return upcomingEvents, nil @@ -193,6 +195,7 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.Ev Status: domain.EventStatus(e.Status.String), IsFeatured: e.IsFeatured, IsActive: e.IsActive, + IsMonitored: e.IsMonitored, } } totalCount, err := s.queries.GetTotalEvents(ctx, dbgen.GetTotalEventsParams{ @@ -255,6 +258,8 @@ func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.Upc Source: event.Source.String, Status: domain.EventStatus(event.Status.String), IsFeatured: event.IsFeatured, + IsActive: event.IsActive, + IsMonitored: event.IsMonitored, }, nil } func (s *Store) UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error { @@ -290,13 +295,28 @@ func (s *Store) UpdateEventStatus(ctx context.Context, eventID string, status do } -func (s *Store) UpdateFeatured(ctx context.Context, eventID string, isFeatured bool) error { - return s.queries.UpdateFeatured(ctx, dbgen.UpdateFeaturedParams{ - ID: eventID, +func (s *Store) UpdateEventFeatured(ctx context.Context, eventID string, isFeatured bool) error { + return s.queries.UpdateEventFeatured(ctx, dbgen.UpdateEventFeaturedParams{ + ID: eventID, IsFeatured: isFeatured, }) } +func (s *Store) IsEventMonitored(ctx context.Context, eventID string) (bool, error) { + isMonitored, err := s.queries.IsEventMonitored(ctx, eventID) + + if err != nil { + return false, err + } + return isMonitored, err +} +func (s *Store) UpdateEventMonitored(ctx context.Context, eventID string, IsMonitored bool) error { + return s.queries.UpdateEventMonitored(ctx, dbgen.UpdateEventMonitoredParams{ + ID: eventID, + IsMonitored: IsMonitored, + }) +} + func (s *Store) DeleteEvent(ctx context.Context, eventID string) error { err := s.queries.DeleteEvent(ctx, eventID) if err != nil { diff --git a/internal/repository/event_history.go b/internal/repository/event_history.go new file mode 100644 index 0000000..0184581 --- /dev/null +++ b/internal/repository/event_history.go @@ -0,0 +1,55 @@ +package repository + +import ( + "context" + "fmt" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +func (s *Store) InsertEventHistory(ctx context.Context, eventHistory domain.CreateEventHistory) (domain.EventHistory, error) { + dbEventHistory, err := s.queries.InsertEventHistory(ctx, domain.ConvertCreateEventHistory(eventHistory)) + + if err != nil { + return domain.EventHistory{}, fmt.Errorf("InsertEventHistory failed: %w", err) + } + + return domain.ConvertDBEventHistory(dbEventHistory), nil +} + +func convertEventHistory(list []dbgen.EventHistory) []domain.EventHistory { + result := make([]domain.EventHistory, 0, len(list)) + for _, item := range list { + result = append(result, domain.ConvertDBEventHistory(item)) + } + return result +} + +func (s *Store) GetAllEventHistory(ctx context.Context, filter domain.EventHistoryFilter) ([]domain.EventHistory, error) { + dbEventHistories, err := s.queries.GetAllEventHistory(ctx, dbgen.GetAllEventHistoryParams{ + EventID: filter.EventID.ToPG(), + CreatedAfter: filter.CreatedAfter.ToPG(), + CreatedBefore: filter.CreatedBefore.ToPG(), + }) + + if err != nil { + return nil, fmt.Errorf("GetAllEventHistory failed: %w", err) + } + + return convertEventHistory(dbEventHistories), nil +} + +func (s *Store) GetInitialEventPerDay(ctx context.Context, filter domain.EventHistoryFilter) ([]domain.EventHistory, error) { + dbEventHistories, err := s.queries.GetInitialEventPerDay(ctx, dbgen.GetInitialEventPerDayParams{ + EventID: filter.EventID.ToPG(), + CreatedAfter: filter.CreatedAfter.ToPG(), + CreatedBefore: filter.CreatedBefore.ToPG(), + }) + + if err != nil { + return nil, fmt.Errorf("GetInitialEventPerDay failed: %w", err) + } + + return convertEventHistory(dbEventHistories), nil +} diff --git a/internal/repository/odd_history.go b/internal/repository/odd_history.go new file mode 100644 index 0000000..b625155 --- /dev/null +++ b/internal/repository/odd_history.go @@ -0,0 +1,60 @@ +package repository + +import ( + "context" + "fmt" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +func (s *Store) InsertOddHistory(ctx context.Context, odd domain.CreateOddHistory) (domain.OddHistory, error) { + dbOddHistory, err := s.queries.InsertOddHistory(ctx, domain.ConvertCreateOddHistory(odd)) + + if err != nil { + return domain.OddHistory{}, fmt.Errorf("InsertOddHistory failed: %w", err) + } + + return domain.ConvertDBOddHistory(dbOddHistory), nil +} + +func convertOddHistories(list []dbgen.OddHistory) []domain.OddHistory { + result := make([]domain.OddHistory, 0, len(list)) + for _, item := range list { + result = append(result, domain.ConvertDBOddHistory(item)) + } + return result +} + +func (s *Store) GetAllOddHistory(ctx context.Context, filter domain.OddHistoryFilter) ([]domain.OddHistory, error) { + dbOddHistories, err := s.queries.GetAllOddHistory(ctx, dbgen.GetAllOddHistoryParams{ + OddID: filter.OddID.ToPG(), + MarketID: filter.MarketID.ToPG(), + RawOddID: filter.RawOddID.ToPG(), + EventID: filter.EventID.ToPG(), + CreatedAfter: filter.CreatedAfter.ToPG(), + CreatedBefore: filter.CreatedBefore.ToPG(), + }) + + if err != nil { + return nil, fmt.Errorf("GetAllOddHistory failed: %w", err) + } + + return convertOddHistories(dbOddHistories), nil +} + +func (s *Store) GetInitialOddPerDay(ctx context.Context, filter domain.OddHistoryFilter) ([]domain.OddHistory, error) { + dbOddHistories, err := s.queries.GetInitialOddPerDay(ctx, dbgen.GetInitialOddPerDayParams{ + OddID: filter.OddID.ToPG(), + MarketID: filter.MarketID.ToPG(), + RawOddID: filter.RawOddID.ToPG(), + EventID: filter.EventID.ToPG(), + CreatedAfter: filter.CreatedAfter.ToPG(), + CreatedBefore: filter.CreatedBefore.ToPG(), + }) + + if err != nil { + return nil, fmt.Errorf("GetInitialOddPerDay failed: %w", err) + } + return convertOddHistories(dbOddHistories), nil +} diff --git a/internal/repository/odds.go b/internal/repository/odds.go index 2a0160e..09f9b4c 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -3,6 +3,7 @@ package repository import ( "context" "encoding/json" + "fmt" "os" "strconv" @@ -53,6 +54,7 @@ func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error { } err := s.queries.InsertNonLiveOdd(ctx, params) + fmt.Printf("Inserting Non Live Odd") if err != nil { _ = writeFailedMarketLog(m, err) continue @@ -110,8 +112,8 @@ func (s *Store) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.O OddsValue: odd.OddsValue.Float64, Section: odd.Section, Category: odd.Category.String, - RawOdds: func() []domain.RawMessage { - var rawOdds []domain.RawMessage + RawOdds: func() []json.RawMessage { + var rawOdds []json.RawMessage if err := json.Unmarshal(odd.RawOdds, &rawOdds); err != nil { rawOdds = nil } @@ -147,8 +149,8 @@ func (s *Store) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error) { OddsValue: row.OddsValue.Float64, Section: row.Section, Category: row.Category.String, - RawOdds: func() []domain.RawMessage { - var rawOdds []domain.RawMessage + RawOdds: func() []json.RawMessage { + var rawOdds []json.RawMessage if err := json.Unmarshal(row.RawOdds, &rawOdds); err != nil { rawOdds = nil } @@ -163,13 +165,13 @@ func (s *Store) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error) { return domainOdds, nil } -func (s *Store) GetRawOddsByMarketID(ctx context.Context, rawOddsID string, upcomingID string) (domain.RawOddsByMarketID, error) { - params := dbgen.GetRawOddsByMarketIDParams{ - MarketID: pgtype.Text{String: rawOddsID, Valid: true}, +func (s *Store) GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) (domain.RawOddsByMarketID, error) { + params := dbgen.GetOddsByMarketIDParams{ + MarketID: pgtype.Text{String: marketID, Valid: true}, Fi: pgtype.Text{String: upcomingID, Valid: true}, } - odds, err := s.queries.GetRawOddsByMarketID(ctx, params) + odds, err := s.queries.GetOddsByMarketID(ctx, params) if err != nil { return domain.RawOddsByMarketID{}, err } @@ -183,27 +185,22 @@ func (s *Store) GetRawOddsByMarketID(ctx context.Context, rawOddsID string, upco ID: int64(odds.ID), MarketName: odds.MarketName.String, Handicap: odds.Handicap.String, - RawOdds: func() []domain.RawMessage { - converted := make([]domain.RawMessage, len(rawOdds)) + RawOdds: func() []json.RawMessage { + converted := make([]json.RawMessage, len(rawOdds)) for i, r := range rawOdds { - converted[i] = domain.RawMessage(r) + converted[i] = json.RawMessage(r) } return converted }(), FetchedAt: odds.FetchedAt.Time, }, nil } -func (s *Store) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit domain.ValidInt64, offset domain.ValidInt64) ([]domain.Odd, error) { + +func (s *Store) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit domain.ValidInt32, offset domain.ValidInt32) ([]domain.Odd, error) { odds, err := s.queries.GetPaginatedPrematchOddsByUpcomingID(ctx, dbgen.GetPaginatedPrematchOddsByUpcomingIDParams{ - ID: upcomingID, - Limit: pgtype.Int4{ - Int32: int32(limit.Value), - Valid: limit.Valid, - }, - Offset: pgtype.Int4{ - Int32: int32(offset.Value), - Valid: offset.Valid, - }, + ID: upcomingID, + Limit: limit.ToPG(), + Offset: offset.ToPG(), }) if err != nil { return nil, err @@ -211,7 +208,7 @@ func (s *Store) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomi // Map the results to domain.Odd domainOdds := make([]domain.Odd, len(odds)) for i, odd := range odds { - var rawOdds []domain.RawMessage + var rawOdds []json.RawMessage if err := json.Unmarshal(odd.RawOdds, &rawOdds); err != nil { rawOdds = nil } @@ -248,7 +245,7 @@ func (s *Store) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID stri // Map the results to domain.Odd domainOdds := make([]domain.Odd, len(odds)) for i, odd := range odds { - var rawOdds []domain.RawMessage + var rawOdds []json.RawMessage if err := json.Unmarshal(odd.RawOdds, &rawOdds); err != nil { rawOdds = nil } diff --git a/internal/services/event/port.go b/internal/services/event/port.go index c95b516..1cd67c8 100644 --- a/internal/services/event/port.go +++ b/internal/services/event/port.go @@ -16,5 +16,7 @@ type Service interface { // GetAndStoreMatchResult(ctx context.Context, eventID string) error UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error UpdateEventStatus(ctx context.Context, eventID string, status domain.EventStatus) error - UpdateFeatured(ctx context.Context, eventID string, flagged bool) error + UpdateEventFeatured(ctx context.Context, eventID string, flagged bool) error + IsEventMonitored(ctx context.Context, eventID string) (bool, error) + UpdateEventMonitored(ctx context.Context, eventID string, IsMonitored bool) error } diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 87496ff..f381887 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -209,7 +209,6 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_ur sportIDs := []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91} // sportIDs := []int{1} // TODO: Add the league skipping again when we have dynamic leagues - // b, err := os.OpenFile("logs/skipped_leagues.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) // if err != nil { // log.Printf("❌ Failed to open leagues file %v", err) @@ -242,7 +241,20 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_ur } defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) + + if err != nil { + s.mongoLogger.Error( + "Failed to read event response body", + zap.String("source", source), + zap.Int("sport_id", sportID), + zap.Int("page", page), + zap.Int("total_pages", totalPages), + zap.Error(err), + ) + continue + + } var data domain.BetResult if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { @@ -345,6 +357,19 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_ur event.MatchName = ev.Home.Name + " vs " + ev.Away.Name } + if err := s.CheckAndInsertEventHistory(ctx, event); err != nil { + s.mongoLogger.Error( + "failed to check and insert event history", + zap.String("leagueID", ev.League.ID), + zap.String("leagueName", ev.League.Name), + zap.String("source", source), + zap.Int("sport_id", sportID), + zap.Int("page", page), + zap.Int("total_pages", totalPages), + zap.Error(err), + ) + } + err = s.store.SaveUpcomingEvent(ctx, event) if err != nil { s.mongoLogger.Error( @@ -388,6 +413,60 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_ur } } +func (s *service) CheckAndInsertEventHistory(ctx context.Context, event domain.UpcomingEvent) error { + isEventMonitored, err := s.store.IsEventMonitored(ctx, event.ID) + + if err != nil { + s.mongoLogger.Error( + "failed to get event is_monitored", + zap.String("eventID", event.ID), + zap.Int32("leagueID", event.LeagueID), + zap.String("leagueName", event.LeagueName), + zap.Int32("sport_id", event.SportID), + zap.Error(err), + ) + } + + if !isEventMonitored { + return nil + } + + oldEvent, err := s.GetUpcomingEventByID(ctx, event.ID) + + if err != nil { + s.mongoLogger.Error( + "failed to get event by id", + zap.String("eventID", event.ID), + zap.Int32("leagueID", event.LeagueID), + zap.String("leagueName", event.LeagueName), + zap.Int32("sport_id", event.SportID), + zap.Error(err), + ) + } + + if oldEvent.Status != event.Status { + _, err := s.store.InsertEventHistory(ctx, domain.CreateEventHistory{ + EventID: event.ID, + Status: string(event.Status), + }) + + if err != nil { + s.mongoLogger.Error( + "failed to get event by id", + zap.String("eventID", event.ID), + zap.Int32("leagueID", event.LeagueID), + zap.String("leagueName", event.LeagueName), + zap.Int32("sport_id", event.SportID), + zap.Error(err), + ) + + return err + } + } + + return nil +} + func getString(v interface{}) string { if str, ok := v.(string); ok { return str @@ -438,8 +517,15 @@ func (s *service) UpdateEventStatus(ctx context.Context, eventID string, status return s.store.UpdateEventStatus(ctx, eventID, status) } -func (s *service) UpdateFeatured(ctx context.Context, eventID string, flagged bool) error { - return s.store.UpdateFeatured(ctx, eventID, flagged) +func (s *service) UpdateEventFeatured(ctx context.Context, eventID string, flagged bool) error { + return s.store.UpdateEventFeatured(ctx, eventID, flagged) +} + +func (s *service) IsEventMonitored(ctx context.Context, eventID string) (bool, error) { + return s.store.IsEventMonitored(ctx, eventID) +} +func (s *service) UpdateEventMonitored(ctx context.Context, eventID string, IsMonitored bool) error { + return s.store.UpdateEventFeatured(ctx, eventID, IsMonitored) } // func (s *service) GetAndStoreMatchResult(ctx context.Context, eventID string) error { diff --git a/internal/services/odds/port.go b/internal/services/odds/port.go index 8dd0088..7387b4d 100644 --- a/internal/services/odds/port.go +++ b/internal/services/odds/port.go @@ -17,4 +17,7 @@ type Service interface { GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error) GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) (domain.RawOddsByMarketID, error) DeleteOddsForEvent(ctx context.Context, eventID string) error + InsertOddHistory(ctx context.Context, odd domain.CreateOddHistory) (domain.OddHistory, error) + GetAllOddHistory(ctx context.Context, filter domain.OddHistoryFilter) ([]domain.OddHistory, error) + GetInitialOddPerDay(ctx context.Context, filter domain.OddHistoryFilter) ([]domain.OddHistory, error) } diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index ee6ea56..178da5b 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -16,21 +16,24 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "go.uber.org/zap" ) type ServiceImpl struct { store *repository.Store config *config.Config + eventSvc event.Service logger *slog.Logger mongoLogger *zap.Logger client *http.Client } -func New(store *repository.Store, cfg *config.Config, logger *slog.Logger, mongoLogger *zap.Logger) *ServiceImpl { +func New(store *repository.Store, cfg *config.Config, eventSvc event.Service, logger *slog.Logger, mongoLogger *zap.Logger) *ServiceImpl { return &ServiceImpl{ store: store, config: cfg, + eventSvc: eventSvc, logger: logger, mongoLogger: mongoLogger, client: &http.Client{Timeout: 10 * time.Second}, @@ -76,7 +79,7 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { } func (s *ServiceImpl) fetchBet365Odds(ctx context.Context) error { - eventIDs, err := s.store.GetAllUpcomingEvents(ctx) + eventIDs, err := s.eventSvc.GetAllUpcomingEvents(ctx) if err != nil { s.mongoLogger.Error( "Failed to fetch upcoming event IDs", @@ -589,6 +592,17 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName Source: "bet365", } + if err := s.CheckAndInsertOddHistory(ctx, marketRecord); err != nil { + s.mongoLogger.Error( + "failed to check and insert odd history", + zap.String("market_id", marketIDstr), + zap.String("market_name", market.Name), + zap.String("eventID", eventID), + zap.Error(err), + ) + continue + } + err = s.store.SaveNonLiveMarket(ctx, marketRecord) if err != nil { s.mongoLogger.Error( @@ -598,7 +612,7 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName zap.String("eventID", eventID), zap.Error(err), ) - errs = append(errs, fmt.Errorf("market %s: %w", market.ID, err)) + errs = append(errs, fmt.Errorf("market %v: %w", market.ID, err)) continue } } @@ -609,6 +623,99 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName return nil } +func (s *ServiceImpl) CheckAndInsertOddHistory(ctx context.Context, market domain.Market) error { + isEventMonitored, err := s.eventSvc.IsEventMonitored(ctx, market.EventID) + + if err != nil { + s.mongoLogger.Error( + "failed to get is_monitored", + zap.String("market_id", market.MarketID), + zap.String("market_name", market.Name), + zap.String("eventID", market.EventID), + zap.Error(err), + ) + } + + if !isEventMonitored { + return nil + } + + oldOdds, err := s.store.GetRawOddsByMarketID(ctx, market.MarketID, market.EventID) + + if err != nil { + s.mongoLogger.Error( + "failed to get raw odds by market id", + zap.String("market_id", market.MarketID), + zap.String("market_name", market.Name), + zap.String("eventID", market.EventID), + zap.Error(err), + ) + return err + } + + if len(oldOdds.RawOdds) != len(market.Odds) { + s.mongoLogger.Error( + "new odds data does not match old odds data", + zap.String("market_id", market.MarketID), + zap.String("market_name", market.Name), + zap.String("eventID", market.EventID), + zap.Error(err), + ) + return fmt.Errorf("new odds data does not match old odds data") + } + + oldRawOdds, err := convertRawMessage(oldOdds.RawOdds) + + if err != nil { + s.mongoLogger.Error( + "failed to convert raw odds to map", + zap.String("market_id", market.MarketID), + zap.String("market_name", market.Name), + zap.String("eventID", market.EventID), + zap.Error(err), + ) + return err + } + + for _, oddData := range market.Odds { + newRawOddID := getInt(oddData["id"]) + newOddsVal := getFloat(oddData["odds"]) + isFound := false + for _, oldOddData := range oldRawOdds { + oldRawOddID := getInt(oldOddData["id"]) + oldOddsVal := getFloat(oldOddData["odds"]) + if newRawOddID == oldRawOddID { + if newOddsVal != oldOddsVal { + _, err := s.store.InsertOddHistory(ctx, domain.CreateOddHistory{ + OddID: oldOdds.ID, + MarketID: market.MarketID, + RawOddID: int64(newRawOddID), + EventID: market.EventID, + OddValue: newOddsVal, + }) + + if err != nil { + s.mongoLogger.Error( + "failed to insert odd history", + zap.String("market_id", market.MarketID), + zap.String("market_name", market.Name), + zap.String("eventID", market.EventID), + zap.Int64("odd_id", oldOdds.ID), + zap.Int("raw_odd_id", newRawOddID), + zap.Error(err), + ) + } + } + isFound = true + } + } + if !isFound { + fmt.Printf("raw odd id %d not found", newRawOddID) + } + } + return nil +} + func (s *ServiceImpl) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) { return s.store.GetPrematchOdds(ctx, eventID) } @@ -630,7 +737,7 @@ func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingI return s.store.GetPrematchOddsByUpcomingID(ctx, upcomingID) } -func (s *ServiceImpl) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset domain.ValidInt64) ([]domain.Odd, error) { +func (s *ServiceImpl) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset domain.ValidInt32) ([]domain.Odd, error) { return s.store.GetPaginatedPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset) } @@ -651,6 +758,12 @@ func getInt(v interface{}) int { } return -1 } +func getFloat(v interface{}) float64 { + if n, ok := v.(float64); ok { + return n + } + return 0 +} func getMap(v interface{}) map[string]interface{} { if m, ok := v.(map[string]interface{}); ok { diff --git a/internal/web_server/handlers/event_handler.go b/internal/web_server/handlers/event_handler.go index 9fa7940..2f8fe93 100644 --- a/internal/web_server/handlers/event_handler.go +++ b/internal/web_server/handlers/event_handler.go @@ -350,7 +350,7 @@ func (h *Handler) UpdateEventFeatured(c *fiber.Ctx) error { ) return fiber.NewError(fiber.StatusBadRequest, errMsg) } - err := h.eventSvc.UpdateFeatured(c.Context(), eventID, req.Featured) + err := h.eventSvc.UpdateEventFeatured(c.Context(), eventID, req.Featured) if err != nil { h.mongoLoggerSvc.Error("Failed to update event featured", diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 3a4083b..b96e163 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -107,7 +107,7 @@ func (a *App) initAppRoutes() { groupV1.Post("/arifpay/b2c/transfer", a.authMiddleware, h.B2CTransferHandler) groupV1.Post("/arifpay/transaction-id/verify-transaction", a.authMiddleware, h.ArifpayVerifyByTransactionIDHandler) groupV1.Get("/arifpay/session-id/verify-transaction/:session_id", a.authMiddleware, h.ArifpayVerifyBySessionIDHandler) - + // User Routes groupV1.Post("/user/resetPassword", h.ResetPassword) groupV1.Post("/user/sendResetCode", h.SendResetCode) From 5615c93fbb1b17b1fb10b964ea6bab0120bc6be5 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Mon, 11 Aug 2025 21:10:33 +0300 Subject: [PATCH 03/10] feat: added settings routes --- db/migrations/000001_fortune.up.sql | 34 +++-- db/migrations/000007_setting_data.up.sql | 11 +- db/query/settings.sql | 11 +- gen/db/settings.sql.go | 26 ++-- internal/domain/common.go | 63 ++++++++ internal/domain/settings.go | 135 ++++++++++++++++-- internal/repository/settings.go | 44 +++--- internal/services/settings/port.go | 3 +- internal/services/settings/service.go | 8 +- .../web_server/handlers/branch_handler.go | 3 +- .../web_server/handlers/settings_handler.go | 105 ++++++++++++++ internal/web_server/routes.go | 6 + new.env | 96 +++++++++++++ 13 files changed, 461 insertions(+), 84 deletions(-) create mode 100644 internal/web_server/handlers/settings_handler.go create mode 100755 new.env diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index e388a5e..ddf5d79 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -1,21 +1,3 @@ -CREATE TABLE direct_deposits ( - id BIGSERIAL PRIMARY KEY, - customer_id BIGINT NOT NULL REFERENCES users(id), - wallet_id BIGINT NOT NULL REFERENCES wallets(id), - amount NUMERIC(15, 2) NOT NULL, - bank_reference TEXT NOT NULL, - sender_account TEXT NOT NULL, - status TEXT NOT NULL CHECK (status IN ('pending', 'completed', 'rejected')), - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - verified_by BIGINT REFERENCES users(id), - verification_notes TEXT, - verified_at TIMESTAMP -); - -CREATE INDEX idx_direct_deposits_status ON direct_deposits(status); -CREATE INDEX idx_direct_deposits_customer ON direct_deposits(customer_id); -CREATE INDEX idx_direct_deposits_reference ON direct_deposits(bank_reference); - CREATE TABLE IF NOT EXISTS users ( id BIGSERIAL PRIMARY KEY, first_name VARCHAR(255) NOT NULL, @@ -398,6 +380,22 @@ CREATE TABLE flags ( ) ) ); +CREATE TABLE direct_deposits ( + id BIGSERIAL PRIMARY KEY, + customer_id BIGINT NOT NULL REFERENCES users(id), + wallet_id BIGINT NOT NULL REFERENCES wallets(id), + amount NUMERIC(15, 2) NOT NULL, + bank_reference TEXT NOT NULL, + sender_account TEXT NOT NULL, + status TEXT NOT NULL CHECK (status IN ('pending', 'completed', 'rejected')), + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + verified_by BIGINT REFERENCES users(id), + verification_notes TEXT, + verified_at TIMESTAMP +); +CREATE INDEX idx_direct_deposits_status ON direct_deposits(status); +CREATE INDEX idx_direct_deposits_customer ON direct_deposits(customer_id); +CREATE INDEX idx_direct_deposits_reference ON direct_deposits(bank_reference); -- Views CREATE VIEW companies_details AS SELECT companies.*, diff --git a/db/migrations/000007_setting_data.up.sql b/db/migrations/000007_setting_data.up.sql index 458da81..e98967b 100644 --- a/db/migrations/000007_setting_data.up.sql +++ b/db/migrations/000007_setting_data.up.sql @@ -1,20 +1,11 @@ -- Settings Initial Data INSERT INTO settings (key, value) VALUES ('sms_provider', '30'), -<<<<<<< HEAD -('max_number_of_outcomes', '30'), - ('bet_amount_limit', '100000'), -======= ('max_number_of_outcomes', '30'), ('bet_amount_limit', '10000000'), ->>>>>>> 7d8d824a94381bd82c40398654c3bd78218c5950 ('daily_ticket_limit', '50'), ('total_winnings_limit', '1000000'), ('amount_for_bet_referral', '1000000'), ('cashback_amount_cap', '1000') ON CONFLICT (key) DO UPDATE -<<<<<<< HEAD -SET value = EXCLUDED.value; -======= -SET value = EXCLUDED.value; ->>>>>>> 7d8d824a94381bd82c40398654c3bd78218c5950 +SET value = EXCLUDED.value; \ No newline at end of file diff --git a/db/query/settings.sql b/db/query/settings.sql index d0f4482..6a06aae 100644 --- a/db/query/settings.sql +++ b/db/query/settings.sql @@ -5,9 +5,8 @@ FROM settings; SELECT * FROM settings WHERE key = $1; --- name: SaveSetting :one -INSERT INTO settings (key, value, updated_at) -VALUES ($1, $2, CURRENT_TIMESTAMP) ON CONFLICT (key) DO -UPDATE -SET value = EXCLUDED.value -RETURNING *; \ No newline at end of file +-- name: UpdateSetting :exec +UPDATE settings +SET value = $2, + updated_at = CURRENT_TIMESTAMP +WHERE key = $1; \ No newline at end of file diff --git a/gen/db/settings.sql.go b/gen/db/settings.sql.go index d842661..8a9281b 100644 --- a/gen/db/settings.sql.go +++ b/gen/db/settings.sql.go @@ -57,27 +57,19 @@ func (q *Queries) GetSettings(ctx context.Context) ([]Setting, error) { return items, nil } -const SaveSetting = `-- name: SaveSetting :one -INSERT INTO settings (key, value, updated_at) -VALUES ($1, $2, CURRENT_TIMESTAMP) ON CONFLICT (key) DO -UPDATE -SET value = EXCLUDED.value -RETURNING key, value, created_at, updated_at +const UpdateSetting = `-- name: UpdateSetting :exec +UPDATE settings +SET value = $2, + updated_at = CURRENT_TIMESTAMP +WHERE key = $1 ` -type SaveSettingParams struct { +type UpdateSettingParams struct { Key string `json:"key"` Value string `json:"value"` } -func (q *Queries) SaveSetting(ctx context.Context, arg SaveSettingParams) (Setting, error) { - row := q.db.QueryRow(ctx, SaveSetting, arg.Key, arg.Value) - var i Setting - err := row.Scan( - &i.Key, - &i.Value, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err +func (q *Queries) UpdateSetting(ctx context.Context, arg UpdateSettingParams) error { + _, err := q.db.Exec(ctx, UpdateSetting, arg.Key, arg.Value) + return err } diff --git a/internal/domain/common.go b/internal/domain/common.go index 88c5852..93db311 100644 --- a/internal/domain/common.go +++ b/internal/domain/common.go @@ -99,6 +99,69 @@ func (v ValidBool) ToPG() pgtype.Bool { } } +func ConvertInt64Ptr(value *int64) ValidInt64 { + if value == nil { + return ValidInt64{} + } + return ValidInt64{ + Value: *value, + Valid: true, + } +} + +func ConvertInt32Ptr(value *int32) ValidInt32 { + if value == nil { + return ValidInt32{} + } + return ValidInt32{ + Value: *value, + Valid: true, + } +} + +func ConvertStringPtr(value *string) ValidString { + if value == nil { + return ValidString{} + } + return ValidString{ + Value: *value, + Valid: true, + } +} + +func ConvertFloat32Ptr(value *float32) ValidFloat32 { + if value == nil { + return ValidFloat32{} + } + return ValidFloat32{ + Value: *value, + Valid: true, + } +} + +func ConvertCurrencyFloatPtr(value *float32) ValidInt64 { + if value == nil { + return ValidInt64{} + } + + convertedCurrency := ToCurrency(*value) + + return ValidInt64{ + Value: int64(convertedCurrency), + Valid: true, + } +} + +func ConvertBoolPtr(value *bool) ValidBool { + if value == nil { + return ValidBool{} + } + return ValidBool{ + Value: *value, + Valid: true, + } +} + type Currency int64 // ToCurrency converts a float32 to Currency diff --git a/internal/domain/settings.go b/internal/domain/settings.go index 3b49c5c..eaa6256 100644 --- a/internal/domain/settings.go +++ b/internal/domain/settings.go @@ -1,6 +1,7 @@ package domain import ( + "strconv" "time" ) @@ -11,9 +12,19 @@ type Setting struct { } type SettingRes struct { - Key string `json:"key"` - Value string `json:"value"` - UpdatedAt string `json:"updated_at"` + Key string `json:"key"` + Value string `json:"value"` + UpdatedAt time.Time `json:"updated_at"` +} + +type UpdateSettingListReq struct { + SMSProvider *string `json:"sms_provider,omitempty"` + MaxNumberOfOutcomes *int64 `json:"max_number_of_outcomes,omitempty"` + BetAmountLimit *float32 `json:"bet_amount_limit,omitempty"` + DailyTicketPerIP *int64 `json:"daily_ticket_limit,omitempty"` + TotalWinningLimit *float32 `json:"total_winning_limit,omitempty"` + AmountForBetReferral *float32 `json:"amount_for_bet_referral,omitempty"` + CashbackAmountCap *float32 `json:"cashback_amount_cap,omitempty"` } type SettingList struct { @@ -26,7 +37,17 @@ type SettingList struct { CashbackAmountCap Currency `json:"cashback_amount_cap"` } -type DBSettingList struct { +type SettingListRes struct { + SMSProvider SMSProvider `json:"sms_provider"` + MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"` + BetAmountLimit float32 `json:"bet_amount_limit"` + DailyTicketPerIP int64 `json:"daily_ticket_limit"` + TotalWinningLimit float32 `json:"total_winning_limit"` + AmountForBetReferral float32 `json:"amount_for_bet_referral"` + CashbackAmountCap float32 `json:"cashback_amount_cap"` +} + +type ValidSettingList struct { SMSProvider ValidString MaxNumberOfOutcomes ValidInt64 BetAmountLimit ValidInt64 @@ -36,7 +57,7 @@ type DBSettingList struct { CashbackAmountCap ValidInt64 } -func ConvertInt64SettingsMap(dbSettingList *DBSettingList) map[string]*ValidInt64 { +func ConvertInt64SettingsMap(dbSettingList *ValidSettingList) map[string]*ValidInt64 { return map[string]*ValidInt64{ "max_number_of_outcomes": &dbSettingList.MaxNumberOfOutcomes, "bet_amount_limit": &dbSettingList.BetAmountLimit, @@ -47,25 +68,26 @@ func ConvertInt64SettingsMap(dbSettingList *DBSettingList) map[string]*ValidInt6 } } -func ConvertStringSettingsMap(dbSettingList *DBSettingList) map[string]*ValidString { +func ConvertStringSettingsMap(dbSettingList *ValidSettingList) map[string]*ValidString { return map[string]*ValidString{ "sms_provider": &dbSettingList.SMSProvider, } } -func ConvertBoolSettingsMap(dbSettingList *DBSettingList) map[string]*ValidBool { +func ConvertBoolSettingsMap(dbSettingList *ValidSettingList) map[string]*ValidBool { return map[string]*ValidBool{} } -func ConvertFloat32SettingsMap(dbSettingList *DBSettingList) map[string]*ValidFloat32 { +func ConvertFloat32SettingsMap(dbSettingList *ValidSettingList) map[string]*ValidFloat32 { return map[string]*ValidFloat32{} } -func ConvertTimeSettingsMap(dbSettingList *DBSettingList) map[string]*ValidTime { +func ConvertTimeSettingsMap(dbSettingList *ValidSettingList) map[string]*ValidTime { return map[string]*ValidTime{} } -func ConvertDBSetting(dbSettingList DBSettingList) SettingList { +func ValidateSettingList(dbSettingList ValidSettingList) SettingList { + // TODO: Add validation here return SettingList{ SMSProvider: SMSProvider(dbSettingList.SMSProvider.Value), MaxNumberOfOutcomes: dbSettingList.MaxNumberOfOutcomes.Value, @@ -76,3 +98,96 @@ func ConvertDBSetting(dbSettingList DBSettingList) SettingList { CashbackAmountCap: Currency(dbSettingList.CashbackAmountCap.Value), } } + +func ConvertValidSettingList(settingList ValidSettingList) []Setting { + var convertedSettings []Setting + // if settingList.AmountForBetReferral.Valid { + // newValue := strconv.FormatInt(settingList.AmountForBetReferral.Value, 10) + // settings = append(settings, Setting{ + // Key: "amount_for_bet_referral", + // Value: newValue, + // }) + // } + + int64SettingsMap := ConvertInt64SettingsMap(&settingList) + stringSettingsMap := ConvertStringSettingsMap(&settingList) + boolSettingsMap := ConvertBoolSettingsMap(&settingList) + float32SettingsMap := ConvertFloat32SettingsMap(&settingList) + timeSettingsMap := ConvertTimeSettingsMap(&settingList) + + for key, settingPtr := range int64SettingsMap { + setting := *settingPtr + if setting.Valid { + stringVal := strconv.FormatInt(setting.Value, 10) + convertedSettings = append(convertedSettings, Setting{ + Key: key, + Value: stringVal, + }) + } + } + + for key, settingPtr := range stringSettingsMap { + setting := *settingPtr + if setting.Valid { + convertedSettings = append(convertedSettings, Setting{ + Key: key, + Value: setting.Value, + }) + } + } + + for key, settingPtr := range boolSettingsMap { + setting := *settingPtr + if setting.Valid { + stringVal := strconv.FormatBool(setting.Value) + convertedSettings = append(convertedSettings, Setting{ + Key: key, + Value: stringVal, + }) + } + } + + for key, settingPtr := range float32SettingsMap { + setting := *settingPtr + if setting.Valid { + stringVal := strconv.FormatFloat(float64(setting.Value), 'E', -1, 64) + convertedSettings = append(convertedSettings, Setting{ + Key: key, + Value: stringVal, + }) + } + } + + for key, settingPtr := range timeSettingsMap { + setting := *settingPtr + if setting.Valid { + var stringVal string = setting.Value.Format("2006-01-02 15:04:05") + convertedSettings = append(convertedSettings, Setting{ + Key: key, + Value: stringVal, + }) + } + } + + return convertedSettings +} + +func ConvertSetting(setting Setting) SettingRes { + return SettingRes{ + Key: setting.Key, + Value: setting.Value, + UpdatedAt: setting.UpdatedAt, + } +} + +func ConvertUpdateSettingListReq(settings UpdateSettingListReq) ValidSettingList { + return ValidSettingList{ + SMSProvider: ConvertStringPtr(settings.SMSProvider), + MaxNumberOfOutcomes: ConvertInt64Ptr(settings.MaxNumberOfOutcomes), + BetAmountLimit: ConvertCurrencyFloatPtr(settings.BetAmountLimit), + DailyTicketPerIP: ConvertInt64Ptr(settings.DailyTicketPerIP), + TotalWinningLimit: ConvertCurrencyFloatPtr(settings.TotalWinningLimit), + AmountForBetReferral: ConvertCurrencyFloatPtr(settings.AmountForBetReferral), + CashbackAmountCap: ConvertCurrencyFloatPtr(settings.CashbackAmountCap), + } +} diff --git a/internal/repository/settings.go b/internal/repository/settings.go index 8f06372..cb2c2b2 100644 --- a/internal/repository/settings.go +++ b/internal/repository/settings.go @@ -12,7 +12,7 @@ import ( ) func GetDBSettingList(settings []dbgen.Setting) (domain.SettingList, error) { - var dbSettingList domain.DBSettingList + var dbSettingList domain.ValidSettingList var int64SettingsMap = domain.ConvertInt64SettingsMap(&dbSettingList) var stringSettingsMap = domain.ConvertStringSettingsMap(&dbSettingList) @@ -21,7 +21,7 @@ func GetDBSettingList(settings []dbgen.Setting) (domain.SettingList, error) { var timeSettingsMap = domain.ConvertTimeSettingsMap(&dbSettingList) for _, setting := range settings { - is_setting_unknown := true + isSettingUnknown := true for key, dbSetting := range int64SettingsMap { if setting.Key == key { value, err := strconv.ParseInt(setting.Value, 10, 64) @@ -32,7 +32,7 @@ func GetDBSettingList(settings []dbgen.Setting) (domain.SettingList, error) { Value: value, Valid: true, } - is_setting_unknown = false + isSettingUnknown = false } } @@ -42,7 +42,7 @@ func GetDBSettingList(settings []dbgen.Setting) (domain.SettingList, error) { Value: setting.Value, Valid: true, } - is_setting_unknown = false + isSettingUnknown = false } } @@ -56,7 +56,7 @@ func GetDBSettingList(settings []dbgen.Setting) (domain.SettingList, error) { Value: value, Valid: true, } - is_setting_unknown = false + isSettingUnknown = false } } @@ -70,7 +70,7 @@ func GetDBSettingList(settings []dbgen.Setting) (domain.SettingList, error) { Value: float32(value), Valid: true, } - is_setting_unknown = false + isSettingUnknown = false } } for key, dbSetting := range timeSettingsMap { @@ -83,11 +83,11 @@ func GetDBSettingList(settings []dbgen.Setting) (domain.SettingList, error) { Value: value, Valid: true, } - is_setting_unknown = false + isSettingUnknown = false } } - if is_setting_unknown { + if isSettingUnknown { domain.MongoDBLogger.Warn("unknown setting found on database", zap.String("setting", setting.Key)) } } @@ -99,7 +99,7 @@ func GetDBSettingList(settings []dbgen.Setting) (domain.SettingList, error) { } } - return domain.ConvertDBSetting(dbSettingList), nil + return domain.ValidateSettingList(dbSettingList), nil } func (s *Store) GetSettingList(ctx context.Context) (domain.SettingList, error) { settings, err := s.queries.GetSettings(ctx) @@ -145,8 +145,8 @@ func (s *Store) GetSetting(ctx context.Context, key string) (domain.Setting, err return result, nil } -func (s *Store) SaveSetting(ctx context.Context, key, value string) (domain.Setting, error) { - dbSetting, err := s.queries.SaveSetting(ctx, dbgen.SaveSettingParams{ +func (s *Store) UpdateSetting(ctx context.Context, key, value string) error { + err := s.queries.UpdateSetting(ctx, dbgen.UpdateSettingParams{ Key: key, Value: value, }) @@ -154,14 +154,22 @@ func (s *Store) SaveSetting(ctx context.Context, key, value string) (domain.Sett if err != nil { domain.MongoDBLogger.Error("failed to update setting", zap.String("key", key), zap.String("value", value), zap.Error(err)) - return domain.Setting{}, err + return err } - setting := domain.Setting{ - Key: dbSetting.Key, - Value: dbSetting.Value, - } - - return setting, err + return err } + +func (s *Store) UpdateSettingList(ctx context.Context, settingList domain.ValidSettingList) error { + convertedSettings := domain.ConvertValidSettingList(settingList) + + for _, setting := range convertedSettings { + err := s.UpdateSetting(ctx, setting.Key, setting.Value) + if err != nil { + domain.MongoDBLogger.Warn("failed to update setting list", zap.String("key", setting.Key), zap.Error(err)) + return err + } + } + return nil +} diff --git a/internal/services/settings/port.go b/internal/services/settings/port.go index ce86f06..9f83ab2 100644 --- a/internal/services/settings/port.go +++ b/internal/services/settings/port.go @@ -10,5 +10,6 @@ type SettingStore interface { GetSettingList(ctx context.Context) (domain.SettingList, error) GetSettings(ctx context.Context) ([]domain.Setting, error) GetSetting(ctx context.Context, key string) (domain.Setting, error) - SaveSetting(ctx context.Context, key, value string) (domain.Setting, error) + UpdateSetting(ctx context.Context, key, value string) error + UpdateSettingList(ctx context.Context, settingList domain.ValidSettingList) error } diff --git a/internal/services/settings/service.go b/internal/services/settings/service.go index 66591a1..8180e94 100644 --- a/internal/services/settings/service.go +++ b/internal/services/settings/service.go @@ -27,6 +27,10 @@ func (s *Service) GetSettings(ctx context.Context) ([]domain.Setting, error) { func (s *Service) GetSetting(ctx context.Context, key string) (domain.Setting, error) { return s.settingStore.GetSetting(ctx, key) } -func (s *Service) SaveSetting(ctx context.Context, key, value string) (domain.Setting, error) { - return s.settingStore.SaveSetting(ctx, key, value) +func (s *Service) UpdateSetting(ctx context.Context, key, value string) error { + return s.settingStore.UpdateSetting(ctx, key, value) +} + +func (s *Service) UpdateSettingList(ctx context.Context, settingList domain.ValidSettingList) error { + return s.settingStore.UpdateSettingList(ctx, settingList) } diff --git a/internal/web_server/handlers/branch_handler.go b/internal/web_server/handlers/branch_handler.go index f73c20d..f65a7f8 100644 --- a/internal/web_server/handlers/branch_handler.go +++ b/internal/web_server/handlers/branch_handler.go @@ -863,7 +863,7 @@ func (h *Handler) UpdateBranch(c *fiber.Ctx) error { var req domain.UpdateBranchReq if err := c.BodyParser(&req); err != nil { - h.mongoLoggerSvc.Info("Failed to parse CreateBranchReq", + h.mongoLoggerSvc.Info("Failed to parse UpdateBranchReq", zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), @@ -879,7 +879,6 @@ func (h *Handler) UpdateBranch(c *fiber.Ctx) error { h.mongoLoggerSvc.Info("Failed to validate UpdateBranchReq", zap.String("branchID", branchID), zap.Int("status_code", fiber.StatusBadRequest), - zap.Error(err), zap.String("errMsg", errMsg), zap.Time("timestamp", time.Now()), ) diff --git a/internal/web_server/handlers/settings_handler.go b/internal/web_server/handlers/settings_handler.go new file mode 100644 index 0000000..016b80b --- /dev/null +++ b/internal/web_server/handlers/settings_handler.go @@ -0,0 +1,105 @@ +package handlers + +import ( + "fmt" + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" + "github.com/gofiber/fiber/v2" + "go.uber.org/zap" +) + +func (h *Handler) GetSettingList(c *fiber.Ctx) error { + settingsList, err := h.settingSvc.GetSettingList(c.Context()) + + if err != nil { + h.mongoLoggerSvc.Error("Failed to fetch settings", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get setting list:"+err.Error()) + } + + return response.WriteJSON(c, fiber.StatusOK, "All Settings retrieved successfully", settingsList, nil) +} + +func (h *Handler) GetSettingByKey(c *fiber.Ctx) error { + settingKey := c.Params("key") + if settingKey == "" { + h.mongoLoggerSvc.Info("empty setting key", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "setting key must be passed") + } + + setting, err := h.settingSvc.GetSetting(c.Context(), settingKey) + + if err != nil { + h.mongoLoggerSvc.Info("invalid setting key", + zap.String("setting_key", settingKey), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "setting key is invalid") + } + + res := domain.ConvertSetting(setting) + + return response.WriteJSON(c, fiber.StatusOK, "setting retrieved successfully", res, nil) + +} + +func (h *Handler) UpdateSettingList(c *fiber.Ctx) error { + + var req domain.UpdateSettingListReq + + if err := c.BodyParser(&req); err != nil { + h.mongoLoggerSvc.Info("Failed to parse UpdateSettingListReq", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request") + } + valErrs, ok := h.validator.Validate(c, req) + if !ok { + var errMsg string + for field, msg := range valErrs { + errMsg += fmt.Sprintf("%s: %s; ", field, msg) + } + h.mongoLoggerSvc.Info("Failed to validate UpdateSettingListReq", + zap.Int("status_code", fiber.StatusBadRequest), + zap.String("errMsg", errMsg), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, errMsg) + } + settingList := domain.ConvertUpdateSettingListReq(req) + err := h.settingSvc.UpdateSettingList(c.Context(), settingList) + + if err != nil { + h.mongoLoggerSvc.Info("failed to update setting", + zap.Any("setting_list", settingList), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "failed to update setting") + } + + settingsList, err := h.settingSvc.GetSettingList(c.Context()) + + if err != nil { + h.mongoLoggerSvc.Error("Failed to fetch settings", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get setting list:"+err.Error()) + } + + return response.WriteJSON(c, fiber.StatusOK, "setting updated", settingsList, nil) + +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 8311bb5..941f076 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -336,4 +336,10 @@ func (a *App) initAppRoutes() { groupV1.Get("/issues", a.authMiddleware, a.OnlyAdminAndAbove, h.GetAllIssues) groupV1.Patch("/issues/:issue_id/status", a.authMiddleware, a.OnlyAdminAndAbove, h.UpdateIssueStatus) groupV1.Delete("/issues/:issue_id", a.authMiddleware, a.OnlyAdminAndAbove, h.DeleteIssue) + + // Settings + groupV1.Get("/settings", a.authMiddleware, h.GetSettingList) + groupV1.Get("/settings/:key", a.authMiddleware, h.GetSettingByKey) + groupV1.Put("/settings", a.authMiddleware, h.UpdateSettingList) + } diff --git a/new.env b/new.env new file mode 100755 index 0000000..1d2ac4c --- /dev/null +++ b/new.env @@ -0,0 +1,96 @@ +# REPORT_EXPORT_PATH="C:\\ProgramData\\FortuneBet\\exported_reports" #prod env +# REPORT_EXPORT_PATH ="./exported_reports" #dev env + + +# Telebirr +TELEBIRR_BASE_URL="https://developerportal.ethiotelebirr.et:38443/payment/web/paygate?" +TELEBIRR_APP_SECRET=your_telebirr_app_secret +TELEBIRR_FABRIC_APP_ID=your_telebirr_fabric_app_id +TELEBIRR_MERCHANT_CODE=your_telebirr_merchant_code +TELEBIRR_CALLBACK_URL=https://api.fortunebets.net/api/v1/telebirr/callback + +RESEND_SENDER_EMAIL=email +RESEND_API_KEY=123 + +ENV=development +PORT=8080 +DB_URL=postgresql://root:secret@localhost:5422/gh?sslmode=disable +REFRESH_EXPIRY=2592000 +JWT_KEY=mysecretkey +ACCESS_EXPIRY=600 +LOG_LEVEL=debug +AFRO_SMS_API_KEY=1 +AFRO_SMS_SENDER_NAME= + +AFRO_SMS_RECEIVER_PHONE_NUMBER= +BET365_TOKEN=158046-hesJDP2Cay2M5G +POPOK_CLIENT_ID=1 +POPOK_PLATFORM=111 +POPOK_SECRET_KEY=XwFQ76Y59zBxGryh +# POPOK_BASE_URL=https://api.pokgaming.com/game/launch #Production +# POPOK_BASE_URL=https://games.pokgaming.com/launch #Production +# POPOK_BASE_URL=https://sandbox.pokgaming.com/game/launch #Staging +# POPOK_BASE_URL=https://test-api.pokgaming.com/launch #Staging +POPOK_BASE_URL=https://st.pokgaming.com/ #Staging +POPOK_CALLBACK_URL=https://api.fortunebets.net + +#Muli-currency Support +FIXER_API_KEY=3b0f1eb30d-63c875026d-sxy9pl +BASE_CURRENCY=ETB +FIXER_BASE_URL=https://api.apilayer.com/fixer + +# Chapa API Configuration +CHAPA_TRANSFER_TYPE=Payout +CHAPA_PAYMENT_TYPE=API +CHAPA_BASE_URL=https://api.chapa.co/v1 +CHAPA_ENCRYPTION_KEY=zLdYrjnBCknMvFikmP5jBfen +CHAPA_PUBLIC_KEY=CHAPUBK_TEST-HJR0qhQRPLTkauNy9Q8UrmskPTOR31aC +CHAPA_SECRET_KEY=CHASECK_TEST-q3jypwmFK6XJGYOK3aX4z9Kogd9KaHhF +CHAPA_CALLBACK_URL=https://api.fortunebets.net/api/v1/payments/webhook/verify +CHAPA_RETURN_URL=https://fortunebets.net/dashboard + +#Alea Play +ALEA_ENABLED=true +ALEA_BASE_URL=https://api.aleaplay.com +ALEA_OPERATOR_ID=operator_id +ALEA_SECRET_KEY=hmac_secret +ALEA_GAME_LIST_URL=https://api.aleaplay.com/games/list # Optional +ALEA_DEFAULT_CURRENCY=USD # Optional (default: USD) +ALEA_SESSION_TIMEOUT=24 # Optional (hours, default: 24) +ALEA_GAME_ID_AVIATOR=aviator_prod + + +# Veli Games +VELI_ENABLED=true +VELI_BASE_URL=https://stage-api.velitech.games +VELI_REGIONAL_URL=https://stage-api.velitech.games +VELI_WEBHOOK_URL=https://api.fortunebets.net/ +VELI_SECRET_KEY=qPfg7PyhyOI9d2_bcx3ovKqzMqtjaVen +VELI_OPERATOR_KEY=111 +VELI_OPERATOR_ID=fortune_bets +VELI_BRAND_ID=fortune_bets +VELI_DEFAULT_CURRENCY=ETB + +# Arifpay +ARIFPAY_API_KEY=OV7MLkA39tgWiEApLLSg6aNf4F1RDobV +ARIFPAY_CANCEL_URL=https://api.fortunebets.net/api/v1/payments/arifpay/cancel +ARIFPAY_ERROR_URL=https://api.fortunebets.net/api/v1/payments/arifpay/error +ARIFPAY_NOTIFY_URL=https://api.fortunebets.net/api/v1/payments/arifpay/notify +ARIFPAY_SUCCESS_URL=https://api.fortunebets.net/api/v1/payments/arifpay/success + +# Santimpay +SANTIMPAY_SECRET_KEY=your_santim_pay_secret_key +SANTIMPAY_MERCHANT_ID=your_santim_pay_merchant_id +SANTIMPAY_Base_URL=https://services.santimpay.com/api/v1/gateway + +#MongoDB +MONGODB_URL=mongodb://root:secret@mongo:27017/?authSource=admin + +# Twilio SMS +TWILIO_ACCOUNT_SID=123 +TWILIO_AUTH_TOKEN=2334eadf +TWILIO_SENDER_PHONE_NUMBER=0912345678 + +# Redis +REDIS_ADDR=redis:6379 + From 5331798009720210d33de88e1bb6c69d83d006aa Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 13 Aug 2025 14:48:35 +0300 Subject: [PATCH 04/10] fix: disabling odds --- db/migrations/000001_fortune.up.sql | 19 ++++++ db/query/odds.sql | 6 +- gen/db/models.go | 1 + gen/db/odds.sql.go | 23 +++++-- internal/domain/log_fields.go | 17 +++++ internal/domain/odds.go | 2 + internal/repository/odds.go | 6 ++ internal/web_server/handlers/odd_handler.go | 75 +++++++-------------- 8 files changed, 90 insertions(+), 59 deletions(-) create mode 100644 internal/domain/log_fields.go diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index ddf5d79..d33c03a 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -265,6 +265,7 @@ CREATE TABLE events ( source TEXT DEFAULT 'b365api', is_featured BOOLEAN NOT NULL DEFAULT FALSE, is_monitored BOOLEAN NOT NULL DEFAULT FALSE, + winning_upper_limit INT NOT NULL, is_active BOOLEAN NOT NULL DEFAULT TRUE ); CREATE TABLE event_history ( @@ -288,6 +289,7 @@ CREATE TABLE odds ( category TEXT, raw_odds JSONB, fetched_at TIMESTAMP DEFAULT now(), + expires_at TIMESTAMP NOT NULL, source TEXT DEFAULT 'b365api', is_active BOOLEAN DEFAULT true, UNIQUE (market_id, name, handicap), @@ -303,6 +305,23 @@ CREATE TABLE odd_history ( odd_value DOUBLE PRECISION NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); +CREATE TABLE custom_odd ( + id BIGSERIAL PRIMARY KEY, + odd_id BIGINT NOT NULL, + raw_odd_id BIGINT NOT NULL, + market_id TEXT NOT NULL, + event_id TEXT NOT NULL, + odd_value DOUBLE PRECISION NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE TABLE disabled_odd ( + id BIGSERIAL PRIMARY KEY, + odd_id BIGINT NOT NULL, + raw_odd_id BIGINT NOT NULL, + market_id TEXT NOT NULL, + event_id TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); CREATE TABLE result_log ( id BIGSERIAL PRIMARY KEY, status_not_finished_count INT NOT NULL, diff --git a/db/query/odds.sql b/db/query/odds.sql index c48097b..4699000 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -14,7 +14,8 @@ INSERT INTO odds ( raw_odds, is_active, source, - fetched_at + fetched_at, + expires_at ) VALUES ( $1, @@ -31,7 +32,8 @@ VALUES ( $12, $13, $14, - $15 + $15, + $16 ) ON CONFLICT (event_id, market_id) DO UPDATE SET odds_value = EXCLUDED.odds_value, diff --git a/gen/db/models.go b/gen/db/models.go index fb4c8c6..f9cc71c 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -351,6 +351,7 @@ type Odd struct { Category pgtype.Text `json:"category"` RawOdds []byte `json:"raw_odds"` FetchedAt pgtype.Timestamp `json:"fetched_at"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` Source pgtype.Text `json:"source"` IsActive pgtype.Bool `json:"is_active"` } diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index f0aa0fa..2865054 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -22,7 +22,7 @@ func (q *Queries) DeleteOddsForEvent(ctx context.Context, fi pgtype.Text) error } const GetALLPrematchOdds = `-- name: GetALLPrematchOdds :many -SELECT id, event_id, fi, market_type, market_name, market_category, market_id, name, handicap, odds_value, section, category, raw_odds, fetched_at, source, is_active +SELECT id, event_id, fi, market_type, market_name, market_category, market_id, name, handicap, odds_value, section, category, raw_odds, fetched_at, expires_at, source, is_active FROM odds WHERE is_active = true AND source = 'bet365' @@ -52,6 +52,7 @@ func (q *Queries) GetALLPrematchOdds(ctx context.Context) ([]Odd, error) { &i.Category, &i.RawOdds, &i.FetchedAt, + &i.ExpiresAt, &i.Source, &i.IsActive, ); err != nil { @@ -66,7 +67,7 @@ func (q *Queries) GetALLPrematchOdds(ctx context.Context) ([]Odd, error) { } const GetOddsByMarketID = `-- name: GetOddsByMarketID :one -SELECT id, event_id, fi, market_type, market_name, market_category, market_id, name, handicap, odds_value, section, category, raw_odds, fetched_at, source, is_active +SELECT id, event_id, fi, market_type, market_name, market_category, market_id, name, handicap, odds_value, section, category, raw_odds, fetched_at, expires_at, source, is_active FROM odds WHERE market_id = $1 AND fi = $2 @@ -97,6 +98,7 @@ func (q *Queries) GetOddsByMarketID(ctx context.Context, arg GetOddsByMarketIDPa &i.Category, &i.RawOdds, &i.FetchedAt, + &i.ExpiresAt, &i.Source, &i.IsActive, ) @@ -104,7 +106,7 @@ func (q *Queries) GetOddsByMarketID(ctx context.Context, arg GetOddsByMarketIDPa } const GetPaginatedPrematchOddsByUpcomingID = `-- name: GetPaginatedPrematchOddsByUpcomingID :many -SELECT o.id, o.event_id, o.fi, o.market_type, o.market_name, o.market_category, o.market_id, o.name, o.handicap, o.odds_value, o.section, o.category, o.raw_odds, o.fetched_at, o.source, o.is_active +SELECT o.id, o.event_id, o.fi, o.market_type, o.market_name, o.market_category, o.market_id, o.name, o.handicap, o.odds_value, o.section, o.category, o.raw_odds, o.fetched_at, o.expires_at, o.source, o.is_active FROM odds o JOIN events e ON o.fi = e.id WHERE e.id = $1 @@ -145,6 +147,7 @@ func (q *Queries) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, arg &i.Category, &i.RawOdds, &i.FetchedAt, + &i.ExpiresAt, &i.Source, &i.IsActive, ); err != nil { @@ -159,7 +162,7 @@ func (q *Queries) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, arg } const GetPrematchOdds = `-- name: GetPrematchOdds :many -SELECT id, event_id, fi, market_type, market_name, market_category, market_id, name, handicap, odds_value, section, category, raw_odds, fetched_at, source, is_active +SELECT id, event_id, fi, market_type, market_name, market_category, market_id, name, handicap, odds_value, section, category, raw_odds, fetched_at, expires_at, source, is_active FROM odds WHERE is_active = true AND source = 'bet365' @@ -189,6 +192,7 @@ func (q *Queries) GetPrematchOdds(ctx context.Context) ([]Odd, error) { &i.Category, &i.RawOdds, &i.FetchedAt, + &i.ExpiresAt, &i.Source, &i.IsActive, ); err != nil { @@ -203,7 +207,7 @@ func (q *Queries) GetPrematchOdds(ctx context.Context) ([]Odd, error) { } const GetPrematchOddsByUpcomingID = `-- name: GetPrematchOddsByUpcomingID :many -SELECT o.id, o.event_id, o.fi, o.market_type, o.market_name, o.market_category, o.market_id, o.name, o.handicap, o.odds_value, o.section, o.category, o.raw_odds, o.fetched_at, o.source, o.is_active +SELECT o.id, o.event_id, o.fi, o.market_type, o.market_name, o.market_category, o.market_id, o.name, o.handicap, o.odds_value, o.section, o.category, o.raw_odds, o.fetched_at, o.expires_at, o.source, o.is_active FROM odds o JOIN events e ON o.fi = e.id WHERE e.id = $1 @@ -237,6 +241,7 @@ func (q *Queries) GetPrematchOddsByUpcomingID(ctx context.Context, id string) ([ &i.Category, &i.RawOdds, &i.FetchedAt, + &i.ExpiresAt, &i.Source, &i.IsActive, ); err != nil { @@ -266,7 +271,8 @@ INSERT INTO odds ( raw_odds, is_active, source, - fetched_at + fetched_at, + expires_at ) VALUES ( $1, @@ -283,7 +289,8 @@ VALUES ( $12, $13, $14, - $15 + $15, + $16 ) ON CONFLICT (event_id, market_id) DO UPDATE SET odds_value = EXCLUDED.odds_value, @@ -315,6 +322,7 @@ type InsertNonLiveOddParams struct { IsActive pgtype.Bool `json:"is_active"` Source pgtype.Text `json:"source"` FetchedAt pgtype.Timestamp `json:"fetched_at"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` } func (q *Queries) InsertNonLiveOdd(ctx context.Context, arg InsertNonLiveOddParams) error { @@ -334,6 +342,7 @@ func (q *Queries) InsertNonLiveOdd(ctx context.Context, arg InsertNonLiveOddPara arg.IsActive, arg.Source, arg.FetchedAt, + arg.ExpiresAt, ) return err } diff --git a/internal/domain/log_fields.go b/internal/domain/log_fields.go new file mode 100644 index 0000000..7ce6b1e --- /dev/null +++ b/internal/domain/log_fields.go @@ -0,0 +1,17 @@ +package domain + +import ( + "time" + + "github.com/gofiber/fiber/v2" + "go.uber.org/zap" +) + +var BadRequestZapFields = []zap.Field{ + zap.Int("status_code", fiber.StatusBadRequest), + zap.Time("timestamp", time.Now()), +} +var InternalServerErrorZapFields = []zap.Field{ + zap.Int("status_code", fiber.StatusBadRequest), + zap.Time("timestamp", time.Now()), +} diff --git a/internal/domain/odds.go b/internal/domain/odds.go index bb1cdc5..79a015c 100644 --- a/internal/domain/odds.go +++ b/internal/domain/odds.go @@ -35,6 +35,7 @@ type Odd struct { Category string `json:"category"` RawOdds []json.RawMessage `json:"raw_odds"` FetchedAt time.Time `json:"fetched_at"` + ExpiresAt time.Time `json:"expires_at"` Source string `json:"source"` IsActive bool `json:"is_active"` } @@ -44,4 +45,5 @@ type RawOddsByMarketID struct { Handicap string `json:"handicap"` RawOdds []json.RawMessage `json:"raw_odds"` FetchedAt time.Time `json:"fetched_at"` + ExpiresAt time.Time `json:"expires_at"` } diff --git a/internal/repository/odds.go b/internal/repository/odds.go index 09f9b4c..ac68ff9 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -51,6 +51,7 @@ func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error { IsActive: pgtype.Bool{Bool: true, Valid: true}, Source: pgtype.Text{String: m.Source, Valid: true}, FetchedAt: pgtype.Timestamp{Time: time.Now(), Valid: true}, + ExpiresAt: pgtype.Timestamp{Time: (time.Now()).Add(time.Hour), Valid: true}, } err := s.queries.InsertNonLiveOdd(ctx, params) @@ -120,6 +121,7 @@ func (s *Store) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.O return rawOdds }(), FetchedAt: odd.FetchedAt.Time, + ExpiresAt: odd.ExpiresAt.Time, Source: odd.Source.String, IsActive: odd.IsActive.Bool, } @@ -157,6 +159,7 @@ func (s *Store) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error) { return rawOdds }(), FetchedAt: row.FetchedAt.Time, + ExpiresAt: row.ExpiresAt.Time, Source: row.Source.String, IsActive: row.IsActive.Bool, } @@ -193,6 +196,7 @@ func (s *Store) GetRawOddsByMarketID(ctx context.Context, marketID string, upcom return converted }(), FetchedAt: odds.FetchedAt.Time, + ExpiresAt: odds.ExpiresAt.Time, }, nil } @@ -227,6 +231,7 @@ func (s *Store) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomi Category: odd.Category.String, RawOdds: rawOdds, FetchedAt: odd.FetchedAt.Time, + ExpiresAt: odd.ExpiresAt.Time, Source: odd.Source.String, IsActive: odd.IsActive.Bool, } @@ -264,6 +269,7 @@ func (s *Store) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID stri Category: odd.Category.String, RawOdds: rawOdds, FetchedAt: odd.FetchedAt.Time, + ExpiresAt: odd.ExpiresAt.Time, Source: odd.Source.String, IsActive: odd.IsActive.Bool, } diff --git a/internal/web_server/handlers/odd_handler.go b/internal/web_server/handlers/odd_handler.go index 291771e..3412499 100644 --- a/internal/web_server/handlers/odd_handler.go +++ b/internal/web_server/handlers/odd_handler.go @@ -1,12 +1,11 @@ package handlers import ( - "strconv" - "time" - + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" "go.uber.org/zap" + "strconv" ) // GetALLPrematchOdds @@ -19,19 +18,15 @@ import ( // @Failure 500 {object} response.APIResponse // @Router /api/v1/odds [get] func (h *Handler) GetALLPrematchOdds(c *fiber.Ctx) error { - odds, err := h.prematchSvc.GetALLPrematchOdds(c.Context()) if err != nil { - h.mongoLoggerSvc.Error("Failed to retrieve all prematch odds", - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) + logFields := append([]zap.Field{}, domain.InternalServerErrorZapFields...) + logFields = append(logFields, zap.Error(err)) + h.mongoLoggerSvc.Error("Failed to retrieve all prematch odds", logFields...) return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } return response.WriteJSON(c, fiber.StatusOK, "All prematch odds retrieved successfully", odds, nil) - } // GetRawOddsByMarketID @@ -47,37 +42,28 @@ func (h *Handler) GetALLPrematchOdds(c *fiber.Ctx) error { // @Failure 500 {object} response.APIResponse // @Router /api/v1/odds/upcoming/{upcoming_id}/market/{market_id} [get] func (h *Handler) GetRawOddsByMarketID(c *fiber.Ctx) error { + logFields := []zap.Field{ + zap.String("market_id", c.Params("market_id")), + zap.String("upcoming_id", c.Params("upcoming_id")), + } marketID := c.Params("market_id") if marketID == "" { - h.mongoLoggerSvc.Info("Missing market_id", - zap.String("market_id", marketID), - zap.Int("status_code", fiber.StatusBadRequest), - zap.Time("timestamp", time.Now()), - ) + h.mongoLoggerSvc.Info("Missing market_id", append(logFields, domain.BadRequestZapFields...)...) return fiber.NewError(fiber.StatusBadRequest, "Missing market_id") } upcomingID := c.Params("upcoming_id") if upcomingID == "" { - h.mongoLoggerSvc.Info("Missing upcoming_id", - zap.String("upcoming", upcomingID), - zap.Int("status_code", fiber.StatusBadRequest), - zap.Time("timestamp", time.Now()), - ) + h.mongoLoggerSvc.Info("Missing upcoming_id", append(logFields, domain.BadRequestZapFields...)...) return fiber.NewError(fiber.StatusBadRequest, "Missing upcoming_id") } rawOdds, err := h.prematchSvc.GetRawOddsByMarketID(c.Context(), marketID, upcomingID) if err != nil { // Lets turn this into a warn because this is constantly going off - h.mongoLoggerSvc.Warn("Failed to get raw odds by market ID", - zap.String("marketID", marketID), - zap.String("upcomingID", upcomingID), - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) + logFields = append(logFields, zap.Error(err)) + h.mongoLoggerSvc.Warn("Failed to get raw odds by market ID", append(logFields, domain.InternalServerErrorZapFields...)...) return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } @@ -98,47 +84,36 @@ func (h *Handler) GetRawOddsByMarketID(c *fiber.Ctx) error { // @Failure 500 {object} response.APIResponse // @Router /api/v1/odds/upcoming/{upcoming_id} [get] func (h *Handler) GetOddsByUpcomingID(c *fiber.Ctx) error { + logFields := []zap.Field{ + zap.String("upcoming_id", c.Params("upcoming_id")), + zap.String("limit_param", c.Query("limit", "10")), + zap.String("offset_param", c.Query("offset", "0")), + } upcomingID := c.Params("upcoming_id") if upcomingID == "" { - h.mongoLoggerSvc.Info("Missing upcoming_id", - zap.String("upcoming", upcomingID), - zap.Int("status_code", fiber.StatusBadRequest), - zap.Time("timestamp", time.Now()), - ) + h.mongoLoggerSvc.Info("Missing upcoming_id", append(logFields, domain.BadRequestZapFields...)...) return fiber.NewError(fiber.StatusBadRequest, "Missing upcoming_id") } limit, err := strconv.Atoi(c.Query("limit", "10")) // Default limit is 10 if err != nil || limit <= 0 { - h.mongoLoggerSvc.Info("Invalid limit value", - zap.Int("limit", limit), - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) + logFields = append(logFields, zap.Error(err)) + h.mongoLoggerSvc.Info("Invalid limit value", append(logFields, domain.BadRequestZapFields...)...) return fiber.NewError(fiber.StatusBadRequest, "Invalid limit value") } offset, err := strconv.Atoi(c.Query("offset", "0")) // Default offset is 0 if err != nil || offset < 0 { - h.mongoLoggerSvc.Info("Invalid offset value", - zap.Int("offset", offset), - zap.Int("status_code", fiber.StatusBadRequest), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) + logFields = append(logFields, zap.Error(err)) + h.mongoLoggerSvc.Info("Invalid offset value", append(logFields, domain.BadRequestZapFields...)...) return fiber.NewError(fiber.StatusBadRequest, err.Error()) } odds, err := h.prematchSvc.GetPrematchOddsByUpcomingID(c.Context(), upcomingID) if err != nil { - h.mongoLoggerSvc.Error("Failed to retrieve prematch odds", - zap.String("upcoming", upcomingID), - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) + logFields = append(logFields, zap.Error(err)) + h.mongoLoggerSvc.Error("Failed to retrieve prematch odds", append(logFields, domain.InternalServerErrorZapFields...)...) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve prematch odds"+err.Error()) } From 192cdb3b263037bd4ca3ba854170e85f3eaf95c8 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 13 Aug 2025 22:49:20 +0300 Subject: [PATCH 05/10] fix: minor fixes --- db/migrations/000001_fortune.up.sql | 22 ------ gen/db/events.sql.go | 118 +++++++++++++++------------- gen/db/models.go | 70 +++++++++++------ internal/web_server/cron.go | 20 ++--- 4 files changed, 117 insertions(+), 113 deletions(-) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 8c29c37..c9a0fd2 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -19,7 +19,6 @@ CREATE TABLE IF NOT EXISTS users ( OR phone_number IS NOT NULL ) ); - CREATE TABLE IF NOT EXISTS wallets ( id BIGSERIAL PRIMARY KEY, balance BIGINT NOT NULL DEFAULT 0, @@ -32,7 +31,6 @@ CREATE TABLE IF NOT EXISTS wallets ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); - CREATE TABLE refresh_tokens ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, @@ -42,26 +40,6 @@ CREATE TABLE refresh_tokens ( revoked BOOLEAN DEFAULT FALSE NOT NULL, CONSTRAINT unique_token UNIQUE (token) ); - - -CREATE TABLE direct_deposits ( - id BIGSERIAL PRIMARY KEY, - customer_id BIGINT NOT NULL REFERENCES users(id), - wallet_id BIGINT NOT NULL REFERENCES wallets(id), - amount NUMERIC(15, 2) NOT NULL, - bank_reference TEXT NOT NULL, - sender_account TEXT NOT NULL, - status TEXT NOT NULL CHECK (status IN ('pending', 'completed', 'rejected')), - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - verified_by BIGINT REFERENCES users(id), - verification_notes TEXT, - verified_at TIMESTAMP -); - -CREATE INDEX idx_direct_deposits_status ON direct_deposits(status); -CREATE INDEX idx_direct_deposits_customer ON direct_deposits(customer_id); -CREATE INDEX idx_direct_deposits_reference ON direct_deposits(bank_reference); - ----- CREATE TABLE otps ( id BIGSERIAL PRIMARY KEY, diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index 423354b..8e1978a 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -22,7 +22,7 @@ func (q *Queries) DeleteEvent(ctx context.Context, id string) error { } const GetAllUpcomingEvents = `-- name: GetAllUpcomingEvents :many -SELECT 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, fetched_at, source, is_featured, is_monitored, is_active +SELECT 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, fetched_at, source, is_featured, is_monitored, winning_upper_limit, is_active FROM events WHERE start_time > now() AND is_live = false @@ -64,6 +64,7 @@ func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]Event, error) { &i.Source, &i.IsFeatured, &i.IsMonitored, + &i.WinningUpperLimit, &i.IsActive, ); err != nil { return nil, err @@ -77,7 +78,7 @@ func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]Event, error) { } const GetExpiredUpcomingEvents = `-- name: GetExpiredUpcomingEvents :many -SELECT events.id, events.sport_id, events.match_name, events.home_team, events.away_team, events.home_team_id, events.away_team_id, events.home_kit_image, events.away_kit_image, events.league_id, events.league_name, events.league_cc, events.start_time, events.score, events.match_minute, events.timer_status, events.added_time, events.match_period, events.is_live, events.status, events.fetched_at, events.source, events.is_featured, events.is_monitored, events.is_active, +SELECT events.id, events.sport_id, events.match_name, events.home_team, events.away_team, events.home_team_id, events.away_team_id, events.home_kit_image, events.away_kit_image, events.league_id, events.league_name, events.league_cc, events.start_time, events.score, events.match_minute, events.timer_status, events.added_time, events.match_period, events.is_live, events.status, events.fetched_at, events.source, events.is_featured, events.is_monitored, events.winning_upper_limit, events.is_active, leagues.country_code as league_cc FROM events LEFT JOIN leagues ON leagues.id = league_id @@ -90,32 +91,33 @@ ORDER BY start_time ASC ` type GetExpiredUpcomingEventsRow struct { - ID string `json:"id"` - SportID pgtype.Int4 `json:"sport_id"` - MatchName pgtype.Text `json:"match_name"` - HomeTeam pgtype.Text `json:"home_team"` - AwayTeam pgtype.Text `json:"away_team"` - HomeTeamID pgtype.Int4 `json:"home_team_id"` - AwayTeamID pgtype.Int4 `json:"away_team_id"` - HomeKitImage pgtype.Text `json:"home_kit_image"` - AwayKitImage pgtype.Text `json:"away_kit_image"` - LeagueID pgtype.Int4 `json:"league_id"` - LeagueName pgtype.Text `json:"league_name"` - LeagueCc pgtype.Text `json:"league_cc"` - StartTime pgtype.Timestamp `json:"start_time"` - Score pgtype.Text `json:"score"` - MatchMinute pgtype.Int4 `json:"match_minute"` - TimerStatus pgtype.Text `json:"timer_status"` - AddedTime pgtype.Int4 `json:"added_time"` - MatchPeriod pgtype.Int4 `json:"match_period"` - IsLive pgtype.Bool `json:"is_live"` - Status pgtype.Text `json:"status"` - FetchedAt pgtype.Timestamp `json:"fetched_at"` - Source pgtype.Text `json:"source"` - IsFeatured bool `json:"is_featured"` - IsMonitored bool `json:"is_monitored"` - IsActive bool `json:"is_active"` - LeagueCc_2 pgtype.Text `json:"league_cc_2"` + ID string `json:"id"` + SportID pgtype.Int4 `json:"sport_id"` + MatchName pgtype.Text `json:"match_name"` + HomeTeam pgtype.Text `json:"home_team"` + AwayTeam pgtype.Text `json:"away_team"` + HomeTeamID pgtype.Int4 `json:"home_team_id"` + AwayTeamID pgtype.Int4 `json:"away_team_id"` + HomeKitImage pgtype.Text `json:"home_kit_image"` + AwayKitImage pgtype.Text `json:"away_kit_image"` + LeagueID pgtype.Int4 `json:"league_id"` + LeagueName pgtype.Text `json:"league_name"` + LeagueCc pgtype.Text `json:"league_cc"` + StartTime pgtype.Timestamp `json:"start_time"` + Score pgtype.Text `json:"score"` + MatchMinute pgtype.Int4 `json:"match_minute"` + TimerStatus pgtype.Text `json:"timer_status"` + AddedTime pgtype.Int4 `json:"added_time"` + MatchPeriod pgtype.Int4 `json:"match_period"` + IsLive pgtype.Bool `json:"is_live"` + Status pgtype.Text `json:"status"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + Source pgtype.Text `json:"source"` + IsFeatured bool `json:"is_featured"` + IsMonitored bool `json:"is_monitored"` + WinningUpperLimit int32 `json:"winning_upper_limit"` + IsActive bool `json:"is_active"` + LeagueCc_2 pgtype.Text `json:"league_cc_2"` } func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context, status pgtype.Text) ([]GetExpiredUpcomingEventsRow, error) { @@ -152,6 +154,7 @@ func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context, status pgtype.Te &i.Source, &i.IsFeatured, &i.IsMonitored, + &i.WinningUpperLimit, &i.IsActive, &i.LeagueCc_2, ); err != nil { @@ -166,7 +169,7 @@ func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context, status pgtype.Te } const GetPaginatedUpcomingEvents = `-- name: GetPaginatedUpcomingEvents :many -SELECT events.id, events.sport_id, events.match_name, events.home_team, events.away_team, events.home_team_id, events.away_team_id, events.home_kit_image, events.away_kit_image, events.league_id, events.league_name, events.league_cc, events.start_time, events.score, events.match_minute, events.timer_status, events.added_time, events.match_period, events.is_live, events.status, events.fetched_at, events.source, events.is_featured, events.is_monitored, events.is_active, +SELECT events.id, events.sport_id, events.match_name, events.home_team, events.away_team, events.home_team_id, events.away_team_id, events.home_kit_image, events.away_kit_image, events.league_id, events.league_name, events.league_cc, events.start_time, events.score, events.match_minute, events.timer_status, events.added_time, events.match_period, events.is_live, events.status, events.fetched_at, events.source, events.is_featured, events.is_monitored, events.winning_upper_limit, events.is_active, leagues.country_code as league_cc FROM events LEFT JOIN leagues ON leagues.id = league_id @@ -219,32 +222,33 @@ type GetPaginatedUpcomingEventsParams struct { } type GetPaginatedUpcomingEventsRow struct { - ID string `json:"id"` - SportID pgtype.Int4 `json:"sport_id"` - MatchName pgtype.Text `json:"match_name"` - HomeTeam pgtype.Text `json:"home_team"` - AwayTeam pgtype.Text `json:"away_team"` - HomeTeamID pgtype.Int4 `json:"home_team_id"` - AwayTeamID pgtype.Int4 `json:"away_team_id"` - HomeKitImage pgtype.Text `json:"home_kit_image"` - AwayKitImage pgtype.Text `json:"away_kit_image"` - LeagueID pgtype.Int4 `json:"league_id"` - LeagueName pgtype.Text `json:"league_name"` - LeagueCc pgtype.Text `json:"league_cc"` - StartTime pgtype.Timestamp `json:"start_time"` - Score pgtype.Text `json:"score"` - MatchMinute pgtype.Int4 `json:"match_minute"` - TimerStatus pgtype.Text `json:"timer_status"` - AddedTime pgtype.Int4 `json:"added_time"` - MatchPeriod pgtype.Int4 `json:"match_period"` - IsLive pgtype.Bool `json:"is_live"` - Status pgtype.Text `json:"status"` - FetchedAt pgtype.Timestamp `json:"fetched_at"` - Source pgtype.Text `json:"source"` - IsFeatured bool `json:"is_featured"` - IsMonitored bool `json:"is_monitored"` - IsActive bool `json:"is_active"` - LeagueCc_2 pgtype.Text `json:"league_cc_2"` + ID string `json:"id"` + SportID pgtype.Int4 `json:"sport_id"` + MatchName pgtype.Text `json:"match_name"` + HomeTeam pgtype.Text `json:"home_team"` + AwayTeam pgtype.Text `json:"away_team"` + HomeTeamID pgtype.Int4 `json:"home_team_id"` + AwayTeamID pgtype.Int4 `json:"away_team_id"` + HomeKitImage pgtype.Text `json:"home_kit_image"` + AwayKitImage pgtype.Text `json:"away_kit_image"` + LeagueID pgtype.Int4 `json:"league_id"` + LeagueName pgtype.Text `json:"league_name"` + LeagueCc pgtype.Text `json:"league_cc"` + StartTime pgtype.Timestamp `json:"start_time"` + Score pgtype.Text `json:"score"` + MatchMinute pgtype.Int4 `json:"match_minute"` + TimerStatus pgtype.Text `json:"timer_status"` + AddedTime pgtype.Int4 `json:"added_time"` + MatchPeriod pgtype.Int4 `json:"match_period"` + IsLive pgtype.Bool `json:"is_live"` + Status pgtype.Text `json:"status"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + Source pgtype.Text `json:"source"` + IsFeatured bool `json:"is_featured"` + IsMonitored bool `json:"is_monitored"` + WinningUpperLimit int32 `json:"winning_upper_limit"` + IsActive bool `json:"is_active"` + LeagueCc_2 pgtype.Text `json:"league_cc_2"` } func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginatedUpcomingEventsParams) ([]GetPaginatedUpcomingEventsRow, error) { @@ -291,6 +295,7 @@ func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginat &i.Source, &i.IsFeatured, &i.IsMonitored, + &i.WinningUpperLimit, &i.IsActive, &i.LeagueCc_2, ); err != nil { @@ -367,7 +372,7 @@ func (q *Queries) GetTotalEvents(ctx context.Context, arg GetTotalEventsParams) } const GetUpcomingByID = `-- name: GetUpcomingByID :one -SELECT 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, fetched_at, source, is_featured, is_monitored, is_active +SELECT 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, fetched_at, source, is_featured, is_monitored, winning_upper_limit, is_active FROM events WHERE id = $1 AND is_live = false @@ -403,6 +408,7 @@ func (q *Queries) GetUpcomingByID(ctx context.Context, id string) (Event, error) &i.Source, &i.IsFeatured, &i.IsMonitored, + &i.WinningUpperLimit, &i.IsActive, ) return i, err diff --git a/gen/db/models.go b/gen/db/models.go index f9cc71c..fa26879 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -208,6 +208,16 @@ type Company struct { UpdatedAt pgtype.Timestamp `json:"updated_at"` } +type CustomOdd struct { + ID int64 `json:"id"` + OddID int64 `json:"odd_id"` + RawOddID int64 `json:"raw_odd_id"` + MarketID string `json:"market_id"` + EventID string `json:"event_id"` + OddValue float64 `json:"odd_value"` + CreatedAt pgtype.Timestamp `json:"created_at"` +} + type CustomerWallet struct { ID int64 `json:"id"` CustomerID int64 `json:"customer_id"` @@ -248,32 +258,42 @@ type DirectDeposit struct { VerifiedAt pgtype.Timestamp `json:"verified_at"` } +type DisabledOdd struct { + ID int64 `json:"id"` + OddID int64 `json:"odd_id"` + RawOddID int64 `json:"raw_odd_id"` + MarketID string `json:"market_id"` + EventID string `json:"event_id"` + CreatedAt pgtype.Timestamp `json:"created_at"` +} + type Event struct { - ID string `json:"id"` - SportID pgtype.Int4 `json:"sport_id"` - MatchName pgtype.Text `json:"match_name"` - HomeTeam pgtype.Text `json:"home_team"` - AwayTeam pgtype.Text `json:"away_team"` - HomeTeamID pgtype.Int4 `json:"home_team_id"` - AwayTeamID pgtype.Int4 `json:"away_team_id"` - HomeKitImage pgtype.Text `json:"home_kit_image"` - AwayKitImage pgtype.Text `json:"away_kit_image"` - LeagueID pgtype.Int4 `json:"league_id"` - LeagueName pgtype.Text `json:"league_name"` - LeagueCc pgtype.Text `json:"league_cc"` - StartTime pgtype.Timestamp `json:"start_time"` - Score pgtype.Text `json:"score"` - MatchMinute pgtype.Int4 `json:"match_minute"` - TimerStatus pgtype.Text `json:"timer_status"` - AddedTime pgtype.Int4 `json:"added_time"` - MatchPeriod pgtype.Int4 `json:"match_period"` - IsLive pgtype.Bool `json:"is_live"` - Status pgtype.Text `json:"status"` - FetchedAt pgtype.Timestamp `json:"fetched_at"` - Source pgtype.Text `json:"source"` - IsFeatured bool `json:"is_featured"` - IsMonitored bool `json:"is_monitored"` - IsActive bool `json:"is_active"` + ID string `json:"id"` + SportID pgtype.Int4 `json:"sport_id"` + MatchName pgtype.Text `json:"match_name"` + HomeTeam pgtype.Text `json:"home_team"` + AwayTeam pgtype.Text `json:"away_team"` + HomeTeamID pgtype.Int4 `json:"home_team_id"` + AwayTeamID pgtype.Int4 `json:"away_team_id"` + HomeKitImage pgtype.Text `json:"home_kit_image"` + AwayKitImage pgtype.Text `json:"away_kit_image"` + LeagueID pgtype.Int4 `json:"league_id"` + LeagueName pgtype.Text `json:"league_name"` + LeagueCc pgtype.Text `json:"league_cc"` + StartTime pgtype.Timestamp `json:"start_time"` + Score pgtype.Text `json:"score"` + MatchMinute pgtype.Int4 `json:"match_minute"` + TimerStatus pgtype.Text `json:"timer_status"` + AddedTime pgtype.Int4 `json:"added_time"` + MatchPeriod pgtype.Int4 `json:"match_period"` + IsLive pgtype.Bool `json:"is_live"` + Status pgtype.Text `json:"status"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + Source pgtype.Text `json:"source"` + IsFeatured bool `json:"is_featured"` + IsMonitored bool `json:"is_monitored"` + WinningUpperLimit int32 `json:"winning_upper_limit"` + IsActive bool `json:"is_active"` } type EventHistory struct { diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index df8b6f0..b2efabb 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -28,65 +28,65 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S // { // spec: "0 0 * * * *", // Every 1 hour // task: func() { - // mongoLogger.Info("Began fetching upcoming events") + // mongoLogger.Info("Began fetching upcoming events cron task") // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { // mongoLogger.Error("Failed to fetch upcoming events", // zap.Error(err), // ) // } else { - // mongoLogger.Info("Successfully fetched upcoming events") + // mongoLogger.Info("Completed fetching upcoming events without errors") // } // }, // }, // { // spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events) // task: func() { - // mongoLogger.Info("Began fetching non live odds") + // mongoLogger.Info("Began fetching non live odds cron task") // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { // mongoLogger.Error("Failed to fetch non live odds", // zap.Error(err), // ) // } else { - // mongoLogger.Info("Successfully fetched non live odds") + // mongoLogger.Info("Completed fetching non live odds without errors") // } // }, // }, { spec: "0 */5 * * * *", // Every 5 Minutes task: func() { - mongoLogger.Info("Began updating all expired events status") + mongoLogger.Info("Began update all expired events status cron task") if _, err := resultService.CheckAndUpdateExpiredEvents(context.Background()); err != nil { mongoLogger.Error("Failed to update expired events status", zap.Error(err), ) } else { - mongoLogger.Info("Successfully updated expired events") + mongoLogger.Info("Completed expired events without errors") } }, }, // { // spec: "0 */15 * * * *", // Every 15 Minutes // task: func() { - // mongoLogger.Info("Fetching results for upcoming events") + // mongoLogger.Info("Began fetching results for upcoming events cron task") // if err := resultService.FetchAndProcessResults(context.Background()); err != nil { // mongoLogger.Error("Failed to process result", // zap.Error(err), // ) // } else { - // mongoLogger.Info("Successfully processed all event result outcomes") + // mongoLogger.Info("Completed processing all event result outcomes without errors") // } // }, // }, { spec: "0 0 * * * *", // Every Day task: func() { - mongoLogger.Info("Send daily result notification") + mongoLogger.Info("Began Send daily result notification cron task") if err := resultService.CheckAndSendResultNotifications(context.Background(), time.Now().Add(-24*time.Hour)); err != nil { mongoLogger.Error("Failed to process result", zap.Error(err), ) } else { - mongoLogger.Info("Successfully processed all event result outcomes") + mongoLogger.Info("Completed sending daily result notification without errors") } }, }, From 6347984102c14888b8b581e873835fabd77292a4 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Tue, 19 Aug 2025 17:15:31 +0300 Subject: [PATCH 06/10] feat: custom odd and disabling odds --- db/migrations/000001_fortune.up.sql | 2 - db/query/custom_odds.sql | 26 ++ db/query/disabled_odds.sql | 25 ++ db/query/odd_history.sql | 5 +- gen/db/custom_odds.sql.go | 139 ++++++++++ gen/db/disabled_odds.sql.go | 128 +++++++++ gen/db/models.go | 2 - gen/db/odd_history.sql.go | 24 +- internal/domain/bet.go | 3 + internal/domain/common.go | 245 ------------------ internal/domain/currency.go | 21 ++ internal/domain/custom_odds.go | 43 +++ internal/domain/disabled_odds.go | 47 ++++ internal/domain/oddres.go | 2 +- internal/domain/odds_history.go | 8 + internal/domain/resultres.go | 24 +- internal/domain/validtypes.go | 184 +++++++++++++ internal/repository/custom_odds.go | 5 + internal/repository/disabled_odds.go | 68 +++++ internal/repository/odd_history.go | 13 +- internal/services/santimpay/service.go | 2 +- internal/services/virtualGame/veli/service.go | 2 +- 22 files changed, 730 insertions(+), 288 deletions(-) create mode 100644 db/query/custom_odds.sql create mode 100644 db/query/disabled_odds.sql create mode 100644 gen/db/custom_odds.sql.go create mode 100644 gen/db/disabled_odds.sql.go create mode 100644 internal/domain/custom_odds.go create mode 100644 internal/domain/disabled_odds.go create mode 100644 internal/domain/validtypes.go create mode 100644 internal/repository/custom_odds.go create mode 100644 internal/repository/disabled_odds.go diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 9e7035b..fb57714 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -322,7 +322,6 @@ CREATE TABLE custom_odd ( id BIGSERIAL PRIMARY KEY, odd_id BIGINT NOT NULL, raw_odd_id BIGINT NOT NULL, - market_id TEXT NOT NULL, event_id TEXT NOT NULL, odd_value DOUBLE PRECISION NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP @@ -331,7 +330,6 @@ CREATE TABLE disabled_odd ( id BIGSERIAL PRIMARY KEY, odd_id BIGINT NOT NULL, raw_odd_id BIGINT NOT NULL, - market_id TEXT NOT NULL, event_id TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); diff --git a/db/query/custom_odds.sql b/db/query/custom_odds.sql new file mode 100644 index 0000000..199cda1 --- /dev/null +++ b/db/query/custom_odds.sql @@ -0,0 +1,26 @@ +-- name: InsertCustomOdd :one +INSERT INTO custom_odd ( + odd_id, + raw_odd_id, + event_id, + odd_value + ) +VALUES ($1, $2, $3, $4) +RETURNING *; +-- name: GetAllCustomOdds :many +SELECT * +FROM custom_odd; +-- name: GetCustomOddByRawOddID :one +SELECT * +FROM custom_odd +WHERE raw_odd_id = $1; +-- name: GetCustomOddByID :one +SELECT * +FROM custom_odd +WHERE id = $1; +-- name: DeleteCustomOddsByID :exec +DELETE FROM disabled_odd +WHERE raw_odd_id = $1; +-- name: DeleteCustomOddsByRawOddID :exec +DELETE FROM disabled_odd +WHERE raw_odd_id = $1; \ No newline at end of file diff --git a/db/query/disabled_odds.sql b/db/query/disabled_odds.sql new file mode 100644 index 0000000..9e328f1 --- /dev/null +++ b/db/query/disabled_odds.sql @@ -0,0 +1,25 @@ +-- name: InsertDisabledOdds :one +INSERT INTO disabled_odd ( + odd_id, + event_id, + raw_odd_id + ) +VALUES ($1, $2, $3) +RETURNING *; +-- name: GetAllDisabledOdds :many +SELECT * +FROM disabled_odd; +-- name: GetDisabledOddByRawOddID :one +SELECT * +FROM disabled_odd +WHERE raw_odd_id = $1; +-- name: GetDisabledOddByID :one +SELECT * +FROM disabled_odd +WHERE raw_odd_id = $1; +-- name: DeleteDisabledOddsByID :exec +DELETE FROM disabled_odd +WHERE raw_odd_id = $1; +-- name: DeleteDisabledOddsByRawOddID :exec +DELETE FROM disabled_odd +WHERE raw_odd_id = $1; \ No newline at end of file diff --git a/db/query/odd_history.sql b/db/query/odd_history.sql index fb9284f..f7368fc 100644 --- a/db/query/odd_history.sql +++ b/db/query/odd_history.sql @@ -35,9 +35,8 @@ WHERE ( created_at < sqlc.narg('created_after') OR sqlc.narg('created_after') IS NULL ); - -- name: GetInitialOddPerDay :many -SELECT DISTINCT ON (DATE_TRUNC('day', created_at)) * +SELECT DISTINCT ON (DATE_TRUNC($1, created_at)) * FROM odd_history WHERE ( odd_id = sqlc.narg('odd_id') @@ -63,5 +62,5 @@ WHERE ( created_at < sqlc.narg('created_after') OR sqlc.narg('created_after') IS NULL ) -ORDER BY DATE_TRUNC('day', created_at), +ORDER BY DATE_TRUNC($1, created_at), created_at ASC; \ No newline at end of file diff --git a/gen/db/custom_odds.sql.go b/gen/db/custom_odds.sql.go new file mode 100644 index 0000000..9e84561 --- /dev/null +++ b/gen/db/custom_odds.sql.go @@ -0,0 +1,139 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: custom_odds.sql + +package dbgen + +import ( + "context" +) + +const DeleteCustomOddsByID = `-- name: DeleteCustomOddsByID :exec +DELETE FROM disabled_odd +WHERE raw_odd_id = $1 +` + +func (q *Queries) DeleteCustomOddsByID(ctx context.Context, rawOddID int64) error { + _, err := q.db.Exec(ctx, DeleteCustomOddsByID, rawOddID) + return err +} + +const DeleteCustomOddsByRawOddID = `-- name: DeleteCustomOddsByRawOddID :exec +DELETE FROM disabled_odd +WHERE raw_odd_id = $1 +` + +func (q *Queries) DeleteCustomOddsByRawOddID(ctx context.Context, rawOddID int64) error { + _, err := q.db.Exec(ctx, DeleteCustomOddsByRawOddID, rawOddID) + return err +} + +const GetAllCustomOdds = `-- name: GetAllCustomOdds :many +SELECT id, odd_id, raw_odd_id, event_id, odd_value, created_at +FROM custom_odd +` + +func (q *Queries) GetAllCustomOdds(ctx context.Context) ([]CustomOdd, error) { + rows, err := q.db.Query(ctx, GetAllCustomOdds) + if err != nil { + return nil, err + } + defer rows.Close() + var items []CustomOdd + for rows.Next() { + var i CustomOdd + if err := rows.Scan( + &i.ID, + &i.OddID, + &i.RawOddID, + &i.EventID, + &i.OddValue, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetCustomOddByID = `-- name: GetCustomOddByID :one +SELECT id, odd_id, raw_odd_id, event_id, odd_value, created_at +FROM custom_odd +WHERE id = $1 +` + +func (q *Queries) GetCustomOddByID(ctx context.Context, id int64) (CustomOdd, error) { + row := q.db.QueryRow(ctx, GetCustomOddByID, id) + var i CustomOdd + err := row.Scan( + &i.ID, + &i.OddID, + &i.RawOddID, + &i.EventID, + &i.OddValue, + &i.CreatedAt, + ) + return i, err +} + +const GetCustomOddByRawOddID = `-- name: GetCustomOddByRawOddID :one +SELECT id, odd_id, raw_odd_id, event_id, odd_value, created_at +FROM custom_odd +WHERE raw_odd_id = $1 +` + +func (q *Queries) GetCustomOddByRawOddID(ctx context.Context, rawOddID int64) (CustomOdd, error) { + row := q.db.QueryRow(ctx, GetCustomOddByRawOddID, rawOddID) + var i CustomOdd + err := row.Scan( + &i.ID, + &i.OddID, + &i.RawOddID, + &i.EventID, + &i.OddValue, + &i.CreatedAt, + ) + return i, err +} + +const InsertCustomOdd = `-- name: InsertCustomOdd :one +INSERT INTO custom_odd ( + odd_id, + raw_odd_id, + event_id, + odd_value + ) +VALUES ($1, $2, $3, $4) +RETURNING id, odd_id, raw_odd_id, event_id, odd_value, created_at +` + +type InsertCustomOddParams struct { + OddID int64 `json:"odd_id"` + RawOddID int64 `json:"raw_odd_id"` + EventID string `json:"event_id"` + OddValue float64 `json:"odd_value"` +} + +func (q *Queries) InsertCustomOdd(ctx context.Context, arg InsertCustomOddParams) (CustomOdd, error) { + row := q.db.QueryRow(ctx, InsertCustomOdd, + arg.OddID, + arg.RawOddID, + arg.EventID, + arg.OddValue, + ) + var i CustomOdd + err := row.Scan( + &i.ID, + &i.OddID, + &i.RawOddID, + &i.EventID, + &i.OddValue, + &i.CreatedAt, + ) + return i, err +} diff --git a/gen/db/disabled_odds.sql.go b/gen/db/disabled_odds.sql.go new file mode 100644 index 0000000..19fa041 --- /dev/null +++ b/gen/db/disabled_odds.sql.go @@ -0,0 +1,128 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: disabled_odds.sql + +package dbgen + +import ( + "context" +) + +const DeleteDisabledOddsByID = `-- name: DeleteDisabledOddsByID :exec +DELETE FROM disabled_odd +WHERE raw_odd_id = $1 +` + +func (q *Queries) DeleteDisabledOddsByID(ctx context.Context, rawOddID int64) error { + _, err := q.db.Exec(ctx, DeleteDisabledOddsByID, rawOddID) + return err +} + +const DeleteDisabledOddsByRawOddID = `-- name: DeleteDisabledOddsByRawOddID :exec +DELETE FROM disabled_odd +WHERE raw_odd_id = $1 +` + +func (q *Queries) DeleteDisabledOddsByRawOddID(ctx context.Context, rawOddID int64) error { + _, err := q.db.Exec(ctx, DeleteDisabledOddsByRawOddID, rawOddID) + return err +} + +const GetAllDisabledOdds = `-- name: GetAllDisabledOdds :many +SELECT id, odd_id, raw_odd_id, event_id, created_at +FROM disabled_odd +` + +func (q *Queries) GetAllDisabledOdds(ctx context.Context) ([]DisabledOdd, error) { + rows, err := q.db.Query(ctx, GetAllDisabledOdds) + if err != nil { + return nil, err + } + defer rows.Close() + var items []DisabledOdd + for rows.Next() { + var i DisabledOdd + if err := rows.Scan( + &i.ID, + &i.OddID, + &i.RawOddID, + &i.EventID, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetDisabledOddByID = `-- name: GetDisabledOddByID :one +SELECT id, odd_id, raw_odd_id, event_id, created_at +FROM disabled_odd +WHERE raw_odd_id = $1 +` + +func (q *Queries) GetDisabledOddByID(ctx context.Context, rawOddID int64) (DisabledOdd, error) { + row := q.db.QueryRow(ctx, GetDisabledOddByID, rawOddID) + var i DisabledOdd + err := row.Scan( + &i.ID, + &i.OddID, + &i.RawOddID, + &i.EventID, + &i.CreatedAt, + ) + return i, err +} + +const GetDisabledOddByRawOddID = `-- name: GetDisabledOddByRawOddID :one +SELECT id, odd_id, raw_odd_id, event_id, created_at +FROM disabled_odd +WHERE raw_odd_id = $1 +` + +func (q *Queries) GetDisabledOddByRawOddID(ctx context.Context, rawOddID int64) (DisabledOdd, error) { + row := q.db.QueryRow(ctx, GetDisabledOddByRawOddID, rawOddID) + var i DisabledOdd + err := row.Scan( + &i.ID, + &i.OddID, + &i.RawOddID, + &i.EventID, + &i.CreatedAt, + ) + return i, err +} + +const InsertDisabledOdds = `-- name: InsertDisabledOdds :one +INSERT INTO disabled_odd ( + odd_id, + event_id, + raw_odd_id + ) +VALUES ($1, $2, $3) +RETURNING id, odd_id, raw_odd_id, event_id, created_at +` + +type InsertDisabledOddsParams struct { + OddID int64 `json:"odd_id"` + EventID string `json:"event_id"` + RawOddID int64 `json:"raw_odd_id"` +} + +func (q *Queries) InsertDisabledOdds(ctx context.Context, arg InsertDisabledOddsParams) (DisabledOdd, error) { + row := q.db.QueryRow(ctx, InsertDisabledOdds, arg.OddID, arg.EventID, arg.RawOddID) + var i DisabledOdd + err := row.Scan( + &i.ID, + &i.OddID, + &i.RawOddID, + &i.EventID, + &i.CreatedAt, + ) + return i, err +} diff --git a/gen/db/models.go b/gen/db/models.go index 0392398..9b1a3fa 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -212,7 +212,6 @@ type CustomOdd struct { ID int64 `json:"id"` OddID int64 `json:"odd_id"` RawOddID int64 `json:"raw_odd_id"` - MarketID string `json:"market_id"` EventID string `json:"event_id"` OddValue float64 `json:"odd_value"` CreatedAt pgtype.Timestamp `json:"created_at"` @@ -262,7 +261,6 @@ type DisabledOdd struct { ID int64 `json:"id"` OddID int64 `json:"odd_id"` RawOddID int64 `json:"raw_odd_id"` - MarketID string `json:"market_id"` EventID string `json:"event_id"` CreatedAt pgtype.Timestamp `json:"created_at"` } diff --git a/gen/db/odd_history.sql.go b/gen/db/odd_history.sql.go index ecb35d4..fa5272a 100644 --- a/gen/db/odd_history.sql.go +++ b/gen/db/odd_history.sql.go @@ -85,37 +85,38 @@ func (q *Queries) GetAllOddHistory(ctx context.Context, arg GetAllOddHistoryPara } const GetInitialOddPerDay = `-- name: GetInitialOddPerDay :many -SELECT DISTINCT ON (DATE_TRUNC('day', created_at)) id, odd_id, raw_odd_id, market_id, event_id, odd_value, created_at +SELECT DISTINCT ON (DATE_TRUNC($1, created_at)) id, odd_id, raw_odd_id, market_id, event_id, odd_value, created_at FROM odd_history WHERE ( - odd_id = $1 - OR $1 IS NULL - ) - AND ( - market_id = $2 + odd_id = $2 OR $2 IS NULL ) AND ( - raw_odd_id = $3 + market_id = $3 OR $3 IS NULL ) AND ( - event_id = $4 + raw_odd_id = $4 OR $4 IS NULL ) AND ( - created_at > $5 + event_id = $5 OR $5 IS NULL ) AND ( - created_at < $6 + created_at > $6 OR $6 IS NULL ) -ORDER BY DATE_TRUNC('day', created_at), + AND ( + created_at < $7 + OR $7 IS NULL + ) +ORDER BY DATE_TRUNC($1, created_at), created_at ASC ` type GetInitialOddPerDayParams struct { + DateTrunc string `json:"date_trunc"` OddID pgtype.Int8 `json:"odd_id"` MarketID pgtype.Text `json:"market_id"` RawOddID pgtype.Int8 `json:"raw_odd_id"` @@ -126,6 +127,7 @@ type GetInitialOddPerDayParams struct { func (q *Queries) GetInitialOddPerDay(ctx context.Context, arg GetInitialOddPerDayParams) ([]OddHistory, error) { rows, err := q.db.Query(ctx, GetInitialOddPerDay, + arg.DateTrunc, arg.OddID, arg.MarketID, arg.RawOddID, diff --git a/internal/domain/bet.go b/internal/domain/bet.go index bc6aae0..16e55b0 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -4,6 +4,9 @@ import ( "time" ) +// The Odd ID here is not the odd id from our database +// but the raw_odd_id from the betapi.from within the raw_odds json +// This can be refactor later type BetOutcome struct { ID int64 `json:"id" example:"1"` BetID int64 `json:"bet_id" example:"1"` diff --git a/internal/domain/common.go b/internal/domain/common.go index 93db311..dd5cc38 100644 --- a/internal/domain/common.go +++ b/internal/domain/common.go @@ -1,188 +1,11 @@ package domain import ( - "encoding/json" - "fmt" - "strconv" - "time" - - "github.com/jackc/pgx/v5/pgtype" "go.uber.org/zap" ) var MongoDBLogger *zap.Logger -type ValidInt64 struct { - Value int64 - Valid bool -} -type ValidInt struct { - Value int - Valid bool -} -type ValidInt32 struct { - Value int32 - Valid bool -} - -type ValidFloat32 struct { - Value float32 - Valid bool -} - -type ValidString struct { - Value string - Valid bool -} -type ValidTime struct { - Value time.Time - Valid bool -} -type ValidBool struct { - Value bool - Valid bool -} - -// ValidInt64 → pgtype.Int8 -func (v ValidInt64) ToPG() pgtype.Int8 { - return pgtype.Int8{ - Int64: v.Value, - Valid: v.Valid, - } -} - -// ValidInt32 → pgtype.Int4 -func (v ValidInt32) ToPG() pgtype.Int4 { - return pgtype.Int4{ - Int32: v.Value, - Valid: v.Valid, - } -} - -// ValidInt → pgtype.Int4 (Go int mapped to int32 for pg compatibility) -func (v ValidInt) ToPG() pgtype.Int4 { - return pgtype.Int4{ - Int32: int32(v.Value), - Valid: v.Valid, - } -} - -// ValidFloat32 → pgtype.Float4 -func (v ValidFloat32) ToPG() pgtype.Float4 { - return pgtype.Float4{ - Float32: v.Value, - Valid: v.Valid, - } -} - -// ValidString → pgtype.Text -func (v ValidString) ToPG() pgtype.Text { - return pgtype.Text{ - String: v.Value, - Valid: v.Valid, - } -} - -// ValidTime → pgtype.Timestamp -func (v ValidTime) ToPG() pgtype.Timestamp { - return pgtype.Timestamp{ - Time: v.Value, - Valid: v.Valid, - } -} - -// ValidBool → pgtype.Bool -func (v ValidBool) ToPG() pgtype.Bool { - return pgtype.Bool{ - Bool: v.Value, - Valid: v.Valid, - } -} - -func ConvertInt64Ptr(value *int64) ValidInt64 { - if value == nil { - return ValidInt64{} - } - return ValidInt64{ - Value: *value, - Valid: true, - } -} - -func ConvertInt32Ptr(value *int32) ValidInt32 { - if value == nil { - return ValidInt32{} - } - return ValidInt32{ - Value: *value, - Valid: true, - } -} - -func ConvertStringPtr(value *string) ValidString { - if value == nil { - return ValidString{} - } - return ValidString{ - Value: *value, - Valid: true, - } -} - -func ConvertFloat32Ptr(value *float32) ValidFloat32 { - if value == nil { - return ValidFloat32{} - } - return ValidFloat32{ - Value: *value, - Valid: true, - } -} - -func ConvertCurrencyFloatPtr(value *float32) ValidInt64 { - if value == nil { - return ValidInt64{} - } - - convertedCurrency := ToCurrency(*value) - - return ValidInt64{ - Value: int64(convertedCurrency), - Valid: true, - } -} - -func ConvertBoolPtr(value *bool) ValidBool { - if value == nil { - return ValidBool{} - } - return ValidBool{ - Value: *value, - Valid: true, - } -} - -type Currency int64 - -// ToCurrency converts a float32 to Currency -func ToCurrency(f float32) Currency { - return Currency((f * 100) + 0.5) -} - -// Float32 converts a Currency to float32 -func (m Currency) Float32() float32 { - x := float32(m) - x = x / 100 - return x -} - -// String returns a formatted Currency value -func (m Currency) String() string { - x := float32(m) - x = x / 100 - return fmt.Sprintf("$%.2f", x) -} - type ResponseWDataFactory[T any] struct { Data T `json:"data"` Response @@ -215,71 +38,3 @@ type Pagination struct { func PtrFloat64(v float64) *float64 { return &v } func PtrInt64(v int64) *int64 { return &v } - -type Int64JSON int64 - -// func (i *Int64JSON) Parse() (int64, error) { -// var asString string -// if err := json.Unmarshal(i, &asString); err == nil { -// // Try to parse the string to int64 -// return strconv.ParseInt(strings.TrimSpace(asString), 10, 64) -// } - -// var asInt int64 -// if err := json.Unmarshal(i, &asInt); err == nil { -// return asInt, nil -// } - -// // Neither string nor int worked -// return 0, fmt.Errorf("invalid int64 value: %s", string(i)) -// } - -func (i *Int64JSON) UnmarshalJSON(data []byte) error { - var s string - if err := json.Unmarshal(data, &s); err == nil { - // it was a string, parse it - v, err := strconv.ParseInt(s, 10, 64) - if err != nil { - return err - } - *i = Int64JSON(v) - return nil - } - - var v int64 - if err := json.Unmarshal(data, &v); err == nil { - *i = Int64JSON(v) - return nil - } - - return fmt.Errorf("invalid int64 value: %s", string(data)) -} - -type NullableInt64JSON struct { - Int64 int64 - Valid bool -} - -func (n *NullableInt64JSON) UnmarshalJSON(data []byte) error { - var s string - if err := json.Unmarshal(data, &s); err == nil { - if s == "" { - n.Valid = false - return nil - } - v, err := strconv.ParseInt(s, 10, 64) - if err != nil { - return err - } - n.Int64, n.Valid = v, true - return nil - } - - var v int64 - if err := json.Unmarshal(data, &v); err == nil { - n.Int64, n.Valid = v, true - return nil - } - - return fmt.Errorf("invalid int64 value: %s", string(data)) -} diff --git a/internal/domain/currency.go b/internal/domain/currency.go index 3a2eb49..e436a87 100644 --- a/internal/domain/currency.go +++ b/internal/domain/currency.go @@ -6,6 +6,27 @@ import ( "time" ) +type Currency int64 + +// ToCurrency converts a float32 to Currency +func ToCurrency(f float32) Currency { + return Currency((f * 100) + 0.5) +} + +// Float32 converts a Currency to float32 +func (m Currency) Float32() float32 { + x := float32(m) + x = x / 100 + return x +} + +// String returns a formatted Currency value +func (m Currency) String() string { + x := float32(m) + x = x / 100 + return fmt.Sprintf("$%.2f", x) +} + type IntCurrency string const ( diff --git a/internal/domain/custom_odds.go b/internal/domain/custom_odds.go new file mode 100644 index 0000000..6f9e919 --- /dev/null +++ b/internal/domain/custom_odds.go @@ -0,0 +1,43 @@ +package domain + +import ( + "time" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" +) + +type CustomOdd struct { + ID int64 + OddID int64 + RawOddID int64 + EventID string + OddValue float64 + CreatedAt time.Time +} + +type CreateCustomOdd struct { + OddID int64 + RawOddID int64 + EventID string + OddValue float64 +} + +func ConvertCreateCustomOdd(odd CreateCustomOdd) dbgen.InsertCustomOddParams { + return dbgen.InsertCustomOddParams{ + OddID: odd.OddID, + RawOddID: odd.RawOddID, + EventID: odd.EventID, + OddValue: odd.OddValue, + } +} + +func ConvertDBCustomOdd(dbCustomOdd dbgen.CustomOdd) CustomOdd { + return CustomOdd{ + ID: dbCustomOdd.ID, + OddID: dbCustomOdd.OddID, + RawOddID: dbCustomOdd.RawOddID, + EventID: dbCustomOdd.EventID, + OddValue: dbCustomOdd.OddValue, + CreatedAt: dbCustomOdd.CreatedAt.Time, + } +} diff --git a/internal/domain/disabled_odds.go b/internal/domain/disabled_odds.go new file mode 100644 index 0000000..d05b3fc --- /dev/null +++ b/internal/domain/disabled_odds.go @@ -0,0 +1,47 @@ +package domain + +import ( + "time" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" +) + +type DisabledOdd struct { + ID int64 + OddID int64 + RawOddID int64 + EventID string + CreatedAt time.Time +} + +type CreateDisabledOdd struct { + OddID int64 + RawOddID int64 + EventID string +} + +func ConvertCreateDisabledOdd(odd CreateDisabledOdd) dbgen.InsertDisabledOddsParams { + return dbgen.InsertDisabledOddsParams{ + OddID: odd.OddID, + EventID: odd.EventID, + RawOddID: odd.RawOddID, + } +} + +func ConvertDBDisabledOdd(dbDisabledOdd dbgen.DisabledOdd) DisabledOdd { + return DisabledOdd{ + ID: dbDisabledOdd.ID, + OddID: dbDisabledOdd.OddID, + RawOddID: dbDisabledOdd.RawOddID, + EventID: dbDisabledOdd.EventID, + CreatedAt: dbDisabledOdd.CreatedAt.Time, + } +} + +func ConvertDisabledOdds(list []dbgen.DisabledOdd) []DisabledOdd { + result := make([]DisabledOdd, 0, len(list)) + for _, item := range list { + result = append(result, ConvertDBDisabledOdd(item)) + } + return result +} diff --git a/internal/domain/oddres.go b/internal/domain/oddres.go index d02374a..2334b16 100644 --- a/internal/domain/oddres.go +++ b/internal/domain/oddres.go @@ -27,7 +27,7 @@ type RawOdd struct { // The Market ID for the json data can be either string / int which is causing problems when UnMarshalling type OddsMarket struct { - ID NullableInt64JSON `json:"id"` + ID ValidInt64 `json:"id"` Name string `json:"name"` Odds []json.RawMessage `json:"odds"` Header string `json:"header,omitempty"` diff --git a/internal/domain/odds_history.go b/internal/domain/odds_history.go index 58b8320..10da63a 100644 --- a/internal/domain/odds_history.go +++ b/internal/domain/odds_history.go @@ -54,3 +54,11 @@ func ConvertDBOddHistory(dbOddHistory dbgen.OddHistory) OddHistory { CreatedAt: dbOddHistory.CreatedAt.Time, } } + +func ConvertOddHistories(list []dbgen.OddHistory) []OddHistory { + result := make([]OddHistory, 0, len(list)) + for _, item := range list { + result = append(result, ConvertDBOddHistory(item)) + } + return result +} diff --git a/internal/domain/resultres.go b/internal/domain/resultres.go index 043c20b..381c86d 100644 --- a/internal/domain/resultres.go +++ b/internal/domain/resultres.go @@ -16,10 +16,10 @@ type LeagueRes struct { } type Team struct { - ID string `json:"id"` - Name string `json:"name"` - ImageID NullableInt64JSON `json:"image_id"` - CC string `json:"cc"` + ID string `json:"id"` + Name string `json:"name"` + ImageID ValidInt64 `json:"image_id"` + CC string `json:"cc"` } type Score struct { @@ -28,14 +28,14 @@ type Score struct { } type CommonResultResponse struct { - ID string `json:"id"` - SportID string `json:"sport_id"` - Time string `json:"time"` - TimeStatus NullableInt64JSON `json:"time_status"` - League LeagueRes `json:"league"` - Home Team `json:"home"` - Away Team `json:"away"` - SS string `json:"ss"` + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + TimeStatus ValidInt64 `json:"time_status"` + League LeagueRes `json:"league"` + Home Team `json:"home"` + Away Team `json:"away"` + SS string `json:"ss"` } type FootballResultResponse struct { diff --git a/internal/domain/validtypes.go b/internal/domain/validtypes.go new file mode 100644 index 0000000..709a401 --- /dev/null +++ b/internal/domain/validtypes.go @@ -0,0 +1,184 @@ +package domain + +import ( + "encoding/json" + "fmt" + "strconv" + "time" + + "github.com/jackc/pgx/v5/pgtype" +) + +type ValidInt64 struct { + Value int64 + Valid bool +} +type ValidInt struct { + Value int + Valid bool +} +type ValidInt32 struct { + Value int32 + Valid bool +} + +type ValidFloat32 struct { + Value float32 + Valid bool +} + +type ValidString struct { + Value string + Valid bool +} +type ValidTime struct { + Value time.Time + Valid bool +} +type ValidBool struct { + Value bool + Valid bool +} + +// ValidInt64 → pgtype.Int8 +func (v ValidInt64) ToPG() pgtype.Int8 { + return pgtype.Int8{ + Int64: v.Value, + Valid: v.Valid, + } +} + +// ValidInt32 → pgtype.Int4 +func (v ValidInt32) ToPG() pgtype.Int4 { + return pgtype.Int4{ + Int32: v.Value, + Valid: v.Valid, + } +} + +// ValidInt → pgtype.Int4 (Go int mapped to int32 for pg compatibility) +func (v ValidInt) ToPG() pgtype.Int4 { + return pgtype.Int4{ + Int32: int32(v.Value), + Valid: v.Valid, + } +} + +// ValidFloat32 → pgtype.Float4 +func (v ValidFloat32) ToPG() pgtype.Float4 { + return pgtype.Float4{ + Float32: v.Value, + Valid: v.Valid, + } +} + +// ValidString → pgtype.Text +func (v ValidString) ToPG() pgtype.Text { + return pgtype.Text{ + String: v.Value, + Valid: v.Valid, + } +} + +// ValidTime → pgtype.Timestamp +func (v ValidTime) ToPG() pgtype.Timestamp { + return pgtype.Timestamp{ + Time: v.Value, + Valid: v.Valid, + } +} + +// ValidBool → pgtype.Bool +func (v ValidBool) ToPG() pgtype.Bool { + return pgtype.Bool{ + Bool: v.Value, + Valid: v.Valid, + } +} + +func ConvertInt64Ptr(value *int64) ValidInt64 { + if value == nil { + return ValidInt64{} + } + return ValidInt64{ + Value: *value, + Valid: true, + } +} + +func ConvertInt32Ptr(value *int32) ValidInt32 { + if value == nil { + return ValidInt32{} + } + return ValidInt32{ + Value: *value, + Valid: true, + } +} + +func ConvertStringPtr(value *string) ValidString { + if value == nil { + return ValidString{} + } + return ValidString{ + Value: *value, + Valid: true, + } +} + +func ConvertFloat32Ptr(value *float32) ValidFloat32 { + if value == nil { + return ValidFloat32{} + } + return ValidFloat32{ + Value: *value, + Valid: true, + } +} + +func ConvertCurrencyFloatPtr(value *float32) ValidInt64 { + if value == nil { + return ValidInt64{} + } + + convertedCurrency := ToCurrency(*value) + + return ValidInt64{ + Value: int64(convertedCurrency), + Valid: true, + } +} + +func ConvertBoolPtr(value *bool) ValidBool { + if value == nil { + return ValidBool{} + } + return ValidBool{ + Value: *value, + Valid: true, + } +} + +func (n *ValidInt64) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err == nil { + if s == "" { + n.Valid = false + return nil + } + v, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return err + } + n.Value, n.Valid = v, true + return nil + } + + var v int64 + if err := json.Unmarshal(data, &v); err == nil { + n.Value, n.Valid = v, true + return nil + } + + return fmt.Errorf("invalid int64 value: %s", string(data)) +} \ No newline at end of file diff --git a/internal/repository/custom_odds.go b/internal/repository/custom_odds.go new file mode 100644 index 0000000..6b2506f --- /dev/null +++ b/internal/repository/custom_odds.go @@ -0,0 +1,5 @@ +package repository + +import "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + +func (s *Store) InsertCustomOdds(ctx context.Context, odd domain.ConvertCreateCustomOdd) (domain.) \ No newline at end of file diff --git a/internal/repository/disabled_odds.go b/internal/repository/disabled_odds.go new file mode 100644 index 0000000..b94ba65 --- /dev/null +++ b/internal/repository/disabled_odds.go @@ -0,0 +1,68 @@ +package repository + +import ( + "context" + "fmt" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +func (s *Store) InsertDisabledOdd(ctx context.Context, odd domain.CreateDisabledOdd) (domain.DisabledOdd, error) { + dbDisabledOdd, err := s.queries.InsertDisabledOdds(ctx, domain.ConvertCreateDisabledOdd(odd)) + + if err != nil { + return domain.DisabledOdd{}, fmt.Errorf("InsertDisabledOdd failed: %w", err) + } + + return domain.ConvertDBDisabledOdd(dbDisabledOdd), nil +} + +func (s *Store) GetAllDisabledOdds(ctx context.Context) ([]domain.DisabledOdd, error) { + dbDisabledOdds, err := s.queries.GetAllDisabledOdds(ctx) + + if err != nil { + return nil, fmt.Errorf("GetAllDisabledOdds failed: %w", err) + } + + return domain.ConvertDisabledOdds(dbDisabledOdds), nil +} + +func (s *Store) GetDisabledOddByRawOddID(ctx context.Context, rawOddID int64) (domain.DisabledOdd, error) { + dbDisabledOdd, err := s.queries.GetDisabledOddByRawOddID(ctx, rawOddID) + + if err != nil { + return domain.DisabledOdd{}, fmt.Errorf("GetDisabledOddByRawOddID failed: %w", err) + } + + return domain.ConvertDBDisabledOdd(dbDisabledOdd), nil +} + +func (s *Store) GetDisabledOddByID(ctx context.Context, id int64) (domain.DisabledOdd, error) { + dbDisabledOdd, err := s.queries.GetDisabledOddByID(ctx, id) + + if err != nil { + return domain.DisabledOdd{}, fmt.Errorf("GetDisabledOddByID failed: %w", err) + } + + return domain.ConvertDBDisabledOdd(dbDisabledOdd), nil +} + +func (s *Store) DeleteDisabledOddsByID(ctx context.Context, id int64) error { + + err := s.queries.DeleteDisabledOddsByID(ctx, id) + if err != nil { + return fmt.Errorf("DeleteDisabledOddsByID failed: %w", err) + } + + return nil +} + +func (s *Store) DeleteDisabledOddsByRawOddID(ctx context.Context, id int64) error { + + err := s.queries.DeleteDisabledOddsByRawOddID(ctx, id) + if err != nil { + return fmt.Errorf("DeleteDisabledOddsByRawOddID failed: %w", err) + } + + return nil +} diff --git a/internal/repository/odd_history.go b/internal/repository/odd_history.go index b625155..115c572 100644 --- a/internal/repository/odd_history.go +++ b/internal/repository/odd_history.go @@ -18,14 +18,6 @@ func (s *Store) InsertOddHistory(ctx context.Context, odd domain.CreateOddHistor return domain.ConvertDBOddHistory(dbOddHistory), nil } -func convertOddHistories(list []dbgen.OddHistory) []domain.OddHistory { - result := make([]domain.OddHistory, 0, len(list)) - for _, item := range list { - result = append(result, domain.ConvertDBOddHistory(item)) - } - return result -} - func (s *Store) GetAllOddHistory(ctx context.Context, filter domain.OddHistoryFilter) ([]domain.OddHistory, error) { dbOddHistories, err := s.queries.GetAllOddHistory(ctx, dbgen.GetAllOddHistoryParams{ OddID: filter.OddID.ToPG(), @@ -40,7 +32,7 @@ func (s *Store) GetAllOddHistory(ctx context.Context, filter domain.OddHistoryFi return nil, fmt.Errorf("GetAllOddHistory failed: %w", err) } - return convertOddHistories(dbOddHistories), nil + return domain.ConvertOddHistories(dbOddHistories), nil } func (s *Store) GetInitialOddPerDay(ctx context.Context, filter domain.OddHistoryFilter) ([]domain.OddHistory, error) { @@ -51,10 +43,11 @@ func (s *Store) GetInitialOddPerDay(ctx context.Context, filter domain.OddHistor EventID: filter.EventID.ToPG(), CreatedAfter: filter.CreatedAfter.ToPG(), CreatedBefore: filter.CreatedBefore.ToPG(), + DateTrunc: "day", }) if err != nil { return nil, fmt.Errorf("GetInitialOddPerDay failed: %w", err) } - return convertOddHistories(dbOddHistories), nil + return domain.ConvertOddHistories(dbOddHistories), nil } diff --git a/internal/services/santimpay/service.go b/internal/services/santimpay/service.go index 27e5b3e..18a15b6 100644 --- a/internal/services/santimpay/service.go +++ b/internal/services/santimpay/service.go @@ -237,7 +237,7 @@ func (s *SantimPayService) ProcessDirectPayment(ctx context.Context, req domain. if err := json.NewDecoder(resp.Body).Decode(&responseBody); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } - + // 5. Save transfer in DB transfer := domain.CreateTransfer{ Amount: domain.Currency(req.Amount), diff --git a/internal/services/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go index 4c675cc..5c4e1f4 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -259,7 +259,7 @@ func (s *service) ProcessWin(ctx context.Context, req domain.WinRequest) (*domai // --- 1. Validate PlayerID --- playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64) if err != nil { - return nil, fmt.Errorf("BAD_REQUEST: invalid PlayerID %s", req.PlayerID) +z`` return nil, fmt.Errorf("BAD_REQUEST: invalid PlayerID %s", req.PlayerID) } // --- 2. Get player wallets --- From 6d74cb8c2833b4df49c908e0ecbc7b7008c7c02c Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Fri, 22 Aug 2025 12:54:48 +0300 Subject: [PATCH 07/10] feat: refactoring entire system to be multi-tenant --- db/data/seed_data.sql | 240 ++++- db/migrations/000001_fortune.up.sql | 212 ++-- db/query/auth.sql | 11 +- db/query/company.sql | 7 +- db/query/custom_odds.sql | 61 +- db/query/disabled_odds.sql | 5 +- db/query/events.sql | 203 ++-- db/query/events_stat.sql | 13 - db/query/flags.sql | 2 +- db/query/leagues.sql | 78 +- db/query/odd_history.sql | 6 +- db/query/odds.sql | 115 +- db/query/user.sql | 24 +- gen/db/auth.sql.go | 13 +- gen/db/bet.sql.go | 18 +- gen/db/company.sql.go | 33 +- gen/db/custom_odds.sql.go | 140 ++- gen/db/disabled_odds.sql.go | 39 +- gen/db/events.sql.go | 981 ++++++++++-------- gen/db/events_stat.sql.go | 28 +- gen/db/flags.sql.go | 14 +- gen/db/leagues.sql.go | 232 +++-- gen/db/models.go | 300 ++++-- gen/db/odd_history.sql.go | 30 +- gen/db/odds.sql.go | 442 ++++---- gen/db/ticket.sql.go | 9 +- gen/db/user.sql.go | 49 +- gen/db/wallet.sql.go | 16 +- internal/domain/common_log.go | 34 + internal/domain/company.go | 12 +- internal/domain/custom_odds.go | 58 +- internal/domain/disabled_odds.go | 4 + internal/domain/event.go | 470 +++++++-- internal/domain/eventres.go | 35 + internal/domain/featured_leagues.go | 63 ++ internal/domain/league.go | 206 ++-- internal/domain/log_fields.go | 17 - internal/domain/odds.go | 179 +++- internal/domain/odds_history.go | 12 +- internal/domain/resultres.go | 152 +-- internal/domain/user.go | 29 +- internal/domain/validtypes.go | 9 + internal/pkgs/helpers/slug.go | 42 + internal/repository/auth.go | 5 +- internal/repository/bet.go | 4 +- internal/repository/company.go | 35 +- internal/repository/custom_odds.go | 104 +- internal/repository/event.go | 306 ++---- internal/repository/league.go | 125 +-- internal/repository/odds.go | 278 ++--- internal/repository/user.go | 44 +- internal/services/authentication/impl.go | 4 +- internal/services/authentication/port.go | 2 +- internal/services/bet/service.go | 10 +- internal/services/company/port.go | 1 + internal/services/company/service.go | 3 + internal/services/event/port.go | 14 +- internal/services/event/service.go | 552 +++++----- internal/services/league/port.go | 9 +- internal/services/league/service.go | 18 +- internal/services/odds/custom_odds.go | 29 + internal/services/odds/disabled_odd.go | 28 + internal/services/odds/odds_history.go | 15 + internal/services/odds/port.go | 27 +- internal/services/odds/service.go | 275 +++-- internal/services/result/service.go | 263 ++--- internal/services/user/port.go | 6 +- internal/services/user/register.go | 11 +- internal/services/user/reset.go | 6 +- internal/web_server/handlers/auth_handler.go | 120 ++- internal/web_server/handlers/event_handler.go | 361 +++++-- internal/web_server/handlers/leagues.go | 222 ++-- internal/web_server/handlers/odd_handler.go | 228 +++- internal/web_server/handlers/user.go | 51 +- .../web_server/handlers/wallet_handler.go | 2 +- internal/web_server/middleware.go | 26 +- internal/web_server/routes.go | 61 +- 77 files changed, 4778 insertions(+), 3110 deletions(-) create mode 100644 internal/domain/common_log.go create mode 100644 internal/domain/eventres.go create mode 100644 internal/domain/featured_leagues.go delete mode 100644 internal/domain/log_fields.go create mode 100644 internal/pkgs/helpers/slug.go create mode 100644 internal/services/odds/custom_odds.go create mode 100644 internal/services/odds/disabled_odd.go create mode 100644 internal/services/odds/odds_history.go diff --git a/db/data/seed_data.sql b/db/data/seed_data.sql index 256b882..732a346 100644 --- a/db/data/seed_data.sql +++ b/db/data/seed_data.sql @@ -1,59 +1,207 @@ BEGIN; - CREATE EXTENSION IF NOT EXISTS pgcrypto; - -- Users INSERT INTO users ( - id, first_name, last_name, email, phone_number, password, role, - email_verified, phone_verified, created_at, updated_at, suspended, company_id -) VALUES -(1, 'John', 'Doe', 'john.doe@example.com', NULL, - crypt('password123', gen_salt('bf'))::bytea, 'customer', - TRUE, FALSE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, FALSE, NULL), - -(2, 'Test', 'Admin', 'test.admin@gmail.com', '0988554466', - crypt('password123', gen_salt('bf'))::bytea, 'admin', - TRUE, TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, FALSE, 1), - -(3, 'Samuel', 'Tariku', 'cybersamt@gmail.com', '0911111111', - crypt('password@123', gen_salt('bf'))::bytea, 'super_admin', - TRUE, TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, FALSE, NULL), - -(4, 'Kirubel', 'Kibru', 'kirubel.jkl679@gmail.com', '0911554486', - crypt('password@123', gen_salt('bf'))::bytea, 'super_admin', - TRUE, TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, FALSE, NULL); - + id, + first_name, + last_name, + email, + phone_number, + password, + role, + email_verified, + phone_verified, + created_at, + updated_at, + suspended, + company_id + ) +VALUES ( + 1, + 'John', + 'Doe', + 'john.doe@example.com', + NULL, + crypt('password@123', gen_salt('bf'))::bytea, + 'customer', + TRUE, + FALSE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + FALSE, + NULL + ), + ( + 2, + 'Test', + 'Admin', + 'test.admin@gmail.com', + '0988554466', + crypt('password123', gen_salt('bf'))::bytea, + 'admin', + TRUE, + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + FALSE, + 1 + ), + ( + 3, + 'Samuel', + 'Tariku', + 'cybersamt@gmail.com', + '0911111111', + crypt('password@123', gen_salt('bf'))::bytea, + 'super_admin', + TRUE, + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + FALSE, + NULL + ), + ( + 4, + 'Kirubel', + 'Kibru', + 'kirubel.jkl679@gmail.com', + '0911554486', + crypt('password@123', gen_salt('bf'))::bytea, + 'super_admin', + TRUE, + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + FALSE, + NULL + ); -- Supported Operations -INSERT INTO supported_operations (id, name, description) VALUES -(1, 'SportBook', 'Sportbook operations'), -(2, 'Virtual', 'Virtual operations'); - +INSERT INTO supported_operations (id, name, description) +VALUES (1, 'SportBook', 'Sportbook operations'), + (2, 'Virtual', 'Virtual operations'); -- Wallets INSERT INTO wallets ( - id, balance, is_withdraw, is_bettable, is_transferable, user_id, - type, currency, is_active, created_at, updated_at -) VALUES -(1, 10000, TRUE, TRUE, TRUE, 1, 'regular', 'ETB', TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), -(2, 5000, FALSE, TRUE, TRUE, 1, 'static', 'ETB', TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), -(3, 20000, TRUE, TRUE, TRUE, 2, 'company_main', 'ETB', TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), -(4, 15000, TRUE, TRUE, TRUE, 2, 'branch_main', 'ETB', TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); - + id, + balance, + is_withdraw, + is_bettable, + is_transferable, + user_id, + type, + currency, + is_active, + created_at, + updated_at + ) +VALUES ( + 1, + 10000, + TRUE, + TRUE, + TRUE, + 1, + 'regular', + 'ETB', + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ), + ( + 2, + 5000, + FALSE, + TRUE, + TRUE, + 1, + 'static', + 'ETB', + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ), + ( + 3, + 20000, + TRUE, + TRUE, + TRUE, + 2, + 'company_main', + 'ETB', + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ), + ( + 4, + 15000, + TRUE, + TRUE, + TRUE, + 2, + 'branch_main', + 'ETB', + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ); -- Customer Wallets -INSERT INTO customer_wallets (id, customer_id, regular_wallet_id, static_wallet_id) +INSERT INTO customer_wallets ( + id, + customer_id, + regular_wallet_id, + static_wallet_id + ) VALUES (1, 1, 1, 2); - -- Company INSERT INTO companies ( - id, name, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at -) VALUES -(1, 'Test Company', 2, 3, 0.10, TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); - + id, + name, + slug + admin_id, + wallet_id, + deducted_percentage, + is_active, + created_at, + updated_at + ) +VALUES ( + 1, + 'FortuneBets', + 'fortunebets', + 2, + 3, + 0.10, + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ); -- Branch INSERT INTO branches ( - id, name, location, wallet_id, branch_manager_id, company_id, - is_self_owned, profit_percent, is_active, created_at, updated_at -) VALUES -(1, 'Test Branch', 'addis_ababa', 4, 2, 1, TRUE, 0.10, TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); - -COMMIT; - + id, + name, + location, + wallet_id, + branch_manager_id, + company_id, + is_self_owned, + profit_percent, + is_active, + created_at, + updated_at + ) +VALUES ( + 1, + 'Test Branch', + 'addis_ababa', + 4, + 2, + 1, + TRUE, + 0.10, + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ); +COMMIT; \ No newline at end of file diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index fb57714..717d0d2 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -17,11 +17,14 @@ CREATE TABLE IF NOT EXISTS users ( CHECK ( email IS NOT NULL OR phone_number IS NOT NULL - ) + ), + UNIQUE(email, company_id), + UNIQUE (phone_number, company_id) ); CREATE TABLE IF NOT EXISTS wallets ( id BIGSERIAL PRIMARY KEY, balance BIGINT NOT NULL DEFAULT 0, + currency VARCHAR(3) NOT NULL DEFAULT 'ETB', is_withdraw BOOLEAN NOT NULL, is_bettable BOOLEAN NOT NULL, is_transferable BOOLEAN NOT NULL, @@ -54,6 +57,7 @@ CREATE TABLE otps ( ); CREATE TABLE IF NOT EXISTS bets ( id BIGSERIAL PRIMARY KEY, + company_id BIGINT NOT NULL, amount BIGINT NOT NULL, total_odds REAL NOT NULL, status INT NOT NULL, @@ -68,6 +72,7 @@ CREATE TABLE IF NOT EXISTS bets ( ); CREATE TABLE IF NOT EXISTS tickets ( id BIGSERIAL PRIMARY KEY, + company_id BIGINT NOT NULL, amount BIGINT NOT NULL, total_odds REAL NOT NULL, IP VARCHAR(255) NOT NULL, @@ -89,14 +94,14 @@ CREATE TABLE IF NOT EXISTS bet_outcomes ( sport_id BIGINT NOT NULL, event_id BIGINT NOT null, odd_id BIGINT NOT NULL, - home_team_name VARCHAR(255) NOT NULL, - away_team_name VARCHAR(255) NOT NULL, + home_team_name TEXT NOT NULL, + away_team_name TEXT NOT NULL, market_id BIGINT NOT NULL, - market_name VARCHAR(255) NOT NULL, + market_name TEXT NOT NULL, odd REAL NOT NULL, - odd_name VARCHAR(255) NOT NULL, - odd_header VARCHAR(255) NOT NULL, - odd_handicap VARCHAR(255) NOT NULL, + odd_name TEXT NOT NULL, + odd_header TEXT NOT NULL, + odd_handicap TEXT NOT NULL, status INT NOT NULL DEFAULT 0, expires TIMESTAMP NOT NULL ); @@ -105,14 +110,14 @@ CREATE TABLE IF NOT EXISTS ticket_outcomes ( ticket_id BIGINT NOT NULL, event_id BIGINT NOT null, odd_id BIGINT NOT NULL, - home_team_name VARCHAR(255) NOT NULL, - away_team_name VARCHAR(255) NOT NULL, + home_team_name TEXT NOT NULL, + away_team_name TEXT NOT NULL, market_id BIGINT NOT NULL, - market_name VARCHAR(255) NOT NULL, + market_name TEXT NOT NULL, odd REAL NOT NULL, - odd_name VARCHAR(255) NOT NULL, - odd_header VARCHAR(255) NOT NULL, - odd_handicap VARCHAR(255) NOT NULL, + odd_name TEXT NOT NULL, + odd_header TEXT NOT NULL, + odd_handicap TEXT NOT NULL, status INT NOT NULL DEFAULT 0, expires TIMESTAMP NOT NULL ); @@ -161,7 +166,7 @@ CREATE TABLE IF NOT EXISTS customer_wallets ( CREATE TABLE IF NOT EXISTS wallet_transfer ( id BIGSERIAL PRIMARY KEY, amount BIGINT, - message VARCHAR(255) NOT NULL, + message TEXT NOT NULL, type VARCHAR(255), receiver_wallet_id BIGINT, sender_wallet_id BIGINT, @@ -255,31 +260,39 @@ CREATE TABLE IF NOT EXISTS branch_locations ( ); CREATE TABLE events ( id TEXT PRIMARY KEY, - sport_id INT, - match_name TEXT, - home_team TEXT, - away_team TEXT, - home_team_id INT, - away_team_id INT, - home_kit_image TEXT, - away_kit_image TEXT, - league_id INT, - league_name TEXT, - league_cc TEXT, - start_time TIMESTAMP, + sport_id INT NOT NULL, + match_name TEXT NOT NULL, + home_team TEXT NOT NULL, + away_team TEXT NOT NULL, + home_team_id BIGINT NOT NULL, + away_team_id BIGINT NOT NULL, + home_kit_image TEXT NOT NULL, + away_kit_image TEXT NOT NULL, + league_id BIGINT NOT NULL, + league_name TEXT NOT NULL, + start_time TIMESTAMP NOT NULL, score TEXT, match_minute INT, timer_status TEXT, added_time INT, match_period INT, - is_live BOOLEAN, - status TEXT, + is_live BOOLEAN NOT NULL DEFAULT false, + status TEXT NOT NULL, fetched_at TIMESTAMP DEFAULT now(), - source TEXT DEFAULT 'b365api', - is_featured BOOLEAN NOT NULL DEFAULT FALSE, + source TEXT NOT NULL DEFAULT 'b365api' CHECK ( + source IN ( + 'b365api', + 'bfair', + '1xbet', + 'bwin', + 'enetpulse' + ) + ), + default_is_active BOOLEAN NOT NULL DEFAULT true, + default_is_featured BOOLEAN NOT NULL DEFAULT false, + default_winning_upper_limit INT NOT NULL, is_monitored BOOLEAN NOT NULL DEFAULT FALSE, - winning_upper_limit INT NOT NULL, - is_active BOOLEAN NOT NULL DEFAULT TRUE + UNIQUE(source_event_id, source) ); CREATE TABLE event_history ( id BIGSERIAL PRIMARY KEY, @@ -287,52 +300,57 @@ CREATE TABLE event_history ( status TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -CREATE TABLE odds ( +CREATE TABLE company_event_settings ( id BIGSERIAL PRIMARY KEY, - event_id TEXT, - fi TEXT, + company_id BIGINT NOT NULL, + event_id TEXT NOT NULL, + is_active BOOLEAN, + is_featured BOOLEAN, + winning_upper_limit INT, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(company_id, event_id) +); +CREATE TABLE odds_market ( + id BIGSERIAL PRIMARY KEY, + event_id TEXT NOT NULL, market_type TEXT NOT NULL, - market_name TEXT, - market_category TEXT, - market_id TEXT, - name TEXT, - handicap TEXT, - odds_value DOUBLE PRECISION, - section TEXT NOT NULL, - category TEXT, - raw_odds JSONB, + market_name TEXT NOT NULL, + market_category TEXT NOT NULL, + market_id TEXT NOT NULL, + raw_odds JSONB NOT NULL, + default_is_active BOOLEAN NOT NULL DEFAULT true, fetched_at TIMESTAMP DEFAULT now(), expires_at TIMESTAMP NOT NULL, - source TEXT DEFAULT 'b365api', - is_active BOOLEAN DEFAULT true, UNIQUE (market_id, name, handicap), UNIQUE (event_id, market_id, name, handicap), UNIQUE (event_id, market_id) ); CREATE TABLE odd_history ( id BIGSERIAL PRIMARY KEY, - odd_id BIGINT NOT NULL, + odds_market_id BIGINT NOT NULL REFERENCES odds_market(id), raw_odd_id BIGINT NOT NULL, market_id TEXT NOT NULL, event_id TEXT NOT NULL, odd_value DOUBLE PRECISION NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -CREATE TABLE custom_odd ( - id BIGSERIAL PRIMARY KEY, - odd_id BIGINT NOT NULL, - raw_odd_id BIGINT NOT NULL, - event_id TEXT NOT NULL, - odd_value DOUBLE PRECISION NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); CREATE TABLE disabled_odd ( id BIGSERIAL PRIMARY KEY, - odd_id BIGINT NOT NULL, + company_id BIGINT NOT NULL, + odds_market_id BIGINT NOT NULL, raw_odd_id BIGINT NOT NULL, event_id TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); +CREATE TABLE company_odd_settings ( + id BIGSERIAL PRIMARY KEY, + company_id BIGINT NOT NULL, + odds_market_id BIGINT NOT NULL, + is_active BOOLEAN, + custom_raw_odds JSONB, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(company_id, odds_market_id) +); CREATE TABLE result_log ( id BIGSERIAL PRIMARY KEY, status_not_finished_count INT NOT NULL, @@ -352,6 +370,7 @@ CREATE TABLE result_log ( CREATE TABLE companies ( id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL, + slug TEXT UNIQUE NOT NULL, admin_id BIGINT NOT NULL, wallet_id BIGINT NOT NULL, deducted_percentage REAL NOT NULL, @@ -366,19 +385,28 @@ CREATE TABLE companies ( CREATE TABLE leagues ( id BIGINT PRIMARY KEY, name TEXT NOT NULL, - img TEXT, + img_url TEXT, country_code TEXT, bet365_id INT, sport_id INT NOT NULL, - is_active BOOLEAN DEFAULT true, - is_featured BOOLEAN DEFAULT false + default_is_active BOOLEAN NOT NULL DEFAULT true, + default_is_featured BOOLEAN NOT NULL DEFAULT false +); +CREATE TABLE company_league_settings ( + id BIGSERIAL PRIMARY KEY, + company_id BIGINT NOT NULL, + league_id BIGINT NOT NULL, + is_active BOOLEAN, + is_featured BOOLEAN, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(league_id, company_id) ); CREATE TABLE teams ( - id TEXT PRIMARY KEY, + id BIGSERIAL PRIMARY KEY, team_name TEXT NOT NULL, - country TEXT, - bet365_id INT, - logo_url TEXT + country_code TEXT NOT NULL, + bet365_id BIGINT, + img_url TEXT ); CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, @@ -387,14 +415,14 @@ CREATE TABLE IF NOT EXISTS settings ( updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE bonus ( - id BIGSERIAL PRIMARY KEY, multiplier REAL NOT NULL, + id BIGSERIAL PRIMARY KEY, balance_cap BIGINT NOT NULL DEFAULT 0 ); CREATE TABLE flags ( id BIGSERIAL PRIMARY KEY, bet_id BIGINT REFERENCES bets(id) ON DELETE CASCADE, - odd_id BIGINT REFERENCES odds(id), + odds_market_id BIGINT REFERENCES odds_market(id), reason TEXT, flagged_at TIMESTAMP DEFAULT NOW(), resolved BOOLEAN DEFAULT FALSE, @@ -544,17 +572,56 @@ SELECT sd.*, st.verified AS transaction_verified FROM shop_deposits AS sd JOIN shop_transactions st ON st.id = sd.shop_transaction_id; +CREATE VIEW league_with_settings AS +SELECT l.*, + cls.company_id, + COALESCE(cls.is_active, l.default_is_active) AS is_active, + COALESCE(cls.is_featured, l.default_is_featured) AS is_featured, + cls.updated_at +FROM leagues l + JOIN company_league_settings cls ON leagues.id = cls.league_id; +CREATE VIEW event_with_settings AS +SELECT e.*, + ces.company_id, + COALESCE(ces.is_active, e.default_is_active) AS is_active, + COALESCE(ces.is_featured, e.default_is_featured) AS is_featured, + COALESCE( + ces.winning_upper_limit, + e.default_winning_upper_limit + ) AS winning_upper_limit, + ces.updated_at, + l.country_code as league_cc +FROM events e + JOIN company_event_settings ces ON events.id = ces.event_id + JOIN leagues l ON leagues.id = e.league_id; +CREATE VIEW event_with_country AS +SELECT events.*, + leagues.country_code as league_cc +FROM events + LEFT JOIN leagues ON leagues.id = league_id; +CREATE VIEW odds_market_with_settings AS +SELECT o.*, + cos.company_id, + COALESCE(cos.is_active, o.default_is_active) AS is_active, + COALESCE(cos.custom_raw_odds, o.raw_odds) AS raw_odds, + cos.updated_at +FROM odds_market o + JOIN company_odd_settings cos ON o.id = cos.odds_market_id; +CREATE VIEW odds_market_with_event AS +SELECT o.*, + e.is_monitored, + e.is_live, + e.status, + e.source +FROM odds_market o + JOIN events e ON o.event_id = e.id; -- Foreign Keys -ALTER TABLE users -ADD CONSTRAINT unique_email UNIQUE (email), - ADD CONSTRAINT unique_phone_number UNIQUE (phone_number); ALTER TABLE refresh_tokens ADD CONSTRAINT fk_refresh_tokens_users FOREIGN KEY (user_id) REFERENCES users(id); ALTER TABLE bets ADD CONSTRAINT fk_bets_users FOREIGN KEY (user_id) REFERENCES users(id); ALTER TABLE wallets -ADD CONSTRAINT fk_wallets_users FOREIGN KEY (user_id) REFERENCES users(id), - ADD COLUMN currency VARCHAR(3) NOT NULL DEFAULT 'ETB'; +ADD CONSTRAINT fk_wallets_users FOREIGN KEY (user_id) REFERENCES users(id); ALTER TABLE customer_wallets ADD CONSTRAINT fk_customer_wallets_customers FOREIGN KEY (customer_id) REFERENCES users(id), ADD CONSTRAINT fk_customer_wallets_regular_wallet FOREIGN KEY (regular_wallet_id) REFERENCES wallets(id), @@ -584,4 +651,7 @@ ADD CONSTRAINT fk_branch_cashiers_users FOREIGN KEY (user_id) REFERENCES users(i ADD CONSTRAINT fk_branch_cashiers_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE; ALTER TABLE companies ADD CONSTRAINT fk_companies_admin FOREIGN KEY (admin_id) REFERENCES users(id), - ADD CONSTRAINT fk_companies_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE; \ No newline at end of file + ADD CONSTRAINT fk_companies_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE; +ALTER TABLE company_league_settings +ADD CONSTRAINT fk_league_settings_company FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, + ADD CONSTRAINT fk_league_settings_league FOREIGN KEY (league_id) REFERENCES league_id(id) ON DELETE CASCADE; \ No newline at end of file diff --git a/db/query/auth.sql b/db/query/auth.sql index 0444eff..3bd28bb 100644 --- a/db/query/auth.sql +++ b/db/query/auth.sql @@ -1,12 +1,17 @@ -- name: GetUserByEmailPhone :one SELECT * FROM users -WHERE email = $1 - OR phone_number = $2; +WHERE ( + email = $1 + OR phone_number = $2 + ) + AND ( + company_id = sqlc.narg('company_id') + OR sqlc.narg('company_id') IS NULL + ); -- name: CreateRefreshToken :exec INSERT INTO refresh_tokens (user_id, token, expires_at, created_at, revoked) VALUES ($1, $2, $3, $4, $5); - -- name: GetRefreshToken :one SELECT * FROM refresh_tokens diff --git a/db/query/company.sql b/db/query/company.sql index 32c7b2f..00128f5 100644 --- a/db/query/company.sql +++ b/db/query/company.sql @@ -1,11 +1,12 @@ -- name: CreateCompany :one INSERT INTO companies ( name, + slug, admin_id, wallet_id, deducted_percentage ) -VALUES ($1, $2, $3, $4) +VALUES ($1, $2, $3, $4, $5) RETURNING *; -- name: GetAllCompanies :many SELECT * @@ -29,6 +30,10 @@ WHERE ( SELECT * FROM companies_details WHERE id = $1; +-- name: GetCompanyIDUsingSlug :one +SELECT id +FROM companies +WHERE slug = $1; -- name: SearchCompanyByName :many SELECT * FROM companies_details diff --git a/db/query/custom_odds.sql b/db/query/custom_odds.sql index 199cda1..e4357e9 100644 --- a/db/query/custom_odds.sql +++ b/db/query/custom_odds.sql @@ -1,26 +1,35 @@ --- name: InsertCustomOdd :one -INSERT INTO custom_odd ( - odd_id, - raw_odd_id, - event_id, - odd_value - ) -VALUES ($1, $2, $3, $4) -RETURNING *; --- name: GetAllCustomOdds :many -SELECT * -FROM custom_odd; --- name: GetCustomOddByRawOddID :one -SELECT * -FROM custom_odd -WHERE raw_odd_id = $1; --- name: GetCustomOddByID :one -SELECT * -FROM custom_odd -WHERE id = $1; --- name: DeleteCustomOddsByID :exec -DELETE FROM disabled_odd -WHERE raw_odd_id = $1; --- name: DeleteCustomOddsByRawOddID :exec -DELETE FROM disabled_odd -WHERE raw_odd_id = $1; \ No newline at end of file +-- -- name: InsertCustomOddsMarket :one +-- INSERT INTO custom_odds_market ( +-- odds_market_id, +-- company_id, +-- event_id, +-- raw_odds +-- ) +-- VALUES ($1, $2, $3, $4) +-- RETURNING *; +-- -- name: GetAllCustomOdds :many +-- SELECT * +-- FROM custom_odds_market +-- WHERE ( +-- company_id = sqlc.narg('company_id') +-- OR sqlc.narg('company_id') IS NULL +-- ); +-- -- name: GetCustomOddByID :one +-- SELECT * +-- FROM custom_odds_market +-- WHERE id = $1; +-- -- name: GetCustomOddByOddID :one +-- SELECT * +-- FROM custom_odds_market +-- WHERE odds_market_id = $1 +-- AND company_id = $2; +-- -- name: DeleteCustomOddsByID :exec +-- DELETE FROM custom_odds_market +-- WHERE id = $1; +-- -- name: DeleteCustomOddsByOddID :exec +-- DELETE FROM custom_odds_market +-- WHERE odds_market_id = $1 +-- AND company_id = $2; +-- -- name: DeleteCustomOddByEventID :exec +-- DELETE FROM custom_odds_market +-- WHERE event_id = $1; \ No newline at end of file diff --git a/db/query/disabled_odds.sql b/db/query/disabled_odds.sql index 9e328f1..d890f7d 100644 --- a/db/query/disabled_odds.sql +++ b/db/query/disabled_odds.sql @@ -1,10 +1,11 @@ -- name: InsertDisabledOdds :one INSERT INTO disabled_odd ( - odd_id, + odds_market_id, + company_id, event_id, raw_odd_id ) -VALUES ($1, $2, $3) +VALUES ($1, $2, $3, $4) RETURNING *; -- name: GetAllDisabledOdds :many SELECT * diff --git a/db/query/events.sql b/db/query/events.sql index 893081e..1a61445 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -11,13 +11,7 @@ INSERT INTO events ( away_kit_image, league_id, league_name, - league_cc, start_time, - score, - match_minute, - timer_status, - added_time, - match_period, is_live, status, source @@ -37,13 +31,7 @@ VALUES ( $12, $13, $14, - $15, - $16, - $17, - $18, - $19, - $20, - $21 + $15 ) ON CONFLICT (id) DO UPDATE SET sport_id = EXCLUDED.sport_id, @@ -64,79 +52,35 @@ SET sport_id = EXCLUDED.sport_id, added_time = EXCLUDED.added_time, match_period = EXCLUDED.match_period, is_live = EXCLUDED.is_live, - status = EXCLUDED.status, source = EXCLUDED.source, fetched_at = now(); --- name: InsertUpcomingEvent :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, - is_live, - status, - source +-- name: InsertEventSettings :exec +INSERT INTO company_event_settings ( + company_id, + event_id, + is_active, + is_featured, + winning_upper_limit ) -VALUES ( - $1, - $2, - $3, - $4, - $5, - $6, - $7, - $8, - $9, - $10, - $11, - $12, - $13, - false, - 'upcoming', - $14 - ) ON CONFLICT (id) DO +VALUES ($1, $2, $3, $4, $5) ON CONFLICT(company_id, event_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, - is_live = false, - status = 'upcoming', - source = EXCLUDED.source, - fetched_at = now(); +SET is_active = EXCLUDED.is_active, + is_featured = EXCLUDED.is_featured, + winning_upper_limit = EXCLUDED.winning_upper_limit; -- name: ListLiveEvents :many SELECT id -FROM events +FROM event_with_country WHERE is_live = true; -- name: GetAllUpcomingEvents :many SELECT * -FROM events +FROM event_with_country WHERE start_time > now() AND is_live = false AND status = 'upcoming' ORDER BY start_time ASC; --- name: GetExpiredUpcomingEvents :many -SELECT events.*, - leagues.country_code as league_cc -FROM events - LEFT JOIN leagues ON leagues.id = league_id +-- name: GetExpiredEvents :many +SELECT * +FROM event_with_country WHERE start_time < now() and ( status = sqlc.narg('status') @@ -145,8 +89,7 @@ WHERE start_time < now() ORDER BY start_time ASC; -- name: GetTotalEvents :one SELECT COUNT(*) -FROM events - LEFT JOIN leagues ON leagues.id = league_id +FROM event_with_country WHERE is_live = false AND status = 'upcoming' AND ( @@ -154,7 +97,7 @@ WHERE is_live = false OR sqlc.narg('league_id') IS NULL ) AND ( - events.sport_id = sqlc.narg('sport_id') + sport_id = sqlc.narg('sport_id') OR sqlc.narg('sport_id') IS NULL ) AND ( @@ -171,18 +114,12 @@ WHERE is_live = false OR sqlc.narg('first_start_time') IS NULL ) AND ( - leagues.country_code = sqlc.narg('country_code') + league_cc = sqlc.narg('country_code') OR sqlc.narg('country_code') IS NULL - ) - AND ( - events.is_featured = sqlc.narg('is_featured') - OR sqlc.narg('is_featured') IS NULL ); -- name: GetPaginatedUpcomingEvents :many -SELECT events.*, - leagues.country_code as league_cc -FROM events - LEFT JOIN leagues ON leagues.id = league_id +SELECT * +FROM event_with_country WHERE start_time > now() AND is_live = false AND status = 'upcoming' @@ -191,7 +128,7 @@ WHERE start_time > now() OR sqlc.narg('league_id') IS NULL ) AND ( - events.sport_id = sqlc.narg('sport_id') + sport_id = sqlc.narg('sport_id') OR sqlc.narg('sport_id') IS NULL ) AND ( @@ -208,31 +145,96 @@ WHERE start_time > now() OR sqlc.narg('first_start_time') IS NULL ) AND ( - leagues.country_code = sqlc.narg('country_code') + league_cc = sqlc.narg('country_code') OR sqlc.narg('country_code') IS NULL ) +ORDER BY start_time ASC +LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); +-- name: GetTotalCompanyEvents :one +SELECT COUNT(*) +FROM event_with_settings +WHERE company_id = $1 + AND is_live = false + AND status = 'upcoming' AND ( - events.is_featured = sqlc.narg('is_featured') - OR sqlc.narg('is_featured') IS NULL + league_id = sqlc.narg('league_id') + OR sqlc.narg('league_id') IS NULL + ) + AND ( + sport_id = sqlc.narg('sport_id') + OR sqlc.narg('sport_id') IS NULL + ) + AND ( + match_name ILIKE '%' || sqlc.narg('query') || '%' + OR league_name ILIKE '%' || sqlc.narg('query') || '%' + OR sqlc.narg('query') IS NULL + ) + AND ( + start_time < sqlc.narg('last_start_time') + OR sqlc.narg('last_start_time') IS NULL + ) + AND ( + start_time > sqlc.narg('first_start_time') + OR sqlc.narg('first_start_time') IS NULL + ) + AND ( + league_cc = sqlc.narg('country_code') + OR sqlc.narg('country_code') IS NULL + ); +-- name: GetEventsWithSettings :many +SELECT * +FROM event_with_settings +WHERE company_id = $1 + AND start_time > now() + AND is_live = false + AND status = 'upcoming' + AND ( + league_id = sqlc.narg('league_id') + OR sqlc.narg('league_id') IS NULL + ) + AND ( + sport_id = sqlc.narg('sport_id') + OR sqlc.narg('sport_id') IS NULL + ) + AND ( + match_name ILIKE '%' || sqlc.narg('query') || '%' + OR league_name ILIKE '%' || sqlc.narg('query') || '%' + OR sqlc.narg('query') IS NULL + ) + AND ( + start_time < sqlc.narg('last_start_time') + OR sqlc.narg('last_start_time') IS NULL + ) + AND ( + start_time > sqlc.narg('first_start_time') + OR sqlc.narg('first_start_time') IS NULL + ) + AND ( + league_cc = sqlc.narg('country_code') + OR sqlc.narg('country_code') IS NULL ) ORDER BY start_time ASC LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- name: GetUpcomingByID :one SELECT * -FROM events +FROM event_with_country WHERE id = $1 AND is_live = false AND status = 'upcoming' LIMIT 1; +-- name: GetEventWithSettingByID :one +SELECT * +FROM event_with_settings +WHERE id = $1 + AND company_id = $2 + AND is_live = false + AND status = 'upcoming' +LIMIT 1; -- name: UpdateMatchResult :exec UPDATE events SET score = $1, status = $2 WHERE id = $3; --- name: UpdateEventFeatured :exec -UPDATE events -SET is_featured = $1 -WHERE id = $2; -- name: IsEventMonitored :one SELECT is_monitored FROM events @@ -241,6 +243,19 @@ WHERE id = $1; UPDATE events SET is_monitored = $1 WHERE id = $2; +-- name: UpdateEventSettings :exec +UPDATE company_event_settings +SET is_active = COALESCE(sqlc.narg('is_active'), is_active), + is_featured = COALESCE( + sqlc.narg('is_featured'), + is_featured + ), + winning_upper_limit = COALESCE( + sqlc.narg('winning_upper_limit'), + winning_upper_limit + ) +WHERE event_id = $1 + AND company_id = $2; -- name: DeleteEvent :exec DELETE FROM events WHERE id = $1; \ No newline at end of file diff --git a/db/query/events_stat.sql b/db/query/events_stat.sql index 733969e..cf96ad5 100644 --- a/db/query/events_stat.sql +++ b/db/query/events_stat.sql @@ -4,20 +4,11 @@ SELECT DATE_TRUNC('month', start_time) AS month, FROM events JOIN leagues ON leagues.id = events.league_id WHERE ( - events.is_featured = sqlc.narg('is_event_featured') - OR sqlc.narg('is_event_featured') IS NULL - ) - AND ( - leagues.is_featured = sqlc.narg('is_league_featured') - OR sqlc.narg('is_league_featured') IS NULL - ) - AND ( events.league_id = sqlc.narg('league_id') OR sqlc.narg('league_id') IS NULL ) GROUP BY month ORDER BY month; - -- name: GetLeagueEventStat :many SELECT leagues.id, leagues.name, @@ -63,9 +54,5 @@ SELECT leagues.id, ) AS removed FROM leagues JOIN events ON leagues.id = events.league_id -WHERE ( - leagues.is_featured = sqlc.narg('is_league_featured') - OR sqlc.narg('is_league_featured') IS NULL - ) GROUP BY leagues.id, leagues.name; \ No newline at end of file diff --git a/db/query/flags.sql b/db/query/flags.sql index 60093c5..bb36e8b 100644 --- a/db/query/flags.sql +++ b/db/query/flags.sql @@ -1,7 +1,7 @@ -- name: CreateFlag :one INSERT INTO flags ( bet_id, - odd_id, + odds_market_id, reason ) VALUES ( $1, $2, $3 diff --git a/db/query/leagues.sql b/db/query/leagues.sql index 368da67..fbbc562 100644 --- a/db/query/leagues.sql +++ b/db/query/leagues.sql @@ -5,25 +5,28 @@ INSERT INTO leagues ( country_code, bet365_id, sport_id, - is_active, - is_featured + default_is_active, + default_is_featured ) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, country_code = EXCLUDED.country_code, bet365_id = EXCLUDED.bet365_id, - is_active = EXCLUDED.is_active, - is_featured = EXCLUDED.is_featured, sport_id = EXCLUDED.sport_id; +-- name: InsertLeagueSettings :exec +INSERT INTO company_league_settings ( + company_id, + league_id, + is_active, + is_featured + ) +VALUES ($1, $2, $3, $4) ON CONFLICT(company_id, league_id) DO +UPDATE +SET is_active = EXCLUDED.is_active, + is_featured = EXCLUDED.is_featured; -- name: GetAllLeagues :many -SELECT id, - name, - country_code, - bet365_id, - is_active, - is_featured, - sport_id +SELECT * FROM leagues WHERE ( country_code = sqlc.narg('country_code') @@ -33,6 +36,20 @@ WHERE ( sport_id = sqlc.narg('sport_id') OR sqlc.narg('sport_id') IS NULL ) +ORDER BY name ASC +LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); +-- name: GetAllLeaguesWithSettings :many +SELECT * +FROM league_with_settings +WHERE (company_id = $1) + AND ( + country_code = sqlc.narg('country_code') + OR sqlc.narg('country_code') IS NULL + ) + AND ( + sport_id = sqlc.narg('sport_id') + OR sqlc.narg('sport_id') IS NULL + ) AND ( is_active = sqlc.narg('is_active') OR sqlc.narg('is_active') IS NULL @@ -44,21 +61,12 @@ WHERE ( ORDER BY is_featured DESC, name ASC LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); --- name: GetFeaturedLeagues :many -SELECT id, - name, - country_code, - bet365_id, - is_active, - is_featured, - sport_id -FROM leagues -WHERE is_featured = true; -- name: CheckLeagueSupport :one SELECT EXISTS( SELECT 1 - FROM leagues - WHERE id = $1 + FROM company_league_settings + WHERE league_id = $1 + AND company_id = $2 AND is_active = true ); -- name: UpdateLeague :exec @@ -66,20 +74,14 @@ UPDATE leagues SET name = COALESCE(sqlc.narg('name'), name), country_code = COALESCE(sqlc.narg('country_code'), country_code), bet365_id = COALESCE(sqlc.narg('bet365_id'), bet365_id), - is_active = COALESCE(sqlc.narg('is_active'), is_active), - is_featured = COALESCE(sqlc.narg('is_featured'), is_featured), sport_id = COALESCE(sqlc.narg('sport_id'), sport_id) WHERE id = $1; --- name: UpdateLeagueByBet365ID :exec -UPDATE leagues -SET name = COALESCE(sqlc.narg('name'), name), - id = COALESCE(sqlc.narg('id'), id), - country_code = COALESCE(sqlc.narg('country_code'), country_code), - is_active = COALESCE(sqlc.narg('is_active'), is_active), - is_featured = COALESCE(sqlc.narg('is_featured'), is_featured), - sport_id = COALESCE(sqlc.narg('sport_id'), sport_id) -WHERE bet365_id = $1; --- name: SetLeagueActive :exec -UPDATE leagues -SET is_active = $2 -WHERE id = $1; \ No newline at end of file +-- name: UpdateLeagueSettings :exec +UPDATE company_league_settings +SET is_active = COALESCE(sqlc.narg('is_active'), is_active), + is_featured = COALESCE( + sqlc.narg('is_featured'), + is_featured + ) +WHERE league_id = $1 + AND company_id = $2; \ No newline at end of file diff --git a/db/query/odd_history.sql b/db/query/odd_history.sql index f7368fc..b040c2e 100644 --- a/db/query/odd_history.sql +++ b/db/query/odd_history.sql @@ -1,6 +1,6 @@ -- name: InsertOddHistory :one INSERT INTO odd_history ( - odd_id, + odds_market_id, market_id, raw_odd_id, event_id, @@ -12,7 +12,7 @@ RETURNING *; SELECT * FROM odd_history WHERE ( - odd_id = sqlc.narg('odd_id') + odds_market_id = sqlc.narg('odd_id') OR sqlc.narg('odd_id') IS NULL ) AND ( @@ -39,7 +39,7 @@ WHERE ( SELECT DISTINCT ON (DATE_TRUNC($1, created_at)) * FROM odd_history WHERE ( - odd_id = sqlc.narg('odd_id') + odds_market_id = sqlc.narg('odd_id') OR sqlc.narg('odd_id') IS NULL ) AND ( diff --git a/db/query/odds.sql b/db/query/odds.sql index 4699000..8c231b1 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -1,19 +1,11 @@ --- name: InsertNonLiveOdd :exec -INSERT INTO odds ( +-- name: InsertOddsMarket :exec +INSERT INTO odds_market ( event_id, - fi, market_type, market_name, market_category, market_id, - name, - handicap, - odds_value, - section, - category, raw_odds, - is_active, - source, fetched_at, expires_at ) @@ -25,64 +17,69 @@ VALUES ( $5, $6, $7, - $8, - $9, - $10, - $11, - $12, - $13, - $14, - $15, - $16 + $8 ) ON CONFLICT (event_id, market_id) DO UPDATE -SET odds_value = EXCLUDED.odds_value, - raw_odds = EXCLUDED.raw_odds, - market_type = EXCLUDED.market_type, +SET market_type = EXCLUDED.market_type, market_name = EXCLUDED.market_name, market_category = EXCLUDED.market_category, - name = EXCLUDED.name, - handicap = EXCLUDED.handicap, + raw_odds = EXCLUDED.raw_odds, fetched_at = EXCLUDED.fetched_at, - is_active = EXCLUDED.is_active, - source = EXCLUDED.source, - fi = EXCLUDED.fi; --- name: GetPrematchOdds :many + expires_at = EXCLUDED.expires_at; +-- name: InsertOddSettings :exec +INSERT INTO company_odd_settings ( + company_id, + odds_market_id, + is_active, + custom_raw_odds + ) +VALUES ($1, $2, $3, $4) ON CONFLICT (company_id, odds_market_id) DO +UPDATE +SET is_active = EXCLUDED.is_active, + custom_raw_odds = EXCLUDED.custom_raw_odds; +-- name: GetAllOdds :many SELECT * -FROM odds -WHERE is_active = true - AND source = 'bet365'; --- name: GetALLPrematchOdds :many +FROM odds_market_with_event +LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); +-- name: GetAllOddsWithSettings :many SELECT * -FROM odds -WHERE is_active = true - AND source = 'bet365'; +FROM odds_market_with_settings +WHERE company_id = $1 +LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- name: GetOddsByMarketID :one SELECT * -FROM odds +FROM odds_market_with_event WHERE market_id = $1 - AND fi = $2 - AND is_active = true - AND source = 'bet365'; --- name: GetPrematchOddsByUpcomingID :many -SELECT o.* -FROM odds o - JOIN events e ON o.fi = e.id -WHERE e.id = $1 - AND e.is_live = false - AND e.status = 'upcoming' - AND o.is_active = true - AND o.source = 'bet365'; --- name: GetPaginatedPrematchOddsByUpcomingID :many -SELECT o.* -FROM odds o - JOIN events e ON o.fi = e.id -WHERE e.id = $1 - AND e.is_live = false - AND e.status = 'upcoming' - AND o.is_active = true - AND o.source = 'bet365' + AND event_id = $2; +-- name: GetOddsWithSettingsByMarketID :one +SELECT * +FROM odds_market_with_settings +WHERE market_id = $1 + AND event_id = $2 + AND company_id = $3; +-- name: GetOddsByEventID :many +SELECT * +FROM odds_market_with_event +WHERE event_id = $1 + AND ( + is_live = sqlc.narg('is_live') + OR sqlc.narg('is_live') IS NULL + ) + AND ( + status = sqlc.narg('status') + OR sqlc.narg('status') IS NULL + ) + AND ( + source = sqlc.narg('source') + OR sqlc.narg('source') IS NULL + ) +LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); +-- name: GetOddsWithSettingsByEventID :many +SELECT * +FROM odds_market_with_settings +WHERE event_id = $1 + AND company_id = $2 LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- name: DeleteOddsForEvent :exec -DELETE FROM odds -Where fi = $1; \ No newline at end of file +DELETE FROM odds_market +Where event_id = $1; \ No newline at end of file diff --git a/db/query/user.sql b/db/query/user.sql index d7eae90..1b408cf 100644 --- a/db/query/user.sql +++ b/db/query/user.sql @@ -107,18 +107,15 @@ SELECT id, suspended_at, company_id FROM users -WHERE ( - first_name ILIKE '%' || $1 || '%' - OR last_name ILIKE '%' || $1 || '%' - OR phone_number LIKE '%' || $1 || '%' +WHERE (company_id = $1) + AND ( + first_name ILIKE '%' || $2 || '%' + OR last_name ILIKE '%' || $2 || '%' + OR phone_number LIKE '%' || $2 || '%' ) AND ( role = sqlc.narg('role') OR sqlc.narg('role') IS NULL - ) - AND ( - company_id = sqlc.narg('company_id') - OR sqlc.narg('company_id') IS NULL ); -- name: UpdateUser :exec UPDATE users @@ -146,12 +143,14 @@ SELECT EXISTS ( FROM users WHERE users.phone_number = $1 AND users.phone_number IS NOT NULL + AND users.company_id = $2 ) AS phone_exists, EXISTS ( SELECT 1 FROM users - WHERE users.email = $2 + WHERE users.email = $3 AND users.email IS NOT NULL + AND users.company_id = $2 ) AS email_exists; -- name: GetUserByEmail :one SELECT id, @@ -168,7 +167,8 @@ SELECT id, suspended_at, company_id FROM users -WHERE email = $1; +WHERE email = $1 + AND company_id = $2; -- name: GetUserByPhone :one SELECT id, first_name, @@ -184,7 +184,8 @@ SELECT id, suspended_at, company_id FROM users -WHERE phone_number = $1; +WHERE phone_number = $1 + AND company_id = $2; -- name: UpdatePassword :exec UPDATE users SET password = $1, @@ -192,6 +193,7 @@ SET password = $1, WHERE ( email = $2 OR phone_number = $3 + AND company_id = $4 ); -- name: GetAdminByCompanyID :one SELECT users.* diff --git a/gen/db/auth.sql.go b/gen/db/auth.sql.go index 9c55b29..1817514 100644 --- a/gen/db/auth.sql.go +++ b/gen/db/auth.sql.go @@ -78,17 +78,24 @@ func (q *Queries) GetRefreshTokenByUserID(ctx context.Context, userID int64) (Re const GetUserByEmailPhone = `-- name: GetUserByEmailPhone :one SELECT id, first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at, company_id, suspended_at, suspended, referral_code, referred_by FROM users -WHERE email = $1 - OR phone_number = $2 +WHERE ( + email = $1 + OR phone_number = $2 + ) + AND ( + company_id = $3 + OR $3 IS NULL + ) ` type GetUserByEmailPhoneParams struct { Email pgtype.Text `json:"email"` PhoneNumber pgtype.Text `json:"phone_number"` + CompanyID pgtype.Int8 `json:"company_id"` } func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPhoneParams) (User, error) { - row := q.db.QueryRow(ctx, GetUserByEmailPhone, arg.Email, arg.PhoneNumber) + row := q.db.QueryRow(ctx, GetUserByEmailPhone, arg.Email, arg.PhoneNumber, arg.CompanyID) var i User err := row.Scan( &i.ID, diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index 31ca511..29d1e10 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -22,7 +22,7 @@ INSERT INTO bets ( fast_code ) VALUES ($1, $2, $3, $4, $5, $6, $7) -RETURNING id, amount, total_odds, status, user_id, is_shop_bet, cashed_out, outcomes_hash, fast_code, processed, created_at, updated_at +RETURNING id, company_id, amount, total_odds, status, user_id, is_shop_bet, cashed_out, outcomes_hash, fast_code, processed, created_at, updated_at ` type CreateBetParams struct { @@ -48,6 +48,7 @@ func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, erro var i Bet err := row.Scan( &i.ID, + &i.CompanyID, &i.Amount, &i.TotalOdds, &i.Status, @@ -100,7 +101,7 @@ func (q *Queries) DeleteBetOutcome(ctx context.Context, betID int64) error { } const GetAllBets = `-- name: GetAllBets :many -SELECT id, amount, total_odds, status, user_id, is_shop_bet, cashed_out, outcomes_hash, fast_code, processed, created_at, updated_at, full_name, phone_number, outcomes +SELECT id, company_id, amount, total_odds, status, user_id, is_shop_bet, cashed_out, outcomes_hash, fast_code, processed, created_at, updated_at, full_name, phone_number, outcomes FROM bet_with_outcomes wHERE ( user_id = $1 @@ -156,6 +157,7 @@ func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWi var i BetWithOutcome if err := rows.Scan( &i.ID, + &i.CompanyID, &i.Amount, &i.TotalOdds, &i.Status, @@ -182,7 +184,7 @@ func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWi } const GetBetByFastCode = `-- name: GetBetByFastCode :one -SELECT id, amount, total_odds, status, user_id, is_shop_bet, cashed_out, outcomes_hash, fast_code, processed, created_at, updated_at, full_name, phone_number, outcomes +SELECT id, company_id, amount, total_odds, status, user_id, is_shop_bet, cashed_out, outcomes_hash, fast_code, processed, created_at, updated_at, full_name, phone_number, outcomes FROM bet_with_outcomes WHERE fast_code = $1 LIMIT 1 @@ -193,6 +195,7 @@ func (q *Queries) GetBetByFastCode(ctx context.Context, fastCode string) (BetWit var i BetWithOutcome err := row.Scan( &i.ID, + &i.CompanyID, &i.Amount, &i.TotalOdds, &i.Status, @@ -212,7 +215,7 @@ func (q *Queries) GetBetByFastCode(ctx context.Context, fastCode string) (BetWit } const GetBetByID = `-- name: GetBetByID :one -SELECT id, amount, total_odds, status, user_id, is_shop_bet, cashed_out, outcomes_hash, fast_code, processed, created_at, updated_at, full_name, phone_number, outcomes +SELECT id, company_id, amount, total_odds, status, user_id, is_shop_bet, cashed_out, outcomes_hash, fast_code, processed, created_at, updated_at, full_name, phone_number, outcomes FROM bet_with_outcomes WHERE id = $1 ` @@ -222,6 +225,7 @@ func (q *Queries) GetBetByID(ctx context.Context, id int64) (BetWithOutcome, err var i BetWithOutcome err := row.Scan( &i.ID, + &i.CompanyID, &i.Amount, &i.TotalOdds, &i.Status, @@ -241,7 +245,7 @@ func (q *Queries) GetBetByID(ctx context.Context, id int64) (BetWithOutcome, err } const GetBetByUserID = `-- name: GetBetByUserID :many -SELECT id, amount, total_odds, status, user_id, is_shop_bet, cashed_out, outcomes_hash, fast_code, processed, created_at, updated_at, full_name, phone_number, outcomes +SELECT id, company_id, amount, total_odds, status, user_id, is_shop_bet, cashed_out, outcomes_hash, fast_code, processed, created_at, updated_at, full_name, phone_number, outcomes FROM bet_with_outcomes WHERE user_id = $1 ` @@ -257,6 +261,7 @@ func (q *Queries) GetBetByUserID(ctx context.Context, userID int64) ([]BetWithOu var i BetWithOutcome if err := rows.Scan( &i.ID, + &i.CompanyID, &i.Amount, &i.TotalOdds, &i.Status, @@ -424,7 +429,7 @@ func (q *Queries) GetBetOutcomeCountByOddID(ctx context.Context, oddID int64) (i } const GetBetsForCashback = `-- name: GetBetsForCashback :many -SELECT id, amount, total_odds, status, user_id, is_shop_bet, cashed_out, outcomes_hash, fast_code, processed, created_at, updated_at, full_name, phone_number, outcomes +SELECT id, company_id, amount, total_odds, status, user_id, is_shop_bet, cashed_out, outcomes_hash, fast_code, processed, created_at, updated_at, full_name, phone_number, outcomes FROM bet_with_outcomes WHERE status = 2 AND processed = false @@ -441,6 +446,7 @@ func (q *Queries) GetBetsForCashback(ctx context.Context) ([]BetWithOutcome, err var i BetWithOutcome if err := rows.Scan( &i.ID, + &i.CompanyID, &i.Amount, &i.TotalOdds, &i.Status, diff --git a/gen/db/company.sql.go b/gen/db/company.sql.go index ac5980a..506eaca 100644 --- a/gen/db/company.sql.go +++ b/gen/db/company.sql.go @@ -14,16 +14,18 @@ import ( const CreateCompany = `-- name: CreateCompany :one INSERT INTO companies ( name, + slug, admin_id, wallet_id, deducted_percentage ) -VALUES ($1, $2, $3, $4) -RETURNING id, name, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at +VALUES ($1, $2, $3, $4, $5) +RETURNING id, name, slug, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at ` type CreateCompanyParams struct { Name string `json:"name"` + Slug string `json:"slug"` AdminID int64 `json:"admin_id"` WalletID int64 `json:"wallet_id"` DeductedPercentage float32 `json:"deducted_percentage"` @@ -32,6 +34,7 @@ type CreateCompanyParams struct { func (q *Queries) CreateCompany(ctx context.Context, arg CreateCompanyParams) (Company, error) { row := q.db.QueryRow(ctx, CreateCompany, arg.Name, + arg.Slug, arg.AdminID, arg.WalletID, arg.DeductedPercentage, @@ -40,6 +43,7 @@ func (q *Queries) CreateCompany(ctx context.Context, arg CreateCompanyParams) (C err := row.Scan( &i.ID, &i.Name, + &i.Slug, &i.AdminID, &i.WalletID, &i.DeductedPercentage, @@ -61,7 +65,7 @@ func (q *Queries) DeleteCompany(ctx context.Context, id int64) error { } const GetAllCompanies = `-- name: GetAllCompanies :many -SELECT id, name, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at, balance, wallet_is_active, admin_first_name, admin_last_name, admin_phone_number +SELECT id, name, slug, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at, balance, wallet_is_active, admin_first_name, admin_last_name, admin_phone_number FROM companies_details WHERE ( name ILIKE '%' || $1 || '%' @@ -98,6 +102,7 @@ func (q *Queries) GetAllCompanies(ctx context.Context, arg GetAllCompaniesParams if err := rows.Scan( &i.ID, &i.Name, + &i.Slug, &i.AdminID, &i.WalletID, &i.DeductedPercentage, @@ -121,7 +126,7 @@ func (q *Queries) GetAllCompanies(ctx context.Context, arg GetAllCompaniesParams } const GetCompanyByID = `-- name: GetCompanyByID :one -SELECT id, name, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at, balance, wallet_is_active, admin_first_name, admin_last_name, admin_phone_number +SELECT id, name, slug, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at, balance, wallet_is_active, admin_first_name, admin_last_name, admin_phone_number FROM companies_details WHERE id = $1 ` @@ -132,6 +137,7 @@ func (q *Queries) GetCompanyByID(ctx context.Context, id int64) (CompaniesDetail err := row.Scan( &i.ID, &i.Name, + &i.Slug, &i.AdminID, &i.WalletID, &i.DeductedPercentage, @@ -147,8 +153,21 @@ func (q *Queries) GetCompanyByID(ctx context.Context, id int64) (CompaniesDetail return i, err } +const GetCompanyIDUsingSlug = `-- name: GetCompanyIDUsingSlug :one +SELECT id +FROM companies +WHERE slug = $1 +` + +func (q *Queries) GetCompanyIDUsingSlug(ctx context.Context, slug string) (int64, error) { + row := q.db.QueryRow(ctx, GetCompanyIDUsingSlug, slug) + var id int64 + err := row.Scan(&id) + return id, err +} + const SearchCompanyByName = `-- name: SearchCompanyByName :many -SELECT id, name, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at, balance, wallet_is_active, admin_first_name, admin_last_name, admin_phone_number +SELECT id, name, slug, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at, balance, wallet_is_active, admin_first_name, admin_last_name, admin_phone_number FROM companies_details WHERE name ILIKE '%' || $1 || '%' ` @@ -165,6 +184,7 @@ func (q *Queries) SearchCompanyByName(ctx context.Context, dollar_1 pgtype.Text) if err := rows.Scan( &i.ID, &i.Name, + &i.Slug, &i.AdminID, &i.WalletID, &i.DeductedPercentage, @@ -198,7 +218,7 @@ SET name = COALESCE($2, name), ), updated_at = CURRENT_TIMESTAMP WHERE id = $1 -RETURNING id, name, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at +RETURNING id, name, slug, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at ` type UpdateCompanyParams struct { @@ -221,6 +241,7 @@ func (q *Queries) UpdateCompany(ctx context.Context, arg UpdateCompanyParams) (C err := row.Scan( &i.ID, &i.Name, + &i.Slug, &i.AdminID, &i.WalletID, &i.DeductedPercentage, diff --git a/gen/db/custom_odds.sql.go b/gen/db/custom_odds.sql.go index 9e84561..efc1f02 100644 --- a/gen/db/custom_odds.sql.go +++ b/gen/db/custom_odds.sql.go @@ -7,48 +7,70 @@ package dbgen import ( "context" + + "github.com/jackc/pgx/v5/pgtype" ) -const DeleteCustomOddsByID = `-- name: DeleteCustomOddsByID :exec -DELETE FROM disabled_odd -WHERE raw_odd_id = $1 +const DeleteCustomOddByEventID = `-- name: DeleteCustomOddByEventID :exec +DELETE FROM custom_odds_market +WHERE event_id = $1 ` -func (q *Queries) DeleteCustomOddsByID(ctx context.Context, rawOddID int64) error { - _, err := q.db.Exec(ctx, DeleteCustomOddsByID, rawOddID) +func (q *Queries) DeleteCustomOddByEventID(ctx context.Context, eventID string) error { + _, err := q.db.Exec(ctx, DeleteCustomOddByEventID, eventID) return err } -const DeleteCustomOddsByRawOddID = `-- name: DeleteCustomOddsByRawOddID :exec -DELETE FROM disabled_odd -WHERE raw_odd_id = $1 +const DeleteCustomOddsByID = `-- name: DeleteCustomOddsByID :exec +DELETE FROM custom_odds_market +WHERE id = $1 ` -func (q *Queries) DeleteCustomOddsByRawOddID(ctx context.Context, rawOddID int64) error { - _, err := q.db.Exec(ctx, DeleteCustomOddsByRawOddID, rawOddID) +func (q *Queries) DeleteCustomOddsByID(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeleteCustomOddsByID, id) + return err +} + +const DeleteCustomOddsByOddID = `-- name: DeleteCustomOddsByOddID :exec +DELETE FROM custom_odds_market +WHERE odds_market_id = $1 + AND company_id = $2 +` + +type DeleteCustomOddsByOddIDParams struct { + OddsMarketID int64 `json:"odds_market_id"` + CompanyID int64 `json:"company_id"` +} + +func (q *Queries) DeleteCustomOddsByOddID(ctx context.Context, arg DeleteCustomOddsByOddIDParams) error { + _, err := q.db.Exec(ctx, DeleteCustomOddsByOddID, arg.OddsMarketID, arg.CompanyID) return err } const GetAllCustomOdds = `-- name: GetAllCustomOdds :many -SELECT id, odd_id, raw_odd_id, event_id, odd_value, created_at -FROM custom_odd +SELECT id, company_id, odds_market_id, event_id, raw_odds, created_at +FROM custom_odds_market +WHERE ( + company_id = $1 + OR $1 IS NULL + ) ` -func (q *Queries) GetAllCustomOdds(ctx context.Context) ([]CustomOdd, error) { - rows, err := q.db.Query(ctx, GetAllCustomOdds) +func (q *Queries) GetAllCustomOdds(ctx context.Context, companyID pgtype.Int8) ([]CustomOddsMarket, error) { + rows, err := q.db.Query(ctx, GetAllCustomOdds, companyID) if err != nil { return nil, err } defer rows.Close() - var items []CustomOdd + var items []CustomOddsMarket for rows.Next() { - var i CustomOdd + var i CustomOddsMarket if err := rows.Scan( &i.ID, - &i.OddID, - &i.RawOddID, + &i.CompanyID, + &i.OddsMarketID, &i.EventID, - &i.OddValue, + &i.RawOdds, &i.CreatedAt, ); err != nil { return nil, err @@ -62,77 +84,83 @@ func (q *Queries) GetAllCustomOdds(ctx context.Context) ([]CustomOdd, error) { } const GetCustomOddByID = `-- name: GetCustomOddByID :one -SELECT id, odd_id, raw_odd_id, event_id, odd_value, created_at -FROM custom_odd +SELECT id, company_id, odds_market_id, event_id, raw_odds, created_at +FROM custom_odds_market WHERE id = $1 ` -func (q *Queries) GetCustomOddByID(ctx context.Context, id int64) (CustomOdd, error) { +func (q *Queries) GetCustomOddByID(ctx context.Context, id int64) (CustomOddsMarket, error) { row := q.db.QueryRow(ctx, GetCustomOddByID, id) - var i CustomOdd + var i CustomOddsMarket err := row.Scan( &i.ID, - &i.OddID, - &i.RawOddID, + &i.CompanyID, + &i.OddsMarketID, &i.EventID, - &i.OddValue, + &i.RawOdds, &i.CreatedAt, ) return i, err } -const GetCustomOddByRawOddID = `-- name: GetCustomOddByRawOddID :one -SELECT id, odd_id, raw_odd_id, event_id, odd_value, created_at -FROM custom_odd -WHERE raw_odd_id = $1 +const GetCustomOddByOddID = `-- name: GetCustomOddByOddID :one +SELECT id, company_id, odds_market_id, event_id, raw_odds, created_at +FROM custom_odds_market +WHERE odds_market_id = $1 + AND company_id = $2 ` -func (q *Queries) GetCustomOddByRawOddID(ctx context.Context, rawOddID int64) (CustomOdd, error) { - row := q.db.QueryRow(ctx, GetCustomOddByRawOddID, rawOddID) - var i CustomOdd +type GetCustomOddByOddIDParams struct { + OddsMarketID int64 `json:"odds_market_id"` + CompanyID int64 `json:"company_id"` +} + +func (q *Queries) GetCustomOddByOddID(ctx context.Context, arg GetCustomOddByOddIDParams) (CustomOddsMarket, error) { + row := q.db.QueryRow(ctx, GetCustomOddByOddID, arg.OddsMarketID, arg.CompanyID) + var i CustomOddsMarket err := row.Scan( &i.ID, - &i.OddID, - &i.RawOddID, + &i.CompanyID, + &i.OddsMarketID, &i.EventID, - &i.OddValue, + &i.RawOdds, &i.CreatedAt, ) return i, err } -const InsertCustomOdd = `-- name: InsertCustomOdd :one -INSERT INTO custom_odd ( - odd_id, - raw_odd_id, +const InsertCustomOddsMarket = `-- name: InsertCustomOddsMarket :one +INSERT INTO custom_odds_market ( + odds_market_id, + company_id, event_id, - odd_value + raw_odds ) VALUES ($1, $2, $3, $4) -RETURNING id, odd_id, raw_odd_id, event_id, odd_value, created_at +RETURNING id, company_id, odds_market_id, event_id, raw_odds, created_at ` -type InsertCustomOddParams struct { - OddID int64 `json:"odd_id"` - RawOddID int64 `json:"raw_odd_id"` - EventID string `json:"event_id"` - OddValue float64 `json:"odd_value"` +type InsertCustomOddsMarketParams struct { + OddsMarketID int64 `json:"odds_market_id"` + CompanyID int64 `json:"company_id"` + EventID string `json:"event_id"` + RawOdds []byte `json:"raw_odds"` } -func (q *Queries) InsertCustomOdd(ctx context.Context, arg InsertCustomOddParams) (CustomOdd, error) { - row := q.db.QueryRow(ctx, InsertCustomOdd, - arg.OddID, - arg.RawOddID, +func (q *Queries) InsertCustomOddsMarket(ctx context.Context, arg InsertCustomOddsMarketParams) (CustomOddsMarket, error) { + row := q.db.QueryRow(ctx, InsertCustomOddsMarket, + arg.OddsMarketID, + arg.CompanyID, arg.EventID, - arg.OddValue, + arg.RawOdds, ) - var i CustomOdd + var i CustomOddsMarket err := row.Scan( &i.ID, - &i.OddID, - &i.RawOddID, + &i.CompanyID, + &i.OddsMarketID, &i.EventID, - &i.OddValue, + &i.RawOdds, &i.CreatedAt, ) return i, err diff --git a/gen/db/disabled_odds.sql.go b/gen/db/disabled_odds.sql.go index 19fa041..85dcd2e 100644 --- a/gen/db/disabled_odds.sql.go +++ b/gen/db/disabled_odds.sql.go @@ -30,7 +30,7 @@ func (q *Queries) DeleteDisabledOddsByRawOddID(ctx context.Context, rawOddID int } const GetAllDisabledOdds = `-- name: GetAllDisabledOdds :many -SELECT id, odd_id, raw_odd_id, event_id, created_at +SELECT id, company_id, odds_market_id, raw_odd_id, event_id, created_at FROM disabled_odd ` @@ -45,7 +45,8 @@ func (q *Queries) GetAllDisabledOdds(ctx context.Context) ([]DisabledOdd, error) var i DisabledOdd if err := rows.Scan( &i.ID, - &i.OddID, + &i.CompanyID, + &i.OddsMarketID, &i.RawOddID, &i.EventID, &i.CreatedAt, @@ -61,7 +62,7 @@ func (q *Queries) GetAllDisabledOdds(ctx context.Context) ([]DisabledOdd, error) } const GetDisabledOddByID = `-- name: GetDisabledOddByID :one -SELECT id, odd_id, raw_odd_id, event_id, created_at +SELECT id, company_id, odds_market_id, raw_odd_id, event_id, created_at FROM disabled_odd WHERE raw_odd_id = $1 ` @@ -71,7 +72,8 @@ func (q *Queries) GetDisabledOddByID(ctx context.Context, rawOddID int64) (Disab var i DisabledOdd err := row.Scan( &i.ID, - &i.OddID, + &i.CompanyID, + &i.OddsMarketID, &i.RawOddID, &i.EventID, &i.CreatedAt, @@ -80,7 +82,7 @@ func (q *Queries) GetDisabledOddByID(ctx context.Context, rawOddID int64) (Disab } const GetDisabledOddByRawOddID = `-- name: GetDisabledOddByRawOddID :one -SELECT id, odd_id, raw_odd_id, event_id, created_at +SELECT id, company_id, odds_market_id, raw_odd_id, event_id, created_at FROM disabled_odd WHERE raw_odd_id = $1 ` @@ -90,7 +92,8 @@ func (q *Queries) GetDisabledOddByRawOddID(ctx context.Context, rawOddID int64) var i DisabledOdd err := row.Scan( &i.ID, - &i.OddID, + &i.CompanyID, + &i.OddsMarketID, &i.RawOddID, &i.EventID, &i.CreatedAt, @@ -100,26 +103,34 @@ func (q *Queries) GetDisabledOddByRawOddID(ctx context.Context, rawOddID int64) const InsertDisabledOdds = `-- name: InsertDisabledOdds :one INSERT INTO disabled_odd ( - odd_id, + odds_market_id, + company_id, event_id, raw_odd_id ) -VALUES ($1, $2, $3) -RETURNING id, odd_id, raw_odd_id, event_id, created_at +VALUES ($1, $2, $3, $4) +RETURNING id, company_id, odds_market_id, raw_odd_id, event_id, created_at ` type InsertDisabledOddsParams struct { - OddID int64 `json:"odd_id"` - EventID string `json:"event_id"` - RawOddID int64 `json:"raw_odd_id"` + OddsMarketID int64 `json:"odds_market_id"` + CompanyID int64 `json:"company_id"` + EventID string `json:"event_id"` + RawOddID int64 `json:"raw_odd_id"` } func (q *Queries) InsertDisabledOdds(ctx context.Context, arg InsertDisabledOddsParams) (DisabledOdd, error) { - row := q.db.QueryRow(ctx, InsertDisabledOdds, arg.OddID, arg.EventID, arg.RawOddID) + row := q.db.QueryRow(ctx, InsertDisabledOdds, + arg.OddsMarketID, + arg.CompanyID, + arg.EventID, + arg.RawOddID, + ) var i DisabledOdd err := row.Scan( &i.ID, - &i.OddID, + &i.CompanyID, + &i.OddsMarketID, &i.RawOddID, &i.EventID, &i.CreatedAt, diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index 8e1978a..313b240 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -22,23 +22,23 @@ func (q *Queries) DeleteEvent(ctx context.Context, id string) error { } const GetAllUpcomingEvents = `-- name: GetAllUpcomingEvents :many -SELECT 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, fetched_at, source, is_featured, is_monitored, winning_upper_limit, is_active -FROM events +SELECT 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, start_time, score, match_minute, timer_status, added_time, match_period, is_live, status, fetched_at, source, default_is_active, default_is_featured, default_winning_upper_limit, is_monitored, league_cc +FROM event_with_country WHERE start_time > now() AND is_live = false AND status = 'upcoming' ORDER BY start_time ASC ` -func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]Event, error) { +func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]EventWithCountry, error) { rows, err := q.db.Query(ctx, GetAllUpcomingEvents) if err != nil { return nil, err } defer rows.Close() - var items []Event + var items []EventWithCountry for rows.Next() { - var i Event + var i EventWithCountry if err := rows.Scan( &i.ID, &i.SportID, @@ -51,7 +51,6 @@ func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]Event, error) { &i.AwayKitImage, &i.LeagueID, &i.LeagueName, - &i.LeagueCc, &i.StartTime, &i.Score, &i.MatchMinute, @@ -62,10 +61,11 @@ func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]Event, error) { &i.Status, &i.FetchedAt, &i.Source, - &i.IsFeatured, + &i.DefaultIsActive, + &i.DefaultIsFeatured, + &i.DefaultWinningUpperLimit, &i.IsMonitored, - &i.WinningUpperLimit, - &i.IsActive, + &i.LeagueCc, ); err != nil { return nil, err } @@ -77,312 +77,24 @@ func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]Event, error) { return items, nil } -const GetExpiredUpcomingEvents = `-- name: GetExpiredUpcomingEvents :many -SELECT events.id, events.sport_id, events.match_name, events.home_team, events.away_team, events.home_team_id, events.away_team_id, events.home_kit_image, events.away_kit_image, events.league_id, events.league_name, events.league_cc, events.start_time, events.score, events.match_minute, events.timer_status, events.added_time, events.match_period, events.is_live, events.status, events.fetched_at, events.source, events.is_featured, events.is_monitored, events.winning_upper_limit, events.is_active, - leagues.country_code as league_cc -FROM events - LEFT JOIN leagues ON leagues.id = league_id -WHERE start_time < now() - and ( - status = $1 - OR $1 IS NULL - ) -ORDER BY start_time ASC -` - -type GetExpiredUpcomingEventsRow struct { - ID string `json:"id"` - SportID pgtype.Int4 `json:"sport_id"` - MatchName pgtype.Text `json:"match_name"` - HomeTeam pgtype.Text `json:"home_team"` - AwayTeam pgtype.Text `json:"away_team"` - HomeTeamID pgtype.Int4 `json:"home_team_id"` - AwayTeamID pgtype.Int4 `json:"away_team_id"` - HomeKitImage pgtype.Text `json:"home_kit_image"` - AwayKitImage pgtype.Text `json:"away_kit_image"` - LeagueID pgtype.Int4 `json:"league_id"` - LeagueName pgtype.Text `json:"league_name"` - LeagueCc pgtype.Text `json:"league_cc"` - StartTime pgtype.Timestamp `json:"start_time"` - Score pgtype.Text `json:"score"` - MatchMinute pgtype.Int4 `json:"match_minute"` - TimerStatus pgtype.Text `json:"timer_status"` - AddedTime pgtype.Int4 `json:"added_time"` - MatchPeriod pgtype.Int4 `json:"match_period"` - IsLive pgtype.Bool `json:"is_live"` - Status pgtype.Text `json:"status"` - FetchedAt pgtype.Timestamp `json:"fetched_at"` - Source pgtype.Text `json:"source"` - IsFeatured bool `json:"is_featured"` - IsMonitored bool `json:"is_monitored"` - WinningUpperLimit int32 `json:"winning_upper_limit"` - IsActive bool `json:"is_active"` - LeagueCc_2 pgtype.Text `json:"league_cc_2"` -} - -func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context, status pgtype.Text) ([]GetExpiredUpcomingEventsRow, error) { - rows, err := q.db.Query(ctx, GetExpiredUpcomingEvents, status) - if err != nil { - return nil, err - } - defer rows.Close() - var items []GetExpiredUpcomingEventsRow - for rows.Next() { - var i GetExpiredUpcomingEventsRow - if err := rows.Scan( - &i.ID, - &i.SportID, - &i.MatchName, - &i.HomeTeam, - &i.AwayTeam, - &i.HomeTeamID, - &i.AwayTeamID, - &i.HomeKitImage, - &i.AwayKitImage, - &i.LeagueID, - &i.LeagueName, - &i.LeagueCc, - &i.StartTime, - &i.Score, - &i.MatchMinute, - &i.TimerStatus, - &i.AddedTime, - &i.MatchPeriod, - &i.IsLive, - &i.Status, - &i.FetchedAt, - &i.Source, - &i.IsFeatured, - &i.IsMonitored, - &i.WinningUpperLimit, - &i.IsActive, - &i.LeagueCc_2, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const GetPaginatedUpcomingEvents = `-- name: GetPaginatedUpcomingEvents :many -SELECT events.id, events.sport_id, events.match_name, events.home_team, events.away_team, events.home_team_id, events.away_team_id, events.home_kit_image, events.away_kit_image, events.league_id, events.league_name, events.league_cc, events.start_time, events.score, events.match_minute, events.timer_status, events.added_time, events.match_period, events.is_live, events.status, events.fetched_at, events.source, events.is_featured, events.is_monitored, events.winning_upper_limit, events.is_active, - leagues.country_code as league_cc -FROM events - LEFT JOIN leagues ON leagues.id = league_id -WHERE start_time > now() - AND is_live = false - AND status = 'upcoming' - AND ( - league_id = $1 - OR $1 IS NULL - ) - AND ( - events.sport_id = $2 - OR $2 IS NULL - ) - AND ( - match_name ILIKE '%' || $3 || '%' - OR league_name ILIKE '%' || $3 || '%' - OR $3 IS NULL - ) - AND ( - start_time < $4 - OR $4 IS NULL - ) - AND ( - start_time > $5 - OR $5 IS NULL - ) - AND ( - leagues.country_code = $6 - OR $6 IS NULL - ) - AND ( - events.is_featured = $7 - OR $7 IS NULL - ) -ORDER BY start_time ASC -LIMIT $9 OFFSET $8 -` - -type GetPaginatedUpcomingEventsParams struct { - LeagueID pgtype.Int4 `json:"league_id"` - SportID pgtype.Int4 `json:"sport_id"` - Query pgtype.Text `json:"query"` - LastStartTime pgtype.Timestamp `json:"last_start_time"` - FirstStartTime pgtype.Timestamp `json:"first_start_time"` - CountryCode pgtype.Text `json:"country_code"` - IsFeatured pgtype.Bool `json:"is_featured"` - Offset pgtype.Int4 `json:"offset"` - Limit pgtype.Int4 `json:"limit"` -} - -type GetPaginatedUpcomingEventsRow struct { - ID string `json:"id"` - SportID pgtype.Int4 `json:"sport_id"` - MatchName pgtype.Text `json:"match_name"` - HomeTeam pgtype.Text `json:"home_team"` - AwayTeam pgtype.Text `json:"away_team"` - HomeTeamID pgtype.Int4 `json:"home_team_id"` - AwayTeamID pgtype.Int4 `json:"away_team_id"` - HomeKitImage pgtype.Text `json:"home_kit_image"` - AwayKitImage pgtype.Text `json:"away_kit_image"` - LeagueID pgtype.Int4 `json:"league_id"` - LeagueName pgtype.Text `json:"league_name"` - LeagueCc pgtype.Text `json:"league_cc"` - StartTime pgtype.Timestamp `json:"start_time"` - Score pgtype.Text `json:"score"` - MatchMinute pgtype.Int4 `json:"match_minute"` - TimerStatus pgtype.Text `json:"timer_status"` - AddedTime pgtype.Int4 `json:"added_time"` - MatchPeriod pgtype.Int4 `json:"match_period"` - IsLive pgtype.Bool `json:"is_live"` - Status pgtype.Text `json:"status"` - FetchedAt pgtype.Timestamp `json:"fetched_at"` - Source pgtype.Text `json:"source"` - IsFeatured bool `json:"is_featured"` - IsMonitored bool `json:"is_monitored"` - WinningUpperLimit int32 `json:"winning_upper_limit"` - IsActive bool `json:"is_active"` - LeagueCc_2 pgtype.Text `json:"league_cc_2"` -} - -func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginatedUpcomingEventsParams) ([]GetPaginatedUpcomingEventsRow, error) { - rows, err := q.db.Query(ctx, GetPaginatedUpcomingEvents, - arg.LeagueID, - arg.SportID, - arg.Query, - arg.LastStartTime, - arg.FirstStartTime, - arg.CountryCode, - arg.IsFeatured, - arg.Offset, - arg.Limit, - ) - if err != nil { - return nil, err - } - defer rows.Close() - var items []GetPaginatedUpcomingEventsRow - for rows.Next() { - var i GetPaginatedUpcomingEventsRow - if err := rows.Scan( - &i.ID, - &i.SportID, - &i.MatchName, - &i.HomeTeam, - &i.AwayTeam, - &i.HomeTeamID, - &i.AwayTeamID, - &i.HomeKitImage, - &i.AwayKitImage, - &i.LeagueID, - &i.LeagueName, - &i.LeagueCc, - &i.StartTime, - &i.Score, - &i.MatchMinute, - &i.TimerStatus, - &i.AddedTime, - &i.MatchPeriod, - &i.IsLive, - &i.Status, - &i.FetchedAt, - &i.Source, - &i.IsFeatured, - &i.IsMonitored, - &i.WinningUpperLimit, - &i.IsActive, - &i.LeagueCc_2, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const GetTotalEvents = `-- name: GetTotalEvents :one -SELECT COUNT(*) -FROM events - LEFT JOIN leagues ON leagues.id = league_id -WHERE is_live = false - AND status = 'upcoming' - AND ( - league_id = $1 - OR $1 IS NULL - ) - AND ( - events.sport_id = $2 - OR $2 IS NULL - ) - AND ( - match_name ILIKE '%' || $3 || '%' - OR league_name ILIKE '%' || $3 || '%' - OR $3 IS NULL - ) - AND ( - start_time < $4 - OR $4 IS NULL - ) - AND ( - start_time > $5 - OR $5 IS NULL - ) - AND ( - leagues.country_code = $6 - OR $6 IS NULL - ) - AND ( - events.is_featured = $7 - OR $7 IS NULL - ) -` - -type GetTotalEventsParams struct { - LeagueID pgtype.Int4 `json:"league_id"` - SportID pgtype.Int4 `json:"sport_id"` - Query pgtype.Text `json:"query"` - LastStartTime pgtype.Timestamp `json:"last_start_time"` - FirstStartTime pgtype.Timestamp `json:"first_start_time"` - CountryCode pgtype.Text `json:"country_code"` - IsFeatured pgtype.Bool `json:"is_featured"` -} - -func (q *Queries) GetTotalEvents(ctx context.Context, arg GetTotalEventsParams) (int64, error) { - row := q.db.QueryRow(ctx, GetTotalEvents, - arg.LeagueID, - arg.SportID, - arg.Query, - arg.LastStartTime, - arg.FirstStartTime, - arg.CountryCode, - arg.IsFeatured, - ) - var count int64 - err := row.Scan(&count) - return count, err -} - -const GetUpcomingByID = `-- name: GetUpcomingByID :one -SELECT 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, fetched_at, source, is_featured, is_monitored, winning_upper_limit, is_active -FROM events +const GetEventWithSettingByID = `-- name: GetEventWithSettingByID :one +SELECT 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, start_time, score, match_minute, timer_status, added_time, match_period, is_live, status, fetched_at, source, default_is_active, default_is_featured, default_winning_upper_limit, is_monitored, company_id, is_active, is_featured, winning_upper_limit, updated_at, league_cc +FROM event_with_settings WHERE id = $1 + AND company_id = $2 AND is_live = false AND status = 'upcoming' LIMIT 1 ` -func (q *Queries) GetUpcomingByID(ctx context.Context, id string) (Event, error) { - row := q.db.QueryRow(ctx, GetUpcomingByID, id) - var i Event +type GetEventWithSettingByIDParams struct { + ID string `json:"id"` + CompanyID int64 `json:"company_id"` +} + +func (q *Queries) GetEventWithSettingByID(ctx context.Context, arg GetEventWithSettingByIDParams) (EventWithSetting, error) { + row := q.db.QueryRow(ctx, GetEventWithSettingByID, arg.ID, arg.CompanyID) + var i EventWithSetting err := row.Scan( &i.ID, &i.SportID, @@ -395,7 +107,6 @@ func (q *Queries) GetUpcomingByID(ctx context.Context, id string) (Event, error) &i.AwayKitImage, &i.LeagueID, &i.LeagueName, - &i.LeagueCc, &i.StartTime, &i.Score, &i.MatchMinute, @@ -406,10 +117,442 @@ func (q *Queries) GetUpcomingByID(ctx context.Context, id string) (Event, error) &i.Status, &i.FetchedAt, &i.Source, - &i.IsFeatured, + &i.DefaultIsActive, + &i.DefaultIsFeatured, + &i.DefaultWinningUpperLimit, &i.IsMonitored, - &i.WinningUpperLimit, + &i.CompanyID, &i.IsActive, + &i.IsFeatured, + &i.WinningUpperLimit, + &i.UpdatedAt, + &i.LeagueCc, + ) + return i, err +} + +const GetEventsWithSettings = `-- name: GetEventsWithSettings :many +SELECT 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, start_time, score, match_minute, timer_status, added_time, match_period, is_live, status, fetched_at, source, default_is_active, default_is_featured, default_winning_upper_limit, is_monitored, company_id, is_active, is_featured, winning_upper_limit, updated_at, league_cc +FROM event_with_settings +WHERE company_id = $1 + AND start_time > now() + AND is_live = false + AND status = 'upcoming' + AND ( + league_id = $2 + OR $2 IS NULL + ) + AND ( + sport_id = $3 + OR $3 IS NULL + ) + AND ( + match_name ILIKE '%' || $4 || '%' + OR league_name ILIKE '%' || $4 || '%' + OR $4 IS NULL + ) + AND ( + start_time < $5 + OR $5 IS NULL + ) + AND ( + start_time > $6 + OR $6 IS NULL + ) + AND ( + league_cc = $7 + OR $7 IS NULL + ) +ORDER BY start_time ASC +LIMIT $9 OFFSET $8 +` + +type GetEventsWithSettingsParams struct { + CompanyID int64 `json:"company_id"` + LeagueID pgtype.Int8 `json:"league_id"` + SportID pgtype.Int4 `json:"sport_id"` + Query pgtype.Text `json:"query"` + LastStartTime pgtype.Timestamp `json:"last_start_time"` + FirstStartTime pgtype.Timestamp `json:"first_start_time"` + CountryCode pgtype.Text `json:"country_code"` + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` +} + +func (q *Queries) GetEventsWithSettings(ctx context.Context, arg GetEventsWithSettingsParams) ([]EventWithSetting, error) { + rows, err := q.db.Query(ctx, GetEventsWithSettings, + arg.CompanyID, + arg.LeagueID, + arg.SportID, + arg.Query, + arg.LastStartTime, + arg.FirstStartTime, + arg.CountryCode, + arg.Offset, + arg.Limit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []EventWithSetting + for rows.Next() { + var i EventWithSetting + if err := rows.Scan( + &i.ID, + &i.SportID, + &i.MatchName, + &i.HomeTeam, + &i.AwayTeam, + &i.HomeTeamID, + &i.AwayTeamID, + &i.HomeKitImage, + &i.AwayKitImage, + &i.LeagueID, + &i.LeagueName, + &i.StartTime, + &i.Score, + &i.MatchMinute, + &i.TimerStatus, + &i.AddedTime, + &i.MatchPeriod, + &i.IsLive, + &i.Status, + &i.FetchedAt, + &i.Source, + &i.DefaultIsActive, + &i.DefaultIsFeatured, + &i.DefaultWinningUpperLimit, + &i.IsMonitored, + &i.CompanyID, + &i.IsActive, + &i.IsFeatured, + &i.WinningUpperLimit, + &i.UpdatedAt, + &i.LeagueCc, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetExpiredEvents = `-- name: GetExpiredEvents :many +SELECT 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, start_time, score, match_minute, timer_status, added_time, match_period, is_live, status, fetched_at, source, default_is_active, default_is_featured, default_winning_upper_limit, is_monitored, league_cc +FROM event_with_country +WHERE start_time < now() + and ( + status = $1 + OR $1 IS NULL + ) +ORDER BY start_time ASC +` + +func (q *Queries) GetExpiredEvents(ctx context.Context, status pgtype.Text) ([]EventWithCountry, error) { + rows, err := q.db.Query(ctx, GetExpiredEvents, status) + if err != nil { + return nil, err + } + defer rows.Close() + var items []EventWithCountry + for rows.Next() { + var i EventWithCountry + if err := rows.Scan( + &i.ID, + &i.SportID, + &i.MatchName, + &i.HomeTeam, + &i.AwayTeam, + &i.HomeTeamID, + &i.AwayTeamID, + &i.HomeKitImage, + &i.AwayKitImage, + &i.LeagueID, + &i.LeagueName, + &i.StartTime, + &i.Score, + &i.MatchMinute, + &i.TimerStatus, + &i.AddedTime, + &i.MatchPeriod, + &i.IsLive, + &i.Status, + &i.FetchedAt, + &i.Source, + &i.DefaultIsActive, + &i.DefaultIsFeatured, + &i.DefaultWinningUpperLimit, + &i.IsMonitored, + &i.LeagueCc, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetPaginatedUpcomingEvents = `-- name: GetPaginatedUpcomingEvents :many +SELECT 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, start_time, score, match_minute, timer_status, added_time, match_period, is_live, status, fetched_at, source, default_is_active, default_is_featured, default_winning_upper_limit, is_monitored, league_cc +FROM event_with_country +WHERE start_time > now() + AND is_live = false + AND status = 'upcoming' + AND ( + league_id = $1 + OR $1 IS NULL + ) + AND ( + sport_id = $2 + OR $2 IS NULL + ) + AND ( + match_name ILIKE '%' || $3 || '%' + OR league_name ILIKE '%' || $3 || '%' + OR $3 IS NULL + ) + AND ( + start_time < $4 + OR $4 IS NULL + ) + AND ( + start_time > $5 + OR $5 IS NULL + ) + AND ( + league_cc = $6 + OR $6 IS NULL + ) +ORDER BY start_time ASC +LIMIT $8 OFFSET $7 +` + +type GetPaginatedUpcomingEventsParams struct { + LeagueID pgtype.Int8 `json:"league_id"` + SportID pgtype.Int4 `json:"sport_id"` + Query pgtype.Text `json:"query"` + LastStartTime pgtype.Timestamp `json:"last_start_time"` + FirstStartTime pgtype.Timestamp `json:"first_start_time"` + CountryCode pgtype.Text `json:"country_code"` + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` +} + +func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginatedUpcomingEventsParams) ([]EventWithCountry, error) { + rows, err := q.db.Query(ctx, GetPaginatedUpcomingEvents, + arg.LeagueID, + arg.SportID, + arg.Query, + arg.LastStartTime, + arg.FirstStartTime, + arg.CountryCode, + arg.Offset, + arg.Limit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []EventWithCountry + for rows.Next() { + var i EventWithCountry + if err := rows.Scan( + &i.ID, + &i.SportID, + &i.MatchName, + &i.HomeTeam, + &i.AwayTeam, + &i.HomeTeamID, + &i.AwayTeamID, + &i.HomeKitImage, + &i.AwayKitImage, + &i.LeagueID, + &i.LeagueName, + &i.StartTime, + &i.Score, + &i.MatchMinute, + &i.TimerStatus, + &i.AddedTime, + &i.MatchPeriod, + &i.IsLive, + &i.Status, + &i.FetchedAt, + &i.Source, + &i.DefaultIsActive, + &i.DefaultIsFeatured, + &i.DefaultWinningUpperLimit, + &i.IsMonitored, + &i.LeagueCc, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetTotalCompanyEvents = `-- name: GetTotalCompanyEvents :one +SELECT COUNT(*) +FROM event_with_settings +WHERE company_id = $1 + AND is_live = false + AND status = 'upcoming' + AND ( + league_id = $2 + OR $2 IS NULL + ) + AND ( + sport_id = $3 + OR $3 IS NULL + ) + AND ( + match_name ILIKE '%' || $4 || '%' + OR league_name ILIKE '%' || $4 || '%' + OR $4 IS NULL + ) + AND ( + start_time < $5 + OR $5 IS NULL + ) + AND ( + start_time > $6 + OR $6 IS NULL + ) + AND ( + league_cc = $7 + OR $7 IS NULL + ) +` + +type GetTotalCompanyEventsParams struct { + CompanyID int64 `json:"company_id"` + LeagueID pgtype.Int8 `json:"league_id"` + SportID pgtype.Int4 `json:"sport_id"` + Query pgtype.Text `json:"query"` + LastStartTime pgtype.Timestamp `json:"last_start_time"` + FirstStartTime pgtype.Timestamp `json:"first_start_time"` + CountryCode pgtype.Text `json:"country_code"` +} + +func (q *Queries) GetTotalCompanyEvents(ctx context.Context, arg GetTotalCompanyEventsParams) (int64, error) { + row := q.db.QueryRow(ctx, GetTotalCompanyEvents, + arg.CompanyID, + arg.LeagueID, + arg.SportID, + arg.Query, + arg.LastStartTime, + arg.FirstStartTime, + arg.CountryCode, + ) + var count int64 + err := row.Scan(&count) + return count, err +} + +const GetTotalEvents = `-- name: GetTotalEvents :one +SELECT COUNT(*) +FROM event_with_country +WHERE is_live = false + AND status = 'upcoming' + AND ( + league_id = $1 + OR $1 IS NULL + ) + AND ( + sport_id = $2 + OR $2 IS NULL + ) + AND ( + match_name ILIKE '%' || $3 || '%' + OR league_name ILIKE '%' || $3 || '%' + OR $3 IS NULL + ) + AND ( + start_time < $4 + OR $4 IS NULL + ) + AND ( + start_time > $5 + OR $5 IS NULL + ) + AND ( + league_cc = $6 + OR $6 IS NULL + ) +` + +type GetTotalEventsParams struct { + LeagueID pgtype.Int8 `json:"league_id"` + SportID pgtype.Int4 `json:"sport_id"` + Query pgtype.Text `json:"query"` + LastStartTime pgtype.Timestamp `json:"last_start_time"` + FirstStartTime pgtype.Timestamp `json:"first_start_time"` + CountryCode pgtype.Text `json:"country_code"` +} + +func (q *Queries) GetTotalEvents(ctx context.Context, arg GetTotalEventsParams) (int64, error) { + row := q.db.QueryRow(ctx, GetTotalEvents, + arg.LeagueID, + arg.SportID, + arg.Query, + arg.LastStartTime, + arg.FirstStartTime, + arg.CountryCode, + ) + var count int64 + err := row.Scan(&count) + return count, err +} + +const GetUpcomingByID = `-- name: GetUpcomingByID :one +SELECT 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, start_time, score, match_minute, timer_status, added_time, match_period, is_live, status, fetched_at, source, default_is_active, default_is_featured, default_winning_upper_limit, is_monitored, league_cc +FROM event_with_country +WHERE id = $1 + AND is_live = false + AND status = 'upcoming' +LIMIT 1 +` + +func (q *Queries) GetUpcomingByID(ctx context.Context, id string) (EventWithCountry, error) { + row := q.db.QueryRow(ctx, GetUpcomingByID, id) + var i EventWithCountry + err := row.Scan( + &i.ID, + &i.SportID, + &i.MatchName, + &i.HomeTeam, + &i.AwayTeam, + &i.HomeTeamID, + &i.AwayTeamID, + &i.HomeKitImage, + &i.AwayKitImage, + &i.LeagueID, + &i.LeagueName, + &i.StartTime, + &i.Score, + &i.MatchMinute, + &i.TimerStatus, + &i.AddedTime, + &i.MatchPeriod, + &i.IsLive, + &i.Status, + &i.FetchedAt, + &i.Source, + &i.DefaultIsActive, + &i.DefaultIsFeatured, + &i.DefaultWinningUpperLimit, + &i.IsMonitored, + &i.LeagueCc, ) return i, err } @@ -427,13 +570,7 @@ INSERT INTO events ( away_kit_image, league_id, league_name, - league_cc, start_time, - score, - match_minute, - timer_status, - added_time, - match_period, is_live, status, source @@ -453,13 +590,7 @@ VALUES ( $12, $13, $14, - $15, - $16, - $17, - $18, - $19, - $20, - $21 + $15 ) ON CONFLICT (id) DO UPDATE SET sport_id = EXCLUDED.sport_id, @@ -480,33 +611,26 @@ SET sport_id = EXCLUDED.sport_id, added_time = EXCLUDED.added_time, match_period = EXCLUDED.match_period, is_live = EXCLUDED.is_live, - status = EXCLUDED.status, source = EXCLUDED.source, fetched_at = now() ` type InsertEventParams struct { ID string `json:"id"` - SportID pgtype.Int4 `json:"sport_id"` - MatchName pgtype.Text `json:"match_name"` - HomeTeam pgtype.Text `json:"home_team"` - AwayTeam pgtype.Text `json:"away_team"` - HomeTeamID pgtype.Int4 `json:"home_team_id"` - AwayTeamID pgtype.Int4 `json:"away_team_id"` - HomeKitImage pgtype.Text `json:"home_kit_image"` - AwayKitImage pgtype.Text `json:"away_kit_image"` - LeagueID pgtype.Int4 `json:"league_id"` - LeagueName pgtype.Text `json:"league_name"` - LeagueCc pgtype.Text `json:"league_cc"` + SportID int32 `json:"sport_id"` + MatchName string `json:"match_name"` + HomeTeam string `json:"home_team"` + AwayTeam string `json:"away_team"` + HomeTeamID int64 `json:"home_team_id"` + AwayTeamID int64 `json:"away_team_id"` + HomeKitImage string `json:"home_kit_image"` + AwayKitImage string `json:"away_kit_image"` + LeagueID int64 `json:"league_id"` + LeagueName string `json:"league_name"` StartTime pgtype.Timestamp `json:"start_time"` - Score pgtype.Text `json:"score"` - MatchMinute pgtype.Int4 `json:"match_minute"` - TimerStatus pgtype.Text `json:"timer_status"` - AddedTime pgtype.Int4 `json:"added_time"` - MatchPeriod pgtype.Int4 `json:"match_period"` - IsLive pgtype.Bool `json:"is_live"` - Status pgtype.Text `json:"status"` - Source pgtype.Text `json:"source"` + IsLive bool `json:"is_live"` + Status string `json:"status"` + Source string `json:"source"` } func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error { @@ -522,13 +646,7 @@ func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error arg.AwayKitImage, arg.LeagueID, arg.LeagueName, - arg.LeagueCc, arg.StartTime, - arg.Score, - arg.MatchMinute, - arg.TimerStatus, - arg.AddedTime, - arg.MatchPeriod, arg.IsLive, arg.Status, arg.Source, @@ -536,95 +654,36 @@ func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error return err } -const InsertUpcomingEvent = `-- name: InsertUpcomingEvent :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, - is_live, - status, - source +const InsertEventSettings = `-- name: InsertEventSettings :exec +INSERT INTO company_event_settings ( + company_id, + event_id, + is_active, + is_featured, + winning_upper_limit ) -VALUES ( - $1, - $2, - $3, - $4, - $5, - $6, - $7, - $8, - $9, - $10, - $11, - $12, - $13, - false, - 'upcoming', - $14 - ) ON CONFLICT (id) DO +VALUES ($1, $2, $3, $4, $5) ON CONFLICT(company_id, event_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, - is_live = false, - status = 'upcoming', - source = EXCLUDED.source, - fetched_at = now() +SET is_active = EXCLUDED.is_active, + is_featured = EXCLUDED.is_featured, + winning_upper_limit = EXCLUDED.winning_upper_limit ` -type InsertUpcomingEventParams struct { - ID string `json:"id"` - SportID pgtype.Int4 `json:"sport_id"` - MatchName pgtype.Text `json:"match_name"` - HomeTeam pgtype.Text `json:"home_team"` - AwayTeam pgtype.Text `json:"away_team"` - HomeTeamID pgtype.Int4 `json:"home_team_id"` - AwayTeamID pgtype.Int4 `json:"away_team_id"` - HomeKitImage pgtype.Text `json:"home_kit_image"` - AwayKitImage pgtype.Text `json:"away_kit_image"` - LeagueID pgtype.Int4 `json:"league_id"` - LeagueName pgtype.Text `json:"league_name"` - LeagueCc pgtype.Text `json:"league_cc"` - StartTime pgtype.Timestamp `json:"start_time"` - Source pgtype.Text `json:"source"` +type InsertEventSettingsParams struct { + CompanyID int64 `json:"company_id"` + EventID string `json:"event_id"` + IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` + WinningUpperLimit pgtype.Int4 `json:"winning_upper_limit"` } -func (q *Queries) InsertUpcomingEvent(ctx context.Context, arg InsertUpcomingEventParams) error { - _, err := q.db.Exec(ctx, InsertUpcomingEvent, - 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.Source, +func (q *Queries) InsertEventSettings(ctx context.Context, arg InsertEventSettingsParams) error { + _, err := q.db.Exec(ctx, InsertEventSettings, + arg.CompanyID, + arg.EventID, + arg.IsActive, + arg.IsFeatured, + arg.WinningUpperLimit, ) return err } @@ -644,7 +703,7 @@ func (q *Queries) IsEventMonitored(ctx context.Context, id string) (bool, error) const ListLiveEvents = `-- name: ListLiveEvents :many SELECT id -FROM events +FROM event_with_country WHERE is_live = true ` @@ -668,22 +727,6 @@ func (q *Queries) ListLiveEvents(ctx context.Context) ([]string, error) { return items, nil } -const UpdateEventFeatured = `-- name: UpdateEventFeatured :exec -UPDATE events -SET is_featured = $1 -WHERE id = $2 -` - -type UpdateEventFeaturedParams struct { - IsFeatured bool `json:"is_featured"` - ID string `json:"id"` -} - -func (q *Queries) UpdateEventFeatured(ctx context.Context, arg UpdateEventFeaturedParams) error { - _, err := q.db.Exec(ctx, UpdateEventFeatured, arg.IsFeatured, arg.ID) - return err -} - const UpdateEventMonitored = `-- name: UpdateEventMonitored :exec UPDATE events SET is_monitored = $1 @@ -700,6 +743,40 @@ func (q *Queries) UpdateEventMonitored(ctx context.Context, arg UpdateEventMonit return err } +const UpdateEventSettings = `-- name: UpdateEventSettings :exec +UPDATE company_event_settings +SET is_active = COALESCE($3, is_active), + is_featured = COALESCE( + $4, + is_featured + ), + winning_upper_limit = COALESCE( + $5, + winning_upper_limit + ) +WHERE event_id = $1 + AND company_id = $2 +` + +type UpdateEventSettingsParams struct { + EventID string `json:"event_id"` + CompanyID int64 `json:"company_id"` + IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` + WinningUpperLimit pgtype.Int4 `json:"winning_upper_limit"` +} + +func (q *Queries) UpdateEventSettings(ctx context.Context, arg UpdateEventSettingsParams) error { + _, err := q.db.Exec(ctx, UpdateEventSettings, + arg.EventID, + arg.CompanyID, + arg.IsActive, + arg.IsFeatured, + arg.WinningUpperLimit, + ) + return err +} + const UpdateMatchResult = `-- name: UpdateMatchResult :exec UPDATE events SET score = $1, @@ -709,7 +786,7 @@ WHERE id = $3 type UpdateMatchResultParams struct { Score pgtype.Text `json:"score"` - Status pgtype.Text `json:"status"` + Status string `json:"status"` ID string `json:"id"` } diff --git a/gen/db/events_stat.sql.go b/gen/db/events_stat.sql.go index 2380d54..677fa2a 100644 --- a/gen/db/events_stat.sql.go +++ b/gen/db/events_stat.sql.go @@ -56,10 +56,6 @@ SELECT leagues.id, ) AS removed FROM leagues JOIN events ON leagues.id = events.league_id -WHERE ( - leagues.is_featured = $1 - OR $1 IS NULL - ) GROUP BY leagues.id, leagues.name ` @@ -83,8 +79,8 @@ type GetLeagueEventStatRow struct { Removed int64 `json:"removed"` } -func (q *Queries) GetLeagueEventStat(ctx context.Context, isLeagueFeatured pgtype.Bool) ([]GetLeagueEventStatRow, error) { - rows, err := q.db.Query(ctx, GetLeagueEventStat, isLeagueFeatured) +func (q *Queries) GetLeagueEventStat(ctx context.Context) ([]GetLeagueEventStatRow, error) { + rows, err := q.db.Query(ctx, GetLeagueEventStat) if err != nil { return nil, err } @@ -126,34 +122,20 @@ SELECT DATE_TRUNC('month', start_time) AS month, FROM events JOIN leagues ON leagues.id = events.league_id WHERE ( - events.is_featured = $1 + events.league_id = $1 OR $1 IS NULL ) - AND ( - leagues.is_featured = $2 - OR $2 IS NULL - ) - AND ( - events.league_id = $3 - OR $3 IS NULL - ) GROUP BY month ORDER BY month ` -type GetTotalMontlyEventStatParams struct { - IsEventFeatured pgtype.Bool `json:"is_event_featured"` - IsLeagueFeatured pgtype.Bool `json:"is_league_featured"` - LeagueID pgtype.Int4 `json:"league_id"` -} - type GetTotalMontlyEventStatRow struct { Month pgtype.Interval `json:"month"` EventCount int64 `json:"event_count"` } -func (q *Queries) GetTotalMontlyEventStat(ctx context.Context, arg GetTotalMontlyEventStatParams) ([]GetTotalMontlyEventStatRow, error) { - rows, err := q.db.Query(ctx, GetTotalMontlyEventStat, arg.IsEventFeatured, arg.IsLeagueFeatured, arg.LeagueID) +func (q *Queries) GetTotalMontlyEventStat(ctx context.Context, leagueID pgtype.Int8) ([]GetTotalMontlyEventStatRow, error) { + rows, err := q.db.Query(ctx, GetTotalMontlyEventStat, leagueID) if err != nil { return nil, err } diff --git a/gen/db/flags.sql.go b/gen/db/flags.sql.go index 17b406e..653543f 100644 --- a/gen/db/flags.sql.go +++ b/gen/db/flags.sql.go @@ -14,26 +14,26 @@ import ( const CreateFlag = `-- name: CreateFlag :one INSERT INTO flags ( bet_id, - odd_id, + odds_market_id, reason ) VALUES ( $1, $2, $3 -) RETURNING id, bet_id, odd_id, reason, flagged_at, resolved +) RETURNING id, bet_id, odds_market_id, reason, flagged_at, resolved ` type CreateFlagParams struct { - BetID pgtype.Int8 `json:"bet_id"` - OddID pgtype.Int8 `json:"odd_id"` - Reason pgtype.Text `json:"reason"` + BetID pgtype.Int8 `json:"bet_id"` + OddsMarketID pgtype.Int8 `json:"odds_market_id"` + Reason pgtype.Text `json:"reason"` } func (q *Queries) CreateFlag(ctx context.Context, arg CreateFlagParams) (Flag, error) { - row := q.db.QueryRow(ctx, CreateFlag, arg.BetID, arg.OddID, arg.Reason) + row := q.db.QueryRow(ctx, CreateFlag, arg.BetID, arg.OddsMarketID, arg.Reason) var i Flag err := row.Scan( &i.ID, &i.BetID, - &i.OddID, + &i.OddsMarketID, &i.Reason, &i.FlaggedAt, &i.Resolved, diff --git a/gen/db/leagues.sql.go b/gen/db/leagues.sql.go index 143f6ca..5d49d4d 100644 --- a/gen/db/leagues.sql.go +++ b/gen/db/leagues.sql.go @@ -14,27 +14,27 @@ import ( const CheckLeagueSupport = `-- name: CheckLeagueSupport :one SELECT EXISTS( SELECT 1 - FROM leagues - WHERE id = $1 + FROM company_league_settings + WHERE league_id = $1 + AND company_id = $2 AND is_active = true ) ` -func (q *Queries) CheckLeagueSupport(ctx context.Context, id int64) (bool, error) { - row := q.db.QueryRow(ctx, CheckLeagueSupport, id) +type CheckLeagueSupportParams struct { + LeagueID int64 `json:"league_id"` + CompanyID int64 `json:"company_id"` +} + +func (q *Queries) CheckLeagueSupport(ctx context.Context, arg CheckLeagueSupportParams) (bool, error) { + row := q.db.QueryRow(ctx, CheckLeagueSupport, arg.LeagueID, arg.CompanyID) var exists bool err := row.Scan(&exists) return exists, err } const GetAllLeagues = `-- name: GetAllLeagues :many -SELECT id, - name, - country_code, - bet365_id, - is_active, - is_featured, - sport_id +SELECT id, name, img_url, country_code, bet365_id, sport_id, default_is_active, default_is_featured FROM leagues WHERE ( country_code = $1 @@ -44,44 +44,21 @@ WHERE ( sport_id = $2 OR $2 IS NULL ) - AND ( - is_active = $3 - OR $3 IS NULL - ) - AND ( - is_featured = $4 - OR $4 IS NULL - ) -ORDER BY is_featured DESC, - name ASC -LIMIT $6 OFFSET $5 +ORDER BY name ASC +LIMIT $4 OFFSET $3 ` type GetAllLeaguesParams struct { CountryCode pgtype.Text `json:"country_code"` SportID pgtype.Int4 `json:"sport_id"` - IsActive pgtype.Bool `json:"is_active"` - IsFeatured pgtype.Bool `json:"is_featured"` Offset pgtype.Int4 `json:"offset"` Limit pgtype.Int4 `json:"limit"` } -type GetAllLeaguesRow struct { - ID int64 `json:"id"` - Name string `json:"name"` - CountryCode pgtype.Text `json:"country_code"` - Bet365ID pgtype.Int4 `json:"bet365_id"` - IsActive pgtype.Bool `json:"is_active"` - IsFeatured pgtype.Bool `json:"is_featured"` - SportID int32 `json:"sport_id"` -} - -func (q *Queries) GetAllLeagues(ctx context.Context, arg GetAllLeaguesParams) ([]GetAllLeaguesRow, error) { +func (q *Queries) GetAllLeagues(ctx context.Context, arg GetAllLeaguesParams) ([]League, error) { rows, err := q.db.Query(ctx, GetAllLeagues, arg.CountryCode, arg.SportID, - arg.IsActive, - arg.IsFeatured, arg.Offset, arg.Limit, ) @@ -89,17 +66,18 @@ func (q *Queries) GetAllLeagues(ctx context.Context, arg GetAllLeaguesParams) ([ return nil, err } defer rows.Close() - var items []GetAllLeaguesRow + var items []League for rows.Next() { - var i GetAllLeaguesRow + var i League if err := rows.Scan( &i.ID, &i.Name, + &i.ImgUrl, &i.CountryCode, &i.Bet365ID, - &i.IsActive, - &i.IsFeatured, &i.SportID, + &i.DefaultIsActive, + &i.DefaultIsFeatured, ); err != nil { return nil, err } @@ -111,45 +89,71 @@ func (q *Queries) GetAllLeagues(ctx context.Context, arg GetAllLeaguesParams) ([ return items, nil } -const GetFeaturedLeagues = `-- name: GetFeaturedLeagues :many -SELECT id, - name, - country_code, - bet365_id, - is_active, - is_featured, - sport_id -FROM leagues -WHERE is_featured = true +const GetAllLeaguesWithSettings = `-- name: GetAllLeaguesWithSettings :many +SELECT id, name, img_url, country_code, bet365_id, sport_id, default_is_active, default_is_featured, company_id, is_active, is_featured, updated_at +FROM league_with_settings +WHERE (company_id = $1) + AND ( + country_code = $2 + OR $2 IS NULL + ) + AND ( + sport_id = $3 + OR $3 IS NULL + ) + AND ( + is_active = $4 + OR $4 IS NULL + ) + AND ( + is_featured = $5 + OR $5 IS NULL + ) +ORDER BY is_featured DESC, + name ASC +LIMIT $7 OFFSET $6 ` -type GetFeaturedLeaguesRow struct { - ID int64 `json:"id"` - Name string `json:"name"` +type GetAllLeaguesWithSettingsParams struct { + CompanyID int64 `json:"company_id"` CountryCode pgtype.Text `json:"country_code"` - Bet365ID pgtype.Int4 `json:"bet365_id"` + SportID pgtype.Int4 `json:"sport_id"` IsActive pgtype.Bool `json:"is_active"` IsFeatured pgtype.Bool `json:"is_featured"` - SportID int32 `json:"sport_id"` + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` } -func (q *Queries) GetFeaturedLeagues(ctx context.Context) ([]GetFeaturedLeaguesRow, error) { - rows, err := q.db.Query(ctx, GetFeaturedLeagues) +func (q *Queries) GetAllLeaguesWithSettings(ctx context.Context, arg GetAllLeaguesWithSettingsParams) ([]LeagueWithSetting, error) { + rows, err := q.db.Query(ctx, GetAllLeaguesWithSettings, + arg.CompanyID, + arg.CountryCode, + arg.SportID, + arg.IsActive, + arg.IsFeatured, + arg.Offset, + arg.Limit, + ) if err != nil { return nil, err } defer rows.Close() - var items []GetFeaturedLeaguesRow + var items []LeagueWithSetting for rows.Next() { - var i GetFeaturedLeaguesRow + var i LeagueWithSetting if err := rows.Scan( &i.ID, &i.Name, + &i.ImgUrl, &i.CountryCode, &i.Bet365ID, + &i.SportID, + &i.DefaultIsActive, + &i.DefaultIsFeatured, + &i.CompanyID, &i.IsActive, &i.IsFeatured, - &i.SportID, + &i.UpdatedAt, ); err != nil { return nil, err } @@ -168,27 +172,25 @@ INSERT INTO leagues ( country_code, bet365_id, sport_id, - is_active, - is_featured + default_is_active, + default_is_featured ) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, country_code = EXCLUDED.country_code, bet365_id = EXCLUDED.bet365_id, - is_active = EXCLUDED.is_active, - is_featured = EXCLUDED.is_featured, sport_id = EXCLUDED.sport_id ` type InsertLeagueParams struct { - ID int64 `json:"id"` - Name string `json:"name"` - CountryCode pgtype.Text `json:"country_code"` - Bet365ID pgtype.Int4 `json:"bet365_id"` - SportID int32 `json:"sport_id"` - IsActive pgtype.Bool `json:"is_active"` - IsFeatured pgtype.Bool `json:"is_featured"` + ID int64 `json:"id"` + Name string `json:"name"` + CountryCode pgtype.Text `json:"country_code"` + Bet365ID pgtype.Int4 `json:"bet365_id"` + SportID int32 `json:"sport_id"` + DefaultIsActive bool `json:"default_is_active"` + DefaultIsFeatured bool `json:"default_is_featured"` } func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) error { @@ -198,25 +200,39 @@ func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) erro arg.CountryCode, arg.Bet365ID, arg.SportID, - arg.IsActive, - arg.IsFeatured, + arg.DefaultIsActive, + arg.DefaultIsFeatured, ) return err } -const SetLeagueActive = `-- name: SetLeagueActive :exec -UPDATE leagues -SET is_active = $2 -WHERE id = $1 +const InsertLeagueSettings = `-- name: InsertLeagueSettings :exec +INSERT INTO company_league_settings ( + company_id, + league_id, + is_active, + is_featured + ) +VALUES ($1, $2, $3, $4) ON CONFLICT(company_id, league_id) DO +UPDATE +SET is_active = EXCLUDED.is_active, + is_featured = EXCLUDED.is_featured ` -type SetLeagueActiveParams struct { - ID int64 `json:"id"` - IsActive pgtype.Bool `json:"is_active"` +type InsertLeagueSettingsParams struct { + CompanyID int64 `json:"company_id"` + LeagueID int64 `json:"league_id"` + IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` } -func (q *Queries) SetLeagueActive(ctx context.Context, arg SetLeagueActiveParams) error { - _, err := q.db.Exec(ctx, SetLeagueActive, arg.ID, arg.IsActive) +func (q *Queries) InsertLeagueSettings(ctx context.Context, arg InsertLeagueSettingsParams) error { + _, err := q.db.Exec(ctx, InsertLeagueSettings, + arg.CompanyID, + arg.LeagueID, + arg.IsActive, + arg.IsFeatured, + ) return err } @@ -225,9 +241,7 @@ UPDATE leagues SET name = COALESCE($2, name), country_code = COALESCE($3, country_code), bet365_id = COALESCE($4, bet365_id), - is_active = COALESCE($5, is_active), - is_featured = COALESCE($6, is_featured), - sport_id = COALESCE($7, sport_id) + sport_id = COALESCE($5, sport_id) WHERE id = $1 ` @@ -236,8 +250,6 @@ type UpdateLeagueParams struct { Name pgtype.Text `json:"name"` CountryCode pgtype.Text `json:"country_code"` Bet365ID pgtype.Int4 `json:"bet365_id"` - IsActive pgtype.Bool `json:"is_active"` - IsFeatured pgtype.Bool `json:"is_featured"` SportID pgtype.Int4 `json:"sport_id"` } @@ -247,43 +259,35 @@ func (q *Queries) UpdateLeague(ctx context.Context, arg UpdateLeagueParams) erro arg.Name, arg.CountryCode, arg.Bet365ID, - arg.IsActive, - arg.IsFeatured, arg.SportID, ) return err } -const UpdateLeagueByBet365ID = `-- name: UpdateLeagueByBet365ID :exec -UPDATE leagues -SET name = COALESCE($2, name), - id = COALESCE($3, id), - country_code = COALESCE($4, country_code), - is_active = COALESCE($5, is_active), - is_featured = COALESCE($6, is_featured), - sport_id = COALESCE($7, sport_id) -WHERE bet365_id = $1 +const UpdateLeagueSettings = `-- name: UpdateLeagueSettings :exec +UPDATE company_league_settings +SET is_active = COALESCE($3, is_active), + is_featured = COALESCE( + $4, + is_featured + ) +WHERE league_id = $1 + AND company_id = $2 ` -type UpdateLeagueByBet365IDParams struct { - Bet365ID pgtype.Int4 `json:"bet365_id"` - Name pgtype.Text `json:"name"` - ID pgtype.Int8 `json:"id"` - CountryCode pgtype.Text `json:"country_code"` - IsActive pgtype.Bool `json:"is_active"` - IsFeatured pgtype.Bool `json:"is_featured"` - SportID pgtype.Int4 `json:"sport_id"` +type UpdateLeagueSettingsParams struct { + LeagueID int64 `json:"league_id"` + CompanyID int64 `json:"company_id"` + IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` } -func (q *Queries) UpdateLeagueByBet365ID(ctx context.Context, arg UpdateLeagueByBet365IDParams) error { - _, err := q.db.Exec(ctx, UpdateLeagueByBet365ID, - arg.Bet365ID, - arg.Name, - arg.ID, - arg.CountryCode, +func (q *Queries) UpdateLeagueSettings(ctx context.Context, arg UpdateLeagueSettingsParams) error { + _, err := q.db.Exec(ctx, UpdateLeagueSettings, + arg.LeagueID, + arg.CompanyID, arg.IsActive, arg.IsFeatured, - arg.SportID, ) return err } diff --git a/gen/db/models.go b/gen/db/models.go index 9b1a3fa..460b84d 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -75,6 +75,7 @@ type Bank struct { type Bet struct { ID int64 `json:"id"` + CompanyID int64 `json:"company_id"` Amount int64 `json:"amount"` TotalOdds float32 `json:"total_odds"` Status int32 `json:"status"` @@ -108,6 +109,7 @@ type BetOutcome struct { type BetWithOutcome struct { ID int64 `json:"id"` + CompanyID int64 `json:"company_id"` Amount int64 `json:"amount"` TotalOdds float32 `json:"total_odds"` Status int32 `json:"status"` @@ -125,8 +127,8 @@ type BetWithOutcome struct { } type Bonu struct { - ID int64 `json:"id"` Multiplier float32 `json:"multiplier"` + ID int64 `json:"id"` BalanceCap int64 `json:"balance_cap"` } @@ -184,6 +186,7 @@ type BranchOperation struct { type CompaniesDetail struct { ID int64 `json:"id"` Name string `json:"name"` + Slug string `json:"slug"` AdminID int64 `json:"admin_id"` WalletID int64 `json:"wallet_id"` DeductedPercentage float32 `json:"deducted_percentage"` @@ -200,6 +203,7 @@ type CompaniesDetail struct { type Company struct { ID int64 `json:"id"` Name string `json:"name"` + Slug string `json:"slug"` AdminID int64 `json:"admin_id"` WalletID int64 `json:"wallet_id"` DeductedPercentage float32 `json:"deducted_percentage"` @@ -208,13 +212,32 @@ type Company struct { UpdatedAt pgtype.Timestamp `json:"updated_at"` } -type CustomOdd struct { - ID int64 `json:"id"` - OddID int64 `json:"odd_id"` - RawOddID int64 `json:"raw_odd_id"` - EventID string `json:"event_id"` - OddValue float64 `json:"odd_value"` - CreatedAt pgtype.Timestamp `json:"created_at"` +type CompanyEventSetting struct { + ID int64 `json:"id"` + CompanyID int64 `json:"company_id"` + EventID string `json:"event_id"` + IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` + WinningUpperLimit pgtype.Int4 `json:"winning_upper_limit"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` +} + +type CompanyLeagueSetting struct { + ID int64 `json:"id"` + CompanyID int64 `json:"company_id"` + LeagueID int64 `json:"league_id"` + IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` +} + +type CompanyOddSetting struct { + ID int64 `json:"id"` + CompanyID int64 `json:"company_id"` + OddsMarketID int64 `json:"odds_market_id"` + IsActive pgtype.Bool `json:"is_active"` + CustomRawOdds []byte `json:"custom_raw_odds"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` } type CustomerWallet struct { @@ -258,40 +281,40 @@ type DirectDeposit struct { } type DisabledOdd struct { - ID int64 `json:"id"` - OddID int64 `json:"odd_id"` - RawOddID int64 `json:"raw_odd_id"` - EventID string `json:"event_id"` - CreatedAt pgtype.Timestamp `json:"created_at"` + ID int64 `json:"id"` + CompanyID int64 `json:"company_id"` + OddsMarketID int64 `json:"odds_market_id"` + RawOddID int64 `json:"raw_odd_id"` + EventID string `json:"event_id"` + CreatedAt pgtype.Timestamp `json:"created_at"` } type Event struct { - ID string `json:"id"` - SportID pgtype.Int4 `json:"sport_id"` - MatchName pgtype.Text `json:"match_name"` - HomeTeam pgtype.Text `json:"home_team"` - AwayTeam pgtype.Text `json:"away_team"` - HomeTeamID pgtype.Int4 `json:"home_team_id"` - AwayTeamID pgtype.Int4 `json:"away_team_id"` - HomeKitImage pgtype.Text `json:"home_kit_image"` - AwayKitImage pgtype.Text `json:"away_kit_image"` - LeagueID pgtype.Int4 `json:"league_id"` - LeagueName pgtype.Text `json:"league_name"` - LeagueCc pgtype.Text `json:"league_cc"` - StartTime pgtype.Timestamp `json:"start_time"` - Score pgtype.Text `json:"score"` - MatchMinute pgtype.Int4 `json:"match_minute"` - TimerStatus pgtype.Text `json:"timer_status"` - AddedTime pgtype.Int4 `json:"added_time"` - MatchPeriod pgtype.Int4 `json:"match_period"` - IsLive pgtype.Bool `json:"is_live"` - Status pgtype.Text `json:"status"` - FetchedAt pgtype.Timestamp `json:"fetched_at"` - Source pgtype.Text `json:"source"` - IsFeatured bool `json:"is_featured"` - IsMonitored bool `json:"is_monitored"` - WinningUpperLimit int32 `json:"winning_upper_limit"` - IsActive bool `json:"is_active"` + ID string `json:"id"` + SportID int32 `json:"sport_id"` + MatchName string `json:"match_name"` + HomeTeam string `json:"home_team"` + AwayTeam string `json:"away_team"` + HomeTeamID int64 `json:"home_team_id"` + AwayTeamID int64 `json:"away_team_id"` + HomeKitImage string `json:"home_kit_image"` + AwayKitImage string `json:"away_kit_image"` + LeagueID int64 `json:"league_id"` + LeagueName string `json:"league_name"` + StartTime pgtype.Timestamp `json:"start_time"` + Score pgtype.Text `json:"score"` + MatchMinute pgtype.Int4 `json:"match_minute"` + TimerStatus pgtype.Text `json:"timer_status"` + AddedTime pgtype.Int4 `json:"added_time"` + MatchPeriod pgtype.Int4 `json:"match_period"` + IsLive bool `json:"is_live"` + Status string `json:"status"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + Source string `json:"source"` + DefaultIsActive bool `json:"default_is_active"` + DefaultIsFeatured bool `json:"default_is_featured"` + DefaultWinningUpperLimit int32 `json:"default_winning_upper_limit"` + IsMonitored bool `json:"is_monitored"` } type EventHistory struct { @@ -301,6 +324,69 @@ type EventHistory struct { CreatedAt pgtype.Timestamp `json:"created_at"` } +type EventWithCountry struct { + ID string `json:"id"` + SportID int32 `json:"sport_id"` + MatchName string `json:"match_name"` + HomeTeam string `json:"home_team"` + AwayTeam string `json:"away_team"` + HomeTeamID int64 `json:"home_team_id"` + AwayTeamID int64 `json:"away_team_id"` + HomeKitImage string `json:"home_kit_image"` + AwayKitImage string `json:"away_kit_image"` + LeagueID int64 `json:"league_id"` + LeagueName string `json:"league_name"` + StartTime pgtype.Timestamp `json:"start_time"` + Score pgtype.Text `json:"score"` + MatchMinute pgtype.Int4 `json:"match_minute"` + TimerStatus pgtype.Text `json:"timer_status"` + AddedTime pgtype.Int4 `json:"added_time"` + MatchPeriod pgtype.Int4 `json:"match_period"` + IsLive bool `json:"is_live"` + Status string `json:"status"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + Source string `json:"source"` + DefaultIsActive bool `json:"default_is_active"` + DefaultIsFeatured bool `json:"default_is_featured"` + DefaultWinningUpperLimit int32 `json:"default_winning_upper_limit"` + IsMonitored bool `json:"is_monitored"` + LeagueCc pgtype.Text `json:"league_cc"` +} + +type EventWithSetting struct { + ID string `json:"id"` + SportID int32 `json:"sport_id"` + MatchName string `json:"match_name"` + HomeTeam string `json:"home_team"` + AwayTeam string `json:"away_team"` + HomeTeamID int64 `json:"home_team_id"` + AwayTeamID int64 `json:"away_team_id"` + HomeKitImage string `json:"home_kit_image"` + AwayKitImage string `json:"away_kit_image"` + LeagueID int64 `json:"league_id"` + LeagueName string `json:"league_name"` + StartTime pgtype.Timestamp `json:"start_time"` + Score pgtype.Text `json:"score"` + MatchMinute pgtype.Int4 `json:"match_minute"` + TimerStatus pgtype.Text `json:"timer_status"` + AddedTime pgtype.Int4 `json:"added_time"` + MatchPeriod pgtype.Int4 `json:"match_period"` + IsLive bool `json:"is_live"` + Status string `json:"status"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + Source string `json:"source"` + DefaultIsActive bool `json:"default_is_active"` + DefaultIsFeatured bool `json:"default_is_featured"` + DefaultWinningUpperLimit int32 `json:"default_winning_upper_limit"` + IsMonitored bool `json:"is_monitored"` + CompanyID int64 `json:"company_id"` + IsActive bool `json:"is_active"` + IsFeatured bool `json:"is_featured"` + WinningUpperLimit int32 `json:"winning_upper_limit"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` + LeagueCc pgtype.Text `json:"league_cc"` +} + type ExchangeRate struct { ID int32 `json:"id"` FromCurrency string `json:"from_currency"` @@ -318,23 +404,38 @@ type FavoriteGame struct { } type Flag struct { - ID int64 `json:"id"` - BetID pgtype.Int8 `json:"bet_id"` - OddID pgtype.Int8 `json:"odd_id"` - Reason pgtype.Text `json:"reason"` - FlaggedAt pgtype.Timestamp `json:"flagged_at"` - Resolved pgtype.Bool `json:"resolved"` + ID int64 `json:"id"` + BetID pgtype.Int8 `json:"bet_id"` + OddsMarketID pgtype.Int8 `json:"odds_market_id"` + Reason pgtype.Text `json:"reason"` + FlaggedAt pgtype.Timestamp `json:"flagged_at"` + Resolved pgtype.Bool `json:"resolved"` } type League struct { - ID int64 `json:"id"` - Name string `json:"name"` - Img pgtype.Text `json:"img"` - CountryCode pgtype.Text `json:"country_code"` - Bet365ID pgtype.Int4 `json:"bet365_id"` - SportID int32 `json:"sport_id"` - IsActive pgtype.Bool `json:"is_active"` - IsFeatured pgtype.Bool `json:"is_featured"` + ID int64 `json:"id"` + Name string `json:"name"` + ImgUrl pgtype.Text `json:"img_url"` + CountryCode pgtype.Text `json:"country_code"` + Bet365ID pgtype.Int4 `json:"bet365_id"` + SportID int32 `json:"sport_id"` + DefaultIsActive bool `json:"default_is_active"` + DefaultIsFeatured bool `json:"default_is_featured"` +} + +type LeagueWithSetting struct { + ID int64 `json:"id"` + Name string `json:"name"` + ImgUrl pgtype.Text `json:"img_url"` + CountryCode pgtype.Text `json:"country_code"` + Bet365ID pgtype.Int4 `json:"bet365_id"` + SportID int32 `json:"sport_id"` + DefaultIsActive bool `json:"default_is_active"` + DefaultIsFeatured bool `json:"default_is_featured"` + CompanyID int64 `json:"company_id"` + IsActive bool `json:"is_active"` + IsFeatured bool `json:"is_featured"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` } type Notification struct { @@ -354,34 +455,61 @@ type Notification struct { Metadata []byte `json:"metadata"` } -type Odd struct { - ID int64 `json:"id"` - EventID pgtype.Text `json:"event_id"` - Fi pgtype.Text `json:"fi"` - MarketType string `json:"market_type"` - MarketName pgtype.Text `json:"market_name"` - MarketCategory pgtype.Text `json:"market_category"` - MarketID pgtype.Text `json:"market_id"` - Name pgtype.Text `json:"name"` - Handicap pgtype.Text `json:"handicap"` - OddsValue pgtype.Float8 `json:"odds_value"` - Section string `json:"section"` - Category pgtype.Text `json:"category"` - RawOdds []byte `json:"raw_odds"` - FetchedAt pgtype.Timestamp `json:"fetched_at"` - ExpiresAt pgtype.Timestamp `json:"expires_at"` - Source pgtype.Text `json:"source"` - IsActive pgtype.Bool `json:"is_active"` +type OddHistory struct { + ID int64 `json:"id"` + OddsMarketID int64 `json:"odds_market_id"` + RawOddID int64 `json:"raw_odd_id"` + MarketID string `json:"market_id"` + EventID string `json:"event_id"` + OddValue float64 `json:"odd_value"` + CreatedAt pgtype.Timestamp `json:"created_at"` } -type OddHistory struct { - ID int64 `json:"id"` - OddID int64 `json:"odd_id"` - RawOddID int64 `json:"raw_odd_id"` - MarketID string `json:"market_id"` - EventID string `json:"event_id"` - OddValue float64 `json:"odd_value"` - CreatedAt pgtype.Timestamp `json:"created_at"` +type OddsMarket struct { + ID int64 `json:"id"` + EventID string `json:"event_id"` + MarketType string `json:"market_type"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID string `json:"market_id"` + RawOdds []byte `json:"raw_odds"` + DefaultIsActive bool `json:"default_is_active"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` +} + +type OddsMarketWithEvent struct { + ID int64 `json:"id"` + EventID string `json:"event_id"` + MarketType string `json:"market_type"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID string `json:"market_id"` + RawOdds []byte `json:"raw_odds"` + DefaultIsActive bool `json:"default_is_active"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + IsMonitored bool `json:"is_monitored"` + IsLive bool `json:"is_live"` + Status string `json:"status"` + Source string `json:"source"` +} + +type OddsMarketWithSetting struct { + ID int64 `json:"id"` + EventID string `json:"event_id"` + MarketType string `json:"market_type"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID string `json:"market_id"` + RawOdds []byte `json:"raw_odds"` + DefaultIsActive bool `json:"default_is_active"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + CompanyID int64 `json:"company_id"` + IsActive bool `json:"is_active"` + RawOdds []byte `json:"raw_odds"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` } type Otp struct { @@ -599,15 +727,16 @@ type SupportedOperation struct { } type Team struct { - ID string `json:"id"` - TeamName string `json:"team_name"` - Country pgtype.Text `json:"country"` - Bet365ID pgtype.Int4 `json:"bet365_id"` - LogoUrl pgtype.Text `json:"logo_url"` + ID int64 `json:"id"` + TeamName string `json:"team_name"` + CountryCode string `json:"country_code"` + Bet365ID pgtype.Int8 `json:"bet365_id"` + ImgUrl pgtype.Text `json:"img_url"` } type Ticket struct { ID int64 `json:"id"` + CompanyID int64 `json:"company_id"` Amount int64 `json:"amount"` TotalOdds float32 `json:"total_odds"` Ip string `json:"ip"` @@ -634,6 +763,7 @@ type TicketOutcome struct { type TicketWithOutcome struct { ID int64 `json:"id"` + CompanyID int64 `json:"company_id"` Amount int64 `json:"amount"` TotalOdds float32 `json:"total_odds"` Ip string `json:"ip"` @@ -738,6 +868,7 @@ type VirtualGameTransaction struct { type Wallet struct { ID int64 `json:"id"` Balance int64 `json:"balance"` + Currency string `json:"currency"` IsWithdraw bool `json:"is_withdraw"` IsBettable bool `json:"is_bettable"` IsTransferable bool `json:"is_transferable"` @@ -746,7 +877,6 @@ type Wallet struct { IsActive bool `json:"is_active"` CreatedAt pgtype.Timestamp `json:"created_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"` - Currency string `json:"currency"` BonusBalance pgtype.Numeric `json:"bonus_balance"` CashBalance pgtype.Numeric `json:"cash_balance"` } diff --git a/gen/db/odd_history.sql.go b/gen/db/odd_history.sql.go index fa5272a..0a0333d 100644 --- a/gen/db/odd_history.sql.go +++ b/gen/db/odd_history.sql.go @@ -12,10 +12,10 @@ import ( ) const GetAllOddHistory = `-- name: GetAllOddHistory :many -SELECT id, odd_id, raw_odd_id, market_id, event_id, odd_value, created_at +SELECT id, odds_market_id, raw_odd_id, market_id, event_id, odd_value, created_at FROM odd_history WHERE ( - odd_id = $1 + odds_market_id = $1 OR $1 IS NULL ) AND ( @@ -67,7 +67,7 @@ func (q *Queries) GetAllOddHistory(ctx context.Context, arg GetAllOddHistoryPara var i OddHistory if err := rows.Scan( &i.ID, - &i.OddID, + &i.OddsMarketID, &i.RawOddID, &i.MarketID, &i.EventID, @@ -85,10 +85,10 @@ func (q *Queries) GetAllOddHistory(ctx context.Context, arg GetAllOddHistoryPara } const GetInitialOddPerDay = `-- name: GetInitialOddPerDay :many -SELECT DISTINCT ON (DATE_TRUNC($1, created_at)) id, odd_id, raw_odd_id, market_id, event_id, odd_value, created_at +SELECT DISTINCT ON (DATE_TRUNC($1, created_at)) id, odds_market_id, raw_odd_id, market_id, event_id, odd_value, created_at FROM odd_history WHERE ( - odd_id = $2 + odds_market_id = $2 OR $2 IS NULL ) AND ( @@ -144,7 +144,7 @@ func (q *Queries) GetInitialOddPerDay(ctx context.Context, arg GetInitialOddPerD var i OddHistory if err := rows.Scan( &i.ID, - &i.OddID, + &i.OddsMarketID, &i.RawOddID, &i.MarketID, &i.EventID, @@ -163,27 +163,27 @@ func (q *Queries) GetInitialOddPerDay(ctx context.Context, arg GetInitialOddPerD const InsertOddHistory = `-- name: InsertOddHistory :one INSERT INTO odd_history ( - odd_id, + odds_market_id, market_id, raw_odd_id, event_id, odd_value ) VALUES ($1, $2, $3, $4, $5) -RETURNING id, odd_id, raw_odd_id, market_id, event_id, odd_value, created_at +RETURNING id, odds_market_id, raw_odd_id, market_id, event_id, odd_value, created_at ` type InsertOddHistoryParams struct { - OddID int64 `json:"odd_id"` - MarketID string `json:"market_id"` - RawOddID int64 `json:"raw_odd_id"` - EventID string `json:"event_id"` - OddValue float64 `json:"odd_value"` + OddsMarketID int64 `json:"odds_market_id"` + MarketID string `json:"market_id"` + RawOddID int64 `json:"raw_odd_id"` + EventID string `json:"event_id"` + OddValue float64 `json:"odd_value"` } func (q *Queries) InsertOddHistory(ctx context.Context, arg InsertOddHistoryParams) (OddHistory, error) { row := q.db.QueryRow(ctx, InsertOddHistory, - arg.OddID, + arg.OddsMarketID, arg.MarketID, arg.RawOddID, arg.EventID, @@ -192,7 +192,7 @@ func (q *Queries) InsertOddHistory(ctx context.Context, arg InsertOddHistoryPara var i OddHistory err := row.Scan( &i.ID, - &i.OddID, + &i.OddsMarketID, &i.RawOddID, &i.MarketID, &i.EventID, diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index 2865054..966fcff 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -12,49 +12,168 @@ import ( ) const DeleteOddsForEvent = `-- name: DeleteOddsForEvent :exec -DELETE FROM odds -Where fi = $1 +DELETE FROM odds_market +Where event_id = $1 ` -func (q *Queries) DeleteOddsForEvent(ctx context.Context, fi pgtype.Text) error { - _, err := q.db.Exec(ctx, DeleteOddsForEvent, fi) +func (q *Queries) DeleteOddsForEvent(ctx context.Context, eventID string) error { + _, err := q.db.Exec(ctx, DeleteOddsForEvent, eventID) return err } -const GetALLPrematchOdds = `-- name: GetALLPrematchOdds :many -SELECT id, event_id, fi, market_type, market_name, market_category, market_id, name, handicap, odds_value, section, category, raw_odds, fetched_at, expires_at, source, is_active -FROM odds -WHERE is_active = true - AND source = 'bet365' +const GetAllOdds = `-- name: GetAllOdds :many +SELECT id, event_id, market_type, market_name, market_category, market_id, raw_odds, default_is_active, fetched_at, expires_at, is_monitored, is_live, status, source +FROM odds_market_with_event +LIMIT $2 OFFSET $1 ` -func (q *Queries) GetALLPrematchOdds(ctx context.Context) ([]Odd, error) { - rows, err := q.db.Query(ctx, GetALLPrematchOdds) +type GetAllOddsParams struct { + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` +} + +func (q *Queries) GetAllOdds(ctx context.Context, arg GetAllOddsParams) ([]OddsMarketWithEvent, error) { + rows, err := q.db.Query(ctx, GetAllOdds, arg.Offset, arg.Limit) if err != nil { return nil, err } defer rows.Close() - var items []Odd + var items []OddsMarketWithEvent for rows.Next() { - var i Odd + var i OddsMarketWithEvent if err := rows.Scan( &i.ID, &i.EventID, - &i.Fi, &i.MarketType, &i.MarketName, &i.MarketCategory, &i.MarketID, - &i.Name, - &i.Handicap, - &i.OddsValue, - &i.Section, - &i.Category, &i.RawOdds, + &i.DefaultIsActive, &i.FetchedAt, &i.ExpiresAt, + &i.IsMonitored, + &i.IsLive, + &i.Status, &i.Source, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetAllOddsWithSettings = `-- name: GetAllOddsWithSettings :many +SELECT id, event_id, market_type, market_name, market_category, market_id, odds_market_with_settings.raw_odds, default_is_active, fetched_at, expires_at, company_id, is_active, odds_market_with_settings.raw_odds, updated_at +FROM odds_market_with_settings +WHERE company_id = $1 +LIMIT $3 OFFSET $2 +` + +type GetAllOddsWithSettingsParams struct { + CompanyID int64 `json:"company_id"` + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` +} + +func (q *Queries) GetAllOddsWithSettings(ctx context.Context, arg GetAllOddsWithSettingsParams) ([]OddsMarketWithSetting, error) { + rows, err := q.db.Query(ctx, GetAllOddsWithSettings, arg.CompanyID, arg.Offset, arg.Limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []OddsMarketWithSetting + for rows.Next() { + var i OddsMarketWithSetting + if err := rows.Scan( + &i.ID, + &i.EventID, + &i.MarketType, + &i.MarketName, + &i.MarketCategory, + &i.MarketID, + &i.RawOdds, + &i.DefaultIsActive, + &i.FetchedAt, + &i.ExpiresAt, + &i.CompanyID, &i.IsActive, + &i.RawOdds, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetOddsByEventID = `-- name: GetOddsByEventID :many +SELECT id, event_id, market_type, market_name, market_category, market_id, raw_odds, default_is_active, fetched_at, expires_at, is_monitored, is_live, status, source +FROM odds_market_with_event +WHERE event_id = $1 + AND ( + is_live = $2 + OR $2 IS NULL + ) + AND ( + status = $3 + OR $3 IS NULL + ) + AND ( + source = $4 + OR $4 IS NULL + ) +LIMIT $6 OFFSET $5 +` + +type GetOddsByEventIDParams struct { + EventID string `json:"event_id"` + IsLive pgtype.Bool `json:"is_live"` + Status pgtype.Text `json:"status"` + Source pgtype.Text `json:"source"` + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` +} + +func (q *Queries) GetOddsByEventID(ctx context.Context, arg GetOddsByEventIDParams) ([]OddsMarketWithEvent, error) { + rows, err := q.db.Query(ctx, GetOddsByEventID, + arg.EventID, + arg.IsLive, + arg.Status, + arg.Source, + arg.Offset, + arg.Limit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []OddsMarketWithEvent + for rows.Next() { + var i OddsMarketWithEvent + if err := rows.Scan( + &i.ID, + &i.EventID, + &i.MarketType, + &i.MarketName, + &i.MarketCategory, + &i.MarketID, + &i.RawOdds, + &i.DefaultIsActive, + &i.FetchedAt, + &i.ExpiresAt, + &i.IsMonitored, + &i.IsLive, + &i.Status, + &i.Source, ); err != nil { return nil, err } @@ -67,89 +186,83 @@ func (q *Queries) GetALLPrematchOdds(ctx context.Context) ([]Odd, error) { } const GetOddsByMarketID = `-- name: GetOddsByMarketID :one -SELECT id, event_id, fi, market_type, market_name, market_category, market_id, name, handicap, odds_value, section, category, raw_odds, fetched_at, expires_at, source, is_active -FROM odds +SELECT id, event_id, market_type, market_name, market_category, market_id, raw_odds, default_is_active, fetched_at, expires_at, is_monitored, is_live, status, source +FROM odds_market_with_event WHERE market_id = $1 - AND fi = $2 - AND is_active = true - AND source = 'bet365' + AND event_id = $2 ` type GetOddsByMarketIDParams struct { - MarketID pgtype.Text `json:"market_id"` - Fi pgtype.Text `json:"fi"` + MarketID string `json:"market_id"` + EventID string `json:"event_id"` } -func (q *Queries) GetOddsByMarketID(ctx context.Context, arg GetOddsByMarketIDParams) (Odd, error) { - row := q.db.QueryRow(ctx, GetOddsByMarketID, arg.MarketID, arg.Fi) - var i Odd +func (q *Queries) GetOddsByMarketID(ctx context.Context, arg GetOddsByMarketIDParams) (OddsMarketWithEvent, error) { + row := q.db.QueryRow(ctx, GetOddsByMarketID, arg.MarketID, arg.EventID) + var i OddsMarketWithEvent err := row.Scan( &i.ID, &i.EventID, - &i.Fi, &i.MarketType, &i.MarketName, &i.MarketCategory, &i.MarketID, - &i.Name, - &i.Handicap, - &i.OddsValue, - &i.Section, - &i.Category, &i.RawOdds, + &i.DefaultIsActive, &i.FetchedAt, &i.ExpiresAt, + &i.IsMonitored, + &i.IsLive, + &i.Status, &i.Source, - &i.IsActive, ) return i, err } -const GetPaginatedPrematchOddsByUpcomingID = `-- name: GetPaginatedPrematchOddsByUpcomingID :many -SELECT o.id, o.event_id, o.fi, o.market_type, o.market_name, o.market_category, o.market_id, o.name, o.handicap, o.odds_value, o.section, o.category, o.raw_odds, o.fetched_at, o.expires_at, o.source, o.is_active -FROM odds o - JOIN events e ON o.fi = e.id -WHERE e.id = $1 - AND e.is_live = false - AND e.status = 'upcoming' - AND o.is_active = true - AND o.source = 'bet365' -LIMIT $3 OFFSET $2 +const GetOddsWithSettingsByEventID = `-- name: GetOddsWithSettingsByEventID :many +SELECT id, event_id, market_type, market_name, market_category, market_id, odds_market_with_settings.raw_odds, default_is_active, fetched_at, expires_at, company_id, is_active, odds_market_with_settings.raw_odds, updated_at +FROM odds_market_with_settings +WHERE event_id = $1 + AND company_id = $2 +LIMIT $4 OFFSET $3 ` -type GetPaginatedPrematchOddsByUpcomingIDParams struct { - ID string `json:"id"` - Offset pgtype.Int4 `json:"offset"` - Limit pgtype.Int4 `json:"limit"` +type GetOddsWithSettingsByEventIDParams struct { + EventID string `json:"event_id"` + CompanyID int64 `json:"company_id"` + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` } -func (q *Queries) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, arg GetPaginatedPrematchOddsByUpcomingIDParams) ([]Odd, error) { - rows, err := q.db.Query(ctx, GetPaginatedPrematchOddsByUpcomingID, arg.ID, arg.Offset, arg.Limit) +func (q *Queries) GetOddsWithSettingsByEventID(ctx context.Context, arg GetOddsWithSettingsByEventIDParams) ([]OddsMarketWithSetting, error) { + rows, err := q.db.Query(ctx, GetOddsWithSettingsByEventID, + arg.EventID, + arg.CompanyID, + arg.Offset, + arg.Limit, + ) if err != nil { return nil, err } defer rows.Close() - var items []Odd + var items []OddsMarketWithSetting for rows.Next() { - var i Odd + var i OddsMarketWithSetting if err := rows.Scan( &i.ID, &i.EventID, - &i.Fi, &i.MarketType, &i.MarketName, &i.MarketCategory, &i.MarketID, - &i.Name, - &i.Handicap, - &i.OddsValue, - &i.Section, - &i.Category, &i.RawOdds, + &i.DefaultIsActive, &i.FetchedAt, &i.ExpiresAt, - &i.Source, + &i.CompanyID, &i.IsActive, + &i.RawOdds, + &i.UpdatedAt, ); err != nil { return nil, err } @@ -161,116 +274,80 @@ func (q *Queries) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, arg return items, nil } -const GetPrematchOdds = `-- name: GetPrematchOdds :many -SELECT id, event_id, fi, market_type, market_name, market_category, market_id, name, handicap, odds_value, section, category, raw_odds, fetched_at, expires_at, source, is_active -FROM odds -WHERE is_active = true - AND source = 'bet365' +const GetOddsWithSettingsByMarketID = `-- name: GetOddsWithSettingsByMarketID :one +SELECT id, event_id, market_type, market_name, market_category, market_id, odds_market_with_settings.raw_odds, default_is_active, fetched_at, expires_at, company_id, is_active, odds_market_with_settings.raw_odds, updated_at +FROM odds_market_with_settings +WHERE market_id = $1 + AND event_id = $2 + AND company_id = $3 ` -func (q *Queries) GetPrematchOdds(ctx context.Context) ([]Odd, error) { - rows, err := q.db.Query(ctx, GetPrematchOdds) - 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.MarketType, - &i.MarketName, - &i.MarketCategory, - &i.MarketID, - &i.Name, - &i.Handicap, - &i.OddsValue, - &i.Section, - &i.Category, - &i.RawOdds, - &i.FetchedAt, - &i.ExpiresAt, - &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 +type GetOddsWithSettingsByMarketIDParams struct { + MarketID string `json:"market_id"` + EventID string `json:"event_id"` + CompanyID int64 `json:"company_id"` } -const GetPrematchOddsByUpcomingID = `-- name: GetPrematchOddsByUpcomingID :many -SELECT o.id, o.event_id, o.fi, o.market_type, o.market_name, o.market_category, o.market_id, o.name, o.handicap, o.odds_value, o.section, o.category, o.raw_odds, o.fetched_at, o.expires_at, o.source, o.is_active -FROM odds o - JOIN events e ON o.fi = e.id -WHERE e.id = $1 - AND e.is_live = false - AND e.status = 'upcoming' - AND o.is_active = true - AND o.source = 'bet365' +func (q *Queries) GetOddsWithSettingsByMarketID(ctx context.Context, arg GetOddsWithSettingsByMarketIDParams) (OddsMarketWithSetting, error) { + row := q.db.QueryRow(ctx, GetOddsWithSettingsByMarketID, arg.MarketID, arg.EventID, arg.CompanyID) + var i OddsMarketWithSetting + err := row.Scan( + &i.ID, + &i.EventID, + &i.MarketType, + &i.MarketName, + &i.MarketCategory, + &i.MarketID, + &i.RawOdds, + &i.DefaultIsActive, + &i.FetchedAt, + &i.ExpiresAt, + &i.CompanyID, + &i.IsActive, + &i.RawOdds, + &i.UpdatedAt, + ) + return i, err +} + +const InsertOddSettings = `-- name: InsertOddSettings :exec +INSERT INTO company_odd_settings ( + company_id, + odds_market_id, + is_active, + custom_raw_odds + ) +VALUES ($1, $2, $3, $4) ON CONFLICT (company_id, odds_market_id) DO +UPDATE +SET is_active = EXCLUDED.is_active, + custom_raw_odds = EXCLUDED.custom_raw_odds ` -func (q *Queries) GetPrematchOddsByUpcomingID(ctx context.Context, id string) ([]Odd, error) { - rows, err := q.db.Query(ctx, GetPrematchOddsByUpcomingID, id) - 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.MarketType, - &i.MarketName, - &i.MarketCategory, - &i.MarketID, - &i.Name, - &i.Handicap, - &i.OddsValue, - &i.Section, - &i.Category, - &i.RawOdds, - &i.FetchedAt, - &i.ExpiresAt, - &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 +type InsertOddSettingsParams struct { + CompanyID int64 `json:"company_id"` + OddsMarketID int64 `json:"odds_market_id"` + IsActive pgtype.Bool `json:"is_active"` + CustomRawOdds []byte `json:"custom_raw_odds"` } -const InsertNonLiveOdd = `-- name: InsertNonLiveOdd :exec -INSERT INTO odds ( +func (q *Queries) InsertOddSettings(ctx context.Context, arg InsertOddSettingsParams) error { + _, err := q.db.Exec(ctx, InsertOddSettings, + arg.CompanyID, + arg.OddsMarketID, + arg.IsActive, + arg.CustomRawOdds, + ) + return err +} + +const InsertOddsMarket = `-- name: InsertOddsMarket :exec +INSERT INTO odds_market ( event_id, - fi, market_type, market_name, market_category, market_id, - name, - handicap, - odds_value, - section, - category, raw_odds, - is_active, - source, fetched_at, expires_at ) @@ -282,65 +359,36 @@ VALUES ( $5, $6, $7, - $8, - $9, - $10, - $11, - $12, - $13, - $14, - $15, - $16 + $8 ) ON CONFLICT (event_id, market_id) DO UPDATE -SET odds_value = EXCLUDED.odds_value, - raw_odds = EXCLUDED.raw_odds, - market_type = EXCLUDED.market_type, +SET market_type = EXCLUDED.market_type, market_name = EXCLUDED.market_name, market_category = EXCLUDED.market_category, - name = EXCLUDED.name, - handicap = EXCLUDED.handicap, + raw_odds = EXCLUDED.raw_odds, fetched_at = EXCLUDED.fetched_at, - is_active = EXCLUDED.is_active, - source = EXCLUDED.source, - fi = EXCLUDED.fi + expires_at = EXCLUDED.expires_at ` -type InsertNonLiveOddParams struct { - EventID pgtype.Text `json:"event_id"` - Fi pgtype.Text `json:"fi"` +type InsertOddsMarketParams struct { + EventID string `json:"event_id"` MarketType string `json:"market_type"` - MarketName pgtype.Text `json:"market_name"` - MarketCategory pgtype.Text `json:"market_category"` - MarketID pgtype.Text `json:"market_id"` - Name pgtype.Text `json:"name"` - Handicap pgtype.Text `json:"handicap"` - OddsValue pgtype.Float8 `json:"odds_value"` - Section string `json:"section"` - Category pgtype.Text `json:"category"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID string `json:"market_id"` RawOdds []byte `json:"raw_odds"` - IsActive pgtype.Bool `json:"is_active"` - Source pgtype.Text `json:"source"` FetchedAt pgtype.Timestamp `json:"fetched_at"` ExpiresAt pgtype.Timestamp `json:"expires_at"` } -func (q *Queries) InsertNonLiveOdd(ctx context.Context, arg InsertNonLiveOddParams) error { - _, err := q.db.Exec(ctx, InsertNonLiveOdd, +func (q *Queries) InsertOddsMarket(ctx context.Context, arg InsertOddsMarketParams) error { + _, err := q.db.Exec(ctx, InsertOddsMarket, arg.EventID, - arg.Fi, arg.MarketType, arg.MarketName, arg.MarketCategory, arg.MarketID, - arg.Name, - arg.Handicap, - arg.OddsValue, - arg.Section, - arg.Category, arg.RawOdds, - arg.IsActive, - arg.Source, arg.FetchedAt, arg.ExpiresAt, ) diff --git a/gen/db/ticket.sql.go b/gen/db/ticket.sql.go index c72c1ea..90e11f6 100644 --- a/gen/db/ticket.sql.go +++ b/gen/db/ticket.sql.go @@ -27,7 +27,7 @@ func (q *Queries) CountTicketByIP(ctx context.Context, ip string) (int64, error) const CreateTicket = `-- name: CreateTicket :one INSERT INTO tickets (amount, total_odds, ip) VALUES ($1, $2, $3) -RETURNING id, amount, total_odds, ip, created_at, updated_at +RETURNING id, company_id, amount, total_odds, ip, created_at, updated_at ` type CreateTicketParams struct { @@ -41,6 +41,7 @@ func (q *Queries) CreateTicket(ctx context.Context, arg CreateTicketParams) (Tic var i Ticket err := row.Scan( &i.ID, + &i.CompanyID, &i.Amount, &i.TotalOdds, &i.Ip, @@ -96,7 +97,7 @@ func (q *Queries) DeleteTicketOutcome(ctx context.Context, ticketID int64) error } const GetAllTickets = `-- name: GetAllTickets :many -SELECT id, amount, total_odds, ip, created_at, updated_at, outcomes +SELECT id, company_id, amount, total_odds, ip, created_at, updated_at, outcomes FROM ticket_with_outcomes ` @@ -111,6 +112,7 @@ func (q *Queries) GetAllTickets(ctx context.Context) ([]TicketWithOutcome, error var i TicketWithOutcome if err := rows.Scan( &i.ID, + &i.CompanyID, &i.Amount, &i.TotalOdds, &i.Ip, @@ -152,7 +154,7 @@ func (q *Queries) GetAllTicketsInRange(ctx context.Context, arg GetAllTicketsInR } const GetTicketByID = `-- name: GetTicketByID :one -SELECT id, amount, total_odds, ip, created_at, updated_at, outcomes +SELECT id, company_id, amount, total_odds, ip, created_at, updated_at, outcomes FROM ticket_with_outcomes WHERE id = $1 ` @@ -162,6 +164,7 @@ func (q *Queries) GetTicketByID(ctx context.Context, id int64) (TicketWithOutcom var i TicketWithOutcome err := row.Scan( &i.ID, + &i.CompanyID, &i.Amount, &i.TotalOdds, &i.Ip, diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index 48faa10..0d4c33b 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -17,17 +17,20 @@ SELECT EXISTS ( FROM users WHERE users.phone_number = $1 AND users.phone_number IS NOT NULL + AND users.company_id = $2 ) AS phone_exists, EXISTS ( SELECT 1 FROM users - WHERE users.email = $2 + WHERE users.email = $3 AND users.email IS NOT NULL + AND users.company_id = $2 ) AS email_exists ` type CheckPhoneEmailExistParams struct { PhoneNumber pgtype.Text `json:"phone_number"` + CompanyID pgtype.Int8 `json:"company_id"` Email pgtype.Text `json:"email"` } @@ -37,7 +40,7 @@ type CheckPhoneEmailExistRow struct { } func (q *Queries) CheckPhoneEmailExist(ctx context.Context, arg CheckPhoneEmailExistParams) (CheckPhoneEmailExistRow, error) { - row := q.db.QueryRow(ctx, CheckPhoneEmailExist, arg.PhoneNumber, arg.Email) + row := q.db.QueryRow(ctx, CheckPhoneEmailExist, arg.PhoneNumber, arg.CompanyID, arg.Email) var i CheckPhoneEmailExistRow err := row.Scan(&i.PhoneExists, &i.EmailExists) return i, err @@ -339,8 +342,14 @@ SELECT id, company_id FROM users WHERE email = $1 + AND company_id = $2 ` +type GetUserByEmailParams struct { + Email pgtype.Text `json:"email"` + CompanyID pgtype.Int8 `json:"company_id"` +} + type GetUserByEmailRow struct { ID int64 `json:"id"` FirstName string `json:"first_name"` @@ -357,8 +366,8 @@ type GetUserByEmailRow struct { CompanyID pgtype.Int8 `json:"company_id"` } -func (q *Queries) GetUserByEmail(ctx context.Context, email pgtype.Text) (GetUserByEmailRow, error) { - row := q.db.QueryRow(ctx, GetUserByEmail, email) +func (q *Queries) GetUserByEmail(ctx context.Context, arg GetUserByEmailParams) (GetUserByEmailRow, error) { + row := q.db.QueryRow(ctx, GetUserByEmail, arg.Email, arg.CompanyID) var i GetUserByEmailRow err := row.Scan( &i.ID, @@ -424,8 +433,14 @@ SELECT id, company_id FROM users WHERE phone_number = $1 + AND company_id = $2 ` +type GetUserByPhoneParams struct { + PhoneNumber pgtype.Text `json:"phone_number"` + CompanyID pgtype.Int8 `json:"company_id"` +} + type GetUserByPhoneRow struct { ID int64 `json:"id"` FirstName string `json:"first_name"` @@ -442,8 +457,8 @@ type GetUserByPhoneRow struct { CompanyID pgtype.Int8 `json:"company_id"` } -func (q *Queries) GetUserByPhone(ctx context.Context, phoneNumber pgtype.Text) (GetUserByPhoneRow, error) { - row := q.db.QueryRow(ctx, GetUserByPhone, phoneNumber) +func (q *Queries) GetUserByPhone(ctx context.Context, arg GetUserByPhoneParams) (GetUserByPhoneRow, error) { + row := q.db.QueryRow(ctx, GetUserByPhone, arg.PhoneNumber, arg.CompanyID) var i GetUserByPhoneRow err := row.Scan( &i.ID, @@ -478,25 +493,22 @@ SELECT id, suspended_at, company_id FROM users -WHERE ( - first_name ILIKE '%' || $1 || '%' - OR last_name ILIKE '%' || $1 || '%' - OR phone_number LIKE '%' || $1 || '%' +WHERE (company_id = $1) + AND ( + first_name ILIKE '%' || $2 || '%' + OR last_name ILIKE '%' || $2 || '%' + OR phone_number LIKE '%' || $2 || '%' ) AND ( - role = $2 - OR $2 IS NULL - ) - AND ( - company_id = $3 + role = $3 OR $3 IS NULL ) ` type SearchUserByNameOrPhoneParams struct { - Column1 pgtype.Text `json:"column_1"` - Role pgtype.Text `json:"role"` CompanyID pgtype.Int8 `json:"company_id"` + Column2 pgtype.Text `json:"column_2"` + Role pgtype.Text `json:"role"` } type SearchUserByNameOrPhoneRow struct { @@ -516,7 +528,7 @@ type SearchUserByNameOrPhoneRow struct { } func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, arg SearchUserByNameOrPhoneParams) ([]SearchUserByNameOrPhoneRow, error) { - rows, err := q.db.Query(ctx, SearchUserByNameOrPhone, arg.Column1, arg.Role, arg.CompanyID) + rows, err := q.db.Query(ctx, SearchUserByNameOrPhone, arg.CompanyID, arg.Column2, arg.Role) if err != nil { return nil, err } @@ -575,6 +587,7 @@ SET password = $1, WHERE ( email = $2 OR phone_number = $3 + AND company_id = $4 ) ` diff --git a/gen/db/wallet.sql.go b/gen/db/wallet.sql.go index 1cb7387..7e3eb7f 100644 --- a/gen/db/wallet.sql.go +++ b/gen/db/wallet.sql.go @@ -50,7 +50,7 @@ INSERT INTO wallets ( type ) VALUES ($1, $2, $3, $4, $5) -RETURNING id, balance, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at, currency, bonus_balance, cash_balance +RETURNING id, balance, currency, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at, bonus_balance, cash_balance ` type CreateWalletParams struct { @@ -73,6 +73,7 @@ func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (Wal err := row.Scan( &i.ID, &i.Balance, + &i.Currency, &i.IsWithdraw, &i.IsBettable, &i.IsTransferable, @@ -81,7 +82,6 @@ func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (Wal &i.IsActive, &i.CreatedAt, &i.UpdatedAt, - &i.Currency, &i.BonusBalance, &i.CashBalance, ) @@ -188,7 +188,7 @@ func (q *Queries) GetAllCustomerWallet(ctx context.Context) ([]CustomerWalletDet } const GetAllWallets = `-- name: GetAllWallets :many -SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at, currency, bonus_balance, cash_balance +SELECT id, balance, currency, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at, bonus_balance, cash_balance FROM wallets ` @@ -204,6 +204,7 @@ func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) { if err := rows.Scan( &i.ID, &i.Balance, + &i.Currency, &i.IsWithdraw, &i.IsBettable, &i.IsTransferable, @@ -212,7 +213,6 @@ func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) { &i.IsActive, &i.CreatedAt, &i.UpdatedAt, - &i.Currency, &i.BonusBalance, &i.CashBalance, ); err != nil { @@ -319,7 +319,7 @@ func (q *Queries) GetCustomerWallet(ctx context.Context, customerID int64) (Cust } const GetWalletByID = `-- name: GetWalletByID :one -SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at, currency, bonus_balance, cash_balance +SELECT id, balance, currency, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at, bonus_balance, cash_balance FROM wallets WHERE id = $1 ` @@ -330,6 +330,7 @@ func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) { err := row.Scan( &i.ID, &i.Balance, + &i.Currency, &i.IsWithdraw, &i.IsBettable, &i.IsTransferable, @@ -338,7 +339,6 @@ func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) { &i.IsActive, &i.CreatedAt, &i.UpdatedAt, - &i.Currency, &i.BonusBalance, &i.CashBalance, ) @@ -346,7 +346,7 @@ func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) { } const GetWalletByUserID = `-- name: GetWalletByUserID :many -SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at, currency, bonus_balance, cash_balance +SELECT id, balance, currency, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at, bonus_balance, cash_balance FROM wallets WHERE user_id = $1 ` @@ -363,6 +363,7 @@ func (q *Queries) GetWalletByUserID(ctx context.Context, userID int64) ([]Wallet if err := rows.Scan( &i.ID, &i.Balance, + &i.Currency, &i.IsWithdraw, &i.IsBettable, &i.IsTransferable, @@ -371,7 +372,6 @@ func (q *Queries) GetWalletByUserID(ctx context.Context, userID int64) ([]Wallet &i.IsActive, &i.CreatedAt, &i.UpdatedAt, - &i.Currency, &i.BonusBalance, &i.CashBalance, ); err != nil { diff --git a/internal/domain/common_log.go b/internal/domain/common_log.go new file mode 100644 index 0000000..a7cda24 --- /dev/null +++ b/internal/domain/common_log.go @@ -0,0 +1,34 @@ +package domain + +import ( + "time" + + "github.com/gofiber/fiber/v2" + "go.uber.org/zap" +) + +// var BadRequestZapFields = []zap.Field{ +// zap.Int("status_code", fiber.StatusBadRequest), +// zap.Time("timestamp", time.Now()), +// } +// var InternalServerErrorZapFields = []zap.Field{ +// zap.Int("status_code", fiber.StatusBadRequest), +// zap.Time("timestamp", time.Now()), +// } + +var SuccessResZapFields = []zap.Field{ + zap.Int("status_code", fiber.StatusBadRequest), + zap.Time("timestamp", time.Now()), +} + +var BadRequestLogger = MongoDBLogger.With( + zap.Int("status_code", fiber.StatusBadRequest), + zap.Time("timestamp", time.Now())) + +var InternalServerErrorLogger = MongoDBLogger.With( + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Time("timestamp", time.Now())) + +var SuccessResLogger = MongoDBLogger.With( + zap.Int("status_code", fiber.StatusOK), + zap.Time("timestamp", time.Now())) diff --git a/internal/domain/company.go b/internal/domain/company.go index ec170be..07d1e25 100644 --- a/internal/domain/company.go +++ b/internal/domain/company.go @@ -11,6 +11,7 @@ import ( type Company struct { ID int64 Name string + Slug string AdminID int64 WalletID int64 DeductedPercentage float32 @@ -27,6 +28,7 @@ type CompanyFilter struct { type GetCompany struct { ID int64 Name string + Slug string AdminID int64 AdminFirstName string AdminLastName string @@ -68,6 +70,7 @@ type UpdateCompanyReq struct { type CompanyRes struct { ID int64 `json:"id" example:"1"` Name string `json:"name" example:"CompanyName"` + Slug string `json:"slug" example:"slug"` AdminID int64 `json:"admin_id" example:"1"` WalletID int64 `json:"wallet_id" example:"1"` DeductedPercentage float32 `json:"deducted_percentage" example:"0.1"` @@ -77,6 +80,7 @@ type CompanyRes struct { type GetCompanyRes struct { ID int64 `json:"id" example:"1"` Name string `json:"name" example:"CompanyName"` + Slug string `json:"slug" example:"slug"` AdminID int64 `json:"admin_id" example:"1"` WalletID int64 `json:"wallet_id" example:"1"` WalletBalance float32 `json:"balance" example:"1"` @@ -92,6 +96,7 @@ func ConvertCompany(company Company) CompanyRes { return CompanyRes{ ID: company.ID, Name: company.Name, + Slug: company.Slug, AdminID: company.AdminID, WalletID: company.WalletID, IsActive: company.IsActive, @@ -103,6 +108,7 @@ func ConvertGetCompany(company GetCompany) GetCompanyRes { return GetCompanyRes{ ID: company.ID, Name: company.Name, + Slug: company.Slug, AdminID: company.AdminID, WalletID: company.WalletID, WalletBalance: company.WalletBalance.Float32(), @@ -112,13 +118,13 @@ func ConvertGetCompany(company GetCompany) GetCompanyRes { AdminFirstName: company.AdminFirstName, AdminLastName: company.AdminLastName, AdminPhoneNumber: company.AdminPhoneNumber, - } } -func ConvertCreateCompany(company CreateCompany) dbgen.CreateCompanyParams { +func ConvertCreateCompany(company CreateCompany, uniqueSlug string) dbgen.CreateCompanyParams { return dbgen.CreateCompanyParams{ Name: company.Name, + Slug: uniqueSlug, AdminID: company.AdminID, WalletID: company.WalletID, DeductedPercentage: company.DeductedPercentage, @@ -129,6 +135,7 @@ func ConvertDBCompany(dbCompany dbgen.Company) Company { return Company{ ID: dbCompany.ID, Name: dbCompany.Name, + Slug: dbCompany.Slug, AdminID: dbCompany.AdminID, WalletID: dbCompany.WalletID, DeductedPercentage: dbCompany.DeductedPercentage, @@ -140,6 +147,7 @@ func ConvertDBCompanyDetails(dbCompany dbgen.CompaniesDetail) GetCompany { return GetCompany{ ID: dbCompany.ID, Name: dbCompany.Name, + Slug: dbCompany.Slug, AdminID: dbCompany.AdminID, WalletID: dbCompany.WalletID, WalletBalance: Currency(dbCompany.Balance), diff --git a/internal/domain/custom_odds.go b/internal/domain/custom_odds.go index 6f9e919..1a0a171 100644 --- a/internal/domain/custom_odds.go +++ b/internal/domain/custom_odds.go @@ -1,6 +1,7 @@ package domain import ( + "encoding/json" "time" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" @@ -9,35 +10,60 @@ import ( type CustomOdd struct { ID int64 OddID int64 - RawOddID int64 + CompanyID int64 EventID string - OddValue float64 + RawOdds []json.RawMessage CreatedAt time.Time } type CreateCustomOdd struct { - OddID int64 - RawOddID int64 - EventID string - OddValue float64 + OddID int64 + CompanyID int64 + EventID string + RawOdds []json.RawMessage } -func ConvertCreateCustomOdd(odd CreateCustomOdd) dbgen.InsertCustomOddParams { - return dbgen.InsertCustomOddParams{ - OddID: odd.OddID, - RawOddID: odd.RawOddID, - EventID: odd.EventID, - OddValue: odd.OddValue, +type CustomOddFilter struct { + CompanyID ValidInt64 +} + +func ConvertCreateCustomOdd(odd CreateCustomOdd) (dbgen.InsertCustomOddParams, error) { + rawOddsBytes, err := json.Marshal(odd.RawOdds) + + if err != nil { + return dbgen.InsertCustomOddParams{}, err } + return dbgen.InsertCustomOddParams{ + OddID: odd.OddID, + CompanyID: odd.CompanyID, + EventID: odd.EventID, + RawOdds: rawOddsBytes, + }, nil } -func ConvertDBCustomOdd(dbCustomOdd dbgen.CustomOdd) CustomOdd { +func ConvertDBCustomOdd(dbCustomOdd dbgen.CustomOdd) (CustomOdd, error) { + var rawOdds []json.RawMessage + if err := json.Unmarshal(dbCustomOdd.RawOdds, &rawOdds); err != nil { + return CustomOdd{}, err + } return CustomOdd{ ID: dbCustomOdd.ID, OddID: dbCustomOdd.OddID, - RawOddID: dbCustomOdd.RawOddID, + CompanyID: dbCustomOdd.CompanyID, EventID: dbCustomOdd.EventID, - OddValue: dbCustomOdd.OddValue, + RawOdds: rawOdds, CreatedAt: dbCustomOdd.CreatedAt.Time, - } + }, nil +} + +func ConvertDbCustomOdds(list []dbgen.CustomOdd) ([]CustomOdd, error) { + result := make([]CustomOdd, 0, len(list)) + for _, item := range list { + convertedItem, err := ConvertDBCustomOdd(item) + if err != nil { + return nil, err + } + result = append(result, convertedItem) + } + return result, nil } diff --git a/internal/domain/disabled_odds.go b/internal/domain/disabled_odds.go index d05b3fc..c4f731f 100644 --- a/internal/domain/disabled_odds.go +++ b/internal/domain/disabled_odds.go @@ -11,6 +11,7 @@ type DisabledOdd struct { OddID int64 RawOddID int64 EventID string + CompanyID int64 CreatedAt time.Time } @@ -18,12 +19,14 @@ type CreateDisabledOdd struct { OddID int64 RawOddID int64 EventID string + CompanyID int64 } func ConvertCreateDisabledOdd(odd CreateDisabledOdd) dbgen.InsertDisabledOddsParams { return dbgen.InsertDisabledOddsParams{ OddID: odd.OddID, EventID: odd.EventID, + CompanyID: odd.CompanyID, RawOddID: odd.RawOddID, } } @@ -34,6 +37,7 @@ func ConvertDBDisabledOdd(dbDisabledOdd dbgen.DisabledOdd) DisabledOdd { OddID: dbDisabledOdd.OddID, RawOddID: dbDisabledOdd.RawOddID, EventID: dbDisabledOdd.EventID, + CompanyID: dbDisabledOdd.CompanyID, CreatedAt: dbDisabledOdd.CreatedAt.Time, } } diff --git a/internal/domain/event.go b/internal/domain/event.go index f241566..0906918 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -1,6 +1,11 @@ package domain -import "time" +import ( + "time" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/jackc/pgx/v5/pgtype" +) // TODO: turn status into an enum // Status represents the status of an event. @@ -35,93 +40,408 @@ const ( STATUS_REMOVED EventStatus = "removed" ) -type Event struct { - ID string - SportID int32 - MatchName string - HomeTeam string - AwayTeam string - HomeTeamID int32 - AwayTeamID int32 - HomeKitImage string - AwayKitImage string - LeagueID int32 - LeagueName string - LeagueCC string - StartTime string - Score string - MatchMinute int - TimerStatus string - AddedTime int - MatchPeriod int - IsLive bool - Status string - Source string +type EventSource string + +const ( + EVENT_SOURCE_BET365 EventSource = "b365api" + EVENT_SOURCE_BWIN EventSource = "bwin" + EVENT_SOURCE_BETFAIR EventSource = "bfair" + EVENT_SOURCE_1XBET EventSource = "1xbet" + EVENT_SOURCE_ENET EventSource = "enetpulse" +) + +type BaseEvent struct { + ID string + SportID int32 + MatchName string + HomeTeam string + AwayTeam string + HomeTeamID int64 + AwayTeamID int64 + HomeTeamImage string + AwayTeamImage string + LeagueID int64 + LeagueName string + LeagueCC ValidString + StartTime time.Time + Source EventSource + Status EventStatus + IsMonitored bool + DefaultIsFeatured bool + DefaultIsActive bool + DefaultWinningUpperLimit int32 + Score ValidString + MatchMinute ValidInt + TimerStatus ValidString + AddedTime ValidInt + MatchPeriod ValidInt + IsLive bool + FetchedAt time.Time +} +type BaseEventRes struct { + ID string `json:"id"` + SportID int32 `json:"sport_id"` + MatchName string `json:"match_name"` + HomeTeam string `json:"home_team"` + AwayTeam string `json:"away_team"` + HomeTeamID int64 `json:"home_team_id"` + AwayTeamID int64 `json:"away_team_id"` + HomeTeamImage string `json:"home_team_image"` + AwayTeamImage string `json:"away_team_image"` + LeagueID int64 `json:"league_id"` + LeagueName string `json:"league_name"` + LeagueCC string `json:"league_cc"` + StartTime time.Time `json:"start_time"` + Source EventSource `json:"source"` + Status EventStatus `json:"status"` + IsMonitored bool `json:"is_monitored"` + DefaultIsFeatured bool `json:"default_is_featured"` + DefaultIsActive bool `json:"default_is_active"` + DefaultWinningUpperLimit int32 `json:"default_winning_upper_limit"` + Score string `json:"score,omitempty"` + MatchMinute int `json:"match_minute,omitempty"` + TimerStatus string `json:"timer_status,omitempty"` + AddedTime int `json:"added_time,omitempty"` + MatchPeriod int `json:"match_period,omitempty"` + IsLive bool `json:"is_live"` + FetchedAt time.Time `json:"fetched_at"` +} +type EventWithSettings struct { + ID string + SportID int32 + MatchName string + HomeTeam string + AwayTeam string + HomeTeamID int64 + AwayTeamID int64 + HomeTeamImage string + AwayTeamImage string + LeagueID int64 + LeagueName string + LeagueCC ValidString + StartTime time.Time + Source EventSource + Status EventStatus + IsFeatured bool + IsMonitored bool + IsActive bool + WinningUpperLimit int32 + Score ValidString + MatchMinute ValidInt + TimerStatus ValidString + AddedTime ValidInt + MatchPeriod ValidInt + IsLive bool + UpdatedAt time.Time + FetchedAt time.Time } -type BetResult struct { - Success int `json:"success"` - Pager struct { - Page int `json:"page"` - PerPage int `json:"per_page"` - Total int `json:"total"` - } - Results []struct { - ID string `json:"id"` - SportID string `json:"sport_id"` - Time string `json:"time"` - League struct { - ID string `json:"id"` - Name string `json:"name"` - } `json:"league"` - Home struct { - ID string `json:"id"` - Name string `json:"name"` - } `json:"home"` - Away *struct { - ID string `json:"id"` - Name string `json:"name"` - } `json:"away"` - } `json:"results"` +type CreateEvent struct { + ID string + SportID int32 + MatchName string + HomeTeam string + AwayTeam string + HomeTeamID int64 + AwayTeamID int64 + HomeTeamImage string + AwayTeamImage string + LeagueID int64 + LeagueName string + StartTime time.Time + IsLive bool + Status EventStatus + Source EventSource } -type UpcomingEvent struct { - ID string `json:"id"` // Event ID - SportID int32 `json:"sport_id"` // Sport ID - MatchName string `json:"match_name"` // Match or event name - HomeTeam string `json:"home_team"` // Home team name (if available) - AwayTeam string `json:"away_team"` // Away team name (can be empty/null) - HomeTeamID int32 `json:"home_team_id"` // Home team ID - AwayTeamID int32 `json:"away_team_id"` // Away team ID (can be empty/null) - HomeKitImage string `json:"home_kit_image"` // Kit or image for home team (optional) - AwayKitImage string `json:"away_kit_image"` // Kit or image for away team (optional) - LeagueID int32 `json:"league_id"` // League ID - LeagueName string `json:"league_name"` // League name - LeagueCC string `json:"league_cc"` // League country code - StartTime time.Time `json:"start_time"` // Converted from "time" field in UNIX format - Source string `json:"source"` // bet api provider (bet365, betfair) - Status EventStatus `json:"status"` //Match Status for event - IsFeatured bool `json:"is_featured"` //Whether the event is featured or not - IsMonitored bool `json:"is_monitored"` //Whether the event is monitored or not - IsActive bool `json:"is_active"` //Whether the event is active or not +type EventWithSettingsRes struct { + ID string `json:"id"` + SportID int32 `json:"sport_id"` + MatchName string `json:"match_name"` + HomeTeam string `json:"home_team"` + AwayTeam string `json:"away_team"` + HomeTeamID int64 `json:"home_team_id"` + AwayTeamID int64 `json:"away_team_id"` + HomeTeamImage string `json:"home_team_image"` + AwayTeamImage string `json:"away_team_image"` + LeagueID int64 `json:"league_id"` + LeagueName string `json:"league_name"` + LeagueCC string `json:"league_cc"` + StartTime time.Time `json:"start_time"` + Source EventSource `json:"source"` + Status EventStatus `json:"status"` + IsFeatured bool `json:"is_featured"` + IsMonitored bool `json:"is_monitored"` + IsActive bool `json:"is_active"` + WinningUpperLimit int32 `json:"winning_upper_limit"` + Score string `json:"score,omitempty"` + MatchMinute int `json:"match_minute,omitempty"` + TimerStatus string `json:"timer_status,omitempty"` + AddedTime int `json:"added_time,omitempty"` + MatchPeriod int `json:"match_period,omitempty"` + IsLive bool `json:"is_live"` + UpdatedAt time.Time `json:"updated_at"` + FetchedAt time.Time `json:"fetched_at"` } -type MatchResult struct { - EventID string - FullScore string - HalfScore string - Status string - Scores map[string]map[string]string + +type EventSettings struct { + CompanyID int64 + EventID string + IsActive ValidBool + IsFeatured ValidBool + WinningUpperLimit ValidInt + UpdatedAt time.Time +} + +type CreateEventSettings struct { + CompanyID int64 + EventID string + IsActive ValidBool + IsFeatured ValidBool + WinningUpperLimit ValidInt } type EventFilter struct { Query ValidString SportID ValidInt32 - LeagueID ValidInt32 + LeagueID ValidInt64 CountryCode ValidString FirstStartTime ValidTime LastStartTime ValidTime - Limit ValidInt64 - Offset ValidInt64 + Limit ValidInt32 + Offset ValidInt32 MatchStatus ValidString // e.g., "upcoming", "in_play", "ended" Featured ValidBool } + +func ConvertDBEvent(event dbgen.EventWithCountry) BaseEvent { + return BaseEvent{ + ID: event.ID, + SportID: event.SportID, + MatchName: event.MatchName, + HomeTeam: event.HomeTeam, + AwayTeam: event.AwayTeam, + HomeTeamID: event.HomeTeamID, + AwayTeamID: event.AwayTeamID, + HomeTeamImage: event.HomeKitImage, + AwayTeamImage: event.AwayKitImage, + LeagueID: event.LeagueID, + LeagueName: event.LeagueName, + LeagueCC: ValidString{ + Value: event.LeagueCc.String, + Valid: event.LeagueCc.Valid, + }, + StartTime: event.StartTime.Time.UTC(), + Source: EventSource(event.Source), + Status: EventStatus(event.Status), + DefaultIsFeatured: event.DefaultIsFeatured, + IsMonitored: event.IsMonitored, + DefaultIsActive: event.DefaultIsActive, + DefaultWinningUpperLimit: event.DefaultWinningUpperLimit, + Score: ValidString{ + Value: event.Score.String, + Valid: event.Score.Valid, + }, + MatchMinute: ValidInt{ + Value: int(event.MatchMinute.Int32), + Valid: event.MatchMinute.Valid, + }, + TimerStatus: ValidString{ + Value: event.TimerStatus.String, + Valid: event.TimerStatus.Valid, + }, + AddedTime: ValidInt{ + Value: int(event.AddedTime.Int32), + Valid: event.AddedTime.Valid, + }, + MatchPeriod: ValidInt{ + Value: int(event.MatchPeriod.Int32), + Valid: event.MatchPeriod.Valid, + }, + IsLive: event.IsLive, + FetchedAt: event.FetchedAt.Time, + } +} + +func ConvertDBEvents(events []dbgen.EventWithCountry) []BaseEvent { + result := make([]BaseEvent, len(events)) + for i, e := range events { + result[i] = ConvertDBEvent(e) + } + return result +} + +func ConvertCreateEvent(e CreateEvent) dbgen.InsertEventParams { + return dbgen.InsertEventParams{ + ID: e.ID, + SportID: e.SportID, + MatchName: e.MatchName, + HomeTeam: e.HomeTeam, + AwayTeam: e.AwayTeam, + HomeTeamID: e.HomeTeamID, + AwayTeamID: e.AwayTeamID, + HomeKitImage: e.HomeTeamImage, + AwayKitImage: e.AwayTeamImage, + LeagueID: e.LeagueID, + LeagueName: e.LeagueName, + StartTime: pgtype.Timestamp{Time: e.StartTime, Valid: true}, + IsLive: e.IsLive, + Status: string(e.Status), + Source: string(e.Source), + } +} + +func ConvertCreateEventSettings(eventSettings CreateEventSettings) dbgen.InsertEventSettingsParams { + return dbgen.InsertEventSettingsParams{ + CompanyID: eventSettings.CompanyID, + EventID: eventSettings.EventID, + IsActive: eventSettings.IsActive.ToPG(), + IsFeatured: eventSettings.IsFeatured.ToPG(), + WinningUpperLimit: eventSettings.WinningUpperLimit.ToPG(), + } +} + +func ConvertDBEventWithSetting(event dbgen.EventWithSetting) EventWithSettings { + return EventWithSettings{ + ID: event.ID, + SportID: event.SportID, + MatchName: event.MatchName, + HomeTeam: event.HomeTeam, + AwayTeam: event.AwayTeam, + HomeTeamID: event.HomeTeamID, + AwayTeamID: event.AwayTeamID, + HomeTeamImage: event.HomeKitImage, + AwayTeamImage: event.AwayKitImage, + LeagueID: event.LeagueID, + LeagueName: event.LeagueName, + LeagueCC: ValidString{ + Value: event.LeagueCc.String, + Valid: event.LeagueCc.Valid, + }, + StartTime: event.StartTime.Time.UTC(), + Source: EventSource(event.Source), + Status: EventStatus(event.Status), + IsFeatured: event.IsFeatured, + IsMonitored: event.IsMonitored, + IsActive: event.IsActive, + WinningUpperLimit: event.WinningUpperLimit, + Score: ValidString{ + Value: event.Score.String, + Valid: event.Score.Valid, + }, + MatchMinute: ValidInt{ + Value: int(event.MatchMinute.Int32), + Valid: event.MatchMinute.Valid, + }, + TimerStatus: ValidString{ + Value: event.TimerStatus.String, + Valid: event.TimerStatus.Valid, + }, + AddedTime: ValidInt{ + Value: int(event.AddedTime.Int32), + Valid: event.AddedTime.Valid, + }, + MatchPeriod: ValidInt{ + Value: int(event.MatchPeriod.Int32), + Valid: event.MatchPeriod.Valid, + }, + IsLive: event.IsLive, + UpdatedAt: event.UpdatedAt.Time, + FetchedAt: event.FetchedAt.Time, + } +} + +func ConvertDBEventWithSettings(events []dbgen.EventWithSetting) []EventWithSettings { + result := make([]EventWithSettings, len(events)) + for i, e := range events { + result[i] = ConvertDBEventWithSetting(e) + } + return result +} + +func ConvertUpdateEventSettings(event CreateEventSettings) dbgen.UpdateEventSettingsParams { + return dbgen.UpdateEventSettingsParams{ + EventID: event.EventID, + CompanyID: event.CompanyID, + IsActive: event.IsActive.ToPG(), + IsFeatured: event.IsFeatured.ToPG(), + WinningUpperLimit: event.WinningUpperLimit.ToPG(), + } +} + +func ConvertEventRes(event BaseEvent) BaseEventRes { + return BaseEventRes{ + ID: event.ID, + SportID: event.SportID, + MatchName: event.MatchName, + HomeTeam: event.HomeTeam, + AwayTeam: event.AwayTeam, + HomeTeamID: event.HomeTeamID, + AwayTeamID: event.AwayTeamID, + HomeTeamImage: event.HomeTeamImage, + AwayTeamImage: event.AwayTeamImage, + LeagueID: event.LeagueID, + LeagueName: event.LeagueName, + LeagueCC: event.LeagueCC.Value, + StartTime: event.StartTime.UTC(), + Source: EventSource(event.Source), + Status: EventStatus(event.Status), + DefaultIsFeatured: event.DefaultIsFeatured, + IsMonitored: event.IsMonitored, + DefaultIsActive: event.DefaultIsActive, + DefaultWinningUpperLimit: event.DefaultWinningUpperLimit, + Score: event.Score.Value, + MatchMinute: event.MatchMinute.Value, + TimerStatus: event.TimerStatus.Value, + AddedTime: event.AddedTime.Value, + MatchPeriod: event.MatchPeriod.Value, + IsLive: event.IsLive, + FetchedAt: event.FetchedAt.UTC(), + } +} +func ConvertEventResList(events []BaseEvent) []BaseEventRes { + result := make([]BaseEventRes, 0, len(events)) + for _, event := range events { + result = append(result, ConvertEventRes(event)) + } + return result +} + +func ConvertEventWitSettingRes(event EventWithSettings) EventWithSettingsRes { + return EventWithSettingsRes{ + ID: event.ID, + SportID: event.SportID, + MatchName: event.MatchName, + HomeTeam: event.HomeTeam, + AwayTeam: event.AwayTeam, + HomeTeamID: event.HomeTeamID, + AwayTeamID: event.AwayTeamID, + HomeTeamImage: event.HomeTeamImage, + AwayTeamImage: event.AwayTeamImage, + LeagueID: event.LeagueID, + LeagueName: event.LeagueName, + LeagueCC: event.LeagueCC.Value, + StartTime: event.StartTime.UTC(), + Source: EventSource(event.Source), + Status: EventStatus(event.Status), + IsFeatured: event.IsFeatured, + IsMonitored: event.IsMonitored, + IsActive: event.IsActive, + WinningUpperLimit: event.WinningUpperLimit, + Score: event.Score.Value, + MatchMinute: event.MatchMinute.Value, + TimerStatus: event.TimerStatus.Value, + AddedTime: event.AddedTime.Value, + MatchPeriod: event.MatchPeriod.Value, + IsLive: event.IsLive, + FetchedAt: event.FetchedAt.UTC(), + } +} + +func ConvertEventWithSettingResList(events []EventWithSettings) []EventWithSettingsRes { + result := make([]EventWithSettingsRes, 0, len(events)) + for _, event := range events { + result = append(result, ConvertEventWitSettingRes(event)) + } + return result +} diff --git a/internal/domain/eventres.go b/internal/domain/eventres.go new file mode 100644 index 0000000..89365a9 --- /dev/null +++ b/internal/domain/eventres.go @@ -0,0 +1,35 @@ +package domain + +type MatchResult struct { + EventID string + FullScore string + HalfScore string + Status string + Scores map[string]map[string]string +} + +type B365UpcomingRes struct { + Success int `json:"success"` + Pager struct { + Page int `json:"page"` + PerPage int `json:"per_page"` + Total int `json:"total"` + } + Results []struct { + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + League struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"league"` + Home struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"home"` + Away *struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"away"` + } `json:"results"` +} \ No newline at end of file diff --git a/internal/domain/featured_leagues.go b/internal/domain/featured_leagues.go new file mode 100644 index 0000000..8afdce1 --- /dev/null +++ b/internal/domain/featured_leagues.go @@ -0,0 +1,63 @@ +package domain + +// These leagues are automatically featured when the league is created +var FeaturedLeagues = []int64{ + // Football + 10044469, // Ethiopian Premier League + 10041282, //Premier League + 10083364, //La Liga + 10041095, //German Bundesliga + 10041100, //Ligue 1 + 10041809, //UEFA Champions League + 10041957, //UEFA Europa League + 10079560, //UEFA Conference League + 10050282, //UEFA Nations League + 10044685, //FIFA Club World Cup + 10050346, //UEFA Super Cup + 10081269, //CONCACAF Champions Cup + 10070189, //CONCACAF Gold Cup + 10076185, //UEFA Regions Cup + + 10067913, //Europe - World Cup Qualifying + 10040162, //Asia - World Cup Qualifying + 10067624, //South America - World Cup Qualifying + 10073057, //North & Central America - World Cup Qualifying + + 10037075, //International Match + 10077480, //Women’s International + 10037109, //Europe Friendlies + 10068837, //Euro U21 + + 10041315, //Italian Serie A + 10036538, //Spain Segunda + 10047168, // US MLS + + 10043156, //England FA Cup + 10042103, //France Cup + 10041088, //Premier League 2 + 10084250, //Turkiye Super League + 10041187, //Kenya Super League + 10041391, //Netherlands Eredivisie + + // Basketball + 10041830, //NBA + 10049984, //WNBA + 10037165, //German Bundesliga + 10036608, //Italian Lega 1 + 10040795, //EuroLeague + 10041534, //Basketball Africa League + + // Ice Hockey + 10037477, //NHL + 10037447, //AHL + 10069385, //IIHF World Championship + + // AMERICAN FOOTBALL + 10037219, //NFL + + // BASEBALL + 10037485, // MLB + + // VOLLEYBALL + 10069666, //FIVB Nations League +} diff --git a/internal/domain/league.go b/internal/domain/league.go index c4a2d12..a44ae70 100644 --- a/internal/domain/league.go +++ b/internal/domain/league.go @@ -1,6 +1,24 @@ package domain -type League struct { +import ( + "time" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" +) + +// The ID and the Bet365 IDs we have here are both gotten from the betapi +type LeagueWithSettings struct { + ID int64 + Name string + CompanyID int64 + CountryCode ValidString + Bet365ID ValidInt32 + SportID int32 + IsActive bool + IsFeatured bool + UpdatedAt time.Time +} +type LeagueWithSettingsRes struct { ID int64 `json:"id" example:"1"` Name string `json:"name" example:"BPL"` CountryCode string `json:"cc" example:"uk"` @@ -9,6 +27,49 @@ type League struct { SportID int32 `json:"sport_id" example:"1"` IsFeatured bool `json:"is_featured" example:"false"` } +type BaseLeague struct { + ID int64 + Name string + CountryCode ValidString + Bet365ID ValidInt32 + SportID int32 + DefaultIsActive bool + DefaultIsFeatured bool +} + +type BaseLeagueRes struct { + ID int64 `json:"id" example:"1"` + Name string `json:"name" example:"BPL"` + CountryCode string `json:"cc" example:"uk"` + Bet365ID int32 `json:"bet365_id" example:"1121"` + SportID int32 `json:"sport_id" example:"1"` + DefaultIsActive bool `json:"default_is_active" example:"false"` + DefaultIsFeatured bool `json:"default_is_featured" example:"false"` +} + +type CreateLeague struct { + ID int64 + Name string + CountryCode ValidString + Bet365ID ValidInt32 + SportID int32 + DefaultIsActive bool + DefaultIsFeatured bool +} + +type LeagueSettings struct { + CompanyID int64 + LeagueID int64 + IsActive ValidBool + IsFeatured ValidBool + UpdatedAt time.Time +} +type CreateLeagueSettings struct { + CompanyID int64 + LeagueID int64 + IsActive ValidBool + IsFeatured ValidBool +} type UpdateLeague struct { ID int64 `json:"id" example:"1"` @@ -29,64 +90,87 @@ type LeagueFilter struct { Offset ValidInt64 } -// These leagues are automatically featured when the league is created -var FeaturedLeagues = []int64{ - // Football - 10044469, // Ethiopian Premier League - 10041282, //Premier League - 10083364, //La Liga - 10041095, //German Bundesliga - 10041100, //Ligue 1 - 10041809, //UEFA Champions League - 10041957, //UEFA Europa League - 10079560, //UEFA Conference League - 10050282, //UEFA Nations League - 10044685, //FIFA Club World Cup - 10050346, //UEFA Super Cup - 10081269, //CONCACAF Champions Cup - 10070189, //CONCACAF Gold Cup - 10076185, //UEFA Regions Cup - - 10067913, //Europe - World Cup Qualifying - 10040162, //Asia - World Cup Qualifying - 10067624, //South America - World Cup Qualifying - 10073057, //North & Central America - World Cup Qualifying - - 10037075, //International Match - 10077480, //Women’s International - 10037109, //Europe Friendlies - 10068837, //Euro U21 - - 10041315, //Italian Serie A - 10036538, //Spain Segunda - 10047168, // US MLS - - 10043156, //England FA Cup - 10042103, //France Cup - 10041088, //Premier League 2 - 10084250, //Turkiye Super League - 10041187, //Kenya Super League - 10041391, //Netherlands Eredivisie - - // Basketball - 10041830, //NBA - 10049984, //WNBA - 10037165, //German Bundesliga - 10036608, //Italian Lega 1 - 10040795, //EuroLeague - 10041534, //Basketball Africa League - - // Ice Hockey - 10037477, //NHL - 10037447, //AHL - 10069385, //IIHF World Championship - - // AMERICAN FOOTBALL - 10037219, //NFL - - // BASEBALL - 10037485, // MLB - - // VOLLEYBALL - 10069666, //FIVB Nations League +func ConvertCreateLeague(league CreateLeague) dbgen.InsertLeagueParams { + return dbgen.InsertLeagueParams{ + ID: league.ID, + Name: league.Name, + CountryCode: league.CountryCode.ToPG(), + Bet365ID: league.Bet365ID.ToPG(), + SportID: league.SportID, + DefaultIsActive: league.DefaultIsActive, + DefaultIsFeatured: league.DefaultIsFeatured, + } +} + +func ConvertCreateLeagueSettings(leagueSetting CreateLeagueSettings) dbgen.InsertLeagueSettingsParams { + return dbgen.InsertLeagueSettingsParams{ + CompanyID: leagueSetting.CompanyID, + LeagueID: leagueSetting.LeagueID, + IsActive: leagueSetting.IsActive.ToPG(), + IsFeatured: leagueSetting.IsFeatured.ToPG(), + } +} + +func ConvertDBBaseLeague(league dbgen.League) BaseLeague { + return BaseLeague{ + ID: league.ID, + Name: league.Name, + CountryCode: ValidString{ + Value: league.CountryCode.String, + Valid: league.CountryCode.Valid, + }, + Bet365ID: ValidInt32{ + Value: league.Bet365ID.Int32, + Valid: league.Bet365ID.Valid, + }, + SportID: league.SportID, + DefaultIsActive: league.DefaultIsActive, + DefaultIsFeatured: league.DefaultIsFeatured, + } +} + +func ConvertDBBaseLeagues(leagues []dbgen.League) []BaseLeague { + result := make([]BaseLeague, len(leagues)) + for i, league := range leagues { + result[i] = ConvertDBBaseLeague(league) + } + return result +} + +func ConvertDBLeagueWithSetting(lws dbgen.LeagueWithSetting) LeagueWithSettings { + return LeagueWithSettings{ + ID: lws.ID, + Name: lws.Name, + CompanyID: lws.CompanyID, + CountryCode: ValidString{ + Value: lws.CountryCode.String, + Valid: lws.CountryCode.Valid, + }, + Bet365ID: ValidInt32{ + Value: lws.Bet365ID.Int32, + Valid: lws.Bet365ID.Valid, + }, + IsActive: lws.IsActive, + SportID: lws.SportID, + IsFeatured: lws.IsFeatured, + UpdatedAt: lws.UpdatedAt.Time, + } +} + +func ConvertDBLeagueWithSettings(lws []dbgen.LeagueWithSetting) []LeagueWithSettings { + result := make([]LeagueWithSettings, len(lws)) + for i, league := range lws { + result[i] = ConvertDBLeagueWithSetting(league) + } + return result +} + +func ConvertUpdateLeague(updateLeague UpdateLeague) dbgen.UpdateLeagueParams { + return dbgen.UpdateLeagueParams{ + ID: updateLeague.ID, + Name: updateLeague.Name.ToPG(), + CountryCode: updateLeague.CountryCode.ToPG(), + Bet365ID: updateLeague.Bet365ID.ToPG(), + SportID: updateLeague.SportID.ToPG(), + } } diff --git a/internal/domain/log_fields.go b/internal/domain/log_fields.go deleted file mode 100644 index 7ce6b1e..0000000 --- a/internal/domain/log_fields.go +++ /dev/null @@ -1,17 +0,0 @@ -package domain - -import ( - "time" - - "github.com/gofiber/fiber/v2" - "go.uber.org/zap" -) - -var BadRequestZapFields = []zap.Field{ - zap.Int("status_code", fiber.StatusBadRequest), - zap.Time("timestamp", time.Now()), -} -var InternalServerErrorZapFields = []zap.Field{ - zap.Int("status_code", fiber.StatusBadRequest), - zap.Time("timestamp", time.Now()), -} diff --git a/internal/domain/odds.go b/internal/domain/odds.go index 79a015c..fa30718 100644 --- a/internal/domain/odds.go +++ b/internal/domain/odds.go @@ -3,47 +3,184 @@ package domain import ( "encoding/json" "time" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/jackc/pgx/v5/pgtype" ) -type Market struct { +type CreateOddMarket struct { EventID string - FI string MarketCategory string MarketType string MarketName string MarketID string UpdatedAt time.Time Odds []map[string]interface{} - Name string - Handicap string - OddsVal float64 - Source string } -type Odd struct { +type OddMarket struct { + ID int64 `json:"id"` + EventID string `json:"event_id"` + MarketType string `json:"market_type"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID string `json:"market_id"` + RawOdds []json.RawMessage `json:"raw_odds"` + FetchedAt time.Time `json:"fetched_at"` + ExpiresAt time.Time `json:"expires_at"` + DefaultIsActive bool `json:"is_active"` + IsMonitored bool `json:"is_monitored"` + IsLive bool `json:"is_live"` + Status EventStatus `json:"status"` + Source EventSource `json:"source"` +} +type OddMarketWithSettings struct { ID int64 `json:"id"` EventID string `json:"event_id"` - Fi string `json:"fi"` MarketType string `json:"market_type"` MarketName string `json:"market_name"` MarketCategory string `json:"market_category"` MarketID string `json:"market_id"` - Name string `json:"name"` - Handicap string `json:"handicap"` - OddsValue float64 `json:"odds_value"` - Section string `json:"section"` - Category string `json:"category"` RawOdds []json.RawMessage `json:"raw_odds"` FetchedAt time.Time `json:"fetched_at"` ExpiresAt time.Time `json:"expires_at"` - Source string `json:"source"` IsActive bool `json:"is_active"` } -type RawOddsByMarketID struct { - ID int64 `json:"id"` - MarketName string `json:"market_name"` - Handicap string `json:"handicap"` - RawOdds []json.RawMessage `json:"raw_odds"` - FetchedAt time.Time `json:"fetched_at"` - ExpiresAt time.Time `json:"expires_at"` + +type OddMarketSettings struct { + CompanyID int64 + OddMarketID int64 + IsActive ValidBool + CustomRawOdds string + UpdatedAt time.Time +} +type CreateOddMarketSettings struct { + CompanyID int64 + OddMarketID int64 + IsActive ValidBool + CustomRawOdds []map[string]interface{} +} + +// type RawOddsByMarketID struct { +// ID int64 `json:"id"` +// MarketName string `json:"market_name"` +// Handicap string `json:"handicap"` +// RawOdds []json.RawMessage `json:"raw_odds"` +// FetchedAt time.Time `json:"fetched_at"` +// ExpiresAt time.Time `json:"expires_at"` +// } + +type OddMarketFilter struct { + Limit ValidInt32 + Offset ValidInt32 +} +type OddMarketWithEventFilter struct { + Limit ValidInt32 + Offset ValidInt32 +} + +func ConvertDBOddMarket(oddMarket dbgen.OddsMarketWithEvent) (OddMarket, error) { + var rawOdds []json.RawMessage + if len(oddMarket.RawOdds) > 0 { + if err := json.Unmarshal(oddMarket.RawOdds, &rawOdds); err != nil { + return OddMarket{}, err + } + } else { + rawOdds = []json.RawMessage{} // explicit empty slice + } + return OddMarket{ + ID: oddMarket.ID, + EventID: oddMarket.EventID, + MarketType: oddMarket.MarketType, + MarketName: oddMarket.MarketName, + MarketCategory: oddMarket.MarketCategory, + MarketID: oddMarket.MarketID, + RawOdds: rawOdds, + FetchedAt: oddMarket.FetchedAt.Time, + ExpiresAt: oddMarket.ExpiresAt.Time, + DefaultIsActive: oddMarket.DefaultIsActive, + IsMonitored: oddMarket.IsMonitored, + IsLive: oddMarket.IsLive, + Status: EventStatus(oddMarket.Status), + Source: EventSource(oddMarket.Source), + }, nil +} + +func ConvertDBOddMarkets(oddMarkets []dbgen.OddsMarketWithEvent) ([]OddMarket, error) { + result := make([]OddMarket, len(oddMarkets)) + for i, om := range oddMarkets { + convertedMarket, err := ConvertDBOddMarket(om) + if err != nil { + return nil, err + } + result[i] = convertedMarket + } + return result, nil +} + +func ConvertCreateOddMarket(oddMarket CreateOddMarket) (dbgen.InsertOddsMarketParams, error) { + rawOddsBytes, err := json.Marshal(oddMarket.Odds) + if err != nil { + return dbgen.InsertOddsMarketParams{}, err + } + + return dbgen.InsertOddsMarketParams{ + EventID: oddMarket.EventID, + MarketType: oddMarket.MarketType, + MarketName: oddMarket.MarketName, + MarketCategory: oddMarket.MarketCategory, + MarketID: oddMarket.MarketID, + RawOdds: rawOddsBytes, + FetchedAt: pgtype.Timestamp{Time: time.Now(), Valid: true}, + ExpiresAt: pgtype.Timestamp{Time: (time.Now()).Add(time.Hour), Valid: true}, + }, nil +} + +func ConvertCreateOddMarketSetting(oms CreateOddMarketSettings) (dbgen.InsertOddSettingsParams, error) { + rawOddsBytes, err := json.Marshal(oms.CustomRawOdds) + if err != nil { + return dbgen.InsertOddSettingsParams{}, err + } + return dbgen.InsertOddSettingsParams{ + CompanyID: oms.CompanyID, + OddsMarketID: oms.OddMarketID, + IsActive: oms.IsActive.ToPG(), + CustomRawOdds: rawOddsBytes, + }, nil +} + +func ConvertDBOddMarketWithSetting(oms dbgen.OddsMarketWithSetting) (OddMarketWithSettings, error) { + var rawOdds []json.RawMessage + if len(oms.RawOdds) > 0 { + if err := json.Unmarshal(oms.RawOdds, &rawOdds); err != nil { + return OddMarketWithSettings{}, err + } + } else { + rawOdds = []json.RawMessage{} // explicit empty slice + } + return OddMarketWithSettings{ + ID: oms.ID, + EventID: oms.EventID, + MarketType: oms.MarketType, + MarketName: oms.MarketName, + MarketCategory: oms.MarketCategory, + MarketID: oms.MarketID, + RawOdds: rawOdds, + FetchedAt: oms.FetchedAt.Time, + ExpiresAt: oms.ExpiresAt.Time, + IsActive: oms.IsActive, + }, nil +} + +func ConvertDBOddMarketWithSettings(oms []dbgen.OddsMarketWithSetting) ([]OddMarketWithSettings, error) { + result := make([]OddMarketWithSettings, len(oms)) + for i, o := range oms { + converted, err := ConvertDBOddMarketWithSetting(o) + if err != nil { + return nil, err + } + result[i] = converted + } + + return result, nil } diff --git a/internal/domain/odds_history.go b/internal/domain/odds_history.go index 10da63a..7413286 100644 --- a/internal/domain/odds_history.go +++ b/internal/domain/odds_history.go @@ -35,18 +35,18 @@ type OddHistoryFilter struct { func ConvertCreateOddHistory(odd CreateOddHistory) dbgen.InsertOddHistoryParams { return dbgen.InsertOddHistoryParams{ - OddID: odd.OddID, - MarketID: odd.MarketID, - RawOddID: odd.RawOddID, - EventID: odd.EventID, - OddValue: odd.OddValue, + OddsMarketID: odd.OddID, + MarketID: odd.MarketID, + RawOddID: odd.RawOddID, + EventID: odd.EventID, + OddValue: odd.OddValue, } } func ConvertDBOddHistory(dbOddHistory dbgen.OddHistory) OddHistory { return OddHistory{ ID: dbOddHistory.ID, - OddID: dbOddHistory.OddID, + OddID: dbOddHistory.OddsMarketID, MarketID: dbOddHistory.MarketID, RawOddID: dbOddHistory.RawOddID, EventID: dbOddHistory.EventID, diff --git a/internal/domain/resultres.go b/internal/domain/resultres.go index 381c86d..064ab61 100644 --- a/internal/domain/resultres.go +++ b/internal/domain/resultres.go @@ -9,47 +9,47 @@ type BaseResultResponse struct { Results []json.RawMessage `json:"results"` } -type LeagueRes struct { +type LeagueResultResponse struct { ID string `json:"id"` Name string `json:"name"` CC string `json:"cc"` } -type Team struct { +type TeamResultResponse struct { ID string `json:"id"` Name string `json:"name"` ImageID ValidInt64 `json:"image_id"` CC string `json:"cc"` } -type Score struct { +type ScoreResultResponse struct { Home string `json:"home"` Away string `json:"away"` } type CommonResultResponse struct { - ID string `json:"id"` - SportID string `json:"sport_id"` - Time string `json:"time"` - TimeStatus ValidInt64 `json:"time_status"` - League LeagueRes `json:"league"` - Home Team `json:"home"` - Away Team `json:"away"` - SS string `json:"ss"` + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + TimeStatus ValidInt64 `json:"time_status"` + League LeagueResultResponse `json:"league"` + Home TeamResultResponse `json:"home"` + Away TeamResultResponse `json:"away"` + SS string `json:"ss"` } type FootballResultResponse struct { - ID string `json:"id"` - SportID string `json:"sport_id"` - Time string `json:"time"` - TimeStatus string `json:"time_status"` - League LeagueRes `json:"league"` - Home Team `json:"home"` - Away Team `json:"away"` - SS string `json:"ss"` + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + TimeStatus string `json:"time_status"` + League LeagueResultResponse `json:"league"` + Home TeamResultResponse `json:"home"` + Away TeamResultResponse `json:"away"` + SS string `json:"ss"` Scores struct { - FirstHalf Score `json:"1"` - SecondHalf Score `json:"2"` + FirstHalf ScoreResultResponse `json:"1"` + SecondHalf ScoreResultResponse `json:"2"` } `json:"scores"` Stats struct { Attacks []string `json:"attacks"` @@ -78,21 +78,21 @@ type FootballResultResponse struct { } type BasketballResultResponse struct { - ID string `json:"id"` - SportID string `json:"sport_id"` - Time string `json:"time"` - TimeStatus string `json:"time_status"` - League LeagueRes `json:"league"` - Home Team `json:"home"` - Away Team `json:"away"` - SS string `json:"ss"` + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + TimeStatus string `json:"time_status"` + League LeagueResultResponse `json:"league"` + Home TeamResultResponse `json:"home"` + Away TeamResultResponse `json:"away"` + SS string `json:"ss"` Scores struct { - FirstQuarter Score `json:"1"` - SecondQuarter Score `json:"2"` - FirstHalf Score `json:"3"` - ThirdQuarter Score `json:"4"` - FourthQuarter Score `json:"5"` - TotalScore Score `json:"7"` + FirstQuarter ScoreResultResponse `json:"1"` + SecondQuarter ScoreResultResponse `json:"2"` + FirstHalf ScoreResultResponse `json:"3"` + ThirdQuarter ScoreResultResponse `json:"4"` + FourthQuarter ScoreResultResponse `json:"5"` + TotalScore ScoreResultResponse `json:"7"` } `json:"scores"` Stats struct { TwoPoints []string `json:"2points"` @@ -125,19 +125,19 @@ type BasketballResultResponse struct { Bet365ID string `json:"bet365_id"` } type IceHockeyResultResponse struct { - ID string `json:"id"` - SportID string `json:"sport_id"` - Time string `json:"time"` - TimeStatus string `json:"time_status"` - League LeagueRes `json:"league"` - Home Team `json:"home"` - Away Team `json:"away"` - SS string `json:"ss"` + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + TimeStatus string `json:"time_status"` + League LeagueResultResponse `json:"league"` + Home TeamResultResponse `json:"home"` + Away TeamResultResponse `json:"away"` + SS string `json:"ss"` Scores struct { - FirstPeriod Score `json:"1"` - SecondPeriod Score `json:"2"` - ThirdPeriod Score `json:"3"` - TotalScore Score `json:"5"` + FirstPeriod ScoreResultResponse `json:"1"` + SecondPeriod ScoreResultResponse `json:"2"` + ThirdPeriod ScoreResultResponse `json:"3"` + TotalScore ScoreResultResponse `json:"5"` } `json:"scores"` Stats struct { @@ -222,11 +222,11 @@ type VolleyballResultResponse struct { } `json:"away"` SS string `json:"ss"` Scores struct { - FirstSet Score `json:"1"` - SecondSet Score `json:"2"` - ThirdSet Score `json:"3"` - FourthSet Score `json:"4"` - FivethSet Score `json:"5"` + FirstSet ScoreResultResponse `json:"1"` + SecondSet ScoreResultResponse `json:"2"` + ThirdSet ScoreResultResponse `json:"3"` + FourthSet ScoreResultResponse `json:"4"` + FivethSet ScoreResultResponse `json:"5"` } `json:"scores"` Stats struct { PointsWonOnServe []string `json:"points_won_on_serve"` @@ -290,10 +290,10 @@ type FutsalResultResponse struct { } `json:"away"` SS string `json:"ss"` Scores struct { - FirstPeriod Score `json:"1"` - SecondPeriod Score `json:"2"` - ThirdPeriod Score `json:"3"` - TotalScore Score `json:"4"` + FirstPeriod ScoreResultResponse `json:"1"` + SecondPeriod ScoreResultResponse `json:"2"` + ThirdPeriod ScoreResultResponse `json:"3"` + TotalScore ScoreResultResponse `json:"4"` } `json:"scores"` Events []map[string]string `json:"events"` InplayCreatedAt string `json:"inplay_created_at"` @@ -327,12 +327,12 @@ type NFLResultResponse struct { } `json:"away"` SS string `json:"ss"` Scores struct { - FirstQuarter Score `json:"1"` - SecondQuarter Score `json:"2"` - ThirdQuarter Score `json:"3"` - FourthQuarter Score `json:"4"` - Overtime Score `json:"5"` - TotalScore Score `json:"7"` + FirstQuarter ScoreResultResponse `json:"1"` + SecondQuarter ScoreResultResponse `json:"2"` + ThirdQuarter ScoreResultResponse `json:"3"` + FourthQuarter ScoreResultResponse `json:"4"` + Overtime ScoreResultResponse `json:"5"` + TotalScore ScoreResultResponse `json:"7"` } `json:"scores"` Stats struct { FirstDowns []string `json:"first_downs"` @@ -381,9 +381,9 @@ type RugbyResultResponse struct { } `json:"away"` SS string `json:"ss"` Scores struct { - FirstHalf Score `json:"1"` - SecondHalf Score `json:"2"` - TotalScore Score `json:"7"` + FirstHalf ScoreResultResponse `json:"1"` + SecondHalf ScoreResultResponse `json:"2"` + TotalScore ScoreResultResponse `json:"7"` } `json:"scores"` Stats struct { Tries []string `json:"tries"` @@ -433,17 +433,17 @@ type BaseballResultResponse struct { } `json:"away"` SS string `json:"ss"` Scores struct { - FirstInning Score `json:"1"` - SecondInning Score `json:"2"` - ThirdInning Score `json:"3"` - FourthInning Score `json:"4"` - FifthInning Score `json:"5"` - SixthInning Score `json:"6"` - SeventhInning Score `json:"7"` - EighthInning Score `json:"8"` - NinthInning Score `json:"9"` - ExtraInnings Score `json:"10"` - TotalScore Score `json:"11"` + FirstInning ScoreResultResponse `json:"1"` + SecondInning ScoreResultResponse `json:"2"` + ThirdInning ScoreResultResponse `json:"3"` + FourthInning ScoreResultResponse `json:"4"` + FifthInning ScoreResultResponse `json:"5"` + SixthInning ScoreResultResponse `json:"6"` + SeventhInning ScoreResultResponse `json:"7"` + EighthInning ScoreResultResponse `json:"8"` + NinthInning ScoreResultResponse `json:"9"` + ExtraInnings ScoreResultResponse `json:"10"` + TotalScore ScoreResultResponse `json:"11"` } `json:"scores"` Stats struct { Hits []string `json:"hits"` diff --git a/internal/domain/user.go b/internal/domain/user.go index eb49ec6..ba0099d 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -10,24 +10,20 @@ var ( ) type User struct { - ID int64 - FirstName string - LastName string - Email string `json:"email"` - PhoneNumber string `json:"phone_number"` - Password []byte - Role Role - // + ID int64 + FirstName string + LastName string + Email string `json:"email"` + PhoneNumber string `json:"phone_number"` + Password []byte + Role Role EmailVerified bool PhoneVerified bool - // - CreatedAt time.Time - UpdatedAt time.Time - // - SuspendedAt time.Time - Suspended bool - // - CompanyID ValidInt64 + CreatedAt time.Time + UpdatedAt time.Time + SuspendedAt time.Time + Suspended bool + CompanyID ValidInt64 //This should be null } type UserFilter struct { @@ -54,6 +50,7 @@ type RegisterUserReq struct { Otp string ReferralCode string `json:"referral_code"` OtpMedium OtpMedium + CompanyID ValidInt64 } type CreateUserReq struct { FirstName string diff --git a/internal/domain/validtypes.go b/internal/domain/validtypes.go index 709a401..d402517 100644 --- a/internal/domain/validtypes.go +++ b/internal/domain/validtypes.go @@ -115,6 +115,15 @@ func ConvertInt32Ptr(value *int32) ValidInt32 { Valid: true, } } +func ConvertIntPtr(value *int) ValidInt { + if value == nil { + return ValidInt{} + } + return ValidInt{ + Value: *value, + Valid: true, + } +} func ConvertStringPtr(value *string) ValidString { if value == nil { diff --git a/internal/pkgs/helpers/slug.go b/internal/pkgs/helpers/slug.go new file mode 100644 index 0000000..2824c37 --- /dev/null +++ b/internal/pkgs/helpers/slug.go @@ -0,0 +1,42 @@ +package helpers + +import ( + "regexp" + "strings" + "unicode" +) + +var ( + // keep letters, numbers, spaces, and hyphens + slugRegex = regexp.MustCompile(`[^a-z0-9\s-]`) + spaceRe = regexp.MustCompile(`\s+`) + dashRe = regexp.MustCompile(`-+`) +) + +// GenerateSlug creates a URL-safe slug from a given string (e.g. company name). +func GenerateSlug(name string) string { + // lowercase + slug := strings.ToLower(name) + + // normalize unicode accents (é -> e, ü -> u, etc.) + slug = strings.Map(func(r rune) rune { + if unicode.IsLetter(r) || unicode.IsDigit(r) || unicode.IsSpace(r) || r == '-' { + return r + } + return -1 + }, slug) + + // remove unwanted chars + slug = slugRegex.ReplaceAllString(slug, "") + + // replace spaces with dash + slug = spaceRe.ReplaceAllString(slug, "-") + + // collapse multiple dashes + slug = dashRe.ReplaceAllString(slug, "-") + + // trim leading/trailing dash + slug = strings.Trim(slug, "-") + + return slug +} diff --git a/internal/repository/auth.go b/internal/repository/auth.go index fcc5f09..367d482 100644 --- a/internal/repository/auth.go +++ b/internal/repository/auth.go @@ -10,8 +10,8 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/jackc/pgx/v5/pgtype" ) - -func (s *Store) GetUserByEmailPhone(ctx context.Context, email, phone string) (domain.User, error) { + +func (s *Store) GetUserByEmailPhone(ctx context.Context, email, phone string, companyID domain.ValidInt64) (domain.User, error) { user, err := s.queries.GetUserByEmailPhone(ctx, dbgen.GetUserByEmailPhoneParams{ Email: pgtype.Text{ String: email, @@ -21,6 +21,7 @@ func (s *Store) GetUserByEmailPhone(ctx context.Context, email, phone string) (d String: phone, Valid: true, }, + CompanyID: companyID.ToPG(), }) if err != nil { if errors.Is(err, sql.ErrNoRows) { diff --git a/internal/repository/bet.go b/internal/repository/bet.go index b9391f1..2aa520a 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -80,7 +80,7 @@ func convertDBFlag(flag dbgen.Flag) domain.Flag { return domain.Flag{ ID: flag.ID, BetID: flag.BetID.Int64, - OddID: flag.OddID.Int64, + OddID: flag.OddsMarketID.Int64, Reason: flag.Reason.String, FlaggedAt: flag.FlaggedAt.Time, Resolved: flag.Resolved.Bool, @@ -157,7 +157,7 @@ func (s *Store) CreateFlag(ctx context.Context, flag domain.CreateFlagReq) (doma Int64: flag.BetID, Valid: flag.BetID != 0, }, - OddID: pgtype.Int8{ + OddsMarketID: pgtype.Int8{ Int64: flag.OddID, Valid: flag.OddID != 0, }, diff --git a/internal/repository/company.go b/internal/repository/company.go index a877b23..dc440e9 100644 --- a/internal/repository/company.go +++ b/internal/repository/company.go @@ -2,14 +2,36 @@ package repository import ( "context" + "database/sql" + "errors" "fmt" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers" "github.com/jackc/pgx/v5/pgtype" ) - func (s *Store) CreateCompany(ctx context.Context, company domain.CreateCompany) (domain.Company, error) { - dbCompany, err := s.queries.CreateCompany(ctx, domain.ConvertCreateCompany(company)) + baseSlug := helpers.GenerateSlug(company.Name) + uniqueSlug := baseSlug + i := 1 + + for { + _, err := s.queries.GetCompanyIDUsingSlug(ctx, uniqueSlug) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + // slug is unique + break + } else { + // real DB error + return domain.Company{}, err + } + } + uniqueSlug = fmt.Sprintf("%s-%d", baseSlug, i) + i++ + } + + dbCompany, err := s.queries.CreateCompany(ctx, domain.ConvertCreateCompany(company, uniqueSlug)) if err != nil { return domain.Company{}, err } @@ -56,6 +78,15 @@ func (s *Store) GetCompanyByID(ctx context.Context, id int64) (domain.GetCompany return domain.ConvertDBCompanyDetails(dbCompany), nil } +func (s *Store) GetCompanyIDBySlug(ctx context.Context, slug string) (int64, error) { + dbCompanyID, err := s.queries.GetCompanyIDUsingSlug(ctx, slug) + + if err != nil { + return 0, err + } + return dbCompanyID, nil +} + func (s *Store) UpdateCompany(ctx context.Context, company domain.UpdateCompany) (domain.Company, error) { dbCompany, err := s.queries.UpdateCompany(ctx, domain.ConvertUpdateCompany(company)) diff --git a/internal/repository/custom_odds.go b/internal/repository/custom_odds.go index 6b2506f..3400878 100644 --- a/internal/repository/custom_odds.go +++ b/internal/repository/custom_odds.go @@ -1,5 +1,105 @@ package repository -import "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +// import ( +// "context" -func (s *Store) InsertCustomOdds(ctx context.Context, odd domain.ConvertCreateCustomOdd) (domain.) \ No newline at end of file +// dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" +// "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +// ) + +// func (s *Store) InsertCustomOdds(ctx context.Context, odd domain.CreateCustomOdd) (domain.CustomOdd, error) { +// convertedCustomOdd, err := domain.ConvertCreateCustomOdd(odd) +// if err != nil { +// return domain.CustomOdd{}, err +// } +// dbCustomOdd, err := s.queries.InsertCustomOdd(ctx, convertedCustomOdd) + +// if err != nil { +// return domain.CustomOdd{}, err +// } + +// convertDbCustomOdd, err := domain.ConvertDBCustomOdd(dbCustomOdd) + +// if err != nil { +// return domain.CustomOdd{}, err +// } + +// return convertDbCustomOdd, nil +// } + +// func (s *Store) GetAllCustomOdds(ctx context.Context, filter domain.CustomOddFilter) ([]domain.CustomOdd, error) { +// dbCustomOdds, err := s.queries.GetAllCustomOdds(ctx, filter.CompanyID.ToPG()) +// if err != nil { +// return nil, err +// } + +// convertDbCustomOdds, err := domain.ConvertDbCustomOdds(dbCustomOdds) + +// if err != nil { +// return nil, err +// } + +// return convertDbCustomOdds, nil +// } + +// func (s *Store) GetCustomOddByID(ctx context.Context, id int64) (domain.CustomOdd, error) { +// dbCustomOdd, err := s.queries.GetCustomOddByID(ctx, id) +// if err != nil { +// return domain.CustomOdd{}, nil +// } + +// convertedDBCustomOdd, err := domain.ConvertDBCustomOdd(dbCustomOdd) +// if err != nil { +// return domain.CustomOdd{}, nil +// } + +// return convertedDBCustomOdd, nil +// } + +// func (s *Store) GetCustomOddByOddID(ctx context.Context, oddId int64, companyID int64) (domain.CustomOdd, error) { +// dbCustomOdd, err := s.queries.GetCustomOddByOddID(ctx, dbgen.GetCustomOddByOddIDParams{ +// OddID: oddId, +// CompanyID: companyID, +// }) + +// if err != nil { +// return domain.CustomOdd{}, nil +// } + +// convertedDBCustomOdd, err := domain.ConvertDBCustomOdd(dbCustomOdd) +// if err != nil { +// return domain.CustomOdd{}, nil +// } + +// return convertedDBCustomOdd, nil +// } + +// func (s *Store) DeleteCustomOddByID(ctx context.Context, id int64) error { +// err := s.queries.DeleteCustomOddsByID(ctx, id) +// if err != nil { +// return err +// } +// return nil +// } + +// func (s *Store) DeleteCustomOddsByOddID(ctx context.Context, oddId int64, companyID int64) error { +// err := s.queries.DeleteCustomOddsByOddID(ctx, dbgen.DeleteCustomOddsByOddIDParams{ +// OddID: oddId, +// CompanyID: companyID, +// }) +// if err != nil { +// return err +// } + +// return nil +// } + +// func (s *Store) DeleteCustomOddByEventID(ctx context.Context, eventID string) error { +// err := s.queries.DeleteCustomOddByEventID(ctx, eventID) + +// if err != nil { +// return err +// } + +// return nil +// } diff --git a/internal/repository/event.go b/internal/repository/event.go index fb43d79..d0798d7 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "math" - "time" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -13,92 +12,29 @@ import ( "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.Int4{Int32: e.SportID, Valid: true}, - MatchName: pgtype.Text{String: e.MatchName, Valid: true}, - HomeTeam: pgtype.Text{String: e.HomeTeam, Valid: true}, - AwayTeam: pgtype.Text{String: e.AwayTeam, Valid: true}, - HomeTeamID: pgtype.Int4{Int32: e.HomeTeamID, Valid: true}, - AwayTeamID: pgtype.Int4{Int32: e.AwayTeamID, Valid: true}, - HomeKitImage: pgtype.Text{String: e.HomeKitImage, Valid: true}, - AwayKitImage: pgtype.Text{String: e.AwayKitImage, Valid: true}, - LeagueID: pgtype.Int4{Int32: e.LeagueID, Valid: true}, - LeagueName: pgtype.Text{String: e.LeagueName, Valid: true}, - LeagueCc: pgtype.Text{String: e.LeagueCC, Valid: true}, - StartTime: pgtype.Timestamp{Time: parsedTime, Valid: true}, - 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}, - Source: pgtype.Text{String: e.Source, Valid: true}, - }) +func (s *Store) SaveEvent(ctx context.Context, e domain.CreateEvent) error { + return s.queries.InsertEvent(ctx, domain.ConvertCreateEvent(e)) } -func (s *Store) SaveUpcomingEvent(ctx context.Context, e domain.UpcomingEvent) error { - return s.queries.InsertUpcomingEvent(ctx, dbgen.InsertUpcomingEventParams{ - ID: e.ID, - SportID: pgtype.Int4{Int32: e.SportID, Valid: true}, - MatchName: pgtype.Text{String: e.MatchName, Valid: true}, - HomeTeam: pgtype.Text{String: e.HomeTeam, Valid: true}, - AwayTeam: pgtype.Text{String: e.AwayTeam, Valid: true}, - HomeTeamID: pgtype.Int4{Int32: e.HomeTeamID, Valid: true}, - AwayTeamID: pgtype.Int4{Int32: e.AwayTeamID, Valid: true}, - HomeKitImage: pgtype.Text{String: e.HomeKitImage, Valid: true}, - AwayKitImage: pgtype.Text{String: e.AwayKitImage, Valid: true}, - LeagueID: pgtype.Int4{Int32: e.LeagueID, Valid: true}, - LeagueName: pgtype.Text{String: e.LeagueName, Valid: true}, - LeagueCc: pgtype.Text{String: e.LeagueCC, Valid: true}, - StartTime: pgtype.Timestamp{Time: e.StartTime, Valid: true}, - Source: pgtype.Text{String: e.Source, Valid: true}, - }) + +func (s *Store) InsertEventSettings(ctx context.Context, eventSetting domain.CreateEventSettings) error { + return s.queries.InsertEventSettings(ctx, domain.ConvertCreateEventSettings(eventSetting)) } func (s *Store) GetLiveEventIDs(ctx context.Context) ([]string, error) { return s.queries.ListLiveEvents(ctx) } -func (s *Store) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) { + +func (s *Store) GetAllUpcomingEvents(ctx context.Context) ([]domain.BaseEvent, error) { events, err := s.queries.GetAllUpcomingEvents(ctx) if err != nil { return nil, err } - upcomingEvents := make([]domain.UpcomingEvent, len(events)) - for i, e := range events { - upcomingEvents[i] = domain.UpcomingEvent{ - ID: e.ID, - SportID: e.SportID.Int32, - MatchName: e.MatchName.String, - HomeTeam: e.HomeTeam.String, - AwayTeam: e.AwayTeam.String, - HomeTeamID: e.HomeTeamID.Int32, - AwayTeamID: e.AwayTeamID.Int32, - HomeKitImage: e.HomeKitImage.String, - AwayKitImage: e.AwayKitImage.String, - LeagueID: e.LeagueID.Int32, - LeagueName: e.LeagueName.String, - LeagueCC: e.LeagueCc.String, - StartTime: e.StartTime.Time.UTC(), - Source: e.Source.String, - Status: domain.EventStatus(e.Status.String), - IsFeatured: e.IsFeatured, - IsMonitored: e.IsMonitored, - IsActive: e.IsActive, - } - } - return upcomingEvents, nil + return domain.ConvertDBEvents(events), nil } -func (s *Store) GetExpiredUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, error) { - events, err := s.queries.GetExpiredUpcomingEvents(ctx, pgtype.Text{ +func (s *Store) GetExpiredUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.BaseEvent, error) { + events, err := s.queries.GetExpiredEvents(ctx, pgtype.Text{ String: filter.MatchStatus.Value, Valid: filter.MatchStatus.Valid, }) @@ -106,166 +42,98 @@ func (s *Store) GetExpiredUpcomingEvents(ctx context.Context, filter domain.Even return nil, err } - upcomingEvents := make([]domain.UpcomingEvent, len(events)) - for i, e := range events { - upcomingEvents[i] = domain.UpcomingEvent{ - ID: e.ID, - SportID: e.SportID.Int32, - MatchName: e.MatchName.String, - HomeTeam: e.HomeTeam.String, - AwayTeam: e.AwayTeam.String, - HomeTeamID: e.HomeTeamID.Int32, - AwayTeamID: e.AwayTeamID.Int32, - HomeKitImage: e.HomeKitImage.String, - AwayKitImage: e.AwayKitImage.String, - LeagueID: e.LeagueID.Int32, - LeagueName: e.LeagueName.String, - LeagueCC: e.LeagueCc.String, - StartTime: e.StartTime.Time.UTC(), - Source: e.Source.String, - Status: domain.EventStatus(e.Status.String), - IsFeatured: e.IsFeatured, - IsActive: e.IsActive, - } - } - return upcomingEvents, nil + return domain.ConvertDBEvents(events), nil } -func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, int64, error) { +func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.BaseEvent, int64, error) { events, err := s.queries.GetPaginatedUpcomingEvents(ctx, dbgen.GetPaginatedUpcomingEventsParams{ - LeagueID: pgtype.Int4{ - Int32: int32(filter.LeagueID.Value), - Valid: filter.LeagueID.Valid, - }, - SportID: pgtype.Int4{ - Int32: int32(filter.SportID.Value), - Valid: filter.SportID.Valid, - }, - Query: pgtype.Text{ - String: filter.Query.Value, - Valid: filter.Query.Valid, - }, - Limit: pgtype.Int4{ - Int32: int32(filter.Limit.Value), - Valid: filter.Limit.Valid, - }, - Offset: pgtype.Int4{ - Int32: int32(filter.Offset.Value * filter.Limit.Value), - Valid: filter.Offset.Valid, - }, - FirstStartTime: pgtype.Timestamp{ - Time: filter.FirstStartTime.Value.UTC(), - Valid: filter.FirstStartTime.Valid, - }, - LastStartTime: pgtype.Timestamp{ - Time: filter.LastStartTime.Value.UTC(), - Valid: filter.LastStartTime.Valid, - }, - CountryCode: pgtype.Text{ - String: filter.CountryCode.Value, - Valid: filter.CountryCode.Valid, - }, - IsFeatured: pgtype.Bool{ - Bool: filter.Featured.Valid, - Valid: filter.Featured.Valid, - }, + LeagueID: filter.LeagueID.ToPG(), + SportID: filter.SportID.ToPG(), + Query: filter.Query.ToPG(), + Limit: filter.Limit.ToPG(), + Offset: filter.Offset.ToPG(), + FirstStartTime: filter.FirstStartTime.ToPG(), + LastStartTime: filter.LastStartTime.ToPG(), + CountryCode: filter.CountryCode.ToPG(), }) if err != nil { return nil, 0, err } - upcomingEvents := make([]domain.UpcomingEvent, len(events)) - for i, e := range events { - upcomingEvents[i] = domain.UpcomingEvent{ - ID: e.ID, - SportID: e.SportID.Int32, - MatchName: e.MatchName.String, - HomeTeam: e.HomeTeam.String, - AwayTeam: e.AwayTeam.String, - HomeTeamID: e.HomeTeamID.Int32, - AwayTeamID: e.AwayTeamID.Int32, - HomeKitImage: e.HomeKitImage.String, - AwayKitImage: e.AwayKitImage.String, - LeagueID: e.LeagueID.Int32, - LeagueName: e.LeagueName.String, - LeagueCC: e.LeagueCc.String, - StartTime: e.StartTime.Time.UTC(), - Source: e.Source.String, - Status: domain.EventStatus(e.Status.String), - IsFeatured: e.IsFeatured, - IsActive: e.IsActive, - IsMonitored: e.IsMonitored, - } - } + totalCount, err := s.queries.GetTotalEvents(ctx, dbgen.GetTotalEventsParams{ - LeagueID: pgtype.Int4{ - Int32: int32(filter.LeagueID.Value), - Valid: filter.LeagueID.Valid, - }, - SportID: pgtype.Int4{ - Int32: int32(filter.SportID.Value), - Valid: filter.SportID.Valid, - }, - Query: pgtype.Text{ - String: filter.Query.Value, - Valid: filter.Query.Valid, - }, - FirstStartTime: pgtype.Timestamp{ - Time: filter.FirstStartTime.Value.UTC(), - Valid: filter.FirstStartTime.Valid, - }, - LastStartTime: pgtype.Timestamp{ - Time: filter.LastStartTime.Value.UTC(), - Valid: filter.LastStartTime.Valid, - }, - CountryCode: pgtype.Text{ - String: filter.CountryCode.Value, - Valid: filter.CountryCode.Valid, - }, - IsFeatured: pgtype.Bool{ - Bool: filter.Featured.Valid, - Valid: filter.Featured.Valid, - }, + LeagueID: filter.LeagueID.ToPG(), + SportID: filter.SportID.ToPG(), + Query: filter.Query.ToPG(), + FirstStartTime: filter.FirstStartTime.ToPG(), + LastStartTime: filter.LastStartTime.ToPG(), + CountryCode: filter.CountryCode.ToPG(), }) if err != nil { return nil, 0, err } numberOfPages := math.Ceil(float64(totalCount) / float64(filter.Limit.Value)) - return upcomingEvents, int64(numberOfPages), nil + return domain.ConvertDBEvents(events), int64(numberOfPages), nil } -func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) { - event, err := s.queries.GetUpcomingByID(ctx, ID) + +func (s *Store) GetEventsWithSettings(ctx context.Context, companyID int64, filter domain.EventFilter) ([]domain.EventWithSettings, int64, error) { + events, err := s.queries.GetEventsWithSettings(ctx, dbgen.GetEventsWithSettingsParams{ + CompanyID: companyID, + LeagueID: filter.LeagueID.ToPG(), + SportID: filter.SportID.ToPG(), + Query: filter.Query.ToPG(), + Limit: filter.Limit.ToPG(), + Offset: filter.Offset.ToPG(), + FirstStartTime: filter.FirstStartTime.ToPG(), + LastStartTime: filter.LastStartTime.ToPG(), + CountryCode: filter.CountryCode.ToPG(), + }) + if err != nil { - return domain.UpcomingEvent{}, err + return nil, 0, err } - return domain.UpcomingEvent{ - ID: event.ID, - SportID: event.SportID.Int32, - MatchName: event.MatchName.String, - HomeTeam: event.HomeTeam.String, - AwayTeam: event.AwayTeam.String, - HomeTeamID: event.HomeTeamID.Int32, - AwayTeamID: event.AwayTeamID.Int32, - HomeKitImage: event.HomeKitImage.String, - AwayKitImage: event.AwayKitImage.String, - LeagueID: event.LeagueID.Int32, - LeagueName: event.LeagueName.String, - LeagueCC: event.LeagueCc.String, - StartTime: event.StartTime.Time.UTC(), - Source: event.Source.String, - Status: domain.EventStatus(event.Status.String), - IsFeatured: event.IsFeatured, - IsActive: event.IsActive, - IsMonitored: event.IsMonitored, - }, nil + totalCount, err := s.queries.GetTotalCompanyEvents(ctx, dbgen.GetTotalCompanyEventsParams{ + CompanyID: companyID, + LeagueID: filter.LeagueID.ToPG(), + SportID: filter.SportID.ToPG(), + Query: filter.Query.ToPG(), + FirstStartTime: filter.FirstStartTime.ToPG(), + LastStartTime: filter.LastStartTime.ToPG(), + CountryCode: filter.CountryCode.ToPG(), + }) + if err != nil { + return nil, 0, err + } + + numberOfPages := math.Ceil(float64(totalCount) / float64(filter.Limit.Value)) + return domain.ConvertDBEventWithSettings(events), int64(numberOfPages), nil +} +func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.BaseEvent, error) { + event, err := s.queries.GetUpcomingByID(ctx, ID) + if err != nil { + return domain.BaseEvent{}, err + } + + return domain.ConvertDBEvent(event), nil +} +func (s *Store) GetEventWithSettingByID(ctx context.Context, ID string, companyID int64) (domain.EventWithSettings, error) { + event, err := s.queries.GetEventWithSettingByID(ctx, dbgen.GetEventWithSettingByIDParams{ + ID: ID, + CompanyID: companyID, + }) + if err != nil { + return domain.EventWithSettings{}, err + } + + return domain.ConvertDBEventWithSetting(event), nil } func (s *Store) UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error { params := dbgen.UpdateMatchResultParams{ Score: pgtype.Text{String: fullScore, Valid: true}, - Status: pgtype.Text{String: string(status), Valid: true}, + Status: string(status), ID: eventID, } @@ -279,11 +147,8 @@ func (s *Store) UpdateFinalScore(ctx context.Context, eventID, fullScore string, func (s *Store) UpdateEventStatus(ctx context.Context, eventID string, status domain.EventStatus) error { params := dbgen.UpdateMatchResultParams{ - Status: pgtype.Text{ - String: string(status), - Valid: true, - }, - ID: eventID, + Status: string(status), + ID: eventID, } err := s.queries.UpdateMatchResult(ctx, params) @@ -295,13 +160,6 @@ func (s *Store) UpdateEventStatus(ctx context.Context, eventID string, status do } -func (s *Store) UpdateEventFeatured(ctx context.Context, eventID string, isFeatured bool) error { - return s.queries.UpdateEventFeatured(ctx, dbgen.UpdateEventFeaturedParams{ - ID: eventID, - IsFeatured: isFeatured, - }) -} - func (s *Store) IsEventMonitored(ctx context.Context, eventID string) (bool, error) { isMonitored, err := s.queries.IsEventMonitored(ctx, eventID) @@ -317,6 +175,10 @@ func (s *Store) UpdateEventMonitored(ctx context.Context, eventID string, IsMoni }) } +func (s *Store) UpdateEventSettings(ctx context.Context, event domain.CreateEventSettings) error { + return s.queries.UpdateEventSettings(ctx, domain.ConvertUpdateEventSettings(event)) +} + func (s *Store) DeleteEvent(ctx context.Context, eventID string) error { err := s.queries.DeleteEvent(ctx, eventID) if err != nil { diff --git a/internal/repository/league.go b/internal/repository/league.go index fa6f870..f003fd9 100644 --- a/internal/repository/league.go +++ b/internal/repository/league.go @@ -8,36 +8,18 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -func (s *Store) SaveLeague(ctx context.Context, l domain.League) error { - return s.queries.InsertLeague(ctx, dbgen.InsertLeagueParams{ - ID: l.ID, - Name: l.Name, - CountryCode: pgtype.Text{String: l.CountryCode, Valid: true}, - Bet365ID: pgtype.Int4{Int32: l.Bet365ID, Valid: true}, - IsActive: pgtype.Bool{Bool: l.IsActive, Valid: true}, - IsFeatured: pgtype.Bool{Bool: l.IsFeatured, Valid: true}, - SportID: l.SportID, - }) +func (s *Store) SaveLeague(ctx context.Context, league domain.CreateLeague) error { + return s.queries.InsertLeague(ctx, domain.ConvertCreateLeague(league)) } -func (s *Store) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.League, error) { +func (s *Store) SaveLeagueSettings(ctx context.Context, leagueSettings domain.CreateLeagueSettings) error { + return s.queries.InsertLeagueSettings(ctx, domain.ConvertCreateLeagueSettings(leagueSettings)) +} + +func (s *Store) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.BaseLeague, error) { l, err := s.queries.GetAllLeagues(ctx, dbgen.GetAllLeaguesParams{ - CountryCode: pgtype.Text{ - String: filter.CountryCode.Value, - Valid: filter.CountryCode.Valid, - }, - SportID: pgtype.Int4{ - Int32: filter.SportID.Value, - Valid: filter.SportID.Valid, - }, - IsActive: pgtype.Bool{ - Bool: filter.IsActive.Value, - Valid: filter.IsActive.Valid, - }, - IsFeatured: pgtype.Bool{ - Bool: filter.IsFeatured.Value, - Valid: filter.IsFeatured.Valid, - }, + CountryCode: filter.CountryCode.ToPG(), + SportID: filter.SportID.ToPG(), Limit: pgtype.Int4{ Int32: int32(filter.Limit.Value), Valid: filter.Limit.Valid, @@ -51,85 +33,38 @@ func (s *Store) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ( return nil, err } - leagues := make([]domain.League, len(l)) - for i, league := range l { - leagues[i] = domain.League{ - ID: league.ID, - Name: league.Name, - CountryCode: league.CountryCode.String, - Bet365ID: league.Bet365ID.Int32, - IsActive: league.IsActive.Bool, - IsFeatured: league.IsFeatured.Bool, - SportID: league.SportID, - } - } - return leagues, nil + return domain.ConvertDBBaseLeagues(l), nil } -func (s *Store) GetFeaturedLeagues(ctx context.Context) ([]domain.League, error) { - l, err := s.queries.GetFeaturedLeagues(ctx) +func (s *Store) GetAllLeaguesByCompany(ctx context.Context, companyID int64, filter domain.LeagueFilter) ([]domain.LeagueWithSettings, error) { + l, err := s.queries.GetAllLeaguesWithSettings(ctx, dbgen.GetAllLeaguesWithSettingsParams{ + CompanyID: companyID, + CountryCode: filter.CountryCode.ToPG(), + SportID: filter.SportID.ToPG(), + Limit: pgtype.Int4{ + Int32: int32(filter.Limit.Value), + Valid: filter.Limit.Valid, + }, + Offset: pgtype.Int4{ + Int32: int32(filter.Offset.Value * filter.Limit.Value), + Valid: filter.Offset.Valid, + }, + }) if err != nil { return nil, err } - leagues := make([]domain.League, len(l)) - for i, league := range l { - leagues[i] = domain.League{ - ID: league.ID, - Name: league.Name, - CountryCode: league.CountryCode.String, - Bet365ID: league.Bet365ID.Int32, - IsActive: league.IsActive.Bool, - - SportID: league.SportID, - } - } - return leagues, nil + return domain.ConvertDBLeagueWithSettings(l), nil } -func (s *Store) CheckLeagueSupport(ctx context.Context, leagueID int64) (bool, error) { - return s.queries.CheckLeagueSupport(ctx, leagueID) -} - -func (s *Store) SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error { - return s.queries.SetLeagueActive(ctx, dbgen.SetLeagueActiveParams{ - ID: leagueId, - IsActive: pgtype.Bool{ - Bool: isActive, - Valid: true, - }, +func (s *Store) CheckLeagueSupport(ctx context.Context, leagueID int64, companyID int64) (bool, error) { + return s.queries.CheckLeagueSupport(ctx, dbgen.CheckLeagueSupportParams{ + LeagueID: leagueID, + CompanyID: companyID, }) } func (s *Store) UpdateLeague(ctx context.Context, league domain.UpdateLeague) error { - err := s.queries.UpdateLeague(ctx, dbgen.UpdateLeagueParams{ - ID: league.ID, - Name: pgtype.Text{ - String: league.Name.Value, - Valid: league.Name.Valid, - }, - CountryCode: pgtype.Text{ - String: league.CountryCode.Value, - Valid: league.CountryCode.Valid, - }, - Bet365ID: pgtype.Int4{ - Int32: league.Bet365ID.Value, - Valid: league.Bet365ID.Valid, - }, - IsActive: pgtype.Bool{ - Bool: league.IsActive.Value, - Valid: league.IsActive.Valid, - }, - IsFeatured: pgtype.Bool{ - Bool: league.IsFeatured.Value, - Valid: league.IsFeatured.Valid, - }, - SportID: pgtype.Int4{ - Int32: league.SportID.Value, - Valid: league.SportID.Valid, - }, - }) - - return err + return s.queries.UpdateLeague(ctx, domain.ConvertUpdateLeague(league)) } diff --git a/internal/repository/odds.go b/internal/repository/odds.go index ac68ff9..373de20 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -3,7 +3,6 @@ package repository import ( "context" "encoding/json" - "fmt" "os" "strconv" @@ -14,57 +13,26 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error { +func (s *Store) SaveOddMarket(ctx context.Context, m domain.CreateOddMarket) error { if len(m.Odds) == 0 { return nil } - for _, item := range m.Odds { - var name string - var oddsVal float64 + params, err := domain.ConvertCreateOddMarket(m) - if m.Source == "bwin" { - nameValue := getMap(item["name"]) - name = getString(nameValue["value"]) - oddsVal = getFloat(item["odds"]) - } else { - name = getString(item["name"]) - oddsVal = getConvertedFloat(item["odds"]) - } - handicap := getString(item["handicap"]) + if err != nil { + return err + } - rawOddsBytes, _ := json.Marshal(m.Odds) - - params := dbgen.InsertNonLiveOddParams{ - EventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""}, - Fi: pgtype.Text{String: m.FI, Valid: m.FI != ""}, - MarketType: m.MarketType, - MarketName: pgtype.Text{String: m.MarketName, Valid: m.MarketName != ""}, - MarketCategory: pgtype.Text{String: m.MarketCategory, Valid: m.MarketCategory != ""}, - MarketID: pgtype.Text{String: m.MarketID, Valid: m.MarketID != ""}, - Name: pgtype.Text{String: name, Valid: name != ""}, - Handicap: pgtype.Text{String: handicap, Valid: handicap != ""}, - OddsValue: pgtype.Float8{Float64: oddsVal, Valid: oddsVal != 0}, - Section: m.MarketCategory, - Category: pgtype.Text{Valid: false}, - RawOdds: rawOddsBytes, - IsActive: pgtype.Bool{Bool: true, Valid: true}, - Source: pgtype.Text{String: m.Source, Valid: true}, - FetchedAt: pgtype.Timestamp{Time: time.Now(), Valid: true}, - ExpiresAt: pgtype.Timestamp{Time: (time.Now()).Add(time.Hour), Valid: true}, - } - - err := s.queries.InsertNonLiveOdd(ctx, params) - fmt.Printf("Inserting Non Live Odd") - if err != nil { - _ = writeFailedMarketLog(m, err) - continue - } + err = s.queries.InsertOddsMarket(ctx, params) + if err != nil { + _ = writeFailedMarketLog(m, err) + return err } return nil } -func writeFailedMarketLog(m domain.Market, err error) error { +func writeFailedMarketLog(m domain.CreateOddMarket, err error) error { logDir := "logs" logFile := logDir + "/failed_markets.log" @@ -79,9 +47,9 @@ func writeFailedMarketLog(m domain.Market, err error) error { defer f.Close() entry := struct { - Time string `json:"time"` - Error string `json:"error"` - Record domain.Market `json:"record"` + Time string `json:"time"` + Error string `json:"error"` + Record domain.CreateOddMarket `json:"record"` }{ Time: time.Now().Format(time.RFC3339), Error: err.Error(), @@ -93,196 +61,130 @@ func writeFailedMarketLog(m domain.Market, err error) error { return writeErr } -func (s *Store) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) { - odds, err := s.queries.GetPrematchOdds(ctx) +func (s *Store) GetAllOdds(ctx context.Context, filter domain.OddMarketFilter) ([]domain.OddMarket, error) { + rows, err := s.queries.GetAllOdds(ctx, dbgen.GetAllOddsParams{ + Offset: filter.Offset.ToPG(), + Limit: filter.Limit.ToPG(), + }) if err != nil { return nil, err } - domainOdds := make([]domain.Odd, len(odds)) - for i, odd := range odds { - domainOdds[i] = domain.Odd{ - EventID: odd.EventID.String, - Fi: odd.Fi.String, - MarketType: odd.MarketType, - MarketName: odd.MarketName.String, - MarketCategory: odd.MarketCategory.String, - MarketID: odd.MarketID.String, - Name: odd.Name.String, - Handicap: odd.Handicap.String, - OddsValue: odd.OddsValue.Float64, - Section: odd.Section, - Category: odd.Category.String, - RawOdds: func() []json.RawMessage { - var rawOdds []json.RawMessage - if err := json.Unmarshal(odd.RawOdds, &rawOdds); err != nil { - rawOdds = nil - } - return rawOdds - }(), - FetchedAt: odd.FetchedAt.Time, - ExpiresAt: odd.ExpiresAt.Time, - Source: odd.Source.String, - IsActive: odd.IsActive.Bool, - } + domainOdds, err := domain.ConvertDBOddMarkets(rows) + + if err != nil { + return nil, err } return domainOdds, nil } -func (s *Store) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error) { - rows, err := s.queries.GetALLPrematchOdds(ctx) +func (s *Store) GetAllOddsWithSettings(ctx context.Context, companyID int64, filter domain.OddMarketFilter) ([]domain.OddMarketWithSettings, error) { + odds, err := s.queries.GetAllOddsWithSettings(ctx, dbgen.GetAllOddsWithSettingsParams{ + CompanyID: companyID, + Offset: filter.Offset.ToPG(), + Limit: filter.Limit.ToPG(), + }) if err != nil { return nil, err } - domainOdds := make([]domain.Odd, len(rows)) - for i, row := range rows { - domainOdds[i] = domain.Odd{ - // ID: int64(row.ID), - EventID: row.EventID.String, - Fi: row.Fi.String, - MarketType: row.MarketType, - MarketName: row.MarketName.String, - MarketCategory: row.MarketCategory.String, - MarketID: row.MarketID.String, - Name: row.Name.String, - Handicap: row.Handicap.String, - OddsValue: row.OddsValue.Float64, - Section: row.Section, - Category: row.Category.String, - RawOdds: func() []json.RawMessage { - var rawOdds []json.RawMessage - if err := json.Unmarshal(row.RawOdds, &rawOdds); err != nil { - rawOdds = nil - } - return rawOdds - }(), - FetchedAt: row.FetchedAt.Time, - ExpiresAt: row.ExpiresAt.Time, - Source: row.Source.String, - IsActive: row.IsActive.Bool, - } + domainOdds, err := domain.ConvertDBOddMarketWithSettings(odds) + + if err != nil { + return nil, err } return domainOdds, nil } -func (s *Store) GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) (domain.RawOddsByMarketID, error) { - params := dbgen.GetOddsByMarketIDParams{ - MarketID: pgtype.Text{String: marketID, Valid: true}, - Fi: pgtype.Text{String: upcomingID, Valid: true}, - } +func (s *Store) GetOddsByMarketID(ctx context.Context, marketID string, eventID string) (domain.OddMarket, error) { - odds, err := s.queries.GetOddsByMarketID(ctx, params) + odds, err := s.queries.GetOddsByMarketID(ctx, dbgen.GetOddsByMarketIDParams{ + MarketID: marketID, + EventID: eventID, + }) if err != nil { - return domain.RawOddsByMarketID{}, err + return domain.OddMarket{}, err } - var rawOdds []json.RawMessage - if err := json.Unmarshal(odds.RawOdds, &rawOdds); err != nil { - return domain.RawOddsByMarketID{}, err - } + convertedOdd, err := domain.ConvertDBOddMarket(odds) - return domain.RawOddsByMarketID{ - ID: int64(odds.ID), - MarketName: odds.MarketName.String, - Handicap: odds.Handicap.String, - RawOdds: func() []json.RawMessage { - converted := make([]json.RawMessage, len(rawOdds)) - for i, r := range rawOdds { - converted[i] = json.RawMessage(r) - } - return converted - }(), - FetchedAt: odds.FetchedAt.Time, - ExpiresAt: odds.ExpiresAt.Time, - }, nil + if err != nil { + return domain.OddMarket{}, err + } + return convertedOdd, nil } -func (s *Store) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit domain.ValidInt32, offset domain.ValidInt32) ([]domain.Odd, error) { - odds, err := s.queries.GetPaginatedPrematchOddsByUpcomingID(ctx, dbgen.GetPaginatedPrematchOddsByUpcomingIDParams{ - ID: upcomingID, - Limit: limit.ToPG(), - Offset: offset.ToPG(), +func (s *Store) GetOddsWithSettingsByMarketID(ctx context.Context, marketID string, eventID string, companyID int64) (domain.OddMarketWithSettings, error) { + + odds, err := s.queries.GetOddsWithSettingsByMarketID(ctx, dbgen.GetOddsWithSettingsByMarketIDParams{ + MarketID: marketID, + EventID: eventID, + CompanyID: companyID, + }) + if err != nil { + return domain.OddMarketWithSettings{}, err + } + + convertedOdd, err := domain.ConvertDBOddMarketWithSetting(odds) + + if err != nil { + return domain.OddMarketWithSettings{}, err + } + return convertedOdd, nil +} + +func (s *Store) GetOddsByEventID(ctx context.Context, upcomingID string, filter domain.OddMarketWithEventFilter) ([]domain.OddMarket, error) { + odds, err := s.queries.GetOddsByEventID(ctx, dbgen.GetOddsByEventIDParams{ + EventID: upcomingID, + Limit: filter.Limit.ToPG(), + Offset: filter.Offset.ToPG(), + IsLive: pgtype.Bool{ + Bool: false, + Valid: true, + }, + Status: pgtype.Text{ + String: string(domain.STATUS_PENDING), + Valid: true, + }, + Source: pgtype.Text{}, }) if err != nil { return nil, err } // Map the results to domain.Odd - domainOdds := make([]domain.Odd, len(odds)) - for i, odd := range odds { - var rawOdds []json.RawMessage - if err := json.Unmarshal(odd.RawOdds, &rawOdds); err != nil { - rawOdds = nil - } - - domainOdds[i] = domain.Odd{ - EventID: odd.EventID.String, - Fi: odd.Fi.String, - MarketType: odd.MarketType, - MarketName: odd.MarketName.String, - MarketCategory: odd.MarketCategory.String, - MarketID: odd.MarketID.String, - Name: odd.Name.String, - Handicap: odd.Handicap.String, - OddsValue: odd.OddsValue.Float64, - Section: odd.Section, - Category: odd.Category.String, - RawOdds: rawOdds, - FetchedAt: odd.FetchedAt.Time, - ExpiresAt: odd.ExpiresAt.Time, - Source: odd.Source.String, - IsActive: odd.IsActive.Bool, - } + domainOdds, err := domain.ConvertDBOddMarkets(odds) + if err != nil { + return nil, err } return domainOdds, nil } -func (s *Store) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string) ([]domain.Odd, error) { +func (s *Store) GetOddsWithSettingsByEventID(ctx context.Context, upcomingID string, companyID int64, filter domain.OddMarketFilter) ([]domain.OddMarketWithSettings, error) { - odds, err := s.queries.GetPrematchOddsByUpcomingID(ctx, upcomingID) + odds, err := s.queries.GetOddsWithSettingsByEventID(ctx, dbgen.GetOddsWithSettingsByEventIDParams{ + EventID: upcomingID, + CompanyID: companyID, + Offset: filter.Offset.ToPG(), + Limit: filter.Limit.ToPG(), + }) if err != nil { return nil, err } // Map the results to domain.Odd - domainOdds := make([]domain.Odd, len(odds)) - for i, odd := range odds { - var rawOdds []json.RawMessage - if err := json.Unmarshal(odd.RawOdds, &rawOdds); err != nil { - rawOdds = nil - } - - domainOdds[i] = domain.Odd{ - EventID: odd.EventID.String, - Fi: odd.Fi.String, - MarketType: odd.MarketType, - MarketName: odd.MarketName.String, - MarketCategory: odd.MarketCategory.String, - MarketID: odd.MarketID.String, - Name: odd.Name.String, - Handicap: odd.Handicap.String, - OddsValue: odd.OddsValue.Float64, - Section: odd.Section, - Category: odd.Category.String, - RawOdds: rawOdds, - FetchedAt: odd.FetchedAt.Time, - ExpiresAt: odd.ExpiresAt.Time, - Source: odd.Source.String, - IsActive: odd.IsActive.Bool, - } + domainOdds, err := domain.ConvertDBOddMarketWithSettings(odds) + if err != nil { + return nil, err } return domainOdds, nil } func (s *Store) DeleteOddsForEvent(ctx context.Context, eventID string) error { - return s.queries.DeleteOddsForEvent(ctx, pgtype.Text{ - String: eventID, - Valid: true, - }) + return s.queries.DeleteOddsForEvent(ctx, eventID) } func getString(v interface{}) string { diff --git a/internal/repository/user.go b/internal/repository/user.go index 4387be4..703e745 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -46,6 +46,8 @@ func (s *Store) CreateUser(ctx context.Context, user domain.User, usedOtpId int6 Time: time.Now(), Valid: true, }, + CompanyID: user.CompanyID.ToPG(), + Suspended: user.Suspended, }) if err != nil { return domain.User{}, err @@ -57,6 +59,15 @@ func (s *Store) CreateUser(ctx context.Context, user domain.User, usedOtpId int6 Email: userRes.Email.String, PhoneNumber: userRes.PhoneNumber.String, Role: domain.Role(userRes.Role), + CompanyID: domain.ValidInt64{ + Value: userRes.CompanyID.Int64, + Valid: userRes.CompanyID.Valid, + }, + EmailVerified: userRes.EmailVerified, + PhoneVerified: userRes.PhoneVerified, + CreatedAt: userRes.CreatedAt.Time, + UpdatedAt: userRes.UpdatedAt.Time, + Suspended: userRes.Suspended, }, nil } func (s *Store) GetUserByID(ctx context.Context, id int64) (domain.User, error) { @@ -247,18 +258,14 @@ func (s *Store) GetCashiersByBranch(ctx context.Context, branchID int64) ([]doma func (s *Store) SearchUserByNameOrPhone(ctx context.Context, searchString string, role *domain.Role, companyID domain.ValidInt64) ([]domain.User, error) { query := dbgen.SearchUserByNameOrPhoneParams{ - Column1: pgtype.Text{ + CompanyID: companyID.ToPG(), + Column2: pgtype.Text{ String: searchString, Valid: true, }, - CompanyID: pgtype.Int8{ - Int64: companyID.Value, - Valid: companyID.Valid, - }, } if role != nil { - query.Role = pgtype.Text{ String: string(*role), Valid: true, @@ -340,7 +347,7 @@ func (s *Store) DeleteUser(ctx context.Context, id int64) error { } return nil } -func (s *Store) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) { +func (s *Store) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string, companyID domain.ValidInt64) (bool, bool, error) { row, err := s.queries.CheckPhoneEmailExist(ctx, dbgen.CheckPhoneEmailExistParams{ PhoneNumber: pgtype.Text{ @@ -352,6 +359,7 @@ func (s *Store) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string Valid: email != "", }, + CompanyID: companyID.ToPG(), }) if err != nil { @@ -360,10 +368,13 @@ func (s *Store) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string return row.EmailExists, row.PhoneExists, nil } -func (s *Store) GetUserByEmail(ctx context.Context, email string) (domain.User, error) { - user, err := s.queries.GetUserByEmail(ctx, pgtype.Text{ - String: email, - Valid: true, +func (s *Store) GetUserByEmail(ctx context.Context, email string, companyID domain.ValidInt64) (domain.User, error) { + user, err := s.queries.GetUserByEmail(ctx, dbgen.GetUserByEmailParams{ + Email: pgtype.Text{ + String: email, + Valid: true, + }, + CompanyID: companyID.ToPG(), }) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -386,10 +397,13 @@ func (s *Store) GetUserByEmail(ctx context.Context, email string) (domain.User, SuspendedAt: user.SuspendedAt.Time, }, nil } -func (s *Store) GetUserByPhone(ctx context.Context, phoneNum string) (domain.User, error) { - user, err := s.queries.GetUserByPhone(ctx, pgtype.Text{ - String: phoneNum, - Valid: true, +func (s *Store) GetUserByPhone(ctx context.Context, phoneNum string, companyID domain.ValidInt64) (domain.User, error) { + user, err := s.queries.GetUserByPhone(ctx, dbgen.GetUserByPhoneParams{ + PhoneNumber: pgtype.Text{ + String: phoneNum, + Valid: true, + }, + CompanyID: companyID.ToPG(), }) if err != nil { if errors.Is(err, sql.ErrNoRows) { diff --git a/internal/services/authentication/impl.go b/internal/services/authentication/impl.go index 2760a63..8bebc32 100644 --- a/internal/services/authentication/impl.go +++ b/internal/services/authentication/impl.go @@ -26,8 +26,8 @@ type LoginSuccess struct { CompanyID domain.ValidInt64 } -func (s *Service) Login(ctx context.Context, email, phone string, password string) (LoginSuccess, error) { - user, err := s.userStore.GetUserByEmailPhone(ctx, email, phone) +func (s *Service) Login(ctx context.Context, email, phone string, password string, companyID domain.ValidInt64) (LoginSuccess, error) { + user, err := s.userStore.GetUserByEmailPhone(ctx, email, phone, companyID) if err != nil { return LoginSuccess{}, err } diff --git a/internal/services/authentication/port.go b/internal/services/authentication/port.go index 2728241..0c313e3 100644 --- a/internal/services/authentication/port.go +++ b/internal/services/authentication/port.go @@ -7,7 +7,7 @@ import ( ) type UserStore interface { - GetUserByEmailPhone(ctx context.Context, email, phone string) (domain.User, error) + GetUserByEmailPhone(ctx context.Context, email, phone string, companyID domain.ValidInt64) (domain.User, error) } type TokenStore interface { CreateRefreshToken(ctx context.Context, rt domain.RefreshToken) error diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index 1ac84bf..ad44770 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -133,7 +133,7 @@ func (s *Service) GenerateBetOutcome(ctx context.Context, eventID int64, marketI return domain.CreateBetOutcome{}, ErrEventHasNotEnded } - odds, err := s.prematchSvc.GetRawOddsByMarketID(ctx, marketIDStr, eventIDStr) + odds, err := s.prematchSvc.GetOddsByMarketID(ctx, marketIDStr, eventIDStr) if err != nil { s.mongoLogger.Error("failed to get raw odds by market ID", zap.Int64("event_id", eventID), @@ -581,7 +581,7 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string, var newOdds []domain.CreateBetOutcome var totalOdds float32 = 1 - markets, err := s.prematchSvc.GetPrematchOddsByUpcomingID(ctx, eventID) + markets, err := s.prematchSvc.GetOddsByEventID(ctx, eventID, domain.OddMarketWithEventFilter{}) if err != nil { s.logger.Error("failed to get odds for event", "event id", eventID, "error", err) s.mongoLogger.Error("failed to get odds for event", @@ -603,7 +603,7 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string, return nil, 0, fmt.Errorf("empty odds or event %v", eventID) } - var selectedMarkets []domain.Odd + var selectedMarkets []domain.OddMarket numMarkets = min(numMarkets, len(markets)) for i := 0; i < numMarkets; i++ { randomIndex := random.Intn(len(markets)) @@ -714,7 +714,7 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string, return newOdds, totalOdds, nil } -func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, leagueID, sportID domain.ValidInt32, firstStartTime, lastStartTime domain.ValidTime) (domain.CreateBetRes, error) { +func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, leagueID domain.ValidInt64, sportID domain.ValidInt32, firstStartTime, lastStartTime domain.ValidTime) (domain.CreateBetRes, error) { // Get a unexpired event id @@ -742,7 +742,7 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, le } // TODO: Add the option of passing number of created events - var selectedUpcomingEvents []domain.UpcomingEvent + var selectedUpcomingEvents []domain.BaseEvent numEventsPerBet := min(random.Intn(4)+1, len(events)) //Eliminate the option of 0 for i := 0; i < int(numEventsPerBet); i++ { diff --git a/internal/services/company/port.go b/internal/services/company/port.go index 992f69f..10bfa70 100644 --- a/internal/services/company/port.go +++ b/internal/services/company/port.go @@ -11,6 +11,7 @@ type CompanyStore interface { GetAllCompanies(ctx context.Context, filter domain.CompanyFilter) ([]domain.GetCompany, error) SearchCompanyByName(ctx context.Context, name string) ([]domain.GetCompany, error) GetCompanyByID(ctx context.Context, id int64) (domain.GetCompany, error) + GetCompanyIDBySlug(ctx context.Context, slug string) (int64, error) UpdateCompany(ctx context.Context, company domain.UpdateCompany) (domain.Company, error) DeleteCompany(ctx context.Context, id int64) error diff --git a/internal/services/company/service.go b/internal/services/company/service.go index ce82da8..a396a10 100644 --- a/internal/services/company/service.go +++ b/internal/services/company/service.go @@ -26,6 +26,9 @@ func (s *Service) GetAllCompanies(ctx context.Context, filter domain.CompanyFilt func (s *Service) GetCompanyByID(ctx context.Context, id int64) (domain.GetCompany, error) { return s.companyStore.GetCompanyByID(ctx, id) } +func (s *Service) GetCompanyIDBySlug(ctx context.Context, slug string) (int64, error){ + return s.companyStore.GetCompanyIDBySlug(ctx, slug) +} func (s *Service) SearchCompanyByName(ctx context.Context, name string) ([]domain.GetCompany, error) { return s.companyStore.SearchCompanyByName(ctx, name) diff --git a/internal/services/event/port.go b/internal/services/event/port.go index 1cd67c8..e02be83 100644 --- a/internal/services/event/port.go +++ b/internal/services/event/port.go @@ -7,16 +7,18 @@ import ( ) type Service interface { - FetchLiveEvents(ctx context.Context) error + // FetchLiveEvents(ctx context.Context) error FetchUpcomingEvents(ctx context.Context) error - GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) - GetExpiredUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, error) - GetPaginatedUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, int64, error) - GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) + GetAllUpcomingEvents(ctx context.Context) ([]domain.BaseEvent, error) + GetExpiredUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.BaseEvent, error) + GetPaginatedUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.BaseEvent, int64, error) + GetUpcomingEventByID(ctx context.Context, ID string) (domain.BaseEvent, error) // GetAndStoreMatchResult(ctx context.Context, eventID string) error UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error UpdateEventStatus(ctx context.Context, eventID string, status domain.EventStatus) error - UpdateEventFeatured(ctx context.Context, eventID string, flagged bool) error IsEventMonitored(ctx context.Context, eventID string) (bool, error) UpdateEventMonitored(ctx context.Context, eventID string, IsMonitored bool) error + GetEventsWithSettings(ctx context.Context, companyID int64, filter domain.EventFilter) ([]domain.EventWithSettings, int64, error) + GetEventWithSettingByID(ctx context.Context, ID string, companyID int64) (domain.EventWithSettings, error) + UpdateEventSettings(ctx context.Context, event domain.CreateEventSettings) error } diff --git a/internal/services/event/service.go b/internal/services/event/service.go index f381887..ab227bc 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -32,162 +32,162 @@ func New(token string, store *repository.Store, mongoLogger *zap.Logger) Service } } -func (s *service) FetchLiveEvents(ctx context.Context) error { - var wg sync.WaitGroup - urls := []struct { - name string - source string - }{ - {"https://api.b365api.com/v1/bet365/inplay?sport_id=%d&token=%s", "bet365"}, - {"https://api.b365api.com/v1/betfair/sb/inplay?sport_id=%d&token=%s", "betfair"}, - {"https://api.b365api.com/v1/1xbet/inplay?sport_id=%d&token=%s", "1xbet"}, - } +// func (s *service) FetchLiveEvents(ctx context.Context) error { +// var wg sync.WaitGroup +// urls := []struct { +// name string +// source string +// }{ +// {"https://api.b365api.com/v1/bet365/inplay?sport_id=%d&token=%s", "bet365"}, +// {"https://api.b365api.com/v1/betfair/sb/inplay?sport_id=%d&token=%s", "betfair"}, +// {"https://api.b365api.com/v1/1xbet/inplay?sport_id=%d&token=%s", "1xbet"}, +// } - for _, url := range urls { - wg.Add(1) +// for _, url := range urls { +// wg.Add(1) - go func() { - defer wg.Done() - s.fetchLiveEvents(ctx, url.name, url.source) - }() - } - wg.Wait() - return nil -} +// go func() { +// defer wg.Done() +// s.fetchLiveEvents(ctx, url.name, url.source) +// }() +// } +// wg.Wait() +// return nil +// } -func (s *service) fetchLiveEvents(ctx context.Context, url, source string) 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} +// func (s *service) fetchLiveEvents(ctx context.Context, url, source string) 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 +// var wg sync.WaitGroup - for _, sportID := range sportIDs { - wg.Add(1) - go func(sportID int) { - defer wg.Done() +// for _, sportID := range sportIDs { +// wg.Add(1) +// go func(sportID int) { +// defer wg.Done() - url := fmt.Sprintf(url, 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() +// url := fmt.Sprintf(url, 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) +// body, _ := io.ReadAll(resp.Body) - events := []domain.Event{} - switch source { - case "bet365": - events = handleBet365prematch(body, sportID, source) - case "betfair": - events = handleBetfairprematch(body, sportID, source) - case "1xbet": - // betfair and 1xbet have the same result structure - events = handleBetfairprematch(body, sportID, source) - } +// events := []domain.Event{} +// switch source { +// case "bet365": +// events = handleBet365prematch(body, sportID, source) +// case "betfair": +// events = handleBetfairprematch(body, sportID, source) +// case "1xbet": +// // betfair and 1xbet have the same result structure +// events = handleBetfairprematch(body, sportID, source) +// } - for _, event := range events { - if err := s.store.SaveEvent(ctx, event); err != nil { - fmt.Printf("Could not store live event [id=%s]: %v\n", event.ID, err) - } - } - }(sportID) - } +// for _, event := range events { +// 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 +// wg.Wait() +// fmt.Println("All live events fetched and stored.") +// return nil -} +// } -func handleBet365prematch(body []byte, sportID int, source string) []domain.Event { - var data struct { - Success int `json:"success"` - Results [][]map[string]interface{} `json:"results"` - } +// func handleBet365prematch(body []byte, sportID int, source string) []domain.Event { +// var data struct { +// Success int `json:"success"` +// Results [][]map[string]interface{} `json:"results"` +// } - events := []domain.Event{} - if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { - fmt.Printf("%s: Decode failed for sport_id=%d\nRaw: %s\n", source, sportID, string(body)) - return events - } +// events := []domain.Event{} +// if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { +// fmt.Printf("%s: Decode failed for sport_id=%d\nRaw: %s\n", source, sportID, string(body)) +// return events +// } - for _, group := range data.Results { - for _, ev := range group { - if getString(ev["type"]) != "EV" { - continue - } +// for _, group := range data.Results { +// for _, ev := range group { +// if getString(ev["type"]) != "EV" { +// continue +// } - event := domain.Event{ - ID: getString(ev["ID"]), - SportID: int32(sportID), - MatchName: getString(ev["NA"]), - Score: getString(ev["SS"]), - MatchMinute: getInt(ev["TM"]), - TimerStatus: getString(ev["TT"]), - HomeTeamID: getInt32(ev["HT"]), - AwayTeamID: getInt32(ev["AT"]), - HomeKitImage: getString(ev["K1"]), - AwayKitImage: getString(ev["K2"]), - LeagueName: getString(ev["CT"]), - LeagueID: getInt32(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"]), - Source: source, - } +// event := domain.Event{ +// ID: getString(ev["ID"]), +// SportID: int32(sportID), +// MatchName: getString(ev["NA"]), +// Score: getString(ev["SS"]), +// MatchMinute: getInt(ev["TM"]), +// TimerStatus: getString(ev["TT"]), +// HomeTeamID: getInt32(ev["HT"]), +// AwayTeamID: getInt32(ev["AT"]), +// HomeKitImage: getString(ev["K1"]), +// AwayKitImage: getString(ev["K2"]), +// LeagueName: getString(ev["CT"]), +// LeagueID: getInt32(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"]), +// Source: source, +// } - events = append(events, event) - } - } +// events = append(events, event) +// } +// } - return events -} +// return events +// } -func handleBetfairprematch(body []byte, sportID int, source string) []domain.Event { - var data struct { - Success int `json:"success"` - Results []map[string]interface{} `json:"results"` - } +// func handleBetfairprematch(body []byte, sportID int, source string) []domain.Event { +// var data struct { +// Success int `json:"success"` +// Results []map[string]interface{} `json:"results"` +// } - events := []domain.Event{} - if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { - fmt.Printf("%s: Decode failed for sport_id=%d\nRaw: %s\n", source, sportID, string(body)) - return events - } +// events := []domain.Event{} +// if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { +// fmt.Printf("%s: Decode failed for sport_id=%d\nRaw: %s\n", source, sportID, string(body)) +// return events +// } - for _, ev := range data.Results { - homeRaw, _ := ev["home"].(map[string]interface{}) - awayRaw, _ := ev["home"].(map[string]interface{}) +// for _, ev := range data.Results { +// homeRaw, _ := ev["home"].(map[string]interface{}) +// awayRaw, _ := ev["home"].(map[string]interface{}) - event := domain.Event{ - ID: getString(ev["id"]), - SportID: int32(sportID), - TimerStatus: getString(ev["time_status"]), - HomeTeamID: getInt32(homeRaw["id"]), - AwayTeamID: getInt32(awayRaw["id"]), - StartTime: time.Now().UTC().Format(time.RFC3339), - IsLive: true, - Status: "live", - Source: source, - } +// event := domain.Event{ +// ID: getString(ev["id"]), +// SportID: int32(sportID), +// TimerStatus: getString(ev["time_status"]), +// HomeTeamID: getInt32(homeRaw["id"]), +// AwayTeamID: getInt32(awayRaw["id"]), +// StartTime: time.Now().UTC().Format(time.RFC3339), +// IsLive: true, +// Status: "live", +// Source: source, +// } - events = append(events, event) - } +// events = append(events, event) +// } - return events -} +// return events +// } func (s *service) FetchUpcomingEvents(ctx context.Context) error { var wg sync.WaitGroup urls := []struct { name string - source string + source domain.EventSource }{ - {"https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s&page=%d", "bet365"}, + {"https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s&page=%d", domain.EVENT_SOURCE_BET365}, // {"https://api.b365api.com/v1/betfair/sb/upcoming?sport_id=%d&token=%s&page=%d", "betfair"}, // {"https://api.b365api.com/v1/1xbet/upcoming?sport_id=%d&token=%s&page=%d", "1xbet"}, } @@ -205,38 +205,38 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { return nil } -func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_url, source string) { +func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_url string, source domain.EventSource) { + const pageLimit int = 200 sportIDs := []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91} // sportIDs := []int{1} - // TODO: Add the league skipping again when we have dynamic leagues - // b, err := os.OpenFile("logs/skipped_leagues.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - // if err != nil { - // log.Printf("❌ Failed to open leagues file %v", err) - // return - // } + for sportIndex, sportID := range sportIDs { var totalPages int = 1 var page int = 0 - var limit int = 200 var count int = 0 var skippedLeague []string var totalEvents = 0 + logger := s.mongoLogger.With( + zap.String("source", string(source)), + zap.Int("sport_id", sportID), + zap.String("sport_name", domain.Sport(sportID).String()), + zap.Int("count", count), + zap.Int("totalEvents", totalEvents), + zap.Int("Skipped leagues", len(skippedLeague)), + ) for page <= totalPages { page = page + 1 url := fmt.Sprintf(source_url, sportID, s.token, page) log.Printf("📡 Fetching data from %s - sport %d (%d/%d), for event data page (%d/%d)", source, sportID, sportIndex+1, len(sportIDs), page, totalPages) + eventLogger := logger.With( + zap.Int("page", page), + zap.Int("total_pages", totalPages), + ) resp, err := http.Get(url) if err != nil { - s.mongoLogger.Error( - "Failed to fetch event data for page", - zap.String("source", source), - zap.Int("sport_id", sportID), - zap.Int("page", page), - zap.Int("total_pages", totalPages), - zap.Error(err), - ) + eventLogger.Error("Failed to fetch event data for page", zap.Error(err)) continue } defer resp.Body.Close() @@ -244,56 +244,31 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_ur body, err := io.ReadAll(resp.Body) if err != nil { - s.mongoLogger.Error( - "Failed to read event response body", - zap.String("source", source), - zap.Int("sport_id", sportID), - zap.Int("page", page), - zap.Int("total_pages", totalPages), - zap.Error(err), - ) + eventLogger.Error("Failed to read event response body", zap.Error(err)) continue } - var data domain.BetResult + var data domain.B365UpcomingRes if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { - s.mongoLogger.Error( - "Failed to parse event json data", - zap.String("source", source), - zap.Int("sport_id", sportID), - zap.Int("page", page), - zap.Int("total_pages", totalPages), - zap.Error(err), - ) + eventLogger.Error("Failed to parse event json data", zap.Error(err)) continue } for _, ev := range data.Results { startUnix, err := strconv.ParseInt(ev.Time, 10, 64) + dataLogger := eventLogger.With( + zap.String("time", ev.Time), + zap.String("leagueID", ev.League.ID), + zap.String("leagueName", ev.League.Name), + ) if err != nil { - s.mongoLogger.Error( - "Invalid time", - zap.String("time", ev.Time), - zap.String("source", source), - zap.Int("sport_id", sportID), - zap.Int("page", page), - zap.Int("total_pages", totalPages), - zap.Error(err), - ) + dataLogger.Error("Invalid time", zap.Error(err)) continue } leagueID, err := strconv.ParseInt(ev.League.ID, 10, 64) if err != nil { - s.mongoLogger.Error( - "Invalid league id", - zap.String("leagueID", ev.League.ID), - zap.String("source", source), - zap.Int("sport_id", sportID), - zap.Int("page", page), - zap.Int("total_pages", totalPages), - zap.Error(err), - ) + dataLogger.Error("Invalid league id", zap.Error(err)) continue } @@ -301,87 +276,67 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_ur // no this its fine to keep it here // but change the league id to bet365 id later //Automatically feature the league if its in the list - err = s.store.SaveLeague(ctx, domain.League{ - ID: leagueID, - Name: ev.League.Name, - IsActive: true, - IsFeatured: slices.Contains(domain.FeaturedLeagues, leagueID), - SportID: convertInt32(ev.SportID), + err = s.store.SaveLeague(ctx, domain.CreateLeague{ + ID: leagueID, + Name: ev.League.Name, + DefaultIsActive: true, + DefaultIsFeatured: slices.Contains(domain.FeaturedLeagues, leagueID), + SportID: convertInt32(ev.SportID), }) if err != nil { - s.mongoLogger.Error( - "error while saving league", - zap.String("leagueID", ev.League.ID), - zap.String("leagueName", ev.League.Name), - zap.String("source", source), - zap.Int("sport_id", sportID), - zap.Int("page", page), - zap.Int("total_pages", totalPages), - zap.Error(err), - ) + dataLogger.Error("error while saving league", zap.Error(err)) continue } - if supported, err := s.store.CheckLeagueSupport(ctx, leagueID); !supported || err != nil { - s.mongoLogger.Warn( - "Skipping league", - zap.String("league", ev.League.Name), - zap.Bool("is_supported", supported), - zap.Error(err), - ) - skippedLeague = append(skippedLeague, ev.League.Name) - continue - } + // Since the system is multi-vendor now, no events are going to be skipped + // if supported, err := s.store.CheckLeagueSupport(ctx, leagueID); !supported || err != nil { + // dataLogger.Warn( + // "Skipping league", + // zap.Bool("is_supported", supported), + // zap.Error(err), + // ) + // skippedLeague = append(skippedLeague, ev.League.Name) + // continue + // } - event := domain.UpcomingEvent{ - ID: ev.ID, - SportID: convertInt32(ev.SportID), - MatchName: "", - HomeTeam: ev.Home.Name, - AwayTeam: "", // handle nil safely - HomeTeamID: convertInt32(ev.Home.ID), - AwayTeamID: 0, - HomeKitImage: "", - AwayKitImage: "", - LeagueID: convertInt32(ev.League.ID), - LeagueName: ev.League.Name, - LeagueCC: "", - StartTime: time.Unix(startUnix, 0).UTC(), - Source: source, + event := domain.CreateEvent{ + ID: ev.ID, + SportID: convertInt32(ev.SportID), + MatchName: "", + HomeTeam: ev.Home.Name, + AwayTeam: "", // handle nil safely + HomeTeamID: convertInt64(ev.Home.ID), + AwayTeamID: 0, + HomeTeamImage: "", + AwayTeamImage: "", + LeagueID: convertInt64(ev.League.ID), + LeagueName: ev.League.Name, + StartTime: time.Unix(startUnix, 0).UTC(), + Source: source, + IsLive: false, + Status: domain.STATUS_PENDING, } if ev.Away != nil { + dataLogger.Info("event away is empty") event.AwayTeam = ev.Away.Name - event.AwayTeamID = convertInt32(ev.Away.ID) + event.AwayTeamID = convertInt64(ev.Away.ID) event.MatchName = ev.Home.Name + " vs " + ev.Away.Name } + ok, err := s.CheckAndInsertEventHistory(ctx, event) - if err := s.CheckAndInsertEventHistory(ctx, event); err != nil { - s.mongoLogger.Error( - "failed to check and insert event history", - zap.String("leagueID", ev.League.ID), - zap.String("leagueName", ev.League.Name), - zap.String("source", source), - zap.Int("sport_id", sportID), - zap.Int("page", page), - zap.Int("total_pages", totalPages), - zap.Error(err), - ) + if err != nil { + dataLogger.Error("failed to check and insert event history", zap.Error(err)) } - err = s.store.SaveUpcomingEvent(ctx, event) + if ok { + dataLogger.Info("event history has been recorded") + } + + err = s.store.SaveEvent(ctx, event) if err != nil { - s.mongoLogger.Error( - "failed to save upcoming event", - zap.String("leagueID", ev.League.ID), - zap.String("leagueName", ev.League.Name), - zap.String("source", source), - zap.Int("sport_id", sportID), - zap.Int("page", page), - zap.Int("total_pages", totalPages), - zap.Error(err), - ) + dataLogger.Error("failed to save upcoming event", zap.Error(err)) } totalEvents += 1 } @@ -391,7 +346,7 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_ur totalPages = data.Pager.Total / data.Pager.PerPage - if count >= limit { + if count >= pageLimit { break } if page > totalPages { @@ -401,7 +356,7 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_ur } s.mongoLogger.Info( "Successfully fetched upcoming events", - zap.String("source", source), + zap.String("source", string(source)), zap.Int("totalEvents", totalEvents), zap.Int("sport_id", sportID), zap.String("sport_name", domain.Sport(sportID).String()), @@ -413,35 +368,30 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_ur } } -func (s *service) CheckAndInsertEventHistory(ctx context.Context, event domain.UpcomingEvent) error { +func (s *service) CheckAndInsertEventHistory(ctx context.Context, event domain.CreateEvent) (bool, error) { isEventMonitored, err := s.store.IsEventMonitored(ctx, event.ID) + eventLogger := s.mongoLogger.With( + zap.String("eventID", event.ID), + zap.Int64("leagueID", event.LeagueID), + zap.String("leagueName", event.LeagueName), + zap.Int32("sport_id", event.SportID), + ) + if err != nil { - s.mongoLogger.Error( - "failed to get event is_monitored", - zap.String("eventID", event.ID), - zap.Int32("leagueID", event.LeagueID), - zap.String("leagueName", event.LeagueName), - zap.Int32("sport_id", event.SportID), - zap.Error(err), - ) + eventLogger.Error("failed to get event is_monitored", zap.Error(err)) + return false, err } if !isEventMonitored { - return nil + return false, nil } oldEvent, err := s.GetUpcomingEventByID(ctx, event.ID) if err != nil { - s.mongoLogger.Error( - "failed to get event by id", - zap.String("eventID", event.ID), - zap.Int32("leagueID", event.LeagueID), - zap.String("leagueName", event.LeagueName), - zap.Int32("sport_id", event.SportID), - zap.Error(err), - ) + eventLogger.Error("failed to get event by id", zap.Error(err)) + return false, err } if oldEvent.Status != event.Status { @@ -451,20 +401,14 @@ func (s *service) CheckAndInsertEventHistory(ctx context.Context, event domain.U }) if err != nil { - s.mongoLogger.Error( - "failed to get event by id", - zap.String("eventID", event.ID), - zap.Int32("leagueID", event.LeagueID), - zap.String("leagueName", event.LeagueName), - zap.Int32("sport_id", event.SportID), - zap.Error(err), - ) - - return err + eventLogger.Error("failed to get event by id", zap.Error(err)) + return false, err } + + return true, nil } - return nil + return false, nil } func getString(v interface{}) string { @@ -494,19 +438,25 @@ func convertInt32(num string) int32 { } return 0 } -func (s *service) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) { +func convertInt64(num string) int64 { + if n, err := strconv.Atoi(num); err == nil { + return int64(n) + } + return 0 +} +func (s *service) GetAllUpcomingEvents(ctx context.Context) ([]domain.BaseEvent, error) { return s.store.GetAllUpcomingEvents(ctx) } -func (s *service) GetExpiredUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, error) { +func (s *service) GetExpiredUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.BaseEvent, error) { return s.store.GetExpiredUpcomingEvents(ctx, filter) } -func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, int64, error) { +func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.BaseEvent, int64, error) { return s.store.GetPaginatedUpcomingEvents(ctx, filter) } -func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) { +func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.BaseEvent, error) { return s.store.GetUpcomingEventByID(ctx, ID) } @@ -517,48 +467,20 @@ func (s *service) UpdateEventStatus(ctx context.Context, eventID string, status return s.store.UpdateEventStatus(ctx, eventID, status) } -func (s *service) UpdateEventFeatured(ctx context.Context, eventID string, flagged bool) error { - return s.store.UpdateEventFeatured(ctx, eventID, flagged) -} - func (s *service) IsEventMonitored(ctx context.Context, eventID string) (bool, error) { return s.store.IsEventMonitored(ctx, eventID) } func (s *service) UpdateEventMonitored(ctx context.Context, eventID string, IsMonitored bool) error { - return s.store.UpdateEventFeatured(ctx, eventID, IsMonitored) + return s.store.UpdateEventMonitored(ctx, eventID, IsMonitored) } -// func (s *service) GetAndStoreMatchResult(ctx context.Context, eventID string) error { -// url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%s", s.token, eventID) +func (s *service) GetEventsWithSettings(ctx context.Context, companyID int64, filter domain.EventFilter) ([]domain.EventWithSettings, int64, error) { + return s.store.GetEventsWithSettings(ctx, companyID, filter) +} -// resp, err := http.Get(url) -// if err != nil { -// return fmt.Errorf("failed to fetch result: %w", err) -// } -// defer resp.Body.Close() - -// body, _ := io.ReadAll(resp.Body) - -// // Parse the API response -// var apiResp struct { -// Results []struct { -// ID string `json:"id"` -// Ss string `json:"ss"` // Full-time score -// Status string `json:"time_status"` -// } `json:"results"` -// } - -// err = json.Unmarshal(body, &apiResp) -// if err != nil || len(apiResp.Results) == 0 { -// return fmt.Errorf("invalid response or no results found") -// } - -// result := apiResp.Results[0] - -// err = s.store.UpdateFinalScore(ctx, result.ID, result.Ss, result.Status) -// if err != nil { -// return fmt.Errorf("failed to update final score in database: %w", err) -// } - -// return nil -// } +func (s *service) GetEventWithSettingByID(ctx context.Context, ID string, companyID int64) (domain.EventWithSettings, error) { + return s.store.GetEventWithSettingByID(ctx, ID, companyID) +} +func (s *service) UpdateEventSettings(ctx context.Context, event domain.CreateEventSettings) error { + return s.store.UpdateEventSettings(ctx, event) +} diff --git a/internal/services/league/port.go b/internal/services/league/port.go index 1f49632..b203c4f 100644 --- a/internal/services/league/port.go +++ b/internal/services/league/port.go @@ -7,9 +7,10 @@ import ( ) type Service interface { - SaveLeague(ctx context.Context, l domain.League) error - GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.League, error) - GetFeaturedLeagues(ctx context.Context) ([]domain.League, error) - SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error + SaveLeague(ctx context.Context, league domain.CreateLeague) error + SaveLeagueSettings(ctx context.Context, leagueSettings domain.CreateLeagueSettings) error + GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.BaseLeague, error) + GetAllLeaguesByCompany(ctx context.Context, companyID int64, filter domain.LeagueFilter) ([]domain.LeagueWithSettings, error) + CheckLeagueSupport(ctx context.Context, leagueID int64, companyID int64) (bool, error) UpdateLeague(ctx context.Context, league domain.UpdateLeague) error } diff --git a/internal/services/league/service.go b/internal/services/league/service.go index 275dfb5..d82b38f 100644 --- a/internal/services/league/service.go +++ b/internal/services/league/service.go @@ -17,20 +17,24 @@ func New(store *repository.Store) Service { } } -func (s *service) SaveLeague(ctx context.Context, l domain.League) error { - return s.store.SaveLeague(ctx, l) +func (s *service) SaveLeague(ctx context.Context, league domain.CreateLeague) error { + return s.store.SaveLeague(ctx, league) } -func (s *service) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.League, error) { +func (s *service) SaveLeagueSettings(ctx context.Context, leagueSettings domain.CreateLeagueSettings) error { + return s.store.SaveLeagueSettings(ctx, leagueSettings) +} + +func (s *service) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.BaseLeague, error) { return s.store.GetAllLeagues(ctx, filter) } -func (s *service) GetFeaturedLeagues(ctx context.Context) ([]domain.League, error) { - return s.store.GetFeaturedLeagues(ctx) +func (s *service) GetAllLeaguesByCompany(ctx context.Context, companyID int64, filter domain.LeagueFilter) ([]domain.LeagueWithSettings, error) { + return s.store.GetAllLeaguesByCompany(ctx, companyID, filter) } -func (s *service) SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error { - return s.store.SetLeagueActive(ctx, leagueId, isActive) +func (s *service) CheckLeagueSupport(ctx context.Context, leagueID int64, companyID int64) (bool, error) { + return s.store.CheckLeagueSupport(ctx, leagueID, companyID) } func (s *service) UpdateLeague(ctx context.Context, league domain.UpdateLeague) error { diff --git a/internal/services/odds/custom_odds.go b/internal/services/odds/custom_odds.go new file mode 100644 index 0000000..f3c7d5d --- /dev/null +++ b/internal/services/odds/custom_odds.go @@ -0,0 +1,29 @@ +package odds + +// import ( +// "context" + +// "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +// ) + +// func (s *ServiceImpl) InsertCustomOdds(ctx context.Context, odd domain.CreateCustomOdd) (domain.CustomOdd, error) { +// return s.store.InsertCustomOdds(ctx, odd) +// } +// func (s *ServiceImpl) GetAllCustomOdds(ctx context.Context, filter domain.CustomOddFilter) ([]domain.CustomOdd, error){ +// return s.store.GetAllCustomOdds(ctx, filter) +// } +// func (s *ServiceImpl) GetCustomOddByID(ctx context.Context, id int64) (domain.CustomOdd, error){ +// return s.store.GetCustomOddByID(ctx, id) +// } +// func (s *ServiceImpl) GetCustomOddByOddID(ctx context.Context, oddId int64, companyID int64) (domain.CustomOdd, error){ +// return s.store.GetCustomOddByOddID(ctx, oddId, companyID) +// } +// func (s *ServiceImpl) DeleteCustomOddByID(ctx context.Context, id int64) error{ +// return s.store.DeleteCustomOddByID(ctx, id) +// } +// func (s *ServiceImpl) DeleteCustomOddsByOddID(ctx context.Context, oddId int64, companyID int64) error{ +// return s.store.DeleteCustomOddsByOddID(ctx, oddId, companyID) +// } +// func (s *ServiceImpl) DeleteCustomOddByEventID(ctx context.Context, eventID string) error{ +// return s.store.DeleteCustomOddByEventID(ctx, eventID) +// } diff --git a/internal/services/odds/disabled_odd.go b/internal/services/odds/disabled_odd.go new file mode 100644 index 0000000..cd3dd06 --- /dev/null +++ b/internal/services/odds/disabled_odd.go @@ -0,0 +1,28 @@ +package odds + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +func (s *ServiceImpl) InsertDisabledOdd(ctx context.Context, odd domain.CreateDisabledOdd) (domain.DisabledOdd, error) { + return s.store.InsertDisabledOdd(ctx, odd) +} + +func (s *ServiceImpl) GetAllDisabledOdds(ctx context.Context) ([]domain.DisabledOdd, error) { + return s.store.GetAllDisabledOdds(ctx) +} +func (s *ServiceImpl) GetDisabledOddByRawOddID(ctx context.Context, rawOddID int64) (domain.DisabledOdd, error) { + return s.store.GetDisabledOddByRawOddID(ctx, rawOddID) +} +func (s *ServiceImpl) GetDisabledOddByID(ctx context.Context, id int64) (domain.DisabledOdd, error) { + return s.store.GetDisabledOddByID(ctx, id) +} +func (s *ServiceImpl) DeleteDisabledOddsByID(ctx context.Context, id int64) error { + return s.store.DeleteDisabledOddsByID(ctx, id) +} +func (s *ServiceImpl) DeleteDisabledOddsByRawOddID(ctx context.Context, id int64) error { + return s.store.DeleteDisabledOddsByRawOddID(ctx, id) +} + diff --git a/internal/services/odds/odds_history.go b/internal/services/odds/odds_history.go new file mode 100644 index 0000000..48b8421 --- /dev/null +++ b/internal/services/odds/odds_history.go @@ -0,0 +1,15 @@ +package odds + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +func (s *ServiceImpl) GetAllOddHistory(ctx context.Context, filter domain.OddHistoryFilter) ([]domain.OddHistory, error) { + return s.store.GetAllOddHistory(ctx, filter) +} + +func (s *ServiceImpl) GetInitialOddPerDay(ctx context.Context, filter domain.OddHistoryFilter) ([]domain.OddHistory, error) { + return s.store.GetInitialOddPerDay(ctx, filter) +} \ No newline at end of file diff --git a/internal/services/odds/port.go b/internal/services/odds/port.go index 7387b4d..6014ed0 100644 --- a/internal/services/odds/port.go +++ b/internal/services/odds/port.go @@ -11,13 +11,32 @@ type Service interface { FetchNonLiveOdds(ctx context.Context) error FetchNonLiveOddsByEventID(ctx context.Context, eventIDStr string) (domain.BaseNonLiveOddResponse, error) ParseOddSections(ctx context.Context, res json.RawMessage, sportID int64) (domain.ParseOddSectionsRes, error) - GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) - GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string) ([]domain.Odd, error) - GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit domain.ValidInt64, offset domain.ValidInt64) ([]domain.Odd, error) - GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error) + GetPrematchOdds(ctx context.Context, eventID string) ([]domain.OddMarket, error) + GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string) ([]domain.OddMarket, error) + GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit domain.ValidInt64, offset domain.ValidInt64) ([]domain.OddMarket, error) + GetALLPrematchOdds(ctx context.Context) ([]domain.OddMarket, error) GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) (domain.RawOddsByMarketID, error) DeleteOddsForEvent(ctx context.Context, eventID string) error + + // Odd History InsertOddHistory(ctx context.Context, odd domain.CreateOddHistory) (domain.OddHistory, error) GetAllOddHistory(ctx context.Context, filter domain.OddHistoryFilter) ([]domain.OddHistory, error) GetInitialOddPerDay(ctx context.Context, filter domain.OddHistoryFilter) ([]domain.OddHistory, error) + + // Disabling Odds + InsertDisabledOdd(ctx context.Context, odd domain.CreateDisabledOdd) (domain.DisabledOdd, error) + GetAllDisabledOdds(ctx context.Context) ([]domain.DisabledOdd, error) + GetDisabledOddByRawOddID(ctx context.Context, rawOddID int64) (domain.DisabledOdd, error) + GetDisabledOddByID(ctx context.Context, id int64) (domain.DisabledOdd, error) + DeleteDisabledOddsByID(ctx context.Context, id int64) error + DeleteDisabledOddsByRawOddID(ctx context.Context, id int64) error + + // Custom Odds + // InsertCustomOdds(ctx context.Context, odd domain.CreateCustomOdd) (domain.CustomOdd, error) + // GetAllCustomOdds(ctx context.Context, filter domain.CustomOddFilter) ([]domain.CustomOdd, error) + // GetCustomOddByID(ctx context.Context, id int64) (domain.CustomOdd, error) + // GetCustomOddByOddID(ctx context.Context, oddId int64, companyID int64) (domain.CustomOdd, error) + // DeleteCustomOddByID(ctx context.Context, id int64) error + // DeleteCustomOddsByOddID(ctx context.Context, oddId int64, companyID int64) error + // DeleteCustomOddByEventID(ctx context.Context, eventID string) error } diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 178da5b..afa9e11 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -164,115 +164,117 @@ func (s *ServiceImpl) fetchBet365Odds(ctx context.Context) error { return nil } -func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error { - // getting odds for a specific event is not possible for bwin, most specific we can get is fetch odds on a single sport - // so instead of having event and odds fetched separetly event will also be fetched along with the odds - sportIds := []int{4, 12, 7} - for _, sportId := range sportIds { - url := fmt.Sprintf("https://api.b365api.com/v1/bwin/prematch?sport_id=%d&token=%s", sportId, s.config.Bet365Token) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - s.mongoLogger.Error( - "Failed to create request for sportId", - zap.Int("sportID", sportId), - zap.Error(err), - ) - continue - } +// func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error { +// // getting odds for a specific event is not possible for bwin, most specific we can get is fetch odds on a single sport +// // so instead of having event and odds fetched separetly event will also be fetched along with the odds +// sportIds := []int{4, 12, 7} +// for _, sportId := range sportIds { +// url := fmt.Sprintf("https://api.b365api.com/v1/bwin/prematch?sport_id=%d&token=%s", sportId, s.config.Bet365Token) +// req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) +// if err != nil { +// s.mongoLogger.Error( +// "Failed to create request for sportId", +// zap.Int("sportID", sportId), +// zap.Error(err), +// ) +// continue +// } - resp, err := s.client.Do(req) - if err != nil { - s.mongoLogger.Error( - "Failed to fetch request for sportId", - zap.Int("sportID", sportId), - zap.Error(err), - ) - continue - } - defer resp.Body.Close() +// resp, err := s.client.Do(req) +// if err != nil { +// s.mongoLogger.Error( +// "Failed to fetch request for sportId", +// zap.Int("sportID", sportId), +// zap.Error(err), +// ) +// continue +// } +// defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - s.mongoLogger.Error( - "Failed to read response body for sportId", - zap.Int("sportID", sportId), - zap.Error(err), - ) - continue - } +// body, err := io.ReadAll(resp.Body) +// if err != nil { +// s.mongoLogger.Error( +// "Failed to read response body for sportId", +// zap.Int("sportID", sportId), +// zap.Error(err), +// ) +// continue +// } - var data struct { - Success int `json:"success"` - Results []map[string]interface{} `json:"results"` - } +// 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)) - s.mongoLogger.Error( - "Failed to decode BWin response body", - zap.Int("sportID", sportId), - zap.Error(err), - ) - continue - } +// 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)) +// s.mongoLogger.Error( +// "Failed to decode BWin response body", +// zap.Int("sportID", sportId), +// zap.Error(err), +// ) +// continue +// } - for _, res := range data.Results { - if getInt(res["Id"]) == -1 { - continue - } +// for _, res := range data.Results { +// if getInt(res["Id"]) == -1 { +// continue +// } - event := domain.Event{ - ID: strconv.Itoa(getInt(res["Id"])), - SportID: int32(getInt(res["SportId"])), - LeagueID: int32(getInt(res["LeagueId"])), - LeagueName: getString(res["Leaguename"]), - HomeTeam: getString(res["HomeTeam"]), - HomeTeamID: int32(getInt(res["HomeTeamId"])), - AwayTeam: getString(res["AwayTeam"]), - AwayTeamID: int32(getInt(res["AwayTeamId"])), - StartTime: time.Now().UTC().Format(time.RFC3339), - TimerStatus: "1", - IsLive: true, - Status: "live", - Source: "bwin", - } +// event := domain.CreateEvent{ +// ID: strconv.Itoa(getInt(res["Id"])), +// SportID: int32(getInt(res["SportId"])), +// LeagueID: int64(getInt(res["LeagueId"])), +// LeagueName: getString(res["Leaguename"]), +// HomeTeam: getString(res["HomeTeam"]), +// HomeTeamID: int64(getInt(res["HomeTeamId"])), +// AwayTeam: getString(res["AwayTeam"]), +// AwayTeamID: int64(getInt(res["AwayTeamId"])), +// StartTime: time.Now().UTC(), +// IsLive: true, +// Status: domain.STATUS_IN_PLAY, +// Source: domain.EVENT_SOURCE_BWIN, +// MatchName: "", +// HomeTeamImage: "", +// AwayTeamImage: "", +// } - if err := s.store.SaveEvent(ctx, event); err != nil { - fmt.Printf("Could not store live event [id=%s]: %v\n", event.ID, err) - s.mongoLogger.Error( - "Could not store live event", - zap.Int("sportID", sportId), - zap.String("eventID", event.ID), - zap.Error(err), - ) - continue - } +// if err := s.store.SaveEvent(ctx, event); err != nil { +// fmt.Printf("Could not store live event [id=%s]: %v\n", event.ID, err) +// s.mongoLogger.Error( +// "Could not store live event", +// zap.Int("sportID", sportId), +// zap.String("eventID", event.ID), +// zap.Error(err), +// ) +// continue +// } - for _, market := range []string{"Markets, optionMarkets"} { - for _, m := range getMapArray(res[market]) { - name := getMap(m["name"]) - marketName := getString(name["value"]) +// for _, market := range []string{"Markets, optionMarkets"} { +// for _, m := range getMapArray(res[market]) { +// name := getMap(m["name"]) +// marketName := getString(name["value"]) - market := domain.Market{ - EventID: event.ID, - MarketID: getString(m["id"]), - MarketCategory: getString(m["category"]), - MarketName: marketName, - Source: "bwin", - } +// market := domain.CreateOddMarket{ +// EventID: event.ID, +// MarketID: getString(m["id"]), +// MarketCategory: getString(m["category"]), +// MarketName: marketName, - results := getMapArray(m["results"]) - market.Odds = results +// } - s.store.SaveNonLiveMarket(ctx, market) +// results := getMapArray(m["results"]) +// market.Odds = results - } - } - } +// s.store.SaveOddMarket(ctx, market) - } - return nil -} +// } +// } +// } + +// } +// return nil +// } func (s *ServiceImpl) FetchNonLiveOddsByEventID(ctx context.Context, eventIDStr string) (domain.BaseNonLiveOddResponse, error) { @@ -545,7 +547,7 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName } // Check if the market id is a string - marketIDint := market.ID.Int64 + marketIDint := market.ID.Value // if err != nil { // s.mongoLogger.Error( // "Invalid market id", @@ -579,9 +581,8 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName continue } - marketRecord := domain.Market{ + marketRecord := domain.CreateOddMarket{ EventID: eventID, - FI: fi, MarketCategory: sectionName, MarketType: marketType, MarketName: market.Name, @@ -589,7 +590,6 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName UpdatedAt: updatedAt, Odds: marketOdds, // bwin won't reach this code so bet365 is hardcoded for now - Source: "bet365", } if err := s.CheckAndInsertOddHistory(ctx, marketRecord); err != nil { @@ -603,7 +603,7 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName continue } - err = s.store.SaveNonLiveMarket(ctx, marketRecord) + err = s.store.SaveOddMarket(ctx, marketRecord) if err != nil { s.mongoLogger.Error( "failed to save market", @@ -623,57 +623,38 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName return nil } -func (s *ServiceImpl) CheckAndInsertOddHistory(ctx context.Context, market domain.Market) error { +func (s *ServiceImpl) CheckAndInsertOddHistory(ctx context.Context, market domain.CreateOddMarket) error { isEventMonitored, err := s.eventSvc.IsEventMonitored(ctx, market.EventID) + marketLogger := s.mongoLogger.With( + zap.String("market_id", market.MarketID), + zap.String("market_name", market.MarketName), + zap.String("eventID", market.EventID), + ) if err != nil { - s.mongoLogger.Error( - "failed to get is_monitored", - zap.String("market_id", market.MarketID), - zap.String("market_name", market.Name), - zap.String("eventID", market.EventID), - zap.Error(err), - ) + marketLogger.Error("failed to get is_monitored", zap.Error(err)) } if !isEventMonitored { return nil } - oldOdds, err := s.store.GetRawOddsByMarketID(ctx, market.MarketID, market.EventID) + oldOdds, err := s.store.GetOddsByMarketID(ctx, market.MarketID, market.EventID) if err != nil { - s.mongoLogger.Error( - "failed to get raw odds by market id", - zap.String("market_id", market.MarketID), - zap.String("market_name", market.Name), - zap.String("eventID", market.EventID), - zap.Error(err), - ) + marketLogger.Error("failed to get raw odds by market id", zap.Error(err)) return err } if len(oldOdds.RawOdds) != len(market.Odds) { - s.mongoLogger.Error( - "new odds data does not match old odds data", - zap.String("market_id", market.MarketID), - zap.String("market_name", market.Name), - zap.String("eventID", market.EventID), - zap.Error(err), - ) + marketLogger.Error("new odds data does not match old odds data", zap.Error(err)) return fmt.Errorf("new odds data does not match old odds data") } oldRawOdds, err := convertRawMessage(oldOdds.RawOdds) if err != nil { - s.mongoLogger.Error( - "failed to convert raw odds to map", - zap.String("market_id", market.MarketID), - zap.String("market_name", market.Name), - zap.String("eventID", market.EventID), - zap.Error(err), - ) + marketLogger.Error("failed to convert raw odds to map", zap.Error(err)) return err } @@ -698,7 +679,7 @@ func (s *ServiceImpl) CheckAndInsertOddHistory(ctx context.Context, market domai s.mongoLogger.Error( "failed to insert odd history", zap.String("market_id", market.MarketID), - zap.String("market_name", market.Name), + zap.String("market_name", market.MarketName), zap.String("eventID", market.EventID), zap.Int64("odd_id", oldOdds.ID), zap.Int("raw_odd_id", newRawOddID), @@ -716,29 +697,37 @@ func (s *ServiceImpl) CheckAndInsertOddHistory(ctx context.Context, market domai return nil } -func (s *ServiceImpl) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) { - return s.store.GetPrematchOdds(ctx, eventID) +func (s *ServiceImpl) GetAllOdds(ctx context.Context, filter domain.OddMarketFilter) ([]domain.OddMarket, error) { + return s.store.GetAllOdds(ctx, filter) } -func (s *ServiceImpl) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error) { - return s.store.GetALLPrematchOdds(ctx) +func (s *ServiceImpl) GetAllOddsWithSettings(ctx context.Context, companyID int64, filter domain.OddMarketFilter) ([]domain.OddMarketWithSettings, error) { + return s.store.GetAllOddsWithSettings(ctx, companyID, filter) } -func (s *ServiceImpl) GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) (domain.RawOddsByMarketID, error) { - rows, err := s.store.GetRawOddsByMarketID(ctx, marketID, upcomingID) +func (s *ServiceImpl) GetOddsByMarketID(ctx context.Context, marketID string, eventID string) (domain.OddMarket, error) { + rows, err := s.store.GetOddsByMarketID(ctx, marketID, eventID) if err != nil { - return domain.RawOddsByMarketID{}, err + return domain.OddMarket{}, err + } + + return rows, nil +} +func (s *ServiceImpl) GetOddsWithSettingsByMarketID(ctx context.Context, marketID string, eventID string, companyID int64) (domain.OddMarketWithSettings, error) { + rows, err := s.store.GetOddsWithSettingsByMarketID(ctx, marketID, eventID, companyID) + if err != nil { + return domain.OddMarketWithSettings{}, err } return rows, nil } -func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string) ([]domain.Odd, error) { - return s.store.GetPrematchOddsByUpcomingID(ctx, upcomingID) +func (s *ServiceImpl) GetOddsByEventID(ctx context.Context, upcomingID string, filter domain.OddMarketWithEventFilter) ([]domain.OddMarket, error) { + return s.store.GetOddsByEventID(ctx, upcomingID, filter) } -func (s *ServiceImpl) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset domain.ValidInt32) ([]domain.Odd, error) { - return s.store.GetPaginatedPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset) +func (s *ServiceImpl) GetOddsWithSettingsByEventID(ctx context.Context, upcomingID string, companyID int64, filter domain.OddMarketFilter) ([]domain.OddMarketWithSettings, error) { + return s.store.GetOddsWithSettingsByEventID(ctx, upcomingID, companyID, filter) } func (s *ServiceImpl) DeleteOddsForEvent(ctx context.Context, eventID string) error { diff --git a/internal/services/result/service.go b/internal/services/result/service.go index cc2138a..ad1f1c5 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -70,89 +70,57 @@ var ( func (s *Service) UpdateResultForOutcomes(ctx context.Context, eventID int64, resultRes json.RawMessage, sportID int64) error { // TODO: Optimize this since there could be many outcomes with the same event_id and market_id that could be updated the same time outcomes, err := s.repo.GetBetOutcomeByEventID(ctx, eventID, true) + logger := s.mongoLogger.With( + zap.Int64("eventID", eventID), + zap.Int64("sportID", sportID), + ) + if err != nil { - s.mongoLogger.Error( - "Failed to get pending bet outcomes", - zap.Int64("eventID", eventID), - zap.Error(err), - ) + logger.Error("Failed to get pending bet outcomes", zap.Error(err)) + return fmt.Errorf("failed to get pending bet outcomes for event %d: %w", eventID, err) } - - // if len(outcomes) == 0 { - // s.mongoLogger.Info( - // "No bets have been placed on event", - // zap.Int64("eventID", eventID), - // ) - - // } for _, outcome := range outcomes { + outcomeLogger := logger.With( + zap.Int64("outcome_id", outcome.ID), + zap.Int32("outcome_status", int32(outcome.Status)), + zap.Int64("outcome_bet_id", outcome.BetID), + zap.String("outcome_market_id", outcome.MarketName), + ) + if outcome.Expires.After(time.Now()) { - s.mongoLogger.Warn( - "Outcome is not expired yet", - zap.Int64("eventID", eventID), - zap.Int64("outcome_id", outcome.ID), - zap.Error(err), - ) + outcomeLogger.Warn("Outcome is not expired yet", zap.Error(err)) return fmt.Errorf("Outcome has not expired yet") } parseResult, err := s.parseResult(resultRes, outcome, sportID) if err != nil { - s.mongoLogger.Error( - "Failed to parse result", - zap.Int64("eventID", eventID), - zap.Int64("outcome_id", outcome.ID), - zap.Error(err), - ) + outcomeLogger.Error("Failed to parse result", zap.Error(err)) return err } outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, parseResult.Status) if err != nil { - s.mongoLogger.Error( - "Failed to update bet outcome status", - zap.Int64("eventID", eventID), - zap.Int64("outcome_id", outcome.ID), - zap.Error(err), - ) + outcomeLogger.Error("Failed to update bet outcome status", zap.Error(err)) return err } if outcome.Status == domain.OUTCOME_STATUS_ERROR || outcome.Status == domain.OUTCOME_STATUS_PENDING { - s.mongoLogger.Error( - "Outcome has been updated to pending or error", - zap.Int64("eventID", eventID), - zap.Error(err), - ) + outcomeLogger.Error("Outcome has been updated to pending or error", zap.Error(err)) return fmt.Errorf("Error while updating outcome") } status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID) if err != nil { if err != bet.ErrOutcomesNotCompleted { - s.mongoLogger.Error( - "Failed to check bet outcome for bet", - zap.Int64("eventID", eventID), - zap.Int64("betID", outcome.BetID), - zap.Error(err), - ) + outcomeLogger.Error("Failed to check bet outcome for bet", zap.Error(err)) } return err } - s.mongoLogger.Info( - "Updating bet status", - zap.Int64("eventID", eventID), - zap.Int64("betID", outcome.BetID), - zap.String("status", status.String()), - zap.Int64("eventID", eventID), - ) + outcomeLogger.Info("Updating bet status", zap.String("status", status.String())) + err = s.betSvc.UpdateStatus(ctx, outcome.BetID, status) if err != nil { - s.mongoLogger.Error( - "Failed to update bet status", - zap.Int64("eventID", eventID), - zap.Error(err), - ) + outcomeLogger.Error("Failed to update bet status", zap.Error(err)) return err } } @@ -268,46 +236,32 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { var resultLog domain.CreateResultLog var resultStatusBets domain.ResultStatusBets for _, event := range events { - + eventLogger := s.mongoLogger.With( + zap.String("eventID", event.ID), + ) eventID, err := strconv.ParseInt(event.ID, 10, 64) if err != nil { - s.mongoLogger.Error( - "Failed to parse Event ID", - zap.String("eventID", event.ID), - zap.Error(err), - ) + eventLogger.Error("Failed to parse Event ID", zap.Error(err)) continue } result, err := s.fetchResult(ctx, eventID) if err != nil { if err == ErrEventIsNotActive { - s.mongoLogger.Warn( - "Event is not active", - zap.Int64("eventID", eventID), - zap.Error(err), - ) + eventLogger.Warn("Event is not active", zap.Error(err)) continue } - s.mongoLogger.Error( - "Failed to fetch result", - zap.Int64("eventID", eventID), - zap.Error(err), - ) + eventLogger.Error("Failed to fetch result", zap.Error(err)) continue } var commonResp domain.CommonResultResponse if err := json.Unmarshal(result.Results[0], &commonResp); err != nil { - s.mongoLogger.Error( - "Failed to unmarshal common result", - zap.Int64("eventID", eventID), - zap.Error(err), - ) + eventLogger.Error("Failed to unmarshal common result", zap.Error(err)) continue } - timeStatusParsed := commonResp.TimeStatus.Int64 + timeStatusParsed := commonResp.TimeStatus.Value // if err != nil { // s.mongoLogger.Error( // "Failed to parse time status", @@ -325,6 +279,10 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { // continue // } // Admin users will be able to review the events + commonRespLogger := eventLogger.With( + zap.Int64("parsed_time_status", timeStatusParsed), + zap.String("response_sport_id", commonResp.SportID), + ) switch timeStatusParsed { case int64(domain.TIME_STATUS_NOT_STARTED), int64(domain.TIME_STATUS_IN_PLAY): resultLog.StatusNotFinishedCount += 1 @@ -342,20 +300,12 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { err = s.repo.DeleteEvent(ctx, event.ID) if err != nil { - s.mongoLogger.Error( - "Failed to remove event", - zap.Int64("eventID", eventID), - zap.Error(err), - ) + commonRespLogger.Error("Failed to remove event", zap.Error(err)) continue } err = s.repo.DeleteOddsForEvent(ctx, event.ID) if err != nil { - s.mongoLogger.Error( - "Failed to remove odds for event", - zap.Int64("eventID", eventID), - zap.Error(err), - ) + commonRespLogger.Error("Failed to remove odds for event", zap.Error(err)) continue } resultLog.RemovedCount += 1 @@ -393,37 +343,21 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { } sportID, err := strconv.ParseInt(commonResp.SportID, 10, 64) if err != nil { - s.mongoLogger.Error( - "Failed to parse sport id", - zap.String("sportID", commonResp.SportID), - zap.Error(err), - ) + commonRespLogger.Error("Failed to parse sport id", zap.Error(err)) continue } err = s.UpdateResultForOutcomes(ctx, eventID, result.Results[0], sportID) if err != nil { - s.mongoLogger.Error( - "Error while updating result for event", - zap.Int64("eventID", eventID), - zap.Error(err), - ) + commonRespLogger.Error("Error while updating result for event", zap.Error(err)) } err = s.repo.DeleteEvent(ctx, event.ID) if err != nil { - s.mongoLogger.Error( - "Failed to remove event", - zap.Int64("eventID", eventID), - zap.Error(err), - ) + commonRespLogger.Error("Failed to remove event", zap.Error(err)) continue } err = s.repo.DeleteOddsForEvent(ctx, event.ID) if err != nil { - s.mongoLogger.Error( - "Failed to remove odds for event", - zap.Int64("eventID", eventID), - zap.Error(err), - ) + commonRespLogger.Error("Failed to remove odds for event", zap.Error(err)) continue } resultLog.RemovedCount += 1 @@ -452,20 +386,12 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { err = s.repo.DeleteEvent(ctx, event.ID) if err != nil { - s.mongoLogger.Error( - "Failed to remove event", - zap.Int64("eventID", eventID), - zap.Error(err), - ) + commonRespLogger.Error("Failed to remove event", zap.Error(err)) continue } err = s.repo.DeleteOddsForEvent(ctx, event.ID) if err != nil { - s.mongoLogger.Error( - "Failed to remove odds for event", - zap.Int64("eventID", eventID), - zap.Error(err), - ) + commonRespLogger.Error("Failed to remove odds for event", zap.Error(err)) continue } resultLog.RemovedCount += 1 @@ -718,7 +644,7 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error var eventStatus domain.EventStatus // TODO Change event status to int64 enum - timeStatus := commonResp.TimeStatus.Int64 + timeStatus := commonResp.TimeStatus.Value // if err != nil { // s.mongoLogger.Error( // "Invalid time status", @@ -958,7 +884,7 @@ func (s *Service) GetResultsForEvent(ctx context.Context, eventID string) (json. outcomes := make([]domain.BetOutcome, 0) for _, section := range parsedOddSections.Sections { for _, market := range section.Sp { - marketIDint := market.ID.Int64 + marketIDint := market.ID.Value // if err != nil { // s.mongoLogger.Error( // "Invalid market id", @@ -1135,147 +1061,80 @@ func (s *Service) parseResult(resultResp json.RawMessage, outcome domain.BetOutc var result domain.CreateResult var err error + logFields := []zap.Field{ + zap.Int64("event id", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("sport_id", sportID), + } switch sportID { case domain.FOOTBALL: result, err = s.parseFootball(resultResp, outcome) if err != nil { - s.mongoLogger.Error( - "Failed to parse football", - zap.Int64("event id", outcome.EventID), - zap.Int64("market_id", outcome.MarketID), - zap.Int64("sport_id", sportID), - zap.Error(err), - ) + s.mongoLogger.Error("Failed to parse football", append(logFields, zap.Error(err))...) return domain.CreateResult{}, err } case domain.BASKETBALL: result, err = s.parseBasketball(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.mongoLogger.Error( - "Failed to parse basketball", - zap.Int64("event id", outcome.EventID), - zap.Int64("market_id", outcome.MarketID), - zap.Int64("sport_id", sportID), - zap.Error(err), - ) + s.mongoLogger.Error("Failed to parse basketball", append(logFields, zap.Error(err))...) return domain.CreateResult{}, err } case domain.ICE_HOCKEY: result, err = s.parseIceHockey(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.mongoLogger.Error( - "Failed to parse ice hockey", - zap.Int64("event id", outcome.EventID), - zap.Int64("market_id", outcome.MarketID), - zap.Int64("sport_id", sportID), - zap.Error(err), - ) + s.mongoLogger.Error("Failed to parse ice hockey", append(logFields, zap.Error(err))...) return domain.CreateResult{}, err } case domain.CRICKET: result, err = s.parseCricket(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.mongoLogger.Error( - "Failed to parse cricket", - zap.Int64("event id", outcome.EventID), - zap.Int64("market_id", outcome.MarketID), - zap.Int64("sport_id", sportID), - zap.Error(err), - ) + s.mongoLogger.Error("Failed to parse cricket", append(logFields, zap.Error(err))...) return domain.CreateResult{}, err } case domain.VOLLEYBALL: result, err = s.parseVolleyball(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.mongoLogger.Error( - "Failed to parse volleyball", - zap.Int64("event id", outcome.EventID), - zap.Int64("market_id", outcome.MarketID), - zap.Int64("sport_id", sportID), - zap.Error(err), - ) + s.mongoLogger.Error("Failed to parse volleyball", append(logFields, zap.Error(err))...) return domain.CreateResult{}, err } case domain.DARTS: result, err = s.parseDarts(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.mongoLogger.Error( - "Failed to parse darts", - zap.Int64("event id", outcome.EventID), - zap.Int64("market_id", outcome.MarketID), - zap.Int64("sport_id", sportID), - zap.Error(err), - ) + s.mongoLogger.Error("Failed to parse darts", append(logFields, zap.Error(err))...) return domain.CreateResult{}, err } case domain.FUTSAL: result, err = s.parseFutsal(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.mongoLogger.Error( - "Failed to parse futsal", - zap.Int64("event id", outcome.EventID), - zap.Int64("market_id", outcome.MarketID), - zap.Int64("sport_id", sportID), - zap.Error(err), - ) + s.mongoLogger.Error("Failed to parse futsal", append(logFields, zap.Error(err))...) return domain.CreateResult{}, err } case domain.AMERICAN_FOOTBALL: result, err = s.parseNFL(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.mongoLogger.Error( - "Failed to parse american football", - zap.Int64("event id", outcome.EventID), - zap.Int64("market_id", outcome.MarketID), - zap.Int64("sport_id", sportID), - zap.Error(err), - ) + s.mongoLogger.Error("Failed to parse american football", append(logFields, zap.Error(err))...) return domain.CreateResult{}, err } case domain.RUGBY_UNION: result, err = s.parseRugbyUnion(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.mongoLogger.Error( - "Failed to parse rugby_union", - zap.Int64("event id", outcome.EventID), - zap.Int64("market_id", outcome.MarketID), - zap.Int64("sport_id", sportID), - zap.Error(err), - ) + s.mongoLogger.Error("Failed to parse rugby_union", append(logFields, zap.Error(err))...) return domain.CreateResult{}, err } case domain.RUGBY_LEAGUE: result, err = s.parseRugbyLeague(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.mongoLogger.Error( - "Failed to parse rugby_league", - zap.Int64("event id", outcome.EventID), - zap.Int64("market_id", outcome.MarketID), - zap.Int64("sport_id", sportID), - zap.Error(err), - ) + s.mongoLogger.Error("Failed to parse rugby_league", append(logFields, zap.Error(err))...) return domain.CreateResult{}, err } case domain.BASEBALL: result, err = s.parseBaseball(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.mongoLogger.Error( - "Failed to parse baseball", - zap.Int64("event id", outcome.EventID), - zap.Int64("market_id", outcome.MarketID), - zap.Int64("sport_id", sportID), - zap.Error(err), - ) + s.mongoLogger.Error("Failed to parse baseball", append(logFields, zap.Error(err))...) return domain.CreateResult{}, err } default: - s.mongoLogger.Error( - "Unsupported sport", - zap.Int64("event id", outcome.EventID), - zap.Int64("market_id", outcome.MarketID), - zap.Int64("sport_id", sportID), - zap.Error(err), - ) + s.mongoLogger.Error("Unsupported sport", append(logFields, zap.Error(err))...) return domain.CreateResult{}, fmt.Errorf("unsupported sport: %v", sportID) } diff --git a/internal/services/user/port.go b/internal/services/user/port.go index 0f2c6fa..8625f35 100644 --- a/internal/services/user/port.go +++ b/internal/services/user/port.go @@ -19,9 +19,9 @@ type UserStore interface { UpdateUserCompany(ctx context.Context, id int64, companyID int64) error UpdateUserSuspend(ctx context.Context, id int64, status bool) error DeleteUser(ctx context.Context, id int64) error - CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) - GetUserByEmail(ctx context.Context, email string) (domain.User, error) - GetUserByPhone(ctx context.Context, phoneNum string) (domain.User, error) + CheckPhoneEmailExist(ctx context.Context, phoneNum, email string, companyID domain.ValidInt64) (bool, bool, error) + GetUserByEmail(ctx context.Context, email string, companyID domain.ValidInt64) (domain.User, error) + GetUserByPhone(ctx context.Context, phoneNum string, companyID domain.ValidInt64) (domain.User, error) SearchUserByNameOrPhone(ctx context.Context, searchString string, role *domain.Role, companyID domain.ValidInt64) ([]domain.User, error) UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64) error // identifier verified email or phone diff --git a/internal/services/user/register.go b/internal/services/user/register.go index 4c8c003..d09a7a7 100644 --- a/internal/services/user/register.go +++ b/internal/services/user/register.go @@ -7,17 +7,17 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) -func (s *Service) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) { // email,phone,error - return s.userStore.CheckPhoneEmailExist(ctx, phoneNum, email) +func (s *Service) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string, companyID domain.ValidInt64) (bool, bool, error) { // email,phone,error + return s.userStore.CheckPhoneEmailExist(ctx, phoneNum, email, companyID) } -func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.SMSProvider) error { +func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.SMSProvider, companyID domain.ValidInt64) error { var err error // check if user exists switch medium { case domain.OtpMediumEmail: - _, err = s.userStore.GetUserByEmail(ctx, sentTo) + _, err = s.userStore.GetUserByEmail(ctx, sentTo, companyID) case domain.OtpMediumSms: - _, err = s.userStore.GetUserByPhone(ctx, sentTo) + _, err = s.userStore.GetUserByPhone(ctx, sentTo, companyID) } if err != nil && err != domain.ErrUserNotFound { @@ -68,6 +68,7 @@ func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterU Role: domain.RoleCustomer, EmailVerified: registerReq.OtpMedium == domain.OtpMediumEmail, PhoneVerified: registerReq.OtpMedium == domain.OtpMediumSms, + CompanyID: registerReq.CompanyID, } // create the user and mark otp as used user, err := s.userStore.CreateUser(ctx, userR, otp.ID, false) diff --git a/internal/services/user/reset.go b/internal/services/user/reset.go index 8834cb0..0b5cc0f 100644 --- a/internal/services/user/reset.go +++ b/internal/services/user/reset.go @@ -8,15 +8,15 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) -func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.SMSProvider) error { +func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.SMSProvider, companyID domain.ValidInt64) error { var err error // check if user exists switch medium { case domain.OtpMediumEmail: - _, err = s.userStore.GetUserByEmail(ctx, sentTo) + _, err = s.userStore.GetUserByEmail(ctx, sentTo, companyID) case domain.OtpMediumSms: - _, err = s.userStore.GetUserByPhone(ctx, sentTo) + _, err = s.userStore.GetUserByPhone(ctx, sentTo, companyID) } if err != nil { diff --git a/internal/web_server/handlers/auth_handler.go b/internal/web_server/handlers/auth_handler.go index 6d92b98..a5f215e 100644 --- a/internal/web_server/handlers/auth_handler.go +++ b/internal/web_server/handlers/auth_handler.go @@ -38,8 +38,13 @@ type loginCustomerRes struct { // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/auth/customer-login [post] +// @Router /api/v1/{tenant_slug}/customer-login [post] func (h *Handler) LoginCustomer(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + domain.BadRequestLogger.Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } var req loginCustomerReq if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Info("Failed to parse LoginCustomer request", @@ -58,7 +63,8 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, errMsg) } - successRes, err := h.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password) + successRes, err := h.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password, companyID) + if err != nil { switch { case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound): @@ -153,8 +159,13 @@ type loginAdminRes struct { // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/auth/admin-login [post] +// @Router /api/v1/{tenant_slug}/admin-login [post] func (h *Handler) LoginAdmin(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + domain.BadRequestLogger.Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } var req loginAdminReq if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Info("Failed to parse LoginAdmin request", @@ -173,7 +184,7 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, errMsg) } - successRes, err := h.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password) + successRes, err := h.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password, companyID) if err != nil { switch { case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound): @@ -243,6 +254,107 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Login successful", res, nil) } +// LoginSuper godoc +// @Summary Login super-admin +// @Description Login super-admin +// @Tags auth +// @Accept json +// @Produce json +// @Param login body loginAdminReq true "Login super-admin" +// @Success 200 {object} loginAdminRes +// @Failure 400 {object} response.APIResponse +// @Failure 401 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/super-login [post] +func (h *Handler) LoginSuper(c *fiber.Ctx) error { + var req loginAdminReq + if err := c.BodyParser(&req); err != nil { + h.mongoLoggerSvc.Info("Failed to parse LoginAdmin request", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body"+err.Error()) + } + + if valErrs, ok := h.validator.Validate(c, req); !ok { + var errMsg string + for field, msg := range valErrs { + errMsg += fmt.Sprintf("%s: %s; ", field, msg) + } + return fiber.NewError(fiber.StatusBadRequest, errMsg) + } + + successRes, err := h.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password, domain.ValidInt64{}) + if err != nil { + switch { + case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound): + h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials", + zap.Int("status_code", fiber.StatusUnauthorized), + zap.String("email", req.Email), + zap.String("phone", req.PhoneNumber), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusUnauthorized, "Invalid credentials") + case errors.Is(err, authentication.ErrUserSuspended): + h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked", + zap.Int("status_code", fiber.StatusUnauthorized), + zap.String("email", req.Email), + zap.String("phone", req.PhoneNumber), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusUnauthorized, "User login has been locked") + default: + h.mongoLoggerSvc.Error("Login failed", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Internal server error") + } + } + + if successRes.Role != domain.RoleSuperAdmin { + h.mongoLoggerSvc.Warn("Login attempt: super-admin login of non-super-admin", + zap.Int("status_code", fiber.StatusForbidden), + zap.String("role", string(successRes.Role)), + zap.String("email", req.Email), + zap.String("phone", req.PhoneNumber), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusForbidden, "Only admin roles are allowed") + } + + accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, successRes.CompanyID, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry) + if err != nil { + h.mongoLoggerSvc.Error("Failed to create access token", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Int64("user_id", successRes.UserId), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token") + } + + res := loginCustomerRes{ + AccessToken: accessToken, + RefreshToken: successRes.RfToken, + Role: string(successRes.Role), + } + + h.mongoLoggerSvc.Info("Login successful", + zap.Int("status_code", fiber.StatusOK), + zap.Int64("user_id", successRes.UserId), + zap.String("role", string(successRes.Role)), + zap.Time("timestamp", time.Now()), + ) + + return response.WriteJSON(c, fiber.StatusOK, "Login successful", res, nil) +} + type refreshToken struct { AccessToken string `json:"access_token" validate:"required" example:""` RefreshToken string `json:"refresh_token" validate:"required" example:""` diff --git a/internal/web_server/handlers/event_handler.go b/internal/web_server/handlers/event_handler.go index 2f8fe93..e503b29 100644 --- a/internal/web_server/handlers/event_handler.go +++ b/internal/web_server/handlers/event_handler.go @@ -29,30 +29,28 @@ import ( func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { page := c.QueryInt("page", 1) pageSize := c.QueryInt("page_size", 10) - limit := domain.ValidInt64{ - Value: int64(pageSize), + limit := domain.ValidInt32{ + Value: int32(pageSize), Valid: true, } - offset := domain.ValidInt64{ - Value: int64(page - 1), + offset := domain.ValidInt32{ + Value: int32(page - 1), Valid: true, } leagueIDQuery := c.Query("league_id") - var leagueID domain.ValidInt32 + var leagueID domain.ValidInt64 if leagueIDQuery != "" { - leagueIDInt, err := strconv.Atoi(leagueIDQuery) + leagueIDInt, err := strconv.ParseInt(leagueIDQuery, 10, 64) if err != nil { - h.mongoLoggerSvc.Error("invalid league id", + domain.BadRequestLogger.Error("invalid league id", zap.String("league_id", leagueIDQuery), - zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), - zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "invalid league id") } - leagueID = domain.ValidInt32{ - Value: int32(leagueIDInt), + leagueID = domain.ValidInt64{ + Value: leagueIDInt, Valid: true, } } @@ -61,11 +59,9 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { if sportIDQuery != "" { sportIDint, err := strconv.Atoi(sportIDQuery) if err != nil { - h.mongoLoggerSvc.Info("invalid sport id", + domain.BadRequestLogger.Info("invalid sport id", zap.String("sportID", sportIDQuery), - zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), - zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "invalid sport id") } @@ -86,11 +82,9 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { if firstStartTimeQuery != "" { firstStartTimeParsed, err := time.Parse(time.RFC3339, firstStartTimeQuery) if err != nil { - h.mongoLoggerSvc.Info("invalid start_time format", + domain.BadRequestLogger.Info("invalid start_time format", zap.String("first_start_time", firstStartTimeQuery), - zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), - zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid start_time format") } @@ -105,11 +99,9 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { if lastStartTimeQuery != "" { lastStartTimeParsed, err := time.Parse(time.RFC3339, lastStartTimeQuery) if err != nil { - h.mongoLoggerSvc.Info("invalid last_start_time format", + domain.BadRequestLogger.Info("invalid last_start_time format", zap.String("last_start_time", lastStartTimeQuery), - zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), - zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid start_time format") } @@ -130,10 +122,9 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { if isFeaturedQuery != "" { isFeaturedParsed, err := strconv.ParseBool(isFeaturedQuery) if err != nil { - h.mongoLoggerSvc.Error("Failed to parse isFeatured", - zap.Int("status_code", fiber.StatusBadRequest), + domain.BadRequestLogger.Error("Failed to parse isFeatured", + zap.String("is_featured", isFeaturedQuery), zap.Error(err), - zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Failed to parse is_shop_bet") } @@ -159,15 +150,172 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { // fmt.Printf("League ID: %v", leagueID) if err != nil { - h.mongoLoggerSvc.Error("Failed to retrieve all upcoming events", - zap.Int("status_code", fiber.StatusInternalServerError), + domain.InternalServerErrorLogger.Error("Failed to retrieve all upcoming events", zap.Error(err), - zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } - return response.WritePaginatedJSON(c, fiber.StatusOK, "All upcoming events retrieved successfully", events, nil, page, int(total)) + res := domain.ConvertEventResList(events) + + return response.WritePaginatedJSON(c, fiber.StatusOK, "All upcoming events retrieved successfully", res, nil, page, int(total)) + +} + +// @Summary Retrieve all upcoming events with settings +// @Description Retrieve all upcoming events settings from the database +// @Tags prematch +// @Accept json +// @Produce json +// @Param page query int false "Page number" +// @Param page_size query int false "Page size" +// @Param league_id query string false "League ID Filter" +// @Param sport_id query string false "Sport ID Filter" +// @Param cc query string false "Country Code Filter" +// @Param first_start_time query string false "Start Time" +// @Param last_start_time query string false "End Time" +// @Success 200 {array} domain.UpcomingEvent +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/{tenant_slug}/events [get] +func (h *Handler) GetTenantUpcomingEvents(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + domain.BadRequestLogger.Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + + page := c.QueryInt("page", 1) + pageSize := c.QueryInt("page_size", 10) + limit := domain.ValidInt32{ + Value: int32(pageSize), + Valid: true, + } + offset := domain.ValidInt32{ + Value: int32(page - 1), + Valid: true, + } + + leagueIDQuery := c.Query("league_id") + var leagueID domain.ValidInt64 + if leagueIDQuery != "" { + leagueIDInt, err := strconv.ParseInt(leagueIDQuery, 10, 64) + if err != nil { + domain.BadRequestLogger.Error("invalid league id", + zap.String("league_id", leagueIDQuery), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, "invalid league id") + } + leagueID = domain.ValidInt64{ + Value: leagueIDInt, + Valid: true, + } + } + sportIDQuery := c.Query("sport_id") + var sportID domain.ValidInt32 + if sportIDQuery != "" { + sportIDint, err := strconv.Atoi(sportIDQuery) + if err != nil { + domain.BadRequestLogger.Info("invalid sport id", + zap.String("sportID", sportIDQuery), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, "invalid sport id") + } + sportID = domain.ValidInt32{ + Value: int32(sportIDint), + Valid: true, + } + } + + searchQuery := c.Query("query") + searchString := domain.ValidString{ + Value: searchQuery, + Valid: searchQuery != "", + } + + firstStartTimeQuery := c.Query("first_start_time") + var firstStartTime domain.ValidTime + if firstStartTimeQuery != "" { + firstStartTimeParsed, err := time.Parse(time.RFC3339, firstStartTimeQuery) + if err != nil { + domain.BadRequestLogger.Info("invalid start_time format", + zap.String("first_start_time", firstStartTimeQuery), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid start_time format") + } + firstStartTime = domain.ValidTime{ + Value: firstStartTimeParsed, + Valid: true, + } + } + + lastStartTimeQuery := c.Query("last_start_time") + var lastStartTime domain.ValidTime + if lastStartTimeQuery != "" { + lastStartTimeParsed, err := time.Parse(time.RFC3339, lastStartTimeQuery) + if err != nil { + domain.BadRequestLogger.Info("invalid last_start_time format", + zap.String("last_start_time", lastStartTimeQuery), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid start_time format") + } + lastStartTime = domain.ValidTime{ + Value: lastStartTimeParsed, + Valid: true, + } + } + + countryCodeQuery := c.Query("cc") + countryCode := domain.ValidString{ + Value: countryCodeQuery, + Valid: countryCodeQuery != "", + } + + isFeaturedQuery := c.Query("is_featured") + var isFeatured domain.ValidBool + if isFeaturedQuery != "" { + isFeaturedParsed, err := strconv.ParseBool(isFeaturedQuery) + if err != nil { + domain.BadRequestLogger.Error("Failed to parse isFeatured", + zap.String("is_featured", isFeaturedQuery), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, "Failed to parse is_shop_bet") + } + + isFeatured = domain.ValidBool{ + Value: isFeaturedParsed, + Valid: true, + } + } + + events, total, err := h.eventSvc.GetEventsWithSettings( + c.Context(), companyID.Value, domain.EventFilter{ + SportID: sportID, + LeagueID: leagueID, + Query: searchString, + FirstStartTime: firstStartTime, + LastStartTime: lastStartTime, + Limit: limit, + Offset: offset, + CountryCode: countryCode, + Featured: isFeatured, + }) + + // fmt.Printf("League ID: %v", leagueID) + if err != nil { + domain.InternalServerErrorLogger.Error("Failed to retrieve all upcoming events", + zap.Error(err), + ) + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + res := domain.ConvertEventWithSettingResList(events) + + return response.WritePaginatedJSON(c, fiber.StatusOK, "All upcoming events retrieved successfully", res, nil, page, int(total)) } @@ -176,11 +324,11 @@ type TopLeaguesRes struct { } type TopLeague struct { - LeagueID int64 `json:"league_id"` - LeagueName string `json:"league_name"` - LeagueCC string `json:"league_cc"` - LeagueSportID int32 `json:"league_sport_id"` - Events []domain.UpcomingEvent `json:"events"` + LeagueID int64 `json:"league_id"` + LeagueName string `json:"league_name"` + LeagueCC string `json:"league_cc"` + LeagueSportID int32 `json:"league_sport_id"` + Events []domain.EventWithSettingsRes `json:"events"` // Total int64 `json:"total"` } @@ -191,43 +339,51 @@ type TopLeague struct { // @Produce json // @Success 200 {array} TopLeague // @Failure 500 {object} response.APIResponse -// @Router /api/v1/top-leagues [get] +// @Router /api/v1/{tenant_slug}/top-leagues [get] func (h *Handler) GetTopLeagues(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + domain.BadRequestLogger.Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } - leagues, err := h.leagueSvc.GetFeaturedLeagues(c.Context()) + leagues, err := h.leagueSvc.GetAllLeaguesByCompany(c.Context(), companyID.Value, domain.LeagueFilter{ + IsFeatured: domain.ValidBool{ + Value: true, + Valid: true, + }, + }) if err != nil { - h.mongoLoggerSvc.Error("Error while fetching top leagues", - zap.Int("status_code", fiber.StatusInternalServerError), + domain.InternalServerErrorLogger.Error("Error while fetching top leagues", + zap.Int64("company_id", companyID.Value), zap.Error(err), - zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } var topLeague []TopLeague = make([]TopLeague, 0, len(leagues)) for _, league := range leagues { - events, _, err := h.eventSvc.GetPaginatedUpcomingEvents( - c.Context(), domain.EventFilter{ - LeagueID: domain.ValidInt32{ - Value: int32(league.ID), + events, _, err := h.eventSvc.GetEventsWithSettings( + c.Context(), companyID.Value, domain.EventFilter{ + LeagueID: domain.ValidInt64{ + Value: league.ID, Valid: true, }, }) if err != nil { - h.mongoLoggerSvc.Warn("Error while fetching events for top league", + domain.InternalServerErrorLogger.Warn("Error while fetching events for top league", zap.Int64("LeagueID", league.ID), - zap.Int("status_code", fiber.StatusInternalServerError), + zap.Int64("company_id", companyID.Value), zap.Error(err), - zap.Time("timestamp", time.Now()), ) } topLeague = append(topLeague, TopLeague{ LeagueID: league.ID, LeagueName: league.Name, - LeagueCC: league.CountryCode, + LeagueCC: league.CountryCode.Value, LeagueSportID: league.SportID, - Events: events, + Events: domain.ConvertEventWithSettingResList(events), }) } @@ -252,26 +408,60 @@ func (h *Handler) GetUpcomingEventByID(c *fiber.Ctx) error { id := c.Params("id") if id == "" { - h.mongoLoggerSvc.Info("Failed to parse event id", - zap.String("id", id), - zap.Int("status_code", fiber.StatusBadRequest), - zap.Time("timestamp", time.Now()), - ) + domain.BadRequestLogger.Info("Failed to parse event id", zap.String("id", id)) return fiber.NewError(fiber.StatusBadRequest, "Missing id") } event, err := h.eventSvc.GetUpcomingEventByID(c.Context(), id) if err != nil { - h.mongoLoggerSvc.Error("Failed to get upcoming event by id", + domain.InternalServerErrorLogger.Error("Failed to get upcoming event by id", zap.String("eventID", id), - zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), - zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } - return response.WriteJSON(c, fiber.StatusOK, "Upcoming event retrieved successfully", event, nil) + res := domain.ConvertEventRes(event) + + return response.WriteJSON(c, fiber.StatusOK, "Upcoming event retrieved successfully", res, nil) + +} + +// @Summary Retrieve an upcoming by ID +// @Description Retrieve an upcoming event by ID +// @Tags prematch +// @Accept json +// @Produce json +// @Param id path string true "ID" +// @Success 200 {object} domain.UpcomingEvent +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/{tenant_slug}events/{id} [get] +func (h *Handler) GetTenantEventByID(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + domain.BadRequestLogger.Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + + id := c.Params("id") + if id == "" { + domain.BadRequestLogger.Info("Failed to parse event id", zap.String("id", id)) + return fiber.NewError(fiber.StatusBadRequest, "Missing id") + } + + event, err := h.eventSvc.GetEventWithSettingByID(c.Context(), id, companyID.Value) + if err != nil { + domain.InternalServerErrorLogger.Error("Failed to get upcoming event by id", + zap.String("eventID", id), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + res := domain.ConvertEventWitSettingRes(event) + + return response.WriteJSON(c, fiber.StatusOK, "Upcoming event retrieved successfully", res, nil) } @@ -294,11 +484,9 @@ func (h *Handler) SetEventStatusToRemoved(c *fiber.Ctx) error { err := h.eventSvc.UpdateEventStatus(c.Context(), eventID, domain.STATUS_REMOVED) if err != nil { - h.mongoLoggerSvc.Error("Failed to update event status", + domain.InternalServerErrorLogger.Error("Failed to update event status", zap.String("EventID", eventID), - zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), - zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update event status") } @@ -307,13 +495,15 @@ func (h *Handler) SetEventStatusToRemoved(c *fiber.Ctx) error { } -type UpdateEventFeaturedReq struct { - Featured bool `json:"is_featured" example:"true"` +type UpdateEventSettingsReq struct { + Featured *bool `json:"is_featured" example:"true"` + IsActive *bool `json:"is_active" example:"true"` + WinningUpperLimit *int `json:"winning_upper_limit" example:"10000"` } -// UpdateEventFeatured godoc -// @Summary update the event featured -// @Description Update the event featured +// UpdateEventSettings godoc +// @Summary update the event settings +// @Description Update the event settings // @Tags event // @Accept json // @Produce json @@ -321,44 +511,53 @@ type UpdateEventFeaturedReq struct { // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/events/{id}/flag [put] -func (h *Handler) UpdateEventFeatured(c *fiber.Ctx) error { - eventID := c.Params("id") +// @Router /api/v1/{tenant_slug}/events/{id}/settings [put] +func (h *Handler) UpdateEventSettings(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + domain.BadRequestLogger.Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } - var req UpdateEventFeaturedReq + eventID := c.Params("id") + var req UpdateEventSettingsReq if err := c.BodyParser(&req); err != nil { - h.mongoLoggerSvc.Info("Failed to parse user id", + domain.BadRequestLogger.Info("Failed to parse user id", zap.String("eventID", eventID), - zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), - zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, err.Error()) } + logFields := []zap.Field{ + zap.String("eventID", eventID), + zap.Int64("companyID", companyID.Value), + zap.Any("is_featured", req.Featured), + zap.Any("is_active", req.IsActive), + zap.Any("winning_upper_limit", req.WinningUpperLimit), + } valErrs, ok := h.validator.Validate(c, req) if !ok { var errMsg string for field, msg := range valErrs { errMsg += fmt.Sprintf("%s: %s; ", field, msg) } - h.mongoLoggerSvc.Error("Failed to update event featured", - zap.Any("request", req), - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Time("timestamp", time.Now()), + domain.BadRequestLogger.Error("Failed to update event featured", + append(logFields, zap.String("errMsg", errMsg))..., ) return fiber.NewError(fiber.StatusBadRequest, errMsg) } - err := h.eventSvc.UpdateEventFeatured(c.Context(), eventID, req.Featured) + err := h.eventSvc.UpdateEventSettings(c.Context(), domain.CreateEventSettings{ + CompanyID: companyID.Value, + EventID: eventID, + IsFeatured: domain.ConvertBoolPtr(req.Featured), + IsActive: domain.ConvertBoolPtr(req.IsActive), + WinningUpperLimit: domain.ConvertIntPtr(req.WinningUpperLimit), + }) if err != nil { - h.mongoLoggerSvc.Error("Failed to update event featured", - zap.String("eventID", eventID), - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) + domain.InternalServerErrorLogger.Error("Failed to update event featured", append(logFields, zap.Error(err))...) return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } diff --git a/internal/web_server/handlers/leagues.go b/internal/web_server/handlers/leagues.go index 09b63a1..222d95d 100644 --- a/internal/web_server/handlers/leagues.go +++ b/internal/web_server/handlers/leagues.go @@ -3,7 +3,6 @@ package handlers import ( "fmt" "strconv" - "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" @@ -51,11 +50,9 @@ func (h *Handler) GetAllLeagues(c *fiber.Ctx) error { if sportIDQuery != "" { sportIDint, err := strconv.Atoi(sportIDQuery) if err != nil { - h.mongoLoggerSvc.Info("invalid sport id", + domain.BadRequestLogger.Info("invalid sport id", zap.String("sport_id", sportIDQuery), - zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), - zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "invalid sport id") } @@ -65,6 +62,23 @@ func (h *Handler) GetAllLeagues(c *fiber.Ctx) error { } } + queryLogFields := []zap.Field{ + zap.Int("page", page), + zap.Int("page_size", pageSize), + zap.Int64("limit_value", limit.Value), + zap.Bool("limit_valid", limit.Valid), + zap.Int64("offset_value", offset.Value), + zap.Bool("offset_valid", offset.Valid), + zap.Bool("is_active_value", isActive.Value), + zap.Bool("is_active_valid", isActive.Valid), + zap.String("cc_query", countryCodeQuery), + zap.String("cc", countryCode.Value), + zap.Bool("cc_valid", countryCode.Valid), + zap.String("sport_id_query", sportIDQuery), + zap.Int32("sport_id", sportID.Value), + zap.Bool("sport_id_valid", sportID.Valid), + } + leagues, err := h.leagueSvc.GetAllLeagues(c.Context(), domain.LeagueFilter{ CountryCode: countryCode, IsActive: isActive, @@ -75,16 +89,106 @@ func (h *Handler) GetAllLeagues(c *fiber.Ctx) error { if err != nil { fmt.Printf("Error fetching league %v \n", err) - h.mongoLoggerSvc.Error("Failed to get all leagues", - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Error(err), - zap.Time("timestamp", time.Now()), + domain.InternalServerErrorLogger.Error("Failed to get all leagues", append(queryLogFields, zap.Error(err))..., ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to get leagues:"+err.Error()) } return response.WriteJSON(c, fiber.StatusOK, "All leagues retrieved", leagues, nil) } +// GetAllLeaguesForTenant godoc +// @Summary Gets all leagues +// @Description Gets all leagues +// @Tags leagues +// @Accept json +// @Produce json +// @Success 200 {array} domain.League +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/{tenant_slug}/leagues [get] +func (h *Handler) GetAllLeaguesForTenant(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + domain.BadRequestLogger.Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + + page := c.QueryInt("page", 1) + pageSize := c.QueryInt("page_size", 10) + + limit := domain.ValidInt64{ + Value: int64(pageSize), + Valid: pageSize == 0, + } + offset := domain.ValidInt64{ + Value: int64(page - 1), + Valid: true, + } + + countryCodeQuery := c.Query("cc") + countryCode := domain.ValidString{ + Value: countryCodeQuery, + Valid: countryCodeQuery != "", + } + isActiveQuery := c.QueryBool("is_active", false) + isActiveFilter := c.QueryBool("is_active_filter", false) + isActive := domain.ValidBool{ + Value: isActiveQuery, + Valid: isActiveFilter, + } + + sportIDQuery := c.Query("sport_id") + var sportID domain.ValidInt32 + if sportIDQuery != "" { + sportIDint, err := strconv.Atoi(sportIDQuery) + if err != nil { + domain.BadRequestLogger.Info("invalid sport id", + zap.String("sport_id", sportIDQuery), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, "invalid sport id") + } + sportID = domain.ValidInt32{ + Value: int32(sportIDint), + Valid: true, + } + } + + queryLogFields := []zap.Field{ + zap.Int64("company_id", companyID.Value), + zap.Bool("company_valid", companyID.Valid), + zap.Int("page", page), + zap.Int("page_size", pageSize), + zap.Int64("limit_value", limit.Value), + zap.Bool("limit_valid", limit.Valid), + zap.Int64("offset_value", offset.Value), + zap.Bool("offset_valid", offset.Valid), + zap.Bool("is_active_value", isActive.Value), + zap.Bool("is_active_valid", isActive.Valid), + zap.String("cc_query", countryCodeQuery), + zap.String("cc", countryCode.Value), + zap.Bool("cc_valid", countryCode.Valid), + zap.String("sport_id_query", sportIDQuery), + zap.Int32("sport_id", sportID.Value), + zap.Bool("sport_id_valid", sportID.Valid), + } + + leagues, err := h.leagueSvc.GetAllLeaguesByCompany(c.Context(), companyID.Value, domain.LeagueFilter{ + CountryCode: countryCode, + IsActive: isActive, + SportID: sportID, + Limit: limit, + Offset: offset, + }) + + if err != nil { + fmt.Printf("Error fetching league %v \n", err) + domain.InternalServerErrorLogger.Error("Failed to get all leagues", append(queryLogFields, zap.Error(err))...) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get leagues:"+err.Error()) + } + return response.WriteJSON(c, fiber.StatusOK, "All leagues retrieved", leagues, nil) +} + type SetLeagueActiveReq struct { IsActive bool `json:"is_active"` } @@ -100,8 +204,13 @@ type SetLeagueActiveReq struct { // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/leagues/{id}/set-active [put] +// @Router /api/v1/{tenant_slug}/leagues/{id}/set-active [put] func (h *Handler) SetLeagueActive(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.mongoLoggerSvc.Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } leagueIdStr := c.Params("id") if leagueIdStr == "" { return fiber.NewError(fiber.StatusBadRequest, "Missing league id") @@ -111,51 +220,44 @@ func (h *Handler) SetLeagueActive(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "invalid league id") } + queryLogFields := []zap.Field{ + zap.Int64("company_id", companyID.Value), + zap.Bool("company_valid", companyID.Valid), + zap.String("league_id", leagueIdStr), + } + var req SetLeagueActiveReq if err := c.BodyParser(&req); err != nil { - h.logger.Error("SetLeagueReq failed", "error", err) - h.mongoLoggerSvc.Error("SetLeagueReq failed to parse request body", - zap.Any("request", req), - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Error(err), - zap.Time("timestamp", time.Now()), + domain.InternalServerErrorLogger.Error("SetLeagueReq failed to parse request body", + append(queryLogFields, zap.Error(err))..., ) return fiber.NewError(fiber.StatusBadRequest, "Failed to parse request:"+err.Error()) } + queryLogFields = append(queryLogFields, zap.Any("is_active", req.IsActive)) valErrs, ok := h.validator.Validate(c, req) if !ok { var errMsg string for field, msg := range valErrs { errMsg += fmt.Sprintf("%s: %s; ", field, msg) } - h.mongoLoggerSvc.Info("Failed to validate SetLeagueActiveReq", - zap.Any("request", req), - zap.Int("status_code", fiber.StatusBadRequest), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) + domain.BadRequestLogger.Info("Failed to validate SetLeagueActiveReq", append(queryLogFields, zap.Error(err))...) return fiber.NewError(fiber.StatusBadRequest, errMsg) } - if err := h.leagueSvc.SetLeagueActive(c.Context(), int64(leagueId), req.IsActive); err != nil { - h.mongoLoggerSvc.Error("Failed to update league active", - zap.Int64("leagueID", int64(leagueId)), - zap.Bool("is_active", req.IsActive), - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) + if err := h.leagueSvc.SaveLeagueSettings(c.Context(), domain.CreateLeagueSettings{ + LeagueID: int64(leagueId), + CompanyID: companyID.Value, + IsActive: domain.ValidBool{ + Value: req.IsActive, + Valid: true, + }, + }); err != nil { + domain.InternalServerErrorLogger.Error("Failed to update league active", append(queryLogFields, zap.Error(err))...) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update league:"+err.Error()) } - h.mongoLoggerSvc.Info("League Active has been successfully updated", - zap.Int64("userID", int64(leagueId)), - zap.Int64("leagueID", int64(leagueId)), - zap.Bool("is_active", req.IsActive), - zap.Int("status_code", fiber.StatusOK), - zap.Time("timestamp", time.Now()), - ) + domain.SuccessResLogger.Info("League Active has been successfully updated", queryLogFields...) return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil) } @@ -175,8 +277,13 @@ type SetLeagueAsFeatured struct { // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/leagues/{id}/featured [put] +// @Router /api/v1/{tenant_slug}/leagues/{id}/featured [put] func (h *Handler) SetLeagueFeatured(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.mongoLoggerSvc.Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } leagueIdStr := c.Params("id") if leagueIdStr == "" { return fiber.NewError(fiber.StatusBadRequest, "Missing league id") @@ -186,54 +293,41 @@ func (h *Handler) SetLeagueFeatured(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "invalid league id") } + queryLogFields := []zap.Field{ + zap.Int64("company_id", companyID.Value), + zap.Bool("company_valid", companyID.Valid), + zap.String("league_id", leagueIdStr), + } var req SetLeagueAsFeatured if err := c.BodyParser(&req); err != nil { - h.logger.Error("SetLeagueFeaturedReq failed", "error", err) - h.mongoLoggerSvc.Info("SetLeagueFeaturedReq failed to parse request body", - zap.Int("status_code", fiber.StatusBadRequest), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) + domain.BadRequestLogger.Info("SetLeagueFeaturedReq failed to parse request body", append(queryLogFields, zap.Error(err))...) return fiber.NewError(fiber.StatusBadRequest, "Failed to parse request body:"+err.Error()) } valErrs, ok := h.validator.Validate(c, req) + queryLogFields = append(queryLogFields, zap.Bool("is_featured", req.IsFeatured)) if !ok { var errMsg string for field, msg := range valErrs { errMsg += fmt.Sprintf("%s: %s; ", field, msg) } - h.mongoLoggerSvc.Info("Failed to validate SetLeagueFeaturedReq", - zap.Any("request", req), - zap.Int("status_code", fiber.StatusBadRequest), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) + domain.BadRequestLogger.Info("Failed to validate SetLeagueFeaturedReq", append(queryLogFields, zap.Error(err))...) return fiber.NewError(fiber.StatusBadRequest, errMsg) } - err = h.leagueSvc.UpdateLeague(c.Context(), domain.UpdateLeague{ - ID: int64(leagueId), + err = h.leagueSvc.SaveLeagueSettings(c.Context(), domain.CreateLeagueSettings{ + LeagueID: int64(leagueId), + CompanyID: companyID.Value, IsFeatured: domain.ValidBool{ Value: req.IsFeatured, Valid: true, }, }) if err != nil { - h.mongoLoggerSvc.Error("Failed to update league", - zap.Int64("leagueID", int64(leagueId)), - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) + domain.InternalServerErrorLogger.Error("Failed to update league", append(queryLogFields, zap.Error(err))...) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update league:"+err.Error()) } - h.mongoLoggerSvc.Info("League Featured has been successfully updated", - zap.Int64("userID", int64(leagueId)), - zap.Int64("leagueID", int64(leagueId)), - zap.Bool("is_featured", req.IsFeatured), - zap.Int("status_code", fiber.StatusOK), - zap.Time("timestamp", time.Now()), - ) + + domain.SuccessResLogger.Info("League Featured has been successfully updated", queryLogFields...) return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil) } diff --git a/internal/web_server/handlers/odd_handler.go b/internal/web_server/handlers/odd_handler.go index 3412499..3d62b35 100644 --- a/internal/web_server/handlers/odd_handler.go +++ b/internal/web_server/handlers/odd_handler.go @@ -1,35 +1,109 @@ package handlers import ( + "strconv" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" "go.uber.org/zap" - "strconv" ) -// GetALLPrematchOdds -// @Summary Retrieve all prematch odds -// @Description Retrieve all prematch odds from the database +// GetAllOdds +// @Summary Retrieve all odds +// @Description Retrieve all odds from the database // @Tags prematch // @Accept json // @Produce json // @Success 200 {array} domain.Odd // @Failure 500 {object} response.APIResponse // @Router /api/v1/odds [get] -func (h *Handler) GetALLPrematchOdds(c *fiber.Ctx) error { - odds, err := h.prematchSvc.GetALLPrematchOdds(c.Context()) +func (h *Handler) GetAllOdds(c *fiber.Ctx) error { + limit, err := strconv.Atoi(c.Query("limit", "10")) // Default limit is 10 + if err != nil || limit <= 0 { + domain.BadRequestLogger.Info("Invalid limit value", zap.Error(err)) + return fiber.NewError(fiber.StatusBadRequest, "Invalid limit value") + } + + offset, err := strconv.Atoi(c.Query("offset", "0")) // Default offset is 0 + if err != nil || offset < 0 { + domain.BadRequestLogger.Info("Invalid offset value", zap.Error(err)) + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + odds, err := h.prematchSvc.GetAllOdds(c.Context(), domain.OddMarketFilter{ + Limit: domain.ValidInt32{ + Value: int32(limit), + Valid: true, + }, + Offset: domain.ValidInt32{ + Value: int32(offset), + Valid: true, + }, + }) if err != nil { - logFields := append([]zap.Field{}, domain.InternalServerErrorZapFields...) - logFields = append(logFields, zap.Error(err)) - h.mongoLoggerSvc.Error("Failed to retrieve all prematch odds", logFields...) + domain.InternalServerErrorLogger.Error("Failed to retrieve all odds", + zap.Int("limit", limit), + zap.Int("offset", offset), + zap.Error(err), + ) return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } - return response.WriteJSON(c, fiber.StatusOK, "All prematch odds retrieved successfully", odds, nil) + return response.WriteJSON(c, fiber.StatusOK, "All odds retrieved successfully", odds, nil) } -// GetRawOddsByMarketID +// GetAllOdds +// @Summary Retrieve all odds +// @Description Retrieve all odds from the database +// @Tags prematch +// @Accept json +// @Produce json +// @Success 200 {array} domain.Odd +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/{tenant_slug}/odds [get] +func (h *Handler) GetAllTenantOdds(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + domain.BadRequestLogger.Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + + limit, err := strconv.Atoi(c.Query("limit", "10")) // Default limit is 10 + if err != nil || limit <= 0 { + domain.BadRequestLogger.Info("Invalid limit value", zap.Error(err)) + return fiber.NewError(fiber.StatusBadRequest, "Invalid limit value") + } + + offset, err := strconv.Atoi(c.Query("offset", "0")) // Default offset is 0 + if err != nil || offset < 0 { + domain.BadRequestLogger.Info("Invalid offset value", zap.Error(err)) + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + odds, err := h.prematchSvc.GetAllOddsWithSettings(c.Context(), companyID.Value, domain.OddMarketFilter{ + Limit: domain.ValidInt32{ + Value: int32(limit), + Valid: true, + }, + Offset: domain.ValidInt32{ + Value: int32(offset), + Valid: true, + }, + }) + if err != nil { + domain.InternalServerErrorLogger.Error("Failed to retrieve all odds", + zap.Int("limit", limit), + zap.Int("offset", offset), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + return response.WriteJSON(c, fiber.StatusOK, "All odds retrieved successfully", odds, nil) +} + +// GetOddsByMarketID // @Summary Retrieve raw odds by Market ID // @Description Retrieve raw odds records using a Market ID // @Tags prematch @@ -41,7 +115,7 @@ func (h *Handler) GetALLPrematchOdds(c *fiber.Ctx) error { // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/odds/upcoming/{upcoming_id}/market/{market_id} [get] -func (h *Handler) GetRawOddsByMarketID(c *fiber.Ctx) error { +func (h *Handler) GetOddsByMarketID(c *fiber.Ctx) error { logFields := []zap.Field{ zap.String("market_id", c.Params("market_id")), zap.String("upcoming_id", c.Params("upcoming_id")), @@ -49,21 +123,20 @@ func (h *Handler) GetRawOddsByMarketID(c *fiber.Ctx) error { marketID := c.Params("market_id") if marketID == "" { - h.mongoLoggerSvc.Info("Missing market_id", append(logFields, domain.BadRequestZapFields...)...) + domain.BadRequestLogger.Info("Missing market_id", logFields...) return fiber.NewError(fiber.StatusBadRequest, "Missing market_id") } upcomingID := c.Params("upcoming_id") if upcomingID == "" { - h.mongoLoggerSvc.Info("Missing upcoming_id", append(logFields, domain.BadRequestZapFields...)...) + domain.BadRequestLogger.Info("Missing upcoming_id", logFields...) return fiber.NewError(fiber.StatusBadRequest, "Missing upcoming_id") } - rawOdds, err := h.prematchSvc.GetRawOddsByMarketID(c.Context(), marketID, upcomingID) + rawOdds, err := h.prematchSvc.GetOddsByMarketID(c.Context(), marketID, upcomingID) if err != nil { // Lets turn this into a warn because this is constantly going off - logFields = append(logFields, zap.Error(err)) - h.mongoLoggerSvc.Warn("Failed to get raw odds by market ID", append(logFields, domain.InternalServerErrorZapFields...)...) + domain.InternalServerErrorLogger.Warn("Failed to get raw odds by market ID", append(logFields, zap.Error(err))...) return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } @@ -71,6 +144,55 @@ func (h *Handler) GetRawOddsByMarketID(c *fiber.Ctx) error { } +// GetTenantOddsByMarketID +// @Summary Retrieve raw odds by Market ID +// @Description Retrieve raw odds records using a Market ID +// @Tags prematch +// @Accept json +// @Produce json +// @Param upcoming_id path string true "Upcoming ID" +// @Param market_id path string true "Market ID" +// @Success 200 {array} domain.RawOddsByMarketID +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/{tenant_slug}/odds/upcoming/{upcoming_id}/market/{market_id} [get] +func (h *Handler) GetTenantOddsByMarketID(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + domain.BadRequestLogger.Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + logFields := []zap.Field{ + zap.String("market_id", c.Params("market_id")), + zap.String("upcoming_id", c.Params("upcoming_id")), + zap.Int64("company_id", companyID.Value), + } + + marketID := c.Params("market_id") + if marketID == "" { + domain.BadRequestLogger.Info("Missing market_id", logFields...) + return fiber.NewError(fiber.StatusBadRequest, "Missing market_id") + } + + upcomingID := c.Params("upcoming_id") + if upcomingID == "" { + domain.BadRequestLogger.Info("Missing upcoming_id", logFields...) + return fiber.NewError(fiber.StatusBadRequest, "Missing upcoming_id") + } + + oddMarket, err := h.prematchSvc.GetOddsWithSettingsByMarketID(c.Context(), marketID, upcomingID, companyID.Value) + + if err != nil { + // Lets turn this into a warn because this is constantly going off + domain.InternalServerErrorLogger.Warn("Failed to get raw odds by market ID", append(logFields, zap.Error(err))...) + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + return response.WriteJSON(c, fiber.StatusOK, "Raw odds retrieved successfully", oddMarket, nil) + +} + +// GetOddsByUpcomingID // @Summary Retrieve prematch odds by upcoming ID (FI) // @Description Retrieve prematch odds by upcoming event ID (FI from Bet365) with optional pagination // @Tags prematch @@ -92,31 +214,89 @@ func (h *Handler) GetOddsByUpcomingID(c *fiber.Ctx) error { upcomingID := c.Params("upcoming_id") if upcomingID == "" { - h.mongoLoggerSvc.Info("Missing upcoming_id", append(logFields, domain.BadRequestZapFields...)...) + domain.BadRequestLogger.Info("Missing upcoming_id", logFields...) return fiber.NewError(fiber.StatusBadRequest, "Missing upcoming_id") } limit, err := strconv.Atoi(c.Query("limit", "10")) // Default limit is 10 if err != nil || limit <= 0 { logFields = append(logFields, zap.Error(err)) - h.mongoLoggerSvc.Info("Invalid limit value", append(logFields, domain.BadRequestZapFields...)...) + domain.BadRequestLogger.Info("Invalid limit value", logFields...) return fiber.NewError(fiber.StatusBadRequest, "Invalid limit value") } offset, err := strconv.Atoi(c.Query("offset", "0")) // Default offset is 0 if err != nil || offset < 0 { logFields = append(logFields, zap.Error(err)) - h.mongoLoggerSvc.Info("Invalid offset value", append(logFields, domain.BadRequestZapFields...)...) + domain.BadRequestLogger.Info("Invalid offset value", logFields...) return fiber.NewError(fiber.StatusBadRequest, err.Error()) } - odds, err := h.prematchSvc.GetPrematchOddsByUpcomingID(c.Context(), upcomingID) + odds, err := h.prematchSvc.GetOddsByEventID(c.Context(), upcomingID, domain.OddMarketWithEventFilter{}) if err != nil { logFields = append(logFields, zap.Error(err)) - h.mongoLoggerSvc.Error("Failed to retrieve prematch odds", append(logFields, domain.InternalServerErrorZapFields...)...) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve prematch odds"+err.Error()) + domain.InternalServerErrorLogger.Error("Failed to retrieve odds", append(logFields, zap.Error(err))...) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve odds"+err.Error()) } - return response.WriteJSON(c, fiber.StatusOK, "Prematch odds retrieved successfully", odds, nil) + return response.WriteJSON(c, fiber.StatusOK, "Odds retrieved successfully", odds, nil) + +} + +// GetTenantOddsByUpcomingID +// @Summary Retrieve prematch odds by upcoming ID (FI) +// @Description Retrieve prematch odds by upcoming event ID (FI from Bet365) with optional pagination +// @Tags prematch +// @Accept json +// @Produce json +// @Param upcoming_id path string true "Upcoming Event ID (FI)" +// @Param limit query int false "Number of results to return (default: 10)" +// @Param offset query int false "Number of results to skip (default: 0)" +// @Success 200 {array} domain.Odd +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/{tenant_slug}/odds/upcoming/{upcoming_id} [get] +func (h *Handler) GetTenantOddsByUpcomingID(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + domain.BadRequestLogger.Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + + logFields := []zap.Field{ + zap.String("upcoming_id", c.Params("upcoming_id")), + zap.String("limit_param", c.Query("limit", "10")), + zap.String("offset_param", c.Query("offset", "0")), + zap.Int64("company_id", companyID.Value), + } + + upcomingID := c.Params("upcoming_id") + if upcomingID == "" { + domain.BadRequestLogger.Info("Missing upcoming_id", logFields...) + return fiber.NewError(fiber.StatusBadRequest, "Missing upcoming_id") + } + + limit, err := strconv.Atoi(c.Query("limit", "10")) // Default limit is 10 + if err != nil || limit <= 0 { + logFields = append(logFields, zap.Error(err)) + domain.BadRequestLogger.Info("Invalid limit value", logFields...) + return fiber.NewError(fiber.StatusBadRequest, "Invalid limit value") + } + + offset, err := strconv.Atoi(c.Query("offset", "0")) // Default offset is 0 + if err != nil || offset < 0 { + logFields = append(logFields, zap.Error(err)) + domain.BadRequestLogger.Info("Invalid offset value", logFields...) + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + odds, err := h.prematchSvc.GetOddsWithSettingsByEventID(c.Context(), upcomingID, companyID.Value, domain.OddMarketFilter{}) + if err != nil { + logFields = append(logFields, zap.Error(err)) + domain.InternalServerErrorLogger.Error("Failed to retrieve odds", append(logFields, zap.Error(err))...) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve odds"+err.Error()) + } + + return response.WriteJSON(c, fiber.StatusOK, "Odds retrieved successfully", odds, nil) } diff --git a/internal/web_server/handlers/user.go b/internal/web_server/handlers/user.go index 8779de9..f31452b 100644 --- a/internal/web_server/handlers/user.go +++ b/internal/web_server/handlers/user.go @@ -33,9 +33,13 @@ type CheckPhoneEmailExistRes struct { // @Success 200 {object} CheckPhoneEmailExistRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/user/checkPhoneEmailExist [post] +// @Router /api/v1/{tenant_slug}/user/checkPhoneEmailExist [post] func (h *Handler) CheckPhoneEmailExist(c *fiber.Ctx) error { - + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + domain.BadRequestLogger.Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } var req CheckPhoneEmailExistReq if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Info("Failed to parse CheckPhoneEmailExist request", @@ -54,7 +58,7 @@ func (h *Handler) CheckPhoneEmailExist(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, errMsg) } - emailExist, phoneExist, err := h.userSvc.CheckPhoneEmailExist(c.Context(), req.PhoneNumber, req.Email) + emailExist, phoneExist, err := h.userSvc.CheckPhoneEmailExist(c.Context(), req.PhoneNumber, req.Email, companyID) if err != nil { h.mongoLoggerSvc.Error("Failed to check phone/email existence", zap.Any("request", req), @@ -87,9 +91,13 @@ type RegisterCodeReq struct { // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/user/sendRegisterCode [post] +// @Router /api/v1/{tenant_slug}/user/sendRegisterCode [post] func (h *Handler) SendRegisterCode(c *fiber.Ctx) error { - + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + domain.BadRequestLogger.Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } var req RegisterCodeReq if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Info("Failed to parse SendRegisterCode request", @@ -120,7 +128,7 @@ func (h *Handler) SendRegisterCode(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided") } - if err := h.userSvc.SendRegisterCode(c.Context(), medium, sentTo, domain.AfroMessage); err != nil { + if err := h.userSvc.SendRegisterCode(c.Context(), medium, sentTo, domain.AfroMessage, companyID); err != nil { h.mongoLoggerSvc.Error("Failed to send register code", zap.String("Medium", string(medium)), zap.String("Send To", string(sentTo)), @@ -154,9 +162,13 @@ type RegisterUserReq struct { // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/user/register [post] +// @Router /api/v1/{tenant_slug}/user/register [post] func (h *Handler) RegisterUser(c *fiber.Ctx) error { - + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + domain.BadRequestLogger.Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } var req RegisterUserReq if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Info("Failed to parse RegisterUser request", @@ -183,6 +195,8 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error { Otp: req.Otp, ReferralCode: req.ReferalCode, OtpMedium: domain.OtpMediumEmail, + CompanyID: companyID, + Role: string(domain.RoleCustomer), } medium, err := getMedium(req.Email, req.PhoneNumber) if err != nil { @@ -279,9 +293,13 @@ type ResetCodeReq struct { // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/user/sendResetCode [post] +// @Router /api/v1/{tenant_slug}/user/sendResetCode [post] func (h *Handler) SendResetCode(c *fiber.Ctx) error { - + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + domain.BadRequestLogger.Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } var req ResetCodeReq if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Info("Failed to parse SendResetCode request", @@ -318,7 +336,7 @@ func (h *Handler) SendResetCode(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided") } - if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo, domain.AfroMessage); err != nil { + if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo, domain.AfroMessage, companyID); err != nil { h.mongoLoggerSvc.Error("Failed to send reset code", zap.String("medium", string(medium)), zap.String("sentTo", string(sentTo)), @@ -349,7 +367,7 @@ type ResetPasswordReq struct { // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/user/resetPassword [post] +// @Router /api/v1/{tenant_slug}/user/resetPassword [post] func (h *Handler) ResetPassword(c *fiber.Ctx) error { var req ResetPasswordReq @@ -447,7 +465,7 @@ type CustomerProfileRes struct { // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Security Bearer -// @Router /api/v1/user/customer-profile [get] +// @Router /api/v1/{tenant_slug}/user/customer-profile [get] func (h *Handler) CustomerProfile(c *fiber.Ctx) error { userID, ok := c.Locals("user_id").(int64) @@ -499,7 +517,6 @@ func (h *Handler) CustomerProfile(c *fiber.Ctx) error { SuspendedAt: user.SuspendedAt, Suspended: user.Suspended, LastLogin: *lastLogin, - } return response.WriteJSON(c, fiber.StatusOK, "User profile retrieved successfully", res, nil) } @@ -530,7 +547,7 @@ type AdminProfileRes struct { // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Security Bearer -// @Router /api/v1/user/admin-profile [get] +// @Router /api/v1/{tenant_slug}/user/admin-profile [get] func (h *Handler) AdminProfile(c *fiber.Ctx) error { userID, ok := c.Locals("user_id").(int64) @@ -612,7 +629,7 @@ type SearchUserByNameOrPhoneReq struct { // @Success 200 {object} UserProfileRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/user/search [post] +// @Router /api/v1/{tenant_slug}/user/search [post] func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error { // TODO: Add filtering by role based on which user is calling this var req SearchUserByNameOrPhoneReq @@ -870,7 +887,7 @@ func (h *Handler) UpdateUserSuspend(c *fiber.Ctx) error { // @Success 200 {array} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/user/bets [get] +// @Router /api/v1/{tenant_slug}/user/bets [get] func (h *Handler) GetBetByUserID(c *fiber.Ctx) error { userID, ok := c.Locals("user_id").(int64) if !ok || userID == 0 { diff --git a/internal/web_server/handlers/wallet_handler.go b/internal/web_server/handlers/wallet_handler.go index 6abd899..6b798c7 100644 --- a/internal/web_server/handlers/wallet_handler.go +++ b/internal/web_server/handlers/wallet_handler.go @@ -313,7 +313,7 @@ func (h *Handler) UpdateWalletActive(c *fiber.Ctx) error { // @Success 200 {object} CustomerWalletRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/user/wallet [get] +// @Router /api/v1/{tenant_slug}/user/wallet [get] func (h *Handler) GetCustomerWallet(c *fiber.Ctx) error { userID, ok := c.Locals("user_id").(int64) diff --git a/internal/web_server/middleware.go b/internal/web_server/middleware.go index c25584b..da941d2 100644 --- a/internal/web_server/middleware.go +++ b/internal/web_server/middleware.go @@ -67,8 +67,8 @@ func (a *App) authMiddleware(c *fiber.Ctx) error { // return fiber.NewError(fiber.StatusUnauthorized, "Refresh token missing") } - // Asserting to make sure that there is no company role without a valid company id - if claim.Role != domain.RoleSuperAdmin && claim.Role != domain.RoleCustomer && !claim.CompanyID.Valid { + // Asserting to make sure that only the super admin can have a nil company ID + if claim.Role != domain.RoleSuperAdmin && !claim.CompanyID.Valid { a.mongoLoggerSvc.Error("Company Role without Company ID", zap.Int64("userID", claim.UserId), zap.Int("status_code", fiber.StatusInternalServerError), @@ -220,3 +220,25 @@ func (a *App) WebsocketAuthMiddleware(c *fiber.Ctx) error { ) return c.Next() } + +func (a *App) TenantMiddleware(c *fiber.Ctx) error { + if tokenCID, ok := c.Locals("company_id").(domain.ValidInt64); ok && tokenCID.Valid { + return c.Next() + } + + tenantSlug := c.Params("tenant_slug") + if tenantSlug == "" { + return fiber.NewError(fiber.StatusBadRequest, "tenant is required for this route") + } + + companyID, err := a.companySvc.GetCompanyIDBySlug(c.Context(), tenantSlug) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "failed to resolve tenant") + } + + c.Locals("company_id", domain.ValidInt64{ + Value: companyID, + Valid: true, + }) + return c.Next() +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 71e45c6..7b8e513 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -62,7 +62,10 @@ func (a *App) initAppRoutes() { }) }) + // Groups groupV1 := a.fiber.Group("/api/v1") + tenant := groupV1.Group("/:tenant_slug", a.TenantMiddleware) + tenantAuth := groupV1.Group("/:tenant_slug", a.authMiddleware, a.TenantMiddleware) //Direct_deposit groupV1.Post("/direct_deposit", a.authMiddleware, h.InitiateDirectDeposit) @@ -82,8 +85,9 @@ func (a *App) initAppRoutes() { }) // Auth Routes - groupV1.Post("/auth/customer-login", h.LoginCustomer) - groupV1.Post("/auth/admin-login", h.LoginAdmin) + tenant.Post("/auth/customer-login", h.LoginCustomer) + tenant.Post("/auth/admin-login", h.LoginAdmin) + groupV1.Post("/auth/super-login", h.LoginSuper) groupV1.Post("/auth/refresh", h.RefreshToken) groupV1.Post("/auth/logout", a.authMiddleware, h.LogOutCustomer) groupV1.Get("/auth/test", a.authMiddleware, func(c *fiber.Ctx) error { @@ -139,20 +143,21 @@ func (a *App) initAppRoutes() { // groupV1.Get("/arifpay/session-id/verify-transaction/:session_id", a.authMiddleware, h.ArifpayVerifyBySessionIDHandler) // User Routes - groupV1.Post("/user/resetPassword", h.ResetPassword) - groupV1.Post("/user/sendResetCode", h.SendResetCode) - groupV1.Post("/user/register", h.RegisterUser) - groupV1.Post("/user/sendRegisterCode", h.SendRegisterCode) - groupV1.Post("/user/checkPhoneEmailExist", h.CheckPhoneEmailExist) - groupV1.Get("/user/customer-profile", a.authMiddleware, h.CustomerProfile) - groupV1.Get("/user/admin-profile", a.authMiddleware, h.AdminProfile) + tenant.Post("/user/resetPassword", h.ResetPassword) + tenant.Post("/user/sendResetCode", h.SendResetCode) + tenant.Post("/user/register", h.RegisterUser) + tenant.Post("/user/sendRegisterCode", h.SendRegisterCode) + tenant.Post("/user/checkPhoneEmailExist", h.CheckPhoneEmailExist) + tenantAuth.Get("/user/customer-profile", h.CustomerProfile) + tenantAuth.Get("/user/admin-profile", h.AdminProfile) + tenantAuth.Get("/user/bets", h.GetBetByUserID) + groupV1.Get("/user/single/:id", a.authMiddleware, h.GetUserByID) - groupV1.Delete("/user/delete/:id", a.authMiddleware, h.DeleteUser) groupV1.Post("/user/suspend", a.authMiddleware, h.UpdateUserSuspend) - groupV1.Get("/user/bets", a.authMiddleware, h.GetBetByUserID) + groupV1.Delete("/user/delete/:id", a.authMiddleware, h.DeleteUser) - groupV1.Get("/user/wallet", a.authMiddleware, h.GetCustomerWallet) - groupV1.Post("/user/search", a.authMiddleware, h.SearchUserByNameOrPhone) + tenantAuth.Get("/user/wallet", a.authMiddleware, h.GetCustomerWallet) + tenantAuth.Post("/user/search", a.authMiddleware, h.SearchUserByNameOrPhone) // Referral Routes groupV1.Post("/referral/create", a.authMiddleware, h.CreateReferralCode) @@ -186,20 +191,28 @@ func (a *App) initAppRoutes() { groupV1.Put("/managers/:id", a.authMiddleware, h.UpdateManagers) groupV1.Get("/manager/:id/branch", a.authMiddleware, h.GetBranchByManagerID) - groupV1.Get("/odds", h.GetALLPrematchOdds) - groupV1.Get("/odds/upcoming/:upcoming_id", h.GetOddsByUpcomingID) - groupV1.Get("/odds/upcoming/:upcoming_id/market/:market_id", h.GetRawOddsByMarketID) + groupV1.Get("/odds", a.authMiddleware, a.SuperAdminOnly, h.GetAllOdds) + groupV1.Get("/odds/upcoming/:upcoming_id", a.authMiddleware, a.SuperAdminOnly, h.GetOddsByUpcomingID) + groupV1.Get("/odds/upcoming/:upcoming_id/market/:market_id", a.authMiddleware, a.SuperAdminOnly, h.GetOddsByMarketID) + + tenant.Get("/odds", h.GetAllTenantOdds) + tenant.Get("/odds/upcoming/:upcoming_id", h.GetTenantOddsByUpcomingID) + tenant.Get("/odds/upcoming/:upcoming_id/market/:market_id", h.GetTenantOddsByMarketID) - groupV1.Get("/events", h.GetAllUpcomingEvents) - groupV1.Get("/events/:id", h.GetUpcomingEventByID) + groupV1.Get("/events", a.authMiddleware, a.SuperAdminOnly, h.GetAllUpcomingEvents) + groupV1.Get("/events/:id", a.authMiddleware, a.SuperAdminOnly, h.GetUpcomingEventByID) groupV1.Delete("/events/:id", a.authMiddleware, a.SuperAdminOnly, h.SetEventStatusToRemoved) - groupV1.Get("/top-leagues", h.GetTopLeagues) - groupV1.Put("/events/:id/featured", h.UpdateEventFeatured) + + tenant.Get("/events", h.GetTenantUpcomingEvents) + tenant.Get("/events/:id", h.GetTenantEventByID) + tenant.Get("/top-leagues", h.GetTopLeagues) + tenant.Put("/events/:id/settings", h.UpdateEventSettings) // Leagues - groupV1.Get("/leagues", h.GetAllLeagues) - groupV1.Put("/leagues/:id/set-active", h.SetLeagueActive) - groupV1.Put("/leagues/:id/featured", h.SetLeagueFeatured) + tenant.Get("/leagues", h.GetAllLeagues) + tenant.Put("/leagues/:id/set-active", h.SetLeagueActive) + tenant.Put("/leagues/:id/featured", h.SetLeagueFeatured) + groupV1.Get("/leagues", a.authMiddleware, a.SuperAdminOnly, h.GetAllLeagues) groupV1.Get("/result/:id", h.GetResultsByEventID) @@ -352,5 +365,5 @@ func (a *App) initAppRoutes() { groupV1.Get("/settings", a.authMiddleware, h.GetSettingList) groupV1.Get("/settings/:key", a.authMiddleware, h.GetSettingByKey) groupV1.Put("/settings", a.authMiddleware, h.UpdateSettingList) - + } From 4e97195404d14c1bfb5f79cb37cd6fd73339e087 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Sat, 23 Aug 2025 21:56:00 +0300 Subject: [PATCH 08/10] feat: added veli test user --- db/data/seed_data.sql | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/db/data/seed_data.sql b/db/data/seed_data.sql index 732a346..860d7e5 100644 --- a/db/data/seed_data.sql +++ b/db/data/seed_data.sql @@ -75,6 +75,21 @@ VALUES ( CURRENT_TIMESTAMP, FALSE, NULL + ), + ( + 5, + 'Test', + 'Veli', + 'test.veli@example.com', + NULL, + crypt('password@123', gen_salt('bf'))::bytea, + 'customer', + TRUE, + FALSE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + FALSE, + NULL ); -- Supported Operations INSERT INTO supported_operations (id, name, description) @@ -158,7 +173,7 @@ VALUES (1, 1, 1, 2); INSERT INTO companies ( id, name, - slug + slug, admin_id, wallet_id, deducted_percentage, @@ -169,7 +184,7 @@ INSERT INTO companies ( VALUES ( 1, 'FortuneBets', - 'fortunebets', + 'fortunebets', 2, 3, 0.10, From aaf14fedcfd6cf48bd5aa4edc96401c846f41f32 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Mon, 25 Aug 2025 07:23:55 +0300 Subject: [PATCH 09/10] fix: global setting, company override settings, bet and ticket fixes for multi tenant --- db/migrations/000001_fortune.up.sql | 45 +- db/migrations/000007_setting_data.up.sql | 4 +- db/query/bet.sql | 11 +- db/query/settings.sql | 43 +- db/query/ticket.sql | 13 +- gen/db/bet.sql.go | 23 +- gen/db/custom_odds.sql.go | 167 ------- gen/db/models.go | 23 +- gen/db/odds.sql.go | 9 +- gen/db/settings.sql.go | 217 ++++++++- gen/db/ticket.sql.go | 23 +- internal/domain/bet.go | 116 ++++- internal/domain/common_log.go | 18 +- internal/domain/currency.go | 18 +- internal/domain/custom_odds.go | 118 ++--- internal/domain/disabled_odds.go | 40 +- internal/domain/result.go | 2 +- internal/domain/role.go | 9 + internal/domain/setting_list.go | 447 ++++++++++++++++++ internal/domain/settings.go | 191 ++------ internal/domain/ticket.go | 8 + internal/domain/user.go | 5 +- internal/domain/validtypes.go | 348 ++++++++------ internal/repository/bet.go | 160 +------ internal/repository/settings.go | 210 ++++---- internal/repository/shop_bet.go | 2 +- internal/repository/ticket.go | 6 +- internal/services/bet/service.go | 19 +- internal/services/messenger/sms.go | 20 +- internal/services/notification/service.go | 2 +- internal/services/odds/port.go | 2 +- internal/services/referal/port.go | 2 +- internal/services/referal/service.go | 7 +- internal/services/settings/port.go | 19 +- internal/services/settings/service.go | 47 +- internal/services/ticket/port.go | 2 +- internal/services/ticket/service.go | 11 +- internal/services/transaction/service.go | 2 +- internal/services/transaction/shop_bet.go | 2 +- internal/services/virtualGame/veli/service.go | 2 +- internal/web_server/handlers/auth_handler.go | 4 +- internal/web_server/handlers/bet_handler.go | 146 +++++- .../web_server/handlers/common_handler.go | 25 + internal/web_server/handlers/event_handler.go | 52 +- internal/web_server/handlers/leagues.go | 26 +- internal/web_server/handlers/odd_handler.go | 46 +- .../web_server/handlers/settings_handler.go | 155 +++++- internal/web_server/handlers/shop_handler.go | 2 +- .../web_server/handlers/ticket_handler.go | 39 +- internal/web_server/handlers/user.go | 10 +- internal/web_server/routes.go | 50 +- 51 files changed, 1897 insertions(+), 1071 deletions(-) delete mode 100644 gen/db/custom_odds.sql.go create mode 100644 internal/domain/setting_list.go create mode 100644 internal/web_server/handlers/common_handler.go diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 717d0d2..0a167ed 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -292,7 +292,7 @@ CREATE TABLE events ( default_is_featured BOOLEAN NOT NULL DEFAULT false, default_winning_upper_limit INT NOT NULL, is_monitored BOOLEAN NOT NULL DEFAULT FALSE, - UNIQUE(source_event_id, source) + UNIQUE(id, source) ); CREATE TABLE event_history ( id BIGSERIAL PRIMARY KEY, @@ -321,8 +321,6 @@ CREATE TABLE odds_market ( default_is_active BOOLEAN NOT NULL DEFAULT true, fetched_at TIMESTAMP DEFAULT now(), expires_at TIMESTAMP NOT NULL, - UNIQUE (market_id, name, handicap), - UNIQUE (event_id, market_id, name, handicap), UNIQUE (event_id, market_id) ); CREATE TABLE odd_history ( @@ -408,12 +406,21 @@ CREATE TABLE teams ( bet365_id BIGINT, img_url TEXT ); -CREATE TABLE IF NOT EXISTS settings ( +CREATE TABLE IF NOT EXISTS global_settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); +-- Tenant/Company-specific overrides +CREATE TABLE IF NOT EXISTS company_settings ( + company_id BIGINT NOT NULL REFERENCES companies(id) ON DELETE CASCADE, + key TEXT NOT NULL, + value TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (company_id, key) +); CREATE TABLE bonus ( multiplier REAL NOT NULL, id BIGSERIAL PRIMARY KEY, @@ -430,11 +437,11 @@ CREATE TABLE flags ( CHECK ( ( bet_id IS NOT NULL - AND odd_id IS NULL + AND odds_market_id IS NULL ) OR ( bet_id IS NULL - AND odd_id IS NOT NULL + AND odds_market_id IS NOT NULL ) ) ); @@ -579,7 +586,7 @@ SELECT l.*, COALESCE(cls.is_featured, l.default_is_featured) AS is_featured, cls.updated_at FROM leagues l - JOIN company_league_settings cls ON leagues.id = cls.league_id; + JOIN company_league_settings cls ON l.id = cls.league_id; CREATE VIEW event_with_settings AS SELECT e.*, ces.company_id, @@ -592,15 +599,23 @@ SELECT e.*, ces.updated_at, l.country_code as league_cc FROM events e - JOIN company_event_settings ces ON events.id = ces.event_id - JOIN leagues l ON leagues.id = e.league_id; + JOIN company_event_settings ces ON e.id = ces.event_id + JOIN leagues l ON l.id = e.league_id; CREATE VIEW event_with_country AS SELECT events.*, leagues.country_code as league_cc FROM events - LEFT JOIN leagues ON leagues.id = league_id; + LEFT JOIN leagues ON leagues.id = events.league_id; CREATE VIEW odds_market_with_settings AS -SELECT o.*, +SELECT o.id, + o.event_id, + o.market_type, + o.market_name, + o.market_category, + o.market_id, + o.default_is_active, + o.fetched_at, + o.expires_at, cos.company_id, COALESCE(cos.is_active, o.default_is_active) AS is_active, COALESCE(cos.custom_raw_odds, o.raw_odds) AS raw_odds, @@ -654,4 +669,10 @@ ADD CONSTRAINT fk_companies_admin FOREIGN KEY (admin_id) REFERENCES users(id), ADD CONSTRAINT fk_companies_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE; ALTER TABLE company_league_settings ADD CONSTRAINT fk_league_settings_company FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, - ADD CONSTRAINT fk_league_settings_league FOREIGN KEY (league_id) REFERENCES league_id(id) ON DELETE CASCADE; \ No newline at end of file + ADD CONSTRAINT fk_league_settings_league FOREIGN KEY (league_id) REFERENCES leagues(id) ON DELETE CASCADE; +ALTER TABLE company_event_settings +ADD CONSTRAINT fk_event_settings_company FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, + ADD CONSTRAINT fk_event_settings_event FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE; +ALTER TABLE company_odd_settings +ADD CONSTRAINT fk_odds_settings_company FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, + ADD CONSTRAINT fk_odds_settings_odds_market FOREIGN KEY (odds_market_id) REFERENCES odds_market(id) ON DELETE CASCADE; \ No newline at end of file diff --git a/db/migrations/000007_setting_data.up.sql b/db/migrations/000007_setting_data.up.sql index e98967b..a381c02 100644 --- a/db/migrations/000007_setting_data.up.sql +++ b/db/migrations/000007_setting_data.up.sql @@ -1,6 +1,6 @@ -- Settings Initial Data -INSERT INTO settings (key, value) -VALUES ('sms_provider', '30'), +INSERT INTO global_settings (key, value) +VALUES ('sms_provider', 'afro_message'), ('max_number_of_outcomes', '30'), ('bet_amount_limit', '10000000'), ('daily_ticket_limit', '50'), diff --git a/db/query/bet.sql b/db/query/bet.sql index 47018e1..caef892 100644 --- a/db/query/bet.sql +++ b/db/query/bet.sql @@ -6,9 +6,10 @@ INSERT INTO bets ( user_id, is_shop_bet, outcomes_hash, - fast_code + fast_code, + company_id ) -VALUES ($1, $2, $3, $4, $5, $6, $7) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *; -- name: CreateBetOutcome :copyfrom INSERT INTO bet_outcomes ( @@ -52,6 +53,10 @@ wHERE ( is_shop_bet = sqlc.narg('is_shop_bet') OR sqlc.narg('is_shop_bet') IS NULL ) + AND ( + company_id = sqlc.narg('company_id') + OR sqlc.narg('company_id') IS NULL + ) AND ( cashed_out = sqlc.narg('cashed_out') OR sqlc.narg('cashed_out') IS NULL @@ -148,4 +153,4 @@ DELETE FROM bets WHERE id = $1; -- name: DeleteBetOutcome :exec DELETE FROM bet_outcomes -WHERE bet_id = $1; +WHERE bet_id = $1; \ No newline at end of file diff --git a/db/query/settings.sql b/db/query/settings.sql index 6a06aae..cda4e87 100644 --- a/db/query/settings.sql +++ b/db/query/settings.sql @@ -1,12 +1,41 @@ --- name: GetSettings :many +-- name: GetGlobalSettings :many SELECT * -FROM settings; --- name: GetSetting :one +FROM global_settings; +-- name: GetGlobalSetting :one SELECT * -FROM settings +FROM global_settings WHERE key = $1; --- name: UpdateSetting :exec -UPDATE settings +-- name: UpdateGlobalSetting :exec +UPDATE global_settings SET value = $2, updated_at = CURRENT_TIMESTAMP -WHERE key = $1; \ No newline at end of file +WHERE key = $1; +-- name: InsertCompanySetting :exec +INSERT INTO company_settings (company_id, key, value) +VALUES ($1, $2, $3) ON CONFLICT (company_id, key) DO +UPDATE +SET value = EXCLUDED.value; +-- name: GetAllCompanySettings :many +SELECT * +FROM company_settings; +-- name: GetCompanySetting :many +SELECT * +FROM company_settings +WHERE company_id = $1; +-- name: GetCompanySettingsByKey :many +SELECT * +FROM company_settings +WHERE key = $1; +-- name: GetOverrideSettings :many +SELECT gs.*, + COALESCE(cs.value, gs.value) AS value +FROM global_settings gs + LEFT JOIN company_settings cs ON cs.key = gs.key + AND cs.company_id = $1; +-- name: DeleteCompanySetting :exec +DELETE FROM company_settings +WHERE company_id = $1 + AND key = $2; +-- name: DeleteAllCompanySetting :exec +DELETE FROM company_settings +WHERE company_id = $1; \ No newline at end of file diff --git a/db/query/ticket.sql b/db/query/ticket.sql index 7648842..b93da70 100644 --- a/db/query/ticket.sql +++ b/db/query/ticket.sql @@ -1,6 +1,6 @@ -- name: CreateTicket :one -INSERT INTO tickets (amount, total_odds, ip) -VALUES ($1, $2, $3) +INSERT INTO tickets (amount, total_odds, ip, company_id) +VALUES ($1, $2, $3, $4) RETURNING *; -- name: CreateTicketOutcome :copyfrom INSERT INTO ticket_outcomes ( @@ -33,7 +33,11 @@ VALUES ( ); -- name: GetAllTickets :many SELECT * -FROM ticket_with_outcomes; +FROM ticket_with_outcomes +WHERE ( + company_id = sqlc.narg('company_id') + OR sqlc.narg('company_id') IS NULL + ); -- name: GetTicketByID :one SELECT * FROM ticket_with_outcomes @@ -60,6 +64,7 @@ where created_at < now() - interval '1 day'; Delete from ticket_outcomes where ticket_id = $1; -- name: GetAllTicketsInRange :one -SELECT COUNT(*) as total_tickets, COALESCE(SUM(amount), 0) as total_amount +SELECT COUNT(*) as total_tickets, + COALESCE(SUM(amount), 0) as total_amount FROM tickets WHERE created_at BETWEEN $1 AND $2; \ No newline at end of file diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index 29d1e10..ff64087 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -19,9 +19,10 @@ INSERT INTO bets ( user_id, is_shop_bet, outcomes_hash, - fast_code + fast_code, + company_id ) -VALUES ($1, $2, $3, $4, $5, $6, $7) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, company_id, amount, total_odds, status, user_id, is_shop_bet, cashed_out, outcomes_hash, fast_code, processed, created_at, updated_at ` @@ -33,6 +34,7 @@ type CreateBetParams struct { IsShopBet bool `json:"is_shop_bet"` OutcomesHash string `json:"outcomes_hash"` FastCode string `json:"fast_code"` + CompanyID int64 `json:"company_id"` } func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, error) { @@ -44,6 +46,7 @@ func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, erro arg.IsShopBet, arg.OutcomesHash, arg.FastCode, + arg.CompanyID, ) var i Bet err := row.Scan( @@ -112,27 +115,32 @@ wHERE ( OR $2 IS NULL ) AND ( - cashed_out = $3 + company_id = $3 OR $3 IS NULL ) AND ( - full_name ILIKE '%' || $4 || '%' - OR phone_number ILIKE '%' || $4 || '%' + cashed_out = $4 OR $4 IS NULL ) AND ( - created_at > $5 + full_name ILIKE '%' || $5 || '%' + OR phone_number ILIKE '%' || $5 || '%' OR $5 IS NULL ) AND ( - created_at < $6 + created_at > $6 OR $6 IS NULL ) + AND ( + created_at < $7 + OR $7 IS NULL + ) ` type GetAllBetsParams struct { UserID pgtype.Int8 `json:"user_id"` IsShopBet pgtype.Bool `json:"is_shop_bet"` + CompanyID pgtype.Int8 `json:"company_id"` CashedOut pgtype.Bool `json:"cashed_out"` Query pgtype.Text `json:"query"` CreatedBefore pgtype.Timestamp `json:"created_before"` @@ -143,6 +151,7 @@ func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWi rows, err := q.db.Query(ctx, GetAllBets, arg.UserID, arg.IsShopBet, + arg.CompanyID, arg.CashedOut, arg.Query, arg.CreatedBefore, diff --git a/gen/db/custom_odds.sql.go b/gen/db/custom_odds.sql.go deleted file mode 100644 index efc1f02..0000000 --- a/gen/db/custom_odds.sql.go +++ /dev/null @@ -1,167 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.29.0 -// source: custom_odds.sql - -package dbgen - -import ( - "context" - - "github.com/jackc/pgx/v5/pgtype" -) - -const DeleteCustomOddByEventID = `-- name: DeleteCustomOddByEventID :exec -DELETE FROM custom_odds_market -WHERE event_id = $1 -` - -func (q *Queries) DeleteCustomOddByEventID(ctx context.Context, eventID string) error { - _, err := q.db.Exec(ctx, DeleteCustomOddByEventID, eventID) - return err -} - -const DeleteCustomOddsByID = `-- name: DeleteCustomOddsByID :exec -DELETE FROM custom_odds_market -WHERE id = $1 -` - -func (q *Queries) DeleteCustomOddsByID(ctx context.Context, id int64) error { - _, err := q.db.Exec(ctx, DeleteCustomOddsByID, id) - return err -} - -const DeleteCustomOddsByOddID = `-- name: DeleteCustomOddsByOddID :exec -DELETE FROM custom_odds_market -WHERE odds_market_id = $1 - AND company_id = $2 -` - -type DeleteCustomOddsByOddIDParams struct { - OddsMarketID int64 `json:"odds_market_id"` - CompanyID int64 `json:"company_id"` -} - -func (q *Queries) DeleteCustomOddsByOddID(ctx context.Context, arg DeleteCustomOddsByOddIDParams) error { - _, err := q.db.Exec(ctx, DeleteCustomOddsByOddID, arg.OddsMarketID, arg.CompanyID) - return err -} - -const GetAllCustomOdds = `-- name: GetAllCustomOdds :many -SELECT id, company_id, odds_market_id, event_id, raw_odds, created_at -FROM custom_odds_market -WHERE ( - company_id = $1 - OR $1 IS NULL - ) -` - -func (q *Queries) GetAllCustomOdds(ctx context.Context, companyID pgtype.Int8) ([]CustomOddsMarket, error) { - rows, err := q.db.Query(ctx, GetAllCustomOdds, companyID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []CustomOddsMarket - for rows.Next() { - var i CustomOddsMarket - if err := rows.Scan( - &i.ID, - &i.CompanyID, - &i.OddsMarketID, - &i.EventID, - &i.RawOdds, - &i.CreatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const GetCustomOddByID = `-- name: GetCustomOddByID :one -SELECT id, company_id, odds_market_id, event_id, raw_odds, created_at -FROM custom_odds_market -WHERE id = $1 -` - -func (q *Queries) GetCustomOddByID(ctx context.Context, id int64) (CustomOddsMarket, error) { - row := q.db.QueryRow(ctx, GetCustomOddByID, id) - var i CustomOddsMarket - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.OddsMarketID, - &i.EventID, - &i.RawOdds, - &i.CreatedAt, - ) - return i, err -} - -const GetCustomOddByOddID = `-- name: GetCustomOddByOddID :one -SELECT id, company_id, odds_market_id, event_id, raw_odds, created_at -FROM custom_odds_market -WHERE odds_market_id = $1 - AND company_id = $2 -` - -type GetCustomOddByOddIDParams struct { - OddsMarketID int64 `json:"odds_market_id"` - CompanyID int64 `json:"company_id"` -} - -func (q *Queries) GetCustomOddByOddID(ctx context.Context, arg GetCustomOddByOddIDParams) (CustomOddsMarket, error) { - row := q.db.QueryRow(ctx, GetCustomOddByOddID, arg.OddsMarketID, arg.CompanyID) - var i CustomOddsMarket - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.OddsMarketID, - &i.EventID, - &i.RawOdds, - &i.CreatedAt, - ) - return i, err -} - -const InsertCustomOddsMarket = `-- name: InsertCustomOddsMarket :one -INSERT INTO custom_odds_market ( - odds_market_id, - company_id, - event_id, - raw_odds - ) -VALUES ($1, $2, $3, $4) -RETURNING id, company_id, odds_market_id, event_id, raw_odds, created_at -` - -type InsertCustomOddsMarketParams struct { - OddsMarketID int64 `json:"odds_market_id"` - CompanyID int64 `json:"company_id"` - EventID string `json:"event_id"` - RawOdds []byte `json:"raw_odds"` -} - -func (q *Queries) InsertCustomOddsMarket(ctx context.Context, arg InsertCustomOddsMarketParams) (CustomOddsMarket, error) { - row := q.db.QueryRow(ctx, InsertCustomOddsMarket, - arg.OddsMarketID, - arg.CompanyID, - arg.EventID, - arg.RawOdds, - ) - var i CustomOddsMarket - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.OddsMarketID, - &i.EventID, - &i.RawOdds, - &i.CreatedAt, - ) - return i, err -} diff --git a/gen/db/models.go b/gen/db/models.go index 460b84d..fc95871 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -240,6 +240,14 @@ type CompanyOddSetting struct { UpdatedAt pgtype.Timestamp `json:"updated_at"` } +type CompanySetting struct { + CompanyID int64 `json:"company_id"` + Key string `json:"key"` + Value string `json:"value"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` +} + type CustomerWallet struct { ID int64 `json:"id"` CustomerID int64 `json:"customer_id"` @@ -412,6 +420,13 @@ type Flag struct { Resolved pgtype.Bool `json:"resolved"` } +type GlobalSetting struct { + Key string `json:"key"` + Value string `json:"value"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` +} + type League struct { ID int64 `json:"id"` Name string `json:"name"` @@ -502,7 +517,6 @@ type OddsMarketWithSetting struct { MarketName string `json:"market_name"` MarketCategory string `json:"market_category"` MarketID string `json:"market_id"` - RawOdds []byte `json:"raw_odds"` DefaultIsActive bool `json:"default_is_active"` FetchedAt pgtype.Timestamp `json:"fetched_at"` ExpiresAt pgtype.Timestamp `json:"expires_at"` @@ -604,13 +618,6 @@ type ResultLog struct { UpdatedAt pgtype.Timestamp `json:"updated_at"` } -type Setting struct { - Key string `json:"key"` - Value string `json:"value"` - CreatedAt pgtype.Timestamp `json:"created_at"` - UpdatedAt pgtype.Timestamp `json:"updated_at"` -} - type ShopBet struct { ID int64 `json:"id"` ShopTransactionID int64 `json:"shop_transaction_id"` diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index 966fcff..33fcde8 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -68,7 +68,7 @@ func (q *Queries) GetAllOdds(ctx context.Context, arg GetAllOddsParams) ([]OddsM } const GetAllOddsWithSettings = `-- name: GetAllOddsWithSettings :many -SELECT id, event_id, market_type, market_name, market_category, market_id, odds_market_with_settings.raw_odds, default_is_active, fetched_at, expires_at, company_id, is_active, odds_market_with_settings.raw_odds, updated_at +SELECT id, event_id, market_type, market_name, market_category, market_id, default_is_active, fetched_at, expires_at, company_id, is_active, raw_odds, updated_at FROM odds_market_with_settings WHERE company_id = $1 LIMIT $3 OFFSET $2 @@ -96,7 +96,6 @@ func (q *Queries) GetAllOddsWithSettings(ctx context.Context, arg GetAllOddsWith &i.MarketName, &i.MarketCategory, &i.MarketID, - &i.RawOdds, &i.DefaultIsActive, &i.FetchedAt, &i.ExpiresAt, @@ -220,7 +219,7 @@ func (q *Queries) GetOddsByMarketID(ctx context.Context, arg GetOddsByMarketIDPa } const GetOddsWithSettingsByEventID = `-- name: GetOddsWithSettingsByEventID :many -SELECT id, event_id, market_type, market_name, market_category, market_id, odds_market_with_settings.raw_odds, default_is_active, fetched_at, expires_at, company_id, is_active, odds_market_with_settings.raw_odds, updated_at +SELECT id, event_id, market_type, market_name, market_category, market_id, default_is_active, fetched_at, expires_at, company_id, is_active, raw_odds, updated_at FROM odds_market_with_settings WHERE event_id = $1 AND company_id = $2 @@ -255,7 +254,6 @@ func (q *Queries) GetOddsWithSettingsByEventID(ctx context.Context, arg GetOddsW &i.MarketName, &i.MarketCategory, &i.MarketID, - &i.RawOdds, &i.DefaultIsActive, &i.FetchedAt, &i.ExpiresAt, @@ -275,7 +273,7 @@ func (q *Queries) GetOddsWithSettingsByEventID(ctx context.Context, arg GetOddsW } const GetOddsWithSettingsByMarketID = `-- name: GetOddsWithSettingsByMarketID :one -SELECT id, event_id, market_type, market_name, market_category, market_id, odds_market_with_settings.raw_odds, default_is_active, fetched_at, expires_at, company_id, is_active, odds_market_with_settings.raw_odds, updated_at +SELECT id, event_id, market_type, market_name, market_category, market_id, default_is_active, fetched_at, expires_at, company_id, is_active, raw_odds, updated_at FROM odds_market_with_settings WHERE market_id = $1 AND event_id = $2 @@ -298,7 +296,6 @@ func (q *Queries) GetOddsWithSettingsByMarketID(ctx context.Context, arg GetOdds &i.MarketName, &i.MarketCategory, &i.MarketID, - &i.RawOdds, &i.DefaultIsActive, &i.FetchedAt, &i.ExpiresAt, diff --git a/gen/db/settings.sql.go b/gen/db/settings.sql.go index 8a9281b..f67fecc 100644 --- a/gen/db/settings.sql.go +++ b/gen/db/settings.sql.go @@ -7,17 +7,140 @@ package dbgen import ( "context" + + "github.com/jackc/pgx/v5/pgtype" ) -const GetSetting = `-- name: GetSetting :one -SELECT key, value, created_at, updated_at -FROM settings +const DeleteAllCompanySetting = `-- name: DeleteAllCompanySetting :exec +DELETE FROM company_settings +WHERE company_id = $1 +` + +func (q *Queries) DeleteAllCompanySetting(ctx context.Context, companyID int64) error { + _, err := q.db.Exec(ctx, DeleteAllCompanySetting, companyID) + return err +} + +const DeleteCompanySetting = `-- name: DeleteCompanySetting :exec +DELETE FROM company_settings +WHERE company_id = $1 + AND key = $2 +` + +type DeleteCompanySettingParams struct { + CompanyID int64 `json:"company_id"` + Key string `json:"key"` +} + +func (q *Queries) DeleteCompanySetting(ctx context.Context, arg DeleteCompanySettingParams) error { + _, err := q.db.Exec(ctx, DeleteCompanySetting, arg.CompanyID, arg.Key) + return err +} + +const GetAllCompanySettings = `-- name: GetAllCompanySettings :many +SELECT company_id, key, value, created_at, updated_at +FROM company_settings +` + +func (q *Queries) GetAllCompanySettings(ctx context.Context) ([]CompanySetting, error) { + rows, err := q.db.Query(ctx, GetAllCompanySettings) + if err != nil { + return nil, err + } + defer rows.Close() + var items []CompanySetting + for rows.Next() { + var i CompanySetting + if err := rows.Scan( + &i.CompanyID, + &i.Key, + &i.Value, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetCompanySetting = `-- name: GetCompanySetting :many +SELECT company_id, key, value, created_at, updated_at +FROM company_settings +WHERE company_id = $1 +` + +func (q *Queries) GetCompanySetting(ctx context.Context, companyID int64) ([]CompanySetting, error) { + rows, err := q.db.Query(ctx, GetCompanySetting, companyID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []CompanySetting + for rows.Next() { + var i CompanySetting + if err := rows.Scan( + &i.CompanyID, + &i.Key, + &i.Value, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetCompanySettingsByKey = `-- name: GetCompanySettingsByKey :many +SELECT company_id, key, value, created_at, updated_at +FROM company_settings WHERE key = $1 ` -func (q *Queries) GetSetting(ctx context.Context, key string) (Setting, error) { - row := q.db.QueryRow(ctx, GetSetting, key) - var i Setting +func (q *Queries) GetCompanySettingsByKey(ctx context.Context, key string) ([]CompanySetting, error) { + rows, err := q.db.Query(ctx, GetCompanySettingsByKey, key) + if err != nil { + return nil, err + } + defer rows.Close() + var items []CompanySetting + for rows.Next() { + var i CompanySetting + if err := rows.Scan( + &i.CompanyID, + &i.Key, + &i.Value, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetGlobalSetting = `-- name: GetGlobalSetting :one +SELECT key, value, created_at, updated_at +FROM global_settings +WHERE key = $1 +` + +func (q *Queries) GetGlobalSetting(ctx context.Context, key string) (GlobalSetting, error) { + row := q.db.QueryRow(ctx, GetGlobalSetting, key) + var i GlobalSetting err := row.Scan( &i.Key, &i.Value, @@ -27,20 +150,20 @@ func (q *Queries) GetSetting(ctx context.Context, key string) (Setting, error) { return i, err } -const GetSettings = `-- name: GetSettings :many +const GetGlobalSettings = `-- name: GetGlobalSettings :many SELECT key, value, created_at, updated_at -FROM settings +FROM global_settings ` -func (q *Queries) GetSettings(ctx context.Context) ([]Setting, error) { - rows, err := q.db.Query(ctx, GetSettings) +func (q *Queries) GetGlobalSettings(ctx context.Context) ([]GlobalSetting, error) { + rows, err := q.db.Query(ctx, GetGlobalSettings) if err != nil { return nil, err } defer rows.Close() - var items []Setting + var items []GlobalSetting for rows.Next() { - var i Setting + var i GlobalSetting if err := rows.Scan( &i.Key, &i.Value, @@ -57,19 +180,79 @@ func (q *Queries) GetSettings(ctx context.Context) ([]Setting, error) { return items, nil } -const UpdateSetting = `-- name: UpdateSetting :exec -UPDATE settings +const GetOverrideSettings = `-- name: GetOverrideSettings :many +SELECT gs.key, gs.value, gs.created_at, gs.updated_at, + COALESCE(cs.value, gs.value) AS value +FROM global_settings gs + LEFT JOIN company_settings cs ON cs.key = gs.key + AND cs.company_id = $1 +` + +type GetOverrideSettingsRow struct { + Key string `json:"key"` + Value string `json:"value"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` + Value_2 string `json:"value_2"` +} + +func (q *Queries) GetOverrideSettings(ctx context.Context, companyID int64) ([]GetOverrideSettingsRow, error) { + rows, err := q.db.Query(ctx, GetOverrideSettings, companyID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetOverrideSettingsRow + for rows.Next() { + var i GetOverrideSettingsRow + if err := rows.Scan( + &i.Key, + &i.Value, + &i.CreatedAt, + &i.UpdatedAt, + &i.Value_2, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const InsertCompanySetting = `-- name: InsertCompanySetting :exec +INSERT INTO company_settings (company_id, key, value) +VALUES ($1, $2, $3) ON CONFLICT (company_id, key) DO +UPDATE +SET value = EXCLUDED.value +` + +type InsertCompanySettingParams struct { + CompanyID int64 `json:"company_id"` + Key string `json:"key"` + Value string `json:"value"` +} + +func (q *Queries) InsertCompanySetting(ctx context.Context, arg InsertCompanySettingParams) error { + _, err := q.db.Exec(ctx, InsertCompanySetting, arg.CompanyID, arg.Key, arg.Value) + return err +} + +const UpdateGlobalSetting = `-- name: UpdateGlobalSetting :exec +UPDATE global_settings SET value = $2, updated_at = CURRENT_TIMESTAMP WHERE key = $1 ` -type UpdateSettingParams struct { +type UpdateGlobalSettingParams struct { Key string `json:"key"` Value string `json:"value"` } -func (q *Queries) UpdateSetting(ctx context.Context, arg UpdateSettingParams) error { - _, err := q.db.Exec(ctx, UpdateSetting, arg.Key, arg.Value) +func (q *Queries) UpdateGlobalSetting(ctx context.Context, arg UpdateGlobalSettingParams) error { + _, err := q.db.Exec(ctx, UpdateGlobalSetting, arg.Key, arg.Value) return err } diff --git a/gen/db/ticket.sql.go b/gen/db/ticket.sql.go index 90e11f6..bc9bb5f 100644 --- a/gen/db/ticket.sql.go +++ b/gen/db/ticket.sql.go @@ -25,8 +25,8 @@ func (q *Queries) CountTicketByIP(ctx context.Context, ip string) (int64, error) } const CreateTicket = `-- name: CreateTicket :one -INSERT INTO tickets (amount, total_odds, ip) -VALUES ($1, $2, $3) +INSERT INTO tickets (amount, total_odds, ip, company_id) +VALUES ($1, $2, $3, $4) RETURNING id, company_id, amount, total_odds, ip, created_at, updated_at ` @@ -34,10 +34,16 @@ type CreateTicketParams struct { Amount int64 `json:"amount"` TotalOdds float32 `json:"total_odds"` Ip string `json:"ip"` + CompanyID int64 `json:"company_id"` } func (q *Queries) CreateTicket(ctx context.Context, arg CreateTicketParams) (Ticket, error) { - row := q.db.QueryRow(ctx, CreateTicket, arg.Amount, arg.TotalOdds, arg.Ip) + row := q.db.QueryRow(ctx, CreateTicket, + arg.Amount, + arg.TotalOdds, + arg.Ip, + arg.CompanyID, + ) var i Ticket err := row.Scan( &i.ID, @@ -99,10 +105,14 @@ func (q *Queries) DeleteTicketOutcome(ctx context.Context, ticketID int64) error const GetAllTickets = `-- name: GetAllTickets :many SELECT id, company_id, amount, total_odds, ip, created_at, updated_at, outcomes FROM ticket_with_outcomes +WHERE ( + company_id = $1 + OR $1 IS NULL + ) ` -func (q *Queries) GetAllTickets(ctx context.Context) ([]TicketWithOutcome, error) { - rows, err := q.db.Query(ctx, GetAllTickets) +func (q *Queries) GetAllTickets(ctx context.Context, companyID pgtype.Int8) ([]TicketWithOutcome, error) { + rows, err := q.db.Query(ctx, GetAllTickets, companyID) if err != nil { return nil, err } @@ -131,7 +141,8 @@ func (q *Queries) GetAllTickets(ctx context.Context) ([]TicketWithOutcome, error } const GetAllTicketsInRange = `-- name: GetAllTicketsInRange :one -SELECT COUNT(*) as total_tickets, COALESCE(SUM(amount), 0) as total_amount +SELECT COUNT(*) as total_tickets, + COALESCE(SUM(amount), 0) as total_amount FROM tickets WHERE created_at BETWEEN $1 AND $2 ` diff --git a/internal/domain/bet.go b/internal/domain/bet.go index 16e55b0..6ee3734 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -2,6 +2,9 @@ package domain import ( "time" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/jackc/pgx/v5/pgtype" ) // The Odd ID here is not the odd id from our database @@ -49,6 +52,7 @@ type Bet struct { TotalOdds float32 Status OutcomeStatus UserID int64 + CompanyID int64 IsShopBet bool CashedOut bool FastCode string @@ -57,6 +61,7 @@ type Bet struct { type BetFilter struct { UserID ValidInt64 + CompanyID ValidInt64 CashedOut ValidBool IsShopBet ValidBool Query ValidString @@ -81,6 +86,7 @@ type GetBet struct { FullName string PhoneNumber string UserID int64 + CompanyID int64 IsShopBet bool CashedOut bool Outcomes []BetOutcome @@ -93,6 +99,7 @@ type CreateBet struct { TotalOdds float32 Status OutcomeStatus UserID int64 + CompanyID int64 IsShopBet bool OutcomesHash string FastCode string @@ -133,6 +140,7 @@ type CreateBetRes struct { TotalOdds float32 `json:"total_odds" example:"4.22"` Status OutcomeStatus `json:"status" example:"1"` UserID int64 `json:"user_id" example:"2"` + CompanyID int64 `json:"company_id" example:"1"` IsShopBet bool `json:"is_shop_bet" example:"false"` CreatedNumber int64 `json:"created_number" example:"2"` FastCode string `json:"fast_code"` @@ -145,19 +153,21 @@ type BetRes struct { Status OutcomeStatus `json:"status" example:"1"` Fullname string `json:"full_name" example:"John Smith"` UserID int64 `json:"user_id" example:"2"` + CompanyID int64 `json:"company_id" example:"1"` IsShopBet bool `json:"is_shop_bet" example:"false"` CashedOut bool `json:"cashed_out" example:"false"` CreatedAt time.Time `json:"created_at" example:"2025-04-08T12:00:00Z"` FastCode string `json:"fast_code"` } -func ConvertCreateBet(bet Bet, createdNumber int64) CreateBetRes { +func ConvertCreateBetRes(bet Bet, createdNumber int64) CreateBetRes { return CreateBetRes{ ID: bet.ID, Amount: bet.Amount.Float32(), TotalOdds: bet.TotalOdds, Status: bet.Status, UserID: bet.UserID, + CompanyID: bet.CompanyID, CreatedNumber: createdNumber, IsShopBet: bet.IsShopBet, FastCode: bet.FastCode, @@ -172,6 +182,7 @@ func ConvertBet(bet GetBet) BetRes { Status: bet.Status, Fullname: bet.FullName, UserID: bet.UserID, + CompanyID: bet.CompanyID, Outcomes: bet.Outcomes, IsShopBet: bet.IsShopBet, CashedOut: bet.CashedOut, @@ -179,3 +190,106 @@ func ConvertBet(bet GetBet) BetRes { FastCode: bet.FastCode, } } + +func ConvertDBBet(bet dbgen.Bet) Bet { + return Bet{ + ID: bet.ID, + Amount: Currency(bet.Amount), + TotalOdds: bet.TotalOdds, + Status: OutcomeStatus(bet.Status), + UserID: bet.UserID, + CompanyID: bet.CompanyID, + IsShopBet: bet.IsShopBet, + CashedOut: bet.CashedOut, + FastCode: bet.FastCode, + CreatedAt: bet.CreatedAt.Time, + } +} + +func ConvertDBBetOutcomes(outcome dbgen.BetOutcome) BetOutcome { + return BetOutcome{ + ID: outcome.ID, + BetID: outcome.BetID, + SportID: outcome.SportID, + EventID: outcome.EventID, + OddID: outcome.OddID, + HomeTeamName: outcome.HomeTeamName, + AwayTeamName: outcome.AwayTeamName, + MarketID: outcome.MarketID, + MarketName: outcome.MarketName, + Odd: outcome.Odd, + OddName: outcome.OddName, + OddHeader: outcome.OddHeader, + OddHandicap: outcome.OddHandicap, + Status: OutcomeStatus(outcome.Status), + Expires: outcome.Expires.Time, + } +} + +func ConvertDBBetWithOutcomes(bet dbgen.BetWithOutcome) GetBet { + var outcomes []BetOutcome = make([]BetOutcome, 0, len(bet.Outcomes)) + + for _, outcome := range bet.Outcomes { + outcomes = append(outcomes, ConvertDBBetOutcomes(outcome)) + } + + return GetBet{ + ID: bet.ID, + Amount: Currency(bet.Amount), + TotalOdds: bet.TotalOdds, + Status: OutcomeStatus(bet.Status), + FullName: bet.FullName.(string), + PhoneNumber: bet.PhoneNumber.String, + UserID: bet.UserID, + IsShopBet: bet.IsShopBet, + CashedOut: bet.CashedOut, + Outcomes: outcomes, + FastCode: bet.FastCode, + CreatedAt: bet.CreatedAt.Time, + } +} + +func ConvertDBFlag(flag dbgen.Flag) Flag { + return Flag{ + ID: flag.ID, + BetID: flag.BetID.Int64, + OddID: flag.OddsMarketID.Int64, + Reason: flag.Reason.String, + FlaggedAt: flag.FlaggedAt.Time, + Resolved: flag.Resolved.Bool, + } +} + +func ConvertDBCreateBetOutcome(betOutcome CreateBetOutcome) dbgen.CreateBetOutcomeParams { + return dbgen.CreateBetOutcomeParams{ + BetID: betOutcome.BetID, + EventID: betOutcome.EventID, + SportID: betOutcome.SportID, + OddID: betOutcome.OddID, + HomeTeamName: betOutcome.HomeTeamName, + AwayTeamName: betOutcome.AwayTeamName, + MarketID: betOutcome.MarketID, + MarketName: betOutcome.MarketName, + Odd: betOutcome.Odd, + OddName: betOutcome.OddName, + OddHeader: betOutcome.OddHeader, + OddHandicap: betOutcome.OddHandicap, + Expires: pgtype.Timestamp{ + Time: betOutcome.Expires, + Valid: true, + }, + } +} + +func ConvertCreateBet(bet CreateBet) dbgen.CreateBetParams { + return dbgen.CreateBetParams{ + Amount: int64(bet.Amount), + TotalOdds: bet.TotalOdds, + Status: int32(bet.Status), + UserID: bet.UserID, + CompanyID: bet.CompanyID, + IsShopBet: bet.IsShopBet, + OutcomesHash: bet.OutcomesHash, + FastCode: bet.FastCode, + } +} diff --git a/internal/domain/common_log.go b/internal/domain/common_log.go index a7cda24..43763a4 100644 --- a/internal/domain/common_log.go +++ b/internal/domain/common_log.go @@ -21,14 +21,14 @@ var SuccessResZapFields = []zap.Field{ zap.Time("timestamp", time.Now()), } -var BadRequestLogger = MongoDBLogger.With( - zap.Int("status_code", fiber.StatusBadRequest), - zap.Time("timestamp", time.Now())) +// var BadRequestLogger = MongoDBLogger.With( +// zap.Int("status_code", fiber.StatusBadRequest), +// zap.Time("timestamp", time.Now())) -var InternalServerErrorLogger = MongoDBLogger.With( - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Time("timestamp", time.Now())) +// var InternalServerErrorLogger = MongoDBLogger.With( +// zap.Int("status_code", fiber.StatusInternalServerError), +// zap.Time("timestamp", time.Now())) -var SuccessResLogger = MongoDBLogger.With( - zap.Int("status_code", fiber.StatusOK), - zap.Time("timestamp", time.Now())) +// var SuccessResLogger = MongoDBLogger.With( +// zap.Int("status_code", fiber.StatusOK), +// zap.Time("timestamp", time.Now())) diff --git a/internal/domain/currency.go b/internal/domain/currency.go index e436a87..7ce3b3c 100644 --- a/internal/domain/currency.go +++ b/internal/domain/currency.go @@ -3,28 +3,26 @@ package domain import ( "errors" "fmt" + "math" "time" ) type Currency int64 -// ToCurrency converts a float32 to Currency +// ToCurrency converts a float32 (like 12.34) into Currency (stored in cents). func ToCurrency(f float32) Currency { - return Currency((f * 100) + 0.5) + cents := math.Round(float64(f) * 100) // avoid float32 precision issues + return Currency(int64(cents)) } -// Float32 converts a Currency to float32 +// Float32 converts a Currency back into float32 (like 12.34). func (m Currency) Float32() float32 { - x := float32(m) - x = x / 100 - return x + return float32(m) / 100 } -// String returns a formatted Currency value +// String returns a formatted Currency value for display. func (m Currency) String() string { - x := float32(m) - x = x / 100 - return fmt.Sprintf("$%.2f", x) + return fmt.Sprintf("$%.2f", m.Float32()) } type IntCurrency string diff --git a/internal/domain/custom_odds.go b/internal/domain/custom_odds.go index 1a0a171..6163c70 100644 --- a/internal/domain/custom_odds.go +++ b/internal/domain/custom_odds.go @@ -1,69 +1,69 @@ package domain -import ( - "encoding/json" - "time" +// import ( +// "encoding/json" +// "time" - dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" -) +// dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" +// ) -type CustomOdd struct { - ID int64 - OddID int64 - CompanyID int64 - EventID string - RawOdds []json.RawMessage - CreatedAt time.Time -} +// type CustomOdd struct { +// ID int64 +// OddID int64 +// CompanyID int64 +// EventID string +// RawOdds []json.RawMessage +// CreatedAt time.Time +// } -type CreateCustomOdd struct { - OddID int64 - CompanyID int64 - EventID string - RawOdds []json.RawMessage -} +// type CreateCustomOdd struct { +// OddID int64 +// CompanyID int64 +// EventID string +// RawOdds []json.RawMessage +// } -type CustomOddFilter struct { - CompanyID ValidInt64 -} +// type CustomOddFilter struct { +// CompanyID ValidInt64 +// } -func ConvertCreateCustomOdd(odd CreateCustomOdd) (dbgen.InsertCustomOddParams, error) { - rawOddsBytes, err := json.Marshal(odd.RawOdds) +// func ConvertCreateCustomOdd(odd CreateCustomOdd) (dbgen.InsertCustomOddParams, error) { +// rawOddsBytes, err := json.Marshal(odd.RawOdds) - if err != nil { - return dbgen.InsertCustomOddParams{}, err - } - return dbgen.InsertCustomOddParams{ - OddID: odd.OddID, - CompanyID: odd.CompanyID, - EventID: odd.EventID, - RawOdds: rawOddsBytes, - }, nil -} +// if err != nil { +// return dbgen.InsertCustomOddParams{}, err +// } +// return dbgen.InsertCustomOddParams{ +// OddID: odd.OddID, +// CompanyID: odd.CompanyID, +// EventID: odd.EventID, +// RawOdds: rawOddsBytes, +// }, nil +// } -func ConvertDBCustomOdd(dbCustomOdd dbgen.CustomOdd) (CustomOdd, error) { - var rawOdds []json.RawMessage - if err := json.Unmarshal(dbCustomOdd.RawOdds, &rawOdds); err != nil { - return CustomOdd{}, err - } - return CustomOdd{ - ID: dbCustomOdd.ID, - OddID: dbCustomOdd.OddID, - CompanyID: dbCustomOdd.CompanyID, - EventID: dbCustomOdd.EventID, - RawOdds: rawOdds, - CreatedAt: dbCustomOdd.CreatedAt.Time, - }, nil -} +// func ConvertDBCustomOdd(dbCustomOdd dbgen.CustomOdd) (CustomOdd, error) { +// var rawOdds []json.RawMessage +// if err := json.Unmarshal(dbCustomOdd.RawOdds, &rawOdds); err != nil { +// return CustomOdd{}, err +// } +// return CustomOdd{ +// ID: dbCustomOdd.ID, +// OddID: dbCustomOdd.OddID, +// CompanyID: dbCustomOdd.CompanyID, +// EventID: dbCustomOdd.EventID, +// RawOdds: rawOdds, +// CreatedAt: dbCustomOdd.CreatedAt.Time, +// }, nil +// } -func ConvertDbCustomOdds(list []dbgen.CustomOdd) ([]CustomOdd, error) { - result := make([]CustomOdd, 0, len(list)) - for _, item := range list { - convertedItem, err := ConvertDBCustomOdd(item) - if err != nil { - return nil, err - } - result = append(result, convertedItem) - } - return result, nil -} +// func ConvertDbCustomOdds(list []dbgen.CustomOdd) ([]CustomOdd, error) { +// result := make([]CustomOdd, 0, len(list)) +// for _, item := range list { +// convertedItem, err := ConvertDBCustomOdd(item) +// if err != nil { +// return nil, err +// } +// result = append(result, convertedItem) +// } +// return result, nil +// } diff --git a/internal/domain/disabled_odds.go b/internal/domain/disabled_odds.go index c4f731f..2a3c83c 100644 --- a/internal/domain/disabled_odds.go +++ b/internal/domain/disabled_odds.go @@ -7,38 +7,38 @@ import ( ) type DisabledOdd struct { - ID int64 - OddID int64 - RawOddID int64 - EventID string - CompanyID int64 - CreatedAt time.Time + ID int64 + OddMarketID int64 + RawOddID int64 + EventID string + CompanyID int64 + CreatedAt time.Time } type CreateDisabledOdd struct { - OddID int64 - RawOddID int64 - EventID string - CompanyID int64 + OddMarketID int64 + RawOddID int64 + EventID string + CompanyID int64 } func ConvertCreateDisabledOdd(odd CreateDisabledOdd) dbgen.InsertDisabledOddsParams { return dbgen.InsertDisabledOddsParams{ - OddID: odd.OddID, - EventID: odd.EventID, - CompanyID: odd.CompanyID, - RawOddID: odd.RawOddID, + OddsMarketID: odd.OddMarketID, + EventID: odd.EventID, + CompanyID: odd.CompanyID, + RawOddID: odd.RawOddID, } } func ConvertDBDisabledOdd(dbDisabledOdd dbgen.DisabledOdd) DisabledOdd { return DisabledOdd{ - ID: dbDisabledOdd.ID, - OddID: dbDisabledOdd.OddID, - RawOddID: dbDisabledOdd.RawOddID, - EventID: dbDisabledOdd.EventID, - CompanyID: dbDisabledOdd.CompanyID, - CreatedAt: dbDisabledOdd.CreatedAt.Time, + ID: dbDisabledOdd.ID, + OddMarketID: dbDisabledOdd.OddsMarketID, + RawOddID: dbDisabledOdd.RawOddID, + EventID: dbDisabledOdd.EventID, + CompanyID: dbDisabledOdd.CompanyID, + CreatedAt: dbDisabledOdd.CreatedAt.Time, } } diff --git a/internal/domain/result.go b/internal/domain/result.go index 69f74fc..5369a2b 100644 --- a/internal/domain/result.go +++ b/internal/domain/result.go @@ -23,7 +23,7 @@ type Result struct { FullTimeScore string HalfTimeScore string SS string - Scores map[string]Score + Scores map[string]ScoreResultResponse CreatedAt time.Time UpdatedAt time.Time } diff --git a/internal/domain/role.go b/internal/domain/role.go index 2643782..dcd2c57 100644 --- a/internal/domain/role.go +++ b/internal/domain/role.go @@ -9,3 +9,12 @@ const ( RoleCustomer Role = "customer" RoleCashier Role = "cashier" ) + +func (r Role) IsValid() bool { + switch r { + case RoleSuperAdmin, RoleAdmin, RoleBranchManager, RoleCustomer, RoleCashier: + return true + default: + return false + } +} diff --git a/internal/domain/setting_list.go b/internal/domain/setting_list.go new file mode 100644 index 0000000..87ac482 --- /dev/null +++ b/internal/domain/setting_list.go @@ -0,0 +1,447 @@ +package domain + +import ( + "errors" + "fmt" + "strconv" + "strings" + "time" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "go.uber.org/zap" +) + +var ( + ErrSettingNotFound = errors.New("cannot find setting in list") +) + +type SettingList struct { + SMSProvider SMSProvider `json:"sms_provider"` + MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"` + BetAmountLimit Currency `json:"bet_amount_limit"` + DailyTicketPerIP int64 `json:"daily_ticket_limit"` + TotalWinningLimit Currency `json:"total_winning_limit"` + AmountForBetReferral Currency `json:"amount_for_bet_referral"` + CashbackAmountCap Currency `json:"cashback_amount_cap"` +} + +type SettingListRes struct { + SMSProvider SMSProvider `json:"sms_provider"` + MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"` + BetAmountLimit float32 `json:"bet_amount_limit"` + DailyTicketPerIP int64 `json:"daily_ticket_limit"` + TotalWinningLimit float32 `json:"total_winning_limit"` + AmountForBetReferral float32 `json:"amount_for_bet_referral"` + CashbackAmountCap float32 `json:"cashback_amount_cap"` +} + +type SaveSettingListReq struct { + SMSProvider *string `json:"sms_provider,omitempty"` + MaxNumberOfOutcomes *int64 `json:"max_number_of_outcomes,omitempty"` + BetAmountLimit *float32 `json:"bet_amount_limit,omitempty"` + DailyTicketPerIP *int64 `json:"daily_ticket_limit,omitempty"` + TotalWinningLimit *float32 `json:"total_winning_limit,omitempty"` + AmountForBetReferral *float32 `json:"amount_for_bet_referral,omitempty"` + CashbackAmountCap *float32 `json:"cashback_amount_cap,omitempty"` +} + +func ConvertSaveSettingListReq(settings SaveSettingListReq) ValidSettingList { + return ValidSettingList{ + SMSProvider: ConvertStringPtr(settings.SMSProvider), + MaxNumberOfOutcomes: ConvertInt64Ptr(settings.MaxNumberOfOutcomes), + BetAmountLimit: ConvertFloat32PtrToCurrency(settings.BetAmountLimit), + DailyTicketPerIP: ConvertInt64Ptr(settings.DailyTicketPerIP), + TotalWinningLimit: ConvertFloat32PtrToCurrency(settings.TotalWinningLimit), + AmountForBetReferral: ConvertFloat32PtrToCurrency(settings.AmountForBetReferral), + CashbackAmountCap: ConvertFloat32PtrToCurrency(settings.CashbackAmountCap), + } +} + +type ValidSettingList struct { + SMSProvider ValidString + MaxNumberOfOutcomes ValidInt64 + BetAmountLimit ValidCurrency + DailyTicketPerIP ValidInt64 + TotalWinningLimit ValidCurrency + AmountForBetReferral ValidCurrency + CashbackAmountCap ValidCurrency +} + +// Always make sure to run the validation before converting this +func (vsl *ValidSettingList) ToSettingList() SettingList { + return SettingList{ + SMSProvider: SMSProvider(vsl.SMSProvider.Value), + MaxNumberOfOutcomes: vsl.MaxNumberOfOutcomes.Value, + BetAmountLimit: Currency(vsl.BetAmountLimit.Value), + DailyTicketPerIP: vsl.DailyTicketPerIP.Value, + TotalWinningLimit: Currency(vsl.TotalWinningLimit.Value), + AmountForBetReferral: Currency(vsl.AmountForBetReferral.Value), + CashbackAmountCap: Currency(vsl.CashbackAmountCap.Value), + } +} + +// Custom Validations for non-generic types +func (vsl *ValidSettingList) CustomValidationSettings() error { + if !SMSProvider(vsl.SMSProvider.Value).IsValid() { + return fmt.Errorf("sms provider invalid") + } + return nil +} + +func (vsl *ValidSettingList) GetInt64SettingsMap() map[string]*ValidInt64 { + return map[string]*ValidInt64{ + "max_number_of_outcomes": &vsl.MaxNumberOfOutcomes, + "daily_ticket_limit": &vsl.DailyTicketPerIP, + } +} + +func (vsl *ValidSettingList) GetCurrencySettingsMap() map[string]*ValidCurrency { + return map[string]*ValidCurrency{ + "bet_amount_limit": &vsl.BetAmountLimit, + "total_winnings_limit": &vsl.TotalWinningLimit, + "amount_for_bet_referral": &vsl.AmountForBetReferral, + "cashback_amount_cap": &vsl.CashbackAmountCap, + } +} + +func (vsl *ValidSettingList) GetStringSettingsMap() map[string]*ValidString { + return map[string]*ValidString{ + "sms_provider": &vsl.SMSProvider, + } +} + +func (vsl *ValidSettingList) GetBoolSettingsMap() map[string]*ValidBool { + return map[string]*ValidBool{} +} + +func (vsl *ValidSettingList) GetFloat32SettingsMap() map[string]*ValidFloat32 { + return map[string]*ValidFloat32{} +} + +func (vsl *ValidSettingList) GetTimeSettingsMap() map[string]*ValidTime { + return map[string]*ValidTime{} +} + +func (vsl *ValidSettingList) GetTotalSettings() int { + return len(vsl.GetInt64SettingsMap()) + + len(vsl.GetCurrencySettingsMap()) + + len(vsl.GetStringSettingsMap()) + + len(vsl.GetBoolSettingsMap()) + + len(vsl.GetFloat32SettingsMap()) + + len(vsl.GetTimeSettingsMap()) +} + +func (vsl *ValidSettingList) GetAllValid() map[string]*bool { + + settingValid := make(map[string]*bool) + + for key, setting := range vsl.GetInt64SettingsMap() { + settingValid[key] = &(*setting).Valid + } + for key, setting := range vsl.GetCurrencySettingsMap() { + settingValid[key] = &(*setting).Valid + } + for key, setting := range vsl.GetStringSettingsMap() { + settingValid[key] = &(*setting).Valid + } + for key, setting := range vsl.GetBoolSettingsMap() { + settingValid[key] = &(*setting).Valid + } + for key, setting := range vsl.GetFloat32SettingsMap() { + settingValid[key] = &(*setting).Valid + } + for key, setting := range vsl.GetTimeSettingsMap() { + settingValid[key] = &(*setting).Valid + } + + return settingValid +} + +func setValidSetting[T any](settings map[string]*T, searchKey string, setVal T) error { + for key, setting := range settings { + if key == searchKey { + *setting = setVal + } + return nil + } + return ErrSettingNotFound +} +func (vsl *ValidSettingList) SetInt64Setting(searchKey string, searchVal string) error { + value, err := strconv.ParseInt(searchVal, 10, 64) + if err != nil { + return err + } + return setValidSetting(vsl.GetInt64SettingsMap(), searchKey, ValidInt64{Value: value, Valid: true}) +} + +func (vsl *ValidSettingList) SetCurrencySetting(searchKey string, searchVal string) error { + value, err := strconv.ParseInt(searchVal, 10, 64) + if err != nil { + return err + } + return setValidSetting(vsl.GetCurrencySettingsMap(), searchKey, ValidCurrency{Value: Currency(value), Valid: true}) +} + +func (vsl *ValidSettingList) SetStringSetting(searchKey string, searchVal string) error { + return setValidSetting(vsl.GetStringSettingsMap(), searchKey, ValidString{Value: searchVal, Valid: true}) +} + +func (vsl *ValidSettingList) SetBoolSetting(searchKey string, searchVal string) error { + value, err := strconv.ParseBool(searchVal) + if err != nil { + return err + } + return setValidSetting(vsl.GetBoolSettingsMap(), searchKey, ValidBool{Value: value, Valid: true}) +} + +func (vsl *ValidSettingList) SetFloat32Setting(searchKey string, searchVal string) error { + value, err := strconv.ParseFloat(searchVal, 32) + if err != nil { + return err + } + return setValidSetting(vsl.GetFloat32SettingsMap(), searchKey, ValidFloat32{Value: float32(value), Valid: true}) +} + +func (vsl *ValidSettingList) SetTimeSetting(searchKey string, searchVal string) error { + value, err := time.Parse(time.RFC3339, searchVal) + if err != nil { + return err + } + return setValidSetting(vsl.GetTimeSettingsMap(), searchKey, ValidTime{Value: value, Valid: true}) +} + +func (vsl *ValidSettingList) SetSetting(searchKey string, searchVal string) error { + setters := []func(string, string) error{ + vsl.SetInt64Setting, + vsl.SetCurrencySetting, + vsl.SetStringSetting, + vsl.SetBoolSetting, + vsl.SetFloat32Setting, + vsl.SetTimeSetting, + } + + for _, setter := range setters { + if err := setter(searchKey, searchVal); err != nil { + if err == ErrSettingNotFound { + continue // not this setter, try the next + } + return fmt.Errorf("error while processing setting %q: %w", searchKey, err) + } + return nil // successfully set + } + + // If we get here, none of the setters matched + return ErrSettingNotFound +} + +func convertValidSettings[T any]( + settings map[string]*T, + isValid func(*T) bool, + toString func(*T) string, +) []Setting { + result := make([]Setting, 0, len(settings)) + for key, s := range settings { + if isValid(s) { + result = append(result, Setting{ + Key: key, + Value: toString(s), + }) + } + } + return result +} + +func (vsl *ValidSettingList) ConvertInt64Settings() []Setting { + return convertValidSettings( + vsl.GetInt64SettingsMap(), + func(s *ValidInt64) bool { return s.Valid }, + func(s *ValidInt64) string { return strconv.FormatInt(s.Value, 10) }, + ) +} + +func (vsl *ValidSettingList) ConvertCurrencySettings() []Setting { + return convertValidSettings( + vsl.GetCurrencySettingsMap(), + func(s *ValidCurrency) bool { return s.Valid }, + func(s *ValidCurrency) string { return strconv.FormatInt(int64(s.Value), 10) }, + ) +} + +func (vsl *ValidSettingList) ConvertStringSettings() []Setting { + return convertValidSettings( + vsl.GetStringSettingsMap(), + func(s *ValidString) bool { return s.Valid }, + func(s *ValidString) string { return s.Value }, + ) +} + +func (vsl *ValidSettingList) ConvertBoolSettings() []Setting { + return convertValidSettings( + vsl.GetBoolSettingsMap(), + func(s *ValidBool) bool { return s.Valid }, + func(s *ValidBool) string { return strconv.FormatBool(s.Value) }, + ) +} + +func (vsl *ValidSettingList) ConvertFloat32Settings() []Setting { + return convertValidSettings( + vsl.GetFloat32SettingsMap(), + func(s *ValidFloat32) bool { return s.Valid }, + func(s *ValidFloat32) string { return strconv.FormatFloat(float64(s.Value), 'f', -1, 32) }, + ) +} + +func (vsl *ValidSettingList) ConvertTimeSettings() []Setting { + return convertValidSettings( + vsl.GetTimeSettingsMap(), + func(s *ValidTime) bool { return s.Valid }, + func(s *ValidTime) string { return s.Value.Format(time.RFC3339) }, + ) +} + +func validateSettings[T any]( + settings map[string]*T, + customValidator func(*T) bool, +) error { + var errs []string + for key, s := range settings { + if !customValidator(s) { + errs = append(errs, fmt.Sprintf("%v is invalid", key)) + } + } + if len(errs) > 0 { + return fmt.Errorf(strings.Join(errs, "; ")) + } + + return nil +} + +func (vsl *ValidSettingList) ValidateInt64Settings() error { + return validateSettings(vsl.GetInt64SettingsMap(), + func(s *ValidInt64) bool { + return s.Valid + }, + ) +} + +func (vsl *ValidSettingList) ValidateCurrencySettings() error { + return validateSettings(vsl.GetCurrencySettingsMap(), + func(s *ValidCurrency) bool { + return s.Valid + }, + ) +} + +func (vsl *ValidSettingList) ValidateStringSettings() error { + return validateSettings(vsl.GetStringSettingsMap(), + func(s *ValidString) bool { + return s.Valid + }, + ) +} + +func (vsl *ValidSettingList) ValidateBoolSettings() error { + return validateSettings(vsl.GetBoolSettingsMap(), + func(s *ValidBool) bool { + return s.Valid + }, + ) +} + +func (vsl *ValidSettingList) ValidateFloat32Settings() error { + return validateSettings(vsl.GetFloat32SettingsMap(), + func(s *ValidFloat32) bool { + return s.Valid + }, + ) +} + +func (vsl *ValidSettingList) ValidateTimeSettings() error { + return validateSettings(vsl.GetTimeSettingsMap(), + func(s *ValidTime) bool { + return s.Valid + }, + ) +} + +func (vsl *ValidSettingList) ValidateAllSettings() error { + var errs []string + validators := []func() error{ + vsl.ValidateInt64Settings, + vsl.ValidateCurrencySettings, + vsl.ValidateStringSettings, + vsl.ValidateBoolSettings, + vsl.ValidateFloat32Settings, + vsl.ValidateTimeSettings, + vsl.CustomValidationSettings, + } + + for _, validator := range validators { + if err := validator(); err != nil { + errs = append(errs, err.Error()) + } + } + if len(errs) > 0 { + return fmt.Errorf(strings.Join(errs, "; ")) + } + + return nil +} + +func (vsl *ValidSettingList) ConvertAllSettings() []Setting { + totalCap := vsl.GetTotalSettings() + all := make([]Setting, 0, totalCap) + + all = append(all, vsl.ConvertInt64Settings()...) + all = append(all, vsl.ConvertCurrencySettings()...) + all = append(all, vsl.ConvertStringSettings()...) + all = append(all, vsl.ConvertBoolSettings()...) + all = append(all, vsl.ConvertFloat32Settings()...) + all = append(all, vsl.ConvertTimeSettings()...) + + return all +} + +func ConvertDBGlobalSettingList(settings []dbgen.GlobalSetting) (SettingList, error) { + var dbSettingList ValidSettingList + + for _, setting := range settings { + if err := dbSettingList.SetSetting(setting.Key, setting.Value); err != nil { + if err == ErrSettingNotFound { + MongoDBLogger.Warn("unknown setting found on database", zap.String("setting", setting.Key)) + } + } + } + + if err := dbSettingList.ValidateAllSettings(); err != nil { + fmt.Printf("setting validation error: %v \n") + MongoDBLogger.Warn("setting validation error", zap.Error(err)) + return SettingList{}, err + } + + settingList := dbSettingList.ToSettingList() + + return settingList, nil +} + +func ConvertDBOverrideSettingList(settings []dbgen.GetOverrideSettingsRow) (SettingList, error) { + var dbSettingList ValidSettingList + + for _, setting := range settings { + if err := dbSettingList.SetSetting(setting.Key, setting.Value); err != nil { + if err == ErrSettingNotFound { + MongoDBLogger.Warn("unknown setting found on database", zap.String("setting", setting.Key)) + } + } + } + + if err := dbSettingList.ValidateAllSettings(); err != nil { + fmt.Printf("setting validation error: %v \n") + MongoDBLogger.Warn("setting validation error", zap.Error(err)) + return SettingList{}, err + } + + settingList := dbSettingList.ToSettingList() + + return settingList, nil +} diff --git a/internal/domain/settings.go b/internal/domain/settings.go index eaa6256..415816c 100644 --- a/internal/domain/settings.go +++ b/internal/domain/settings.go @@ -1,8 +1,9 @@ package domain import ( - "strconv" "time" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" ) type Setting struct { @@ -16,160 +17,20 @@ type SettingRes struct { Value string `json:"value"` UpdatedAt time.Time `json:"updated_at"` } - -type UpdateSettingListReq struct { - SMSProvider *string `json:"sms_provider,omitempty"` - MaxNumberOfOutcomes *int64 `json:"max_number_of_outcomes,omitempty"` - BetAmountLimit *float32 `json:"bet_amount_limit,omitempty"` - DailyTicketPerIP *int64 `json:"daily_ticket_limit,omitempty"` - TotalWinningLimit *float32 `json:"total_winning_limit,omitempty"` - AmountForBetReferral *float32 `json:"amount_for_bet_referral,omitempty"` - CashbackAmountCap *float32 `json:"cashback_amount_cap,omitempty"` +type CompanySetting struct { + Key string + Value string + CompanyID int64 + UpdatedAt time.Time + CreatedAt time.Time } -type SettingList struct { - SMSProvider SMSProvider `json:"sms_provider"` - MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"` - BetAmountLimit Currency `json:"bet_amount_limit"` - DailyTicketPerIP int64 `json:"daily_ticket_limit"` - TotalWinningLimit Currency `json:"total_winning_limit"` - AmountForBetReferral Currency `json:"amount_for_bet_referral"` - CashbackAmountCap Currency `json:"cashback_amount_cap"` -} - -type SettingListRes struct { - SMSProvider SMSProvider `json:"sms_provider"` - MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"` - BetAmountLimit float32 `json:"bet_amount_limit"` - DailyTicketPerIP int64 `json:"daily_ticket_limit"` - TotalWinningLimit float32 `json:"total_winning_limit"` - AmountForBetReferral float32 `json:"amount_for_bet_referral"` - CashbackAmountCap float32 `json:"cashback_amount_cap"` -} - -type ValidSettingList struct { - SMSProvider ValidString - MaxNumberOfOutcomes ValidInt64 - BetAmountLimit ValidInt64 - DailyTicketPerIP ValidInt64 - TotalWinningLimit ValidInt64 - AmountForBetReferral ValidInt64 - CashbackAmountCap ValidInt64 -} - -func ConvertInt64SettingsMap(dbSettingList *ValidSettingList) map[string]*ValidInt64 { - return map[string]*ValidInt64{ - "max_number_of_outcomes": &dbSettingList.MaxNumberOfOutcomes, - "bet_amount_limit": &dbSettingList.BetAmountLimit, - "daily_ticket_limit": &dbSettingList.DailyTicketPerIP, - "total_winnings_limit": &dbSettingList.TotalWinningLimit, - "amount_for_bet_referral": &dbSettingList.AmountForBetReferral, - "cashback_amount_cap": &dbSettingList.CashbackAmountCap, - } -} - -func ConvertStringSettingsMap(dbSettingList *ValidSettingList) map[string]*ValidString { - return map[string]*ValidString{ - "sms_provider": &dbSettingList.SMSProvider, - } -} - -func ConvertBoolSettingsMap(dbSettingList *ValidSettingList) map[string]*ValidBool { - return map[string]*ValidBool{} -} - -func ConvertFloat32SettingsMap(dbSettingList *ValidSettingList) map[string]*ValidFloat32 { - return map[string]*ValidFloat32{} -} - -func ConvertTimeSettingsMap(dbSettingList *ValidSettingList) map[string]*ValidTime { - return map[string]*ValidTime{} -} - -func ValidateSettingList(dbSettingList ValidSettingList) SettingList { - // TODO: Add validation here - return SettingList{ - SMSProvider: SMSProvider(dbSettingList.SMSProvider.Value), - MaxNumberOfOutcomes: dbSettingList.MaxNumberOfOutcomes.Value, - BetAmountLimit: Currency(dbSettingList.BetAmountLimit.Value), - DailyTicketPerIP: dbSettingList.DailyTicketPerIP.Value, - TotalWinningLimit: Currency(dbSettingList.TotalWinningLimit.Value), - AmountForBetReferral: Currency(dbSettingList.AmountForBetReferral.Value), - CashbackAmountCap: Currency(dbSettingList.CashbackAmountCap.Value), - } -} - -func ConvertValidSettingList(settingList ValidSettingList) []Setting { - var convertedSettings []Setting - // if settingList.AmountForBetReferral.Valid { - // newValue := strconv.FormatInt(settingList.AmountForBetReferral.Value, 10) - // settings = append(settings, Setting{ - // Key: "amount_for_bet_referral", - // Value: newValue, - // }) - // } - - int64SettingsMap := ConvertInt64SettingsMap(&settingList) - stringSettingsMap := ConvertStringSettingsMap(&settingList) - boolSettingsMap := ConvertBoolSettingsMap(&settingList) - float32SettingsMap := ConvertFloat32SettingsMap(&settingList) - timeSettingsMap := ConvertTimeSettingsMap(&settingList) - - for key, settingPtr := range int64SettingsMap { - setting := *settingPtr - if setting.Valid { - stringVal := strconv.FormatInt(setting.Value, 10) - convertedSettings = append(convertedSettings, Setting{ - Key: key, - Value: stringVal, - }) - } - } - - for key, settingPtr := range stringSettingsMap { - setting := *settingPtr - if setting.Valid { - convertedSettings = append(convertedSettings, Setting{ - Key: key, - Value: setting.Value, - }) - } - } - - for key, settingPtr := range boolSettingsMap { - setting := *settingPtr - if setting.Valid { - stringVal := strconv.FormatBool(setting.Value) - convertedSettings = append(convertedSettings, Setting{ - Key: key, - Value: stringVal, - }) - } - } - - for key, settingPtr := range float32SettingsMap { - setting := *settingPtr - if setting.Valid { - stringVal := strconv.FormatFloat(float64(setting.Value), 'E', -1, 64) - convertedSettings = append(convertedSettings, Setting{ - Key: key, - Value: stringVal, - }) - } - } - - for key, settingPtr := range timeSettingsMap { - setting := *settingPtr - if setting.Valid { - var stringVal string = setting.Value.Format("2006-01-02 15:04:05") - convertedSettings = append(convertedSettings, Setting{ - Key: key, - Value: stringVal, - }) - } - } - - return convertedSettings +type CompanySettingRes struct { + Key string `json:"key"` + Value string `json:"value"` + CompanyID int64 `json:"company_id"` + UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` } func ConvertSetting(setting Setting) SettingRes { @@ -180,14 +41,20 @@ func ConvertSetting(setting Setting) SettingRes { } } -func ConvertUpdateSettingListReq(settings UpdateSettingListReq) ValidSettingList { - return ValidSettingList{ - SMSProvider: ConvertStringPtr(settings.SMSProvider), - MaxNumberOfOutcomes: ConvertInt64Ptr(settings.MaxNumberOfOutcomes), - BetAmountLimit: ConvertCurrencyFloatPtr(settings.BetAmountLimit), - DailyTicketPerIP: ConvertInt64Ptr(settings.DailyTicketPerIP), - TotalWinningLimit: ConvertCurrencyFloatPtr(settings.TotalWinningLimit), - AmountForBetReferral: ConvertCurrencyFloatPtr(settings.AmountForBetReferral), - CashbackAmountCap: ConvertCurrencyFloatPtr(settings.CashbackAmountCap), +func ConvertCompanySetting(companySetting dbgen.CompanySetting) CompanySetting { + return CompanySetting{ + Key: companySetting.Key, + Value: companySetting.Value, + CompanyID: companySetting.CompanyID, + UpdatedAt: companySetting.UpdatedAt.Time, + CreatedAt: companySetting.CreatedAt.Time, } } + +func ConvertCompanySettings(settings []dbgen.CompanySetting) []CompanySetting { + result := make([]CompanySetting, 0, len(settings)) + for _, setting := range settings { + result = append(result, ConvertCompanySetting(setting)) + } + return result +} diff --git a/internal/domain/ticket.go b/internal/domain/ticket.go index 63c4d29..aa724e3 100644 --- a/internal/domain/ticket.go +++ b/internal/domain/ticket.go @@ -39,6 +39,7 @@ type Ticket struct { ID int64 Amount Currency TotalOdds float32 + CompanyID int64 } type GetTicket struct { @@ -46,12 +47,14 @@ type GetTicket struct { Amount Currency TotalOdds float32 Outcomes []TicketOutcome + CompanyID int64 } type CreateTicket struct { Amount Currency TotalOdds float32 IP string + CompanyID int64 } type CreateTicketOutcomeReq struct { @@ -80,4 +83,9 @@ type TicketRes struct { Outcomes []TicketOutcome `json:"outcomes"` Amount float32 `json:"amount" example:"100.0"` TotalOdds float32 `json:"total_odds" example:"4.22"` + CompanyID int64 `json:"company_id" example:"1"` +} + +type TicketFilter struct { + CompanyID ValidInt64 } diff --git a/internal/domain/user.go b/internal/domain/user.go index ba0099d..194a89b 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -35,10 +35,7 @@ type UserFilter struct { CreatedBefore ValidTime CreatedAfter ValidTime } -type ValidRole struct { - Value Role - Valid bool -} + type RegisterUserReq struct { FirstName string diff --git a/internal/domain/validtypes.go b/internal/domain/validtypes.go index d402517..c20f6c3 100644 --- a/internal/domain/validtypes.go +++ b/internal/domain/validtypes.go @@ -9,38 +9,12 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +// Valid Int64 type ValidInt64 struct { Value int64 Valid bool } -type ValidInt struct { - Value int - Valid bool -} -type ValidInt32 struct { - Value int32 - Valid bool -} -type ValidFloat32 struct { - Value float32 - Valid bool -} - -type ValidString struct { - Value string - Valid bool -} -type ValidTime struct { - Value time.Time - Valid bool -} -type ValidBool struct { - Value bool - Valid bool -} - -// ValidInt64 → pgtype.Int8 func (v ValidInt64) ToPG() pgtype.Int8 { return pgtype.Int8{ Int64: v.Value, @@ -48,126 +22,6 @@ func (v ValidInt64) ToPG() pgtype.Int8 { } } -// ValidInt32 → pgtype.Int4 -func (v ValidInt32) ToPG() pgtype.Int4 { - return pgtype.Int4{ - Int32: v.Value, - Valid: v.Valid, - } -} - -// ValidInt → pgtype.Int4 (Go int mapped to int32 for pg compatibility) -func (v ValidInt) ToPG() pgtype.Int4 { - return pgtype.Int4{ - Int32: int32(v.Value), - Valid: v.Valid, - } -} - -// ValidFloat32 → pgtype.Float4 -func (v ValidFloat32) ToPG() pgtype.Float4 { - return pgtype.Float4{ - Float32: v.Value, - Valid: v.Valid, - } -} - -// ValidString → pgtype.Text -func (v ValidString) ToPG() pgtype.Text { - return pgtype.Text{ - String: v.Value, - Valid: v.Valid, - } -} - -// ValidTime → pgtype.Timestamp -func (v ValidTime) ToPG() pgtype.Timestamp { - return pgtype.Timestamp{ - Time: v.Value, - Valid: v.Valid, - } -} - -// ValidBool → pgtype.Bool -func (v ValidBool) ToPG() pgtype.Bool { - return pgtype.Bool{ - Bool: v.Value, - Valid: v.Valid, - } -} - -func ConvertInt64Ptr(value *int64) ValidInt64 { - if value == nil { - return ValidInt64{} - } - return ValidInt64{ - Value: *value, - Valid: true, - } -} - -func ConvertInt32Ptr(value *int32) ValidInt32 { - if value == nil { - return ValidInt32{} - } - return ValidInt32{ - Value: *value, - Valid: true, - } -} -func ConvertIntPtr(value *int) ValidInt { - if value == nil { - return ValidInt{} - } - return ValidInt{ - Value: *value, - Valid: true, - } -} - -func ConvertStringPtr(value *string) ValidString { - if value == nil { - return ValidString{} - } - return ValidString{ - Value: *value, - Valid: true, - } -} - -func ConvertFloat32Ptr(value *float32) ValidFloat32 { - if value == nil { - return ValidFloat32{} - } - return ValidFloat32{ - Value: *value, - Valid: true, - } -} - -func ConvertCurrencyFloatPtr(value *float32) ValidInt64 { - if value == nil { - return ValidInt64{} - } - - convertedCurrency := ToCurrency(*value) - - return ValidInt64{ - Value: int64(convertedCurrency), - Valid: true, - } -} - -func ConvertBoolPtr(value *bool) ValidBool { - if value == nil { - return ValidBool{} - } - return ValidBool{ - Value: *value, - Valid: true, - } -} - func (n *ValidInt64) UnmarshalJSON(data []byte) error { var s string if err := json.Unmarshal(data, &s); err == nil { @@ -190,4 +44,202 @@ func (n *ValidInt64) UnmarshalJSON(data []byte) error { } return fmt.Errorf("invalid int64 value: %s", string(data)) -} \ No newline at end of file +} + +func ConvertInt64Ptr(value *int64) ValidInt64 { + if value == nil { + return ValidInt64{} + } + return ValidInt64{ + Value: *value, + Valid: true, + } +} + +// Valid Int +type ValidInt struct { + Value int + Valid bool +} + +func (v ValidInt) ToPG() pgtype.Int4 { + return pgtype.Int4{ + Int32: int32(v.Value), + Valid: v.Valid, + } +} +func ConvertIntPtr(value *int) ValidInt { + if value == nil { + return ValidInt{} + } + return ValidInt{ + Value: *value, + Valid: true, + } +} + +// Valid Int32 +type ValidInt32 struct { + Value int32 + Valid bool +} + +func (v ValidInt32) ToPG() pgtype.Int4 { + return pgtype.Int4{ + Int32: v.Value, + Valid: v.Valid, + } +} + +func ConvertInt32Ptr(value *int32) ValidInt32 { + if value == nil { + return ValidInt32{} + } + return ValidInt32{ + Value: *value, + Valid: true, + } +} + +// Valid Float32 +type ValidFloat32 struct { + Value float32 + Valid bool +} + +func (v ValidFloat32) ToPG() pgtype.Float4 { + return pgtype.Float4{ + Float32: v.Value, + Valid: v.Valid, + } +} + +func ConvertFloat32Ptr(value *float32) ValidFloat32 { + if value == nil { + return ValidFloat32{} + } + return ValidFloat32{ + Value: *value, + Valid: true, + } +} + +// Valid String +type ValidString struct { + Value string + Valid bool +} + +func (v ValidString) ToPG() pgtype.Text { + return pgtype.Text{ + String: v.Value, + Valid: v.Valid, + } +} + +func ConvertStringPtr(value *string) ValidString { + if value == nil { + return ValidString{} + } + return ValidString{ + Value: *value, + Valid: true, + } +} + +// Valid Time +type ValidTime struct { + Value time.Time + Valid bool +} + +func (v ValidTime) ToPG() pgtype.Timestamp { + return pgtype.Timestamp{ + Time: v.Value, + Valid: v.Valid, + } +} + +// Valid Bool +type ValidBool struct { + Value bool + Valid bool +} + +func (v ValidBool) ToPG() pgtype.Bool { + return pgtype.Bool{ + Bool: v.Value, + Valid: v.Valid, + } +} + +func ConvertBoolPtr(value *bool) ValidBool { + if value == nil { + return ValidBool{} + } + return ValidBool{ + Value: *value, + Valid: true, + } +} + +// Valid Currency +type ValidCurrency struct { + Value Currency + Valid bool +} + +func (v ValidCurrency) ToPG() pgtype.Int8 { + return pgtype.Int8{ + Int64: int64(v.Value), + Valid: v.Valid, + } +} + +func ConvertCurrencyPtr(value *Currency) ValidCurrency { + if value == nil { + return ValidCurrency{} + } + + return ValidCurrency{ + Value: *value, + Valid: true, + } +} + +func ConvertFloat32PtrToCurrency(value *float32) ValidCurrency { + if value == nil { + return ValidCurrency{} + } + + converted := ToCurrency(*value) + + return ValidCurrency{ + Value: converted, + Valid: true, + } +} + +// Valid Role +type ValidRole struct { + Value Role + Valid bool +} + +func (v ValidRole) ToPG() pgtype.Text { + return pgtype.Text{ + String: string(v.Value), + Valid: v.Valid, + } +} + +func ConvertRolePtr(value *Role) ValidRole { + if value == nil { + return ValidRole{} + } + + return ValidRole{ + Value: *value, + Valid: true, + } +} diff --git a/internal/repository/bet.go b/internal/repository/bet.go index 2aa520a..b1d1e52 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -19,115 +19,14 @@ var ( mongoLogger *zap.Logger ) -func convertDBBet(bet dbgen.Bet) domain.Bet { - return domain.Bet{ - ID: bet.ID, - Amount: domain.Currency(bet.Amount), - TotalOdds: bet.TotalOdds, - Status: domain.OutcomeStatus(bet.Status), - UserID: bet.UserID, - IsShopBet: bet.IsShopBet, - CashedOut: bet.CashedOut, - FastCode: bet.FastCode, - CreatedAt: bet.CreatedAt.Time, - } -} - -func convertDBBetOutcomes(outcome dbgen.BetOutcome) domain.BetOutcome { - return domain.BetOutcome{ - ID: outcome.ID, - BetID: outcome.BetID, - SportID: outcome.SportID, - EventID: outcome.EventID, - OddID: outcome.OddID, - HomeTeamName: outcome.HomeTeamName, - AwayTeamName: outcome.AwayTeamName, - MarketID: outcome.MarketID, - MarketName: outcome.MarketName, - Odd: outcome.Odd, - OddName: outcome.OddName, - OddHeader: outcome.OddHeader, - OddHandicap: outcome.OddHandicap, - Status: domain.OutcomeStatus(outcome.Status), - Expires: outcome.Expires.Time, - } -} - -func convertDBBetWithOutcomes(bet dbgen.BetWithOutcome) domain.GetBet { - var outcomes []domain.BetOutcome = make([]domain.BetOutcome, 0, len(bet.Outcomes)) - - for _, outcome := range bet.Outcomes { - outcomes = append(outcomes, convertDBBetOutcomes(outcome)) - } - - return domain.GetBet{ - ID: bet.ID, - Amount: domain.Currency(bet.Amount), - TotalOdds: bet.TotalOdds, - Status: domain.OutcomeStatus(bet.Status), - FullName: bet.FullName.(string), - PhoneNumber: bet.PhoneNumber.String, - UserID: bet.UserID, - IsShopBet: bet.IsShopBet, - CashedOut: bet.CashedOut, - Outcomes: outcomes, - FastCode: bet.FastCode, - CreatedAt: bet.CreatedAt.Time, - } -} - -func convertDBFlag(flag dbgen.Flag) domain.Flag { - return domain.Flag{ - ID: flag.ID, - BetID: flag.BetID.Int64, - OddID: flag.OddsMarketID.Int64, - Reason: flag.Reason.String, - FlaggedAt: flag.FlaggedAt.Time, - Resolved: flag.Resolved.Bool, - } -} - -func convertDBCreateBetOutcome(betOutcome domain.CreateBetOutcome) dbgen.CreateBetOutcomeParams { - return dbgen.CreateBetOutcomeParams{ - BetID: betOutcome.BetID, - EventID: betOutcome.EventID, - SportID: betOutcome.SportID, - OddID: betOutcome.OddID, - HomeTeamName: betOutcome.HomeTeamName, - AwayTeamName: betOutcome.AwayTeamName, - MarketID: betOutcome.MarketID, - MarketName: betOutcome.MarketName, - Odd: betOutcome.Odd, - OddName: betOutcome.OddName, - OddHeader: betOutcome.OddHeader, - OddHandicap: betOutcome.OddHandicap, - Expires: pgtype.Timestamp{ - Time: betOutcome.Expires, - Valid: true, - }, - } -} - -func convertCreateBet(bet domain.CreateBet) dbgen.CreateBetParams { - return dbgen.CreateBetParams{ - Amount: int64(bet.Amount), - TotalOdds: bet.TotalOdds, - Status: int32(bet.Status), - UserID: bet.UserID, - IsShopBet: bet.IsShopBet, - OutcomesHash: bet.OutcomesHash, - FastCode: bet.FastCode, - } -} - func (s *Store) CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) { - newBet, err := s.queries.CreateBet(ctx, convertCreateBet(bet)) + newBet, err := s.queries.CreateBet(ctx, domain.ConvertCreateBet(bet)) if err != nil { fmt.Println("We are here") logger.Error("Failed to create bet", slog.String("error", err.Error()), slog.Any("bet", bet)) return domain.Bet{}, err } - return convertDBBet(newBet), err + return domain.ConvertDBBet(newBet), err } @@ -135,7 +34,7 @@ func (s *Store) CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBe var dbParams []dbgen.CreateBetOutcomeParams = make([]dbgen.CreateBetOutcomeParams, 0, len(outcomes)) for _, outcome := range outcomes { - dbParams = append(dbParams, convertDBCreateBetOutcome(outcome)) + dbParams = append(dbParams, domain.ConvertDBCreateBetOutcome(outcome)) } rows, err := s.queries.CreateBetOutcome(ctx, dbParams) @@ -177,7 +76,7 @@ func (s *Store) CreateFlag(ctx context.Context, flag domain.CreateFlagReq) (doma return domain.Flag{}, err } - return convertDBFlag(f), nil + return domain.ConvertDBFlag(f), nil } func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) { @@ -190,35 +89,18 @@ func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) return domain.GetBet{}, err } - return convertDBBetWithOutcomes(bet), nil + return domain.ConvertDBBetWithOutcomes(bet), nil } func (s *Store) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]domain.GetBet, error) { bets, err := s.queries.GetAllBets(ctx, dbgen.GetAllBetsParams{ - UserID: pgtype.Int8{ - Int64: filter.UserID.Value, - Valid: filter.UserID.Valid, - }, - CashedOut: pgtype.Bool{ - Bool: filter.CashedOut.Value, - Valid: filter.CashedOut.Valid, - }, - IsShopBet: pgtype.Bool{ - Bool: filter.IsShopBet.Value, - Valid: filter.IsShopBet.Valid, - }, - Query: pgtype.Text{ - String: filter.Query.Value, - Valid: filter.Query.Valid, - }, - CreatedBefore: pgtype.Timestamp{ - Time: filter.CreatedBefore.Value, - Valid: filter.CreatedBefore.Valid, - }, - CreatedAfter: pgtype.Timestamp{ - Time: filter.CreatedAfter.Value, - Valid: filter.CreatedAfter.Valid, - }, + UserID: filter.UserID.ToPG(), + CompanyID: filter.CompanyID.ToPG(), + CashedOut: filter.CashedOut.ToPG(), + IsShopBet: filter.IsShopBet.ToPG(), + Query: filter.Query.ToPG(), + CreatedBefore: filter.CreatedBefore.ToPG(), + CreatedAfter: filter.CreatedAfter.ToPG(), }) if err != nil { domain.MongoDBLogger.Error("failed to get all bets", @@ -230,7 +112,7 @@ func (s *Store) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]doma var result []domain.GetBet = make([]domain.GetBet, 0, len(bets)) for _, bet := range bets { - result = append(result, convertDBBetWithOutcomes(bet)) + result = append(result, domain.ConvertDBBetWithOutcomes(bet)) } return result, nil @@ -245,7 +127,7 @@ func (s *Store) GetBetByUserID(ctx context.Context, UserID int64) ([]domain.GetB var result []domain.GetBet = make([]domain.GetBet, 0, len(bets)) for _, bet := range bets { - result = append(result, convertDBBetWithOutcomes(bet)) + result = append(result, domain.ConvertDBBetWithOutcomes(bet)) } return result, nil @@ -258,7 +140,7 @@ func (s *Store) GetBetByFastCode(ctx context.Context, fastcode string) (domain.G return domain.GetBet{}, err } - return convertDBBetWithOutcomes(bet), nil + return domain.ConvertDBBetWithOutcomes(bet), nil } func (s *Store) GetBetsForCashback(ctx context.Context) ([]domain.GetBet, error) { @@ -270,7 +152,7 @@ func (s *Store) GetBetsForCashback(ctx context.Context) ([]domain.GetBet, error) } for _, bet := range bets { - cashbackBet := convertDBBetWithOutcomes(bet) + cashbackBet := domain.ConvertDBBetWithOutcomes(bet) res = append(res, cashbackBet) } @@ -361,7 +243,7 @@ func (s *Store) GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_fi var result []domain.BetOutcome = make([]domain.BetOutcome, 0, len(outcomes)) for _, outcome := range outcomes { - result = append(result, convertDBBetOutcomes(outcome)) + result = append(result, domain.ConvertDBBetOutcomes(outcome)) } return result, nil } @@ -378,7 +260,7 @@ func (s *Store) GetBetOutcomeByBetID(ctx context.Context, betID int64) ([]domain var result []domain.BetOutcome = make([]domain.BetOutcome, 0, len(outcomes)) for _, outcome := range outcomes { - result = append(result, convertDBBetOutcomes(outcome)) + result = append(result, domain.ConvertDBBetOutcomes(outcome)) } return result, nil } @@ -397,7 +279,7 @@ func (s *Store) UpdateBetOutcomeStatus(ctx context.Context, id int64, status dom return domain.BetOutcome{}, err } - res := convertDBBetOutcomes(update) + res := domain.ConvertDBBetOutcomes(update) return res, nil } @@ -415,7 +297,7 @@ func (s *Store) UpdateBetOutcomeStatusByBetID(ctx context.Context, id int64, sta return domain.BetOutcome{}, err } - res := convertDBBetOutcomes(update) + res := domain.ConvertDBBetOutcomes(update) return res, nil } @@ -436,7 +318,7 @@ func (s *Store) UpdateBetOutcomeStatusForEvent(ctx context.Context, eventID int6 var result []domain.BetOutcome = make([]domain.BetOutcome, 0, len(outcomes)) for _, outcome := range outcomes { - result = append(result, convertDBBetOutcomes(outcome)) + result = append(result, domain.ConvertDBBetOutcomes(outcome)) } return result, nil } diff --git a/internal/repository/settings.go b/internal/repository/settings.go index cb2c2b2..333f280 100644 --- a/internal/repository/settings.go +++ b/internal/repository/settings.go @@ -2,116 +2,24 @@ package repository import ( "context" - "fmt" - "strconv" - "time" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "go.uber.org/zap" ) -func GetDBSettingList(settings []dbgen.Setting) (domain.SettingList, error) { - var dbSettingList domain.ValidSettingList - - var int64SettingsMap = domain.ConvertInt64SettingsMap(&dbSettingList) - var stringSettingsMap = domain.ConvertStringSettingsMap(&dbSettingList) - var boolSettingsMap = domain.ConvertBoolSettingsMap(&dbSettingList) - var float32SettingsMap = domain.ConvertFloat32SettingsMap(&dbSettingList) - var timeSettingsMap = domain.ConvertTimeSettingsMap(&dbSettingList) - - for _, setting := range settings { - isSettingUnknown := true - for key, dbSetting := range int64SettingsMap { - if setting.Key == key { - value, err := strconv.ParseInt(setting.Value, 10, 64) - if err != nil { - return domain.SettingList{}, err - } - *dbSetting = domain.ValidInt64{ - Value: value, - Valid: true, - } - isSettingUnknown = false - } - } - - for key, dbSetting := range stringSettingsMap { - if setting.Key == key { - *dbSetting = domain.ValidString{ - Value: setting.Value, - Valid: true, - } - isSettingUnknown = false - } - } - - for key, dbSetting := range boolSettingsMap { - if setting.Key == key { - value, err := strconv.ParseBool(setting.Value) - if err != nil { - return domain.SettingList{}, err - } - *dbSetting = domain.ValidBool{ - Value: value, - Valid: true, - } - isSettingUnknown = false - } - } - - for key, dbSetting := range float32SettingsMap { - if setting.Key == key { - value, err := strconv.ParseFloat(setting.Value, 32) - if err != nil { - return domain.SettingList{}, err - } - *dbSetting = domain.ValidFloat32{ - Value: float32(value), - Valid: true, - } - isSettingUnknown = false - } - } - for key, dbSetting := range timeSettingsMap { - if setting.Key == key { - value, err := time.Parse(time.RFC3339, setting.Value) - if err != nil { - return domain.SettingList{}, err - } - *dbSetting = domain.ValidTime{ - Value: value, - Valid: true, - } - isSettingUnknown = false - } - } - - if isSettingUnknown { - domain.MongoDBLogger.Warn("unknown setting found on database", zap.String("setting", setting.Key)) - } - } - - for key, dbSetting := range int64SettingsMap { - if !dbSetting.Valid { - fmt.Printf("setting value not found on database: %v \n", key) - domain.MongoDBLogger.Warn("setting value not found on database", zap.String("setting", key)) - } - } - - return domain.ValidateSettingList(dbSettingList), nil -} -func (s *Store) GetSettingList(ctx context.Context) (domain.SettingList, error) { - settings, err := s.queries.GetSettings(ctx) +func (s *Store) GetGlobalSettingList(ctx context.Context) (domain.SettingList, error) { + settings, err := s.queries.GetGlobalSettings(ctx) if err != nil { domain.MongoDBLogger.Error("failed to get all settings", zap.Error(err)) + return domain.SettingList{}, err } - return GetDBSettingList(settings) + return domain.ConvertDBGlobalSettingList(settings) } -func (s *Store) GetSettings(ctx context.Context) ([]domain.Setting, error) { - settings, err := s.queries.GetSettings(ctx) +func (s *Store) GetGlobalSettings(ctx context.Context) ([]domain.Setting, error) { + settings, err := s.queries.GetGlobalSettings(ctx) if err != nil { domain.MongoDBLogger.Error("failed to get all settings", zap.Error(err)) @@ -129,8 +37,8 @@ func (s *Store) GetSettings(ctx context.Context) ([]domain.Setting, error) { return result, nil } -func (s *Store) GetSetting(ctx context.Context, key string) (domain.Setting, error) { - dbSetting, err := s.queries.GetSetting(ctx, key) +func (s *Store) GetGlobalSetting(ctx context.Context, key string) (domain.Setting, error) { + dbSetting, err := s.queries.GetGlobalSetting(ctx, key) if err != nil { domain.MongoDBLogger.Error("failed to get all settings", zap.Error(err)) @@ -145,14 +53,18 @@ func (s *Store) GetSetting(ctx context.Context, key string) (domain.Setting, err return result, nil } -func (s *Store) UpdateSetting(ctx context.Context, key, value string) error { - err := s.queries.UpdateSetting(ctx, dbgen.UpdateSettingParams{ +func (s *Store) UpdateGlobalSetting(ctx context.Context, key, value string) error { + err := s.queries.UpdateGlobalSetting(ctx, dbgen.UpdateGlobalSettingParams{ Key: key, Value: value, }) if err != nil { - domain.MongoDBLogger.Error("failed to update setting", zap.String("key", key), zap.String("value", value), zap.Error(err)) + domain.MongoDBLogger.Error("failed to update setting", + zap.String("key", key), + zap.String("value", value), + zap.Error(err), + ) return err } @@ -161,11 +73,11 @@ func (s *Store) UpdateSetting(ctx context.Context, key, value string) error { } -func (s *Store) UpdateSettingList(ctx context.Context, settingList domain.ValidSettingList) error { - convertedSettings := domain.ConvertValidSettingList(settingList) +func (s *Store) UpdateGlobalSettingList(ctx context.Context, settingList domain.ValidSettingList) error { + convertedSettings := settingList.ConvertAllSettings() for _, setting := range convertedSettings { - err := s.UpdateSetting(ctx, setting.Key, setting.Value) + err := s.UpdateGlobalSetting(ctx, setting.Key, setting.Value) if err != nil { domain.MongoDBLogger.Warn("failed to update setting list", zap.String("key", setting.Key), zap.Error(err)) return err @@ -173,3 +85,89 @@ func (s *Store) UpdateSettingList(ctx context.Context, settingList domain.ValidS } return nil } + +func (s *Store) InsertCompanySetting(ctx context.Context, key, value string, companyID int64) error { + err := s.queries.InsertCompanySetting(ctx, dbgen.InsertCompanySettingParams{ + CompanyID: companyID, + Key: key, + Value: value, + }) + + if err != nil { + return err + } + + return nil +} + +func (s *Store) InsertCompanySettingList(ctx context.Context, settingList domain.ValidSettingList, companyID int64) error { + convertedSettings := settingList.ConvertAllSettings() + + for _, setting := range convertedSettings { + err := s.InsertCompanySetting(ctx, setting.Key, setting.Value, companyID) + if err != nil { + domain.MongoDBLogger.Warn("failed to update setting list", zap.String("key", setting.Key), zap.Error(err)) + return err + } + } + return nil +} + +func (s *Store) GetAllCompanySettings(ctx context.Context) ([]domain.CompanySetting, error) { + settings, err := s.queries.GetAllCompanySettings(ctx) + + if err != nil { + return nil, err + } + + return domain.ConvertCompanySettings(settings), nil +} + +func (s *Store) GetCompanySettingsByKey(ctx context.Context, key string) ([]domain.CompanySetting, error) { + settings, err := s.queries.GetCompanySettingsByKey(ctx, key) + + if err != nil { + return nil, err + } + + return domain.ConvertCompanySettings(settings), nil +} + +func (s *Store) GetOverrideSettings(ctx context.Context, companyID int64) ([]domain.Setting, error) { + settings, err := s.queries.GetOverrideSettings(ctx, companyID) + + if err != nil { + return nil, err + } + + result := make([]domain.Setting, 0, len(settings)) + for _, setting := range settings { + result = append(result, domain.Setting{ + Key: setting.Key, + Value: setting.Value, + UpdatedAt: setting.UpdatedAt.Time, + }) + } + + return result, nil +} + +func (s *Store) GetOverrideSettingsList(ctx context.Context, companyID int64) (domain.SettingList, error) { + settings, err := s.queries.GetOverrideSettings(ctx, companyID) + + if err != nil { + return domain.SettingList{}, err + } + + return domain.ConvertDBOverrideSettingList(settings) +} + +func (s *Store) DeleteCompanySetting(ctx context.Context, companyID int64, key string) error { + return s.queries.DeleteCompanySetting(ctx, dbgen.DeleteCompanySettingParams{ + CompanyID: companyID, + Key: key, + }) +} +func (s *Store) DeleteAllCompanySetting(ctx context.Context, companyID int64,) error { + return s.queries.DeleteAllCompanySetting(ctx, companyID) +} diff --git a/internal/repository/shop_bet.go b/internal/repository/shop_bet.go index d431dce..6896640 100644 --- a/internal/repository/shop_bet.go +++ b/internal/repository/shop_bet.go @@ -24,7 +24,7 @@ func convertDBShopBetDetail(bet dbgen.ShopBetDetail) domain.ShopBetDetail { var outcomes []domain.BetOutcome = make([]domain.BetOutcome, 0, len(bet.Outcomes)) for _, outcome := range bet.Outcomes { - outcomes = append(outcomes, convertDBBetOutcomes(outcome)) + outcomes = append(outcomes, domain.ConvertDBBetOutcomes(outcome)) } return domain.ShopBetDetail{ ID: bet.ID, diff --git a/internal/repository/ticket.go b/internal/repository/ticket.go index 337accb..ac140bf 100644 --- a/internal/repository/ticket.go +++ b/internal/repository/ticket.go @@ -13,6 +13,7 @@ func convertDBTicket(ticket dbgen.Ticket) domain.Ticket { ID: ticket.ID, Amount: domain.Currency(ticket.Amount), TotalOdds: ticket.TotalOdds, + CompanyID: ticket.CompanyID, } } @@ -71,6 +72,7 @@ func convertCreateTicket(ticket domain.CreateTicket) dbgen.CreateTicketParams { Amount: int64(ticket.Amount), TotalOdds: ticket.TotalOdds, Ip: ticket.IP, + CompanyID: ticket.CompanyID, } } @@ -110,8 +112,8 @@ func (s *Store) GetTicketByID(ctx context.Context, id int64) (domain.GetTicket, return convertDBTicketOutcomes(ticket), nil } -func (s *Store) GetAllTickets(ctx context.Context) ([]domain.GetTicket, error) { - tickets, err := s.queries.GetAllTickets(ctx) +func (s *Store) GetAllTickets(ctx context.Context, filter domain.TicketFilter) ([]domain.GetTicket, error) { + tickets, err := s.queries.GetAllTickets(ctx, filter.CompanyID.ToPG()) if err != nil { return nil, err } diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index ad44770..900317c 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -215,8 +215,8 @@ func (s *Service) GenerateBetOutcome(ctx context.Context, eventID int64, marketI return newOutcome, nil } -func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID int64, role domain.Role, companyID domain.ValidInt64) (domain.CreateBetRes, error) { - settingsList, err := s.settingSvc.GetSettingList(ctx) +func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID int64, role domain.Role, companyID int64) (domain.CreateBetRes, error) { + settingsList, err := s.settingSvc.GetOverrideSettingsList(ctx, companyID) if err != nil { return domain.CreateBetRes{}, err @@ -297,6 +297,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID OutcomesHash: outcomesHash, FastCode: fastCode, UserID: userID, + CompanyID: companyID, } switch role { @@ -347,7 +348,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID return domain.CreateBetRes{}, err } - if companyID.Valid && branch.CompanyID == companyID.Value { + if branch.CompanyID == companyID { s.mongoLogger.Warn("unauthorized company", zap.Int64("branch_id", *req.BranchID), zap.Error(err), @@ -477,7 +478,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID } } - res := domain.ConvertCreateBet(bet, rows) + res := domain.ConvertCreateBetRes(bet, rows) return res, nil } @@ -714,7 +715,7 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string, return newOdds, totalOdds, nil } -func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, leagueID domain.ValidInt64, sportID domain.ValidInt32, firstStartTime, lastStartTime domain.ValidTime) (domain.CreateBetRes, error) { +func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID, companyID int64, leagueID domain.ValidInt64, sportID domain.ValidInt32, firstStartTime, lastStartTime domain.ValidTime) (domain.CreateBetRes, error) { // Get a unexpired event id @@ -816,6 +817,7 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, le TotalOdds: totalOdds, Status: domain.OUTCOME_STATUS_PENDING, UserID: userID, + CompanyID: companyID, IsShopBet: true, FastCode: fastCode, } @@ -842,7 +844,7 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, le return domain.CreateBetRes{}, err } - res := domain.ConvertCreateBet(bet, rows) + res := domain.ConvertCreateBetRes(bet, rows) s.mongoLogger.Info("Random bets placed successfully", zap.Int64("userID", userID), @@ -1310,7 +1312,7 @@ func (s *Service) SetBetToRemoved(ctx context.Context, id int64) error { } func (s *Service) ProcessBetCashback(ctx context.Context) error { - settingsList, err := s.settingSvc.GetSettingList(ctx) + bets, err := s.betStore.GetBetsForCashback(ctx) if err != nil { s.mongoLogger.Error("failed to fetch bets", @@ -1319,6 +1321,7 @@ func (s *Service) ProcessBetCashback(ctx context.Context) error { return err } + for _, bet := range bets { shouldProcess := true loseCount := 0 @@ -1359,6 +1362,8 @@ func (s *Service) ProcessBetCashback(ctx context.Context) error { ) continue } + + settingsList, err := s.settingSvc.GetOverrideSettingsList(ctx, bet.CompanyID) cashbackAmount := math.Min(float64(settingsList.CashbackAmountCap.Float32()), float64(calculateCashbackAmount(bet.Amount.Float32(), bet.TotalOdds))) _, err = s.walletSvc.AddToWallet(ctx, wallets.StaticID, domain.ToCurrency(float32(cashbackAmount)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, diff --git a/internal/services/messenger/sms.go b/internal/services/messenger/sms.go index d750d9a..77e1cd8 100644 --- a/internal/services/messenger/sms.go +++ b/internal/services/messenger/sms.go @@ -15,12 +15,24 @@ var ( ErrSMSProviderNotFound = errors.New("SMS Provider Not Found") ) -func (s *Service) SendSMS(ctx context.Context, receiverPhone, message string) error { +// If the company id is valid, it is a company based notification else its a global notification (by the super admin) +func (s *Service) SendSMS(ctx context.Context, receiverPhone, message string, companyID domain.ValidInt64) error { - settingsList, err := s.settingSvc.GetSettingList(ctx) + var settingsList domain.SettingList + var err error - if err != nil { - return err + if companyID.Valid { + settingsList, err = s.settingSvc.GetOverrideSettingsList(ctx, companyID.Value) + if err != nil { + // TODO: Send a log about the error + return err + } + } else { + settingsList, err = s.settingSvc.GetGlobalSettingList(ctx) + if err != nil { + // TODO: Send a log about the error + return err + } } switch settingsList.SMSProvider { diff --git a/internal/services/notification/service.go b/internal/services/notification/service.go index 38caf8d..ae0b990 100644 --- a/internal/services/notification/service.go +++ b/internal/services/notification/service.go @@ -340,7 +340,7 @@ func (s *Service) SendNotificationSMS(ctx context.Context, recipientID int64, me if user.PhoneNumber == "" { return fmt.Errorf("Phone Number is invalid") } - err = s.messengerSvc.SendSMS(ctx, user.PhoneNumber, message) + err = s.messengerSvc.SendSMS(ctx, user.PhoneNumber, message, user.CompanyID) if err != nil { return err } diff --git a/internal/services/odds/port.go b/internal/services/odds/port.go index 6014ed0..d1026aa 100644 --- a/internal/services/odds/port.go +++ b/internal/services/odds/port.go @@ -15,7 +15,7 @@ type Service interface { GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string) ([]domain.OddMarket, error) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit domain.ValidInt64, offset domain.ValidInt64) ([]domain.OddMarket, error) GetALLPrematchOdds(ctx context.Context) ([]domain.OddMarket, error) - GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) (domain.RawOddsByMarketID, error) + // GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) (domain.OddMarket, error) DeleteOddsForEvent(ctx context.Context, eventID string) error // Odd History diff --git a/internal/services/referal/port.go b/internal/services/referal/port.go index 1946e99..6add199 100644 --- a/internal/services/referal/port.go +++ b/internal/services/referal/port.go @@ -9,7 +9,7 @@ import ( type ReferralStore interface { GenerateReferralCode() (string, error) CreateReferral(ctx context.Context, userID int64) error - ProcessReferral(ctx context.Context, referredID, referralCode string) error + ProcessReferral(ctx context.Context, referredPhone, referralCode string, companyID int64) error ProcessDepositBonus(ctx context.Context, userID string, amount float64) error GetReferralStats(ctx context.Context, userID string) (*domain.ReferralStats, error) CreateReferralSettings(ctx context.Context, req domain.ReferralSettingsReq) error diff --git a/internal/services/referal/service.go b/internal/services/referal/service.go index f75a809..d89b023 100644 --- a/internal/services/referal/service.go +++ b/internal/services/referal/service.go @@ -107,7 +107,7 @@ func (s *Service) CreateReferral(ctx context.Context, userID int64) error { return nil } -func (s *Service) ProcessReferral(ctx context.Context, referredPhone, referralCode string) error { +func (s *Service) ProcessReferral(ctx context.Context, referredPhone, referralCode string, companyID int64) error { s.logger.Info("Processing referral", "referredPhone", referredPhone, "referralCode", referralCode) referral, err := s.repo.GetReferralByCode(ctx, referralCode) @@ -121,7 +121,10 @@ func (s *Service) ProcessReferral(ctx context.Context, referredPhone, referralCo return ErrInvalidReferral } - user, err := s.store.GetUserByPhone(ctx, referredPhone) + user, err := s.store.GetUserByPhone(ctx, referredPhone, domain.ValidInt64{ + Value: companyID, + Valid: true, + }) if err != nil { if errors.Is(err, domain.ErrUserNotFound) { s.logger.Warn("User not found for referral", "referredPhone", referredPhone) diff --git a/internal/services/settings/port.go b/internal/services/settings/port.go index 9f83ab2..9f37866 100644 --- a/internal/services/settings/port.go +++ b/internal/services/settings/port.go @@ -7,9 +7,18 @@ import ( ) type SettingStore interface { - GetSettingList(ctx context.Context) (domain.SettingList, error) - GetSettings(ctx context.Context) ([]domain.Setting, error) - GetSetting(ctx context.Context, key string) (domain.Setting, error) - UpdateSetting(ctx context.Context, key, value string) error - UpdateSettingList(ctx context.Context, settingList domain.ValidSettingList) error + GetGlobalSettingList(ctx context.Context) (domain.SettingList, error) + GetGlobalSettings(ctx context.Context) ([]domain.Setting, error) + GetGlobalSetting(ctx context.Context, key string) (domain.Setting, error) + UpdateGlobalSetting(ctx context.Context, key, value string) error + UpdateGlobalSettingList(ctx context.Context, settingList domain.ValidSettingList) error + + InsertCompanySetting(ctx context.Context, key, value string, companyID int64) error + InsertCompanySettingList(ctx context.Context, settingList domain.ValidSettingList, companyID int64) error + GetAllCompanySettings(ctx context.Context) ([]domain.CompanySetting, error) + GetCompanySettingsByKey(ctx context.Context, key string) ([]domain.CompanySetting, error) + GetOverrideSettings(ctx context.Context, companyID int64) ([]domain.Setting, error) + GetOverrideSettingsList(ctx context.Context, companyID int64) (domain.SettingList, error) + DeleteCompanySetting(ctx context.Context, companyID int64, key string) error + DeleteAllCompanySetting(ctx context.Context, companyID int64) error } diff --git a/internal/services/settings/service.go b/internal/services/settings/service.go index 8180e94..5b5ea37 100644 --- a/internal/services/settings/service.go +++ b/internal/services/settings/service.go @@ -16,21 +16,48 @@ func NewService(settingStore SettingStore) *Service { } } -func (s *Service) GetSettingList(ctx context.Context) (domain.SettingList, error) { - return s.settingStore.GetSettingList(ctx) +func (s *Service) GetGlobalSettingList(ctx context.Context) (domain.SettingList, error) { + return s.settingStore.GetGlobalSettingList(ctx) } -func (s *Service) GetSettings(ctx context.Context) ([]domain.Setting, error) { - return s.settingStore.GetSettings(ctx) +func (s *Service) GetGlobalSettings(ctx context.Context) ([]domain.Setting, error) { + return s.settingStore.GetGlobalSettings(ctx) } -func (s *Service) GetSetting(ctx context.Context, key string) (domain.Setting, error) { - return s.settingStore.GetSetting(ctx, key) +func (s *Service) GetGlobalSetting(ctx context.Context, key string) (domain.Setting, error) { + return s.settingStore.GetGlobalSetting(ctx, key) } -func (s *Service) UpdateSetting(ctx context.Context, key, value string) error { - return s.settingStore.UpdateSetting(ctx, key, value) +func (s *Service) UpdateGlobalSetting(ctx context.Context, key, value string) error { + return s.settingStore.UpdateGlobalSetting(ctx, key, value) } -func (s *Service) UpdateSettingList(ctx context.Context, settingList domain.ValidSettingList) error { - return s.settingStore.UpdateSettingList(ctx, settingList) +func (s *Service) UpdateGlobalSettingList(ctx context.Context, settingList domain.ValidSettingList) error { + return s.settingStore.UpdateGlobalSettingList(ctx, settingList) +} + +func (s *Service) InsertCompanySetting(ctx context.Context, key, value string, companyID int64) error { + return s.settingStore.InsertCompanySetting(ctx, key, value, companyID) +} +func (s *Service) InsertCompanySettingList(ctx context.Context, settingList domain.ValidSettingList, companyID int64) error { + return s.settingStore.InsertCompanySettingList(ctx, settingList, companyID) +} +func (s *Service) GetAllCompanySettings(ctx context.Context) ([]domain.CompanySetting, error) { + return s.settingStore.GetAllCompanySettings(ctx) +} +func (s *Service) GetCompanySettingsByKey(ctx context.Context, key string) ([]domain.CompanySetting, error) { + return s.settingStore.GetCompanySettingsByKey(ctx, key) +} +func (s *Service) GetOverrideSettings(ctx context.Context, companyID int64) ([]domain.Setting, error) { + return s.settingStore.GetOverrideSettings(ctx, companyID) +} +func (s *Service) GetOverrideSettingsList(ctx context.Context, companyID int64) (domain.SettingList, error) { + return s.settingStore.GetOverrideSettingsList(ctx, companyID) +} + +func (s *Service) DeleteCompanySetting(ctx context.Context, companyID int64, key string) error { + return s.settingStore.DeleteCompanySetting(ctx, companyID, key) +} + +func (s *Service) DeleteAllCompanySetting(ctx context.Context, companyID int64) error { + return s.settingStore.DeleteAllCompanySetting(ctx, companyID) } diff --git a/internal/services/ticket/port.go b/internal/services/ticket/port.go index d4201e3..855d997 100644 --- a/internal/services/ticket/port.go +++ b/internal/services/ticket/port.go @@ -10,7 +10,7 @@ type TicketStore interface { CreateTicket(ctx context.Context, ticket domain.CreateTicket) (domain.Ticket, error) CreateTicketOutcome(ctx context.Context, outcomes []domain.CreateTicketOutcome) (int64, error) GetTicketByID(ctx context.Context, id int64) (domain.GetTicket, error) - GetAllTickets(ctx context.Context) ([]domain.GetTicket, error) + GetAllTickets(ctx context.Context, filter domain.TicketFilter) ([]domain.GetTicket, error) CountTicketByIP(ctx context.Context, IP string) (int64, error) UpdateTicketOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error DeleteOldTickets(ctx context.Context) error diff --git a/internal/services/ticket/service.go b/internal/services/ticket/service.go index c67a657..3514960 100644 --- a/internal/services/ticket/service.go +++ b/internal/services/ticket/service.go @@ -80,7 +80,7 @@ func (s *Service) GenerateTicketOutcome(ctx context.Context, settings domain.Set return domain.CreateTicketOutcome{}, ErrTicketHasExpired } - odds, err := s.prematchSvc.GetRawOddsByMarketID(ctx, marketIDStr, eventIDStr) + odds, err := s.prematchSvc.GetOddsByMarketID(ctx, marketIDStr, eventIDStr) if err != nil { s.mongoLogger.Error("failed to get raw odds by market ID", @@ -158,8 +158,8 @@ func (s *Service) GenerateTicketOutcome(ctx context.Context, settings domain.Set } -func (s *Service) CreateTicket(ctx context.Context, req domain.CreateTicketReq, clientIP string) (domain.Ticket, int64, error) { - settingsList, err := s.settingSvc.GetSettingList(ctx) +func (s *Service) CreateTicket(ctx context.Context, req domain.CreateTicketReq, clientIP string, companyID int64) (domain.Ticket, int64, error) { + settingsList, err := s.settingSvc.GetOverrideSettingsList(ctx, companyID) // Check to see if the number of outcomes is above a set limit if len(req.Outcomes) > int(settingsList.MaxNumberOfOutcomes) { @@ -221,6 +221,7 @@ func (s *Service) CreateTicket(ctx context.Context, req domain.CreateTicketReq, Amount: domain.ToCurrency(req.Amount), TotalOdds: totalOdds, IP: clientIP, + CompanyID: companyID, }) if err != nil { s.mongoLogger.Error("Error Creating Ticket", zap.Float32("Total Odds", totalOdds), zap.Float32("amount", req.Amount)) @@ -261,8 +262,8 @@ func (s *Service) CreateTicketOutcome(ctx context.Context, outcomes []domain.Cre func (s *Service) GetTicketByID(ctx context.Context, id int64) (domain.GetTicket, error) { return s.ticketStore.GetTicketByID(ctx, id) } -func (s *Service) GetAllTickets(ctx context.Context) ([]domain.GetTicket, error) { - return s.ticketStore.GetAllTickets(ctx) +func (s *Service) GetAllTickets(ctx context.Context, filter domain.TicketFilter) ([]domain.GetTicket, error) { + return s.ticketStore.GetAllTickets(ctx, filter) } func (s *Service) CountTicketByIP(ctx context.Context, IP string) (int64, error) { diff --git a/internal/services/transaction/service.go b/internal/services/transaction/service.go index bca21f6..1a10a78 100644 --- a/internal/services/transaction/service.go +++ b/internal/services/transaction/service.go @@ -155,7 +155,7 @@ func (s *Service) GetBranchByRole(ctx context.Context, branchID *int64, role dom // Check if the user has access to the company if role != domain.RoleSuperAdmin { - if !userCompanyID.Valid || userCompanyID.Value != branch.CompanyID { + if userCompanyID.Valid && userCompanyID.Value != branch.CompanyID { return nil, nil, ErrUnauthorizedCompanyID } } diff --git a/internal/services/transaction/shop_bet.go b/internal/services/transaction/shop_bet.go index 8296952..6c1ed20 100644 --- a/internal/services/transaction/shop_bet.go +++ b/internal/services/transaction/shop_bet.go @@ -52,7 +52,7 @@ func (s *Service) CreateShopBet(ctx context.Context, userID int64, role domain.R newBet, err := s.betSvc.PlaceBet(ctx, domain.CreateBetReq{ Outcomes: req.Outcomes, Amount: req.Amount, - }, userID, role, userCompanyID) + }, userID, role, *companyID) if err != nil { return domain.ShopBet{}, err diff --git a/internal/services/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go index 432c706..e0fa68b 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -261,7 +261,7 @@ func (s *service) ProcessWin(ctx context.Context, req domain.WinRequest) (*domai // --- 1. Validate PlayerID --- playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64) if err != nil { -z`` return nil, fmt.Errorf("BAD_REQUEST: invalid PlayerID %s", req.PlayerID) + return nil, fmt.Errorf("BAD_REQUEST: invalid PlayerID %s", req.PlayerID) } // --- 2. Get player wallets --- diff --git a/internal/web_server/handlers/auth_handler.go b/internal/web_server/handlers/auth_handler.go index a5f215e..57081c1 100644 --- a/internal/web_server/handlers/auth_handler.go +++ b/internal/web_server/handlers/auth_handler.go @@ -42,7 +42,7 @@ type loginCustomerRes struct { func (h *Handler) LoginCustomer(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) if !companyID.Valid { - domain.BadRequestLogger.Error("invalid company id") + h.BadRequestLogger().Error("invalid company id") return fiber.NewError(fiber.StatusBadRequest, "invalid company id") } var req loginCustomerReq @@ -163,7 +163,7 @@ type loginAdminRes struct { func (h *Handler) LoginAdmin(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) if !companyID.Valid { - domain.BadRequestLogger.Error("invalid company id") + h.BadRequestLogger().Error("invalid company id") return fiber.NewError(fiber.StatusBadRequest, "invalid company id") } var req loginAdminReq diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index a323560..d9cea40 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -23,11 +23,16 @@ import ( // @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/sport/bet [post] +// @Router /api/v1/{tenant_slug}/sport/bet [post] func (h *Handler) CreateBet(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + userID := c.Locals("user_id").(int64) role := c.Locals("role").(domain.Role) - companyID := c.Locals("company_id").(domain.ValidInt64) var req domain.CreateBetReq if err := c.BodyParser(&req); err != nil { @@ -39,7 +44,7 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error()) } - res, err := h.CreateBetInternal(c, req, userID, role, companyID) + res, err := h.CreateBetInternal(c, req, userID, role, companyID.Value) if err != nil { h.mongoLoggerSvc.Error("Failed to create bet", zap.Int("status_code", fiber.StatusInternalServerError), @@ -70,11 +75,15 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error { // @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/sport/bet/fastcode [post] +// @Router /api/v1/{tenant_slug}/sport/bet/fastcode [post] func (h *Handler) CreateBetWithFastCode(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } userID := c.Locals("user_id").(int64) role := c.Locals("role").(domain.Role) - companyID := c.Locals("company_id").(domain.ValidInt64) var req domain.CreateBetWithFastCodeReq @@ -136,7 +145,7 @@ func (h *Handler) CreateBetWithFastCode(c *fiber.Ctx) error { BranchID: req.BranchID, } - res, err := h.CreateBetInternal(c, newReq, userID, role, companyID) + res, err := h.CreateBetInternal(c, newReq, userID, role, companyID.Value) if err != nil { h.mongoLoggerSvc.Error("Failed to create bet", zap.Int("status_code", fiber.StatusInternalServerError), @@ -152,7 +161,7 @@ func (h *Handler) CreateBetWithFastCode(c *fiber.Ctx) error { wallet, err := h.walletSvc.GetCustomerWallet(c.Context(), bet.UserID) // amount added for fast code owner can be fetched from settings in db - settingList, err := h.settingSvc.GetSettingList(c.Context()) + settingList, err := h.settingSvc.GetOverrideSettingsList(c.Context(), companyID.Value) amount := settingList.AmountForBetReferral _, err = h.walletSvc.AddToWallet(c.Context(), wallet.StaticID, amount, domain.ValidInt64{}, @@ -177,7 +186,7 @@ func (h *Handler) CreateBetWithFastCode(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Bet Created", res, nil) } -func (h *Handler) CreateBetInternal(c *fiber.Ctx, req domain.CreateBetReq, userID int64, role domain.Role, companyID domain.ValidInt64) (domain.CreateBetRes, error) { +func (h *Handler) CreateBetInternal(c *fiber.Ctx, req domain.CreateBetReq, userID int64, role domain.Role, companyID int64) (domain.CreateBetRes, error) { valErrs, ok := h.validator.Validate(c, req) if !ok { h.mongoLoggerSvc.Error("CreateBet validation failed", @@ -195,7 +204,7 @@ func (h *Handler) CreateBetInternal(c *fiber.Ctx, req domain.CreateBetReq, userI h.mongoLoggerSvc.Info("PlaceBet failed", zap.Int("status_code", fiber.StatusBadRequest), zap.Int64("userID", userID), - zap.Int64("companyID", companyID.Value), + zap.Int64("companyID", companyID), zap.String("role", string(role)), zap.Error(err), zap.Time("timestamp", time.Now()), @@ -206,7 +215,7 @@ func (h *Handler) CreateBetInternal(c *fiber.Ctx, req domain.CreateBetReq, userI h.mongoLoggerSvc.Error("PlaceBet failed", zap.Int("status_code", fiber.StatusInternalServerError), zap.Int64("userID", userID), - zap.Int64("companyID", companyID.Value), + zap.Int64("companyID", companyID), zap.String("role", string(role)), zap.Error(err), zap.Time("timestamp", time.Now()), @@ -228,11 +237,17 @@ func (h *Handler) CreateBetInternal(c *fiber.Ctx, req domain.CreateBetReq, userI // @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/sport/random/bet [post] +// @Router /api/v1/{tenant_slug}/sport/random/bet [post] func (h *Handler) RandomBet(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + userID := c.Locals("user_id").(int64) - leagueIDQuery, err := strconv.Atoi(c.Query("league_id")) + leagueIDQuery, err := strconv.ParseInt(c.Query("league_id"), 10, 64) if err != nil { h.mongoLoggerSvc.Info("invalid league id", zap.Int("status_code", fiber.StatusBadRequest), @@ -255,8 +270,8 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error { firstStartTimeQuery := c.Query("first_start_time") lastStartTimeQuery := c.Query("last_start_time") - leagueID := domain.ValidInt32{ - Value: int32(leagueIDQuery), + leagueID := domain.ValidInt64{ + Value: leagueIDQuery, Valid: leagueIDQuery != 0, } sportID := domain.ValidInt32{ @@ -326,7 +341,7 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error { var res domain.CreateBetRes for i := 0; i < int(req.NumberOfBets); i++ { - res, err = h.betSvc.PlaceRandomBet(c.Context(), userID, req.BranchID, leagueID, sportID, firstStartTime, lastStartTime) + res, err = h.betSvc.PlaceRandomBet(c.Context(), userID, req.BranchID, companyID.Value, leagueID, sportID, firstStartTime, lastStartTime) if err != nil { switch err { case bet.ErrNoEventsAvailable: @@ -362,8 +377,13 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error { // @Success 200 {array} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/sport/bet [get] +// @Router /api/v1/{tenant_slug}/sport/bet [get] func (h *Handler) GetAllBet(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } role := c.Locals("role").(domain.Role) // companyID := c.Locals("company_id").(domain.ValidInt64) // branchID := c.Locals("branch_id").(domain.ValidInt64) @@ -432,6 +452,7 @@ func (h *Handler) GetAllBet(c *fiber.Ctx) error { } bets, err := h.betSvc.GetAllBets(c.Context(), domain.BetFilter{ + CompanyID: companyID, IsShopBet: isShopBet, Query: searchString, CreatedBefore: createdBefore, @@ -464,8 +485,13 @@ func (h *Handler) GetAllBet(c *fiber.Ctx) error { // @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/sport/bet/{id} [get] +// @Router /api/v1/{tenant_slug}/sport/bet/{id} [get] func (h *Handler) GetBetByID(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } betID := c.Params("id") id, err := strconv.ParseInt(betID, 10, 64) if err != nil { @@ -489,6 +515,16 @@ func (h *Handler) GetBetByID(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusNotFound, "Failed to retrieve bet") } + if bet.CompanyID != companyID.Value { + h.mongoLoggerSvc.Warn("User Attempt to access another company bet", + zap.Int64("betID", id), + zap.Int("status_code", fiber.StatusNotFound), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusNotFound, "Failed to retrieve bet") + } + res := domain.ConvertBet(bet) // h.mongoLoggerSvc.Info("Bet retrieved successfully", @@ -510,8 +546,14 @@ func (h *Handler) GetBetByID(c *fiber.Ctx) error { // @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/sport/bet/fastcode/{fast_code} [get] +// @Router /api/v1/{tenant_slug}/sport/bet/fastcode/{fast_code} [get] func (h *Handler) GetBetByFastCode(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + fastCode := c.Params("fast_code") bet, err := h.betSvc.GetBetByFastCode(c.Context(), fastCode) @@ -525,6 +567,15 @@ func (h *Handler) GetBetByFastCode(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusNotFound, "Failed to find bet by fast code") } + if bet.CompanyID != companyID.Value { + h.mongoLoggerSvc.Warn("User Attempt to access another company bet", + zap.String("fast_code", fastCode), + zap.Int("status_code", fiber.StatusNotFound), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusNotFound, "Failed to retrieve bet") + } res := domain.ConvertBet(bet) // h.mongoLoggerSvc.Info("Bet retrieved successfully", @@ -551,8 +602,14 @@ type UpdateCashOutReq struct { // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/sport/bet/{id} [patch] +// @Router /api/v1/{tenant_slug}/sport/bet/{id} [patch] func (h *Handler) UpdateCashOut(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + type UpdateCashOutReq struct { CashedOut bool `json:"cashed_out" validate:"required" example:"true"` } @@ -588,6 +645,27 @@ func (h *Handler) UpdateCashOut(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, errMsg) } + bet, err := h.betSvc.GetBetByID(c.Context(), id) + if err != nil { + h.mongoLoggerSvc.Info("Failed to get bet", + zap.Int64("betID", id), + zap.Int("status_code", fiber.StatusNotFound), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusNotFound, "Failed to retrieve bet") + } + + if bet.CompanyID != companyID.Value { + h.mongoLoggerSvc.Warn("User Attempt to access another company bet", + zap.Int64("betID", id), + zap.Int("status_code", fiber.StatusNotFound), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusNotFound, "Failed to retrieve bet") + } + err = h.betSvc.UpdateCashOut(c.Context(), id, req.CashedOut) if err != nil { h.mongoLoggerSvc.Error("Failed to update cash out bet", @@ -618,8 +696,14 @@ func (h *Handler) UpdateCashOut(c *fiber.Ctx) error { // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/sport/bet/{id} [delete] +// @Router /api/v1/{tenant_slug}/sport/bet/{id} [delete] func (h *Handler) DeleteBet(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + betID := c.Params("id") id, err := strconv.ParseInt(betID, 10, 64) if err != nil { @@ -632,6 +716,28 @@ func (h *Handler) DeleteBet(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid bet ID") } + // This is to make sure that you can remove a bet only from the right route + bet, err := h.betSvc.GetBetByID(c.Context(), id) + if err != nil { + h.mongoLoggerSvc.Info("Failed to get bet", + zap.Int64("betID", id), + zap.Int("status_code", fiber.StatusNotFound), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusNotFound, "Failed to retrieve bet") + } + + if bet.CompanyID != companyID.Value { + h.mongoLoggerSvc.Warn("User Attempt to access another company bet", + zap.Int64("betID", id), + zap.Int("status_code", fiber.StatusNotFound), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusNotFound, "Failed to retrieve bet") + } + err = h.betSvc.SetBetToRemoved(c.Context(), id) if err != nil { h.mongoLoggerSvc.Error("Failed to delete bet by ID", diff --git a/internal/web_server/handlers/common_handler.go b/internal/web_server/handlers/common_handler.go new file mode 100644 index 0000000..1c2f09f --- /dev/null +++ b/internal/web_server/handlers/common_handler.go @@ -0,0 +1,25 @@ +package handlers + +import ( + "time" + + "github.com/gofiber/fiber/v2" + "go.uber.org/zap" +) + +func (h *Handler) BadRequestLogger() *zap.Logger { + return h.mongoLoggerSvc.With( + zap.Int("status_code", fiber.StatusBadRequest), + zap.Time("timestamp", time.Now())) +} +func (h *Handler) InternalServerErrorLogger() *zap.Logger { + return h.mongoLoggerSvc.With( + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Time("timestamp", time.Now())) +} +func (h *Handler) SuccessResLogger() *zap.Logger { + return h.mongoLoggerSvc.With( + zap.Int("status_code", fiber.StatusOK), + zap.Time("timestamp", time.Now()), + ) +} diff --git a/internal/web_server/handlers/event_handler.go b/internal/web_server/handlers/event_handler.go index e503b29..51dd68a 100644 --- a/internal/web_server/handlers/event_handler.go +++ b/internal/web_server/handlers/event_handler.go @@ -43,7 +43,7 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { if leagueIDQuery != "" { leagueIDInt, err := strconv.ParseInt(leagueIDQuery, 10, 64) if err != nil { - domain.BadRequestLogger.Error("invalid league id", + h.BadRequestLogger().Error("invalid league id", zap.String("league_id", leagueIDQuery), zap.Error(err), ) @@ -59,7 +59,7 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { if sportIDQuery != "" { sportIDint, err := strconv.Atoi(sportIDQuery) if err != nil { - domain.BadRequestLogger.Info("invalid sport id", + h.BadRequestLogger().Info("invalid sport id", zap.String("sportID", sportIDQuery), zap.Error(err), ) @@ -82,7 +82,7 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { if firstStartTimeQuery != "" { firstStartTimeParsed, err := time.Parse(time.RFC3339, firstStartTimeQuery) if err != nil { - domain.BadRequestLogger.Info("invalid start_time format", + h.BadRequestLogger().Info("invalid start_time format", zap.String("first_start_time", firstStartTimeQuery), zap.Error(err), ) @@ -99,7 +99,7 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { if lastStartTimeQuery != "" { lastStartTimeParsed, err := time.Parse(time.RFC3339, lastStartTimeQuery) if err != nil { - domain.BadRequestLogger.Info("invalid last_start_time format", + h.BadRequestLogger().Info("invalid last_start_time format", zap.String("last_start_time", lastStartTimeQuery), zap.Error(err), ) @@ -122,7 +122,7 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { if isFeaturedQuery != "" { isFeaturedParsed, err := strconv.ParseBool(isFeaturedQuery) if err != nil { - domain.BadRequestLogger.Error("Failed to parse isFeatured", + h.BadRequestLogger().Error("Failed to parse isFeatured", zap.String("is_featured", isFeaturedQuery), zap.Error(err), ) @@ -150,7 +150,7 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { // fmt.Printf("League ID: %v", leagueID) if err != nil { - domain.InternalServerErrorLogger.Error("Failed to retrieve all upcoming events", + h.InternalServerErrorLogger().Error("Failed to retrieve all upcoming events", zap.Error(err), ) return fiber.NewError(fiber.StatusInternalServerError, err.Error()) @@ -180,7 +180,7 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { func (h *Handler) GetTenantUpcomingEvents(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) if !companyID.Valid { - domain.BadRequestLogger.Error("invalid company id") + h.BadRequestLogger().Error("invalid company id") return fiber.NewError(fiber.StatusBadRequest, "invalid company id") } @@ -200,7 +200,7 @@ func (h *Handler) GetTenantUpcomingEvents(c *fiber.Ctx) error { if leagueIDQuery != "" { leagueIDInt, err := strconv.ParseInt(leagueIDQuery, 10, 64) if err != nil { - domain.BadRequestLogger.Error("invalid league id", + h.BadRequestLogger().Error("invalid league id", zap.String("league_id", leagueIDQuery), zap.Error(err), ) @@ -216,7 +216,7 @@ func (h *Handler) GetTenantUpcomingEvents(c *fiber.Ctx) error { if sportIDQuery != "" { sportIDint, err := strconv.Atoi(sportIDQuery) if err != nil { - domain.BadRequestLogger.Info("invalid sport id", + h.BadRequestLogger().Info("invalid sport id", zap.String("sportID", sportIDQuery), zap.Error(err), ) @@ -239,7 +239,7 @@ func (h *Handler) GetTenantUpcomingEvents(c *fiber.Ctx) error { if firstStartTimeQuery != "" { firstStartTimeParsed, err := time.Parse(time.RFC3339, firstStartTimeQuery) if err != nil { - domain.BadRequestLogger.Info("invalid start_time format", + h.BadRequestLogger().Info("invalid start_time format", zap.String("first_start_time", firstStartTimeQuery), zap.Error(err), ) @@ -256,7 +256,7 @@ func (h *Handler) GetTenantUpcomingEvents(c *fiber.Ctx) error { if lastStartTimeQuery != "" { lastStartTimeParsed, err := time.Parse(time.RFC3339, lastStartTimeQuery) if err != nil { - domain.BadRequestLogger.Info("invalid last_start_time format", + h.BadRequestLogger().Info("invalid last_start_time format", zap.String("last_start_time", lastStartTimeQuery), zap.Error(err), ) @@ -279,7 +279,7 @@ func (h *Handler) GetTenantUpcomingEvents(c *fiber.Ctx) error { if isFeaturedQuery != "" { isFeaturedParsed, err := strconv.ParseBool(isFeaturedQuery) if err != nil { - domain.BadRequestLogger.Error("Failed to parse isFeatured", + h.BadRequestLogger().Error("Failed to parse isFeatured", zap.String("is_featured", isFeaturedQuery), zap.Error(err), ) @@ -307,7 +307,7 @@ func (h *Handler) GetTenantUpcomingEvents(c *fiber.Ctx) error { // fmt.Printf("League ID: %v", leagueID) if err != nil { - domain.InternalServerErrorLogger.Error("Failed to retrieve all upcoming events", + h.InternalServerErrorLogger().Error("Failed to retrieve all upcoming events", zap.Error(err), ) return fiber.NewError(fiber.StatusInternalServerError, err.Error()) @@ -343,7 +343,7 @@ type TopLeague struct { func (h *Handler) GetTopLeagues(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) if !companyID.Valid { - domain.BadRequestLogger.Error("invalid company id") + h.BadRequestLogger().Error("invalid company id") return fiber.NewError(fiber.StatusBadRequest, "invalid company id") } @@ -355,7 +355,7 @@ func (h *Handler) GetTopLeagues(c *fiber.Ctx) error { }) if err != nil { - domain.InternalServerErrorLogger.Error("Error while fetching top leagues", + h.InternalServerErrorLogger().Error("Error while fetching top leagues", zap.Int64("company_id", companyID.Value), zap.Error(err), ) @@ -372,7 +372,7 @@ func (h *Handler) GetTopLeagues(c *fiber.Ctx) error { }, }) if err != nil { - domain.InternalServerErrorLogger.Warn("Error while fetching events for top league", + h.InternalServerErrorLogger().Warn("Error while fetching events for top league", zap.Int64("LeagueID", league.ID), zap.Int64("company_id", companyID.Value), zap.Error(err), @@ -408,13 +408,13 @@ func (h *Handler) GetUpcomingEventByID(c *fiber.Ctx) error { id := c.Params("id") if id == "" { - domain.BadRequestLogger.Info("Failed to parse event id", zap.String("id", id)) + h.BadRequestLogger().Info("Failed to parse event id", zap.String("id", id)) return fiber.NewError(fiber.StatusBadRequest, "Missing id") } event, err := h.eventSvc.GetUpcomingEventByID(c.Context(), id) if err != nil { - domain.InternalServerErrorLogger.Error("Failed to get upcoming event by id", + h.InternalServerErrorLogger().Error("Failed to get upcoming event by id", zap.String("eventID", id), zap.Error(err), ) @@ -440,19 +440,19 @@ func (h *Handler) GetUpcomingEventByID(c *fiber.Ctx) error { func (h *Handler) GetTenantEventByID(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) if !companyID.Valid { - domain.BadRequestLogger.Error("invalid company id") + h.BadRequestLogger().Error("invalid company id") return fiber.NewError(fiber.StatusBadRequest, "invalid company id") } id := c.Params("id") if id == "" { - domain.BadRequestLogger.Info("Failed to parse event id", zap.String("id", id)) + h.BadRequestLogger().Info("Failed to parse event id", zap.String("id", id)) return fiber.NewError(fiber.StatusBadRequest, "Missing id") } event, err := h.eventSvc.GetEventWithSettingByID(c.Context(), id, companyID.Value) if err != nil { - domain.InternalServerErrorLogger.Error("Failed to get upcoming event by id", + h.InternalServerErrorLogger().Error("Failed to get upcoming event by id", zap.String("eventID", id), zap.Error(err), ) @@ -484,7 +484,7 @@ func (h *Handler) SetEventStatusToRemoved(c *fiber.Ctx) error { err := h.eventSvc.UpdateEventStatus(c.Context(), eventID, domain.STATUS_REMOVED) if err != nil { - domain.InternalServerErrorLogger.Error("Failed to update event status", + h.InternalServerErrorLogger().Error("Failed to update event status", zap.String("EventID", eventID), zap.Error(err), ) @@ -515,7 +515,7 @@ type UpdateEventSettingsReq struct { func (h *Handler) UpdateEventSettings(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) if !companyID.Valid { - domain.BadRequestLogger.Error("invalid company id") + h.BadRequestLogger().Error("invalid company id") return fiber.NewError(fiber.StatusBadRequest, "invalid company id") } @@ -523,7 +523,7 @@ func (h *Handler) UpdateEventSettings(c *fiber.Ctx) error { var req UpdateEventSettingsReq if err := c.BodyParser(&req); err != nil { - domain.BadRequestLogger.Info("Failed to parse user id", + h.BadRequestLogger().Info("Failed to parse user id", zap.String("eventID", eventID), zap.Error(err), ) @@ -543,7 +543,7 @@ func (h *Handler) UpdateEventSettings(c *fiber.Ctx) error { for field, msg := range valErrs { errMsg += fmt.Sprintf("%s: %s; ", field, msg) } - domain.BadRequestLogger.Error("Failed to update event featured", + h.BadRequestLogger().Error("Failed to update event featured", append(logFields, zap.String("errMsg", errMsg))..., ) return fiber.NewError(fiber.StatusBadRequest, errMsg) @@ -557,7 +557,7 @@ func (h *Handler) UpdateEventSettings(c *fiber.Ctx) error { }) if err != nil { - domain.InternalServerErrorLogger.Error("Failed to update event featured", append(logFields, zap.Error(err))...) + h.InternalServerErrorLogger().Error("Failed to update event featured", append(logFields, zap.Error(err))...) return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } diff --git a/internal/web_server/handlers/leagues.go b/internal/web_server/handlers/leagues.go index 222d95d..6755b9a 100644 --- a/internal/web_server/handlers/leagues.go +++ b/internal/web_server/handlers/leagues.go @@ -50,7 +50,7 @@ func (h *Handler) GetAllLeagues(c *fiber.Ctx) error { if sportIDQuery != "" { sportIDint, err := strconv.Atoi(sportIDQuery) if err != nil { - domain.BadRequestLogger.Info("invalid sport id", + h.BadRequestLogger().Info("invalid sport id", zap.String("sport_id", sportIDQuery), zap.Error(err), ) @@ -89,7 +89,7 @@ func (h *Handler) GetAllLeagues(c *fiber.Ctx) error { if err != nil { fmt.Printf("Error fetching league %v \n", err) - domain.InternalServerErrorLogger.Error("Failed to get all leagues", append(queryLogFields, zap.Error(err))..., + h.InternalServerErrorLogger().Error("Failed to get all leagues", append(queryLogFields, zap.Error(err))..., ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to get leagues:"+err.Error()) } @@ -109,7 +109,7 @@ func (h *Handler) GetAllLeagues(c *fiber.Ctx) error { func (h *Handler) GetAllLeaguesForTenant(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) if !companyID.Valid { - domain.BadRequestLogger.Error("invalid company id") + h.BadRequestLogger().Error("invalid company id") return fiber.NewError(fiber.StatusBadRequest, "invalid company id") } @@ -142,7 +142,7 @@ func (h *Handler) GetAllLeaguesForTenant(c *fiber.Ctx) error { if sportIDQuery != "" { sportIDint, err := strconv.Atoi(sportIDQuery) if err != nil { - domain.BadRequestLogger.Info("invalid sport id", + h.BadRequestLogger().Info("invalid sport id", zap.String("sport_id", sportIDQuery), zap.Error(err), ) @@ -183,7 +183,7 @@ func (h *Handler) GetAllLeaguesForTenant(c *fiber.Ctx) error { if err != nil { fmt.Printf("Error fetching league %v \n", err) - domain.InternalServerErrorLogger.Error("Failed to get all leagues", append(queryLogFields, zap.Error(err))...) + h.InternalServerErrorLogger().Error("Failed to get all leagues", append(queryLogFields, zap.Error(err))...) return fiber.NewError(fiber.StatusInternalServerError, "Failed to get leagues:"+err.Error()) } return response.WriteJSON(c, fiber.StatusOK, "All leagues retrieved", leagues, nil) @@ -228,7 +228,7 @@ func (h *Handler) SetLeagueActive(c *fiber.Ctx) error { var req SetLeagueActiveReq if err := c.BodyParser(&req); err != nil { - domain.InternalServerErrorLogger.Error("SetLeagueReq failed to parse request body", + h.InternalServerErrorLogger().Error("SetLeagueReq failed to parse request body", append(queryLogFields, zap.Error(err))..., ) return fiber.NewError(fiber.StatusBadRequest, "Failed to parse request:"+err.Error()) @@ -241,7 +241,7 @@ func (h *Handler) SetLeagueActive(c *fiber.Ctx) error { for field, msg := range valErrs { errMsg += fmt.Sprintf("%s: %s; ", field, msg) } - domain.BadRequestLogger.Info("Failed to validate SetLeagueActiveReq", append(queryLogFields, zap.Error(err))...) + h.BadRequestLogger().Info("Failed to validate SetLeagueActiveReq", append(queryLogFields, zap.Error(err))...) return fiber.NewError(fiber.StatusBadRequest, errMsg) } @@ -253,11 +253,11 @@ func (h *Handler) SetLeagueActive(c *fiber.Ctx) error { Valid: true, }, }); err != nil { - domain.InternalServerErrorLogger.Error("Failed to update league active", append(queryLogFields, zap.Error(err))...) + h.InternalServerErrorLogger().Error("Failed to update league active", append(queryLogFields, zap.Error(err))...) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update league:"+err.Error()) } - domain.SuccessResLogger.Info("League Active has been successfully updated", queryLogFields...) + h.SuccessResLogger().Info("League Active has been successfully updated", queryLogFields...) return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil) } @@ -300,7 +300,7 @@ func (h *Handler) SetLeagueFeatured(c *fiber.Ctx) error { } var req SetLeagueAsFeatured if err := c.BodyParser(&req); err != nil { - domain.BadRequestLogger.Info("SetLeagueFeaturedReq failed to parse request body", append(queryLogFields, zap.Error(err))...) + h.BadRequestLogger().Info("SetLeagueFeaturedReq failed to parse request body", append(queryLogFields, zap.Error(err))...) return fiber.NewError(fiber.StatusBadRequest, "Failed to parse request body:"+err.Error()) } @@ -311,7 +311,7 @@ func (h *Handler) SetLeagueFeatured(c *fiber.Ctx) error { for field, msg := range valErrs { errMsg += fmt.Sprintf("%s: %s; ", field, msg) } - domain.BadRequestLogger.Info("Failed to validate SetLeagueFeaturedReq", append(queryLogFields, zap.Error(err))...) + h.BadRequestLogger().Info("Failed to validate SetLeagueFeaturedReq", append(queryLogFields, zap.Error(err))...) return fiber.NewError(fiber.StatusBadRequest, errMsg) } @@ -324,10 +324,10 @@ func (h *Handler) SetLeagueFeatured(c *fiber.Ctx) error { }, }) if err != nil { - domain.InternalServerErrorLogger.Error("Failed to update league", append(queryLogFields, zap.Error(err))...) + h.InternalServerErrorLogger().Error("Failed to update league", append(queryLogFields, zap.Error(err))...) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update league:"+err.Error()) } - domain.SuccessResLogger.Info("League Featured has been successfully updated", queryLogFields...) + h.SuccessResLogger().Info("League Featured has been successfully updated", queryLogFields...) return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil) } diff --git a/internal/web_server/handlers/odd_handler.go b/internal/web_server/handlers/odd_handler.go index 3d62b35..bf89c84 100644 --- a/internal/web_server/handlers/odd_handler.go +++ b/internal/web_server/handlers/odd_handler.go @@ -21,13 +21,13 @@ import ( func (h *Handler) GetAllOdds(c *fiber.Ctx) error { limit, err := strconv.Atoi(c.Query("limit", "10")) // Default limit is 10 if err != nil || limit <= 0 { - domain.BadRequestLogger.Info("Invalid limit value", zap.Error(err)) + h.BadRequestLogger().Info("Invalid limit value", zap.Error(err)) return fiber.NewError(fiber.StatusBadRequest, "Invalid limit value") } offset, err := strconv.Atoi(c.Query("offset", "0")) // Default offset is 0 if err != nil || offset < 0 { - domain.BadRequestLogger.Info("Invalid offset value", zap.Error(err)) + h.BadRequestLogger().Info("Invalid offset value", zap.Error(err)) return fiber.NewError(fiber.StatusBadRequest, err.Error()) } @@ -42,7 +42,7 @@ func (h *Handler) GetAllOdds(c *fiber.Ctx) error { }, }) if err != nil { - domain.InternalServerErrorLogger.Error("Failed to retrieve all odds", + h.InternalServerErrorLogger().Error("Failed to retrieve all odds", zap.Int("limit", limit), zap.Int("offset", offset), zap.Error(err), @@ -65,19 +65,19 @@ func (h *Handler) GetAllOdds(c *fiber.Ctx) error { func (h *Handler) GetAllTenantOdds(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) if !companyID.Valid { - domain.BadRequestLogger.Error("invalid company id") + h.BadRequestLogger().Error("invalid company id") return fiber.NewError(fiber.StatusBadRequest, "invalid company id") } limit, err := strconv.Atoi(c.Query("limit", "10")) // Default limit is 10 if err != nil || limit <= 0 { - domain.BadRequestLogger.Info("Invalid limit value", zap.Error(err)) + h.BadRequestLogger().Info("Invalid limit value", zap.Error(err)) return fiber.NewError(fiber.StatusBadRequest, "Invalid limit value") } offset, err := strconv.Atoi(c.Query("offset", "0")) // Default offset is 0 if err != nil || offset < 0 { - domain.BadRequestLogger.Info("Invalid offset value", zap.Error(err)) + h.BadRequestLogger().Info("Invalid offset value", zap.Error(err)) return fiber.NewError(fiber.StatusBadRequest, err.Error()) } @@ -92,7 +92,7 @@ func (h *Handler) GetAllTenantOdds(c *fiber.Ctx) error { }, }) if err != nil { - domain.InternalServerErrorLogger.Error("Failed to retrieve all odds", + h.InternalServerErrorLogger().Error("Failed to retrieve all odds", zap.Int("limit", limit), zap.Int("offset", offset), zap.Error(err), @@ -123,20 +123,20 @@ func (h *Handler) GetOddsByMarketID(c *fiber.Ctx) error { marketID := c.Params("market_id") if marketID == "" { - domain.BadRequestLogger.Info("Missing market_id", logFields...) + h.BadRequestLogger().Info("Missing market_id", logFields...) return fiber.NewError(fiber.StatusBadRequest, "Missing market_id") } upcomingID := c.Params("upcoming_id") if upcomingID == "" { - domain.BadRequestLogger.Info("Missing upcoming_id", logFields...) + h.BadRequestLogger().Info("Missing upcoming_id", logFields...) return fiber.NewError(fiber.StatusBadRequest, "Missing upcoming_id") } rawOdds, err := h.prematchSvc.GetOddsByMarketID(c.Context(), marketID, upcomingID) if err != nil { // Lets turn this into a warn because this is constantly going off - domain.InternalServerErrorLogger.Warn("Failed to get raw odds by market ID", append(logFields, zap.Error(err))...) + h.InternalServerErrorLogger().Warn("Failed to get raw odds by market ID", append(logFields, zap.Error(err))...) return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } @@ -159,7 +159,7 @@ func (h *Handler) GetOddsByMarketID(c *fiber.Ctx) error { func (h *Handler) GetTenantOddsByMarketID(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) if !companyID.Valid { - domain.BadRequestLogger.Error("invalid company id") + h.BadRequestLogger().Error("invalid company id") return fiber.NewError(fiber.StatusBadRequest, "invalid company id") } logFields := []zap.Field{ @@ -170,13 +170,13 @@ func (h *Handler) GetTenantOddsByMarketID(c *fiber.Ctx) error { marketID := c.Params("market_id") if marketID == "" { - domain.BadRequestLogger.Info("Missing market_id", logFields...) + h.BadRequestLogger().Info("Missing market_id", logFields...) return fiber.NewError(fiber.StatusBadRequest, "Missing market_id") } upcomingID := c.Params("upcoming_id") if upcomingID == "" { - domain.BadRequestLogger.Info("Missing upcoming_id", logFields...) + h.BadRequestLogger().Info("Missing upcoming_id", logFields...) return fiber.NewError(fiber.StatusBadRequest, "Missing upcoming_id") } @@ -184,7 +184,7 @@ func (h *Handler) GetTenantOddsByMarketID(c *fiber.Ctx) error { if err != nil { // Lets turn this into a warn because this is constantly going off - domain.InternalServerErrorLogger.Warn("Failed to get raw odds by market ID", append(logFields, zap.Error(err))...) + h.InternalServerErrorLogger().Warn("Failed to get raw odds by market ID", append(logFields, zap.Error(err))...) return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } @@ -214,28 +214,28 @@ func (h *Handler) GetOddsByUpcomingID(c *fiber.Ctx) error { upcomingID := c.Params("upcoming_id") if upcomingID == "" { - domain.BadRequestLogger.Info("Missing upcoming_id", logFields...) + h.BadRequestLogger().Info("Missing upcoming_id", logFields...) return fiber.NewError(fiber.StatusBadRequest, "Missing upcoming_id") } limit, err := strconv.Atoi(c.Query("limit", "10")) // Default limit is 10 if err != nil || limit <= 0 { logFields = append(logFields, zap.Error(err)) - domain.BadRequestLogger.Info("Invalid limit value", logFields...) + h.BadRequestLogger().Info("Invalid limit value", logFields...) return fiber.NewError(fiber.StatusBadRequest, "Invalid limit value") } offset, err := strconv.Atoi(c.Query("offset", "0")) // Default offset is 0 if err != nil || offset < 0 { logFields = append(logFields, zap.Error(err)) - domain.BadRequestLogger.Info("Invalid offset value", logFields...) + h.BadRequestLogger().Info("Invalid offset value", logFields...) return fiber.NewError(fiber.StatusBadRequest, err.Error()) } odds, err := h.prematchSvc.GetOddsByEventID(c.Context(), upcomingID, domain.OddMarketWithEventFilter{}) if err != nil { logFields = append(logFields, zap.Error(err)) - domain.InternalServerErrorLogger.Error("Failed to retrieve odds", append(logFields, zap.Error(err))...) + h.InternalServerErrorLogger().Error("Failed to retrieve odds", append(logFields, zap.Error(err))...) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve odds"+err.Error()) } @@ -259,7 +259,7 @@ func (h *Handler) GetOddsByUpcomingID(c *fiber.Ctx) error { func (h *Handler) GetTenantOddsByUpcomingID(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) if !companyID.Valid { - domain.BadRequestLogger.Error("invalid company id") + h.BadRequestLogger().Error("invalid company id") return fiber.NewError(fiber.StatusBadRequest, "invalid company id") } @@ -272,28 +272,28 @@ func (h *Handler) GetTenantOddsByUpcomingID(c *fiber.Ctx) error { upcomingID := c.Params("upcoming_id") if upcomingID == "" { - domain.BadRequestLogger.Info("Missing upcoming_id", logFields...) + h.BadRequestLogger().Info("Missing upcoming_id", logFields...) return fiber.NewError(fiber.StatusBadRequest, "Missing upcoming_id") } limit, err := strconv.Atoi(c.Query("limit", "10")) // Default limit is 10 if err != nil || limit <= 0 { logFields = append(logFields, zap.Error(err)) - domain.BadRequestLogger.Info("Invalid limit value", logFields...) + h.BadRequestLogger().Info("Invalid limit value", logFields...) return fiber.NewError(fiber.StatusBadRequest, "Invalid limit value") } offset, err := strconv.Atoi(c.Query("offset", "0")) // Default offset is 0 if err != nil || offset < 0 { logFields = append(logFields, zap.Error(err)) - domain.BadRequestLogger.Info("Invalid offset value", logFields...) + h.BadRequestLogger().Info("Invalid offset value", logFields...) return fiber.NewError(fiber.StatusBadRequest, err.Error()) } odds, err := h.prematchSvc.GetOddsWithSettingsByEventID(c.Context(), upcomingID, companyID.Value, domain.OddMarketFilter{}) if err != nil { logFields = append(logFields, zap.Error(err)) - domain.InternalServerErrorLogger.Error("Failed to retrieve odds", append(logFields, zap.Error(err))...) + h.InternalServerErrorLogger().Error("Failed to retrieve odds", append(logFields, zap.Error(err))...) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve odds"+err.Error()) } diff --git a/internal/web_server/handlers/settings_handler.go b/internal/web_server/handlers/settings_handler.go index 016b80b..bc30195 100644 --- a/internal/web_server/handlers/settings_handler.go +++ b/internal/web_server/handlers/settings_handler.go @@ -10,8 +10,8 @@ import ( "go.uber.org/zap" ) -func (h *Handler) GetSettingList(c *fiber.Ctx) error { - settingsList, err := h.settingSvc.GetSettingList(c.Context()) +func (h *Handler) GetGlobalSettingList(c *fiber.Ctx) error { + settingsList, err := h.settingSvc.GetGlobalSettingList(c.Context()) if err != nil { h.mongoLoggerSvc.Error("Failed to fetch settings", @@ -25,7 +25,7 @@ func (h *Handler) GetSettingList(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "All Settings retrieved successfully", settingsList, nil) } -func (h *Handler) GetSettingByKey(c *fiber.Ctx) error { +func (h *Handler) GetGlobalSettingByKey(c *fiber.Ctx) error { settingKey := c.Params("key") if settingKey == "" { h.mongoLoggerSvc.Info("empty setting key", @@ -35,7 +35,7 @@ func (h *Handler) GetSettingByKey(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "setting key must be passed") } - setting, err := h.settingSvc.GetSetting(c.Context(), settingKey) + setting, err := h.settingSvc.GetGlobalSetting(c.Context(), settingKey) if err != nil { h.mongoLoggerSvc.Info("invalid setting key", @@ -52,12 +52,12 @@ func (h *Handler) GetSettingByKey(c *fiber.Ctx) error { } -func (h *Handler) UpdateSettingList(c *fiber.Ctx) error { +func (h *Handler) UpdateGlobalSettingList(c *fiber.Ctx) error { - var req domain.UpdateSettingListReq + var req domain.SaveSettingListReq if err := c.BodyParser(&req); err != nil { - h.mongoLoggerSvc.Info("Failed to parse UpdateSettingListReq", + h.mongoLoggerSvc.Info("Failed to parse SaveSettingListReq", zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), @@ -70,26 +70,82 @@ func (h *Handler) UpdateSettingList(c *fiber.Ctx) error { for field, msg := range valErrs { errMsg += fmt.Sprintf("%s: %s; ", field, msg) } - h.mongoLoggerSvc.Info("Failed to validate UpdateSettingListReq", + h.mongoLoggerSvc.Info("Failed to validate SaveSettingListReq", zap.Int("status_code", fiber.StatusBadRequest), zap.String("errMsg", errMsg), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, errMsg) } - settingList := domain.ConvertUpdateSettingListReq(req) - err := h.settingSvc.UpdateSettingList(c.Context(), settingList) + settingList := domain.ConvertSaveSettingListReq(req) + err := h.settingSvc.UpdateGlobalSettingList(c.Context(), settingList) if err != nil { - h.mongoLoggerSvc.Info("failed to update setting", + h.mongoLoggerSvc.Info("failed to save setting", zap.Any("setting_list", settingList), zap.Int("status_code", fiber.StatusInternalServerError), zap.Time("timestamp", time.Now()), ) - return fiber.NewError(fiber.StatusInternalServerError, "failed to update setting") + return fiber.NewError(fiber.StatusInternalServerError, "failed to save setting") } - settingsList, err := h.settingSvc.GetSettingList(c.Context()) + settingsList, err := h.settingSvc.GetGlobalSettingList(c.Context()) + + if err != nil { + h.mongoLoggerSvc.Error("Failed to fetch settings", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get setting list:"+err.Error()) + } + + return response.WriteJSON(c, fiber.StatusOK, "setting updated", settingsList, nil) +} + +func (h *Handler) SaveCompanySettingList(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + + var req domain.SaveSettingListReq + + if err := c.BodyParser(&req); err != nil { + h.mongoLoggerSvc.Info("Failed to parse SaveSettingListReq", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request") + } + valErrs, ok := h.validator.Validate(c, req) + if !ok { + var errMsg string + for field, msg := range valErrs { + errMsg += fmt.Sprintf("%s: %s; ", field, msg) + } + h.mongoLoggerSvc.Info("Failed to validate SaveSettingListReq", + zap.Int("status_code", fiber.StatusBadRequest), + zap.String("errMsg", errMsg), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, errMsg) + } + settingList := domain.ConvertSaveSettingListReq(req) + err := h.settingSvc.InsertCompanySettingList(c.Context(), settingList, companyID.Value) + + if err != nil { + h.mongoLoggerSvc.Info("failed to save setting", + zap.Any("setting_list", settingList), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "failed to save setting") + } + + settingsList, err := h.settingSvc.GetOverrideSettingsList(c.Context(), companyID.Value) if err != nil { h.mongoLoggerSvc.Error("Failed to fetch settings", @@ -103,3 +159,76 @@ func (h *Handler) UpdateSettingList(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "setting updated", settingsList, nil) } + +func (h *Handler) GetCompanySettingList(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + + settingsList, err := h.settingSvc.GetOverrideSettingsList(c.Context(), companyID.Value) + + if err != nil { + h.mongoLoggerSvc.Error("Failed to fetch settings", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get setting list:"+err.Error()) + } + + return response.WriteJSON(c, fiber.StatusOK, "All Settings retrieved successfully", settingsList, nil) +} + +// /api/v1/{tenant_slug}/settings/{key} +func (h *Handler) DeleteCompanySetting(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + + settingKey := c.Params("key") + if settingKey == "" { + h.mongoLoggerSvc.Info("empty setting key", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "setting key must be passed") + } + + err := h.settingSvc.DeleteCompanySetting(c.Context(), companyID.Value, settingKey) + + if err != nil { + h.mongoLoggerSvc.Error("Failed to delete company override settings", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete company override settings:"+err.Error()) + } + + return response.WriteJSON(c, fiber.StatusOK, "setting deleted", nil, nil) +} + +func (h *Handler) DeleteAllCompanySetting(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + + err := h.settingSvc.DeleteAllCompanySetting(c.Context(), companyID.Value) + + if err != nil { + h.mongoLoggerSvc.Error("Failed to delete company override settings", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete company override settings:"+err.Error()) + } + + return response.WriteJSON(c, fiber.StatusOK, "setting deleted", nil, nil) +} diff --git a/internal/web_server/handlers/shop_handler.go b/internal/web_server/handlers/shop_handler.go index f63341f..7a25615 100644 --- a/internal/web_server/handlers/shop_handler.go +++ b/internal/web_server/handlers/shop_handler.go @@ -27,7 +27,7 @@ func (h *Handler) CreateShopBet(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) role := c.Locals("role").(domain.Role) company_id := c.Locals("company_id").(domain.ValidInt64) - + var req domain.ShopBetReq if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Info("CreateBetReq failed to parse request", diff --git a/internal/web_server/handlers/ticket_handler.go b/internal/web_server/handlers/ticket_handler.go index b77f1e6..f3b108a 100644 --- a/internal/web_server/handlers/ticket_handler.go +++ b/internal/web_server/handlers/ticket_handler.go @@ -21,8 +21,13 @@ import ( // @Success 200 {object} domain.CreateTicketRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/ticket [post] +// @Router /api/v1/{tenant_slug}/ticket [post] func (h *Handler) CreateTicket(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } var req domain.CreateTicketReq if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Info("Failed to parse CreateTicket request", @@ -47,7 +52,7 @@ func (h *Handler) CreateTicket(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, errMsg) } - newTicket, rows, err := h.ticketSvc.CreateTicket(c.Context(), req, c.IP()) + newTicket, rows, err := h.ticketSvc.CreateTicket(c.Context(), req, c.IP(), companyID.Value) if err != nil { @@ -85,8 +90,13 @@ func (h *Handler) CreateTicket(c *fiber.Ctx) error { // @Success 200 {object} domain.TicketRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/ticket/{id} [get] +// @Router /api/v1/{tenant_slug}/ticket/{id} [get] func (h *Handler) GetTicketByID(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } ticketID := c.Params("id") id, err := strconv.ParseInt(ticketID, 10, 64) if err != nil { @@ -110,11 +120,22 @@ func (h *Handler) GetTicketByID(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusNotFound, "Failed to retrieve ticket") } + if ticket.CompanyID != companyID.Value { + h.mongoLoggerSvc.Warn("User attempt to access another company ticket", + zap.Int64("ticketID", id), + zap.Int("status_code", fiber.StatusNotFound), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusNotFound, "Failed to retrieve ticket") + } + res := domain.TicketRes{ ID: ticket.ID, Outcomes: ticket.Outcomes, Amount: ticket.Amount.Float32(), TotalOdds: ticket.TotalOdds, + CompanyID: ticket.CompanyID, } return response.WriteJSON(c, fiber.StatusOK, "Ticket retrieved successfully", res, nil) } @@ -128,10 +149,18 @@ func (h *Handler) GetTicketByID(c *fiber.Ctx) error { // @Success 200 {array} domain.TicketRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/ticket [get] +// @Router /api/v1/{tenant_slug}/ticket [get] func (h *Handler) GetAllTickets(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + + tickets, err := h.ticketSvc.GetAllTickets(c.Context(), domain.TicketFilter{ + CompanyID: companyID, + }) - tickets, err := h.ticketSvc.GetAllTickets(c.Context()) if err != nil { h.mongoLoggerSvc.Error("Failed to get tickets", zap.Int("status_code", fiber.StatusInternalServerError), diff --git a/internal/web_server/handlers/user.go b/internal/web_server/handlers/user.go index f31452b..decf177 100644 --- a/internal/web_server/handlers/user.go +++ b/internal/web_server/handlers/user.go @@ -37,7 +37,7 @@ type CheckPhoneEmailExistRes struct { func (h *Handler) CheckPhoneEmailExist(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) if !companyID.Valid { - domain.BadRequestLogger.Error("invalid company id") + h.BadRequestLogger().Error("invalid company id") return fiber.NewError(fiber.StatusBadRequest, "invalid company id") } var req CheckPhoneEmailExistReq @@ -95,7 +95,7 @@ type RegisterCodeReq struct { func (h *Handler) SendRegisterCode(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) if !companyID.Valid { - domain.BadRequestLogger.Error("invalid company id") + h.BadRequestLogger().Error("invalid company id") return fiber.NewError(fiber.StatusBadRequest, "invalid company id") } var req RegisterCodeReq @@ -166,7 +166,7 @@ type RegisterUserReq struct { func (h *Handler) RegisterUser(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) if !companyID.Valid { - domain.BadRequestLogger.Error("invalid company id") + h.BadRequestLogger().Error("invalid company id") return fiber.NewError(fiber.StatusBadRequest, "invalid company id") } var req RegisterUserReq @@ -248,7 +248,7 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error { } if req.ReferalCode != "" { - err = h.referralSvc.ProcessReferral(c.Context(), req.PhoneNumber, req.ReferalCode) + err = h.referralSvc.ProcessReferral(c.Context(), req.PhoneNumber, req.ReferalCode, companyID.Value) if err != nil { h.mongoLoggerSvc.Error("Failed to process referral during registration", zap.String("phone", req.PhoneNumber), @@ -297,7 +297,7 @@ type ResetCodeReq struct { func (h *Handler) SendResetCode(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) if !companyID.Valid { - domain.BadRequestLogger.Error("invalid company id") + h.BadRequestLogger().Error("invalid company id") return fiber.NewError(fiber.StatusBadRequest, "invalid company id") } var req ResetCodeReq diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 0457919..2a4ead3 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -151,7 +151,7 @@ func (a *App) initAppRoutes() { tenantAuth.Get("/user/customer-profile", h.CustomerProfile) tenantAuth.Get("/user/admin-profile", h.AdminProfile) tenantAuth.Get("/user/bets", h.GetBetByUserID) - + groupV1.Get("/user/single/:id", a.authMiddleware, h.GetUserByID) groupV1.Post("/user/suspend", a.authMiddleware, h.UpdateUserSuspend) groupV1.Delete("/user/delete/:id", a.authMiddleware, h.DeleteUser) @@ -194,7 +194,7 @@ func (a *App) initAppRoutes() { groupV1.Get("/odds", a.authMiddleware, a.SuperAdminOnly, h.GetAllOdds) groupV1.Get("/odds/upcoming/:upcoming_id", a.authMiddleware, a.SuperAdminOnly, h.GetOddsByUpcomingID) groupV1.Get("/odds/upcoming/:upcoming_id/market/:market_id", a.authMiddleware, a.SuperAdminOnly, h.GetOddsByMarketID) - + tenant.Get("/odds", h.GetAllTenantOdds) tenant.Get("/odds/upcoming/:upcoming_id", h.GetTenantOddsByUpcomingID) tenant.Get("/odds/upcoming/:upcoming_id/market/:market_id", h.GetTenantOddsByMarketID) @@ -234,8 +234,8 @@ func (a *App) initAppRoutes() { groupV1.Get("/branchCashier", a.authMiddleware, h.GetBranchForCashier) // Branch Operation - groupV1.Get("/supportedOperation", a.authMiddleware, h.GetAllSupportedOperations) - groupV1.Post("/supportedOperation", a.authMiddleware, h.CreateSupportedOperation) + groupV1.Get("/supportedOperation", a.authMiddleware, a.SuperAdminOnly, h.GetAllSupportedOperations) + groupV1.Post("/supportedOperation", a.authMiddleware, a.SuperAdminOnly, h.CreateSupportedOperation) groupV1.Post("/operation", a.authMiddleware, h.CreateBranchOperation) groupV1.Get("/branch/:id/operation", a.authMiddleware, h.GetBranchOperations) @@ -252,20 +252,20 @@ func (a *App) initAppRoutes() { groupV1.Get("/admin-company", a.authMiddleware, h.GetCompanyForAdmin) // Ticket Routes - groupV1.Post("/ticket", h.CreateTicket) - groupV1.Get("/ticket", h.GetAllTickets) - groupV1.Get("/ticket/:id", h.GetTicketByID) + tenant.Post("/ticket", h.CreateTicket) + tenant.Get("/ticket", h.GetAllTickets) + tenant.Get("/ticket/:id", h.GetTicketByID) // Bet Routes - groupV1.Post("/sport/bet", a.authMiddleware, h.CreateBet) - groupV1.Post("/sport/bet/fastcode", a.authMiddleware, h.CreateBetWithFastCode) - groupV1.Get("/sport/bet/fastcode/:fast_code", h.GetBetByFastCode) - groupV1.Get("/sport/bet", a.authMiddleware, h.GetAllBet) - groupV1.Get("/sport/bet/:id", h.GetBetByID) - groupV1.Patch("/sport/bet/:id", a.authMiddleware, h.UpdateCashOut) - groupV1.Delete("/sport/bet/:id", a.authMiddleware, h.DeleteBet) + tenantAuth.Post("/sport/bet", h.CreateBet) + tenantAuth.Post("/sport/bet/fastcode", h.CreateBetWithFastCode) + tenant.Get("/sport/bet/fastcode/:fast_code", h.GetBetByFastCode) + tenantAuth.Get("/sport/bet", h.GetAllBet) + tenantAuth.Get("/sport/bet/:id", h.GetBetByID) + tenantAuth.Patch("/sport/bet/:id", h.UpdateCashOut) + tenantAuth.Delete("/sport/bet/:id", h.DeleteBet) - groupV1.Post("/sport/random/bet", a.authMiddleware, h.RandomBet) + tenantAuth.Post("/sport/random/bet", h.RandomBet) // Wallet groupV1.Get("/wallet", h.GetAllWallets) @@ -326,10 +326,11 @@ func (a *App) initAppRoutes() { groupV1.Post("/shop/cashout", a.authMiddleware, a.CompanyOnly, h.CashoutByCashoutID) groupV1.Post("/shop/deposit", a.authMiddleware, a.CompanyOnly, h.DepositForCustomer) // groupV1.Get("/shop/deposit", a.authMiddleware, a.CompanyOnly, h.DepositForCustomer) - groupV1.Get("/shop/transaction", a.authMiddleware, h.GetAllTransactions) - groupV1.Get("/shop/transaction/:id", a.authMiddleware, h.GetTransactionByID) - groupV1.Get("/shop/transaction/:id/bet", a.authMiddleware, h.GetShopBetByTransactionID) - groupV1.Put("/shop/transaction/:id", a.authMiddleware, h.UpdateTransactionVerified) + + groupV1.Get("/shop/transaction", a.authMiddleware, a.CompanyOnly, h.GetAllTransactions) + groupV1.Get("/shop/transaction/:id", a.authMiddleware, a.CompanyOnly, h.GetTransactionByID) + groupV1.Get("/shop/transaction/:id/bet", a.authMiddleware, a.CompanyOnly, h.GetShopBetByTransactionID) + groupV1.Put("/shop/transaction/:id", a.authMiddleware, a.CompanyOnly, h.UpdateTransactionVerified) // Notification Routes groupV1.Get("/ws/connect", a.WebsocketAuthMiddleware, h.ConnectSocket) @@ -362,8 +363,13 @@ func (a *App) initAppRoutes() { groupV1.Delete("/issues/:issue_id", a.authMiddleware, a.OnlyAdminAndAbove, h.DeleteIssue) // Settings - groupV1.Get("/settings", a.authMiddleware, h.GetSettingList) - groupV1.Get("/settings/:key", a.authMiddleware, h.GetSettingByKey) - groupV1.Put("/settings", a.authMiddleware, h.UpdateSettingList) + groupV1.Get("/settings", a.authMiddleware, a.SuperAdminOnly, h.GetGlobalSettingList) + groupV1.Get("/settings/:key", a.authMiddleware, a.SuperAdminOnly, h.GetGlobalSettingByKey) + groupV1.Put("/settings", a.authMiddleware, a.SuperAdminOnly, h.UpdateGlobalSettingList) + + tenantAuth.Post("/settings", h.SaveCompanySettingList) + tenantAuth.Get("/settings", h.GetCompanySettingList) + tenantAuth.Delete("/settings/:key", h.DeleteCompanySetting) + tenantAuth.Delete("/settings", h.DeleteAllCompanySetting) } From 556860e93243f22ab3c4d105776aa7d296fa2f49 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 27 Aug 2025 21:35:03 +0300 Subject: [PATCH 10/10] minor fix --- db/migrations/000010_seed_data.up.sql | 252 +++++++++++++++++++++----- internal/services/bet/service.go | 1 + internal/web_server/routes.go | 7 +- 3 files changed, 211 insertions(+), 49 deletions(-) diff --git a/db/migrations/000010_seed_data.up.sql b/db/migrations/000010_seed_data.up.sql index 4bc76fa..6534e76 100644 --- a/db/migrations/000010_seed_data.up.sql +++ b/db/migrations/000010_seed_data.up.sql @@ -1,58 +1,218 @@ - CREATE EXTENSION IF NOT EXISTS pgcrypto; - -- Users INSERT INTO users ( - id, first_name, last_name, email, phone_number, password, role, - email_verified, phone_verified, created_at, updated_at, suspended, company_id -) VALUES -(1, 'John', 'Doe', 'john.doe@example.com', NULL, - crypt('password123', gen_salt('bf'))::bytea, 'customer', - TRUE, FALSE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, FALSE, NULL), - -(2, 'Test', 'Admin', 'test.admin@gmail.com', '0988554466', - crypt('password123', gen_salt('bf'))::bytea, 'admin', - TRUE, TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, FALSE, 1), - -(3, 'Samuel', 'Tariku', 'cybersamt@gmail.com', '0911111111', - crypt('password@123', gen_salt('bf'))::bytea, 'super_admin', - TRUE, TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, FALSE, NULL), - -(4, 'Kirubel', 'Kibru', 'kirubel.jkl679@gmail.com', '0911554486', - crypt('password@123', gen_salt('bf'))::bytea, 'super_admin', - TRUE, TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, FALSE, NULL); - + id, + first_name, + last_name, + email, + phone_number, + password, + role, + email_verified, + phone_verified, + created_at, + updated_at, + suspended, + company_id + ) +VALUES ( + 1, + 'John', + 'Doe', + 'john.doe@example.com', + NULL, + crypt('password123', gen_salt('bf'))::bytea, + 'customer', + TRUE, + FALSE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + FALSE, + NULL + ), + ( + 2, + 'Test', + 'Admin', + 'test.admin@gmail.com', + '0988554466', + crypt('password123', gen_salt('bf'))::bytea, + 'admin', + TRUE, + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + FALSE, + 1 + ), + ( + 3, + 'Samuel', + 'Tariku', + 'cybersamt@gmail.com', + '0911111111', + crypt('password@123', gen_salt('bf'))::bytea, + 'super_admin', + TRUE, + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + FALSE, + NULL + ), + ( + 4, + 'Kirubel', + 'Kibru', + 'kirubel.jkl679@gmail.com', + '0911554486', + crypt('password@123', gen_salt('bf'))::bytea, + 'super_admin', + TRUE, + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + FALSE, + NULL + ), + ( + 5, + 'Test', + 'Veli', + 'test.veli@example.com', + NULL, + crypt('password@123', gen_salt('bf'))::bytea, + 'customer', + TRUE, + FALSE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + FALSE, + NULL + ); -- Supported Operations -INSERT INTO supported_operations (id, name, description) VALUES -(1, 'SportBook', 'Sportbook operations'), -(2, 'Virtual', 'Virtual operations'); - +INSERT INTO supported_operations (id, name, description) +VALUES (1, 'SportBook', 'Sportbook operations'), + (2, 'Virtual', 'Virtual operations'); -- Wallets INSERT INTO wallets ( - id, balance, is_withdraw, is_bettable, is_transferable, user_id, - type, currency, is_active, created_at, updated_at -) VALUES -(1, 10000, TRUE, TRUE, TRUE, 1, 'regular', 'ETB', TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), -(2, 5000, FALSE, TRUE, TRUE, 1, 'static', 'ETB', TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), -(3, 20000, TRUE, TRUE, TRUE, 2, 'company_main', 'ETB', TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), -(4, 15000, TRUE, TRUE, TRUE, 2, 'branch_main', 'ETB', TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); - + id, + balance, + is_withdraw, + is_bettable, + is_transferable, + user_id, + type, + currency, + is_active, + created_at, + updated_at + ) +VALUES ( + 1, + 10000, + TRUE, + TRUE, + TRUE, + 1, + 'regular', + 'ETB', + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ), + ( + 2, + 5000, + FALSE, + TRUE, + TRUE, + 1, + 'static', + 'ETB', + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ), + ( + 3, + 20000, + TRUE, + TRUE, + TRUE, + 2, + 'company_main', + 'ETB', + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ), + ( + 4, + 15000, + TRUE, + TRUE, + TRUE, + 2, + 'branch_main', + 'ETB', + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ); -- Customer Wallets -INSERT INTO customer_wallets (id, customer_id, regular_wallet_id, static_wallet_id) +INSERT INTO customer_wallets ( + id, + customer_id, + regular_wallet_id, + static_wallet_id + ) VALUES (1, 1, 1, 2); - -- Company INSERT INTO companies ( - id, name, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at -) VALUES -(1, 'Test Company', 2, 3, 0.10, TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); - + id, + name, + admin_id, + wallet_id, + deducted_percentage, + is_active, + created_at, + updated_at + ) +VALUES ( + 1, + 'Test Company', + 2, + 3, + 0.10, + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ); -- Branch INSERT INTO branches ( - id, name, location, wallet_id, branch_manager_id, company_id, - is_self_owned, profit_percent, is_active, created_at, updated_at -) VALUES -(1, 'Test Branch', 'addis_ababa', 4, 2, 1, TRUE, 0.10, TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); - - - + id, + name, + location, + wallet_id, + branch_manager_id, + company_id, + is_self_owned, + profit_percent, + is_active, + created_at, + updated_at + ) +VALUES ( + 1, + 'Test Branch', + 'addis_ababa', + 4, + 2, + 1, + TRUE, + 0.10, + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ); \ No newline at end of file diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index 900317c..e026110 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -376,6 +376,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID ) return domain.CreateBetRes{}, err } + // default: s.mongoLogger.Error("unknown role type", zap.String("role", string(role)), diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 2a4ead3..d7baa03 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -71,8 +71,7 @@ func (a *App) initAppRoutes() { groupV1.Post("/direct_deposit", a.authMiddleware, h.InitiateDirectDeposit) groupV1.Post("/direct_deposit/verify", a.authMiddleware, h.VerifyDirectDeposit) groupV1.Get("/direct_deposit/pending", a.authMiddleware, h.GetPendingDirectDeposits) - groupV1.Post("/auth/admin-login", h.LoginAdmin) - groupV1.Post("/auth/refresh", h.RefreshToken) + // Swagger a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler()) @@ -142,6 +141,9 @@ func (a *App) initAppRoutes() { // groupV1.Post("/arifpay/transaction-id/verify-transaction", a.authMiddleware, h.ArifpayVerifyByTransactionIDHandler) // groupV1.Get("/arifpay/session-id/verify-transaction/:session_id", a.authMiddleware, h.ArifpayVerifyBySessionIDHandler) + + + // User Routes tenant.Post("/user/resetPassword", h.ResetPassword) tenant.Post("/user/sendResetCode", h.SendResetCode) @@ -155,7 +157,6 @@ func (a *App) initAppRoutes() { groupV1.Get("/user/single/:id", a.authMiddleware, h.GetUserByID) groupV1.Post("/user/suspend", a.authMiddleware, h.UpdateUserSuspend) groupV1.Delete("/user/delete/:id", a.authMiddleware, h.DeleteUser) - tenantAuth.Get("/user/wallet", a.authMiddleware, h.GetCustomerWallet) tenantAuth.Post("/user/search", a.authMiddleware, h.SearchUserByNameOrPhone)