Merge branch 'main' into ticket-bet

This commit is contained in:
Samuel Tariku 2025-07-01 14:17:46 +03:00
commit f27a75eb66
29 changed files with 535 additions and 127 deletions

View File

@ -115,6 +115,7 @@ func main() {
wallet.WalletStore(store), wallet.WalletStore(store),
wallet.TransferStore(store), wallet.TransferStore(store),
notificatioStore, notificatioStore,
notificationSvc,
logger, logger,
) )
@ -206,22 +207,22 @@ func main() {
httpserver.StartTicketCrons(*ticketSvc) httpserver.StartTicketCrons(*ticketSvc)
// Fetch companies and branches for live wallet metrics update // Fetch companies and branches for live wallet metrics update
ctx := context.Background() // ctx := context.Background()
companies := []domain.GetCompany{ // companies := []domain.GetCompany{
{ID: 1, Name: "Company A", WalletBalance: 1000.0}, // {ID: 1, Name: "Company A", WalletBalance: 1000.0},
} // }
branches := []domain.BranchWallet{ // branches := []domain.BranchWallet{
{ID: 10, Name: "Branch Z", CompanyID: 1, Balance: 500.0}, // {ID: 10, Name: "Branch Z", CompanyID: 1, Balance: 500.0},
} // }
notificationSvc.UpdateLiveWalletMetrics(ctx, companies, branches) // notificationSvc.UpdateLiveWalletMetrics(ctx, companies, branches)
if err != nil { // if err != nil {
log.Println("Failed to update live metrics:", err) // log.Println("Failed to update live metrics:", err)
} else { // } else {
log.Println("Live metrics broadcasted successfully") // log.Println("Live metrics broadcasted successfully")
} // }
issueReportingRepo := repository.NewReportedIssueRepository(store) issueReportingRepo := repository.NewReportedIssueRepository(store)

View File

@ -295,7 +295,8 @@ CREATE TABLE IF NOT EXISTS settings (
); );
CREATE TABLE bonus ( CREATE TABLE bonus (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
multiplier REAL NOT NULL multiplier REAL NOT NULL,
balance_cap BIGINT NOT NULL DEFAULT 0
); );
-- Views -- Views
CREATE VIEW companies_details AS CREATE VIEW companies_details AS

View File

@ -1,12 +1,17 @@
-- name: CreateBonusMultiplier :exec -- name: CreateBonusMultiplier :exec
INSERT INTO bonus (multiplier) INSERT INTO bonus (multiplier, balance_cap)
VALUES ($1); VALUES ($1, $2);
-- name: GetBonusMultiplier :many -- name: GetBonusMultiplier :many
SELECT id, multiplier SELECT id, multiplier
FROM bonus; FROM bonus;
-- name: GetBonusBalanceCap :many
SELECT id, balance_cap
FROM bonus;
-- name: UpdateBonusMultiplier :exec -- name: UpdateBonusMultiplier :exec
UPDATE bonus UPDATE bonus
SET multiplier = $1 SET multiplier = $1,
WHERE id = $2; balance_cap = $2
WHERE id = $3;

View File

