846 lines
25 KiB
Go
846 lines
25 KiB
Go
package bet
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"math/big"
|
|
random "math/rand"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
var (
|
|
ErrNoEventsAvailable = errors.New("Not enough events available with the given filters")
|
|
ErrGenerateRandomOutcome = errors.New("Failed to generate any random outcome for events")
|
|
ErrOutcomesNotCompleted = errors.New("Some bet outcomes are still pending")
|
|
ErrEventHasBeenRemoved = errors.New("Event has been removed")
|
|
)
|
|
|
|
type Service struct {
|
|
betStore BetStore
|
|
eventSvc event.Service
|
|
prematchSvc odds.ServiceImpl
|
|
walletSvc wallet.Service
|
|
branchSvc branch.Service
|
|
logger *slog.Logger
|
|
mongoLogger *zap.Logger
|
|
}
|
|
|
|
func NewService(betStore BetStore, eventSvc event.Service, prematchSvc odds.ServiceImpl, walletSvc wallet.Service, branchSvc branch.Service, logger *slog.Logger, mongoLogger *zap.Logger) *Service {
|
|
return &Service{
|
|
betStore: betStore,
|
|
eventSvc: eventSvc,
|
|
prematchSvc: prematchSvc,
|
|
walletSvc: walletSvc,
|
|
branchSvc: branchSvc,
|
|
logger: logger,
|
|
mongoLogger: mongoLogger,
|
|
}
|
|
}
|
|
|
|
var (
|
|
ErrEventHasNotEnded = errors.New("Event has not ended yet")
|
|
ErrRawOddInvalid = errors.New("Prematch Raw Odd is Invalid")
|
|
ErrBranchIDRequired = errors.New("Branch ID required for this role")
|
|
ErrOutcomeLimit = errors.New("Too many outcomes on a single bet")
|
|
)
|
|
|
|
func (s *Service) GenerateCashoutID() (string, error) {
|
|
const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
|
|
const length int = 13
|
|
charLen := big.NewInt(int64(len(chars)))
|
|
result := make([]byte, length)
|
|
|
|
for i := 0; i < length; i++ {
|
|
index, err := rand.Int(rand.Reader, charLen)
|
|
if err != nil {
|
|
s.mongoLogger.Error("failed to generate random index for cashout ID",
|
|
zap.Int("position", i),
|
|
zap.Error(err),
|
|
)
|
|
return "", err
|
|
}
|
|
result[i] = chars[index.Int64()]
|
|
}
|
|
|
|
return string(result), nil
|
|
}
|
|
|
|
func (s *Service) GenerateBetOutcome(ctx context.Context, eventID int64, marketID int64, oddID int64) (domain.CreateBetOutcome, error) {
|
|
eventIDStr := strconv.FormatInt(eventID, 10)
|
|
marketIDStr := strconv.FormatInt(marketID, 10)
|
|
oddIDStr := strconv.FormatInt(oddID, 10)
|
|
|
|
event, err := s.eventSvc.GetUpcomingEventByID(ctx, eventIDStr)
|
|
if err != nil {
|
|
s.mongoLogger.Error("failed to fetch upcoming event by ID",
|
|
zap.Int64("event_id", eventID),
|
|
zap.Error(err),
|
|
)
|
|
return domain.CreateBetOutcome{}, ErrEventHasBeenRemoved
|
|
}
|
|
|
|
currentTime := time.Now()
|
|
if event.StartTime.Before(currentTime) {
|
|
s.mongoLogger.Error("event has already started",
|
|
zap.Int64("event_id", eventID),
|
|
zap.Time("event_start_time", event.StartTime),
|
|
zap.Time("current_time", currentTime),
|
|
)
|
|
return domain.CreateBetOutcome{}, ErrEventHasNotEnded
|
|
}
|
|
|
|
odds, err := s.prematchSvc.GetRawOddsByMarketID(ctx, marketIDStr, eventIDStr)
|
|
if err != nil {
|
|
s.mongoLogger.Error("failed to get raw odds by market ID",
|
|
zap.Int64("event_id", eventID),
|
|
zap.Int64("market_id", marketID),
|
|
zap.Error(err),
|
|
)
|
|
return domain.CreateBetOutcome{}, err
|
|
}
|
|
|
|
type rawOddType struct {
|
|
ID string
|
|
Name string
|
|
Odds string
|
|
Header string
|
|
Handicap string
|
|
}
|
|
|
|
var selectedOdd rawOddType
|
|
var isOddFound bool
|
|
|
|
for _, raw := range odds.RawOdds {
|
|
var rawOdd rawOddType
|
|
rawBytes, err := json.Marshal(raw)
|
|
if err != nil {
|
|
s.mongoLogger.Error("failed to marshal raw odd",
|
|
zap.Any("raw", raw),
|
|
zap.Error(err),
|
|
)
|
|
continue
|
|
}
|
|
err = json.Unmarshal(rawBytes, &rawOdd)
|
|
if err != nil {
|
|
s.mongoLogger.Error("failed to unmarshal raw odd",
|
|
zap.ByteString("raw_bytes", rawBytes),
|
|
zap.Error(err),
|
|
)
|
|
continue
|
|
}
|
|
if rawOdd.ID == oddIDStr {
|
|
selectedOdd = rawOdd
|
|
isOddFound = true
|
|
break
|
|
}
|
|
}
|
|
if !isOddFound {
|
|
s.mongoLogger.Error("odd ID not found in raw odds",
|
|
zap.Int64("odd_id", oddID),
|
|
zap.Int64("market_id", marketID),
|
|
zap.Int64("event_id", eventID),
|
|
)
|
|
return domain.CreateBetOutcome{}, ErrRawOddInvalid
|
|
}
|
|
|
|
parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32)
|
|
if err != nil {
|
|
s.mongoLogger.Error("failed to parse selected odd value",
|
|
zap.String("odd", selectedOdd.Odds),
|
|
zap.Int64("odd_id", oddID),
|
|
zap.Error(err),
|
|
)
|
|
return domain.CreateBetOutcome{}, err
|
|
}
|
|
|
|
newOutcome := domain.CreateBetOutcome{
|
|
EventID: eventID,
|
|
OddID: oddID,
|
|
MarketID: marketID,
|
|
SportID: int64(event.SportID),
|
|
HomeTeamName: event.HomeTeam,
|
|
AwayTeamName: event.AwayTeam,
|
|
MarketName: odds.MarketName,
|
|
Odd: float32(parsedOdd),
|
|
OddName: selectedOdd.Name,
|
|
OddHeader: selectedOdd.Header,
|
|
OddHandicap: selectedOdd.Handicap,
|
|
Expires: event.StartTime,
|
|
}
|
|
|
|
return newOutcome, nil
|
|
}
|
|
|
|
func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID int64, role domain.Role) (domain.CreateBetRes, error) {
|
|
if len(req.Outcomes) > 30 {
|
|
s.mongoLogger.Error("too many outcomes",
|
|
zap.Int("count", len(req.Outcomes)),
|
|
zap.Int64("user_id", userID),
|
|
)
|
|
return domain.CreateBetRes{}, ErrOutcomeLimit
|
|
}
|
|
|
|
var outcomes []domain.CreateBetOutcome = make([]domain.CreateBetOutcome, 0, len(req.Outcomes))
|
|
var totalOdds float32 = 1
|
|
|
|
for _, outcomeReq := range req.Outcomes {
|
|
fmt.Println("reqq: ", outcomeReq)
|
|
newOutcome, err := s.GenerateBetOutcome(ctx, outcomeReq.EventID, outcomeReq.MarketID, outcomeReq.OddID)
|
|
if err != nil {
|
|
s.mongoLogger.Error("failed to generate outcome",
|
|
zap.Int64("event_id", outcomeReq.EventID),
|
|
zap.Int64("market_id", outcomeReq.MarketID),
|
|
zap.Int64("odd_id", outcomeReq.OddID),
|
|
zap.Int64("user_id", userID),
|
|
zap.Error(err),
|
|
)
|
|
return domain.CreateBetRes{}, err
|
|
}
|
|
totalOdds *= float32(newOutcome.Odd)
|
|
outcomes = append(outcomes, newOutcome)
|
|
}
|
|
|
|
outcomesHash, err := generateOutcomeHash(outcomes)
|
|
if err != nil {
|
|
s.mongoLogger.Error("failed to generate outcome hash",
|
|
zap.Int64("user_id", userID),
|
|
zap.Error(err),
|
|
)
|
|
return domain.CreateBetRes{}, err
|
|
}
|
|
|
|
count, err := s.GetBetCount(ctx, userID, outcomesHash)
|
|
if err != nil {
|
|
return domain.CreateBetRes{}, err
|
|
}
|
|
if count == 2 {
|
|
return domain.CreateBetRes{}, fmt.Errorf("bet already pleaced twice")
|
|
}
|
|
|
|
cashoutID, err := s.GenerateCashoutID()
|
|
if err != nil {
|
|
s.mongoLogger.Error("failed to generate cashout ID",
|
|
zap.Int64("user_id", userID),
|
|
zap.Error(err),
|
|
)
|
|
return domain.CreateBetRes{}, err
|
|
}
|
|
|
|
newBet := domain.CreateBet{
|
|
Amount: domain.ToCurrency(req.Amount),
|
|
TotalOdds: totalOdds,
|
|
Status: req.Status,
|
|
FullName: req.FullName,
|
|
PhoneNumber: req.PhoneNumber,
|
|
CashoutID: cashoutID,
|
|
OutcomesHash: outcomesHash,
|
|
}
|
|
|
|
switch role {
|
|
case domain.RoleCashier:
|
|
branch, err := s.branchSvc.GetBranchByCashier(ctx, userID)
|
|
if err != nil {
|
|
s.mongoLogger.Error("failed to get branch by cashier",
|
|
zap.Int64("user_id", userID),
|
|
zap.Error(err),
|
|
)
|
|
return domain.CreateBetRes{}, err
|
|
}
|
|
|
|
deductedAmount := req.Amount / 10
|
|
_, err = s.walletSvc.DeductFromWallet(ctx,
|
|
branch.WalletID, domain.ToCurrency(deductedAmount), domain.BranchWalletType, domain.ValidInt64{
|
|
Value: userID,
|
|
Valid: true,
|
|
}, domain.TRANSFER_DIRECT)
|
|
if err != nil {
|
|
s.mongoLogger.Error("failed to deduct from wallet",
|
|
zap.Int64("wallet_id", branch.WalletID),
|
|
zap.Float32("amount", deductedAmount),
|
|
zap.Error(err),
|
|
)
|
|
return domain.CreateBetRes{}, err
|
|
}
|
|
|
|
newBet.BranchID = domain.ValidInt64{Value: branch.ID, Valid: true}
|
|
newBet.CompanyID = domain.ValidInt64{Value: branch.CompanyID, Valid: true}
|
|
newBet.UserID = domain.ValidInt64{Value: userID, Valid: true}
|
|
newBet.IsShopBet = true
|
|
|
|
case domain.RoleBranchManager, domain.RoleAdmin, domain.RoleSuperAdmin:
|
|
if req.BranchID == nil {
|
|
s.mongoLogger.Error("branch ID required for admin/manager",
|
|
zap.Int64("user_id", userID),
|
|
)
|
|
return domain.CreateBetRes{}, ErrBranchIDRequired
|
|
}
|
|
|
|
branch, err := s.branchSvc.GetBranchByID(ctx, *req.BranchID)
|
|
if err != nil {
|
|
s.mongoLogger.Error("failed to get branch by ID",
|
|
zap.Int64("branch_id", *req.BranchID),
|
|
zap.Error(err),
|
|
)
|
|
return domain.CreateBetRes{}, err
|
|
}
|
|
|
|
deductedAmount := req.Amount / 10
|
|
_, err = s.walletSvc.DeductFromWallet(ctx, branch.WalletID, domain.ToCurrency(deductedAmount), domain.BranchWalletType, domain.ValidInt64{
|
|
Value: userID,
|
|
Valid: true,
|
|
}, domain.TRANSFER_DIRECT)
|
|
if err != nil {
|
|
s.mongoLogger.Error("wallet deduction failed",
|
|
zap.Int64("wallet_id", branch.WalletID),
|
|
zap.Float32("amount", deductedAmount),
|
|
zap.Error(err),
|
|
)
|
|
return domain.CreateBetRes{}, err
|
|
}
|
|
|
|
newBet.BranchID = domain.ValidInt64{Value: branch.ID, Valid: true}
|
|
newBet.CompanyID = domain.ValidInt64{Value: branch.CompanyID, Valid: true}
|
|
newBet.UserID = domain.ValidInt64{Value: userID, Valid: true}
|
|
newBet.IsShopBet = true
|
|
|
|
case domain.RoleCustomer:
|
|
wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID)
|
|
if err != nil {
|
|
s.mongoLogger.Error("failed to get customer wallets",
|
|
zap.Int64("user_id", userID),
|
|
zap.Error(err),
|
|
)
|
|
return domain.CreateBetRes{}, err
|
|
}
|
|
|
|
userWallet := wallets[0]
|
|
_, err = s.walletSvc.DeductFromWallet(ctx, userWallet.ID,
|
|
domain.ToCurrency(req.Amount), domain.CustomerWalletType, domain.ValidInt64{}, domain.TRANSFER_DIRECT)
|
|
if err != nil {
|
|
s.mongoLogger.Error("wallet deduction failed for customer",
|
|
zap.Int64("wallet_id", userWallet.ID),
|
|
zap.Float32("amount", req.Amount),
|
|
zap.Error(err),
|
|
)
|
|
return domain.CreateBetRes{}, err
|
|
}
|
|
|
|
newBet.UserID = domain.ValidInt64{Value: userID, Valid: true}
|
|
newBet.IsShopBet = false
|
|
|
|
default:
|
|
s.mongoLogger.Error("unknown role type",
|
|
zap.String("role", string(role)),
|
|
zap.Int64("user_id", userID),
|
|
)
|
|
return domain.CreateBetRes{}, fmt.Errorf("Unknown Role Type")
|
|
}
|
|
|
|
fmt.Println("Bet is: ", newBet)
|
|
bet, err := s.CreateBet(ctx, newBet)
|
|
if err != nil {
|
|
s.mongoLogger.Error("failed to create bet",
|
|
zap.Int64("user_id", userID),
|
|
zap.Error(err),
|
|
)
|
|
return domain.CreateBetRes{}, err
|
|
}
|
|
|
|
for i := range outcomes {
|
|
outcomes[i].BetID = bet.ID
|
|
}
|
|
rows, err := s.betStore.CreateBetOutcome(ctx, outcomes)
|
|
if err != nil {
|
|
s.mongoLogger.Error("failed to create bet outcomes",
|
|
zap.Int64("bet_id", bet.ID),
|
|
zap.Error(err),
|
|
)
|
|
return domain.CreateBetRes{}, err
|
|
}
|
|
|
|
res := domain.ConvertCreateBet(bet, rows)
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string, sportID int32, HomeTeam, AwayTeam string, StartTime time.Time, numMarkets int) ([]domain.CreateBetOutcome, float32, error) {
|
|
|
|
var newOdds []domain.CreateBetOutcome
|
|
var totalOdds float32 = 1
|
|
|
|
markets, err := s.prematchSvc.GetPrematchOddsByUpcomingID(ctx, eventID)
|
|
if err != nil {
|
|
s.logger.Error("failed to get odds for event", "event id", eventID, "error", err)
|
|
s.mongoLogger.Error("failed to get odds for event",
|
|
zap.String("eventID", eventID),
|
|
zap.Int32("sportID", sportID),
|
|
zap.String("homeTeam", HomeTeam),
|
|
zap.String("awayTeam", AwayTeam),
|
|
zap.Error(err))
|
|
return nil, 0, err
|
|
}
|
|
|
|
if len(markets) == 0 {
|
|
s.logger.Error("empty odds for event", "event id", eventID)
|
|
s.mongoLogger.Warn("empty odds for event",
|
|
zap.String("eventID", eventID),
|
|
zap.Int32("sportID", sportID),
|
|
zap.String("homeTeam", HomeTeam),
|
|
zap.String("awayTeam", AwayTeam))
|
|
return nil, 0, fmt.Errorf("empty odds or event %v", eventID)
|
|
}
|
|
|
|
var selectedMarkets []domain.Odd
|
|
numMarkets = min(numMarkets, len(markets))
|
|
for i := 0; i < numMarkets; i++ {
|
|
randomIndex := random.Intn(len(markets))
|
|
selectedMarkets = append(selectedMarkets, markets[randomIndex])
|
|
markets = append(markets[:randomIndex], markets[randomIndex+1:]...)
|
|
}
|
|
|
|
for _, market := range selectedMarkets {
|
|
|
|
randomRawOdd := market.RawOdds[random.Intn(len(market.RawOdds))]
|
|
|
|
type rawOddType struct {
|
|
ID string
|
|
Name string
|
|
Odds string
|
|
Header string
|
|
Handicap string
|
|
}
|
|
|
|
var selectedOdd rawOddType
|
|
rawBytes, err := json.Marshal(randomRawOdd)
|
|
err = json.Unmarshal(rawBytes, &selectedOdd)
|
|
|
|
if err != nil {
|
|
s.logger.Error("Failed to unmarshal raw odd", "error", err)
|
|
s.mongoLogger.Warn("Failed to unmarshal raw odd",
|
|
zap.String("eventID", eventID),
|
|
zap.Int32("sportID", sportID),
|
|
zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32)
|
|
if err != nil {
|
|
s.logger.Error("Failed to parse odd", "error", err)
|
|
s.mongoLogger.Warn("Failed to parse odd",
|
|
zap.String("eventID", eventID),
|
|
zap.String("oddValue", selectedOdd.Odds),
|
|
zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
eventIDInt, err := strconv.ParseInt(eventID, 10, 64)
|
|
if err != nil {
|
|
s.logger.Error("Failed to parse eventID", "error", err)
|
|
s.mongoLogger.Warn("Failed to parse eventID",
|
|
zap.String("eventID", eventID),
|
|
zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
oddID, err := strconv.ParseInt(selectedOdd.ID, 10, 64)
|
|
if err != nil {
|
|
s.logger.Error("Failed to parse oddID", "error", err)
|
|
s.mongoLogger.Warn("Failed to parse oddID",
|
|
zap.String("oddID", selectedOdd.ID),
|
|
zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
marketID, err := strconv.ParseInt(market.MarketID, 10, 64)
|
|
if err != nil {
|
|
s.logger.Error("Failed to parse marketID", "error", err)
|
|
s.mongoLogger.Warn("Failed to parse marketID",
|
|
zap.String("marketID", market.MarketID),
|
|
zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
marketName := market.MarketName
|
|
|
|
newOdds = append(newOdds, domain.CreateBetOutcome{
|
|
EventID: eventIDInt,
|
|
OddID: oddID,
|
|
MarketID: marketID,
|
|
SportID: int64(sportID),
|
|
HomeTeamName: HomeTeam,
|
|
AwayTeamName: AwayTeam,
|
|
MarketName: marketName,
|
|
Odd: float32(parsedOdd),
|
|
OddName: selectedOdd.Name,
|
|
OddHeader: selectedOdd.Header,
|
|
OddHandicap: selectedOdd.Handicap,
|
|
Expires: StartTime,
|
|
})
|
|
|
|
totalOdds *= float32(parsedOdd)
|
|
}
|
|
|
|
if len(newOdds) == 0 {
|
|
s.logger.Error("Bet Outcomes is empty for market", "selectedMarkets", len(selectedMarkets))
|
|
s.mongoLogger.Error("Bet Outcomes is empty for market",
|
|
zap.String("eventID", eventID),
|
|
zap.Int32("sportID", sportID),
|
|
zap.String("homeTeam", HomeTeam),
|
|
zap.String("awayTeam", AwayTeam),
|
|
zap.Int("selectedMarkets", len(selectedMarkets)))
|
|
return nil, 0, ErrGenerateRandomOutcome
|
|
}
|
|
|
|
// ✅ Final success log (optional)
|
|
s.mongoLogger.Info("Random bet outcomes generated successfully",
|
|
zap.String("eventID", eventID),
|
|
zap.Int32("sportID", sportID),
|
|
zap.Int("numOutcomes", len(newOdds)),
|
|
zap.Float32("totalOdds", totalOdds))
|
|
|
|
return newOdds, totalOdds, nil
|
|
}
|
|
|
|
func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, leagueID, sportID domain.ValidInt32, firstStartTime, lastStartTime domain.ValidTime) (domain.CreateBetRes, error) {
|
|
|
|
// Get a unexpired event id
|
|
|
|
events, _, err := s.eventSvc.GetPaginatedUpcomingEvents(ctx,
|
|
domain.EventFilter{
|
|
SportID: sportID,
|
|
LeagueID: leagueID,
|
|
FirstStartTime: firstStartTime,
|
|
LastStartTime: lastStartTime,
|
|
})
|
|
|
|
if err != nil {
|
|
s.mongoLogger.Error("failed to get paginated upcoming events",
|
|
zap.Int64("userID", userID),
|
|
zap.Int64("branchID", branchID),
|
|
zap.Error(err))
|
|
return domain.CreateBetRes{}, err
|
|
}
|
|
|
|
if len(events) == 0 {
|
|
s.mongoLogger.Warn("no events available for random bet",
|
|
zap.Int64("userID", userID),
|
|
zap.Int64("branchID", branchID))
|
|
return domain.CreateBetRes{}, ErrNoEventsAvailable
|
|
}
|
|
|
|
// TODO: Add the option of passing number of created events
|
|
var selectedUpcomingEvents []domain.UpcomingEvent
|
|
numEventsPerBet := min(random.Intn(4)+1, len(events)) //Eliminate the option of 0
|
|
|
|
for i := 0; i < int(numEventsPerBet); i++ {
|
|
randomIndex := random.Intn(len(events))
|
|
selectedUpcomingEvents = append(selectedUpcomingEvents, events[randomIndex])
|
|
events = append(events[:randomIndex], events[randomIndex+1:]...)
|
|
|
|
}
|
|
|
|
// s.logger.Info("Generating random bet events", "selectedUpcomingEvents", len(selectedUpcomingEvents))
|
|
|
|
// Get market and odds for that
|
|
var randomOdds []domain.CreateBetOutcome
|
|
var totalOdds float32 = 1
|
|
numMarketsPerBet := random.Intn(2) + 1
|
|
for _, event := range selectedUpcomingEvents {
|
|
|
|
newOdds, total, err := s.GenerateRandomBetOutcomes(ctx, event.ID, event.SportID, event.HomeTeam, event.AwayTeam, event.StartTime, numMarketsPerBet)
|
|
|
|
if err != nil {
|
|
s.logger.Error("failed to generate random bet outcome", "event id", event.ID, "error", err)
|
|
s.mongoLogger.Error("failed to generate random bet outcome",
|
|
zap.Int64("userID", userID),
|
|
zap.Int64("branchID", branchID),
|
|
zap.String("eventID", event.ID),
|
|
zap.String("error", fmt.Sprintf("%v", err)))
|
|
continue
|
|
}
|
|
|
|
randomOdds = append(randomOdds, newOdds...)
|
|
totalOdds = totalOdds * total
|
|
|
|
}
|
|
if len(randomOdds) == 0 {
|
|
s.logger.Error("Failed to generate random any outcomes for all events")
|
|
s.mongoLogger.Error("Failed to generate random any outcomes for all events",
|
|
zap.Int64("userID", userID),
|
|
zap.Int64("branchID", branchID))
|
|
return domain.CreateBetRes{}, ErrGenerateRandomOutcome
|
|
}
|
|
|
|
// s.logger.Info("Generated Random bet Outcome", "randomOdds", len(randomOdds))
|
|
|
|
var cashoutID string
|
|
|
|
cashoutID, err = s.GenerateCashoutID()
|
|
if err != nil {
|
|
s.mongoLogger.Error("Failed to generate cash out ID",
|
|
zap.Int64("userID", userID),
|
|
zap.Int64("branchID", branchID))
|
|
return domain.CreateBetRes{}, err
|
|
}
|
|
|
|
randomNumber := strconv.FormatInt(int64(random.Intn(100000000000)), 10)
|
|
newBet := domain.CreateBet{
|
|
Amount: domain.ToCurrency(123.5),
|
|
TotalOdds: totalOdds,
|
|
Status: domain.OUTCOME_STATUS_PENDING,
|
|
FullName: "test" + randomNumber,
|
|
PhoneNumber: "0900000000",
|
|
CashoutID: cashoutID,
|
|
BranchID: domain.ValidInt64{Valid: true, Value: branchID},
|
|
UserID: domain.ValidInt64{Valid: true, Value: userID},
|
|
}
|
|
|
|
bet, err := s.CreateBet(ctx, newBet)
|
|
if err != nil {
|
|
s.mongoLogger.Error("Failed to create a new random bet",
|
|
zap.Int64("userID", userID),
|
|
zap.Int64("branchID", branchID),
|
|
zap.String("bet", fmt.Sprintf("%+v", newBet)))
|
|
return domain.CreateBetRes{}, err
|
|
}
|
|
|
|
for i := range randomOdds {
|
|
randomOdds[i].BetID = bet.ID
|
|
}
|
|
|
|
rows, err := s.betStore.CreateBetOutcome(ctx, randomOdds)
|
|
if err != nil {
|
|
s.mongoLogger.Error("Failed to create a new random bet outcome",
|
|
zap.Int64("userID", userID),
|
|
zap.Int64("branchID", branchID),
|
|
zap.String("randomOdds", fmt.Sprintf("%+v", randomOdds)))
|
|
return domain.CreateBetRes{}, err
|
|
}
|
|
|
|
res := domain.ConvertCreateBet(bet, rows)
|
|
|
|
s.mongoLogger.Info("Random bets placed successfully",
|
|
zap.Int64("userID", userID),
|
|
zap.Int64("branchID", branchID),
|
|
zap.String("response", fmt.Sprintf("%+v", res)))
|
|
return res, nil
|
|
}
|
|
|
|
func (s *Service) CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) {
|
|
return s.betStore.CreateBet(ctx, bet)
|
|
}
|
|
|
|
func (s *Service) CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBetOutcome) (int64, error) {
|
|
return s.betStore.CreateBetOutcome(ctx, outcomes)
|
|
}
|
|
|
|
func (s *Service) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) {
|
|
return s.betStore.GetBetByID(ctx, id)
|
|
}
|
|
func (s *Service) GetBetByCashoutID(ctx context.Context, id string) (domain.GetBet, error) {
|
|
return s.betStore.GetBetByCashoutID(ctx, id)
|
|
}
|
|
func (s *Service) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]domain.GetBet, error) {
|
|
return s.betStore.GetAllBets(ctx, filter)
|
|
}
|
|
|
|
func (s *Service) GetBetByBranchID(ctx context.Context, branchID int64) ([]domain.GetBet, error) {
|
|
return s.betStore.GetBetByBranchID(ctx, branchID)
|
|
}
|
|
|
|
func (s *Service) GetBetByUserID(ctx context.Context, UserID int64) ([]domain.GetBet, error) {
|
|
return s.betStore.GetBetByUserID(ctx, UserID)
|
|
}
|
|
|
|
func (s *Service) GetBetCount(ctx context.Context, UserID int64, outcomesHash string) (int64, error) {
|
|
return s.betStore.GetBetCount(ctx, UserID, outcomesHash)
|
|
}
|
|
|
|
func (s *Service) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error {
|
|
return s.betStore.UpdateCashOut(ctx, id, cashedOut)
|
|
}
|
|
|
|
func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error {
|
|
bet, err := s.GetBetByID(ctx, id)
|
|
if err != nil {
|
|
s.mongoLogger.Error("failed to update bet status: invalid bet ID",
|
|
zap.Int64("bet_id", id),
|
|
zap.Error(err),
|
|
)
|
|
return err
|
|
}
|
|
|
|
if bet.IsShopBet ||
|
|
status == domain.OUTCOME_STATUS_ERROR ||
|
|
status == domain.OUTCOME_STATUS_PENDING ||
|
|
status == domain.OUTCOME_STATUS_LOSS {
|
|
return s.betStore.UpdateStatus(ctx, id, status)
|
|
}
|
|
|
|
customerWallet, err := s.walletSvc.GetCustomerWallet(ctx, id)
|
|
if err != nil {
|
|
s.mongoLogger.Error("failed to get customer wallet",
|
|
zap.Int64("bet_id", id),
|
|
zap.Error(err),
|
|
)
|
|
return err
|
|
}
|
|
|
|
var amount domain.Currency
|
|
switch status {
|
|
case domain.OUTCOME_STATUS_WIN:
|
|
amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds)
|
|
case domain.OUTCOME_STATUS_HALF:
|
|
amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds) / 2
|
|
default:
|
|
amount = bet.Amount
|
|
}
|
|
|
|
_, err = s.walletSvc.AddToWallet(ctx, customerWallet.RegularID, amount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{})
|
|
|
|
if err != nil {
|
|
s.mongoLogger.Error("failed to add winnings to wallet",
|
|
zap.Int64("wallet_id", customerWallet.RegularID),
|
|
zap.Float32("amount", float32(amount)),
|
|
zap.Error(err),
|
|
)
|
|
return err
|
|
}
|
|
|
|
return s.betStore.UpdateStatus(ctx, id, status)
|
|
}
|
|
|
|
func (s *Service) CheckBetOutcomeForBet(ctx context.Context, betID int64) (domain.OutcomeStatus, error) {
|
|
betOutcomes, err := s.betStore.GetBetOutcomeByBetID(ctx, betID)
|
|
if err != nil {
|
|
s.mongoLogger.Error("failed to get bet outcomes",
|
|
zap.Int64("bet_id", betID),
|
|
zap.Error(err),
|
|
)
|
|
return domain.OUTCOME_STATUS_PENDING, err
|
|
}
|
|
|
|
status := domain.OUTCOME_STATUS_PENDING
|
|
|
|
for _, betOutcome := range betOutcomes {
|
|
if betOutcome.Status == domain.OUTCOME_STATUS_PENDING {
|
|
s.mongoLogger.Info("outcome still pending",
|
|
zap.Int64("bet_id", betID),
|
|
)
|
|
return domain.OUTCOME_STATUS_PENDING, ErrOutcomesNotCompleted
|
|
}
|
|
if betOutcome.Status == domain.OUTCOME_STATUS_ERROR {
|
|
s.mongoLogger.Info("outcome contains error",
|
|
zap.Int64("bet_id", betID),
|
|
)
|
|
return domain.OUTCOME_STATUS_ERROR, nil
|
|
}
|
|
|
|
switch status {
|
|
case domain.OUTCOME_STATUS_PENDING:
|
|
status = betOutcome.Status
|
|
case domain.OUTCOME_STATUS_WIN:
|
|
switch betOutcome.Status {
|
|
case domain.OUTCOME_STATUS_LOSS:
|
|
status = domain.OUTCOME_STATUS_LOSS
|
|
case domain.OUTCOME_STATUS_HALF:
|
|
status = domain.OUTCOME_STATUS_HALF
|
|
case domain.OUTCOME_STATUS_VOID:
|
|
status = domain.OUTCOME_STATUS_VOID
|
|
case domain.OUTCOME_STATUS_WIN:
|
|
// remain win
|
|
default:
|
|
status = domain.OUTCOME_STATUS_ERROR
|
|
}
|
|
case domain.OUTCOME_STATUS_LOSS:
|
|
// stay as LOSS regardless of others
|
|
case domain.OUTCOME_STATUS_VOID:
|
|
switch betOutcome.Status {
|
|
case domain.OUTCOME_STATUS_LOSS:
|
|
status = domain.OUTCOME_STATUS_LOSS
|
|
case domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_HALF, domain.OUTCOME_STATUS_VOID:
|
|
// remain VOID
|
|
default:
|
|
status = domain.OUTCOME_STATUS_ERROR
|
|
}
|
|
case domain.OUTCOME_STATUS_HALF:
|
|
switch betOutcome.Status {
|
|
case domain.OUTCOME_STATUS_LOSS:
|
|
status = domain.OUTCOME_STATUS_LOSS
|
|
case domain.OUTCOME_STATUS_VOID:
|
|
status = domain.OUTCOME_STATUS_VOID
|
|
case domain.OUTCOME_STATUS_HALF, domain.OUTCOME_STATUS_WIN:
|
|
// remain HALF
|
|
default:
|
|
status = domain.OUTCOME_STATUS_ERROR
|
|
}
|
|
default:
|
|
status = domain.OUTCOME_STATUS_ERROR
|
|
}
|
|
}
|
|
|
|
if status == domain.OUTCOME_STATUS_PENDING || status == domain.OUTCOME_STATUS_ERROR {
|
|
s.mongoLogger.Info("bet status not updated due to status",
|
|
zap.Int64("bet_id", betID),
|
|
zap.String("final_status", string(status)),
|
|
)
|
|
}
|
|
|
|
return status, nil
|
|
}
|
|
|
|
func (s *Service) UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) {
|
|
betOutcome, err := s.betStore.UpdateBetOutcomeStatus(ctx, id, status)
|
|
if err != nil {
|
|
s.mongoLogger.Error("failed to update bet outcome status",
|
|
zap.Int64("betID", id),
|
|
zap.Error(err),
|
|
)
|
|
return domain.BetOutcome{}, err
|
|
}
|
|
|
|
return betOutcome, err
|
|
|
|
}
|
|
|
|
func (s *Service) DeleteBet(ctx context.Context, id int64) error {
|
|
return s.betStore.DeleteBet(ctx, id)
|
|
}
|
|
|
|
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 {
|
|
if outcomes[i].EventID != outcomes[j].EventID {
|
|
return outcomes[i].EventID < outcomes[j].EventID
|
|
}
|
|
if outcomes[i].MarketID != outcomes[j].MarketID {
|
|
return outcomes[i].MarketID < outcomes[j].MarketID
|
|
}
|
|
return outcomes[i].OddID < outcomes[j].OddID
|
|
})
|
|
|
|
var sb strings.Builder
|
|
for _, o := range outcomes {
|
|
sb.WriteString(fmt.Sprintf("%d-%d-%d;", o.EventID, o.MarketID, o.OddID))
|
|
}
|
|
|
|
sum := sha256.Sum256([]byte(sb.String()))
|
|
return hex.EncodeToString(sum[:]), nil
|
|
}
|