feat: Implement wallet notification system and refactor related services

- Added new notification handling in the wallet service to notify admins when wallet balances are low or insufficient.
- Created a new file for wallet notifications and moved relevant functions from the wallet service to this new file.
- Updated the wallet service to publish wallet events including wallet type.
- Refactored the client code to improve readability and maintainability.
- Enhanced the bet handler to support pagination and status filtering for bets.
- Updated routes and handlers for user search functionality to improve clarity and organization.
- Modified cron job scheduling to comment out unused jobs for clarity.
- Updated the WebSocket broadcast to include wallet type in notifications.
- Adjusted the makefile to include Kafka in the docker-compose setup for local development.
This commit is contained in:
Samuel Tariku 2025-09-25 21:26:24 +03:00
parent 9b56131f79
commit e49ff366d5
83 changed files with 1185 additions and 732 deletions

View File

@ -119,13 +119,13 @@ func main() {
oddsSvc := odds.New(store, cfg, eventSvc, logger, domain.MongoDBLogger) oddsSvc := odds.New(store, cfg, eventSvc, logger, domain.MongoDBLogger)
notificationRepo := repository.NewNotificationRepository(store) notificationRepo := repository.NewNotificationRepository(store)
virtuaGamesRepo := repository.NewVirtualGameRepository(store) virtuaGamesRepo := repository.NewVirtualGameRepository(store)
notificationSvc := notificationservice.New(notificationRepo, domain.MongoDBLogger, logger, cfg, messengerSvc, userSvc)
// var userStore user.UserStore // var userStore user.UserStore
// Initialize producer // Initialize producer
brokers := []string{"localhost:9092"}
topic := "wallet-balance-topic" topic := "wallet-balance-topic"
producer := kafka.NewProducer(brokers, topic) producer := kafka.NewProducer(cfg.KafkaBrokers, topic)
notificationSvc := notificationservice.New(notificationRepo, domain.MongoDBLogger, logger, cfg, messengerSvc, userSvc, cfg.KafkaBrokers)
walletSvc := wallet.NewService( walletSvc := wallet.NewService(
wallet.WalletStore(store), wallet.WalletStore(store),

View File

@ -78,9 +78,11 @@ SET value = EXCLUDED.value;
INSERT INTO global_settings (key, value) INSERT INTO global_settings (key, value)
VALUES ('sms_provider', 'afro_message'), VALUES ('sms_provider', 'afro_message'),
('max_number_of_outcomes', '30'), ('max_number_of_outcomes', '30'),
('max_unsettled_bets', '100'),
('bet_amount_limit', '10000000'), ('bet_amount_limit', '10000000'),
('daily_ticket_limit', '50'), ('daily_ticket_limit', '50'),
('total_winnings_limit', '1000000'), ('total_winnings_limit', '100000000000'),
('total_winnings_notify', '100000000'),
('amount_for_bet_referral', '1000000'), ('amount_for_bet_referral', '1000000'),
('cashback_amount_cap', '1000'), ('cashback_amount_cap', '1000'),
('default_winning_limit', '5000000'), ('default_winning_limit', '5000000'),

View File

@ -76,7 +76,7 @@ VALUES (
TRUE, TRUE,
TRUE, TRUE,
TRUE, TRUE,
1, 5,
'regular_wallet', 'regular_wallet',
'ETB', 'ETB',
TRUE, TRUE,
@ -89,7 +89,7 @@ VALUES (
FALSE, FALSE,
TRUE, TRUE,
TRUE, TRUE,
1, 5,
'static_wallet', 'static_wallet',
'ETB', 'ETB',
TRUE, TRUE,
@ -102,7 +102,7 @@ VALUES (
TRUE, TRUE,
TRUE, TRUE,
TRUE, TRUE,
1, 6,
'regular_wallet', 'regular_wallet',
'ETB', 'ETB',
TRUE, TRUE,
@ -115,7 +115,7 @@ VALUES (
FALSE, FALSE,
TRUE, TRUE,
TRUE, TRUE,
1, 6,
'static_wallet', 'static_wallet',
'ETB', 'ETB',
TRUE, TRUE,

View File

@ -72,8 +72,11 @@ CREATE TABLE IF NOT EXISTS wallets (
), ),
is_active BOOLEAN NOT NULL DEFAULT true, is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, type)
); );
CREATE TABLE refresh_tokens ( CREATE TABLE refresh_tokens (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL, user_id BIGINT NOT NULL,
@ -537,6 +540,16 @@ CREATE TABLE IF NOT EXISTS raffle_game_filters (
game_id VARCHAR(150) NOT NULL, game_id VARCHAR(150) NOT NULL,
CONSTRAINT unique_raffle_game UNIQUE (raffle_id, game_id) CONSTRAINT unique_raffle_game UNIQUE (raffle_id, game_id)
); );
CREATE TABLE IF NOT EXISTS accumulator (
outcome_count BIGINT PRIMARY KEY,
default_multiplier REAL NOT NULL
);
CREATE TABLE IF NOT EXISTS company_accumulator (
id SERIAL PRIMARY KEY,
company_id BIGINT NOT NULL,
outcome_count BIGINT NOT NULL,
multiplier REAL NOT NULL
);
------ Views ------ Views
CREATE VIEW companies_details AS CREATE VIEW companies_details AS
SELECT companies.*, SELECT companies.*,

View File

@ -13,6 +13,7 @@ CREATE TABLE IF NOT EXISTS notifications (
'signup_welcome', 'signup_welcome',
'otp_sent', 'otp_sent',
'wallet_threshold', 'wallet_threshold',
'wallet_updated',
'transfer_failed', 'transfer_failed',
'transfer_success', 'transfer_success',
'admin_alert', 'admin_alert',

View File

@ -57,6 +57,47 @@ wHERE (
company_id = sqlc.narg('company_id') company_id = sqlc.narg('company_id')
OR sqlc.narg('company_id') IS NULL OR sqlc.narg('company_id') IS NULL
) )
AND (
status = sqlc.narg('status')
OR sqlc.narg('status') IS NULL
)
AND (
cashed_out = sqlc.narg('cashed_out')
OR sqlc.narg('cashed_out') IS NULL
)
AND (
full_name ILIKE '%' || sqlc.narg('query') || '%'
OR phone_number ILIKE '%' || sqlc.narg('query') || '%'
OR sqlc.narg('query') 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
)
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
-- name: GetTotalBets :one
SELECT COUNT(*)
FROM bets
wHERE (
user_id = sqlc.narg('user_id')
OR sqlc.narg('user_id') IS NULL
)
AND (
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 (
status = sqlc.narg('status')
OR sqlc.narg('status') IS NULL
)
AND ( AND (
cashed_out = sqlc.narg('cashed_out') cashed_out = sqlc.narg('cashed_out')
OR sqlc.narg('cashed_out') IS NULL OR sqlc.narg('cashed_out') IS NULL

View File

@ -40,12 +40,19 @@ SELECT *
FROM notifications FROM notifications
ORDER BY timestamp DESC ORDER BY timestamp DESC
LIMIT $1 OFFSET $2; LIMIT $1 OFFSET $2;
-- name: ListNotifications :many -- name: GetTotalNotificationCount :one
SELECT COUNT(*)
FROM notifications;
-- name: GetUserNotifications :many
SELECT * SELECT *
FROM notifications FROM notifications
WHERE recipient_id = $1 WHERE recipient_id = $1
ORDER BY timestamp DESC ORDER BY timestamp DESC
LIMIT $2 OFFSET $3; LIMIT $2 OFFSET $3;
-- name: GetUserNotificationCount :one
SELECT COUNT(*)
FROM notifications
WHERE recipient_id = $1;
-- name: CountUnreadNotifications :one -- name: CountUnreadNotifications :one
SELECT count(id) SELECT count(id)
FROM notifications FROM notifications
@ -69,10 +76,16 @@ LIMIT $1;
SELECT recipient_id SELECT recipient_id
FROM notifications FROM notifications
WHERE reciever = $1; WHERE reciever = $1;
-- name: GetNotificationCounts :many -- name: GetNotificationCounts :many
SELECT SELECT COUNT(*) as total,
COUNT(*) as total, COUNT(
COUNT(CASE WHEN is_read = true THEN 1 END) as read, CASE
COUNT(CASE WHEN is_read = false THEN 1 END) as unread WHEN is_read = true THEN 1
END
) as read,
COUNT(
CASE
WHEN is_read = false THEN 1
END
) as unread
FROM notifications; FROM notifications;

View File

@ -107,11 +107,14 @@ SELECT id,
suspended_at, suspended_at,
company_id company_id
FROM users FROM users
WHERE (company_id = $1) WHERE (
company_id = sqlc.narg('company_id')
OR sqlc.narg('company_id') IS NULL
)
AND ( AND (
first_name ILIKE '%' || $2 || '%' first_name ILIKE '%' || $1 || '%'
OR last_name ILIKE '%' || $2 || '%' OR last_name ILIKE '%' || $1 || '%'
OR phone_number LIKE '%' || $2 || '%' OR phone_number LIKE '%' || $1 || '%'
) )
AND ( AND (
role = sqlc.narg('role') role = sqlc.narg('role')

View File

@ -69,6 +69,37 @@ services:
timeout: 5s timeout: 5s
retries: 5 retries: 5
zookeeper:
image: confluentinc/cp-zookeeper:7.5.0
container_name: zookeeper
ports:
- "2181:2181"
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
networks:
- app
kafka:
image: confluentinc/cp-kafka:7.5.0
depends_on:
- zookeeper
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,PLAINTEXT_HOST://0.0.0.0:29092
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
ports:
- "9092:9092"
- "29092:29092"
networks:
- app
app: app:
build: build:
context: . context: .
@ -80,6 +111,7 @@ services:
- DB_URL=postgresql://root:secret@postgres:5432/gh?sslmode=disable - DB_URL=postgresql://root:secret@postgres:5432/gh?sslmode=disable
- MONGO_URI=mongodb://root:secret@mongo:27017 - MONGO_URI=mongodb://root:secret@mongo:27017
- REDIS_ADDR=redis:6379 - REDIS_ADDR=redis:6379
- KAFKA_BROKERS=kafka:9092
depends_on: depends_on:
migrate: migrate:
condition: service_completed_successfully condition: service_completed_successfully

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: auth.sql // source: auth.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: bet.sql // source: bet.sql
package dbgen package dbgen
@ -119,32 +119,40 @@ wHERE (
OR $3 IS NULL OR $3 IS NULL
) )
AND ( AND (
cashed_out = $4 status = $4
OR $4 IS NULL OR $4 IS NULL
) )
AND ( AND (
full_name ILIKE '%' || $5 || '%' cashed_out = $5
OR phone_number ILIKE '%' || $5 || '%'
OR $5 IS NULL OR $5 IS NULL
) )
AND ( AND (
created_at > $6 full_name ILIKE '%' || $6 || '%'
OR phone_number ILIKE '%' || $6 || '%'
OR $6 IS NULL OR $6 IS NULL
) )
AND ( AND (
created_at < $7 created_at > $7
OR $7 IS NULL OR $7 IS NULL
) )
AND (
created_at < $8
OR $8 IS NULL
)
LIMIT $10 OFFSET $9
` `
type GetAllBetsParams struct { type GetAllBetsParams struct {
UserID pgtype.Int8 `json:"user_id"` UserID pgtype.Int8 `json:"user_id"`
IsShopBet pgtype.Bool `json:"is_shop_bet"` IsShopBet pgtype.Bool `json:"is_shop_bet"`
CompanyID pgtype.Int8 `json:"company_id"` CompanyID pgtype.Int8 `json:"company_id"`
Status pgtype.Int4 `json:"status"`
CashedOut pgtype.Bool `json:"cashed_out"` CashedOut pgtype.Bool `json:"cashed_out"`
Query pgtype.Text `json:"query"` Query pgtype.Text `json:"query"`
CreatedBefore pgtype.Timestamp `json:"created_before"` CreatedBefore pgtype.Timestamp `json:"created_before"`
CreatedAfter pgtype.Timestamp `json:"created_after"` CreatedAfter pgtype.Timestamp `json:"created_after"`
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
} }
func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWithOutcome, error) { func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWithOutcome, error) {
@ -152,10 +160,13 @@ func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWi
arg.UserID, arg.UserID,
arg.IsShopBet, arg.IsShopBet,
arg.CompanyID, arg.CompanyID,
arg.Status,
arg.CashedOut, arg.CashedOut,
arg.Query, arg.Query,
arg.CreatedBefore, arg.CreatedBefore,
arg.CreatedAfter, arg.CreatedAfter,
arg.Offset,
arg.Limit,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -481,6 +492,71 @@ func (q *Queries) GetBetsForCashback(ctx context.Context) ([]BetWithOutcome, err
return items, nil return items, nil
} }
const GetTotalBets = `-- name: GetTotalBets :one
SELECT COUNT(*)
FROM bets
wHERE (
user_id = $1
OR $1 IS NULL
)
AND (
is_shop_bet = $2
OR $2 IS NULL
)
AND (
company_id = $3
OR $3 IS NULL
)
AND (
status = $4
OR $4 IS NULL
)
AND (
cashed_out = $5
OR $5 IS NULL
)
AND (
full_name ILIKE '%' || $6 || '%'
OR phone_number ILIKE '%' || $6 || '%'
OR $6 IS NULL
)
AND (
created_at > $7
OR $7 IS NULL
)
AND (
created_at < $8
OR $8 IS NULL
)
`
type GetTotalBetsParams struct {
UserID pgtype.Int8 `json:"user_id"`
IsShopBet pgtype.Bool `json:"is_shop_bet"`
CompanyID pgtype.Int8 `json:"company_id"`
Status pgtype.Int4 `json:"status"`
CashedOut pgtype.Bool `json:"cashed_out"`
Query pgtype.Text `json:"query"`
CreatedBefore pgtype.Timestamp `json:"created_before"`
CreatedAfter pgtype.Timestamp `json:"created_after"`
}
func (q *Queries) GetTotalBets(ctx context.Context, arg GetTotalBetsParams) (int64, error) {
row := q.db.QueryRow(ctx, GetTotalBets,
arg.UserID,
arg.IsShopBet,
arg.CompanyID,
arg.Status,
arg.CashedOut,
arg.Query,
arg.CreatedBefore,
arg.CreatedAfter,
)
var count int64
err := row.Scan(&count)
return count, err
}
const UpdateBetOutcomeStatus = `-- name: UpdateBetOutcomeStatus :one const UpdateBetOutcomeStatus = `-- name: UpdateBetOutcomeStatus :one
UPDATE bet_outcomes UPDATE bet_outcomes
SET status = $1 SET status = $1

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: bet_stat.sql // source: bet_stat.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: bonus.sql // source: bonus.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: branch.sql // source: branch.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: cashier.sql // source: cashier.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: company.sql // source: company.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: copyfrom.go // source: copyfrom.go
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: direct_deposit.sql // source: direct_deposit.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: disabled_odds.sql // source: disabled_odds.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: event_history.sql // source: event_history.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: events.sql // source: events.sql
package dbgen package dbgen
@ -513,166 +513,6 @@ func (q *Queries) GetEventsWithSettings(ctx context.Context, arg GetEventsWithSe
return items, nil 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 GetSportAndLeagueIDs = `-- name: GetSportAndLeagueIDs :one const GetSportAndLeagueIDs = `-- name: GetSportAndLeagueIDs :one
SELECT sport_id, league_id FROM events SELECT sport_id, league_id FROM events
WHERE id = $1 WHERE id = $1
@ -683,7 +523,7 @@ type GetSportAndLeagueIDsRow struct {
LeagueID int64 `json:"league_id"` LeagueID int64 `json:"league_id"`
} }
func (q *Queries) GetSportAndLeagueIDs(ctx context.Context, id string) (GetSportAndLeagueIDsRow, error) { func (q *Queries) GetSportAndLeagueIDs(ctx context.Context, id int64) (GetSportAndLeagueIDsRow, error) {
row := q.db.QueryRow(ctx, GetSportAndLeagueIDs, id) row := q.db.QueryRow(ctx, GetSportAndLeagueIDs, id)
var i GetSportAndLeagueIDsRow var i GetSportAndLeagueIDsRow
err := row.Scan(&i.SportID, &i.LeagueID) err := row.Scan(&i.SportID, &i.LeagueID)

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: events_stat.sql // source: events_stat.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: flags.sql // source: flags.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: institutions.sql // source: institutions.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: issue_reporting.sql // source: issue_reporting.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: leagues.sql // source: leagues.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: location.sql // source: location.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
package dbgen package dbgen
@ -8,6 +8,11 @@ import (
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
type Accumulator struct {
OutcomeCount int64 `json:"outcome_count"`
DefaultMultiplier float32 `json:"default_multiplier"`
}
type Bank struct { type Bank struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Slug string `json:"slug"` Slug string `json:"slug"`
@ -159,6 +164,13 @@ type Company struct {
UpdatedAt pgtype.Timestamp `json:"updated_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"`
} }
type CompanyAccumulator struct {
ID int32 `json:"id"`
CompanyID int64 `json:"company_id"`
OutcomeCount int64 `json:"outcome_count"`
Multiplier float32 `json:"multiplier"`
}
type CompanyEventSetting struct { type CompanyEventSetting struct {
ID int64 `json:"id"` ID int64 `json:"id"`
CompanyID int64 `json:"company_id"` CompanyID int64 `json:"company_id"`

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: monitor.sql // source: monitor.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: notification.sql // source: notification.sql
package dbgen package dbgen
@ -188,10 +188,17 @@ func (q *Queries) GetNotification(ctx context.Context, id string) (Notification,
} }
const GetNotificationCounts = `-- name: GetNotificationCounts :many const GetNotificationCounts = `-- name: GetNotificationCounts :many
SELECT SELECT COUNT(*) as total,
COUNT(*) as total, COUNT(
COUNT(CASE WHEN is_read = true THEN 1 END) as read, CASE
COUNT(CASE WHEN is_read = false THEN 1 END) as unread WHEN is_read = true THEN 1
END
) as read,
COUNT(
CASE
WHEN is_read = false THEN 1
END
) as unread
FROM notifications FROM notifications
` `
@ -221,17 +228,47 @@ func (q *Queries) GetNotificationCounts(ctx context.Context) ([]GetNotificationC
return items, nil return items, nil
} }
const ListFailedNotifications = `-- name: ListFailedNotifications :many const GetTotalNotificationCount = `-- name: GetTotalNotificationCount :one
SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata SELECT COUNT(*)
FROM notifications FROM notifications
WHERE delivery_status = 'failed'
AND timestamp < NOW() - INTERVAL '1 hour'
ORDER BY timestamp ASC
LIMIT $1
` `
func (q *Queries) ListFailedNotifications(ctx context.Context, limit int32) ([]Notification, error) { func (q *Queries) GetTotalNotificationCount(ctx context.Context) (int64, error) {
rows, err := q.db.Query(ctx, ListFailedNotifications, limit) row := q.db.QueryRow(ctx, GetTotalNotificationCount)
var count int64
err := row.Scan(&count)
return count, err
}
const GetUserNotificationCount = `-- name: GetUserNotificationCount :one
SELECT COUNT(*)
FROM notifications
WHERE recipient_id = $1
`
func (q *Queries) GetUserNotificationCount(ctx context.Context, recipientID int64) (int64, error) {
row := q.db.QueryRow(ctx, GetUserNotificationCount, recipientID)
var count int64
err := row.Scan(&count)
return count, err
}
const GetUserNotifications = `-- name: GetUserNotifications :many
SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata
FROM notifications
WHERE recipient_id = $1
ORDER BY timestamp DESC
LIMIT $2 OFFSET $3
`
type GetUserNotificationsParams struct {
RecipientID int64 `json:"recipient_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
func (q *Queries) GetUserNotifications(ctx context.Context, arg GetUserNotificationsParams) ([]Notification, error) {
rows, err := q.db.Query(ctx, GetUserNotifications, arg.RecipientID, arg.Limit, arg.Offset)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -265,22 +302,17 @@ func (q *Queries) ListFailedNotifications(ctx context.Context, limit int32) ([]N
return items, nil return items, nil
} }
const ListNotifications = `-- name: ListNotifications :many const ListFailedNotifications = `-- name: ListFailedNotifications :many
SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata
FROM notifications FROM notifications
WHERE recipient_id = $1 WHERE delivery_status = 'failed'
ORDER BY timestamp DESC AND timestamp < NOW() - INTERVAL '1 hour'
LIMIT $2 OFFSET $3 ORDER BY timestamp ASC
LIMIT $1
` `
type ListNotificationsParams struct { func (q *Queries) ListFailedNotifications(ctx context.Context, limit int32) ([]Notification, error) {
RecipientID int64 `json:"recipient_id"` rows, err := q.db.Query(ctx, ListFailedNotifications, limit)
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
func (q *Queries) ListNotifications(ctx context.Context, arg ListNotificationsParams) ([]Notification, error) {
rows, err := q.db.Query(ctx, ListNotifications, arg.RecipientID, arg.Limit, arg.Offset)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: odd_history.sql // source: odd_history.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: odds.sql // source: odds.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: otp.sql // source: otp.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: raffle.sql // source: raffle.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: referal.sql // source: referal.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: report.sql // source: report.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: result.sql // source: result.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: result_log.sql // source: result_log.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: settings.sql // source: settings.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: shop_transactions.sql // source: shop_transactions.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: ticket.sql // source: ticket.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: transfer.sql // source: transfer.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: user.sql // source: user.sql
package dbgen package dbgen
@ -489,11 +489,14 @@ SELECT id,
suspended_at, suspended_at,
company_id company_id
FROM users FROM users
WHERE (company_id = $1) WHERE (
company_id = $2
OR $2 IS NULL
)
AND ( AND (
first_name ILIKE '%' || $2 || '%' first_name ILIKE '%' || $1 || '%'
OR last_name ILIKE '%' || $2 || '%' OR last_name ILIKE '%' || $1 || '%'
OR phone_number LIKE '%' || $2 || '%' OR phone_number LIKE '%' || $1 || '%'
) )
AND ( AND (
role = $3 role = $3
@ -502,8 +505,8 @@ WHERE (company_id = $1)
` `
type SearchUserByNameOrPhoneParams struct { type SearchUserByNameOrPhoneParams struct {
Column1 pgtype.Text `json:"column_1"`
CompanyID pgtype.Int8 `json:"company_id"` CompanyID pgtype.Int8 `json:"company_id"`
Column2 pgtype.Text `json:"column_2"`
Role pgtype.Text `json:"role"` Role pgtype.Text `json:"role"`
} }
@ -524,7 +527,7 @@ type SearchUserByNameOrPhoneRow struct {
} }
func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, arg SearchUserByNameOrPhoneParams) ([]SearchUserByNameOrPhoneRow, error) { func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, arg SearchUserByNameOrPhoneParams) ([]SearchUserByNameOrPhoneRow, error) {
rows, err := q.db.Query(ctx, SearchUserByNameOrPhone, arg.CompanyID, arg.Column2, arg.Role) rows, err := q.db.Query(ctx, SearchUserByNameOrPhone, arg.Column1, arg.CompanyID, arg.Role)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: virtual_games.sql // source: virtual_games.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.29.0
// source: wallet.sql // source: wallet.sql
package dbgen package dbgen

View File

@ -6,6 +6,7 @@ import (
"log/slog" "log/slog"
"os" "os"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
@ -31,6 +32,12 @@ var (
ErrInvalidVeliAPIURL = errors.New("Veli API URL is invalid") ErrInvalidVeliAPIURL = errors.New("Veli API URL is invalid")
ErrInvalidVeliOperatorKey = errors.New("Veli operator key is invalid") ErrInvalidVeliOperatorKey = errors.New("Veli operator key is invalid")
ErrInvalidVeliSecretKey = errors.New("Veli secret key is invalid") ErrInvalidVeliSecretKey = errors.New("Veli secret key is invalid")
ErrInvalidAtlasBaseUrl = errors.New("Atlas Base URL is invalid")
ErrInvalidAtlasOperatorID = errors.New("Atlas operator ID is invalid")
ErrInvalidAtlasSecretKey = errors.New("Atlas secret key is invalid")
ErrInvalidAtlasBrandID = errors.New("Atlas brand ID is invalid")
ErrInvalidAtlasPartnerID = errors.New("Atlas Partner ID is invalid")
ErrMissingResendApiKey = errors.New("missing Resend Api key") ErrMissingResendApiKey = errors.New("missing Resend Api key")
ErrMissingResendSenderEmail = errors.New("missing Resend sender name") ErrMissingResendSenderEmail = errors.New("missing Resend sender name")
ErrMissingTwilioAccountSid = errors.New("missing twilio account sid") ErrMissingTwilioAccountSid = errors.New("missing twilio account sid")
@ -151,6 +158,7 @@ type Config struct {
TwilioAuthToken string TwilioAuthToken string
TwilioSenderPhoneNumber string TwilioSenderPhoneNumber string
RedisAddr string RedisAddr string
KafkaBrokers []string
} }
func NewConfig() (*Config, error) { func NewConfig() (*Config, error) {
@ -176,6 +184,7 @@ func (c *Config) loadEnv() error {
c.ReportExportPath = os.Getenv("REPORT_EXPORT_PATH") c.ReportExportPath = os.Getenv("REPORT_EXPORT_PATH")
c.RedisAddr = os.Getenv("REDIS_ADDR") c.RedisAddr = os.Getenv("REDIS_ADDR")
c.KafkaBrokers = strings.Split(os.Getenv("KAFKA_BROKERS"), ",")
c.EnetPulseConfig.Token = os.Getenv("ENETPULSE_TOKEN") c.EnetPulseConfig.Token = os.Getenv("ENETPULSE_TOKEN")
c.EnetPulseConfig.UserName = os.Getenv("ENETPULSE_USERNAME") c.EnetPulseConfig.UserName = os.Getenv("ENETPULSE_USERNAME")
@ -410,6 +419,36 @@ func (c *Config) loadEnv() error {
CallbackURL: popOKCallbackURL, CallbackURL: popOKCallbackURL,
Platform: popOKPlatform, Platform: popOKPlatform,
} }
AtlasBaseUrl := os.Getenv("ATLAS_BASE_URL")
if AtlasBaseUrl == "" {
return ErrInvalidAtlasBaseUrl
}
AtlasSecretKey := os.Getenv("ATLAS_SECRET_KEY")
if AtlasSecretKey == "" {
return ErrInvalidAtlasSecretKey
}
AtlasBrandID := os.Getenv("ATLAS_BRAND_ID")
if AtlasBrandID == "" {
return ErrInvalidAtlasBrandID
}
AtlasPartnerID := os.Getenv("ATLAS_PARTNER_ID")
if AtlasPartnerID == "" {
return ErrInvalidAtlasPartnerID
}
AtlasOperatorID := os.Getenv("ATLAS_OPERATOR_ID")
if AtlasOperatorID == "" {
return ErrInvalidAtlasOperatorID
}
c.Atlas = AtlasConfig{
BaseURL: AtlasBaseUrl,
SecretKey: AtlasSecretKey,
CasinoID: AtlasBrandID,
PartnerID: AtlasPartnerID,
OperatorID: AtlasOperatorID,
}
betToken := os.Getenv("BET365_TOKEN") betToken := os.Getenv("BET365_TOKEN")
if betToken == "" { if betToken == "" {
return ErrMissingBetToken return ErrMissingBetToken

View File

@ -60,6 +60,7 @@ type Bet struct {
} }
type BetFilter struct { type BetFilter struct {
Status ValidOutcomeStatus
UserID ValidInt64 UserID ValidInt64
CompanyID ValidInt64 CompanyID ValidInt64
CashedOut ValidBool CashedOut ValidBool
@ -67,6 +68,8 @@ type BetFilter struct {
Query ValidString Query ValidString
CreatedBefore ValidTime CreatedBefore ValidTime
CreatedAfter ValidTime CreatedAfter ValidTime
Limit ValidInt32
Offset ValidInt32
} }
type Flag struct { type Flag struct {

View File

@ -14,6 +14,7 @@ type NotificationDeliveryStatus string
type DeliveryChannel string type DeliveryChannel string
const ( const (
NotificationTypeWalletUpdated NotificationType = "wallet_updated"
NotificationTypeDepositResult NotificationType = "deposit_result" NotificationTypeDepositResult NotificationType = "deposit_result"
NotificationTypeDepositVerification NotificationType = "deposit_verification" NotificationTypeDepositVerification NotificationType = "deposit_verification"
NotificationTypeCashOutSuccess NotificationType = "cash_out_success" NotificationTypeCashOutSuccess NotificationType = "cash_out_success"

View File

@ -282,10 +282,6 @@ type BetAnalysis struct {
AverageOdds float64 `json:"average_odds"` AverageOdds float64 `json:"average_odds"`
} }
type ValidOutcomeStatus struct {
Value OutcomeStatus
Valid bool // Valid is true if Value is not NULL
}
// ReportFilter contains filters for report generation // ReportFilter contains filters for report generation
type ReportFilter struct { type ReportFilter struct {

View File

@ -3,7 +3,7 @@ package domain
import ( import (
"time" "time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/jackc/pgx/v5/pgtype"
) )
type MarketConfig struct { type MarketConfig struct {
@ -67,6 +67,20 @@ func (o *OutcomeStatus) String() string {
} }
} }
type ValidOutcomeStatus struct {
Value OutcomeStatus
Valid bool
}
func (v ValidOutcomeStatus) ToPG() pgtype.Int4 {
return pgtype.Int4{
Int32: int32(v.Value),
Valid: v.Valid,
}
}
type TimeStatus int32 type TimeStatus int32
const ( const (
@ -84,79 +98,3 @@ const (
TIME_STATUS_DECIDED_BY_FA TimeStatus = 11 TIME_STATUS_DECIDED_BY_FA TimeStatus = 11
TIME_STATUS_REMOVED TimeStatus = 99 TIME_STATUS_REMOVED TimeStatus = 99
) )
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 {
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),
}
}

View File

@ -0,0 +1,84 @@
package domain
import (
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
)
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 ResultLogFilter struct {
CreatedBefore ValidTime
CreatedAfter ValidTime
}
type ResultStatusBets struct {
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),
}
}

View File

@ -18,9 +18,11 @@ var (
type SettingList struct { type SettingList struct {
SMSProvider SMSProvider `json:"sms_provider"` SMSProvider SMSProvider `json:"sms_provider"`
MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"` MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"`
MaxUnsettledBets int64 `json:"max_unsettled_bets"`
BetAmountLimit Currency `json:"bet_amount_limit"` BetAmountLimit Currency `json:"bet_amount_limit"`
DailyTicketPerIP int64 `json:"daily_ticket_limit"` DailyTicketPerIP int64 `json:"daily_ticket_limit"`
TotalWinningLimit Currency `json:"total_winning_limit"` TotalWinningLimit Currency `json:"total_winning_limit"`
TotalWinningNotify Currency `json:"total_winning_notify"`
AmountForBetReferral Currency `json:"amount_for_bet_referral"` AmountForBetReferral Currency `json:"amount_for_bet_referral"`
CashbackAmountCap Currency `json:"cashback_amount_cap"` CashbackAmountCap Currency `json:"cashback_amount_cap"`
DefaultWinningLimit int64 `json:"default_winning_limit"` DefaultWinningLimit int64 `json:"default_winning_limit"`
@ -41,9 +43,11 @@ type SettingList struct {
type SettingListRes struct { type SettingListRes struct {
SMSProvider SMSProvider `json:"sms_provider"` SMSProvider SMSProvider `json:"sms_provider"`
MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"` MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"`
MaxUnsettledBets int64 `json:"max_unsettled_bets"`
BetAmountLimit float32 `json:"bet_amount_limit"` BetAmountLimit float32 `json:"bet_amount_limit"`
DailyTicketPerIP int64 `json:"daily_ticket_limit"` DailyTicketPerIP int64 `json:"daily_ticket_limit"`
TotalWinningLimit float32 `json:"total_winning_limit"` TotalWinningLimit float32 `json:"total_winning_limit"`
TotalWinningNotify float32 `json:"total_winning_notify"`
AmountForBetReferral float32 `json:"amount_for_bet_referral"` AmountForBetReferral float32 `json:"amount_for_bet_referral"`
CashbackAmountCap float32 `json:"cashback_amount_cap"` CashbackAmountCap float32 `json:"cashback_amount_cap"`
DefaultWinningLimit int64 `json:"default_winning_limit"` DefaultWinningLimit int64 `json:"default_winning_limit"`
@ -65,9 +69,11 @@ func ConvertSettingListRes(settings SettingList) SettingListRes {
return SettingListRes{ return SettingListRes{
SMSProvider: settings.SMSProvider, SMSProvider: settings.SMSProvider,
MaxNumberOfOutcomes: settings.MaxNumberOfOutcomes, MaxNumberOfOutcomes: settings.MaxNumberOfOutcomes,
MaxUnsettledBets: settings.MaxUnsettledBets,
BetAmountLimit: settings.BetAmountLimit.Float32(), BetAmountLimit: settings.BetAmountLimit.Float32(),
DailyTicketPerIP: settings.DailyTicketPerIP, DailyTicketPerIP: settings.DailyTicketPerIP,
TotalWinningLimit: settings.TotalWinningLimit.Float32(), TotalWinningLimit: settings.TotalWinningLimit.Float32(),
TotalWinningNotify: settings.TotalWinningNotify.Float32(),
AmountForBetReferral: settings.AmountForBetReferral.Float32(), AmountForBetReferral: settings.AmountForBetReferral.Float32(),
CashbackAmountCap: settings.CashbackAmountCap.Float32(), CashbackAmountCap: settings.CashbackAmountCap.Float32(),
DefaultWinningLimit: settings.DefaultWinningLimit, DefaultWinningLimit: settings.DefaultWinningLimit,
@ -89,32 +95,36 @@ func ConvertSettingListRes(settings SettingList) SettingListRes {
type SaveSettingListReq struct { type SaveSettingListReq struct {
SMSProvider *string `json:"sms_provider,omitempty"` SMSProvider *string `json:"sms_provider,omitempty"`
MaxNumberOfOutcomes *int64 `json:"max_number_of_outcomes,omitempty"` MaxNumberOfOutcomes *int64 `json:"max_number_of_outcomes,omitempty"`
MaxUnsettledBets *int64 `json:"max_unsettled_bets,omitempty"`
BetAmountLimit *float32 `json:"bet_amount_limit,omitempty"` BetAmountLimit *float32 `json:"bet_amount_limit,omitempty"`
DailyTicketPerIP *int64 `json:"daily_ticket_limit,omitempty"` DailyTicketPerIP *int64 `json:"daily_ticket_limit,omitempty"`
TotalWinningLimit *float32 `json:"total_winning_limit,omitempty"` TotalWinningLimit *float32 `json:"total_winning_limit,omitempty"`
TotalWinningNotify *float32 `json:"total_winning_notify,omitempty"`
AmountForBetReferral *float32 `json:"amount_for_bet_referral,omitempty"` AmountForBetReferral *float32 `json:"amount_for_bet_referral,omitempty"`
CashbackAmountCap *float32 `json:"cashback_amount_cap,omitempty"` CashbackAmountCap *float32 `json:"cashback_amount_cap,omitempty"`
DefaultWinningLimit *int64 `json:"default_winning_limit,omitempty"` DefaultWinningLimit *int64 `json:"default_winning_limit,omitempty"`
ReferralRewardAmount *float32 `json:"referral_reward_amount"` ReferralRewardAmount *float32 `json:"referral_reward_amount,omitempty"`
CashbackPercentage *float32 `json:"cashback_percentage"` CashbackPercentage *float32 `json:"cashback_percentage,omitempty"`
DefaultMaxReferrals *int64 `json:"default_max_referrals"` DefaultMaxReferrals *int64 `json:"default_max_referrals,omitempty"`
MinimumBetAmount *float32 `json:"minimum_bet_amount"` MinimumBetAmount *float32 `json:"minimum_bet_amount,omitempty"`
BetDuplicateLimit *int64 `json:"bet_duplicate_limit"` BetDuplicateLimit *int64 `json:"bet_duplicate_limit,omitempty"`
SendEmailOnBetFinish *bool `json:"send_email_on_bet_finish"` SendEmailOnBetFinish *bool `json:"send_email_on_bet_finish,omitempty"`
SendSMSOnBetFinish *bool `json:"send_sms_on_bet_finish"` SendSMSOnBetFinish *bool `json:"send_sms_on_bet_finish,omitempty"`
WelcomeBonusActive *bool `json:"welcome_bonus_active"` WelcomeBonusActive *bool `json:"welcome_bonus_active,omitempty"`
WelcomeBonusMultiplier *float32 `json:"welcome_bonus_multiplier"` WelcomeBonusMultiplier *float32 `json:"welcome_bonus_multiplier,omitempty"`
WelcomeBonusCap *float32 `json:"welcome_bonus_cap"` WelcomeBonusCap *float32 `json:"welcome_bonus_cap,omitempty"`
WelcomeBonusCount *int64 `json:"welcome_bonus_count"` WelcomeBonusCount *int64 `json:"welcome_bonus_count,omitempty"`
WelcomeBonusExpire *int64 `json:"welcome_bonus_expiry"` WelcomeBonusExpire *int64 `json:"welcome_bonus_expiry,omitempty"`
} }
type ValidSettingList struct { type ValidSettingList struct {
SMSProvider ValidString SMSProvider ValidString
MaxNumberOfOutcomes ValidInt64 MaxNumberOfOutcomes ValidInt64
MaxUnsettledBets ValidInt64
BetAmountLimit ValidCurrency BetAmountLimit ValidCurrency
DailyTicketPerIP ValidInt64 DailyTicketPerIP ValidInt64
TotalWinningLimit ValidCurrency TotalWinningLimit ValidCurrency
TotalWinningNotify ValidCurrency
AmountForBetReferral ValidCurrency AmountForBetReferral ValidCurrency
CashbackAmountCap ValidCurrency CashbackAmountCap ValidCurrency
DefaultWinningLimit ValidInt64 DefaultWinningLimit ValidInt64
@ -136,9 +146,11 @@ func ConvertSaveSettingListReq(settings SaveSettingListReq) ValidSettingList {
return ValidSettingList{ return ValidSettingList{
SMSProvider: ConvertStringPtr(settings.SMSProvider), SMSProvider: ConvertStringPtr(settings.SMSProvider),
MaxNumberOfOutcomes: ConvertInt64Ptr(settings.MaxNumberOfOutcomes), MaxNumberOfOutcomes: ConvertInt64Ptr(settings.MaxNumberOfOutcomes),
MaxUnsettledBets: ConvertInt64Ptr(settings.MaxUnsettledBets),
BetAmountLimit: ConvertFloat32PtrToCurrency(settings.BetAmountLimit), BetAmountLimit: ConvertFloat32PtrToCurrency(settings.BetAmountLimit),
DailyTicketPerIP: ConvertInt64Ptr(settings.DailyTicketPerIP), DailyTicketPerIP: ConvertInt64Ptr(settings.DailyTicketPerIP),
TotalWinningLimit: ConvertFloat32PtrToCurrency(settings.TotalWinningLimit), TotalWinningLimit: ConvertFloat32PtrToCurrency(settings.TotalWinningLimit),
TotalWinningNotify: ConvertFloat32PtrToCurrency(settings.TotalWinningNotify),
AmountForBetReferral: ConvertFloat32PtrToCurrency(settings.AmountForBetReferral), AmountForBetReferral: ConvertFloat32PtrToCurrency(settings.AmountForBetReferral),
CashbackAmountCap: ConvertFloat32PtrToCurrency(settings.CashbackAmountCap), CashbackAmountCap: ConvertFloat32PtrToCurrency(settings.CashbackAmountCap),
DefaultWinningLimit: ConvertInt64Ptr(settings.DefaultWinningLimit), DefaultWinningLimit: ConvertInt64Ptr(settings.DefaultWinningLimit),
@ -162,9 +174,11 @@ func (vsl *ValidSettingList) ToSettingList() SettingList {
return SettingList{ return SettingList{
SMSProvider: SMSProvider(vsl.SMSProvider.Value), SMSProvider: SMSProvider(vsl.SMSProvider.Value),
MaxNumberOfOutcomes: vsl.MaxNumberOfOutcomes.Value, MaxNumberOfOutcomes: vsl.MaxNumberOfOutcomes.Value,
MaxUnsettledBets: vsl.MaxUnsettledBets.Value,
BetAmountLimit: vsl.BetAmountLimit.Value, BetAmountLimit: vsl.BetAmountLimit.Value,
DailyTicketPerIP: vsl.DailyTicketPerIP.Value, DailyTicketPerIP: vsl.DailyTicketPerIP.Value,
TotalWinningLimit: vsl.TotalWinningLimit.Value, TotalWinningLimit: vsl.TotalWinningLimit.Value,
TotalWinningNotify: vsl.TotalWinningNotify.Value,
AmountForBetReferral: vsl.AmountForBetReferral.Value, AmountForBetReferral: vsl.AmountForBetReferral.Value,
CashbackAmountCap: vsl.CashbackAmountCap.Value, CashbackAmountCap: vsl.CashbackAmountCap.Value,
DefaultWinningLimit: vsl.DefaultWinningLimit.Value, DefaultWinningLimit: vsl.DefaultWinningLimit.Value,
@ -194,6 +208,7 @@ func (vsl *ValidSettingList) CustomValidationSettings() error {
func (vsl *ValidSettingList) GetInt64SettingsMap() map[string]*ValidInt64 { func (vsl *ValidSettingList) GetInt64SettingsMap() map[string]*ValidInt64 {
return map[string]*ValidInt64{ return map[string]*ValidInt64{
"max_number_of_outcomes": &vsl.MaxNumberOfOutcomes, "max_number_of_outcomes": &vsl.MaxNumberOfOutcomes,
"max_unsettled_bets": &vsl.MaxUnsettledBets,
"daily_ticket_limit": &vsl.DailyTicketPerIP, "daily_ticket_limit": &vsl.DailyTicketPerIP,
"default_winning_limit": &vsl.DefaultWinningLimit, "default_winning_limit": &vsl.DefaultWinningLimit,
"default_max_referrals": &vsl.DefaultMaxReferrals, "default_max_referrals": &vsl.DefaultMaxReferrals,
@ -207,6 +222,7 @@ func (vsl *ValidSettingList) GetCurrencySettingsMap() map[string]*ValidCurrency
return map[string]*ValidCurrency{ return map[string]*ValidCurrency{
"bet_amount_limit": &vsl.BetAmountLimit, "bet_amount_limit": &vsl.BetAmountLimit,
"total_winnings_limit": &vsl.TotalWinningLimit, "total_winnings_limit": &vsl.TotalWinningLimit,
"total_winnings_notify": &vsl.TotalWinningNotify,
"amount_for_bet_referral": &vsl.AmountForBetReferral, "amount_for_bet_referral": &vsl.AmountForBetReferral,
"cashback_amount_cap": &vsl.CashbackAmountCap, "cashback_amount_cap": &vsl.CashbackAmountCap,
"referral_reward_amount": &vsl.ReferralRewardAmount, "referral_reward_amount": &vsl.ReferralRewardAmount,

View File

@ -14,5 +14,6 @@ type WalletEvent struct {
WalletID int64 `json:"wallet_id"` WalletID int64 `json:"wallet_id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Balance domain.Currency `json:"balance"` Balance domain.Currency `json:"balance"`
WalletType domain.WalletType `json:"wallet_type"`
Trigger string `json:"trigger"` // e.g. "AddToWallet", "DeductFromWallet" Trigger string `json:"trigger"` // e.g. "AddToWallet", "DeductFromWallet"
} }

View File

@ -93,30 +93,44 @@ func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error)
return domain.ConvertDBBetWithOutcomes(bet), nil return domain.ConvertDBBetWithOutcomes(bet), nil
} }
func (s *Store) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]domain.GetBet, error) { func (s *Store) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]domain.GetBet, int64, error) {
bets, err := s.queries.GetAllBets(ctx, dbgen.GetAllBetsParams{ bets, err := s.queries.GetAllBets(ctx, dbgen.GetAllBetsParams{
UserID: filter.UserID.ToPG(), UserID: filter.UserID.ToPG(),
CompanyID: filter.CompanyID.ToPG(), CompanyID: filter.CompanyID.ToPG(),
Status: filter.Status.ToPG(),
CashedOut: filter.CashedOut.ToPG(), CashedOut: filter.CashedOut.ToPG(),
IsShopBet: filter.IsShopBet.ToPG(), IsShopBet: filter.IsShopBet.ToPG(),
Query: filter.Query.ToPG(), Query: filter.Query.ToPG(),
CreatedBefore: filter.CreatedBefore.ToPG(), CreatedBefore: filter.CreatedBefore.ToPG(),
CreatedAfter: filter.CreatedAfter.ToPG(), CreatedAfter: filter.CreatedAfter.ToPG(),
Offset: filter.Offset.ToPG(),
Limit: filter.Limit.ToPG(),
}) })
if err != nil { if err != nil {
domain.MongoDBLogger.Error("failed to get all bets", domain.MongoDBLogger.Error("failed to get all bets",
zap.Any("filter", filter), zap.Any("filter", filter),
zap.Error(err), zap.Error(err),
) )
return nil, err return nil, 0, err
} }
total, err := s.queries.GetTotalBets(ctx, dbgen.GetTotalBetsParams{
UserID: filter.UserID.ToPG(),
CompanyID: filter.CompanyID.ToPG(),
Status: filter.Status.ToPG(),
CashedOut: filter.CashedOut.ToPG(),
IsShopBet: filter.IsShopBet.ToPG(),
Query: filter.Query.ToPG(),
CreatedBefore: filter.CreatedBefore.ToPG(),
CreatedAfter: filter.CreatedAfter.ToPG(),
});
var result []domain.GetBet = make([]domain.GetBet, 0, len(bets)) var result []domain.GetBet = make([]domain.GetBet, 0, len(bets))
for _, bet := range bets { for _, bet := range bets {
result = append(result, domain.ConvertDBBetWithOutcomes(bet)) result = append(result, domain.ConvertDBBetWithOutcomes(bet))
} }
return result, nil return result, total, nil
} }
func (s *Store) GetBetByUserID(ctx context.Context, UserID int64) ([]domain.GetBet, error) { func (s *Store) GetBetByUserID(ctx context.Context, UserID int64) ([]domain.GetBet, error) {

View File

@ -293,7 +293,7 @@ func (s *Store) DeleteEvent(ctx context.Context, eventID int64) error {
return nil return nil
} }
func (s *Store) GetSportAndLeagueIDs(ctx context.Context, eventID string) ([]int64, error) { func (s *Store) GetSportAndLeagueIDs(ctx context.Context, eventID int64) ([]int64, error) {
sportAndLeagueIDs, err := s.queries.GetSportAndLeagueIDs(ctx, eventID) sportAndLeagueIDs, err := s.queries.GetSportAndLeagueIDs(ctx, eventID)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -14,7 +14,7 @@ import (
type NotificationRepository interface { type NotificationRepository interface {
CreateNotification(ctx context.Context, notification *domain.Notification) (*domain.Notification, error) CreateNotification(ctx context.Context, notification *domain.Notification) (*domain.Notification, error)
UpdateNotificationStatus(ctx context.Context, id, status string, isRead bool, metadata []byte) (*domain.Notification, error) UpdateNotificationStatus(ctx context.Context, id, status string, isRead bool, metadata []byte) (*domain.Notification, error)
ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error) GetUserNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, int64, error)
ListFailedNotifications(ctx context.Context, limit int) ([]domain.Notification, error) ListFailedNotifications(ctx context.Context, limit int) ([]domain.Notification, error)
ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error)
CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error)
@ -96,16 +96,22 @@ func (r *Repository) UpdateNotificationStatus(ctx context.Context, id, status st
return r.mapDBToDomain(&dbNotification), nil return r.mapDBToDomain(&dbNotification), nil
} }
func (r *Repository) ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error) { func (r *Repository) GetUserNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, int64, error) {
params := dbgen.ListNotificationsParams{ params := dbgen.GetUserNotificationsParams{
RecipientID: recipientID, RecipientID: recipientID,
Limit: int32(limit), Limit: int32(limit),
Offset: int32(offset), Offset: int32(offset),
} }
dbNotifications, err := r.store.queries.ListNotifications(ctx, params) dbNotifications, err := r.store.queries.GetUserNotifications(ctx, params)
if err != nil { if err != nil {
return nil, err return nil, 0, err
}
total, err := r.store.queries.GetUserNotificationCount(ctx, recipientID)
if err != nil {
return nil, 0, err
} }
var result []domain.Notification = make([]domain.Notification, 0, len(dbNotifications)) var result []domain.Notification = make([]domain.Notification, 0, len(dbNotifications))
@ -114,7 +120,7 @@ func (r *Repository) ListNotifications(ctx context.Context, recipientID int64, l
result = append(result, *domainNotif) result = append(result, *domainNotif)
} }
return result, nil return result, total, nil
} }
func (r *Repository) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) { func (r *Repository) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) {

View File

@ -8,9 +8,6 @@ import (
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
func (s *Store) CreateResultLog(ctx context.Context, result domain.CreateResultLog) (domain.ResultLog, error) { func (s *Store) CreateResultLog(ctx context.Context, result domain.CreateResultLog) (domain.ResultLog, error) {
dbResult, err := s.queries.CreateResultLog(ctx, domain.ConvertCreateResultLog(result)) dbResult, err := s.queries.CreateResultLog(ctx, domain.ConvertCreateResultLog(result))
if err != nil { if err != nil {
@ -19,7 +16,7 @@ func (s *Store) CreateResultLog(ctx context.Context, result domain.CreateResultL
return domain.ConvertDBResultLog(dbResult), nil return domain.ConvertDBResultLog(dbResult), nil
} }
func (s *Store) GetAllResultLog(ctx context.Context, filter domain.ResultFilter) ([]domain.ResultLog, error) { func (s *Store) GetAllResultLog(ctx context.Context, filter domain.ResultLogFilter) ([]domain.ResultLog, error) {
dbResultLogs, err := s.queries.GetAllResultLog(ctx, dbgen.GetAllResultLogParams{ dbResultLogs, err := s.queries.GetAllResultLog(ctx, dbgen.GetAllResultLogParams{
CreatedBefore: pgtype.Timestamp{ CreatedBefore: pgtype.Timestamp{
Time: filter.CreatedBefore.Value, Time: filter.CreatedBefore.Value,

View File

@ -260,7 +260,7 @@ func (s *Store) SearchUserByNameOrPhone(ctx context.Context, searchString string
query := dbgen.SearchUserByNameOrPhoneParams{ query := dbgen.SearchUserByNameOrPhoneParams{
CompanyID: companyID.ToPG(), CompanyID: companyID.ToPG(),
Column2: pgtype.Text{ Column1: pgtype.Text{
String: searchString, String: searchString,
Valid: true, Valid: true,
}, },

View File

@ -245,19 +245,10 @@ func (s *Service) SendAdminErrorNotification(ctx context.Context, betID int64, s
return nil return nil
} }
func (s *Service) SendAdminLargeNotification(ctx context.Context, betID int64, status domain.OutcomeStatus, extra string, companyID int64) error { func (s *Service) SendAdminLargeBetNotification(ctx context.Context, betID int64, totalWinnings float32, extra string, companyID int64) error {
var headline string headline := fmt.Sprintf("SYSTEM WARNING: High Risk Bet", betID, totalWinnings)
var message string message := fmt.Sprintf("Bet #%d has been created with %v payout", betID, totalWinnings)
switch status {
case domain.OUTCOME_STATUS_ERROR, domain.OUTCOME_STATUS_PENDING:
headline = fmt.Sprintf("Processing Error for Bet #%v", betID)
message = "A processing error occurred with this bet. Please review and take corrective action."
default:
return fmt.Errorf("unsupported status: %v", status)
}
super_admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{ super_admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{
Role: string(domain.RoleSuperAdmin), Role: string(domain.RoleSuperAdmin),
@ -294,10 +285,27 @@ func (s *Service) SendAdminLargeNotification(ctx context.Context, betID int64, s
domain.DeliveryChannelInApp, domain.DeliveryChannelInApp,
domain.DeliveryChannelEmail, domain.DeliveryChannelEmail,
} { } {
n := newBetResultNotification(user.ID, domain.NotificationLevelError, channel, headline, message, map[string]any{ raw, _ := json.Marshal(map[string]any{
"status": status, "winnings": totalWinnings,
"more": extra, "more": extra,
}) })
n := &domain.Notification{
RecipientID: user.ID,
DeliveryStatus: domain.DeliveryStatusPending,
IsRead: false,
Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT,
Level: domain.NotificationLevelWarning,
Reciever: domain.NotificationRecieverSideAdmin,
DeliveryChannel: channel,
Payload: domain.NotificationPayload{
Headline: headline,
Message: message,
},
Priority: 2,
Metadata: raw,
}
// n := newBetResultNotification(user.ID, domain.NotificationLevelWarning, channel, headline, message)
if err := s.notificationSvc.SendNotification(ctx, n); err != nil { if err := s.notificationSvc.SendNotification(ctx, n); err != nil {
return err return err
} }

View File

@ -12,7 +12,7 @@ type BetStore interface {
CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBetOutcome) (int64, error) CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBetOutcome) (int64, error)
CreateFlag(ctx context.Context, flag domain.CreateFlagReq) (domain.Flag, error) CreateFlag(ctx context.Context, flag domain.CreateFlagReq) (domain.Flag, error)
GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error)
GetAllBets(ctx context.Context, filter domain.BetFilter) ([]domain.GetBet, error) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]domain.GetBet, int64, error)
GetBetByUserID(ctx context.Context, UserID int64) ([]domain.GetBet, error) GetBetByUserID(ctx context.Context, UserID int64) ([]domain.GetBet, error)
GetBetByFastCode(ctx context.Context, fastcode string) (domain.GetBet, error) GetBetByFastCode(ctx context.Context, fastcode string) (domain.GetBet, error)
GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error) GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error)

View File

@ -42,6 +42,7 @@ var (
ErrOutcomeLimit = errors.New("too many outcomes on a single bet") ErrOutcomeLimit = errors.New("too many outcomes on a single bet")
ErrTotalBalanceNotEnough = errors.New("total Wallet balance is insufficient to create bet") ErrTotalBalanceNotEnough = errors.New("total Wallet balance is insufficient to create bet")
ErrTooManyUnsettled = errors.New("too many unsettled bets")
ErrInvalidAmount = errors.New("invalid amount") ErrInvalidAmount = errors.New("invalid amount")
ErrBetAmountTooHigh = errors.New("cannot create a bet with an amount above limit") ErrBetAmountTooHigh = errors.New("cannot create a bet with an amount above limit")
ErrBetWinningTooHigh = errors.New("total Winnings over set limit") ErrBetWinningTooHigh = errors.New("total Winnings over set limit")
@ -215,6 +216,25 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
if err != nil { if err != nil {
return domain.CreateBetRes{}, err return domain.CreateBetRes{}, err
} }
_, totalUnsettledBets, err := s.GetAllBets(ctx, domain.BetFilter{
Status: domain.ValidOutcomeStatus{
Value: domain.OUTCOME_STATUS_ERROR,
Valid: true,
},
})
if err != nil {
return domain.CreateBetRes{}, err
}
if totalUnsettledBets > settingsList.MaxUnsettledBets {
s.mongoLogger.Error("System block bet creation until unsettled bets fixed",
zap.Int64("total_unsettled_bets", totalUnsettledBets),
zap.Int64("user_id", userID),
)
return domain.CreateBetRes{}, ErrTooManyUnsettled
}
if req.Amount < settingsList.MinimumBetAmount.Float32() { if req.Amount < settingsList.MinimumBetAmount.Float32() {
return domain.CreateBetRes{}, ErrInvalidAmount return domain.CreateBetRes{}, ErrInvalidAmount
} }
@ -283,7 +303,8 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
} }
fastCode := helpers.GenerateFastCode() fastCode := helpers.GenerateFastCode()
amount := req.Amount + (req.Amount * calculateAccumulator(len(outcomes))) accumulator := calculateAccumulator(len(outcomes))
amount := req.Amount + (req.Amount * accumulator)
newBet := domain.CreateBet{ newBet := domain.CreateBet{
Amount: domain.ToCurrency(amount), Amount: domain.ToCurrency(amount),
@ -316,6 +337,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
return domain.CreateBetRes{}, err return domain.CreateBetRes{}, err
} }
// For
case domain.RoleBranchManager, domain.RoleAdmin, domain.RoleSuperAdmin: case domain.RoleBranchManager, domain.RoleAdmin, domain.RoleSuperAdmin:
newBet.IsShopBet = true newBet.IsShopBet = true
// Branch Manager, Admin and Super Admin are required to pass a branch id if they want to create a bet // Branch Manager, Admin and Super Admin are required to pass a branch id if they want to create a bet
@ -367,7 +389,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
newBet.IsShopBet = false newBet.IsShopBet = false
err = s.DeductBetFromCustomerWallet(ctx, req.Amount, userID) err = s.DeductBetFromCustomerWallet(ctx, req.Amount, userID)
if err != nil { if err != nil {
s.mongoLogger.Error("customer wallet deduction failed", s.mongoLogger.Warn("customer wallet deduction failed",
zap.Float32("amount", req.Amount), zap.Float32("amount", req.Amount),
zap.Int64("user_id", userID), zap.Int64("user_id", userID),
zap.Error(err), zap.Error(err),
@ -477,6 +499,14 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
} }
} }
if totalWinnings > settingsList.TotalWinningNotify.Float32() {
err = s.SendAdminLargeBetNotification(ctx, bet.ID, totalWinnings, "", companyID)
if err != nil {
s.mongoLogger.Error("Failed to send large bet notification", zap.Int64("betID", bet.ID),
zap.Int64("companyID", companyID), zap.Float32("totalWinnings", totalWinnings))
}
}
res := domain.ConvertCreateBetRes(bet, rows) res := domain.ConvertCreateBetRes(bet, rows)
return res, nil return res, nil
@ -557,7 +587,7 @@ func (s *Service) DeductBetFromCustomerWallet(ctx context.Context, amount float3
return err return err
} }
// Empty remaining from static balance // Empty remaining from static balance
remainingAmount := wallets.RegularBalance - domain.Currency(amount) remainingAmount := wallets.RegularBalance - domain.ToCurrency(amount)
_, err = s.walletSvc.DeductFromWallet(ctx, wallets.StaticID, _, err = s.walletSvc.DeductFromWallet(ctx, wallets.StaticID,
remainingAmount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, remainingAmount, domain.ValidInt64{}, domain.TRANSFER_DIRECT,
fmt.Sprintf("Deducted %v amount from wallet by system while placing bet", remainingAmount.Float32())) fmt.Sprintf("Deducted %v amount from wallet by system while placing bet", remainingAmount.Float32()))
@ -806,7 +836,7 @@ func (s *Service) CreateBetOutcome(ctx context.Context, outcomes []domain.Create
func (s *Service) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) { func (s *Service) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) {
return s.betStore.GetBetByID(ctx, id) return s.betStore.GetBetByID(ctx, id)
} }
func (s *Service) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]domain.GetBet, error) { func (s *Service) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]domain.GetBet, int64, error) {
return s.betStore.GetAllBets(ctx, filter) return s.betStore.GetAllBets(ctx, filter)
} }

View File

@ -40,7 +40,7 @@ func (s *Service) SendBonusNotification(ctx context.Context, param SendBonusNoti
headline = "You've been awarded a welcome bonus!" headline = "You've been awarded a welcome bonus!"
message = fmt.Sprintf( message = fmt.Sprintf(
"Congratulations! A you've been given %.2f as a welcome bonus for you to bet on.", "Congratulations! A you've been given %.2f as a welcome bonus for you to bet on.",
param.Amount, param.Amount.Float32(),
) )
default: default:
return fmt.Errorf("unsupported bonus type: %v", param.Type) return fmt.Errorf("unsupported bonus type: %v", param.Type)

View File

@ -19,5 +19,5 @@ type Service interface {
GetEventsWithSettings(ctx context.Context, companyID int64, filter domain.EventFilter) ([]domain.EventWithSettings, int64, error) GetEventsWithSettings(ctx context.Context, companyID int64, filter domain.EventFilter) ([]domain.EventWithSettings, int64, error)
GetEventWithSettingByID(ctx context.Context, ID int64, companyID int64) (domain.EventWithSettings, error) GetEventWithSettingByID(ctx context.Context, ID int64, companyID int64) (domain.EventWithSettings, error)
UpdateEventSettings(ctx context.Context, event domain.CreateEventSettings) error UpdateEventSettings(ctx context.Context, event domain.CreateEventSettings) error
GetSportAndLeagueIDs(ctx context.Context, eventID string) ([]int64, error) GetSportAndLeagueIDs(ctx context.Context, eventID int64) ([]int64, error)
} }

View File

@ -225,7 +225,7 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_ur
// Restricting the page to 1 on development, which drastically reduces the amount of events that is fetched // Restricting the page to 1 on development, which drastically reduces the amount of events that is fetched
if s.cfg.Env == "development" { if s.cfg.Env == "development" {
pageLimit = 1 pageLimit = 2
sportIDs = []int{1} sportIDs = []int{1}
} else { } else {
pageLimit = 200 pageLimit = 200
@ -465,7 +465,6 @@ func (s *service) GetAllEvents(ctx context.Context, filter domain.EventFilter) (
return s.store.GetAllEvents(ctx, filter) return s.store.GetAllEvents(ctx, filter)
} }
func (s *service) GetEventByID(ctx context.Context, ID int64) (domain.BaseEvent, error) { func (s *service) GetEventByID(ctx context.Context, ID int64) (domain.BaseEvent, error) {
return s.store.GetEventByID(ctx, ID) return s.store.GetEventByID(ctx, ID)
} }
@ -496,6 +495,6 @@ func (s *service) UpdateEventSettings(ctx context.Context, event domain.CreateEv
return s.store.UpdateEventSettings(ctx, event) return s.store.UpdateEventSettings(ctx, event)
} }
func (s *service) GetSportAndLeagueIDs(ctx context.Context, eventID string) ([]int64, error) { func (s *service) GetSportAndLeagueIDs(ctx context.Context, eventID int64) ([]int64, error) {
return s.store.GetSportAndLeagueIDs(ctx, eventID) return s.store.GetSportAndLeagueIDs(ctx, eventID)
} }

View File

@ -52,6 +52,7 @@ func (c *WalletConsumer) Start(ctx context.Context) {
"wallet_id": evt.WalletID, "wallet_id": evt.WalletID,
"user_id": evt.UserID, "user_id": evt.UserID,
"balance": evt.Balance, "balance": evt.Balance,
"wallet_type": evt.WalletType,
"trigger": evt.Trigger, "trigger": evt.Trigger,
"recipient_id": evt.UserID, "recipient_id": evt.UserID,
} }

View File

@ -10,7 +10,7 @@ import (
type NotificationStore interface { type NotificationStore interface {
SendNotification(ctx context.Context, notification *domain.Notification) error SendNotification(ctx context.Context, notification *domain.Notification) error
MarkAsRead(ctx context.Context, notificationID string, recipientID int64) error MarkAsRead(ctx context.Context, notificationID string, recipientID int64) error
ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error) GetUserNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, int64, error)
ConnectWebSocket(ctx context.Context, recipientID int64, c *websocket.Conn) error ConnectWebSocket(ctx context.Context, recipientID int64, c *websocket.Conn) error
DisconnectWebSocket(recipientID int64) DisconnectWebSocket(recipientID int64)
SendSMS(ctx context.Context, recipientID int64, message string) error SendSMS(ctx context.Context, recipientID int64, message string) error

View File

@ -12,10 +12,12 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers" "github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/messenger" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/messenger"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
"github.com/segmentio/kafka-go"
"go.uber.org/zap" "go.uber.org/zap"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" // "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
@ -38,6 +40,7 @@ type Service struct {
mongoLogger *zap.Logger mongoLogger *zap.Logger
logger *slog.Logger logger *slog.Logger
redisClient *redis.Client redisClient *redis.Client
reader *kafka.Reader
} }
func New(repo repository.NotificationRepository, func New(repo repository.NotificationRepository,
@ -46,11 +49,17 @@ func New(repo repository.NotificationRepository,
cfg *config.Config, cfg *config.Config,
messengerSvc *messenger.Service, messengerSvc *messenger.Service,
userSvc *user.Service, userSvc *user.Service,
kafkaBrokers []string,
) *Service { ) *Service {
hub := ws.NewNotificationHub() hub := ws.NewNotificationHub()
rdb := redis.NewClient(&redis.Options{ rdb := redis.NewClient(&redis.Options{
Addr: cfg.RedisAddr, // e.g., "redis:6379" Addr: cfg.RedisAddr, // e.g., "redis:6379"
}) })
walletReader := kafka.NewReader(kafka.ReaderConfig{
Brokers: kafkaBrokers,
Topic: "wallet-balance-topic",
GroupID: "notification-service-group", // Each service should have its own group
})
svc := &Service{ svc := &Service{
repo: repo, repo: repo,
@ -64,12 +73,14 @@ func New(repo repository.NotificationRepository,
userSvc: userSvc, userSvc: userSvc,
config: cfg, config: cfg,
redisClient: rdb, redisClient: rdb,
reader: walletReader,
} }
go hub.Run() go hub.Run()
go svc.startWorker() go svc.startWorker()
go svc.startRetryWorker() go svc.startRetryWorker()
go svc.RunRedisSubscriber(context.Background()) go svc.RunRedisSubscriber(context.Background())
go svc.StartKafkaConsumer(context.Background())
return svc return svc
} }
@ -167,24 +178,25 @@ func (s *Service) MarkAsRead(ctx context.Context, notificationIDs []string, reci
return nil return nil
} }
func (s *Service) ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error) { func (s *Service) GetUserNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, int64, error) {
notifications, err := s.repo.ListNotifications(ctx, recipientID, limit, offset) notifications, total, err := s.repo.GetUserNotifications(ctx, recipientID, limit, offset)
if err != nil { if err != nil {
s.mongoLogger.Error("[NotificationSvc.ListNotifications] Failed to list notifications", s.mongoLogger.Error("[NotificationSvc.GetUserNotifications] Failed to list notifications",
zap.Int64("recipientID", recipientID), zap.Int64("recipientID", recipientID),
zap.Int("limit", limit), zap.Int("limit", limit),
zap.Int("offset", offset), zap.Int("offset", offset),
zap.Error(err), zap.Error(err),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
return nil, err return nil, 0, err
} }
s.mongoLogger.Info("[NotificationSvc.ListNotifications] Successfully listed notifications", s.mongoLogger.Info("[NotificationSvc.GetUserNotifications] Successfully listed notifications",
zap.Int64("recipientID", recipientID), zap.Int64("recipientID", recipientID),
zap.Int("count", len(notifications)), zap.Int("count", len(notifications)),
zap.Int64("total", total),
zap.Time("timestamp", time.Now()), zap.Time("timestamp", time.Now()),
) )
return notifications, nil return notifications, total, nil
} }
func (s *Service) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) { func (s *Service) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) {
@ -574,6 +586,88 @@ func (s *Service) GetLiveMetrics(ctx context.Context) (domain.LiveMetric, error)
return metric, nil return metric, nil
} }
func (s *Service) StartKafkaConsumer(ctx context.Context) {
go func() {
for {
m, err := s.reader.ReadMessage(ctx)
if err != nil {
if err == context.Canceled {
s.mongoLogger.Info("[NotificationSvc.KafkaConsumer] Stopped by context")
return
}
s.mongoLogger.Error("[NotificationSvc.KafkaConsumer] Error reading message",
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
time.Sleep(1 * time.Second) // backoff
continue
}
var walletEvent event.WalletEvent
if err := json.Unmarshal(m.Value, &walletEvent); err != nil {
s.mongoLogger.Error("[NotificationSvc.KafkaConsumer] Failed to unmarshal wallet event",
zap.String("message", string(m.Value)),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
continue
}
raw, _ := json.Marshal(map[string]any{
"balance": walletEvent.Balance.Float32(),
"type": walletEvent.WalletType,
"timestamp": time.Now(),
})
headline := ""
message := ""
var receiver domain.NotificationRecieverSide
switch walletEvent.WalletType {
case domain.StaticWalletType:
headline = "Referral and Bonus Wallet Updated"
message = fmt.Sprintf("Your referral and bonus wallet balance is now %.2f", walletEvent.Balance.Float32())
receiver = domain.NotificationRecieverSideCustomer
case domain.RegularWalletType:
headline = "Wallet Updated"
message = fmt.Sprintf("Your wallet balance is now %.2f", walletEvent.Balance.Float32())
receiver = domain.NotificationRecieverSideCustomer
case domain.BranchWalletType:
headline = "Branch Wallet Updated"
message = fmt.Sprintf("branch wallet balance is now %.2f", walletEvent.Balance.Float32())
receiver = domain.NotificationRecieverSideBranchManager
case domain.CompanyWalletType:
headline = "Company Wallet Updated"
message = fmt.Sprintf("company wallet balance is now %.2f", walletEvent.Balance.Float32())
receiver = domain.NotificationRecieverSideAdmin
}
// Handle the wallet event: send notification
notification := &domain.Notification{
RecipientID: walletEvent.UserID,
DeliveryChannel: domain.DeliveryChannelInApp,
Reciever: receiver,
Type: domain.NotificationTypeWalletUpdated,
DeliveryStatus: domain.DeliveryStatusPending,
IsRead: false,
Level: domain.NotificationLevelInfo,
Priority: 2,
Metadata: raw,
Payload: domain.NotificationPayload{
Headline: headline,
Message: message,
},
}
if err := s.SendNotification(ctx, notification); err != nil {
s.mongoLogger.Error("[NotificationSvc.KafkaConsumer] Failed to send notification",
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
}
}
}()
}
// func (s *Service) UpdateLiveWalletMetricForWallet(ctx context.Context, wallet domain.Wallet) { // func (s *Service) UpdateLiveWalletMetricForWallet(ctx context.Context, wallet domain.Wallet) {
// var ( // var (
// payload domain.LiveWalletMetrics // payload domain.LiveWalletMetrics

View File

@ -80,10 +80,6 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
func (s *ServiceImpl) ProcessBet365Odds(ctx context.Context) error { func (s *ServiceImpl) ProcessBet365Odds(ctx context.Context) error {
eventIDs, _, err := s.eventSvc.GetAllEvents(ctx, domain.EventFilter{ eventIDs, _, err := s.eventSvc.GetAllEvents(ctx, domain.EventFilter{
LastStartTime: domain.ValidTime{
Value: time.Now(),
Valid: true,
},
Status: domain.ValidEventStatus{ Status: domain.ValidEventStatus{
Value: domain.STATUS_PENDING, Value: domain.STATUS_PENDING,
Valid: true, Valid: true,

View File

@ -13,5 +13,5 @@ type ResultService interface {
type ResultLogStore interface { type ResultLogStore interface {
CreateResultLog(ctx context.Context, result domain.CreateResultLog) (domain.ResultLog, error) CreateResultLog(ctx context.Context, result domain.CreateResultLog) (domain.ResultLog, error)
GetAllResultLog(ctx context.Context, filter domain.ResultFilter) ([]domain.ResultLog, error) GetAllResultLog(ctx context.Context, filter domain.ResultLogFilter) ([]domain.ResultLog, error)
} }

View File

@ -459,7 +459,7 @@ func (s *Service) FetchB365ResultAndUpdateBets(ctx context.Context) error {
func (s *Service) CheckAndSendResultNotifications(ctx context.Context, createdAfter time.Time) error { func (s *Service) CheckAndSendResultNotifications(ctx context.Context, createdAfter time.Time) error {
resultLog, err := s.repo.GetAllResultLog(ctx, domain.ResultFilter{ resultLog, err := s.repo.GetAllResultLog(ctx, domain.ResultLogFilter{
CreatedAfter: domain.ValidTime{ CreatedAfter: domain.ValidTime{
Value: createdAfter, Value: createdAfter,
Valid: true, Valid: true,
@ -557,10 +557,10 @@ func buildHeadlineAndMessageEmail(counts domain.ResultLog, user domain.User) (st
greeting := fmt.Sprintf("Hi %s %s,", user.FirstName, user.LastName) greeting := fmt.Sprintf("Hi %s %s,", user.FirstName, user.LastName)
if totalIssues == 0 { if totalIssues == 0 {
headline := "✅ Daily Results Report — All Events Processed Successfully" headline := "✅ Weekly Results Report — All Events Processed Successfully"
plain := fmt.Sprintf(`%s plain := fmt.Sprintf(`%s
Daily Results Summary: Weekly Results Summary:
- %d Ended Events - %d Ended Events
- %d Total Bets - %d Total Bets
@ -570,7 +570,7 @@ Best regards,
The System`, greeting, counts.StatusEndedCount, totalBets) The System`, greeting, counts.StatusEndedCount, totalBets)
html := fmt.Sprintf(`<p>%s</p> html := fmt.Sprintf(`<p>%s</p>
<h2>Daily Results Summary</h2> <h2>Weekly Results Summary</h2>
<ul> <ul>
<li><strong>%d Ended Events</strong></li> <li><strong>%d Ended Events</strong></li>
<li><strong>%d Total Bets</strong></li> <li><strong>%d Total Bets</strong></li>
@ -616,11 +616,11 @@ The System`, greeting, counts.StatusEndedCount, totalBets)
fmt.Sprintf("<li><strong>%d Successfully Ended Events</strong> (%d Bets)</li>", counts.StatusEndedCount, counts.StatusEndedBets)) fmt.Sprintf("<li><strong>%d Successfully Ended Events</strong> (%d Bets)</li>", counts.StatusEndedCount, counts.StatusEndedBets))
} }
headline := "⚠️ Daily Results Report — Review Required" headline := "⚠️ Weekly Results Report — Review Required"
plain := fmt.Sprintf(`%s plain := fmt.Sprintf(`%s
Daily Results Summary: Weekly Results Summary:
%s %s
Totals: Totals:
@ -639,7 +639,7 @@ The System`,
) )
html := fmt.Sprintf(`<p>%s</p> html := fmt.Sprintf(`<p>%s</p>
<h2>Daily Results Summary</h2> <h2>Weekly Results Summary</h2>
<ul> <ul>
%s %s
</ul> </ul>

View File

@ -61,6 +61,7 @@ func (c *Client) post(ctx context.Context, path string, body map[string]any, res
hash := c.generateHash(tmp, timestamp) hash := c.generateHash(tmp, timestamp)
body["hash"] = hash body["hash"] = hash
fmt.Printf("atlasPost: %v \n", body)
// Marshal final body // Marshal final body
data, _ := json.Marshal(body) data, _ := json.Marshal(body)

View File

@ -0,0 +1,234 @@
package wallet
import (
"context"
"fmt"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"go.uber.org/zap"
"time"
)
func (s *Service) GetAdminNotificationRecipients(ctx context.Context, walletID int64, walletType domain.WalletType) ([]int64, error) {
var recipients []int64
switch walletType {
case domain.BranchWalletType:
branch, err := s.GetBranchByWalletID(ctx, walletID)
if err != nil {
s.mongoLogger.Error("[GetAdminNotificationRecipients] failed to GetBranchWalletByID", zap.Int64("walletID", walletID))
return nil, err
}
// Branch managers will be notified when branch wallet is empty
recipients = append(recipients, branch.BranchManagerID)
// Cashier will be notified
cashiers, err := s.userSvc.GetCashiersByBranch(ctx, branch.ID)
if err != nil {
return nil, err
}
for _, cashier := range cashiers {
recipients = append(recipients, cashier.ID)
}
// Admin will also be notified
admin, err := s.userSvc.GetAdminByCompanyID(ctx, branch.CompanyID)
if err != nil {
return nil, err
}
recipients = append(recipients, admin.ID)
case domain.CompanyWalletType:
company, err := s.GetCompanyByWalletID(ctx, walletID)
if err != nil {
return nil, err
}
recipients = append(recipients, company.AdminID)
default:
return nil, fmt.Errorf("Invalid wallet type")
}
users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{
Role: string(domain.RoleSuperAdmin),
})
if err != nil {
return nil, err
}
for _, user := range users {
recipients = append(recipients, user.ID)
}
return recipients, nil
}
func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWallet domain.Wallet) error {
// Send different messages
// Send notification to admin team
adminNotification := &domain.Notification{
ErrorSeverity: "low",
IsRead: false,
DeliveryStatus: domain.DeliveryStatusPending,
RecipientID: adminWallet.UserID,
Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT,
Level: domain.NotificationLevelWarning,
Reciever: domain.NotificationRecieverSideAdmin,
DeliveryChannel: domain.DeliveryChannelInApp, // Or any preferred admin channel
Payload: domain.NotificationPayload{
Headline: "CREDIT WARNING: System Running Out of Funds",
Message: fmt.Sprintf(
"Wallet ID %d is running low. Current balance: %.2f",
adminWallet.ID,
adminWallet.Balance.Float32(),
),
},
Priority: 1, // High priority for admin alerts
Metadata: fmt.Appendf(nil, `{
"wallet_id": %d,
"balance": %d,
"notification_type": "admin_alert"
}`, adminWallet.ID, adminWallet.Balance),
}
// Get admin recipients and send to all
adminRecipients, err := s.GetAdminNotificationRecipients(ctx, adminWallet.ID, adminWallet.Type)
if err != nil {
s.mongoLogger.Error("failed to get admin recipients",
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
for _, adminID := range adminRecipients {
adminNotification.RecipientID = adminID
if err := s.notificationSvc.SendNotification(ctx, adminNotification); err != nil {
s.mongoLogger.Error("failed to send admin notification",
zap.Int64("admin_id", adminID),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
}
adminNotification.DeliveryChannel = domain.DeliveryChannelEmail
if err := s.notificationSvc.SendNotification(ctx, adminNotification); err != nil {
s.mongoLogger.Error("failed to send email admin notification",
zap.Int64("admin_id", adminID),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
}
return nil
}
func (s *Service) SendAdminWalletInsufficientNotification(ctx context.Context, adminWallet domain.Wallet, amount domain.Currency) error {
// Send notification to admin team
adminNotification := &domain.Notification{
ErrorSeverity: domain.NotificationErrorSeverityLow,
IsRead: false,
DeliveryStatus: domain.DeliveryStatusPending,
RecipientID: adminWallet.UserID,
Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT,
Level: domain.NotificationLevelError,
Reciever: domain.NotificationRecieverSideAdmin,
DeliveryChannel: domain.DeliveryChannelInApp, // Or any preferred admin channel
Payload: domain.NotificationPayload{
Headline: "CREDIT Error: Admin Wallet insufficient to process customer request",
Message: fmt.Sprintf(
"Wallet ID %d. Transaction Amount %.2f. Current balance: %.2f",
adminWallet.ID,
amount.Float32(),
adminWallet.Balance.Float32(),
),
},
Priority: 1, // High priority for admin alerts
Metadata: fmt.Appendf(nil, `{
"wallet_id": %d,
"balance": %d,
"transaction amount": %.2f,
"notification_type": "admin_alert"
}`, adminWallet.ID, adminWallet.Balance, amount.Float32()),
}
// Get admin recipients and send to all
recipients, err := s.GetAdminNotificationRecipients(ctx, adminWallet.ID, adminWallet.Type)
if err != nil {
s.mongoLogger.Error("failed to get admin recipients",
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
for _, adminID := range recipients {
adminNotification.RecipientID = adminID
if err := s.notificationSvc.SendNotification(ctx, adminNotification); err != nil {
s.mongoLogger.Error("failed to send admin notification",
zap.Int64("admin_id", adminID),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
}
adminNotification.DeliveryChannel = domain.DeliveryChannelEmail
if err := s.notificationSvc.SendNotification(ctx, adminNotification); err != nil {
s.mongoLogger.Error("failed to send email admin notification",
zap.Int64("admin_id", adminID),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
}
return nil
}
func (s *Service) SendCustomerWalletInsufficientNotification(ctx context.Context, customerWallet domain.Wallet, amount domain.Currency) error {
// Send notification to admin team
customerNotification := &domain.Notification{
ErrorSeverity: domain.NotificationErrorSeverityLow,
IsRead: false,
DeliveryStatus: domain.DeliveryStatusPending,
RecipientID: customerWallet.UserID,
Type: domain.NOTIFICATION_TYPE_WALLET,
Level: domain.NotificationLevelError,
Reciever: domain.NotificationRecieverSideCustomer,
DeliveryChannel: domain.DeliveryChannelInApp, // Or any preferred admin channel
Payload: domain.NotificationPayload{
Headline: "CREDIT Error: Wallet insufficient",
Message: fmt.Sprintf(
"Wallet ID %d. Transaction Amount %.2f. Current balance: %.2f",
customerWallet.ID,
amount.Float32(),
customerWallet.Balance.Float32(),
),
},
Priority: 1, // High priority for admin alerts
Metadata: fmt.Appendf(nil, `{
"wallet_id": %d,
"balance": %d,
"transaction amount": %.2f,
"notification_type": "admin_alert"
}`, customerWallet.ID, customerWallet.Balance, amount.Float32()),
}
if err := s.notificationSvc.SendNotification(ctx, customerNotification); err != nil {
s.mongoLogger.Error("failed to create customer notification",
zap.Int64("customer_id", customerWallet.UserID),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
return nil
}

View File

@ -4,11 +4,9 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/event"
"go.uber.org/zap"
) )
var ( var (
@ -100,6 +98,7 @@ func (s *Service) UpdateBalance(ctx context.Context, id int64, balance domain.Cu
WalletID: wallet.ID, WalletID: wallet.ID,
UserID: wallet.UserID, UserID: wallet.UserID,
Balance: balance, Balance: balance,
WalletType: wallet.Type,
Trigger: "UpdateBalance", Trigger: "UpdateBalance",
}) })
}() }()
@ -125,6 +124,7 @@ func (s *Service) AddToWallet(
WalletID: wallet.ID, WalletID: wallet.ID,
UserID: wallet.UserID, UserID: wallet.UserID,
Balance: wallet.Balance + amount, Balance: wallet.Balance + amount,
WalletType: wallet.Type,
Trigger: "AddToWallet", Trigger: "AddToWallet",
}) })
}() }()
@ -173,11 +173,8 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.
} }
balance := wallet.Balance.Float32() balance := wallet.Balance.Float32()
for _, threshold := range thresholds { if balance < thresholds[0] {
if balance < threshold {
s.SendAdminWalletLowNotification(ctx, wallet) s.SendAdminWalletLowNotification(ctx, wallet)
break // only send once per check
}
} }
} }
@ -193,6 +190,7 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.
WalletID: wallet.ID, WalletID: wallet.ID,
UserID: wallet.UserID, UserID: wallet.UserID,
Balance: wallet.Balance - amount, Balance: wallet.Balance - amount,
WalletType: wallet.Type,
Trigger: "DeductFromWallet", Trigger: "DeductFromWallet",
}) })
}() }()
@ -215,9 +213,6 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.
return newTransfer, err return newTransfer, err
} }
// Directly Refilling wallet without // Directly Refilling wallet without
// func (s *Service) RefillWallet(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) { // func (s *Service) RefillWallet(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) {
// receiverWallet, err := s.GetWalletByID(ctx, transfer.ReceiverWalletID) // receiverWallet, err := s.GetWalletByID(ctx, transfer.ReceiverWalletID)
@ -257,219 +252,3 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.
func (s *Service) UpdateWalletActive(ctx context.Context, id int64, isActive bool) error { func (s *Service) UpdateWalletActive(ctx context.Context, id int64, isActive bool) error {
return s.walletStore.UpdateWalletActive(ctx, id, isActive) return s.walletStore.UpdateWalletActive(ctx, id, isActive)
} }
func (s *Service) GetAdminNotificationRecipients(ctx context.Context, walletID int64, walletType domain.WalletType) ([]int64, error) {
var recipients []int64
switch walletType {
case domain.BranchWalletType:
branch, err := s.GetBranchByWalletID(ctx, walletID)
if err != nil {
return nil, err
}
recipients = append(recipients, branch.BranchManagerID)
cashiers, err := s.userSvc.GetCashiersByBranch(ctx, branch.ID)
if err != nil {
return nil, err
}
for _, cashier := range cashiers {
recipients = append(recipients, cashier.ID)
}
admin, err := s.userSvc.GetAdminByCompanyID(ctx, branch.CompanyID)
if err != nil {
return nil, err
}
recipients = append(recipients, admin.ID)
case domain.CompanyWalletType:
company, err := s.GetCompanyByWalletID(ctx, walletID)
if err != nil {
return nil, err
}
recipients = append(recipients, company.AdminID)
default:
return nil, fmt.Errorf("Invalid wallet type")
}
users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{
Role: string(domain.RoleSuperAdmin),
})
if err != nil {
return nil, err
}
for _, user := range users {
recipients = append(recipients, user.ID)
}
return recipients, nil
}
func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWallet domain.Wallet) error {
// Send notification to admin team
adminNotification := &domain.Notification{
ErrorSeverity: "low",
IsRead: false,
DeliveryStatus: domain.DeliveryStatusPending,
RecipientID: adminWallet.UserID,
Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT,
Level: domain.NotificationLevelWarning,
Reciever: domain.NotificationRecieverSideAdmin,
DeliveryChannel: domain.DeliveryChannelInApp, // Or any preferred admin channel
Payload: domain.NotificationPayload{
Headline: "CREDIT WARNING: System Running Out of Funds",
Message: fmt.Sprintf(
"Wallet ID %d is running low. Current balance: %.2f",
adminWallet.ID,
adminWallet.Balance.Float32(),
),
},
Priority: 1, // High priority for admin alerts
Metadata: fmt.Appendf(nil, `{
"wallet_id": %d,
"balance": %d,
"notification_type": "admin_alert"
}`, adminWallet.ID, adminWallet.Balance),
}
// Get admin recipients and send to all
adminRecipients, err := s.GetAdminNotificationRecipients(ctx, adminWallet.ID, adminWallet.Type)
if err != nil {
s.mongoLogger.Error("failed to get admin recipients",
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
for _, adminID := range adminRecipients {
adminNotification.RecipientID = adminID
if err := s.notificationSvc.SendNotification(ctx, adminNotification); err != nil {
s.mongoLogger.Error("failed to send admin notification",
zap.Int64("admin_id", adminID),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
}
adminNotification.DeliveryChannel = domain.DeliveryChannelEmail
if err := s.notificationSvc.SendNotification(ctx, adminNotification); err != nil {
s.mongoLogger.Error("failed to send email admin notification",
zap.Int64("admin_id", adminID),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
}
return nil
}
func (s *Service) SendAdminWalletInsufficientNotification(ctx context.Context, adminWallet domain.Wallet, amount domain.Currency) error {
// Send notification to admin team
adminNotification := &domain.Notification{
ErrorSeverity: domain.NotificationErrorSeverityLow,
IsRead: false,
DeliveryStatus: domain.DeliveryStatusPending,
RecipientID: adminWallet.UserID,
Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT,
Level: domain.NotificationLevelError,
Reciever: domain.NotificationRecieverSideAdmin,
DeliveryChannel: domain.DeliveryChannelInApp, // Or any preferred admin channel
Payload: domain.NotificationPayload{
Headline: "CREDIT Error: Admin Wallet insufficient to process customer request",
Message: fmt.Sprintf(
"Wallet ID %d. Transaction Amount %.2f. Current balance: %.2f",
adminWallet.ID,
amount.Float32(),
adminWallet.Balance.Float32(),
),
},
Priority: 1, // High priority for admin alerts
Metadata: fmt.Appendf(nil, `{
"wallet_id": %d,
"balance": %d,
"transaction amount": %.2f,
"notification_type": "admin_alert"
}`, adminWallet.ID, adminWallet.Balance, amount.Float32()),
}
// Get admin recipients and send to all
recipients, err := s.GetAdminNotificationRecipients(ctx, adminWallet.ID, adminWallet.Type)
if err != nil {
s.mongoLogger.Error("failed to get admin recipients",
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
for _, adminID := range recipients {
adminNotification.RecipientID = adminID
if err := s.notificationSvc.SendNotification(ctx, adminNotification); err != nil {
s.mongoLogger.Error("failed to send admin notification",
zap.Int64("admin_id", adminID),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
}
adminNotification.DeliveryChannel = domain.DeliveryChannelEmail
if err := s.notificationSvc.SendNotification(ctx, adminNotification); err != nil {
s.mongoLogger.Error("failed to send email admin notification",
zap.Int64("admin_id", adminID),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
}
return nil
}
func (s *Service) SendCustomerWalletInsufficientNotification(ctx context.Context, customerWallet domain.Wallet, amount domain.Currency) error {
// Send notification to admin team
customerNotification := &domain.Notification{
ErrorSeverity: domain.NotificationErrorSeverityLow,
IsRead: false,
DeliveryStatus: domain.DeliveryStatusPending,
RecipientID: customerWallet.UserID,
Type: domain.NOTIFICATION_TYPE_WALLET,
Level: domain.NotificationLevelError,
Reciever: domain.NotificationRecieverSideCustomer,
DeliveryChannel: domain.DeliveryChannelInApp, // Or any preferred admin channel
Payload: domain.NotificationPayload{
Headline: "CREDIT Error: Wallet insufficient",
Message: fmt.Sprintf(
"Wallet ID %d. Transaction Amount %.2f. Current balance: %.2f",
customerWallet.ID,
amount.Float32(),
customerWallet.Balance.Float32(),
),
},
Priority: 1, // High priority for admin alerts
Metadata: fmt.Appendf(nil, `{
"wallet_id": %d,
"balance": %d,
"transaction amount": %.2f,
"notification_type": "admin_alert"
}`, customerWallet.ID, customerWallet.Balance, amount.Float32()),
}
if err := s.notificationSvc.SendNotification(ctx, customerNotification); err != nil {
s.mongoLogger.Error("failed to create customer notification",
zap.Int64("customer_id", customerWallet.UserID),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
return nil
}

View File

@ -78,23 +78,23 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
// } // }
// }, // },
// }, // },
{ // {
spec: "0 0 0 * * 1", // Every Monday // spec: "0 0 0 * * 1", // Every Monday
task: func() { // task: func() {
mongoLogger.Info("Began Send weekly result notification cron task") // mongoLogger.Info("Began Send weekly result notification cron task")
if err := resultService.CheckAndSendResultNotifications(context.Background(), time.Now().Add(-7*24*time.Hour)); err != nil { // if err := resultService.CheckAndSendResultNotifications(context.Background(), time.Now().Add(-7*24*time.Hour)); err != nil {
mongoLogger.Error("Failed to process result", // mongoLogger.Error("Failed to process result",
zap.Error(err), // zap.Error(err),
) // )
} else { // } else {
mongoLogger.Info("Completed sending weekly result notification without errors") // mongoLogger.Info("Completed sending weekly result notification without errors")
} // }
}, // },
}, // },
} }
for _, job := range schedule { for _, job := range schedule {
job.task() // job.task()
if _, err := c.AddFunc(job.spec, job.task); err != nil { if _, err := c.AddFunc(job.spec, job.task); err != nil {
mongoLogger.Error("Failed to schedule data fetching cron job", mongoLogger.Error("Failed to schedule data fetching cron job",
zap.Error(err), zap.Error(err),

View File

@ -251,7 +251,7 @@ func (h *Handler) CreateBetInternal(c *fiber.Ctx, req domain.CreateBetReq, userI
sportAndLeagueIDs := [][]int64{} sportAndLeagueIDs := [][]int64{}
for _, outcome := range req.Outcomes { for _, outcome := range req.Outcomes {
ids, err := h.eventSvc.GetSportAndLeagueIDs(c.Context(), fmt.Sprintf("%d", outcome.EventID)) ids, err := h.eventSvc.GetSportAndLeagueIDs(c.Context(), outcome.EventID)
if err != nil { if err != nil {
continue continue
} }
@ -459,8 +459,17 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error {
// @Router /api/v1/{tenant_slug}/sport/bet [get] // @Router /api/v1/{tenant_slug}/sport/bet [get]
func (h *Handler) GetAllBet(c *fiber.Ctx) error { func (h *Handler) GetAllBet(c *fiber.Ctx) error {
role := c.Locals("role").(domain.Role) role := c.Locals("role").(domain.Role)
// companyID := c.Locals("company_id").(domain.ValidInt64)
// branchID := c.Locals("branch_id").(domain.ValidInt64) 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,
}
var isShopBet domain.ValidBool var isShopBet domain.ValidBool
isShopBetQuery := c.Query("is_shop") isShopBetQuery := c.Query("is_shop")
@ -525,11 +534,32 @@ func (h *Handler) GetAllBet(c *fiber.Ctx) error {
} }
} }
bets, err := h.betSvc.GetAllBets(c.Context(), domain.BetFilter{ var statusFilter domain.ValidOutcomeStatus
statusQuery := c.Query("status")
if statusQuery != "" {
statusParsed, err := strconv.ParseInt(statusQuery, 10, 32)
if err != nil {
h.mongoLoggerSvc.Info("invalid status format",
zap.String("status", statusQuery),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid status format")
}
statusFilter = domain.ValidOutcomeStatus{
Value: domain.OutcomeStatus(statusParsed),
Valid: true,
}
}
bets, total, err := h.betSvc.GetAllBets(c.Context(), domain.BetFilter{
IsShopBet: isShopBet, IsShopBet: isShopBet,
Query: searchString, Query: searchString,
CreatedBefore: createdBefore, CreatedBefore: createdBefore,
CreatedAfter: createdAfter, CreatedAfter: createdAfter,
Status: statusFilter,
Limit: limit,
Offset: offset,
}) })
if err != nil { if err != nil {
h.mongoLoggerSvc.Error("Failed to get all bets", h.mongoLoggerSvc.Error("Failed to get all bets",
@ -545,7 +575,7 @@ func (h *Handler) GetAllBet(c *fiber.Ctx) error {
res[i] = domain.ConvertBet(bet) res[i] = domain.ConvertBet(bet)
} }
return response.WriteJSON(c, fiber.StatusOK, "All bets retrieved successfully", res, nil) return response.WritePaginatedJSON(c, fiber.StatusOK, "All bets retrieved successfully", res, nil, page, int(total))
} }
// GetAllTenants godoc // GetAllTenants godoc
@ -565,9 +595,17 @@ func (h *Handler) GetAllTenantBets(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "invalid company id") return fiber.NewError(fiber.StatusBadRequest, "invalid company id")
} }
role := c.Locals("role").(domain.Role) role := c.Locals("role").(domain.Role)
// companyID := c.Locals("company_id").(domain.ValidInt64)
// branchID := c.Locals("branch_id").(domain.ValidInt64)
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,
}
var isShopBet domain.ValidBool var isShopBet domain.ValidBool
isShopBetQuery := c.Query("is_shop") isShopBetQuery := c.Query("is_shop")
if isShopBetQuery != "" && role == domain.RoleSuperAdmin { if isShopBetQuery != "" && role == domain.RoleSuperAdmin {
@ -631,12 +669,33 @@ func (h *Handler) GetAllTenantBets(c *fiber.Ctx) error {
} }
} }
bets, err := h.betSvc.GetAllBets(c.Context(), domain.BetFilter{ var statusFilter domain.ValidOutcomeStatus
statusQuery := c.Query("status")
if statusQuery != "" {
statusParsed, err := strconv.ParseInt(statusQuery, 10, 32)
if err != nil {
h.mongoLoggerSvc.Info("invalid status format",
zap.String("status", statusQuery),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid status format")
}
statusFilter = domain.ValidOutcomeStatus{
Value: domain.OutcomeStatus(statusParsed),
Valid: true,
}
}
bets, total, err := h.betSvc.GetAllBets(c.Context(), domain.BetFilter{
CompanyID: companyID, CompanyID: companyID,
IsShopBet: isShopBet, IsShopBet: isShopBet,
Query: searchString, Query: searchString,
CreatedBefore: createdBefore, CreatedBefore: createdBefore,
CreatedAfter: createdAfter, CreatedAfter: createdAfter,
Status: statusFilter,
Limit: limit,
Offset: offset,
}) })
if err != nil { if err != nil {
h.mongoLoggerSvc.Error("Failed to get all bets", h.mongoLoggerSvc.Error("Failed to get all bets",
@ -652,7 +711,7 @@ func (h *Handler) GetAllTenantBets(c *fiber.Ctx) error {
res[i] = domain.ConvertBet(bet) res[i] = domain.ConvertBet(bet)
} }
return response.WriteJSON(c, fiber.StatusOK, "All bets retrieved successfully", res, nil) return response.WritePaginatedJSON(c, fiber.StatusOK, "All bets retrieved successfully", res, nil, page, int(total))
} }
// GetBetByID godoc // GetBetByID godoc

View File

@ -234,22 +234,22 @@ func (h *Handler) GetTenantUpcomingEvents(c *fiber.Ctx) error {
Valid: searchQuery != "", Valid: searchQuery != "",
} }
firstStartTimeQuery := c.Query("first_start_time") // firstStartTimeQuery := c.Query("first_start_time")
var firstStartTime domain.ValidTime // var firstStartTime domain.ValidTime
if firstStartTimeQuery != "" { // if firstStartTimeQuery != "" {
firstStartTimeParsed, err := time.Parse(time.RFC3339, firstStartTimeQuery) // firstStartTimeParsed, err := time.Parse(time.RFC3339, firstStartTimeQuery)
if err != nil { // if err != nil {
h.BadRequestLogger().Info("invalid start_time format", // h.BadRequestLogger().Info("invalid start_time format",
zap.String("first_start_time", firstStartTimeQuery), // zap.String("first_start_time", firstStartTimeQuery),
zap.Error(err), // zap.Error(err),
) // )
return fiber.NewError(fiber.StatusBadRequest, "Invalid start_time format") // return fiber.NewError(fiber.StatusBadRequest, "Invalid start_time format")
} // }
firstStartTime = domain.ValidTime{ // firstStartTime = domain.ValidTime{
Value: firstStartTimeParsed, // Value: firstStartTimeParsed,
Valid: true, // Valid: true,
} // }
} // }
lastStartTimeQuery := c.Query("last_start_time") lastStartTimeQuery := c.Query("last_start_time")
var lastStartTime domain.ValidTime var lastStartTime domain.ValidTime
@ -297,7 +297,10 @@ func (h *Handler) GetTenantUpcomingEvents(c *fiber.Ctx) error {
SportID: sportID, SportID: sportID,
LeagueID: leagueID, LeagueID: leagueID,
Query: searchString, Query: searchString,
FirstStartTime: firstStartTime, FirstStartTime: domain.ValidTime{
Value: time.Now(),
Valid: true,
},
LastStartTime: lastStartTime, LastStartTime: lastStartTime,
Limit: limit, Limit: limit,
Offset: offset, Offset: offset,

View File

@ -316,14 +316,14 @@ func (h *Handler) CreateAndSendNotification(c *fiber.Ctx) error {
} }
} }
func (h *Handler) GetNotifications(c *fiber.Ctx) error { func (h *Handler) GetUserNotification(c *fiber.Ctx) error {
limitStr := c.Query("limit", "10") limitStr := c.Query("limit", "10")
offsetStr := c.Query("offset", "0") offsetStr := c.Query("offset", "0")
// Convert limit and offset to integers // Convert limit and offset to integers
limit, err := strconv.Atoi(limitStr) limit, err := strconv.Atoi(limitStr)
if err != nil || limit <= 0 { if err != nil || limit <= 0 {
h.mongoLoggerSvc.Info("[NotificationSvc.GetNotifications] Invalid limit value", h.mongoLoggerSvc.Info("[NotificationSvc.GetUserNotification] Invalid limit value",
zap.String("limit", limitStr), zap.String("limit", limitStr),
zap.Int("status_code", fiber.StatusBadRequest), zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err), zap.Error(err),
@ -333,7 +333,7 @@ func (h *Handler) GetNotifications(c *fiber.Ctx) error {
} }
offset, err := strconv.Atoi(offsetStr) offset, err := strconv.Atoi(offsetStr)
if err != nil || offset < 0 { if err != nil || offset < 0 {
h.mongoLoggerSvc.Info("[NotificationSvc.GetNotifications] Invalid offset value", h.mongoLoggerSvc.Info("[NotificationSvc.GetUserNotification] Invalid offset value",
zap.String("offset", offsetStr), zap.String("offset", offsetStr),
zap.Int("status_code", fiber.StatusBadRequest), zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err), zap.Error(err),
@ -344,7 +344,7 @@ func (h *Handler) GetNotifications(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(int64) userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 { if !ok || userID == 0 {
h.mongoLoggerSvc.Error("[NotificationSvc.GetNotifications] Invalid user ID in context", h.mongoLoggerSvc.Error("[NotificationSvc.GetUserNotification] Invalid user ID in context",
zap.Int64("userID", userID), zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusInternalServerError), zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err), zap.Error(err),
@ -353,9 +353,9 @@ func (h *Handler) GetNotifications(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification") return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification")
} }
notifications, err := h.notificationSvc.ListNotifications(context.Background(), userID, limit, offset) notifications, total, err := h.notificationSvc.GetUserNotifications(context.Background(), userID, limit, offset)
if err != nil { if err != nil {
h.mongoLoggerSvc.Error("[NotificationSvc.GetNotifications] Failed to fetch notifications", h.mongoLoggerSvc.Error("[NotificationSvc.GetUserNotification] Failed to fetch notifications",
zap.Int64("userID", userID), zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusInternalServerError), zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err), zap.Error(err),
@ -366,7 +366,7 @@ func (h *Handler) GetNotifications(c *fiber.Ctx) error {
return c.Status(fiber.StatusOK).JSON(fiber.Map{ return c.Status(fiber.StatusOK).JSON(fiber.Map{
"notifications": notifications, "notifications": notifications,
"total_count": len(notifications), "total_count": total,
"limit": limit, "limit": limit,
"offset": offset, "offset": offset,
}) })

View File

@ -139,7 +139,7 @@ func (h *Handler) GetOddsByMarketID(c *fiber.Ctx) error {
rawOdds, err := h.prematchSvc.GetOddsByMarketID(c.Context(), marketID, eventID) rawOdds, err := h.prematchSvc.GetOddsByMarketID(c.Context(), marketID, eventID)
if err != nil { if err != nil {
// Lets turn this into a warn because this is constantly going off // Lets turn this into a warn because this is constantly going off
h.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()) return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} }
@ -189,7 +189,7 @@ func (h *Handler) GetTenantOddsByMarketID(c *fiber.Ctx) error {
if err != nil { if err != nil {
// Lets turn this into a warn because this is constantly going off // Lets turn this into a warn because this is constantly going off
h.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()) return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} }

View File

@ -762,8 +762,9 @@ type SearchUserByNameOrPhoneReq struct {
// @Success 200 {object} UserProfileRes // @Success 200 {object} UserProfileRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}/user/search [post] // @Router /api/v1/user/search [post]
func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error { func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error {
// TODO: Add filtering by role based on which user is calling this // TODO: Add filtering by role based on which user is calling this
var req SearchUserByNameOrPhoneReq var req SearchUserByNameOrPhoneReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
@ -783,6 +784,7 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error {
} }
return fiber.NewError(fiber.StatusBadRequest, errMsg) return fiber.NewError(fiber.StatusBadRequest, errMsg)
} }
companyID := c.Locals("company_id").(domain.ValidInt64) companyID := c.Locals("company_id").(domain.ValidInt64)
users, err := h.userSvc.SearchUserByNameOrPhone(c.Context(), req.SearchString, req.Role, companyID) users, err := h.userSvc.SearchUserByNameOrPhone(c.Context(), req.SearchString, req.Role, companyID)
@ -831,6 +833,89 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error {
} }
// SearchUserByNameOrPhone godoc
// @Summary Search for user using name or phone
// @Description Search for user using name or phone
// @Tags user
// @Accept json
// @Produce json
// @Param searchUserByNameOrPhone body SearchUserByNameOrPhoneReq true "Search for using his name or phone"
// @Success 200 {object} UserProfileRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}/user/search [post]
func (h *Handler) SearchCompanyUserByNameOrPhone(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 SearchUserByNameOrPhoneReq
if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Error("Failed to Search UserBy Name Or Phone failed",
zap.Any("request", req),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error())
}
valErrs, ok := h.validator.Validate(c, req)
if !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
return fiber.NewError(fiber.StatusBadRequest, errMsg)
}
users, err := h.userSvc.SearchUserByNameOrPhone(c.Context(), req.SearchString, req.Role, companyID)
if err != nil {
h.mongoLoggerSvc.Error("Failed to get user by name or phone",
zap.Any("request", req),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "failed to get users"+err.Error())
}
var res []UserProfileRes = make([]UserProfileRes, 0, len(users))
for _, user := range users {
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
if err != nil {
if err != authentication.ErrRefreshTokenNotFound {
h.mongoLoggerSvc.Error("Failed to get user last login",
zap.Any("userID", user.ID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login"+err.Error())
}
lastLogin = &user.CreatedAt
}
res = append(res, UserProfileRes{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Role: user.Role,
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
SuspendedAt: user.SuspendedAt,
Suspended: user.Suspended,
LastLogin: *lastLogin,
})
}
return response.WriteJSON(c, fiber.StatusOK, "Search Successful", res, nil)
}
// GetUserByID godoc // GetUserByID godoc
// @Summary Get user by id // @Summary Get user by id
// @Description Get a single user by id // @Description Get a single user by id

View File

@ -186,8 +186,8 @@ func (a *App) initAppRoutes() {
groupV1.Get("/user/single/:id", a.authMiddleware, h.GetUserByID) groupV1.Get("/user/single/:id", a.authMiddleware, h.GetUserByID)
groupV1.Post("/user/suspend", a.authMiddleware, h.UpdateUserSuspend) groupV1.Post("/user/suspend", a.authMiddleware, h.UpdateUserSuspend)
groupV1.Delete("/user/delete/:id", a.authMiddleware, h.DeleteUser) groupV1.Delete("/user/delete/:id", a.authMiddleware, h.DeleteUser)
groupV1.Post("/user/search", a.authMiddleware, h.SearchUserByNameOrPhone)
tenant.Get("/user/wallet", a.authMiddleware, h.GetCustomerWallet) tenant.Get("/user/wallet", a.authMiddleware, h.GetCustomerWallet)
tenant.Post("/user/search", a.authMiddleware, h.SearchUserByNameOrPhone)
// Referral Routes // Referral Routes
tenant.Post("/referral/create", a.authMiddleware, h.CreateReferralCode) tenant.Post("/referral/create", a.authMiddleware, h.CreateReferralCode)
@ -264,7 +264,7 @@ func (a *App) initAppRoutes() {
groupV1.Delete("/events/:id", a.authMiddleware, a.SuperAdminOnly, h.SetEventStatusToRemoved) groupV1.Delete("/events/:id", a.authMiddleware, a.SuperAdminOnly, h.SetEventStatusToRemoved)
groupV1.Patch("/events/:id/is_monitored", a.authMiddleware, a.SuperAdminOnly, h.SetEventIsMonitored) groupV1.Patch("/events/:id/is_monitored", a.authMiddleware, a.SuperAdminOnly, h.SetEventIsMonitored)
tenant.Get("/events", h.GetTenantUpcomingEvents) tenant.Get("/upcoming-events", h.GetTenantUpcomingEvents)
tenant.Get("/events/:id", h.GetTenantEventByID) tenant.Get("/events/:id", h.GetTenantEventByID)
tenant.Get("/top-leagues", h.GetTopLeagues) tenant.Get("/top-leagues", h.GetTopLeagues)
tenant.Put("/events/:id/settings", h.UpdateEventSettings) tenant.Put("/events/:id/settings", h.UpdateEventSettings)
@ -414,7 +414,7 @@ func (a *App) initAppRoutes() {
// Notification Routes // Notification Routes
groupV1.Get("/ws/connect", a.WebsocketAuthMiddleware, h.ConnectSocket) groupV1.Get("/ws/connect", a.WebsocketAuthMiddleware, h.ConnectSocket)
groupV1.Get("/notifications", a.authMiddleware, h.GetNotifications) groupV1.Get("/notifications", a.authMiddleware, h.GetUserNotification)
groupV1.Get("/notifications/all", a.authMiddleware, h.GetAllNotifications) groupV1.Get("/notifications/all", a.authMiddleware, h.GetAllNotifications)
groupV1.Post("/notifications/mark-as-read", a.authMiddleware, h.MarkNotificationAsRead) groupV1.Post("/notifications/mark-as-read", a.authMiddleware, h.MarkNotificationAsRead)
groupV1.Get("/notifications/unread", a.authMiddleware, h.CountUnreadNotifications) groupV1.Get("/notifications/unread", a.authMiddleware, h.CountUnreadNotifications)

View File

@ -165,6 +165,7 @@ func (h *NotificationHub) BroadcastWalletUpdate(userID int64, event event.Wallet
payload := map[string]interface{}{ payload := map[string]interface{}{
"type": event.EventType, "type": event.EventType,
"wallet_id": event.WalletID, "wallet_id": event.WalletID,
"wallet_type": event.WalletType,
"user_id": event.UserID, "user_id": event.UserID,
"balance": event.Balance, "balance": event.Balance,
"trigger": event.Trigger, "trigger": event.Trigger,

View File

@ -79,7 +79,7 @@ logs:
@mkdir -p logs @mkdir -p logs
db-up: | logs db-up: | logs
@mkdir -p logs @mkdir -p logs
@docker compose up -d postgres migrate mongo redis @docker compose up -d postgres migrate mongo redis kafka
@docker logs fortunebet-backend-postgres-1 > logs/postgres.log 2>&1 & @docker logs fortunebet-backend-postgres-1 > logs/postgres.log 2>&1 &
.PHONY: db-down .PHONY: db-down
db-down: db-down: