1620 lines
57 KiB
Go
1620 lines
57 KiB
Go
package result
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
|
)
|
|
|
|
// Football evaluations
|
|
|
|
// helper function to parse minute from event string like "77'" or "90+1'"
|
|
func parseEventMinute(eventText string) (int, error) {
|
|
parts := strings.Split(eventText, "'")
|
|
if len(parts) == 0 {
|
|
return 0, fmt.Errorf("invalid event text format")
|
|
}
|
|
timeStr := strings.TrimSpace(parts[0])
|
|
if strings.Contains(timeStr, "+") {
|
|
timeParts := strings.Split(timeStr, "+")
|
|
base, err1 := strconv.Atoi(timeParts[0])
|
|
extra, err2 := strconv.Atoi(timeParts[1])
|
|
if err1 != nil || err2 != nil {
|
|
return 0, fmt.Errorf("invalid injury time format")
|
|
}
|
|
return base + extra, nil
|
|
}
|
|
return strconv.Atoi(timeStr)
|
|
}
|
|
|
|
// Full Time Result betting is a type of bet where the bettor predicts the outcome of a match at the end of the full 90 minutes of play.
|
|
func evaluateFullTimeResult(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
switch outcome.OddName {
|
|
case "1": // Home win
|
|
if score.Home > score.Away {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case "Draw":
|
|
if score.Home == score.Away {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case "2": // Away win
|
|
if score.Away > score.Home {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName)
|
|
}
|
|
}
|
|
|
|
// Over/Under betting is a type of bet where the bettor predicts whether the total number of goals scored in a match will be over or under a specified number.
|
|
func evaluateGoalsOverUnder(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
totalGoals := float64(score.Home + score.Away)
|
|
threshold, err := strconv.ParseFloat(outcome.OddName, 64)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName)
|
|
}
|
|
|
|
switch outcome.OddHeader {
|
|
case "Over":
|
|
if totalGoals > threshold {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
} else if totalGoals == threshold {
|
|
return domain.OUTCOME_STATUS_VOID, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case "Under":
|
|
if totalGoals < threshold {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
} else if totalGoals == threshold {
|
|
return domain.OUTCOME_STATUS_VOID, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
|
|
}
|
|
|
|
// Correct Score betting is a type of bet where the bettor predicts the exact final score of a match.
|
|
func evaluateCorrectScore(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
expectedScore := fmt.Sprintf("%d-%d", score.Home, score.Away)
|
|
if outcome.OddName == expectedScore {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
// Half Time Result betting is a type of bet where the bettor predicts the outcome of a match at the end of the first half.
|
|
// This is the same as the full time result but only for the first half of the game
|
|
func evaluateHalfTimeResult(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
return evaluateFullTimeResult(outcome, score)
|
|
}
|
|
|
|
// This is a multiple outcome checker for the asian handicap and other kinds of bets
|
|
// The only outcome that are allowed are "Both Bets win", "Both Bets Lose", "Half Win and Half Void"
|
|
func checkMultiOutcome(outcome domain.OutcomeStatus, secondOutcome domain.OutcomeStatus) (domain.OutcomeStatus, error) {
|
|
if secondOutcome == domain.OUTCOME_STATUS_PENDING {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("cannot check pending outcome")
|
|
}
|
|
|
|
if outcome == domain.OUTCOME_STATUS_ERROR || secondOutcome == domain.OUTCOME_STATUS_ERROR {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("❌ mutli outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
|
|
}
|
|
|
|
// fmt.Printf("| Multi Outcome | %v -> %v \n", outcome.String(), secondOutcome.String())
|
|
|
|
switch outcome {
|
|
case domain.OUTCOME_STATUS_PENDING:
|
|
return secondOutcome, nil
|
|
case domain.OUTCOME_STATUS_WIN:
|
|
switch secondOutcome {
|
|
case domain.OUTCOME_STATUS_WIN:
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
case domain.OUTCOME_STATUS_LOSS:
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case domain.OUTCOME_STATUS_HALF:
|
|
return domain.OUTCOME_STATUS_VOID, nil
|
|
case domain.OUTCOME_STATUS_VOID:
|
|
return domain.OUTCOME_STATUS_HALF, nil
|
|
default:
|
|
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
|
|
}
|
|
case domain.OUTCOME_STATUS_LOSS:
|
|
switch secondOutcome {
|
|
case domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_HALF:
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case domain.OUTCOME_STATUS_VOID:
|
|
return domain.OUTCOME_STATUS_VOID, nil
|
|
default:
|
|
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
|
|
}
|
|
case domain.OUTCOME_STATUS_VOID:
|
|
switch secondOutcome {
|
|
case domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_LOSS:
|
|
return domain.OUTCOME_STATUS_VOID, nil
|
|
case domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_HALF:
|
|
return domain.OUTCOME_STATUS_VOID, nil
|
|
default:
|
|
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
|
|
}
|
|
case domain.OUTCOME_STATUS_HALF:
|
|
switch secondOutcome {
|
|
case domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_HALF:
|
|
return domain.OUTCOME_STATUS_VOID, nil
|
|
case domain.OUTCOME_STATUS_LOSS:
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case domain.OUTCOME_STATUS_VOID:
|
|
return domain.OUTCOME_STATUS_VOID, nil
|
|
default:
|
|
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
|
|
}
|
|
default:
|
|
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
|
|
}
|
|
}
|
|
|
|
// Asian Handicap betting is a type of betting that eliminates the possibility of a draw by giving one team a virtual advantage or disadvantage.
|
|
// When the handicap has two values like "+0.5, +1.0" or "-0.5, -1.0", then it a multi outcome bet
|
|
// .
|
|
//
|
|
// {
|
|
// "id": "548319135",
|
|
// "odds": "1.750",
|
|
// "header": "1",
|
|
// "handicap": "+0.5, +1.0"
|
|
// },
|
|
//
|
|
// {
|
|
// "id": "548319139",
|
|
// "odds": "1.950",
|
|
// "header": "2",
|
|
// "handicap": "-0.5, -1.0"
|
|
// }
|
|
func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
handicapList := strings.Split(outcome.OddHandicap, ",")
|
|
newOutcome := domain.OUTCOME_STATUS_PENDING
|
|
for _, handicapStr := range handicapList {
|
|
handicapStr = strings.TrimSpace(handicapStr)
|
|
handicap, err := strconv.ParseFloat(handicapStr, 64)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid handicap: %s", outcome.OddHandicap)
|
|
}
|
|
adjustedHomeScore := float64(score.Home)
|
|
adjustedAwayScore := float64(score.Away)
|
|
switch outcome.OddHeader {
|
|
case "1": // Home team
|
|
adjustedHomeScore += handicap
|
|
case "2": // Away team
|
|
adjustedAwayScore += handicap
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
|
|
}
|
|
|
|
if adjustedHomeScore > adjustedAwayScore {
|
|
if outcome.OddHeader == "1" {
|
|
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, err
|
|
}
|
|
} else {
|
|
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS)
|
|
if err != nil {
|
|
fmt.Printf("multi outcome check error")
|
|
return domain.OUTCOME_STATUS_ERROR, err
|
|
}
|
|
}
|
|
} else if adjustedHomeScore < adjustedAwayScore {
|
|
if outcome.OddHeader == "2" {
|
|
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, err
|
|
}
|
|
} else {
|
|
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS)
|
|
if err != nil {
|
|
fmt.Printf("multi outcome check error")
|
|
return domain.OUTCOME_STATUS_ERROR, err
|
|
}
|
|
}
|
|
}
|
|
if newOutcome == domain.OUTCOME_STATUS_PENDING {
|
|
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID)
|
|
if err != nil {
|
|
fmt.Printf("multi outcome check error")
|
|
return domain.OUTCOME_STATUS_ERROR, err
|
|
}
|
|
}
|
|
}
|
|
return newOutcome, nil
|
|
}
|
|
|
|
// Goal Line betting, also known as Over/Under betting,
|
|
// involves predicting the total number of goals scored in a match, regardless of which team wins.
|
|
//
|
|
// {
|
|
// "id": "548319141",
|
|
// "odds": "1.800",
|
|
// "header": "Over",
|
|
// "name": "1.5, 2.0"
|
|
// },
|
|
//
|
|
// {
|
|
// "id": "548319146",
|
|
// "odds": "1.900",
|
|
// "header": "Under",
|
|
// "name": "1.5, 2.0"
|
|
// }
|
|
func evaluateGoalLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
|
|
totalGoals := float64(score.Home + score.Away)
|
|
thresholdList := strings.Split(outcome.OddName, ",")
|
|
|
|
newOutcome := domain.OUTCOME_STATUS_PENDING
|
|
for _, thresholdStr := range thresholdList {
|
|
thresholdStr = strings.TrimSpace(thresholdStr)
|
|
threshold, err := strconv.ParseFloat(thresholdStr, 64)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: '%s', %v", thresholdStr, err)
|
|
}
|
|
|
|
oddHeader := strings.TrimSpace(outcome.OddHeader)
|
|
switch oddHeader {
|
|
case "Over":
|
|
if totalGoals > threshold {
|
|
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, err
|
|
}
|
|
} else if totalGoals == threshold {
|
|
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID)
|
|
if err != nil {
|
|
|
|
return domain.OUTCOME_STATUS_ERROR, err
|
|
}
|
|
}
|
|
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, err
|
|
}
|
|
case "Under":
|
|
if totalGoals < threshold {
|
|
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN)
|
|
if err != nil {
|
|
|
|
return domain.OUTCOME_STATUS_ERROR, err
|
|
}
|
|
} else if totalGoals == threshold {
|
|
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID)
|
|
if err != nil {
|
|
|
|
return domain.OUTCOME_STATUS_ERROR, err
|
|
}
|
|
}
|
|
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, err
|
|
}
|
|
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: '%s'", oddHeader)
|
|
}
|
|
|
|
}
|
|
|
|
return newOutcome, nil
|
|
}
|
|
|
|
// First Team To Score betting is a type of bet where the bettor predicts which team will score first in a match.
|
|
// We can get this from the "events" field on the result json
|
|
func evaluateFirstTeamToScore(outcome domain.BetOutcome, events []map[string]string) (domain.OutcomeStatus, error) {
|
|
for _, event := range events {
|
|
if strings.Contains(event["text"], "1st Goal") || strings.Contains(event["text"], "Goal 1") {
|
|
if strings.Contains(event["text"], outcome.HomeTeamName) && outcome.OddName == "1" {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
} else if strings.Contains(event["text"], outcome.AwayTeamName) && outcome.OddName == "2" {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
}
|
|
return domain.OUTCOME_STATUS_VOID, nil // No goals scored
|
|
}
|
|
|
|
// Goals Odd/Even betting is a type of bet where the bettor predicts whether the total number of goals scored in a match will be odd or even.
|
|
func evaluateGoalsOddEven(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
totalGoals := score.Home + score.Away
|
|
isOdd := totalGoals%2 == 1
|
|
if outcome.OddName == "Odd" && isOdd {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
} else if outcome.OddName == "Even" && !isOdd {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
func evaluateTeamOddEven(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
|
|
switch outcome.OddHeader {
|
|
case "1":
|
|
switch outcome.OddHandicap {
|
|
case "Odd":
|
|
if score.Home%2 == 1 {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case "Even":
|
|
if score.Home%2 == 0 {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd handicap: %s", outcome.OddHandicap)
|
|
}
|
|
case "2":
|
|
switch outcome.OddHandicap {
|
|
case "Odd":
|
|
if score.Away%2 == 1 {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case "Even":
|
|
if score.Away%2 == 0 {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd handicap: %s", outcome.OddHandicap)
|
|
}
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
|
|
|
|
}
|
|
}
|
|
|
|
// Double Chance betting is a type of bet where the bettor predicts two of the three possible outcomes of a match.
|
|
func evaluateDoubleChance(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
isHomeWin := score.Home > score.Away
|
|
isDraw := score.Home == score.Away
|
|
isAwayWin := score.Away > score.Home
|
|
switch outcome.OddName {
|
|
case "1 or Draw", (outcome.HomeTeamName + " or " + "Draw"), ("Draw" + " or " + outcome.HomeTeamName):
|
|
if isHomeWin || isDraw {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case "Draw or 2", ("Draw" + " or " + outcome.AwayTeamName), (outcome.AwayTeamName + " or " + "Draw"):
|
|
if isDraw || isAwayWin {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case "1 or 2", (outcome.HomeTeamName + " or " + outcome.AwayTeamName), (outcome.AwayTeamName + " or " + outcome.HomeTeamName):
|
|
if isHomeWin || isAwayWin {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName)
|
|
}
|
|
}
|
|
|
|
// Draw No Bet betting is a type of bet where the bettor predicts the outcome of a match, but if the match ends in a draw, the bet is voided.
|
|
func evaluateDrawNoBet(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
if score.Home == score.Away {
|
|
return domain.OUTCOME_STATUS_VOID, nil
|
|
}
|
|
if outcome.OddName == "1" && score.Home > score.Away {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
} else if outcome.OddName == "2" && score.Away > score.Home {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
func evaluateCorners(outcome domain.BetOutcome, corners struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
|
|
totalCorners := corners.Home + corners.Away
|
|
threshold, err := strconv.ParseFloat(outcome.OddName, 10)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName)
|
|
}
|
|
switch outcome.OddHeader {
|
|
case "Over":
|
|
if totalCorners > int(threshold) {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case "Under":
|
|
if totalCorners < int(threshold) {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case "Exactly":
|
|
if totalCorners == int(threshold) {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
|
|
}
|
|
}
|
|
|
|
|
|
// evaluateBothTeamsToScore checks if both teams scored in the match.
|
|
func evaluateBothTeamsToScore(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
btts := score.Home > 0 && score.Away > 0
|
|
switch outcome.OddName {
|
|
case "Yes":
|
|
if btts {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case "No":
|
|
if !btts {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name for BTTS: %s", outcome.OddName)
|
|
}
|
|
}
|
|
|
|
// evaluateResultAndBTTS checks for a combination of the match result and if both teams scored.
|
|
func evaluateResultAndBTTS(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
btts := score.Home > 0 && score.Away > 0
|
|
var bttsMatch bool
|
|
switch outcome.OddHeader {
|
|
case "Yes":
|
|
bttsMatch = btts
|
|
case "No":
|
|
bttsMatch = !btts
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header for Result/BTTS: %s", outcome.OddHeader)
|
|
}
|
|
|
|
var resultMatch bool
|
|
switch outcome.OddName {
|
|
case "1":
|
|
resultMatch = score.Home > score.Away
|
|
case "2":
|
|
resultMatch = score.Away > score.Home
|
|
case "Draw":
|
|
resultMatch = score.Home == score.Away
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name for Result/BTTS: %s", outcome.OddName)
|
|
}
|
|
|
|
if bttsMatch && resultMatch {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
// evaluateCleanSheet checks if a selected team did not concede any goals.
|
|
func evaluateCleanSheet(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
var cleanSheet bool
|
|
switch outcome.OddHeader {
|
|
case "1": // Corresponds to Home team
|
|
cleanSheet = (score.Away == 0)
|
|
case "2": // Corresponds to Away team
|
|
cleanSheet = (score.Home == 0)
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header for Clean Sheet: %s", outcome.OddHeader)
|
|
}
|
|
|
|
betOnYes := outcome.OddHandicap == "Yes"
|
|
|
|
if cleanSheet == betOnYes {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
// evaluateLastTeamToScore finds the last team that scored by checking events in reverse.
|
|
func evaluateLastTeamToScore(outcome domain.BetOutcome, events []map[string]string) (domain.OutcomeStatus, error) {
|
|
var lastGoalTeam string
|
|
for i := len(events) - 1; i >= 0; i-- {
|
|
event := events[i]
|
|
// A simple check for "Goal" in the event text
|
|
if strings.Contains(event["text"], "Goal") && !strings.Contains(event["text"], "disallowed") {
|
|
if strings.Contains(event["text"], outcome.HomeTeamName) {
|
|
lastGoalTeam = "1"
|
|
break
|
|
} else if strings.Contains(event["text"], outcome.AwayTeamName) {
|
|
lastGoalTeam = "2"
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
switch outcome.OddName {
|
|
case "1":
|
|
if lastGoalTeam == "1" {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case "2":
|
|
if lastGoalTeam == "2" {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case "No Goal", "No Goals":
|
|
if lastGoalTeam == "" {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name for Last Team to Score: %s", outcome.OddName)
|
|
}
|
|
}
|
|
|
|
// evaluateWinningMargin checks the margin of victory.
|
|
func evaluateFootballWinningMargin(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
homeWin := score.Home > score.Away
|
|
awayWin := score.Away > score.Home
|
|
margin := score.Home - score.Away
|
|
|
|
switch outcome.OddName {
|
|
case "Score Draw":
|
|
if margin == 0 && score.Home > 0 {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
case "No Goal":
|
|
if margin == 0 && score.Home == 0 {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
default:
|
|
// Handles margins like "1", "2", "3", "4+"
|
|
var expectedMargin int
|
|
isPlus := strings.HasSuffix(outcome.OddName, "+")
|
|
marginStr := strings.TrimSuffix(outcome.OddName, "+")
|
|
|
|
_, err := fmt.Sscanf(marginStr, "%d", &expectedMargin)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("could not parse winning margin: %s", outcome.OddName)
|
|
}
|
|
|
|
teamWon := (outcome.OddHeader == "1" && homeWin) || (outcome.OddHeader == "2" && awayWin)
|
|
if !teamWon {
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
actualMargin := abs(margin)
|
|
if isPlus {
|
|
if actualMargin >= expectedMargin {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
} else {
|
|
if actualMargin == expectedMargin {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
}
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
func abs(x int) int {
|
|
if x < 0 {
|
|
return -x
|
|
}
|
|
return x
|
|
}
|
|
|
|
// evaluateBothTeamsToReceiveCards checks if both teams received at least one card.
|
|
func evaluateBothTeamsToReceiveCards(outcome domain.BetOutcome, yellowCards, redCards struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
homeCards := yellowCards.Home + redCards.Home
|
|
awayCards := yellowCards.Away + redCards.Away
|
|
|
|
var conditionMet bool
|
|
switch outcome.OddName {
|
|
case "a Card":
|
|
conditionMet = homeCards > 0 && awayCards > 0
|
|
case "2+ Cards":
|
|
conditionMet = homeCards >= 2 && awayCards >= 2
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name for Both Teams To Receive Cards: %s", outcome.OddName)
|
|
}
|
|
|
|
isBetOnYes := outcome.OddHeader == "Yes"
|
|
|
|
if conditionMet == isBetOnYes {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
// evaluateHalfWithMostGoals compares total goals in each half.
|
|
func evaluateHalfWithMostGoals(outcome domain.BetOutcome, firstHalf, secondHalf struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
firstHalfGoals := firstHalf.Home + firstHalf.Away
|
|
secondHalfGoals := secondHalf.Home + secondHalf.Away
|
|
|
|
var won bool
|
|
switch outcome.OddName {
|
|
case "1st Half":
|
|
won = firstHalfGoals > secondHalfGoals
|
|
case "2nd Half":
|
|
won = secondHalfGoals > firstHalfGoals
|
|
case "Tie":
|
|
won = firstHalfGoals == secondHalfGoals
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name for Half with Most Goals: %s", outcome.OddName)
|
|
}
|
|
|
|
if won {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
// evaluateTeamHighestScoringHalf checks which half a specific team scored more goals in.
|
|
func evaluateTeamHighestScoringHalf(outcome domain.BetOutcome, firstHalf, secondHalf struct{ Home, Away int }, team string) (domain.OutcomeStatus, error) {
|
|
var first, second int
|
|
if team == "home" {
|
|
first = firstHalf.Home
|
|
second = secondHalf.Home
|
|
} else {
|
|
first = firstHalf.Away
|
|
second = secondHalf.Away
|
|
}
|
|
|
|
var won bool
|
|
switch outcome.OddName {
|
|
case "1st Half":
|
|
won = first > second
|
|
case "2nd Half":
|
|
won = second > first
|
|
case "Tie":
|
|
won = first == second
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name for Team Highest Scoring Half: %s", outcome.OddName)
|
|
}
|
|
|
|
if won {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
// evaluateTeamTotalGoals checks the total goals for a single team.
|
|
func evaluateTeamTotalGoals(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
var teamGoals int
|
|
if outcome.OddHeader == "1" {
|
|
teamGoals = score.Home
|
|
} else if outcome.OddHeader == "2" {
|
|
teamGoals = score.Away
|
|
} else {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header for Team Total Goals: %s", outcome.OddHeader)
|
|
}
|
|
|
|
handicapStr := strings.TrimPrefix(outcome.OddHandicap, "Over ")
|
|
handicapStr = strings.TrimPrefix(handicapStr, "Under ")
|
|
handicap, err := strconv.ParseFloat(handicapStr, 64)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid handicap for Team Total Goals: %s", outcome.OddHandicap)
|
|
}
|
|
|
|
var won bool
|
|
if strings.HasPrefix(outcome.OddHandicap, "Over") {
|
|
won = float64(teamGoals) > handicap
|
|
} else if strings.HasPrefix(outcome.OddHandicap, "Under") {
|
|
won = float64(teamGoals) < handicap
|
|
} else {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid handicap type for Team Total Goals: %s", outcome.OddHandicap)
|
|
}
|
|
|
|
if won {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
// evaluateExactTotalGoals checks for the exact number of goals scored.
|
|
func evaluateExactTotalGoals(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
totalGoals := score.Home + score.Away
|
|
betGoalsStr := strings.TrimSuffix(strings.Fields(outcome.OddName)[0], "+")
|
|
betGoals, err := strconv.Atoi(betGoalsStr)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid bet value for Exact Total Goals: %s", outcome.OddName)
|
|
}
|
|
|
|
var won bool
|
|
if strings.HasSuffix(outcome.OddName, "+") {
|
|
won = totalGoals >= betGoals
|
|
} else {
|
|
won = totalGoals == betGoals
|
|
}
|
|
|
|
if won {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
// evaluateTeamsToScore checks which teams scored in the match.
|
|
func evaluateTeamsToScore(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
homeScored := score.Home > 0
|
|
awayScored := score.Away > 0
|
|
|
|
var won bool
|
|
switch outcome.OddName {
|
|
case "Both Teams":
|
|
won = homeScored && awayScored
|
|
case "No Goal":
|
|
won = !homeScored && !awayScored
|
|
default:
|
|
if strings.HasSuffix(outcome.OddName, "Only") {
|
|
teamName := strings.TrimSuffix(outcome.OddName, " Only")
|
|
if teamName == outcome.HomeTeamName {
|
|
won = homeScored && !awayScored
|
|
} else if teamName == outcome.AwayTeamName {
|
|
won = !homeScored && awayScored
|
|
}
|
|
} else {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name for Teams to Score: %s", outcome.OddName)
|
|
}
|
|
}
|
|
|
|
if won {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
// evaluateFirstMatchCorner checks which team took the first corner.
|
|
func evaluateFirstMatchCorner(outcome domain.BetOutcome, events []map[string]string) (domain.OutcomeStatus, error) {
|
|
for _, event := range events {
|
|
if strings.Contains(event["text"], "Corner") {
|
|
var firstCornerTeam string
|
|
if strings.Contains(event["text"], outcome.HomeTeamName) {
|
|
firstCornerTeam = "1"
|
|
} else if strings.Contains(event["text"], outcome.AwayTeamName) {
|
|
firstCornerTeam = "2"
|
|
}
|
|
|
|
if outcome.OddName == firstCornerTeam {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil // No corners in the match
|
|
}
|
|
|
|
// evaluateLastMatchCorner checks which team took the last corner.
|
|
func evaluateLastMatchCorner(outcome domain.BetOutcome, events []map[string]string) (domain.OutcomeStatus, error) {
|
|
for i := len(events) - 1; i >= 0; i-- {
|
|
event := events[i]
|
|
if strings.Contains(event["text"], "Corner") {
|
|
var lastCornerTeam string
|
|
if strings.Contains(event["text"], outcome.HomeTeamName) {
|
|
lastCornerTeam = "1"
|
|
} else if strings.Contains(event["text"], outcome.AwayTeamName) {
|
|
lastCornerTeam = "2"
|
|
}
|
|
|
|
if outcome.OddName == lastCornerTeam {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil // No corners in the match
|
|
}
|
|
|
|
// evaluateCornerMatchBet determines which team had more corners.
|
|
func evaluateCornerMatchBet(outcome domain.BetOutcome, corners struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
var won bool
|
|
switch outcome.OddName {
|
|
case "1":
|
|
won = corners.Home > corners.Away
|
|
case "Tie":
|
|
won = corners.Home == corners.Away
|
|
case "2":
|
|
won = corners.Away > corners.Home
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name for Corner Match Bet: %s", outcome.OddName)
|
|
}
|
|
if won {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
// evaluateMultiCorners multiplies 1st half corners by 2nd half corners.
|
|
func evaluateMultiCorners(outcome domain.BetOutcome, totalCorners, halfTimeCorners struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
firstHalfTotal := halfTimeCorners.Home + halfTimeCorners.Away
|
|
secondHalfTotal := (totalCorners.Home + totalCorners.Away) - firstHalfTotal
|
|
multiCornerValue := firstHalfTotal * secondHalfTotal
|
|
|
|
handicap, err := strconv.ParseFloat(outcome.OddName, 64)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid handicap for Multi Corners: %s", outcome.OddName)
|
|
}
|
|
|
|
var won bool
|
|
if outcome.OddHeader == "Over" {
|
|
won = float64(multiCornerValue) > handicap
|
|
} else if outcome.OddHeader == "Under" {
|
|
won = float64(multiCornerValue) < handicap
|
|
} else {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header for Multi Corners: %s", outcome.OddHeader)
|
|
}
|
|
|
|
if won {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
// evaluateMatchShotsOnTarget evaluates over/under for total shots on target.
|
|
func evaluateMatchShotsOnTarget(outcome domain.BetOutcome, onTarget struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
totalSOT := onTarget.Home + onTarget.Away
|
|
handicap, err := strconv.ParseFloat(outcome.OddName, 64)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid handicap for Match Shots on Target: %s", outcome.OddName)
|
|
}
|
|
|
|
var won bool
|
|
if outcome.OddHeader == "Over" {
|
|
won = float64(totalSOT) > handicap
|
|
} else if outcome.OddHeader == "Under" {
|
|
won = float64(totalSOT) < handicap
|
|
} else {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header for Match Shots on Target: %s", outcome.OddHeader)
|
|
}
|
|
|
|
if won {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
// evaluateTeamShotsOnTarget evaluates over/under for a single team's shots on target.
|
|
func evaluateTeamShotsOnTarget(outcome domain.BetOutcome, onTarget struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
var teamSOT int
|
|
if outcome.OddHeader == "1" {
|
|
teamSOT = onTarget.Home
|
|
} else if outcome.OddHeader == "2" {
|
|
teamSOT = onTarget.Away
|
|
} else {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header for Team Shots on Target: %s", outcome.OddHeader)
|
|
}
|
|
|
|
handicapStr := strings.TrimPrefix(outcome.OddHandicap, "Over ")
|
|
handicapStr = strings.TrimPrefix(handicapStr, "Under ")
|
|
handicap, err := strconv.ParseFloat(handicapStr, 64)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid handicap for Team Shots on Target: %s", outcome.OddHandicap)
|
|
}
|
|
|
|
var won bool
|
|
if strings.HasPrefix(outcome.OddHandicap, "Over") {
|
|
won = float64(teamSOT) > handicap
|
|
} else if strings.HasPrefix(outcome.OddHandicap, "Under") {
|
|
won = float64(teamSOT) < handicap
|
|
} else {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid handicap type for Team Shots on Target: %s", outcome.OddHandicap)
|
|
}
|
|
|
|
if won {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
// evaluateTimeOfFirstGoal checks if the first goal's timing matches the bet.
|
|
func evaluateTimeOfFirstGoal(outcome domain.BetOutcome, events []map[string]string) (domain.OutcomeStatus, error) {
|
|
firstGoalMin := -1
|
|
for _, event := range events {
|
|
if strings.Contains(event["text"], "1st Goal") {
|
|
min, err := parseEventMinute(event["text"])
|
|
if err == nil {
|
|
firstGoalMin = min
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Logic for Late Goal / Early Goal
|
|
if strings.Contains(outcome.OddName, "before") || strings.Contains(outcome.OddName, "after") {
|
|
var timeMarker int
|
|
var isBefore, betOnYes bool
|
|
|
|
if _, err := fmt.Sscanf(outcome.OddName, "Goal before %d:%d", &timeMarker, new(int)); err == nil {
|
|
isBefore = true
|
|
} else if _, err := fmt.Sscanf(outcome.OddName, "No Goal before %d:%d", &timeMarker, new(int)); err == nil {
|
|
isBefore = true
|
|
betOnYes = false
|
|
} else if _, err := fmt.Sscanf(outcome.OddName, "Goal after %d:%d", &timeMarker, new(int)); err == nil {
|
|
isBefore = false
|
|
} else if _, err := fmt.Sscanf(outcome.OddName, "No Goal after %d:%d", &timeMarker, new(int)); err == nil {
|
|
isBefore = false
|
|
betOnYes = false
|
|
} else {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("could not parse time from odd name: %s", outcome.OddName)
|
|
}
|
|
|
|
var conditionMet bool
|
|
if isBefore {
|
|
conditionMet = firstGoalMin != -1 && firstGoalMin <= timeMarker
|
|
} else { // isAfter
|
|
// This requires finding the last goal, not just the first. This implementation is simplified for first goal.
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("late goal logic not fully implemented for all cases")
|
|
}
|
|
|
|
if conditionMet == betOnYes {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
// Logic for Time Brackets
|
|
if firstGoalMin == -1 { // No Goal
|
|
if outcome.OddName == "No Goal" {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
parts := strings.Split(outcome.OddName, "-")
|
|
if len(parts) == 2 {
|
|
start, _ := strconv.Atoi(strings.TrimSpace(parts[0]))
|
|
endStr := strings.Fields(parts[1])[0]
|
|
end, _ := strconv.Atoi(endStr)
|
|
|
|
if firstGoalMin >= start && firstGoalMin <= end {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("unhandled time of goal format: %s", outcome.OddName)
|
|
}
|
|
|
|
// evaluateSpecials handles various markets grouped under the "Specials" ID.
|
|
func evaluateSpecials(outcome domain.BetOutcome, finalScore, firstHalfScore, secondHalfScore struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
var team, betType string
|
|
team = outcome.OddHeader // "1" for home, "2" for away
|
|
betType = outcome.OddName
|
|
|
|
var won bool
|
|
switch betType {
|
|
case "To Win From Behind":
|
|
if team == "1" {
|
|
won = firstHalfScore.Home < firstHalfScore.Away && finalScore.Home > finalScore.Away
|
|
} else {
|
|
won = firstHalfScore.Away < firstHalfScore.Home && finalScore.Away > finalScore.Home
|
|
}
|
|
case "To Win to Nil":
|
|
if team == "1" {
|
|
won = finalScore.Home > finalScore.Away && finalScore.Away == 0
|
|
} else {
|
|
won = finalScore.Away > finalScore.Home && finalScore.Home == 0
|
|
}
|
|
case "To Win Either Half":
|
|
if team == "1" {
|
|
won = (firstHalfScore.Home > firstHalfScore.Away) || (secondHalfScore.Home > secondHalfScore.Away)
|
|
} else {
|
|
won = (firstHalfScore.Away > firstHalfScore.Home) || (secondHalfScore.Away > secondHalfScore.Home)
|
|
}
|
|
case "To Win Both Halves":
|
|
if team == "1" {
|
|
won = (firstHalfScore.Home > firstHalfScore.Away) && (secondHalfScore.Home > secondHalfScore.Away)
|
|
} else {
|
|
won = (firstHalfScore.Away > firstHalfScore.Home) && (secondHalfScore.Away > secondHalfScore.Home)
|
|
}
|
|
case "To Score in Both Halves":
|
|
if team == "1" {
|
|
won = firstHalfScore.Home > 0 && secondHalfScore.Home > 0
|
|
} else {
|
|
won = firstHalfScore.Away > 0 && secondHalfScore.Away > 0
|
|
}
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("unsupported special market: %s", betType)
|
|
}
|
|
|
|
if won {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
|
|
// Basketball evaluations
|
|
|
|
// Game Lines is an aggregate of money line, spread and total betting markets in one
|
|
func evaluateGameLines(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
switch outcome.OddName {
|
|
case "Money Line":
|
|
return evaluateMoneyLine(outcome, score)
|
|
|
|
case "Spread", "Line", "Run Line":
|
|
// Since Spread betting is essentially the same thing
|
|
return evaluateAsianHandicap(outcome, score)
|
|
case "Total":
|
|
return evaluateTotalOverUnder(outcome, score)
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName)
|
|
}
|
|
}
|
|
|
|
// Money Line betting is a type of bet where the bettor predicts the outcome of a match without any point spread.
|
|
func evaluateMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
switch outcome.OddHeader {
|
|
case "1":
|
|
if score.Home > score.Away {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
|
|
case "2":
|
|
if score.Home < score.Away {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case "Tie", "Draw":
|
|
if score.Home == score.Away {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName)
|
|
}
|
|
}
|
|
|
|
// Total Over/Under betting is a type of bet where the bettor predicts whether the total number of points scored in a match will be over or under a specified number.
|
|
func evaluateTotalOverUnder(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
|
|
// The handicap will be in the format "U {float}" or "O {float}"
|
|
// U and O denoting over and under for this case
|
|
overUnderStr := strings.Split(outcome.OddHandicap, " ")
|
|
if len(overUnderStr) != 2 {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName)
|
|
}
|
|
threshold, err := strconv.ParseFloat(overUnderStr[1], 64)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName)
|
|
}
|
|
|
|
// Since the threshold will come in a xx.5 format, there is no VOID for this kind of bet
|
|
totalScore := float64(score.Home + score.Away)
|
|
|
|
switch overUnderStr[0] {
|
|
case "O":
|
|
if totalScore > threshold {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case "U":
|
|
if totalScore < threshold {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case "E":
|
|
if totalScore == threshold {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
|
|
}
|
|
|
|
func evaluateTotalLegs(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
total, err := strconv.ParseFloat(outcome.OddName, 64)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid : %s", outcome.OddName)
|
|
}
|
|
|
|
totalLegs := float64(score.Home + score.Away)
|
|
|
|
switch outcome.OddHeader {
|
|
case "Over":
|
|
if totalLegs > total {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case "Under":
|
|
if totalLegs < total {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
|
|
}
|
|
|
|
// Result and Total betting is a type of bet where the bettor predicts
|
|
// the outcome of a match and whether the total number of points scored will be over or under a specified number.
|
|
func evaluateResultAndTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
|
|
// The handicap will be in the format "U {float}" or "O {float}"
|
|
// U and O denoting over and under for this case
|
|
overUnderStr := strings.Split(outcome.OddHandicap, " ")
|
|
if len(overUnderStr) != 2 {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName)
|
|
}
|
|
|
|
overUnder := overUnderStr[0]
|
|
|
|
if overUnder != "Over" && overUnder != "Under" {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over under: %s", outcome.OddHeader)
|
|
}
|
|
threshold, err := strconv.ParseFloat(overUnderStr[1], 64)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName)
|
|
}
|
|
|
|
// Since the threshold will come in a xx.5 format, there is no VOID for this kind of bet
|
|
totalScore := float64(score.Home + score.Away)
|
|
|
|
switch outcome.OddHeader {
|
|
case "1":
|
|
if score.Home < score.Away {
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
if overUnder == "Over" && totalScore > threshold {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
} else if overUnder == "Under" && totalScore < threshold {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case "2":
|
|
if score.Away < score.Home {
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
if overUnder == "Over" && totalScore > threshold {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
} else if overUnder == "Under" && totalScore < threshold {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed to parse over and under: %s", outcome.OddName)
|
|
}
|
|
}
|
|
|
|
// Team Total betting is a type of bet where the bettor predicts the total number of points scored by a specific team in a match
|
|
// is over or under a specified number.
|
|
func evaluateTeamTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
|
|
// The handicap will be in the format "U {float}" or "O {float}"
|
|
// U and O denoting over and under for this case
|
|
overUnderStr := strings.Split(outcome.OddHandicap, " ")
|
|
if len(overUnderStr) != 2 {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddHandicap)
|
|
}
|
|
|
|
overUnder := overUnderStr[0]
|
|
|
|
if overUnder != "Over" && overUnder != "Under" {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over under: %s", outcome.OddHeader)
|
|
}
|
|
threshold, err := strconv.ParseFloat(overUnderStr[1], 64)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddHandicap)
|
|
}
|
|
|
|
// Since the threshold will come in a xx.5 format, there is no VOID for this kind of bet
|
|
HomeScore := float64(score.Home)
|
|
AwayScore := float64(score.Away)
|
|
|
|
switch outcome.OddHeader {
|
|
case "1":
|
|
if overUnder == "Over" && HomeScore > threshold {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
} else if overUnder == "Under" && HomeScore < threshold {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case "2":
|
|
if overUnder == "Over" && AwayScore > threshold {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
} else if overUnder == "Under" && AwayScore < threshold {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed to parse over and under: %s", outcome.OddName)
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
// Result and Both Teams To Score X Points is a type of bet where the bettor predicts whether both teams will score a certain number of points
|
|
// and also the result fo the match
|
|
func evaluateResultAndBTTSX(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
|
|
// The name parameter will hold value "name": "{team_name} and {Yes | No}"
|
|
// The best way to do this is to evaluate backwards since there might be
|
|
// teams with 'and' in their name
|
|
// We know that there is going to be "Yes" and "No "
|
|
oddNameSplit := strings.Split(outcome.OddName, " ")
|
|
|
|
scoreCheckSplit := oddNameSplit[len(oddNameSplit)-1]
|
|
var isScorePoints bool
|
|
switch scoreCheckSplit {
|
|
case "Yes":
|
|
isScorePoints = true
|
|
case "No":
|
|
isScorePoints = false
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName)
|
|
}
|
|
|
|
teamName := strings.TrimSpace(strings.Join(oddNameSplit[:len(oddNameSplit)-2], ""))
|
|
|
|
threshold, err := strconv.ParseInt(outcome.OddHeader, 10, 64)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddHeader)
|
|
}
|
|
|
|
// above code removes any space from team name, so do the same for outcome.HomeTeamName and outcome.AwayTeamName
|
|
outcome.HomeTeamName = strings.Join(strings.Split(outcome.HomeTeamName, " "), "")
|
|
outcome.AwayTeamName = strings.Join(strings.Split(outcome.AwayTeamName, " "), "")
|
|
|
|
switch teamName {
|
|
case outcome.HomeTeamName:
|
|
if score.Home > score.Away {
|
|
if isScorePoints && score.Home >= int(threshold) && score.Away >= int(threshold) {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
} else if !isScorePoints && score.Home < int(threshold) && score.Away < int(threshold) {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
}
|
|
case outcome.AwayTeamName:
|
|
if score.Away > score.Home {
|
|
if isScorePoints && score.Home >= int(threshold) && score.Away >= int(threshold) {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
} else if !isScorePoints && score.Home < int(threshold) && score.Away < int(threshold) {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
}
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("team name error: %s", teamName)
|
|
}
|
|
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
|
|
}
|
|
|
|
// Both Teams To Score X Points is a type of bet where the bettor predicts whether both teams will score a certain number of points.
|
|
func evaluateBTTSX(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
threshold, err := strconv.ParseInt(outcome.OddName, 10, 64)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName)
|
|
}
|
|
|
|
switch outcome.OddHeader {
|
|
case "Yes":
|
|
if score.Home >= int(threshold) && score.Away >= int(threshold) {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
case "No":
|
|
if score.Home < int(threshold) && score.Away < int(threshold) {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
|
|
}
|
|
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
// Money Line 3 Way betting is a type of bet where the bettor predicts the outcome of a match with three possible outcomes: home win, away win, or draw.
|
|
func evaluateMoneyLine3Way(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
switch outcome.OddName {
|
|
case "1": // Home win
|
|
if score.Home > score.Away {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case "Tie":
|
|
if score.Home == score.Away {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case "2": // Away win
|
|
if score.Away > score.Home {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName)
|
|
}
|
|
}
|
|
|
|
func evaluateDoubleResult(outcome domain.BetOutcome, firstHalfScore struct{ Home, Away int }, fullTimeScore struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
halfWins := strings.Split(outcome.OddName, "-")
|
|
if len(halfWins) != 2 {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName)
|
|
}
|
|
firstHalfWinner := strings.TrimSpace(halfWins[0])
|
|
fullTimeWinner := strings.TrimSpace(halfWins[1])
|
|
|
|
if firstHalfWinner != outcome.HomeTeamName && firstHalfWinner != outcome.AwayTeamName && firstHalfWinner != "Tie" {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", firstHalfWinner)
|
|
}
|
|
if fullTimeWinner != outcome.HomeTeamName && fullTimeWinner != outcome.AwayTeamName && fullTimeWinner != "Tie" {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", firstHalfWinner)
|
|
}
|
|
|
|
switch {
|
|
case firstHalfWinner == outcome.HomeTeamName && firstHalfScore.Home < firstHalfScore.Away:
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case firstHalfWinner == outcome.AwayTeamName && firstHalfScore.Away < firstHalfScore.Home:
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case firstHalfWinner == "Tie" && firstHalfScore.Home != firstHalfScore.Away:
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
switch {
|
|
case fullTimeWinner == outcome.HomeTeamName && fullTimeScore.Home < fullTimeScore.Away:
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case fullTimeWinner == outcome.AwayTeamName && fullTimeScore.Away < fullTimeScore.Home:
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case fullTimeWinner == "Tie" && fullTimeScore.Home != fullTimeScore.Away:
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
|
|
// Highest Scoring Half betting is a type of bet where the bettor predicts which half of the match will have the highest total score.
|
|
func evaluateHighestScoringHalf(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
firstHalfTotal := firstScore.Home + firstScore.Away
|
|
secondHalfTotal := secondScore.Home + secondScore.Away
|
|
switch outcome.OddName {
|
|
case "1st Half":
|
|
if firstHalfTotal > secondHalfTotal {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
case "2nd Half":
|
|
if firstHalfTotal < secondHalfTotal {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
case "Tie":
|
|
if firstHalfTotal == secondHalfTotal {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName)
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
// Highest Scoring Quarter betting is a type of bet where the bettor predicts which quarter of the match will have the highest score.
|
|
func evaluateHighestScoringQuarter(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }, thirdScore struct{ Home, Away int }, fourthScore struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
firstQuarterTotal := firstScore.Home + firstScore.Away
|
|
secondQuarterTotal := secondScore.Home + secondScore.Away
|
|
thirdQuarterTotal := thirdScore.Home + thirdScore.Away
|
|
fourthQuarterTotal := fourthScore.Home + fourthScore.Away
|
|
|
|
switch outcome.OddName {
|
|
case "1st Quarter":
|
|
if firstQuarterTotal > secondQuarterTotal && firstQuarterTotal > thirdQuarterTotal && firstQuarterTotal > fourthQuarterTotal {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
case "2nd Quarter":
|
|
if secondQuarterTotal > firstQuarterTotal && secondQuarterTotal > thirdQuarterTotal && secondQuarterTotal > fourthQuarterTotal {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
case "3rd Quarter":
|
|
if thirdQuarterTotal > firstQuarterTotal && thirdQuarterTotal > secondQuarterTotal && thirdQuarterTotal > fourthQuarterTotal {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
case "4th Quarter":
|
|
if fourthQuarterTotal > firstQuarterTotal && fourthQuarterTotal > secondQuarterTotal && fourthQuarterTotal > thirdQuarterTotal {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
case "Tie":
|
|
if firstQuarterTotal == secondQuarterTotal || secondQuarterTotal == thirdQuarterTotal || thirdQuarterTotal == fourthQuarterTotal {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName)
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
// Team With Highest Scoring Quarter betting is a type of bet where the bettor predicts which team will have the highest score in a specific quarter.
|
|
func evaluateTeamWithHighestScoringQuarter(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }, thirdScore struct{ Home, Away int }, fourthScore struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
homeTeamHighestQuarter := max(firstScore.Home, secondScore.Home, thirdScore.Home, fourthScore.Home)
|
|
awayTeamHighestQuarter := max(firstScore.Away, secondScore.Away, thirdScore.Away, fourthScore.Away)
|
|
|
|
switch outcome.OddName {
|
|
case "1":
|
|
if homeTeamHighestQuarter > awayTeamHighestQuarter {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
case "2":
|
|
if awayTeamHighestQuarter > homeTeamHighestQuarter {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
case "Tie":
|
|
if homeTeamHighestQuarter == awayTeamHighestQuarter {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName)
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
// Handicap and Total betting is a combination of spread betting and total points betting
|
|
// where the bettor predicts the outcome of a match with a point spread and the total number of points scored is over or under a specified number.
|
|
func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
|
|
nameSplit := strings.Split(outcome.OddName, " ")
|
|
// Evaluate from bottom to get the threshold and find out if its over or under
|
|
threshold, err := strconv.ParseFloat(nameSplit[len(nameSplit)-1], 10)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing threshold: %s", outcome.OddName)
|
|
}
|
|
total := float64(score.Home + score.Away)
|
|
overUnder := nameSplit[len(nameSplit)-2]
|
|
switch overUnder {
|
|
case "Over":
|
|
if total < threshold {
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
case "Under":
|
|
if total > threshold {
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over and under: %s", outcome.OddName)
|
|
}
|
|
|
|
handicap, err := strconv.ParseFloat(nameSplit[len(nameSplit)-4], 10)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing handicap: %s", outcome.OddName)
|
|
}
|
|
|
|
teamName := strings.TrimSpace(strings.Join(nameSplit[:len(nameSplit)-4], ""))
|
|
adjustedHomeScore := float64(score.Home)
|
|
adjustedAwayScore := float64(score.Away)
|
|
|
|
outcome.HomeTeamName = strings.Join(strings.Split(outcome.HomeTeamName, " "), "")
|
|
outcome.AwayTeamName = strings.Join(strings.Split(outcome.AwayTeamName, " "), "")
|
|
|
|
switch teamName {
|
|
case outcome.HomeTeamName:
|
|
adjustedHomeScore += handicap
|
|
if adjustedHomeScore > adjustedAwayScore {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case outcome.AwayTeamName:
|
|
adjustedAwayScore += handicap
|
|
if adjustedAwayScore > adjustedHomeScore {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing team name: %s", outcome.OddName)
|
|
}
|
|
|
|
}
|
|
|
|
func evaluateWinningMargin(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
if len(outcome.OddName) < 1 {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName)
|
|
}
|
|
|
|
isGtr := false
|
|
idx := len(outcome.OddName)
|
|
if outcome.OddName[idx-1] == '+' {
|
|
isGtr = true
|
|
idx--
|
|
}
|
|
|
|
margin, err := strconv.ParseInt(outcome.OddName[:idx], 10, 64)
|
|
if err != nil {
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName)
|
|
}
|
|
|
|
switch outcome.OddHeader {
|
|
case "1":
|
|
if score.Home == (score.Away + int(margin)) {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
} else if isGtr && score.Home > (score.Away+int(margin)) {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case "2":
|
|
if score.Away == (score.Home + int(margin)) {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
} else if isGtr && score.Away > (score.Home+int(margin)) {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddheader: %s", outcome.OddHeader)
|
|
}
|
|
|
|
// Highest Scoring Period betting is a type of bet where the bettor predicts which period of the match will have the highest total score.
|
|
func evaluateHighestScoringPeriod(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }, thirdScore struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
firstPeriodTotal := firstScore.Home + firstScore.Away
|
|
secondPeriodTotal := secondScore.Home + secondScore.Away
|
|
thirdPeriodTotal := thirdScore.Home + thirdScore.Away
|
|
|
|
switch outcome.OddName {
|
|
case "Period 1":
|
|
if firstPeriodTotal > secondPeriodTotal && firstPeriodTotal > thirdPeriodTotal {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
case "Period 2":
|
|
if secondPeriodTotal > firstPeriodTotal && secondPeriodTotal > thirdPeriodTotal {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
case "Period 3":
|
|
if thirdPeriodTotal > firstPeriodTotal && thirdPeriodTotal > secondPeriodTotal {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
case "Tie":
|
|
if firstPeriodTotal == secondPeriodTotal || secondPeriodTotal == thirdPeriodTotal {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName)
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
// Tied After Regulation is a type of bet where the bettor predicts whether the match will end in a tie after regulation time.
|
|
func evaluateTiedAfterRegulation(outcome domain.BetOutcome, scores []struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
totalScore := struct{ Home, Away int }{0, 0}
|
|
for _, score := range scores {
|
|
totalScore.Home += score.Home
|
|
totalScore.Away += score.Away
|
|
}
|
|
switch outcome.OddName {
|
|
case "Yes":
|
|
if totalScore.Home == totalScore.Away {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
case "No":
|
|
if totalScore.Home != totalScore.Away {
|
|
return domain.OUTCOME_STATUS_WIN, nil
|
|
}
|
|
return domain.OUTCOME_STATUS_LOSS, nil
|
|
}
|
|
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName)
|
|
}
|
|
|
|
func evaluateVolleyballGamelines(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
switch outcome.OddName {
|
|
case "Total":
|
|
return evaluateTotalOverUnder(outcome, score)
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName)
|
|
}
|
|
}
|
|
|
|
func evaluateGameBettingTwoWay(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
|
|
switch outcome.OddName {
|
|
case "Handicap":
|
|
return evaluateAsianHandicap(outcome, score)
|
|
case "Total":
|
|
return evaluateTotalOverUnder(outcome, score)
|
|
case "To Win":
|
|
return evaluateFullTimeResult(outcome, score)
|
|
default:
|
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName)
|
|
}
|
|
}
|