From 9ec7d0cfc1774888502cfbf1bbd40c79b6842a25 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Sat, 21 Jun 2025 17:44:34 +0300 Subject: [PATCH] feat: finished the setting service --- cmd/main.go | 5 +- db/migrations/000007_setting_data.up.sql | 12 +++ db/query/settings.sql | 6 +- gen/db/settings.sql.go | 20 ++++- internal/domain/settings.go | 7 ++ internal/repository/settings.go | 76 ++++++++++++++++++- internal/services/settings/port.go | 4 +- internal/services/settings/service.go | 7 ++ internal/services/ticket/service.go | 26 ++++--- internal/web_server/app.go | 11 ++- internal/web_server/handlers/handlers.go | 4 + .../web_server/handlers/ticket_handler.go | 5 +- internal/web_server/routes.go | 1 + 13 files changed, 165 insertions(+), 19 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 1035672..1fb127c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -43,6 +43,7 @@ import ( referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/report" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" @@ -98,6 +99,7 @@ func main() { v := customvalidator.NewCustomValidator(validator.New()) // Initialize services + settingSvc := settings.NewService(store) authSvc := authentication.NewService(store, store, cfg.RefreshExpiry) userSvc := user.NewService(store, store, cfg) eventSvc := event.New(cfg.Bet365Token, store) @@ -119,7 +121,7 @@ func main() { branchSvc := branch.NewService(store) companySvc := company.NewService(store) leagueSvc := league.New(store) - ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger) + ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger, *settingSvc) betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, logger, domain.MongoDBLogger) resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc) referalRepo := repository.NewReferralRepository(store) @@ -202,6 +204,7 @@ func main() { currSvc, cfg.Port, v, + settingSvc, authSvc, logger, jwtutil.JwtConfig{ diff --git a/db/migrations/000007_setting_data.up.sql b/db/migrations/000007_setting_data.up.sql index d01ab65..f4cdfd6 100644 --- a/db/migrations/000007_setting_data.up.sql +++ b/db/migrations/000007_setting_data.up.sql @@ -1,5 +1,17 @@ -- Settings Initial Data INSERT INTO settings (key, value) +VALUES ('max_number_of_outcomes', '30') ON CONFLICT (key) DO +UPDATE +SET value = EXCLUDED.value; +INSERT INTO settings (key, value) +VALUES ('bet_amount_limit', '100000') ON CONFLICT (key) DO +UPDATE +SET value = EXCLUDED.value; +INSERT INTO settings (key, value) +VALUES ('daily_ticket_limit', '50') ON CONFLICT (key) DO +UPDATE +SET value = EXCLUDED.value; +INSERT INTO settings (key, value) VALUES ('total_winnings_limit', '1000000') ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; \ No newline at end of file diff --git a/db/query/settings.sql b/db/query/settings.sql index f8a1e31..d0f4482 100644 --- a/db/query/settings.sql +++ b/db/query/settings.sql @@ -1,6 +1,10 @@ -- name: GetSettings :many SELECT * -from settings; +FROM settings; +-- name: GetSetting :one +SELECT * +FROM settings +WHERE key = $1; -- name: SaveSetting :one INSERT INTO settings (key, value, updated_at) VALUES ($1, $2, CURRENT_TIMESTAMP) ON CONFLICT (key) DO diff --git a/gen/db/settings.sql.go b/gen/db/settings.sql.go index a7c9187..d659755 100644 --- a/gen/db/settings.sql.go +++ b/gen/db/settings.sql.go @@ -9,9 +9,27 @@ import ( "context" ) +const GetSetting = `-- name: GetSetting :one +SELECT key, value, created_at, updated_at +FROM settings +WHERE key = $1 +` + +func (q *Queries) GetSetting(ctx context.Context, key string) (Setting, error) { + row := q.db.QueryRow(ctx, GetSetting, key) + var i Setting + err := row.Scan( + &i.Key, + &i.Value, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + const GetSettings = `-- name: GetSettings :many SELECT key, value, created_at, updated_at -from settings +FROM settings ` func (q *Queries) GetSettings(ctx context.Context) ([]Setting, error) { diff --git a/internal/domain/settings.go b/internal/domain/settings.go index 083f915..c0c8368 100644 --- a/internal/domain/settings.go +++ b/internal/domain/settings.go @@ -13,3 +13,10 @@ type SettingRes struct { Value string `json:"value"` UpdatedAt string `json:"updated_at"` } + +type SettingList struct { + MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"` + BetAmountLimit Currency `json:"bet_amount_limit"` + DailyTicketPerIP int64 `json:"daily_ticket_limit"` + TotalWinningLimit Currency `json:"total_winning_limit"` +} diff --git a/internal/repository/settings.go b/internal/repository/settings.go index 3bf0c8e..93635ab 100644 --- a/internal/repository/settings.go +++ b/internal/repository/settings.go @@ -2,12 +2,68 @@ package repository import ( "context" + "strconv" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "go.uber.org/zap" ) +type DBSettingList struct { + MaxNumberOfOutcomes domain.ValidInt64 + BetAmountLimit domain.ValidInt64 + DailyTicketPerIP domain.ValidInt64 + TotalWinningLimit domain.ValidInt64 +} + +func GetDBSettingList(settings []dbgen.Setting) (domain.SettingList, error) { + var dbSettingList DBSettingList + var int64SettingsMap = map[string]*domain.ValidInt64{ + "max_number_of_outcomes": &dbSettingList.MaxNumberOfOutcomes, + "bet_amount_limit": &dbSettingList.BetAmountLimit, + "daily_ticket_limit": &dbSettingList.DailyTicketPerIP, + "total_winnings_limit": &dbSettingList.DailyTicketPerIP, + } + + for _, setting := range settings { + for key, dbSetting := range int64SettingsMap { + if setting.Key == key { + value, err := strconv.ParseInt(setting.Value, 10, 64) + if err != nil { + return domain.SettingList{}, err + } + *dbSetting = domain.ValidInt64{ + Value: value, + Valid: true, + } + } else { + domain.MongoDBLogger.Error("unknown setting found on database", zap.String("setting", setting.Key)) + } + } + } + + for key, dbSetting := range int64SettingsMap { + if !dbSetting.Valid { + domain.MongoDBLogger.Warn("setting value not found on database", zap.String("setting", key)) + } + } + + return domain.SettingList{ + MaxNumberOfOutcomes: dbSettingList.MaxNumberOfOutcomes.Value, + BetAmountLimit: domain.Currency(dbSettingList.BetAmountLimit.Value), + DailyTicketPerIP: dbSettingList.DailyTicketPerIP.Value, + TotalWinningLimit: domain.Currency(dbSettingList.TotalWinningLimit.Value), + }, nil +} +func (s *Store) GetSettingList(ctx context.Context) (domain.SettingList, error) { + settings, err := s.queries.GetSettings(ctx) + if err != nil { + domain.MongoDBLogger.Error("failed to get all settings", zap.Error(err)) + } + + return GetDBSettingList(settings) +} + func (s *Store) GetSettings(ctx context.Context) ([]domain.Setting, error) { settings, err := s.queries.GetSettings(ctx) @@ -27,9 +83,25 @@ func (s *Store) GetSettings(ctx context.Context) ([]domain.Setting, error) { return result, nil } +func (s *Store) GetSetting(ctx context.Context, key string) (domain.Setting, error) { + dbSetting, err := s.queries.GetSetting(ctx, key) + + if err != nil { + domain.MongoDBLogger.Error("failed to get all settings", zap.Error(err)) + } + + result := domain.Setting{ + Key: dbSetting.Key, + Value: dbSetting.Value, + UpdatedAt: dbSetting.UpdatedAt.Time, + } + + return result, nil +} + func (s *Store) SaveSetting(ctx context.Context, key, value string) (domain.Setting, error) { dbSetting, err := s.queries.SaveSetting(ctx, dbgen.SaveSettingParams{ - Key: key, + Key: key, Value: value, }) @@ -40,7 +112,7 @@ func (s *Store) SaveSetting(ctx context.Context, key, value string) (domain.Sett } setting := domain.Setting{ - Key: dbSetting.Key, + Key: dbSetting.Key, Value: dbSetting.Value, } diff --git a/internal/services/settings/port.go b/internal/services/settings/port.go index 587805a..ce86f06 100644 --- a/internal/services/settings/port.go +++ b/internal/services/settings/port.go @@ -7,6 +7,8 @@ import ( ) type SettingStore interface { + GetSettingList(ctx context.Context) (domain.SettingList, error) GetSettings(ctx context.Context) ([]domain.Setting, error) + GetSetting(ctx context.Context, key string) (domain.Setting, error) SaveSetting(ctx context.Context, key, value string) (domain.Setting, error) -} +} diff --git a/internal/services/settings/service.go b/internal/services/settings/service.go index 95f0083..66591a1 100644 --- a/internal/services/settings/service.go +++ b/internal/services/settings/service.go @@ -16,10 +16,17 @@ func NewService(settingStore SettingStore) *Service { } } +func (s *Service) GetSettingList(ctx context.Context) (domain.SettingList, error) { + return s.settingStore.GetSettingList(ctx) +} + func (s *Service) GetSettings(ctx context.Context) ([]domain.Setting, error) { return s.settingStore.GetSettings(ctx) } +func (s *Service) GetSetting(ctx context.Context, key string) (domain.Setting, error) { + return s.settingStore.GetSetting(ctx, key) +} func (s *Service) SaveSetting(ctx context.Context, key, value string) (domain.Setting, error) { return s.settingStore.SaveSetting(ctx, key, value) } diff --git a/internal/services/ticket/service.go b/internal/services/ticket/service.go index 2f36e88..f3b378b 100644 --- a/internal/services/ticket/service.go +++ b/internal/services/ticket/service.go @@ -10,13 +10,14 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings" "go.uber.org/zap" ) var ( // ErrGenerateRandomOutcome = errors.New("Failed to generate any random outcome for events") // ErrOutcomesNotCompleted = errors.New("Some bet outcomes are still pending") - ErrEventHasNotEnded = errors.New("Event has not ended yet") + ErrTicketHasExpired = errors.New("Ticket has expired") ErrNoEventsAvailable = errors.New("Not enough events available with the given filters") ErrEventHasBeenRemoved = errors.New("Event has been removed") ErrTooManyOutcomesForTicket = errors.New("Too many odds/outcomes for a single ticket") @@ -32,6 +33,7 @@ type Service struct { eventSvc event.Service prematchSvc odds.ServiceImpl mongoLogger *zap.Logger + settingSvc settings.Service } func NewService( @@ -39,16 +41,18 @@ func NewService( eventSvc event.Service, prematchSvc odds.ServiceImpl, mongoLogger *zap.Logger, + settingSvc settings.Service, ) *Service { return &Service{ ticketStore: ticketStore, eventSvc: eventSvc, prematchSvc: prematchSvc, mongoLogger: mongoLogger, + settingSvc: settingSvc, } } -func (s *Service) GenerateTicketOutcome(ctx context.Context, eventID int64, marketID int64, oddID int64) (domain.CreateTicketOutcome, error) { +func (s *Service) GenerateTicketOutcome(ctx context.Context, settings domain.SettingList, eventID int64, marketID int64, oddID int64) (domain.CreateTicketOutcome, error) { eventIDStr := strconv.FormatInt(eventID, 10) marketIDStr := strconv.FormatInt(marketID, 10) oddIDStr := strconv.FormatInt(oddID, 10) @@ -69,7 +73,7 @@ func (s *Service) GenerateTicketOutcome(ctx context.Context, eventID int64, mark zap.Time("event_start_time", event.StartTime), zap.Time("current_time", currentTime), ) - return domain.CreateTicketOutcome{}, ErrEventHasNotEnded + return domain.CreateTicketOutcome{}, ErrTicketHasExpired } odds, err := s.prematchSvc.GetRawOddsByMarketID(ctx, marketIDStr, eventIDStr) @@ -151,17 +155,18 @@ func (s *Service) GenerateTicketOutcome(ctx context.Context, eventID int64, mark } func (s *Service) CreateTicket(ctx context.Context, req domain.CreateTicketReq, clientIP string) (domain.Ticket, int64, error) { + settingsList, err := s.settingSvc.GetSettingList(ctx) // s.mongoLogger.Info("Creating ticket") // TODO Validate Outcomes Here and make sure they didn't expire // Validation for creating tickets - if len(req.Outcomes) > 30 { + if len(req.Outcomes) > int(settingsList.MaxNumberOfOutcomes) { // return response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, nil) return domain.Ticket{}, 0, ErrTooManyOutcomesForTicket } - if req.Amount > 100000 { + if req.Amount > settingsList.BetAmountLimit.Float32() { // return response.WriteJSON(c, fiber.StatusBadRequest, "Cannot create a ticket with an amount above 100,000 birr", nil, nil) return domain.Ticket{}, 0, ErrTicketAmountTooHigh } @@ -170,17 +175,20 @@ func (s *Service) CreateTicket(ctx context.Context, req domain.CreateTicketReq, if err != nil { // return response.WriteJSON(c, fiber.StatusInternalServerError, "Error fetching user info", nil, nil) + s.mongoLogger.Error("failed to count number of ticket using ip", + zap.Error(err), + ) return domain.Ticket{}, 0, err } - if count > 50 { + if count > settingsList.DailyTicketPerIP { // return response.WriteJSON(c, fiber.StatusBadRequest, "Ticket Limit reached", nil, nil) return domain.Ticket{}, 0, ErrTicketLimitForSingleUser - } + } var outcomes []domain.CreateTicketOutcome = make([]domain.CreateTicketOutcome, 0, len(req.Outcomes)) var totalOdds float32 = 1 for _, outcomeReq := range req.Outcomes { - newOutcome, err := s.GenerateTicketOutcome(ctx, outcomeReq.EventID, outcomeReq.MarketID, outcomeReq.OddID) + newOutcome, err := s.GenerateTicketOutcome(ctx, settingsList, outcomeReq.EventID, outcomeReq.MarketID, outcomeReq.OddID) if err != nil { s.mongoLogger.Error("failed to generate outcome", zap.Int64("event_id", outcomeReq.EventID), @@ -194,7 +202,7 @@ func (s *Service) CreateTicket(ctx context.Context, req domain.CreateTicketReq, outcomes = append(outcomes, newOutcome) } totalWinnings := req.Amount * totalOdds - if totalWinnings > 1000000 { + if totalWinnings > settingsList.TotalWinningLimit.Float32() { s.mongoLogger.Error("Total Winnings over limit", zap.Float32("Total Odds", totalOdds), zap.Float32("amount", req.Amount)) // return response.WriteJSON(c, fiber.StatusBadRequest, "Cannot create a ticket with 1,000,000 winnings", nil, nil) return domain.Ticket{}, 0, ErrTicketWinningTooHigh diff --git a/internal/web_server/app.go b/internal/web_server/app.go index 246bbd5..ab1e028 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -18,6 +18,7 @@ import ( referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/report" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" @@ -45,6 +46,7 @@ type App struct { NotidicationStore *notificationservice.Service referralSvc referralservice.ReferralStore port int + settingSvc *settings.Service authSvc *authentication.Service userSvc *user.Service betSvc *bet.Service @@ -68,6 +70,7 @@ type App struct { func NewApp( currSvc *currency.Service, port int, validator *customvalidator.CustomValidator, + settingSvc *settings.Service, authSvc *authentication.Service, logger *slog.Logger, JwtConfig jwtutil.JwtConfig, @@ -107,9 +110,11 @@ func NewApp( })) s := &App{ - currSvc: currSvc, - fiber: app, - port: port, + currSvc: currSvc, + fiber: app, + port: port, + + settingSvc: settingSvc, authSvc: authSvc, validator: validator, logger: logger, diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index a79c8a4..4743cb9 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -18,6 +18,7 @@ import ( referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/report" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" @@ -31,6 +32,7 @@ import ( type Handler struct { currSvc *currency.Service logger *slog.Logger + settingSvc *settings.Service notificationSvc *notificationservice.Service userSvc *user.Service referralSvc referralservice.ReferralStore @@ -59,6 +61,7 @@ type Handler struct { func New( currSvc *currency.Service, logger *slog.Logger, + settingSvc *settings.Service, notificationSvc *notificationservice.Service, validator *customvalidator.CustomValidator, reportSvc report.ReportStore, @@ -86,6 +89,7 @@ func New( return &Handler{ currSvc: currSvc, logger: logger, + settingSvc: settingSvc, notificationSvc: notificationSvc, reportSvc: reportSvc, chapaSvc: chapaSvc, diff --git a/internal/web_server/handlers/ticket_handler.go b/internal/web_server/handlers/ticket_handler.go index e665162..17e6276 100644 --- a/internal/web_server/handlers/ticket_handler.go +++ b/internal/web_server/handlers/ticket_handler.go @@ -35,7 +35,10 @@ func (h *Handler) CreateTicket(c *fiber.Ctx) error { if err != nil { switch err { - case ticket.ErrEventHasBeenRemoved, ticket.ErrEventHasNotEnded, ticket.ErrRawOddInvalid: + case ticket.ErrEventHasBeenRemoved, ticket.ErrTicketHasExpired, + ticket.ErrRawOddInvalid, ticket.ErrTooManyOutcomesForTicket, + ticket.ErrTicketAmountTooHigh, ticket.ErrTicketLimitForSingleUser, + ticket.ErrTicketWinningTooHigh: return fiber.NewError(fiber.StatusBadRequest, err.Error()) } return fiber.NewError(fiber.StatusInternalServerError, err.Error()) diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 6fa1c57..3794c5c 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -22,6 +22,7 @@ func (a *App) initAppRoutes() { h := handlers.New( a.currSvc, a.logger, + a.settingSvc, a.NotidicationStore, a.validator, a.reportSvc,