@ -40,7 +40,6 @@ WHERE referrer_id = $1;
-- name: GetReferralSettings :one -- name: GetReferralSettings :one
SELECT * FROM referral_settings SELECT * FROM referral_settings
WHERE id = 'default'
LIMIT 1; LIMIT 1;
-- name: UpdateReferralSettings :one -- name: UpdateReferralSettings :one
@ -70,3 +69,9 @@ INSERT INTO referral_settings (
-- name: GetReferralByReferredID :one -- name: GetReferralByReferredID :one
SELECT * FROM referrals WHERE referred_id = $1 LIMIT 1; 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;

View File

@ -10,29 +10,69 @@ import (
) )
const CreateBonusMultiplier = `-- name: CreateBonusMultiplier :exec const CreateBonusMultiplier = `-- name: CreateBonusMultiplier :exec
INSERT INTO bonus (multiplier) INSERT INTO bonus (multiplier, balance_cap)
VALUES ($1) VALUES ($1, $2)
` `
func (q *Queries) CreateBonusMultiplier(ctx context.Context, multiplier float32) error { type CreateBonusMultiplierParams struct {
_, err := q.db.Exec(ctx, CreateBonusMultiplier, multiplier) 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 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 const GetBonusMultiplier = `-- name: GetBonusMultiplier :many
SELECT id, multiplier SELECT id, multiplier
FROM bonus 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) rows, err := q.db.Query(ctx, GetBonusMultiplier)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var items []Bonu var items []GetBonusMultiplierRow
for rows.Next() { for rows.Next() {
var i Bonu var i GetBonusMultiplierRow
if err := rows.Scan(&i.ID, &i.Multiplier); err != nil { if err := rows.Scan(&i.ID, &i.Multiplier); err != nil {
return nil, err return nil, err
} }
@ -46,16 +86,18 @@ func (q *Queries) GetBonusMultiplier(ctx context.Context) ([]Bonu, error) {
const UpdateBonusMultiplier = `-- name: UpdateBonusMultiplier :exec const UpdateBonusMultiplier = `-- name: UpdateBonusMultiplier :exec
UPDATE bonus UPDATE bonus
SET multiplier = $1 SET multiplier = $1,
WHERE id = $2 balance_cap = $2
WHERE id = $3
` `
type UpdateBonusMultiplierParams struct { type UpdateBonusMultiplierParams struct {
Multiplier float32 `json:"multiplier"` Multiplier float32 `json:"multiplier"`
BalanceCap int64 `json:"balance_cap"`
ID int64 `json:"id"` ID int64 `json:"id"`
} }
func (q *Queries) UpdateBonusMultiplier(ctx context.Context, arg UpdateBonusMultiplierParams) error { 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 return err
} }

View File

@ -131,6 +131,7 @@ type BetWithOutcome struct {
type Bonu struct { type Bonu struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Multiplier float32 `json:"multiplier"` Multiplier float32 `json:"multiplier"`
BalanceCap int64 `json:"balance_cap"`
} }
type Branch struct { type Branch struct {

View File

@ -102,6 +102,28 @@ func (q *Queries) CreateReferralSettings(ctx context.Context, arg CreateReferral
return i, err 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 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 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 WHERE referral_code = $1
@ -147,9 +169,19 @@ func (q *Queries) GetReferralByReferredID(ctx context.Context, referredID pgtype
return i, err 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 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 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 LIMIT 1
` `

View File

@ -51,6 +51,14 @@ type ReferralSettings struct {
Version int32 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 { type Referral struct {
ID int64 ID int64
ReferralCode string ReferralCode string

View File

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

View File

@ -6,17 +6,25 @@ import (
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
) )
func (s *Store) CreateBonusMultiplier(ctx context.Context, multiplier float32) error { func (s *Store) CreateBonusMultiplier(ctx context.Context, multiplier float32, balance_cap int64) error {
return s.queries.CreateBonusMultiplier(ctx, multiplier) 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) 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{ return s.queries.UpdateBonusMultiplier(ctx, dbgen.UpdateBonusMultiplierParams{
ID: id, ID: id,
Multiplier: mulitplier, Multiplier: mulitplier,
BalanceCap: balance_cap,
}) })
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"errors" "errors"
"fmt"
"strconv" "strconv"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
@ -20,6 +21,8 @@ type ReferralRepository interface {
UpdateSettings(ctx context.Context, settings *domain.ReferralSettings) error UpdateSettings(ctx context.Context, settings *domain.ReferralSettings) error
CreateSettings(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 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 UpdateUserReferalCode(ctx context.Context, codedata domain.UpdateUserReferalCode) error
} }
@ -145,17 +148,17 @@ func (r *ReferralRepo) UpdateSettings(ctx context.Context, settings *domain.Refe
func (r *ReferralRepo) CreateSettings(ctx context.Context, settings *domain.ReferralSettings) error { func (r *ReferralRepo) CreateSettings(ctx context.Context, settings *domain.ReferralSettings) error {
rewardAmount := pgtype.Numeric{} rewardAmount := pgtype.Numeric{}
if err := rewardAmount.Scan(settings.ReferralRewardAmount); err != nil { if err := rewardAmount.Scan(fmt.Sprintf("%f", settings.ReferralRewardAmount)); err != nil {
return err return err
} }
cashbackPercentage := pgtype.Numeric{} cashbackPercentage := pgtype.Numeric{}
if err := cashbackPercentage.Scan(settings.CashbackPercentage); err != nil { if err := cashbackPercentage.Scan(fmt.Sprintf("%f", settings.CashbackPercentage)); err != nil {
return err return err
} }
betReferralBonusPercentage := pgtype.Numeric{} betReferralBonusPercentage := pgtype.Numeric{}
if err := betReferralBonusPercentage.Scan(settings.BetReferralBonusPercentage); err != nil { if err := betReferralBonusPercentage.Scan(fmt.Sprintf("%f", settings.BetReferralBonusPercentage)); err != nil {
return err return err
} }
@ -183,6 +186,30 @@ func (r *ReferralRepo) GetReferralByReferredID(ctx context.Context, referredID s
return r.mapToDomainReferral(&dbReferral), nil 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 { func (r *ReferralRepo) mapToDomainReferral(dbRef *dbgen.Referral) *domain.Referral {
var referredID *string var referredID *string
if dbRef.ReferredID.Valid { if dbRef.ReferredID.Valid {

View File

@ -7,7 +7,8 @@ import (
) )
type BonusStore interface { type BonusStore interface {
CreateBonusMultiplier(ctx context.Context, multiplier float32) error CreateBonusMultiplier(ctx context.Context, multiplier float32, balance_cap int64) error
GetBonusMultiplier(ctx context.Context) ([]dbgen.Bonu, error) GetBonusMultiplier(ctx context.Context) ([]dbgen.GetBonusMultiplierRow, error)
UpdateBonusMultiplier(ctx context.Context, id int64, mulitplier float32) error GetBonusBalanceCap(ctx context.Context) ([]dbgen.GetBonusBalanceCapRow, error)
UpdateBonusMultiplier(ctx context.Context, id int64, mulitplier float32, balance_cap int64) error
} }

View File

@ -16,14 +16,18 @@ func NewService(bonusStore BonusStore) *Service {
} }
} }
func (s *Service) CreateBonusMultiplier(ctx context.Context, multiplier float32) error { func (s *Service) CreateBonusMultiplier(ctx context.Context, multiplier float32, balance_cap int64) error {
return s.bonusStore.CreateBonusMultiplier(ctx, multiplier) 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) return s.bonusStore.GetBonusMultiplier(ctx)
} }
func (s *Service) UpdateBonusMultiplier(ctx context.Context, id int64, mulitplier float32) error { func (s *Service) GetBonusBalanceCap(ctx context.Context) ([]dbgen.GetBonusBalanceCapRow, error) {
return s.bonusStore.UpdateBonusMultiplier(ctx, id, mulitplier) 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)
} }

View File

@ -31,7 +31,7 @@ func NewClient(baseURL, secretKey string) *Client {
func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error) { func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error) {
payload := map[string]interface{}{ payload := map[string]interface{}{
"amount": fmt.Sprintf("%.2f", float64(req.Amount)/100), "amount": fmt.Sprintf("%.2f", float64(req.Amount)),
"currency": req.Currency, "currency": req.Currency,
// "email": req.Email, // "email": req.Email,
"first_name": req.FirstName, "first_name": req.FirstName,

View File

@ -12,7 +12,9 @@ type ReferralStore interface {
ProcessReferral(ctx context.Context, referredID, referralCode string) error ProcessReferral(ctx context.Context, referredID, referralCode string) error
ProcessDepositBonus(ctx context.Context, userID string, amount float64) error ProcessDepositBonus(ctx context.Context, userID string, amount float64) error
GetReferralStats(ctx context.Context, userID string) (*domain.ReferralStats, 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 UpdateReferralSettings(ctx context.Context, settings *domain.ReferralSettings) error
GetReferralSettings(ctx context.Context) (*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 ProcessBetReferral(ctx context.Context, userPhone string, betAmount float64) error
} }

View File

@ -54,16 +54,45 @@ func (s *Service) GenerateReferralCode() (string, error) {
func (s *Service) CreateReferral(ctx context.Context, userID int64) error { func (s *Service) CreateReferral(ctx context.Context, userID int64) error {
s.logger.Info("Creating referral code for user", "userID", userID) 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.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
}
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
}
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() code, err := s.GenerateReferralCode()
if err != nil { if err != nil {
s.logger.Error("Failed to generate referral code", "error", err) s.logger.Error("Failed to generate referral code", "error", err)
return err return err
} }
// TODO: get the referral settings from db var rewardAmount float64 = settings.ReferralRewardAmount
var rewardAmount float64 = 100 var expireDuration time.Time = time.Now().Add(time.Duration((24 * settings.ExpiresAfterDays)) * time.Hour)
var expireDuration time.Time = time.Now().Add(24 * time.Hour)
if err := s.repo.CreateReferral(ctx, &domain.Referral{ if err := s.repo.CreateReferral(ctx, &domain.Referral{
ReferralCode: code, ReferralCode: code,
@ -249,6 +278,26 @@ func (s *Service) GetReferralStats(ctx context.Context, userPhone string) (*doma
return stats, nil 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 { func (s *Service) UpdateReferralSettings(ctx context.Context, settings *domain.ReferralSettings) error {
s.logger.Info("Updating referral settings", "settingsID", settings.ID) s.logger.Info("Updating referral settings", "settingsID", settings.ID)
@ -272,6 +321,16 @@ func (s *Service) GetReferralSettings(ctx context.Context) (*domain.ReferralSett
return nil, err 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 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
}

View File

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

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) { 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 {
@ -126,7 +171,7 @@ func (s *service) HandleCallback(ctx context.Context, callback *domain.PopOKCall
} }
walletID := wallets[0].ID walletID := wallets[0].ID
amount := int64(callback.Amount * 100) // Convert to cents amount := int64(callback.Amount) // Convert to cents
transactionType := callback.Type transactionType := callback.Type
switch transactionType { switch transactionType {
@ -187,7 +232,7 @@ func (s *service) GetPlayerInfo(ctx context.Context, req *domain.PopOKPlayerInfo
return &domain.PopOKPlayerInfoResponse{ return &domain.PopOKPlayerInfoResponse{
Country: "ET", Country: "ET",
Currency: claims.Currency, 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), PlayerID: fmt.Sprintf("%d", claims.UserID),
}, nil }, nil
} }
@ -200,7 +245,7 @@ func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) (
} }
// Convert amount to cents (assuming wallet uses cents) // Convert amount to cents (assuming wallet uses cents)
amountCents := int64(req.Amount * 100) amountCents := int64(req.Amount)
// Deduct from wallet // Deduct from wallet
@ -237,7 +282,7 @@ func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) (
return &domain.PopOKBetResponse{ return &domain.PopOKBetResponse{
TransactionID: req.TransactionID, TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", tx.ID), // Your internal transaction ID ExternalTrxID: fmt.Sprintf("%v", tx.ID), // Your internal transaction ID
Balance: float64(userWallets[0].Balance) / 100, Balance: float64(userWallets[0].Balance),
}, nil }, nil
} }
@ -263,7 +308,7 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (
wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
balance := 0.0 balance := 0.0
if len(wallets) > 0 { if len(wallets) > 0 {
balance = float64(wallets[0].Balance) / 100 balance = float64(wallets[0].Balance)
} }
return &domain.PopOKWinResponse{ return &domain.PopOKWinResponse{
TransactionID: req.TransactionID, TransactionID: req.TransactionID,
@ -273,7 +318,7 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (
} }
// 3. Convert amount to cents // 3. Convert amount to cents
amountCents := int64(req.Amount * 100) amountCents := int64(req.Amount)
// 4. Credit to wallet // 4. Credit to wallet
_, err = s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{}, _, err = s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{},
@ -308,10 +353,12 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (
return nil, fmt.Errorf("transaction recording failed") return nil, fmt.Errorf("transaction recording failed")
} }
fmt.Printf("\n\n Win balance is:%v\n\n", float64(userWallets[0].Balance))
return &domain.PopOKWinResponse{ return &domain.PopOKWinResponse{
TransactionID: req.TransactionID, TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", tx.ID), ExternalTrxID: fmt.Sprintf("%v", tx.ID),
Balance: float64(userWallets[0].Balance) / 100, Balance: float64(userWallets[0].Balance),
}, nil }, nil
} }
@ -334,7 +381,7 @@ func (s *service) ProcessTournamentWin(ctx context.Context, req *domain.PopOKWin
wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
balance := 0.0 balance := 0.0
if len(wallets) > 0 { if len(wallets) > 0 {
balance = float64(wallets[0].Balance) / 100 balance = float64(wallets[0].Balance)
} }
return &domain.PopOKWinResponse{ return &domain.PopOKWinResponse{
TransactionID: req.TransactionID, TransactionID: req.TransactionID,
@ -344,7 +391,7 @@ func (s *service) ProcessTournamentWin(ctx context.Context, req *domain.PopOKWin
} }
// 3. Convert amount to cents // 3. Convert amount to cents
amountCents := int64(req.Amount * 100) amountCents := int64(req.Amount)
// 4. Credit user wallet // 4. Credit user wallet
_, err = s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, _, err = s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{},
@ -379,7 +426,7 @@ func (s *service) ProcessTournamentWin(ctx context.Context, req *domain.PopOKWin
return &domain.PopOKWinResponse{ return &domain.PopOKWinResponse{
TransactionID: req.TransactionID, TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", tx.ID), ExternalTrxID: fmt.Sprintf("%v", tx.ID),
Balance: float64(wallets[0].Balance) / 100, Balance: float64(wallets[0].Balance),
}, nil }, nil
} }
@ -400,7 +447,7 @@ func (s *service) ProcessPromoWin(ctx context.Context, req *domain.PopOKWinReque
wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
balance := 0.0 balance := 0.0
if len(wallets) > 0 { if len(wallets) > 0 {
balance = float64(wallets[0].Balance) / 100 balance = float64(wallets[0].Balance)
} }
return &domain.PopOKWinResponse{ return &domain.PopOKWinResponse{
TransactionID: req.TransactionID, TransactionID: req.TransactionID,
@ -440,7 +487,7 @@ func (s *service) ProcessPromoWin(ctx context.Context, req *domain.PopOKWinReque
return &domain.PopOKWinResponse{ return &domain.PopOKWinResponse{
TransactionID: req.TransactionID, TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", tx.ID), ExternalTrxID: fmt.Sprintf("%v", tx.ID),
Balance: float64(wallets[0].Balance) / 100, Balance: float64(wallets[0].Balance),
}, nil }, nil
} }
@ -506,7 +553,7 @@ func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequ
wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
balance := 0.0 balance := 0.0
if len(wallets) > 0 { if len(wallets) > 0 {
balance = float64(wallets[0].Balance) / 100 balance = float64(wallets[0].Balance)
} }
return &domain.PopOKCancelResponse{ return &domain.PopOKCancelResponse{
TransactionID: req.TransactionID, TransactionID: req.TransactionID,
@ -567,14 +614,23 @@ func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequ
return &domain.PopOKCancelResponse{ return &domain.PopOKCancelResponse{
TransactionID: req.TransactionID, TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", cancelTx.ID), ExternalTrxID: fmt.Sprintf("%v", cancelTx.ID),
Balance: float64(userWallets[0].Balance) / 100, Balance: float64(userWallets[0].Balance),
}, 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 {
@ -607,32 +663,46 @@ func (s *service) GetGameCounts(ctx context.Context, filter domain.ReportFilter)
func (s *service) ListGames(ctx context.Context, currency string) ([]domain.PopOKGame, error) { 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 now := time.Now().Format("02-01-2006 15:04:05") // dd-mm-yyyy hh:mm:ss
// Calculate hash: sha256(privateKey + time) // Step 1: Construct payload without the hash
rawHash := s.config.PopOK.SecretKey + now data := map[string]interface{}{
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(rawHash)))
// Construct request payload
payload := map[string]interface{}{
"action": "gameList", "action": "gameList",
"platform": s.config.PopOK.Platform, "platform": s.config.PopOK.Platform,
"partnerId": s.config.PopOK.ClientID, "partnerId": s.config.PopOK.ClientID,
"currency": currency, "currency": currency,
"time": now, "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 { 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 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)) req, err := http.NewRequestWithContext(ctx, "POST", s.config.PopOK.BaseURL+"/serviceApi.php", bytes.NewReader(bodyBytes))
if err != nil { if err != nil {
s.logger.Error("Failed to create game list request", "error", err) s.logger.Error("Failed to create game list request", "error", err)
return nil, err return nil, err
} }
req.Header.Set("Content-Type", "application/json") 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} client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req) resp, err := client.Do(req)
@ -642,6 +712,7 @@ func (s *service) ListGames(ctx context.Context, currency string) ([]domain.PopO
} }
defer resp.Body.Close() defer resp.Body.Close()
// Step 7: Handle response
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body) b, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("PopOK game list failed with status %d: %s", resp.StatusCode, string(b)) return nil, fmt.Errorf("PopOK game list failed with status %d: %s", resp.StatusCode, string(b))

View File

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

View File

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

View File

@ -209,7 +209,7 @@ func buildNotificationMessage(thresholdPercent int, currentBalance, initialDepos
return fmt.Sprintf( return fmt.Sprintf(
"Company wallet balance has reached %d%% of initial deposit. Current balance: %.2f, Initial deposit: %.2f", "Company wallet balance has reached %d%% of initial deposit. Current balance: %.2f, Initial deposit: %.2f",
thresholdPercent, thresholdPercent,
float64(currentBalance)/100, // Assuming currency is in cents float64(currentBalance), // Assuming currency is in cents
float64(initialDeposit)/100, float64(initialDeposit),
) )
} }

View File

@ -14,11 +14,12 @@ type Service struct {
logger *slog.Logger 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{ return &Service{
walletStore: walletStore, walletStore: walletStore,
transferStore: transferStore, transferStore: transferStore,
notificationStore: notificationStore, notificationStore: notificationStore,
notificationSvc: notificationSvc,
logger: logger, logger: logger,
} }
} }

View File

@ -95,7 +95,7 @@ func (s *Service) AddToWallet(
return domain.Transfer{}, err return domain.Transfer{}, err
} }
go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet) // go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet)
// Log the transfer here for reference // Log the transfer here for reference
newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{
@ -135,7 +135,7 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.
return domain.Transfer{}, nil return domain.Transfer{}, nil
} }
go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet) // go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet)
// Log the transfer here for reference // Log the transfer here for reference
newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{

View File

@ -8,6 +8,7 @@ import (
func (h *Handler) CreateBonusMultiplier(c *fiber.Ctx) error { func (h *Handler) CreateBonusMultiplier(c *fiber.Ctx) error {
var req struct { var req struct {
Multiplier float32 `json:"multiplier"` Multiplier float32 `json:"multiplier"`
BalanceCap int64 `json:"balance_cap"`
} }
if err := c.BodyParser(&req); err != nil { 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) 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) h.logger.Error("failed to create bonus multiplier", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "failed to create bonus mulitplier", nil, nil) 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 { var req struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Multiplier float32 `json:"multiplier"` Multiplier float32 `json:"multiplier"`
BalanceCap int64 `json:"balance_cap"`
} }
if err := c.BodyParser(&req); err != nil { 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) 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) h.logger.Error("failed to update bonus multiplier", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "failed to update bonus mulitplier", nil, nil) return response.WriteJSON(c, fiber.StatusInternalServerError, "failed to update bonus mulitplier", nil, nil)
} }

View File

@ -2,6 +2,7 @@ package handlers
import ( import (
"fmt" "fmt"
"math"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@ -66,8 +67,16 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error {
multiplier = bonusMultiplier[0].Multiplier 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
fmt.Sprintf("Added %v to static wallet because of deposit bonus using multiplier %v", float32(amount)*multiplier, multiplier), 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{},
fmt.Sprintf("Added %v to static wallet because of deposit bonus using multiplier %v", capedBalanceAmount, multiplier),
) )
if err != nil { if err != nil {
h.logger.Error("Failed to add bonus to static wallet", "walletID", wallet.StaticID, "user id", userID, "error", err) h.logger.Error("Failed to add bonus to static wallet", "walletID", wallet.StaticID, "user id", userID, "error", err)

View File

@ -21,6 +21,38 @@ func (h *Handler) CreateReferralCode(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusOK, "Referral created successfully", nil, nil) 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 // GetReferralStats godoc
// @Summary Get referral statistics // @Summary Get referral statistics
// @Description Retrieves referral statistics for the authenticated user // @Description Retrieves referral statistics for the authenticated user
@ -112,11 +144,12 @@ func (h *Handler) UpdateReferralSettings(c *fiber.Ctx) error {
// @Security Bearer // @Security Bearer
// @Router /referral/settings [get] // @Router /referral/settings [get]
func (h *Handler) GetReferralSettings(c *fiber.Ctx) error { func (h *Handler) GetReferralSettings(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(int64) // userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 { // if !ok || userID == 0 {
h.logger.Error("Invalid user ID in context") // h.logger.Error("Invalid user ID in context")
return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification") // return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification")
} // }
userID := int64(2)
user, err := h.userSvc.GetUserByID(c.Context(), userID) user, err := h.userSvc.GetUserByID(c.Context(), userID)
if err != nil { if err != nil {

View File

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

View File

@ -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"`
} }
@ -108,7 +108,11 @@ func (h *Handler) HandlePlayerInfo(c *fiber.Ctx) error {
func (h *Handler) HandleBet(c *fiber.Ctx) error { func (h *Handler) HandleBet(c *fiber.Ctx) error {
var req domain.PopOKBetRequest var req domain.PopOKBetRequest
if err := c.BodyParser(&req); err != nil { 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) 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) games, err := h.virtualGameSvc.ListGames(c.Context(), currency)
if err != nil { 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) return c.JSON(games)
} }

