CSV report fix + popok hash

This commit is contained in:
Yared Yemane 2025-06-29 19:51:30 +03:00
parent bd0859d3ad
commit 77a7428b48
8 changed files with 173 additions and 32 deletions

View File

@ -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"`
}

View File

@ -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)

View File

@ -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 {

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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"`
}

View File

@ -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 {