From 3fb3da6cc8bbcc19978f00b3af92d26d87ea85aa Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 30 Jul 2025 16:55:57 +0300 Subject: [PATCH] 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 {