View File

@ -57,7 +57,7 @@ func (a *App) initAppRoutes() {
a.fiber.Get("/", func(c *fiber.Ctx) error { a.fiber.Get("/", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{ return c.JSON(fiber.Map{
"message": "Welcome to the FortuneBet API", "message": "Welcome to the FortuneBet API",
"version": "1.0dev7", "version": "1.0dev7.5",
}) })
}) })
@ -109,7 +109,8 @@ func (a *App) initAppRoutes() {
// Referral Routes // Referral Routes
a.fiber.Post("/referral/create", a.authMiddleware, h.CreateReferralCode) a.fiber.Post("/referral/create", a.authMiddleware, h.CreateReferralCode)
a.fiber.Get("/referral/stats", a.authMiddleware, h.GetReferralStats) 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) a.fiber.Patch("/referral/settings", a.authMiddleware, h.UpdateReferralSettings)
// Bonus Routes // Bonus Routes
@ -226,8 +227,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 {
@ -259,9 +260,9 @@ func (a *App) initAppRoutes() {
group.Post("/veli/start-game", a.authMiddleware, h.StartGame) group.Post("/veli/start-game", a.authMiddleware, h.StartGame)
group.Post("/veli/start-demo-game", a.authMiddleware, h.StartDemoGame) group.Post("/veli/start-demo-game", a.authMiddleware, h.StartDemoGame)
a.fiber.Post("/balance", h.GetBalance) a.fiber.Post("/balance", h.GetBalance)
a.fiber.Post("/bet", h.PlaceBet) // a.fiber.Post("/bet", h.PlaceBet)
a.fiber.Post("/win", h.RegisterWin) // a.fiber.Post("/win", h.RegisterWin)
a.fiber.Post("/cancel", h.CancelTransaction) // a.fiber.Post("/cancel", h.CancelTransaction)
group.Post("/veli/gaming-activity", h.GetGamingActivity) group.Post("/veli/gaming-activity", h.GetGamingActivity)
//mongoDB logs //mongoDB logs