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"`
|
Bets []float64 `json:"bets"`
|
||||||
Reason string `json:"reason"` // e.g., "Based on your activity", "Popular", "Random pick"
|
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)
|
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)
|
file, err := os.Create(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create file: %w", err)
|
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) {
|
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)
|
user, err := s.store.GetUserByID(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to get user", "userID", userID, "error", err)
|
s.logger.Error("Failed to get user", "userID", userID, "error", err)
|
||||||
return "", 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(
|
token, err := jwtutil.CreatePopOKJwt(
|
||||||
userID,
|
userID,
|
||||||
user.CompanyID,
|
user.CompanyID,
|
||||||
user.FirstName,
|
user.PhoneNumber,
|
||||||
currency,
|
currency,
|
||||||
"en",
|
"en",
|
||||||
mode,
|
mode,
|
||||||
sessionId,
|
sessionID,
|
||||||
s.config.PopOK.SecretKey,
|
s.config.PopOK.SecretKey,
|
||||||
24*time.Hour,
|
24*time.Hour,
|
||||||
)
|
)
|
||||||
|
|
@ -66,9 +68,9 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record game launch as a transaction (for history and recommendation purposes)
|
// 3. Record virtual game history (optional but recommended)
|
||||||
tx := &domain.VirtualGameHistory{
|
history := &domain.VirtualGameHistory{
|
||||||
SessionID: sessionId, // Optional: populate if session tracking is implemented
|
SessionID: sessionID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
CompanyID: user.CompanyID.Value,
|
CompanyID: user.CompanyID.Value,
|
||||||
Provider: string(domain.PROVIDER_POPOK),
|
Provider: string(domain.PROVIDER_POPOK),
|
||||||
|
|
@ -76,23 +78,66 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI
|
||||||
TransactionType: "LAUNCH",
|
TransactionType: "LAUNCH",
|
||||||
Amount: 0,
|
Amount: 0,
|
||||||
Currency: currency,
|
Currency: currency,
|
||||||
ExternalTransactionID: sessionId,
|
ExternalTransactionID: sessionID,
|
||||||
Status: "COMPLETED",
|
Status: "COMPLETED",
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
if err := s.repo.CreateVirtualGameHistory(ctx, history); err != nil {
|
||||||
if err := s.repo.CreateVirtualGameHistory(ctx, tx); err != nil {
|
|
||||||
s.logger.Error("Failed to record game launch transaction", "error", err)
|
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(
|
// 4. Prepare PopOK API request
|
||||||
"partnerId=%s&gameId=%s&gameMode=%s&lang=en&platform=%s&externalToken=%s",
|
timestamp := time.Now().Format("02-01-2006 15:04:05")
|
||||||
s.config.PopOK.ClientID, gameID, mode, s.config.PopOK.Platform, token,
|
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 {
|
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
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) GenerateSignature(params string) string {
|
func generatePopOKHash(privateKey, timestamp string, data domain.PopokLaunchRequestData) (string, error) {
|
||||||
h := hmac.New(sha256.New, []byte(s.config.PopOK.SecretKey))
|
// Marshal data to JSON (compact format, like json_encode in PHP)
|
||||||
h.Write([]byte(params))
|
dataBytes, err := json.Marshal(data)
|
||||||
return hex.EncodeToString(h.Sum(nil))
|
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 {
|
func (s *service) verifySignature(callback *domain.PopOKCallback) bool {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
|
|
@ -23,16 +24,17 @@ type Client struct {
|
||||||
OperatorID string
|
OperatorID string
|
||||||
SecretKey string
|
SecretKey string
|
||||||
BrandID 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{
|
return &Client{
|
||||||
http: &http.Client{Timeout: 10 * time.Second},
|
http: &http.Client{Timeout: 10 * time.Second},
|
||||||
BaseURL: cfg.VeliGames.BaseURL,
|
BaseURL: cfg.VeliGames.BaseURL,
|
||||||
OperatorID: cfg.VeliGames.OperatorID,
|
OperatorID: cfg.VeliGames.OperatorID,
|
||||||
SecretKey: cfg.VeliGames.SecretKey,
|
SecretKey: cfg.VeliGames.SecretKey,
|
||||||
BrandID: cfg.VeliGames.BrandID,
|
BrandID: cfg.VeliGames.BrandID,
|
||||||
|
walletSvc: walletSvc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"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
|
var res domain.BetResponse
|
||||||
err := c.post(ctx, "/bet", req, sigParams, &res)
|
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
|
return &res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,6 +145,19 @@ func (c *Client) ProcessWin(ctx context.Context, req domain.WinRequest) (*domain
|
||||||
|
|
||||||
var res domain.WinResponse
|
var res domain.WinResponse
|
||||||
err := c.post(ctx, "/win", req, sigParams, &res)
|
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
|
return &res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,6 +188,18 @@ func (c *Client) ProcessCancel(ctx context.Context, req domain.CancelRequest) (*
|
||||||
|
|
||||||
var res domain.CancelResponse
|
var res domain.CancelResponse
|
||||||
err := c.post(ctx, "/cancel", req, sigParams, &res)
|
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
|
return &res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -137,16 +137,28 @@ func parseReportFilter(c *fiber.Ctx) (domain.ReportFilter, error) {
|
||||||
// @Router /api/v1/report-files/download/{filename} [get]
|
// @Router /api/v1/report-files/download/{filename} [get]
|
||||||
func (h *Handler) DownloadReportFile(c *fiber.Ctx) error {
|
func (h *Handler) DownloadReportFile(c *fiber.Ctx) error {
|
||||||
filename := c.Params("filename")
|
filename := c.Params("filename")
|
||||||
if filename == "" {
|
if filename == "" || strings.Contains(filename, "..") {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
Message: "Missing filename parameter",
|
Message: "Invalid filename parameter",
|
||||||
Error: "filename is required",
|
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) {
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
Message: "Report file not found",
|
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-Type", "text/csv")
|
||||||
c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
||||||
|
|
||||||
|
// Serve the file
|
||||||
if err := c.SendFile(filePath); err != nil {
|
if err := c.SendFile(filePath); err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
Message: "Failed to serve file",
|
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"
|
// @Failure 500 {object} domain.ErrorResponse "Failed to read report directory"
|
||||||
// @Router /api/v1/report-files/list [get]
|
// @Router /api/v1/report-files/list [get]
|
||||||
func (h *Handler) ListReportFiles(c *fiber.Ctx) error {
|
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)
|
files, err := os.ReadDir(reportDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type launchVirtualGameReq struct {
|
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"`
|
Currency string `json:"currency" validate:"required,len=3" example:"USD"`
|
||||||
Mode string `json:"mode" validate:"required,oneof=fun real" example:"real"`
|
Mode string `json:"mode" validate:"required,oneof=fun real" example:"real"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -220,8 +220,8 @@ func (a *App) initAppRoutes() {
|
||||||
|
|
||||||
//Report Routes
|
//Report Routes
|
||||||
group.Get("/reports/dashboard", h.GetDashboardReport)
|
group.Get("/reports/dashboard", h.GetDashboardReport)
|
||||||
group.Get("/report-files/download/:filename", a.authMiddleware, a.SuperAdminOnly, h.DownloadReportFile)
|
group.Get("/report-files/download/:filename", a.authMiddleware, a.OnlyAdminAndAbove, h.DownloadReportFile)
|
||||||
group.Get("/report-files/list", a.authMiddleware, a.SuperAdminOnly, h.ListReportFiles)
|
group.Get("/report-files/list", a.authMiddleware, a.OnlyAdminAndAbove, h.ListReportFiles)
|
||||||
|
|
||||||
//Wallet Monitor Service
|
//Wallet Monitor Service
|
||||||
// group.Get("/debug/wallet-monitor/status", func(c *fiber.Ctx) error {
|
// group.Get("/debug/wallet-monitor/status", func(c *fiber.Ctx) error {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user