From 77a7428b48f1a202585bf4fb8b006d6b31569378 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Sun, 29 Jun 2025 19:51:30 +0300 Subject: [PATCH] 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 {