CSV report fix + popok hash
This commit is contained in:
parent
bd0859d3ad
commit
77a7428b48
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user