Merge branch 'cashback'
This commit is contained in:
commit
fd55639c02
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user