From 77a7428b48f1a202585bf4fb8b006d6b31569378 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Sun, 29 Jun 2025 19:51:30 +0300 Subject: [PATCH 02/10] CSV report fix + popok hash --- internal/domain/virtual_game.go | 25 +++++ internal/services/report/service.go | 2 +- internal/services/virtualGame/service.go | 92 +++++++++++++++---- internal/services/virtualGame/veli/client.go | 6 +- internal/services/virtualGame/veli/service.go | 37 ++++++++ internal/web_server/handlers/report.go | 37 ++++++-- .../handlers/virtual_games_hadlers.go | 2 +- internal/web_server/routes.go | 4 +- 8 files changed, 173 insertions(+), 32 deletions(-) diff --git a/internal/domain/virtual_game.go b/internal/domain/virtual_game.go index d2174cc..39177ba 100644 --- a/internal/domain/virtual_game.go +++ b/internal/domain/virtual_game.go @@ -269,3 +269,28 @@ type GameRecommendation struct { Bets []float64 `json:"bets"` Reason string `json:"reason"` // e.g., "Based on your activity", "Popular", "Random pick" } + +type PopokLaunchRequest struct { + Action string `json:"action"` + Platform int `json:"platform"` + PartnerID int `json:"partnerId"` + Time string `json:"time"` + Hash string `json:"hash"` + Data PopokLaunchRequestData `json:"data"` +} + +type PopokLaunchRequestData struct { + GameMode string `json:"gameMode"` + GameID string `json:"gameId"` + Lang string `json:"lang"` + Token string `json:"token"` + ExitURL string `json:"exitURL"` +} + +type PopokLaunchResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data struct { + LauncherURL string `json:"launcherURL"` + } `json:"data"` +} diff --git a/internal/services/report/service.go b/internal/services/report/service.go index 3e047e3..6d4cb6a 100644 --- a/internal/services/report/service.go +++ b/internal/services/report/service.go @@ -465,7 +465,7 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error { return fmt.Errorf("fetch data: %w", err) } - filePath := fmt.Sprintf("/host-desktop/report_%s_%s.csv", period, time.Now().Format("2006-01-02_15-04")) + filePath := fmt.Sprintf("reports/report_%s_%s.csv", period, time.Now().Format("2006-01-02_15-04")) file, err := os.Create(filePath) if err != nil { return fmt.Errorf("create file: %w", err) diff --git a/internal/services/virtualGame/service.go b/internal/services/virtualGame/service.go index e413993..7373fdc 100644 --- a/internal/services/virtualGame/service.go +++ b/internal/services/virtualGame/service.go @@ -43,21 +43,23 @@ func New(repo repository.VirtualGameRepository, walletSvc wallet.Service, store } func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error) { + // 1. Fetch user user, err := s.store.GetUserByID(ctx, userID) if err != nil { s.logger.Error("Failed to get user", "userID", userID, "error", err) return "", err } - sessionId := fmt.Sprintf("%d-%s-%d", userID, gameID, time.Now().UnixNano()) + // 2. Generate session and token + sessionID := fmt.Sprintf("%d-%s-%d", userID, gameID, time.Now().UnixNano()) token, err := jwtutil.CreatePopOKJwt( userID, user.CompanyID, - user.FirstName, + user.PhoneNumber, currency, "en", mode, - sessionId, + sessionID, s.config.PopOK.SecretKey, 24*time.Hour, ) @@ -66,9 +68,9 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI return "", err } - // Record game launch as a transaction (for history and recommendation purposes) - tx := &domain.VirtualGameHistory{ - SessionID: sessionId, // Optional: populate if session tracking is implemented + // 3. Record virtual game history (optional but recommended) + history := &domain.VirtualGameHistory{ + SessionID: sessionID, UserID: userID, CompanyID: user.CompanyID.Value, Provider: string(domain.PROVIDER_POPOK), @@ -76,23 +78,66 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI TransactionType: "LAUNCH", Amount: 0, Currency: currency, - ExternalTransactionID: sessionId, + ExternalTransactionID: sessionID, Status: "COMPLETED", CreatedAt: time.Now(), UpdatedAt: time.Now(), } - - if err := s.repo.CreateVirtualGameHistory(ctx, tx); err != nil { + if err := s.repo.CreateVirtualGameHistory(ctx, history); err != nil { s.logger.Error("Failed to record game launch transaction", "error", err) - // Do not fail game launch on logging error — just log and continue + // Non-fatal: log and continue } - params := fmt.Sprintf( - "partnerId=%s&gameId=%s&gameMode=%s&lang=en&platform=%s&externalToken=%s", - s.config.PopOK.ClientID, gameID, mode, s.config.PopOK.Platform, token, - ) + // 4. Prepare PopOK API request + timestamp := time.Now().Format("02-01-2006 15:04:05") + partnerID, err := strconv.Atoi(s.config.PopOK.ClientID) + if err != nil { + return "", fmt.Errorf("invalid PopOK ClientID: %v", err) + } - return fmt.Sprintf("%s?%s", s.config.PopOK.BaseURL, params), nil + data := domain.PopokLaunchRequestData{ + GameMode: mode, + GameID: gameID, + Lang: "en", + Token: token, + ExitURL: "", + } + + hash, err := generatePopOKHash(s.config.PopOK.SecretKey, timestamp, data) + if err != nil { + return "", fmt.Errorf("failed to generate PopOK hash: %w", err) + } + + platformInt, err := strconv.Atoi(s.config.PopOK.Platform) + if err != nil { + return "", fmt.Errorf("invalid PopOK Platform: %v", err) + } + reqBody := domain.PopokLaunchRequest{ + Action: "getLauncherURL", + Platform: platformInt, + PartnerID: partnerID, + Time: timestamp, + Hash: hash, + Data: data, + } + + // 5. Make API request + bodyBytes, _ := json.Marshal(reqBody) + resp, err := http.Post(s.config.PopOK.BaseURL+"/serviceApi.php", "application/json", bytes.NewReader(bodyBytes)) + if err != nil { + return "", fmt.Errorf("PopOK POST failed: %w", err) + } + defer resp.Body.Close() + + var parsedResp domain.PopokLaunchResponse + if err := json.NewDecoder(resp.Body).Decode(&parsedResp); err != nil { + return "", fmt.Errorf("failed to parse PopOK response: %w", err) + } + if parsedResp.Code != 0 { + return "", fmt.Errorf("PopOK error: %s", parsedResp.Message) + } + + return parsedResp.Data.LauncherURL, nil } func (s *service) HandleCallback(ctx context.Context, callback *domain.PopOKCallback) error { @@ -559,10 +604,19 @@ func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequ }, nil } -func (s *service) GenerateSignature(params string) string { - h := hmac.New(sha256.New, []byte(s.config.PopOK.SecretKey)) - h.Write([]byte(params)) - return hex.EncodeToString(h.Sum(nil)) +func generatePopOKHash(privateKey, timestamp string, data domain.PopokLaunchRequestData) (string, error) { + // Marshal data to JSON (compact format, like json_encode in PHP) + dataBytes, err := json.Marshal(data) + if err != nil { + return "", err + } + + // Concatenate: privateKey + time + json_encoded(data) + hashInput := fmt.Sprintf("%s%s%s", privateKey, timestamp, string(dataBytes)) + + // SHA-256 hash + hash := sha256.Sum256([]byte(hashInput)) + return hex.EncodeToString(hash[:]), nil } func (s *service) verifySignature(callback *domain.PopOKCallback) bool { diff --git a/internal/services/virtualGame/veli/client.go b/internal/services/virtualGame/veli/client.go index 6c4b4ee..67079f6 100644 --- a/internal/services/virtualGame/veli/client.go +++ b/internal/services/virtualGame/veli/client.go @@ -15,6 +15,7 @@ import ( "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" ) type Client struct { @@ -23,16 +24,17 @@ type Client struct { OperatorID string SecretKey string BrandID string - cfg *config.Config + walletSvc *wallet.Service } -func NewClient(cfg *config.Config) *Client { +func NewClient(cfg *config.Config, walletSvc *wallet.Service) *Client { return &Client{ http: &http.Client{Timeout: 10 * time.Second}, BaseURL: cfg.VeliGames.BaseURL, OperatorID: cfg.VeliGames.OperatorID, SecretKey: cfg.VeliGames.SecretKey, BrandID: cfg.VeliGames.BrandID, + walletSvc: walletSvc, } } diff --git a/internal/services/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go index 634d03d..7622424 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strconv" "strings" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -105,6 +106,17 @@ func (c *Client) ProcessBet(ctx context.Context, req domain.BetRequest) (*domain var res domain.BetResponse err := c.post(ctx, "/bet", req, sigParams, &res) + playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64) + if err != nil { + return &domain.BetResponse{}, fmt.Errorf("invalid PlayerID: %w", err) + } + wallets, err := c.walletSvc.GetWalletsByUser(ctx, playerIDInt64) + if err != nil { + return &domain.BetResponse{}, err + } + + c.walletSvc.DeductFromWallet(ctx, wallets[0].ID, domain.Currency(req.Amount.Amount), domain.CustomerWalletType, domain.ValidInt64{}, domain.TRANSFER_DIRECT) + return &res, err } @@ -133,6 +145,19 @@ func (c *Client) ProcessWin(ctx context.Context, req domain.WinRequest) (*domain var res domain.WinResponse err := c.post(ctx, "/win", req, sigParams, &res) + + playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64) + if err != nil { + return &domain.WinResponse{}, fmt.Errorf("invalid PlayerID: %w", err) + } + + wallets, err := c.walletSvc.GetWalletsByUser(ctx, playerIDInt64) + if err != nil { + return &domain.WinResponse{}, err + } + + c.walletSvc.AddToWallet(ctx, wallets[0].ID, domain.Currency(req.Amount.Amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}) + return &res, err } @@ -163,6 +188,18 @@ func (c *Client) ProcessCancel(ctx context.Context, req domain.CancelRequest) (* var res domain.CancelResponse err := c.post(ctx, "/cancel", req, sigParams, &res) + + playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64) + if err != nil { + return &domain.CancelResponse{}, fmt.Errorf("invalid PlayerID: %w", err) + } + + wallets, err := c.walletSvc.GetWalletsByUser(ctx, playerIDInt64) + if err != nil { + return &domain.CancelResponse{}, err + } + + c.walletSvc.AddToWallet(ctx, wallets[0].ID, domain.Currency(req.AdjustmentRefund.Amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}) return &res, err } diff --git a/internal/web_server/handlers/report.go b/internal/web_server/handlers/report.go index ed396a2..c2ed79f 100644 --- a/internal/web_server/handlers/report.go +++ b/internal/web_server/handlers/report.go @@ -137,16 +137,28 @@ func parseReportFilter(c *fiber.Ctx) (domain.ReportFilter, error) { // @Router /api/v1/report-files/download/{filename} [get] func (h *Handler) DownloadReportFile(c *fiber.Ctx) error { filename := c.Params("filename") - if filename == "" { + if filename == "" || strings.Contains(filename, "..") { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ - Message: "Missing filename parameter", - Error: "filename is required", + Message: "Invalid filename parameter", + Error: "filename is required and must not contain '..'", }) } - filePath := fmt.Sprintf("/host-desktop/%s", filename) + reportDir := "reports" - // Check if file exists + // Ensure reports directory exists + if _, err := os.Stat(reportDir); os.IsNotExist(err) { + if err := os.MkdirAll(reportDir, os.ModePerm); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to create report directory", + Error: err.Error(), + }) + } + } + + filePath := fmt.Sprintf("%s/%s", reportDir, filename) + + // Check if the report file exists if _, err := os.Stat(filePath); os.IsNotExist(err) { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Report file not found", @@ -154,10 +166,11 @@ func (h *Handler) DownloadReportFile(c *fiber.Ctx) error { }) } - // Set download headers and return file + // Set download headers c.Set("Content-Type", "text/csv") c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) + // Serve the file if err := c.SendFile(filePath); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to serve file", @@ -177,7 +190,17 @@ func (h *Handler) DownloadReportFile(c *fiber.Ctx) error { // @Failure 500 {object} domain.ErrorResponse "Failed to read report directory" // @Router /api/v1/report-files/list [get] func (h *Handler) ListReportFiles(c *fiber.Ctx) error { - reportDir := "/host-desktop" + reportDir := "reports" + + // Create the reports directory if it doesn't exist + if _, err := os.Stat(reportDir); os.IsNotExist(err) { + if err := os.MkdirAll(reportDir, os.ModePerm); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to create report directory", + Error: err.Error(), + }) + } + } files, err := os.ReadDir(reportDir) if err != nil { diff --git a/internal/web_server/handlers/virtual_games_hadlers.go b/internal/web_server/handlers/virtual_games_hadlers.go index 4b51f58..6c8baca 100644 --- a/internal/web_server/handlers/virtual_games_hadlers.go +++ b/internal/web_server/handlers/virtual_games_hadlers.go @@ -9,7 +9,7 @@ import ( ) type launchVirtualGameReq struct { - GameID string `json:"game_id" validate:"required" example:"crash_001"` + GameID string `json:"game_id" validate:"required" example:"1"` Currency string `json:"currency" validate:"required,len=3" example:"USD"` Mode string `json:"mode" validate:"required,oneof=fun real" example:"real"` } diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index b7b0aca..2668d6a 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -220,8 +220,8 @@ func (a *App) initAppRoutes() { //Report Routes group.Get("/reports/dashboard", h.GetDashboardReport) - group.Get("/report-files/download/:filename", a.authMiddleware, a.SuperAdminOnly, h.DownloadReportFile) - group.Get("/report-files/list", a.authMiddleware, a.SuperAdminOnly, h.ListReportFiles) + group.Get("/report-files/download/:filename", a.authMiddleware, a.OnlyAdminAndAbove, h.DownloadReportFile) + group.Get("/report-files/list", a.authMiddleware, a.OnlyAdminAndAbove, h.ListReportFiles) //Wallet Monitor Service // group.Get("/debug/wallet-monitor/status", func(c *fiber.Ctx) error { From 19b7a151d6dfc5cedce3fe652d76d83c916ffa65 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Sun, 29 Jun 2025 22:57:50 +0300 Subject: [PATCH 03/10] balance amount cap for deposit bonus --- db/migrations/000001_fortune.up.sql | 3 +- db/query/bonus.sql | 13 ++++-- gen/db/bonus.sql.go | 62 ++++++++++++++++++++++----- gen/db/models.go | 1 + internal/repository/bonus.go | 16 +++++-- internal/services/bonus/port.go | 7 +-- internal/services/bonus/service.go | 14 +++--- internal/web_server/handlers/bonus.go | 6 ++- internal/web_server/handlers/chapa.go | 11 ++++- 9 files changed, 103 insertions(+), 30 deletions(-) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 3456bb4..a935963 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -289,7 +289,8 @@ CREATE TABLE IF NOT EXISTS settings ( ); CREATE TABLE bonus ( id BIGSERIAL PRIMARY KEY, - multiplier REAL NOT NULL + multiplier REAL NOT NULL, + balance_cap BIGINT NOT NULL DEFAULT 0 ); -- Views CREATE VIEW companies_details AS diff --git a/db/query/bonus.sql b/db/query/bonus.sql index c516162..82b3113 100644 --- a/db/query/bonus.sql +++ b/db/query/bonus.sql @@ -1,12 +1,17 @@ -- name: CreateBonusMultiplier :exec -INSERT INTO bonus (multiplier) -VALUES ($1); +INSERT INTO bonus (multiplier, balance_cap) +VALUES ($1, $2); -- name: GetBonusMultiplier :many SELECT id, multiplier FROM bonus; +-- name: GetBonusBalanceCap :many +SELECT id, balance_cap +FROM bonus; + -- name: UpdateBonusMultiplier :exec UPDATE bonus -SET multiplier = $1 -WHERE id = $2; \ No newline at end of file +SET multiplier = $1, + balance_cap = $2 +WHERE id = $3; \ No newline at end of file diff --git a/gen/db/bonus.sql.go b/gen/db/bonus.sql.go index 21ef5c7..12677b8 100644 --- a/gen/db/bonus.sql.go +++ b/gen/db/bonus.sql.go @@ -10,29 +10,69 @@ import ( ) const CreateBonusMultiplier = `-- name: CreateBonusMultiplier :exec -INSERT INTO bonus (multiplier) -VALUES ($1) +INSERT INTO bonus (multiplier, balance_cap) +VALUES ($1, $2) ` -func (q *Queries) CreateBonusMultiplier(ctx context.Context, multiplier float32) error { - _, err := q.db.Exec(ctx, CreateBonusMultiplier, multiplier) +type CreateBonusMultiplierParams struct { + Multiplier float32 `json:"multiplier"` + BalanceCap int64 `json:"balance_cap"` +} + +func (q *Queries) CreateBonusMultiplier(ctx context.Context, arg CreateBonusMultiplierParams) error { + _, err := q.db.Exec(ctx, CreateBonusMultiplier, arg.Multiplier, arg.BalanceCap) return err } +const GetBonusBalanceCap = `-- name: GetBonusBalanceCap :many +SELECT id, balance_cap +FROM bonus +` + +type GetBonusBalanceCapRow struct { + ID int64 `json:"id"` + BalanceCap int64 `json:"balance_cap"` +} + +func (q *Queries) GetBonusBalanceCap(ctx context.Context) ([]GetBonusBalanceCapRow, error) { + rows, err := q.db.Query(ctx, GetBonusBalanceCap) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetBonusBalanceCapRow + for rows.Next() { + var i GetBonusBalanceCapRow + if err := rows.Scan(&i.ID, &i.BalanceCap); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const GetBonusMultiplier = `-- name: GetBonusMultiplier :many SELECT id, multiplier FROM bonus ` -func (q *Queries) GetBonusMultiplier(ctx context.Context) ([]Bonu, error) { +type GetBonusMultiplierRow struct { + ID int64 `json:"id"` + Multiplier float32 `json:"multiplier"` +} + +func (q *Queries) GetBonusMultiplier(ctx context.Context) ([]GetBonusMultiplierRow, error) { rows, err := q.db.Query(ctx, GetBonusMultiplier) if err != nil { return nil, err } defer rows.Close() - var items []Bonu + var items []GetBonusMultiplierRow for rows.Next() { - var i Bonu + var i GetBonusMultiplierRow if err := rows.Scan(&i.ID, &i.Multiplier); err != nil { return nil, err } @@ -46,16 +86,18 @@ func (q *Queries) GetBonusMultiplier(ctx context.Context) ([]Bonu, error) { const UpdateBonusMultiplier = `-- name: UpdateBonusMultiplier :exec UPDATE bonus -SET multiplier = $1 -WHERE id = $2 +SET multiplier = $1, + balance_cap = $2 +WHERE id = $3 ` type UpdateBonusMultiplierParams struct { Multiplier float32 `json:"multiplier"` + BalanceCap int64 `json:"balance_cap"` ID int64 `json:"id"` } func (q *Queries) UpdateBonusMultiplier(ctx context.Context, arg UpdateBonusMultiplierParams) error { - _, err := q.db.Exec(ctx, UpdateBonusMultiplier, arg.Multiplier, arg.ID) + _, err := q.db.Exec(ctx, UpdateBonusMultiplier, arg.Multiplier, arg.BalanceCap, arg.ID) return err } diff --git a/gen/db/models.go b/gen/db/models.go index b99623f..e801d9d 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -131,6 +131,7 @@ type BetWithOutcome struct { type Bonu struct { ID int64 `json:"id"` Multiplier float32 `json:"multiplier"` + BalanceCap int64 `json:"balance_cap"` } type Branch struct { diff --git a/internal/repository/bonus.go b/internal/repository/bonus.go index b253ad2..c4f57ac 100644 --- a/internal/repository/bonus.go +++ b/internal/repository/bonus.go @@ -6,17 +6,25 @@ import ( dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" ) -func (s *Store) CreateBonusMultiplier(ctx context.Context, multiplier float32) error { - return s.queries.CreateBonusMultiplier(ctx, multiplier) +func (s *Store) CreateBonusMultiplier(ctx context.Context, multiplier float32, balance_cap int64) error { + return s.queries.CreateBonusMultiplier(ctx, dbgen.CreateBonusMultiplierParams{ + Multiplier: multiplier, + BalanceCap: balance_cap, + }) } -func (s *Store) GetBonusMultiplier(ctx context.Context) ([]dbgen.Bonu, error) { +func (s *Store) GetBonusMultiplier(ctx context.Context) ([]dbgen.GetBonusMultiplierRow, error) { return s.queries.GetBonusMultiplier(ctx) } -func (s *Store) UpdateBonusMultiplier(ctx context.Context, id int64, mulitplier float32) error { +func (s *Store) GetBonusBalanceCap(ctx context.Context) ([]dbgen.GetBonusBalanceCapRow, error) { + return s.queries.GetBonusBalanceCap(ctx) +} + +func (s *Store) UpdateBonusMultiplier(ctx context.Context, id int64, mulitplier float32, balance_cap int64) error { return s.queries.UpdateBonusMultiplier(ctx, dbgen.UpdateBonusMultiplierParams{ ID: id, Multiplier: mulitplier, + BalanceCap: balance_cap, }) } diff --git a/internal/services/bonus/port.go b/internal/services/bonus/port.go index 02b59ca..2147b51 100644 --- a/internal/services/bonus/port.go +++ b/internal/services/bonus/port.go @@ -7,7 +7,8 @@ import ( ) type BonusStore interface { - CreateBonusMultiplier(ctx context.Context, multiplier float32) error - GetBonusMultiplier(ctx context.Context) ([]dbgen.Bonu, error) - UpdateBonusMultiplier(ctx context.Context, id int64, mulitplier float32) error + CreateBonusMultiplier(ctx context.Context, multiplier float32, balance_cap int64) error + GetBonusMultiplier(ctx context.Context) ([]dbgen.GetBonusMultiplierRow, error) + GetBonusBalanceCap(ctx context.Context) ([]dbgen.GetBonusBalanceCapRow, error) + UpdateBonusMultiplier(ctx context.Context, id int64, mulitplier float32, balance_cap int64) error } diff --git a/internal/services/bonus/service.go b/internal/services/bonus/service.go index f55107c..51e008a 100644 --- a/internal/services/bonus/service.go +++ b/internal/services/bonus/service.go @@ -16,14 +16,18 @@ func NewService(bonusStore BonusStore) *Service { } } -func (s *Service) CreateBonusMultiplier(ctx context.Context, multiplier float32) error { - return s.bonusStore.CreateBonusMultiplier(ctx, multiplier) +func (s *Service) CreateBonusMultiplier(ctx context.Context, multiplier float32, balance_cap int64) error { + return s.bonusStore.CreateBonusMultiplier(ctx, multiplier, balance_cap) } -func (s *Service) GetBonusMultiplier(ctx context.Context) ([]dbgen.Bonu, error) { +func (s *Service) GetBonusMultiplier(ctx context.Context) ([]dbgen.GetBonusMultiplierRow, error) { return s.bonusStore.GetBonusMultiplier(ctx) } -func (s *Service) UpdateBonusMultiplier(ctx context.Context, id int64, mulitplier float32) error { - return s.bonusStore.UpdateBonusMultiplier(ctx, id, mulitplier) +func (s *Service) GetBonusBalanceCap(ctx context.Context) ([]dbgen.GetBonusBalanceCapRow, error) { + return s.bonusStore.GetBonusBalanceCap(ctx) +} + +func (s *Service) UpdateBonusMultiplier(ctx context.Context, id int64, mulitplier float32, balance_cap int64) error { + return s.bonusStore.UpdateBonusMultiplier(ctx, id, mulitplier, balance_cap) } diff --git a/internal/web_server/handlers/bonus.go b/internal/web_server/handlers/bonus.go index 19f0e4f..f4e5a27 100644 --- a/internal/web_server/handlers/bonus.go +++ b/internal/web_server/handlers/bonus.go @@ -8,6 +8,7 @@ import ( func (h *Handler) CreateBonusMultiplier(c *fiber.Ctx) error { var req struct { Multiplier float32 `json:"multiplier"` + BalanceCap int64 `json:"balance_cap"` } if err := c.BodyParser(&req); err != nil { @@ -27,7 +28,7 @@ func (h *Handler) CreateBonusMultiplier(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } - if err := h.bonusSvc.CreateBonusMultiplier(c.Context(), req.Multiplier); err != nil { + if err := h.bonusSvc.CreateBonusMultiplier(c.Context(), req.Multiplier, req.BalanceCap); err != nil { h.logger.Error("failed to create bonus multiplier", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "failed to create bonus mulitplier", nil, nil) } @@ -49,6 +50,7 @@ func (h *Handler) UpdateBonusMultiplier(c *fiber.Ctx) error { var req struct { ID int64 `json:"id"` Multiplier float32 `json:"multiplier"` + BalanceCap int64 `json:"balance_cap"` } if err := c.BodyParser(&req); err != nil { @@ -56,7 +58,7 @@ func (h *Handler) UpdateBonusMultiplier(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } - if err := h.bonusSvc.UpdateBonusMultiplier(c.Context(), req.ID, req.Multiplier); err != nil { + if err := h.bonusSvc.UpdateBonusMultiplier(c.Context(), req.ID, req.Multiplier, req.BalanceCap); err != nil { h.logger.Error("failed to update bonus multiplier", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "failed to update bonus mulitplier", nil, nil) } diff --git a/internal/web_server/handlers/chapa.go b/internal/web_server/handlers/chapa.go index ddfb32d..3ec49db 100644 --- a/internal/web_server/handlers/chapa.go +++ b/internal/web_server/handlers/chapa.go @@ -2,6 +2,7 @@ package handlers import ( "fmt" + "math" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/gofiber/fiber/v2" @@ -66,7 +67,15 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error { multiplier = bonusMultiplier[0].Multiplier } - _, err = h.walletSvc.AddToWallet(c.Context(), wallet.StaticID, domain.ToCurrency(float32(amount)*multiplier), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}) + var balanceCap int64 = 0 + bonusBalanceCap, err := h.bonusSvc.GetBonusBalanceCap(c.Context()) + if err == nil { + balanceCap = bonusBalanceCap[0].BalanceCap + } + + capedBalanceAmount := domain.Currency((math.Min(req.Amount, float64(balanceCap)) * float64(multiplier)) * 100) + + _, err = h.walletSvc.AddToWallet(c.Context(), wallet.StaticID, capedBalanceAmount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}) if err != nil { h.logger.Error("Failed to add bonus to static wallet", "walletID", wallet.StaticID, "user id", userID, "error", err) return err From 82187902928bdc1fd6bb82900ad2df4fdb2a8d3c Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Mon, 30 Jun 2025 12:23:14 +0300 Subject: [PATCH 04/10] control over referral settings --- db/query/referal.sql | 1 - gen/db/referal.sql.go | 1 - internal/domain/referal.go | 8 ++++ internal/repository/referal.go | 7 +-- internal/services/referal/port.go | 1 + internal/services/referal/service.go | 46 +++++++++++++++++-- .../web_server/handlers/referal_handlers.go | 43 +++++++++++++++-- internal/web_server/routes.go | 3 +- 8 files changed, 94 insertions(+), 16 deletions(-) diff --git a/db/query/referal.sql b/db/query/referal.sql index a10b274..dd868ca 100644 --- a/db/query/referal.sql +++ b/db/query/referal.sql @@ -40,7 +40,6 @@ WHERE referrer_id = $1; -- name: GetReferralSettings :one SELECT * FROM referral_settings -WHERE id = 'default' LIMIT 1; -- name: UpdateReferralSettings :one diff --git a/gen/db/referal.sql.go b/gen/db/referal.sql.go index 3a7f337..9784440 100644 --- a/gen/db/referal.sql.go +++ b/gen/db/referal.sql.go @@ -149,7 +149,6 @@ func (q *Queries) GetReferralByReferredID(ctx context.Context, referredID pgtype const GetReferralSettings = `-- name: GetReferralSettings :one SELECT id, referral_reward_amount, cashback_percentage, bet_referral_bonus_percentage, max_referrals, expires_after_days, updated_by, created_at, updated_at, version FROM referral_settings -WHERE id = 'default' LIMIT 1 ` diff --git a/internal/domain/referal.go b/internal/domain/referal.go index 9923806..1e528a4 100644 --- a/internal/domain/referal.go +++ b/internal/domain/referal.go @@ -51,6 +51,14 @@ type ReferralSettings struct { Version int32 } +type ReferralSettingsReq struct { + ReferralRewardAmount float64 `json:"referral_reward_amount" validate:"required"` + CashbackPercentage float64 `json:"cashback_percentage" validate:"required"` + MaxReferrals int32 `json:"max_referrals" validate:"required"` + ExpiresAfterDays int32 `json:"expires_afterdays" validate:"required"` + UpdatedBy string `json:"updated_by" validate:"required"` +} + type Referral struct { ID int64 ReferralCode string diff --git a/internal/repository/referal.go b/internal/repository/referal.go index a782cfb..105ce5d 100644 --- a/internal/repository/referal.go +++ b/internal/repository/referal.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "fmt" "strconv" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" @@ -145,17 +146,17 @@ func (r *ReferralRepo) UpdateSettings(ctx context.Context, settings *domain.Refe func (r *ReferralRepo) CreateSettings(ctx context.Context, settings *domain.ReferralSettings) error { rewardAmount := pgtype.Numeric{} - if err := rewardAmount.Scan(settings.ReferralRewardAmount); err != nil { + if err := rewardAmount.Scan(fmt.Sprintf("%f", settings.ReferralRewardAmount)); err != nil { return err } cashbackPercentage := pgtype.Numeric{} - if err := cashbackPercentage.Scan(settings.CashbackPercentage); err != nil { + if err := cashbackPercentage.Scan(fmt.Sprintf("%f", settings.CashbackPercentage)); err != nil { return err } betReferralBonusPercentage := pgtype.Numeric{} - if err := betReferralBonusPercentage.Scan(settings.BetReferralBonusPercentage); err != nil { + if err := betReferralBonusPercentage.Scan(fmt.Sprintf("%f", settings.BetReferralBonusPercentage)); err != nil { return err } diff --git a/internal/services/referal/port.go b/internal/services/referal/port.go index 5fb867b..703f986 100644 --- a/internal/services/referal/port.go +++ b/internal/services/referal/port.go @@ -12,6 +12,7 @@ type ReferralStore interface { ProcessReferral(ctx context.Context, referredID, referralCode string) error ProcessDepositBonus(ctx context.Context, userID string, amount float64) error GetReferralStats(ctx context.Context, userID string) (*domain.ReferralStats, error) + CreateReferralSettings(ctx context.Context, req domain.ReferralSettingsReq) error UpdateReferralSettings(ctx context.Context, settings *domain.ReferralSettings) error GetReferralSettings(ctx context.Context) (*domain.ReferralSettings, error) ProcessBetReferral(ctx context.Context, userPhone string, betAmount float64) error diff --git a/internal/services/referal/service.go b/internal/services/referal/service.go index 5585d74..0a812f0 100644 --- a/internal/services/referal/service.go +++ b/internal/services/referal/service.go @@ -54,16 +54,32 @@ func (s *Service) GenerateReferralCode() (string, error) { func (s *Service) CreateReferral(ctx context.Context, userID int64) error { s.logger.Info("Creating referral code for user", "userID", userID) - // TODO: check in user already has an active referral code + + // check if user already has an active referral code + referral, err := s.repo.GetReferralByReferredID(ctx, fmt.Sprintf("%d", userID)) + if err != nil { + s.logger.Error("Failed to check if user alredy has active referral code", "error", err) + return err + } + if referral != nil && referral.Status == domain.ReferralPending && referral.ExpiresAt.After(time.Now()) { + s.logger.Error("user already has an active referral code", "error", err) + return err + } + code, err := s.GenerateReferralCode() if err != nil { s.logger.Error("Failed to generate referral code", "error", err) return err } - // TODO: get the referral settings from db - var rewardAmount float64 = 100 - var expireDuration time.Time = time.Now().Add(24 * time.Hour) + settings, err := s.GetReferralSettings(ctx) + if err != nil || settings == nil { + s.logger.Error("Failed to fetch referral settings", "error", err) + return err + } + + var rewardAmount float64 = settings.ReferralRewardAmount + var expireDuration time.Time = time.Now().Add(time.Duration((24 * settings.ExpiresAfterDays)) * time.Hour) if err := s.repo.CreateReferral(ctx, &domain.Referral{ ReferralCode: code, @@ -242,6 +258,26 @@ func (s *Service) GetReferralStats(ctx context.Context, userPhone string) (*doma return stats, nil } +func (s *Service) CreateReferralSettings(ctx context.Context, req domain.ReferralSettingsReq) error { + s.logger.Info("Creating referral setting") + + if err := s.repo.CreateSettings(ctx, &domain.ReferralSettings{ + ReferralRewardAmount: req.ReferralRewardAmount, + CashbackPercentage: req.CashbackPercentage, + MaxReferrals: req.MaxReferrals, + ExpiresAfterDays: req.ExpiresAfterDays, + UpdatedBy: req.UpdatedBy, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }); err != nil { + s.logger.Error("Failed to create referral setting", "error", err) + return err + } + + s.logger.Info("Referral setting created succesfully") + return nil +} + func (s *Service) UpdateReferralSettings(ctx context.Context, settings *domain.ReferralSettings) error { s.logger.Info("Updating referral settings", "settingsID", settings.ID) @@ -265,6 +301,6 @@ func (s *Service) GetReferralSettings(ctx context.Context) (*domain.ReferralSett return nil, err } - s.logger.Info("Referral settings retrieved successfully", "settingsID", settings.ID) + s.logger.Info("Referral settings retrieved successfully", "settings", settings) return settings, nil } diff --git a/internal/web_server/handlers/referal_handlers.go b/internal/web_server/handlers/referal_handlers.go index d978e0b..a2fe09e 100644 --- a/internal/web_server/handlers/referal_handlers.go +++ b/internal/web_server/handlers/referal_handlers.go @@ -21,6 +21,38 @@ func (h *Handler) CreateReferralCode(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Referral created successfully", nil, nil) } +func (h *Handler) CreateReferralSettings(c *fiber.Ctx) error { + var req domain.ReferralSettingsReq + if err := c.BodyParser(&req); err != nil { + h.logger.Error("Failed to parse settings", "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + if valErrs, ok := h.validator.Validate(c, req); !ok { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + } + + settings, err := h.referralSvc.GetReferralSettings(c.Context()) + if err != nil { + h.logger.Error("Failed to fetch previous referral setting", "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create referral") + } + + // only allow one referral setting for now + // for future it can be multiple and be able to choose from them + if settings != nil { + h.logger.Error("referral setting already exists", "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "referral setting already exists") + } + + if err := h.referralSvc.CreateReferralSettings(c.Context(), req); err != nil { + h.logger.Error("Failed to create referral setting", "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create referral") + } + + return response.WriteJSON(c, fiber.StatusOK, "Referral created successfully", nil, nil) +} + // GetReferralStats godoc // @Summary Get referral statistics // @Description Retrieves referral statistics for the authenticated user @@ -112,11 +144,12 @@ func (h *Handler) UpdateReferralSettings(c *fiber.Ctx) error { // @Security Bearer // @Router /referral/settings [get] func (h *Handler) GetReferralSettings(c *fiber.Ctx) error { - userID, ok := c.Locals("user_id").(int64) - if !ok || userID == 0 { - h.logger.Error("Invalid user ID in context") - return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification") - } + // userID, ok := c.Locals("user_id").(int64) + // if !ok || userID == 0 { + // h.logger.Error("Invalid user ID in context") + // return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification") + // } + userID := int64(2) user, err := h.userSvc.GetUserByID(c.Context(), userID) if err != nil { diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 8e6e4aa..cf0ad4c 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -109,7 +109,8 @@ func (a *App) initAppRoutes() { // Referral Routes a.fiber.Post("/referral/create", a.authMiddleware, h.CreateReferralCode) a.fiber.Get("/referral/stats", a.authMiddleware, h.GetReferralStats) - a.fiber.Get("/referral/settings", h.GetReferralSettings) + a.fiber.Post("/referral/settings", a.authMiddleware, h.CreateReferralSettings) + a.fiber.Get("/referral/settings", a.authMiddleware, h.GetReferralSettings) a.fiber.Patch("/referral/settings", a.authMiddleware, h.UpdateReferralSettings) // Bonus Routes From 52cfb809a9fea7c92007b67b1bdeb9817cd1dbdd Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Mon, 30 Jun 2025 18:16:07 +0300 Subject: [PATCH 05/10] referral amount limit --- db/query/referal.sql | 6 +++++ gen/db/referal.sql.go | 33 +++++++++++++++++++++++++ internal/repository/referal.go | 26 +++++++++++++++++++ internal/services/referal/port.go | 1 + internal/services/referal/service.go | 37 ++++++++++++++++++++++------ 5 files changed, 96 insertions(+), 7 deletions(-) diff --git a/db/query/referal.sql b/db/query/referal.sql index dd868ca..206606e 100644 --- a/db/query/referal.sql +++ b/db/query/referal.sql @@ -69,3 +69,9 @@ INSERT INTO referral_settings ( -- name: GetReferralByReferredID :one SELECT * FROM referrals WHERE referred_id = $1 LIMIT 1; + +-- name: GetActiveReferralByReferrerID :one +SELECT * FROM referrals WHERE referrer_id = $1 AND status = 'PENDING' LIMIT 1; + +-- name: GetReferralCountByID :one +SELECT count(*) FROM referrals WHERE referrer_id = $1; \ No newline at end of file diff --git a/gen/db/referal.sql.go b/gen/db/referal.sql.go index 9784440..b5ceeed 100644 --- a/gen/db/referal.sql.go +++ b/gen/db/referal.sql.go @@ -102,6 +102,28 @@ func (q *Queries) CreateReferralSettings(ctx context.Context, arg CreateReferral return i, err } +const GetActiveReferralByReferrerID = `-- name: GetActiveReferralByReferrerID :one +SELECT id, referral_code, referrer_id, referred_id, status, reward_amount, cashback_amount, created_at, updated_at, expires_at FROM referrals WHERE referrer_id = $1 AND status = 'PENDING' LIMIT 1 +` + +func (q *Queries) GetActiveReferralByReferrerID(ctx context.Context, referrerID string) (Referral, error) { + row := q.db.QueryRow(ctx, GetActiveReferralByReferrerID, referrerID) + var i Referral + err := row.Scan( + &i.ID, + &i.ReferralCode, + &i.ReferrerID, + &i.ReferredID, + &i.Status, + &i.RewardAmount, + &i.CashbackAmount, + &i.CreatedAt, + &i.UpdatedAt, + &i.ExpiresAt, + ) + return i, err +} + const GetReferralByCode = `-- name: GetReferralByCode :one SELECT id, referral_code, referrer_id, referred_id, status, reward_amount, cashback_amount, created_at, updated_at, expires_at FROM referrals WHERE referral_code = $1 @@ -147,6 +169,17 @@ func (q *Queries) GetReferralByReferredID(ctx context.Context, referredID pgtype return i, err } +const GetReferralCountByID = `-- name: GetReferralCountByID :one +SELECT count(*) FROM referrals WHERE referrer_id = $1 +` + +func (q *Queries) GetReferralCountByID(ctx context.Context, referrerID string) (int64, error) { + row := q.db.QueryRow(ctx, GetReferralCountByID, referrerID) + var count int64 + err := row.Scan(&count) + return count, err +} + const GetReferralSettings = `-- name: GetReferralSettings :one SELECT id, referral_reward_amount, cashback_percentage, bet_referral_bonus_percentage, max_referrals, expires_after_days, updated_by, created_at, updated_at, version FROM referral_settings LIMIT 1 diff --git a/internal/repository/referal.go b/internal/repository/referal.go index 105ce5d..d214c54 100644 --- a/internal/repository/referal.go +++ b/internal/repository/referal.go @@ -21,6 +21,8 @@ type ReferralRepository interface { UpdateSettings(ctx context.Context, settings *domain.ReferralSettings) error CreateSettings(ctx context.Context, settings *domain.ReferralSettings) error GetReferralByReferredID(ctx context.Context, referredID string) (*domain.Referral, error) // New method + GetReferralCountByID(ctx context.Context, referrerID string) (int64, error) + GetActiveReferralByReferrerID(ctx context.Context, referrerID string) (*domain.Referral, error) UpdateUserReferalCode(ctx context.Context, codedata domain.UpdateUserReferalCode) error } @@ -184,6 +186,30 @@ func (r *ReferralRepo) GetReferralByReferredID(ctx context.Context, referredID s return r.mapToDomainReferral(&dbReferral), nil } +func (r *ReferralRepo) GetReferralCountByID(ctx context.Context, referrerID string) (int64, error) { + count, err := r.store.queries.GetReferralCountByID(ctx, referrerID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return 0, nil + } + return 0, err + } + + return count, nil +} + +func (r *ReferralRepo) GetActiveReferralByReferrerID(ctx context.Context, referrerID string) (*domain.Referral, error) { + referral, err := r.store.queries.GetActiveReferralByReferrerID(ctx, referrerID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return &domain.Referral{}, nil + } + return &domain.Referral{}, err + } + + return r.mapToDomainReferral(&referral), nil +} + func (r *ReferralRepo) mapToDomainReferral(dbRef *dbgen.Referral) *domain.Referral { var referredID *string if dbRef.ReferredID.Valid { diff --git a/internal/services/referal/port.go b/internal/services/referal/port.go index 703f986..1946e99 100644 --- a/internal/services/referal/port.go +++ b/internal/services/referal/port.go @@ -15,5 +15,6 @@ type ReferralStore interface { CreateReferralSettings(ctx context.Context, req domain.ReferralSettingsReq) error UpdateReferralSettings(ctx context.Context, settings *domain.ReferralSettings) error GetReferralSettings(ctx context.Context) (*domain.ReferralSettings, error) + GetReferralCountByID(ctx context.Context, referrerID string) (int64, error) ProcessBetReferral(ctx context.Context, userPhone string, betAmount float64) error } diff --git a/internal/services/referal/service.go b/internal/services/referal/service.go index 0a812f0..aaa7af3 100644 --- a/internal/services/referal/service.go +++ b/internal/services/referal/service.go @@ -56,7 +56,7 @@ func (s *Service) CreateReferral(ctx context.Context, userID int64) error { s.logger.Info("Creating referral code for user", "userID", userID) // check if user already has an active referral code - referral, err := s.repo.GetReferralByReferredID(ctx, fmt.Sprintf("%d", userID)) + referral, err := s.repo.GetActiveReferralByReferrerID(ctx, fmt.Sprintf("%d", userID)) if err != nil { s.logger.Error("Failed to check if user alredy has active referral code", "error", err) return err @@ -66,18 +66,31 @@ func (s *Service) CreateReferral(ctx context.Context, userID int64) error { return err } - code, err := s.GenerateReferralCode() - if err != nil { - s.logger.Error("Failed to generate referral code", "error", err) - return err - } - settings, err := s.GetReferralSettings(ctx) if err != nil || settings == nil { s.logger.Error("Failed to fetch referral settings", "error", err) return err } + // check referral count limit + referralCount, err := s.GetReferralCountByID(ctx, fmt.Sprintf("%d", userID)) + if err != nil { + s.logger.Error("Failed to get referral count", "userID", userID, "error", err) + return err + } + + fmt.Println("referralCount: ", referralCount) + if referralCount == int64(settings.MaxReferrals) { + s.logger.Error("referral count limit has been reached", "referralCount", referralCount, "error", err) + return err + } + + code, err := s.GenerateReferralCode() + if err != nil { + s.logger.Error("Failed to generate referral code", "error", err) + return err + } + var rewardAmount float64 = settings.ReferralRewardAmount var expireDuration time.Time = time.Now().Add(time.Duration((24 * settings.ExpiresAfterDays)) * time.Hour) @@ -304,3 +317,13 @@ func (s *Service) GetReferralSettings(ctx context.Context) (*domain.ReferralSett s.logger.Info("Referral settings retrieved successfully", "settings", settings) return settings, nil } + +func (s *Service) GetReferralCountByID(ctx context.Context, referrerID string) (int64, error) { + count, err := s.repo.GetReferralCountByID(ctx, referrerID) + if err != nil { + s.logger.Error("Failed to get referral count", "userID", referrerID, "error", err) + return 0, err + } + + return count, nil +} From 3768ff857a8f85bc3254800710fc955c1078df2b Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Mon, 30 Jun 2025 18:53:19 +0300 Subject: [PATCH 06/10] popok 502 fix --- cmd/main.go | 27 ++++++++++--------- internal/services/wallet/service.go | 3 ++- internal/services/wallet/wallet.go | 2 +- .../handlers/virtual_games_hadlers.go | 12 +++++++-- internal/web_server/routes.go | 6 ++--- 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index b13d7b9..bbe042e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -115,6 +115,7 @@ func main() { wallet.WalletStore(store), wallet.TransferStore(store), notificatioStore, + notificationSvc, logger, ) @@ -206,22 +207,22 @@ func main() { httpserver.StartTicketCrons(*ticketSvc) // Fetch companies and branches for live wallet metrics update - ctx := context.Background() + // ctx := context.Background() - companies := []domain.GetCompany{ - {ID: 1, Name: "Company A", WalletBalance: 1000.0}, - } + // companies := []domain.GetCompany{ + // {ID: 1, Name: "Company A", WalletBalance: 1000.0}, + // } - branches := []domain.BranchWallet{ - {ID: 10, Name: "Branch Z", CompanyID: 1, Balance: 500.0}, - } + // branches := []domain.BranchWallet{ + // {ID: 10, Name: "Branch Z", CompanyID: 1, Balance: 500.0}, + // } - notificationSvc.UpdateLiveWalletMetrics(ctx, companies, branches) - if err != nil { - log.Println("Failed to update live metrics:", err) - } else { - log.Println("Live metrics broadcasted successfully") - } + // notificationSvc.UpdateLiveWalletMetrics(ctx, companies, branches) + // if err != nil { + // log.Println("Failed to update live metrics:", err) + // } else { + // log.Println("Live metrics broadcasted successfully") + // } issueReportingRepo := repository.NewReportedIssueRepository(store) diff --git a/internal/services/wallet/service.go b/internal/services/wallet/service.go index 8186593..4d0bd76 100644 --- a/internal/services/wallet/service.go +++ b/internal/services/wallet/service.go @@ -14,11 +14,12 @@ type Service struct { logger *slog.Logger } -func NewService(walletStore WalletStore, transferStore TransferStore, notificationStore notificationservice.NotificationStore, logger *slog.Logger) *Service { +func NewService(walletStore WalletStore, transferStore TransferStore, notificationStore notificationservice.NotificationStore, notificationSvc *notificationservice.Service, logger *slog.Logger) *Service { return &Service{ walletStore: walletStore, transferStore: transferStore, notificationStore: notificationStore, + notificationSvc: notificationSvc, logger: logger, } } diff --git a/internal/services/wallet/wallet.go b/internal/services/wallet/wallet.go index eab19f1..b1f8d61 100644 --- a/internal/services/wallet/wallet.go +++ b/internal/services/wallet/wallet.go @@ -134,7 +134,7 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain. return domain.Transfer{}, nil } - go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet) + // go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet) // Log the transfer here for reference newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ diff --git a/internal/web_server/handlers/virtual_games_hadlers.go b/internal/web_server/handlers/virtual_games_hadlers.go index 6c8baca..f3627b2 100644 --- a/internal/web_server/handlers/virtual_games_hadlers.go +++ b/internal/web_server/handlers/virtual_games_hadlers.go @@ -108,7 +108,11 @@ func (h *Handler) HandlePlayerInfo(c *fiber.Ctx) error { func (h *Handler) HandleBet(c *fiber.Ctx) error { var req domain.PopOKBetRequest if err := c.BodyParser(&req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid bet request") + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid bet request", + Error: err.Error(), + }) + // return fiber.NewError(fiber.StatusBadRequest, "Invalid bet request") } resp, _ := h.virtualGameSvc.ProcessBet(c.Context(), &req) @@ -179,7 +183,11 @@ func (h *Handler) GetGameList(c *fiber.Ctx) error { games, err := h.virtualGameSvc.ListGames(c.Context(), currency) if err != nil { - return fiber.NewError(fiber.StatusBadGateway, "failed to fetch games") + return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + Message: "Falied to fetch games", + Error: err.Error(), + }) + // return fiber.NewError(fiber.StatusBadGateway, "failed to fetch games") } return c.JSON(games) } diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 53878a7..ac49edc 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -259,9 +259,9 @@ func (a *App) initAppRoutes() { group.Post("/veli/start-game", a.authMiddleware, h.StartGame) group.Post("/veli/start-demo-game", a.authMiddleware, h.StartDemoGame) a.fiber.Post("/balance", h.GetBalance) - a.fiber.Post("/bet", h.PlaceBet) - a.fiber.Post("/win", h.RegisterWin) - a.fiber.Post("/cancel", h.CancelTransaction) + // a.fiber.Post("/bet", h.PlaceBet) + // a.fiber.Post("/win", h.RegisterWin) + // a.fiber.Post("/cancel", h.CancelTransaction) group.Post("/veli/gaming-activity", h.GetGamingActivity) //mongoDB logs From a48cfa4db25bbedde85ae70940136dc1a833ac45 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Mon, 30 Jun 2025 19:01:04 +0300 Subject: [PATCH 07/10] change version --- internal/web_server/routes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index abd3a55..6b98818 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -57,7 +57,7 @@ func (a *App) initAppRoutes() { a.fiber.Get("/", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{ "message": "Welcome to the FortuneBet API", - "version": "1.0dev7", + "version": "1.0dev7.5", }) }) From abbcf90febbca215aa1560bcef887f9d6dd7056e Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Mon, 30 Jun 2025 19:23:05 +0300 Subject: [PATCH 08/10] popok /win fix --- internal/services/virtualGame/service.go | 2 ++ internal/services/wallet/wallet.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/services/virtualGame/service.go b/internal/services/virtualGame/service.go index 7373fdc..cd69636 100644 --- a/internal/services/virtualGame/service.go +++ b/internal/services/virtualGame/service.go @@ -346,6 +346,8 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) ( return nil, fmt.Errorf("transaction recording failed") } + fmt.Printf("\n\n Win balance is:%v\n\n", float64(userWallets[0].Balance)/100) + return &domain.PopOKWinResponse{ TransactionID: req.TransactionID, ExternalTrxID: fmt.Sprintf("%v", tx.ID), diff --git a/internal/services/wallet/wallet.go b/internal/services/wallet/wallet.go index b1f8d61..01d4468 100644 --- a/internal/services/wallet/wallet.go +++ b/internal/services/wallet/wallet.go @@ -95,7 +95,7 @@ func (s *Service) AddToWallet( return domain.Transfer{}, err } - go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet) + // go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet) // Log the transfer here for reference newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ From 95499cea7a7debf0166e762828adf7604216bd43 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Mon, 30 Jun 2025 19:32:36 +0300 Subject: [PATCH 09/10] wallet amount fix --- internal/services/chapa/client.go | 2 +- internal/services/virtualGame/service.go | 4 ++-- internal/services/wallet/monitor/service.go | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/services/chapa/client.go b/internal/services/chapa/client.go index baac2fd..3beed5b 100644 --- a/internal/services/chapa/client.go +++ b/internal/services/chapa/client.go @@ -31,7 +31,7 @@ func NewClient(baseURL, secretKey string) *Client { func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error) { payload := map[string]interface{}{ - "amount": fmt.Sprintf("%.2f", float64(req.Amount)/100), + "amount": fmt.Sprintf("%.2f", float64(req.Amount)), "currency": req.Currency, // "email": req.Email, "first_name": req.FirstName, diff --git a/internal/services/virtualGame/service.go b/internal/services/virtualGame/service.go index cd69636..2bc4f9e 100644 --- a/internal/services/virtualGame/service.go +++ b/internal/services/virtualGame/service.go @@ -346,12 +346,12 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) ( return nil, fmt.Errorf("transaction recording failed") } - fmt.Printf("\n\n Win balance is:%v\n\n", float64(userWallets[0].Balance)/100) + fmt.Printf("\n\n Win balance is:%v\n\n", float64(userWallets[0].Balance)) return &domain.PopOKWinResponse{ TransactionID: req.TransactionID, ExternalTrxID: fmt.Sprintf("%v", tx.ID), - Balance: float64(userWallets[0].Balance) / 100, + Balance: float64(userWallets[0].Balance), }, nil } diff --git a/internal/services/wallet/monitor/service.go b/internal/services/wallet/monitor/service.go index 4f48115..e3f7bc5 100644 --- a/internal/services/wallet/monitor/service.go +++ b/internal/services/wallet/monitor/service.go @@ -209,7 +209,7 @@ func buildNotificationMessage(thresholdPercent int, currentBalance, initialDepos return fmt.Sprintf( "Company wallet balance has reached %d%% of initial deposit. Current balance: %.2f, Initial deposit: %.2f", thresholdPercent, - float64(currentBalance)/100, // Assuming currency is in cents - float64(initialDeposit)/100, + float64(currentBalance), // Assuming currency is in cents + float64(initialDeposit), ) } From 7b728abac66bdd55329bad6b09d4c32c47ce26d2 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Tue, 1 Jul 2025 12:06:04 +0300 Subject: [PATCH 10/10] popok game list browser-like header fix --- internal/services/virtualGame/service.go | 61 +++++++++++++++--------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/internal/services/virtualGame/service.go b/internal/services/virtualGame/service.go index 2bc4f9e..6f60fef 100644 --- a/internal/services/virtualGame/service.go +++ b/internal/services/virtualGame/service.go @@ -171,7 +171,7 @@ func (s *service) HandleCallback(ctx context.Context, callback *domain.PopOKCall } walletID := wallets[0].ID - amount := int64(callback.Amount * 100) // Convert to cents + amount := int64(callback.Amount) // Convert to cents transactionType := callback.Type switch transactionType { @@ -230,7 +230,7 @@ func (s *service) GetPlayerInfo(ctx context.Context, req *domain.PopOKPlayerInfo return &domain.PopOKPlayerInfoResponse{ Country: "ET", Currency: claims.Currency, - Balance: float64(wallets[0].Balance) / 100, // Convert cents to currency + Balance: float64(wallets[0].Balance), // Convert cents to currency PlayerID: fmt.Sprintf("%d", claims.UserID), }, nil } @@ -243,7 +243,7 @@ func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) ( } // Convert amount to cents (assuming wallet uses cents) - amountCents := int64(req.Amount * 100) + amountCents := int64(req.Amount) // Deduct from wallet @@ -278,7 +278,7 @@ func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) ( return &domain.PopOKBetResponse{ TransactionID: req.TransactionID, ExternalTrxID: fmt.Sprintf("%v", tx.ID), // Your internal transaction ID - Balance: float64(userWallets[0].Balance) / 100, + Balance: float64(userWallets[0].Balance), }, nil } @@ -304,7 +304,7 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) ( wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) balance := 0.0 if len(wallets) > 0 { - balance = float64(wallets[0].Balance) / 100 + balance = float64(wallets[0].Balance) } return &domain.PopOKWinResponse{ TransactionID: req.TransactionID, @@ -314,7 +314,7 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) ( } // 3. Convert amount to cents - amountCents := int64(req.Amount * 100) + amountCents := int64(req.Amount) // 4. Credit to wallet @@ -374,7 +374,7 @@ func (s *service) ProcessTournamentWin(ctx context.Context, req *domain.PopOKWin wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) balance := 0.0 if len(wallets) > 0 { - balance = float64(wallets[0].Balance) / 100 + balance = float64(wallets[0].Balance) } return &domain.PopOKWinResponse{ TransactionID: req.TransactionID, @@ -384,7 +384,7 @@ func (s *service) ProcessTournamentWin(ctx context.Context, req *domain.PopOKWin } // 3. Convert amount to cents - amountCents := int64(req.Amount * 100) + amountCents := int64(req.Amount) // 4. Credit user wallet if _, err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil { @@ -417,7 +417,7 @@ func (s *service) ProcessTournamentWin(ctx context.Context, req *domain.PopOKWin return &domain.PopOKWinResponse{ TransactionID: req.TransactionID, ExternalTrxID: fmt.Sprintf("%v", tx.ID), - Balance: float64(wallets[0].Balance) / 100, + Balance: float64(wallets[0].Balance), }, nil } @@ -438,7 +438,7 @@ func (s *service) ProcessPromoWin(ctx context.Context, req *domain.PopOKWinReque wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) balance := 0.0 if len(wallets) > 0 { - balance = float64(wallets[0].Balance) / 100 + balance = float64(wallets[0].Balance) } return &domain.PopOKWinResponse{ TransactionID: req.TransactionID, @@ -447,7 +447,7 @@ func (s *service) ProcessPromoWin(ctx context.Context, req *domain.PopOKWinReque }, nil } - amountCents := int64(req.Amount * 100) + amountCents := int64(req.Amount) if _, err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil { s.logger.Error("Failed to credit wallet for promo", "userID", claims.UserID, "error", err) @@ -477,7 +477,7 @@ func (s *service) ProcessPromoWin(ctx context.Context, req *domain.PopOKWinReque return &domain.PopOKWinResponse{ TransactionID: req.TransactionID, ExternalTrxID: fmt.Sprintf("%v", tx.ID), - Balance: float64(wallets[0].Balance) / 100, + Balance: float64(wallets[0].Balance), }, nil } @@ -543,7 +543,7 @@ func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequ wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) balance := 0.0 if len(wallets) > 0 { - balance = float64(wallets[0].Balance) / 100 + balance = float64(wallets[0].Balance) } return &domain.PopOKCancelResponse{ TransactionID: req.TransactionID, @@ -602,7 +602,7 @@ func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequ return &domain.PopOKCancelResponse{ TransactionID: req.TransactionID, ExternalTrxID: fmt.Sprintf("%v", cancelTx.ID), - Balance: float64(userWallets[0].Balance) / 100, + Balance: float64(userWallets[0].Balance), }, nil } @@ -651,32 +651,46 @@ func (s *service) GetGameCounts(ctx context.Context, filter domain.ReportFilter) func (s *service) ListGames(ctx context.Context, currency string) ([]domain.PopOKGame, error) { now := time.Now().Format("02-01-2006 15:04:05") // dd-mm-yyyy hh:mm:ss - // Calculate hash: sha256(privateKey + time) - rawHash := s.config.PopOK.SecretKey + now - hash := fmt.Sprintf("%x", sha256.Sum256([]byte(rawHash))) - - // Construct request payload - payload := map[string]interface{}{ + // Step 1: Construct payload without the hash + data := map[string]interface{}{ "action": "gameList", "platform": s.config.PopOK.Platform, "partnerId": s.config.PopOK.ClientID, "currency": currency, "time": now, - "hash": hash, } - bodyBytes, err := json.Marshal(payload) + // Step 2: Marshal data to JSON for hash calculation + // dataBytes, err := json.Marshal(data) + // if err != nil { + // s.logger.Error("Failed to marshal data for hash generation", "error", err) + // return nil, err + // } + + // Step 3: Calculate hash: sha256(privateKey + time + json(data)) + rawHash := s.config.PopOK.SecretKey + now + hash := fmt.Sprintf("%x", sha256.Sum256([]byte(rawHash))) + + // Step 4: Add the hash to the payload + data["hash"] = hash + + // Step 5: Marshal full payload with hash + bodyBytes, err := json.Marshal(data) if err != nil { - s.logger.Error("Failed to marshal game list request", "error", err) + s.logger.Error("Failed to marshal final payload with hash", "error", err) return nil, err } + // Step 6: Create and send the request req, err := http.NewRequestWithContext(ctx, "POST", s.config.PopOK.BaseURL+"/serviceApi.php", bytes.NewReader(bodyBytes)) if err != nil { s.logger.Error("Failed to create game list request", "error", err) return nil, err } req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36") + req.Header.Set("Accept", "application/json, text/plain, */*") + req.Header.Set("Accept-Language", "en-US,en;q=0.9") client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) @@ -686,6 +700,7 @@ func (s *service) ListGames(ctx context.Context, currency string) ([]domain.PopO } defer resp.Body.Close() + // Step 7: Handle response if resp.StatusCode != http.StatusOK { b, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("PopOK game list failed with status %d: %s", resp.StatusCode, string(b))