Merge branch 'cashback'

This commit is contained in:
Asher Samuel 2025-07-10 15:22:47 +03:00
commit fd55639c02
9 changed files with 251 additions and 8 deletions

View File

@ -168,6 +168,7 @@ func main() {
)
go httpserver.SetupReportCronJobs(context.Background(), reportSvc)
go httpserver.ProcessBetCashback(context.TODO(), betSvc)
bankRepository := repository.NewBankRepository(store)
instSvc := institutions.New(bankRepository)

View File

@ -57,6 +57,7 @@ CREATE TABLE IF NOT EXISTS bets (
is_shop_bet BOOLEAN NOT NULL,
outcomes_hash TEXT NOT NULL,
fast_code VARCHAR(10) NOT NULL,
processed BOOLEAN DEFAULT FALSE NOT NULL,
UNIQUE(cashout_id),
CHECK (
user_id IS NOT NULL

View File

@ -99,6 +99,11 @@ SELECT *
FROM bet_with_outcomes
WHERE fast_code = $1
LIMIT 1;
-- name: GetBetsForCashback :many
SELECT *
FROM bet_with_outcomes
WHERE status = 2
AND processed = false;
-- name: GetBetOutcomeByEventID :many
SELECT *
FROM bet_outcomes
@ -143,6 +148,10 @@ UPDATE bets
SET status = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2;
-- name: UpdateBetWithCashback :exec
UPDATE bets
SET processed = $1
WHERE id = $2;
-- name: DeleteBet :exec
DELETE FROM bets
WHERE id = $1;

View File

@ -27,7 +27,7 @@ INSERT INTO bets (
fast_code
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
RETURNING id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, fast_code
RETURNING id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, fast_code, processed
`
type CreateBetParams struct {
@ -78,6 +78,7 @@ func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, erro
&i.IsShopBet,
&i.OutcomesHash,
&i.FastCode,
&i.Processed,
)
return i, err
}
@ -119,7 +120,7 @@ func (q *Queries) DeleteBetOutcome(ctx context.Context, betID int64) error {
}
const GetAllBets = `-- name: GetAllBets :many
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, fast_code, outcomes
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, fast_code, processed, outcomes
FROM bet_with_outcomes
wHERE (
branch_id = $1
@ -196,6 +197,7 @@ func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWi
&i.IsShopBet,
&i.OutcomesHash,
&i.FastCode,
&i.Processed,
&i.Outcomes,
); err != nil {
return nil, err
@ -209,7 +211,7 @@ func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWi
}
const GetBetByBranchID = `-- name: GetBetByBranchID :many
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, fast_code, outcomes
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, fast_code, processed, outcomes
FROM bet_with_outcomes
WHERE branch_id = $1
`
@ -240,6 +242,7 @@ func (q *Queries) GetBetByBranchID(ctx context.Context, branchID pgtype.Int8) ([
&i.IsShopBet,
&i.OutcomesHash,
&i.FastCode,
&i.Processed,
&i.Outcomes,
); err != nil {
return nil, err
@ -253,7 +256,7 @@ func (q *Queries) GetBetByBranchID(ctx context.Context, branchID pgtype.Int8) ([
}
const GetBetByCashoutID = `-- name: GetBetByCashoutID :one
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, fast_code, outcomes
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, fast_code, processed, outcomes
FROM bet_with_outcomes
WHERE cashout_id = $1
`
@ -278,13 +281,14 @@ func (q *Queries) GetBetByCashoutID(ctx context.Context, cashoutID string) (BetW
&i.IsShopBet,
&i.OutcomesHash,
&i.FastCode,
&i.Processed,
&i.Outcomes,
)
return i, err
}
const GetBetByFastCode = `-- name: GetBetByFastCode :one
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, fast_code, outcomes
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, fast_code, processed, outcomes
FROM bet_with_outcomes
WHERE fast_code = $1
LIMIT 1
@ -310,13 +314,14 @@ func (q *Queries) GetBetByFastCode(ctx context.Context, fastCode string) (BetWit
&i.IsShopBet,
&i.OutcomesHash,
&i.FastCode,
&i.Processed,
&i.Outcomes,
)
return i, err
}
const GetBetByID = `-- name: GetBetByID :one
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, fast_code, outcomes
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, fast_code, processed, outcomes
FROM bet_with_outcomes
WHERE id = $1
`
@ -341,13 +346,14 @@ func (q *Queries) GetBetByID(ctx context.Context, id int64) (BetWithOutcome, err
&i.IsShopBet,
&i.OutcomesHash,
&i.FastCode,
&i.Processed,
&i.Outcomes,
)
return i, err
}
const GetBetByUserID = `-- name: GetBetByUserID :many
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, fast_code, outcomes
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, fast_code, processed, outcomes
FROM bet_with_outcomes
WHERE user_id = $1
`
@ -378,6 +384,7 @@ func (q *Queries) GetBetByUserID(ctx context.Context, userID pgtype.Int8) ([]Bet
&i.IsShopBet,
&i.OutcomesHash,
&i.FastCode,
&i.Processed,
&i.Outcomes,
); err != nil {
return nil, err
@ -393,7 +400,7 @@ func (q *Queries) GetBetByUserID(ctx context.Context, userID pgtype.Int8) ([]Bet
const GetBetCount = `-- name: GetBetCount :one
SELECT COUNT(*)
FROM bets
where user_id = $1
WHERE user_id = $1
AND outcomes_hash = $2
`
@ -505,6 +512,52 @@ func (q *Queries) GetBetOutcomeByEventID(ctx context.Context, arg GetBetOutcomeB
return items, nil
}
const GetBetsForCashback = `-- name: GetBetsForCashback :many
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, fast_code, processed, outcomes
FROM bet_with_outcomes
WHERE status = 2
AND processed = false
`
func (q *Queries) GetBetsForCashback(ctx context.Context) ([]BetWithOutcome, error) {
rows, err := q.db.Query(ctx, GetBetsForCashback)
if err != nil {
return nil, err
}
defer rows.Close()
var items []BetWithOutcome
for rows.Next() {
var i BetWithOutcome
if err := rows.Scan(
&i.ID,
&i.Amount,
&i.TotalOdds,
&i.Status,
&i.FullName,
&i.PhoneNumber,
&i.CompanyID,
&i.BranchID,
&i.UserID,
&i.CashedOut,
&i.CashoutID,
&i.CreatedAt,
&i.UpdatedAt,
&i.IsShopBet,
&i.OutcomesHash,
&i.FastCode,
&i.Processed,
&i.Outcomes,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const UpdateBetOutcomeStatus = `-- name: UpdateBetOutcomeStatus :one
UPDATE bet_outcomes
SET status = $1
@ -623,6 +676,22 @@ func (q *Queries) UpdateBetOutcomeStatusForEvent(ctx context.Context, arg Update
return items, nil
}
const UpdateBetWithCashback = `-- name: UpdateBetWithCashback :exec
UPDATE bets
SET processed = $1
WHERE id = $2
`
type UpdateBetWithCashbackParams struct {
Processed bool `json:"processed"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateBetWithCashback(ctx context.Context, arg UpdateBetWithCashbackParams) error {
_, err := q.db.Exec(ctx, UpdateBetWithCashback, arg.Processed, arg.ID)
return err
}
const UpdateCashOut = `-- name: UpdateCashOut :exec
UPDATE bets
SET cashed_out = $2,

View File

@ -90,6 +90,7 @@ type Bet struct {
IsShopBet bool `json:"is_shop_bet"`
OutcomesHash string `json:"outcomes_hash"`
FastCode string `json:"fast_code"`
Processed bool `json:"processed"`
}
type BetOutcome struct {
@ -127,6 +128,7 @@ type BetWithOutcome struct {
IsShopBet bool `json:"is_shop_bet"`
OutcomesHash string `json:"outcomes_hash"`
FastCode string `json:"fast_code"`
Processed bool `json:"processed"`
Outcomes []BetOutcome `json:"outcomes"`
}

View File

@ -293,6 +293,22 @@ func (s *Store) GetBetByFastCode(ctx context.Context, fastcode string) (domain.G
return convertDBBetWithOutcomes(bet), nil
}
func (s *Store) GetBetsForCashback(ctx context.Context) ([]domain.GetBet, error) {
bets, err := s.queries.GetBetsForCashback(ctx)
var res []domain.GetBet
if err != nil {
return nil, err
}
for _, bet := range bets {
cashbackBet := convertDBBetWithOutcomes(bet)
res = append(res, cashbackBet)
}
return res, nil
}
func (s *Store) GetBetCount(ctx context.Context, UserID int64, outcomesHash string) (int64, error) {
count, err := s.queries.GetBetCount(ctx, dbgen.GetBetCountParams{
UserID: pgtype.Int8{Int64: UserID, Valid: true},
@ -439,6 +455,24 @@ func (s *Store) UpdateBetOutcomeStatusForEvent(ctx context.Context, eventID int6
return result, nil
}
func (s *Store) UpdateBetWithCashback(ctx context.Context, betID int64, cashbackStatus bool) error {
err := s.queries.UpdateBetWithCashback(ctx, dbgen.UpdateBetWithCashbackParams{
ID: betID,
Processed: cashbackStatus,
})
if err != nil {
domain.MongoDBLogger.Error("failed to update bet outcome status for event",
zap.Int64("betID", betID),
zap.Bool("status", cashbackStatus),
zap.Error(err),
)
return err
}
return nil
}
// GetBetSummary returns aggregated bet statistics
func (s *Store) GetBetSummary(ctx context.Context, filter domain.ReportFilter) (
totalStakes domain.Currency,

View File

@ -44,4 +44,7 @@ type BetStore interface {
GetSportBetActivity(ctx context.Context, filter domain.ReportFilter) ([]domain.SportBetActivity, error)
GetSportDetails(ctx context.Context, filter domain.ReportFilter) (map[string]string, error)
GetSportMarketPopularity(ctx context.Context, filter domain.ReportFilter) (map[string]string, error)
GetBetsForCashback(ctx context.Context) ([]domain.GetBet, error)
UpdateBetWithCashback(ctx context.Context, betID int64, cashbackStatus bool) error
}

View File

@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"log/slog"
"math"
"math/big"
random "math/rand"
"sort"
@ -907,6 +908,71 @@ func (s *Service) SetBetToRemoved(ctx context.Context, id int64) error {
return nil
}
func (s *Service) ProcessBetCashback(ctx context.Context) error {
bets, err := s.betStore.GetBetsForCashback(ctx)
if err != nil {
s.mongoLogger.Error("failed to fetch bets",
zap.Error(err),
)
return err
}
for _, bet := range bets {
shouldProcess := true
loseCount := 0
for _, outcome := range bet.Outcomes {
// stop if other outcomes exists in bet outcomes
if outcome.Status != domain.OUTCOME_STATUS_LOSS && outcome.Status != domain.OUTCOME_STATUS_WIN {
shouldProcess = false
break
}
if outcome.Status == domain.OUTCOME_STATUS_LOSS {
loseCount++
// only process caseback if bet is lost by one
if loseCount > 1 {
break
}
}
}
if !shouldProcess || loseCount != 1 {
continue
}
if err := s.betStore.UpdateBetWithCashback(ctx, bet.ID, true); err != nil {
s.mongoLogger.Error("failed to process cashback for bet",
zap.Int64("betID", bet.ID),
zap.Error(err),
)
continue
}
wallets, err := s.walletSvc.GetCustomerWallet(ctx, bet.UserID.Value)
if err != nil {
s.mongoLogger.Error("failed to get wallets of a user",
zap.Int64("userID", bet.UserID.Value),
zap.Error(err),
)
continue
}
// TODO: get cashback amount cap (currently 1000) from settings in the db
cashbackAmount := math.Min(10, float64(calculateCashbackAmount(bet.Amount.Float32(), bet.TotalOdds)))
_, err = s.walletSvc.AddToWallet(ctx, wallets.StaticID, domain.ToCurrency(float32(cashbackAmount)), domain.ValidInt64{}, domain.TRANSFER_DIRECT,
domain.PaymentDetails{}, fmt.Sprintf("cashback amount of %f added to users static wallet", cashbackAmount))
if err != nil {
s.mongoLogger.Error("Failed to update wallet for user",
zap.Int64("userID", bet.UserID.Value),
zap.Error(err))
}
}
return nil
}
func generateOutcomeHash(outcomes []domain.CreateBetOutcome) (string, error) {
// should always be in the same order for producing the same hash
sort.Slice(outcomes, func(i, j int) bool {
@ -927,3 +993,29 @@ func generateOutcomeHash(outcomes []domain.CreateBetOutcome) (string, error) {
sum := sha256.Sum256([]byte(sb.String()))
return hex.EncodeToString(sum[:]), nil
}
func calculateCashbackAmount(amount, total_odds float32) float32 {
var multiplier float32
if total_odds < 18 {
multiplier = 0
} else if total_odds >= 18 && total_odds <= 35 {
multiplier = 1
} else if total_odds > 35 && total_odds <= 55 {
multiplier = 2
} else if total_odds > 55 && total_odds <= 95 {
multiplier = 3
} else if total_odds > 95 && total_odds <= 250 {
multiplier = 5
} else if total_odds > 250 && total_odds <= 450 {
multiplier = 10
} else if total_odds > 450 && total_odds <= 1000 {
multiplier = 50
} else if total_odds > 1000 && total_odds <= 2000 {
multiplier = 100
} else {
multiplier = 500
}
return amount * multiplier
}

View File

@ -8,6 +8,7 @@ import (
// "time"
betSvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/report"
@ -148,3 +149,34 @@ func SetupReportCronJobs(ctx context.Context, reportService *report.Service) {
c.Start()
log.Println("Cron jobs started for report generation service")
}
func ProcessBetCashback(ctx context.Context, betService *betSvc.Service) {
c := cron.New(cron.WithSeconds())
schedule := []struct {
spec string
task func()
}{
{
spec: "*/10 * * * * *", // 10 seconds for testing
// spec: "0 0 0 * * *", // Daily at midnight
task: func() {
log.Println("process bet cashbacks...")
if err := betService.ProcessBetCashback(ctx); err != nil {
log.Printf("Failed to process bet cashbacks: %v", err)
} else {
log.Printf("Successfully processed bet cashbacks")
}
},
},
}
for _, job := range schedule {
if _, err := c.AddFunc(job.spec, job.task); err != nil {
log.Fatalf("Failed to schedule cron job: %v", err)
}
}
c.Start()
log.Println("Cron jobs started for bet cashbacks")
}