Yimaru-BackEnd/internal/services/ticket/service.go
2025-06-19 19:58:37 +03:00

257 lines
8.1 KiB
Go

package ticket
import (
"context"
"encoding/json"
"errors"
"strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
"go.uber.org/zap"
)
var (
// ErrGenerateRandomOutcome = errors.New("Failed to generate any random outcome for events")
// ErrOutcomesNotCompleted = errors.New("Some bet outcomes are still pending")
ErrEventHasNotEnded = errors.New("Event has not ended yet")
ErrNoEventsAvailable = errors.New("Not enough events available with the given filters")
ErrEventHasBeenRemoved = errors.New("Event has been removed")
ErrTooManyOutcomesForTicket = errors.New("Too many odds/outcomes for a single ticket")
ErrTicketAmountTooHigh = errors.New("Cannot create a ticket with an amount above limit")
ErrTicketLimitForSingleUser = errors.New("Number of Ticket Limit reached")
ErrTicketWinningTooHigh = errors.New("Total Winnings over set limit")
ErrRawOddInvalid = errors.New("Prematch Raw Odd is Invalid")
)
type Service struct {
ticketStore TicketStore
eventSvc event.Service
prematchSvc odds.ServiceImpl
mongoLogger *zap.Logger
}
func NewService(
ticketStore TicketStore,
eventSvc event.Service,
prematchSvc odds.ServiceImpl,
mongoLogger *zap.Logger,
) *Service {
return &Service{
ticketStore: ticketStore,
eventSvc: eventSvc,
prematchSvc: prematchSvc,
mongoLogger: mongoLogger,
}
}
func (s *Service) GenerateTicketOutcome(ctx context.Context, eventID int64, marketID int64, oddID int64) (domain.CreateTicketOutcome, 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.CreateTicketOutcome{}, ErrEventHasBeenRemoved
}
// Checking to make sure the event hasn't already started
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.CreateTicketOutcome{}, 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 response.WriteJSON(c, fiber.StatusBadRequest, "Invalid market id", err, nil)
}
type rawOddType struct {
ID string
Name string
Odds string
Header string
Handicap string
}
var selectedOdd rawOddType
var isOddFound bool = false
for _, raw := range odds.RawOdds {
var rawOdd rawOddType
rawBytes, err := json.Marshal(raw)
err = json.Unmarshal(rawBytes, &rawOdd)
if err != nil {
s.mongoLogger.Error("failed to unmarshal raw ods",
zap.Int64("event_id", eventID),
zap.String("rawOddID", rawOdd.ID),
zap.Error(err),
)
continue
}
if rawOdd.ID == oddIDStr {
selectedOdd = rawOdd
isOddFound = true
}
}
if !isOddFound {
// return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid odd id", nil, nil)
s.mongoLogger.Error("Invalid Odd ID",
zap.Int64("event_id", eventID),
zap.String("oddIDStr", oddIDStr),
)
return domain.CreateTicketOutcome{}, 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.CreateTicketOutcome{}, err
}
newOutcome := domain.CreateTicketOutcome{
EventID: eventID,
OddID: oddID,
MarketID: marketID,
HomeTeamName: event.HomeTeam,
AwayTeamName: event.AwayTeam,
MarketName: odds.MarketName,
Odd: float32(parsedOdd),
OddName: selectedOdd.Name,
OddHeader: selectedOdd.Header,
OddHandicap: selectedOdd.Handicap,
Expires: event.StartTime,
}
// outcomes = append(outcomes, )
return newOutcome, nil
}
func (s *Service) CreateTicket(ctx context.Context, req domain.CreateTicketReq, clientIP string) (domain.Ticket, int64, error) {
// s.mongoLogger.Info("Creating ticket")
// TODO Validate Outcomes Here and make sure they didn't expire
// Validation for creating tickets
if len(req.Outcomes) > 30 {
// return response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, nil)
return domain.Ticket{}, 0, ErrTooManyOutcomesForTicket
}
if req.Amount > 100000 {
// return response.WriteJSON(c, fiber.StatusBadRequest, "Cannot create a ticket with an amount above 100,000 birr", nil, nil)
return domain.Ticket{}, 0, ErrTicketAmountTooHigh
}
count, err := s.CountTicketByIP(ctx, clientIP)
if err != nil {
// return response.WriteJSON(c, fiber.StatusInternalServerError, "Error fetching user info", nil, nil)
return domain.Ticket{}, 0, err
}
if count > 50 {
// return response.WriteJSON(c, fiber.StatusBadRequest, "Ticket Limit reached", nil, nil)
return domain.Ticket{}, 0, ErrTicketLimitForSingleUser
}
var outcomes []domain.CreateTicketOutcome = make([]domain.CreateTicketOutcome, 0, len(req.Outcomes))
var totalOdds float32 = 1
for _, outcomeReq := range req.Outcomes {
newOutcome, err := s.GenerateTicketOutcome(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.Error(err),
)
return domain.Ticket{}, 0, err
}
totalOdds *= float32(newOutcome.Odd)
outcomes = append(outcomes, newOutcome)
}
totalWinnings := req.Amount * totalOdds
if totalWinnings > 1000000 {
s.mongoLogger.Error("Total Winnings over limit", zap.Float32("Total Odds", totalOdds), zap.Float32("amount", req.Amount))
// return response.WriteJSON(c, fiber.StatusBadRequest, "Cannot create a ticket with 1,000,000 winnings", nil, nil)
return domain.Ticket{}, 0, ErrTicketWinningTooHigh
}
ticket, err := s.ticketStore.CreateTicket(ctx, domain.CreateTicket{
Amount: domain.ToCurrency(req.Amount),
TotalOdds: totalOdds,
IP: clientIP,
})
if err != nil {
s.mongoLogger.Error("Error Creating Ticket", zap.Float32("Total Odds", totalOdds), zap.Float32("amount", req.Amount))
return domain.Ticket{}, 0, err
}
// Add the ticket id now that it has fetched from the database
for index := range outcomes {
outcomes[index].TicketID = ticket.ID
}
rows, err := s.CreateTicketOutcome(ctx, outcomes)
if err != nil {
s.mongoLogger.Error("Error Creating Ticket Outcomes", zap.Any("outcomes", outcomes))
return domain.Ticket{}, rows, err
}
return ticket, rows, nil
}
// func (s *Service) CreateTicket(ctx context.Context, ticket domain.CreateTicket) (domain.Ticket, error) {
// return s.ticketStore.CreateTicket(ctx, ticket)
// }
func (s *Service) CreateTicketOutcome(ctx context.Context, outcomes []domain.CreateTicketOutcome) (int64, error) {
return s.ticketStore.CreateTicketOutcome(ctx, outcomes)
}
func (s *Service) GetTicketByID(ctx context.Context, id int64) (domain.GetTicket, error) {
return s.ticketStore.GetTicketByID(ctx, id)
}
func (s *Service) GetAllTickets(ctx context.Context) ([]domain.GetTicket, error) {
return s.ticketStore.GetAllTickets(ctx)
}
func (s *Service) CountTicketByIP(ctx context.Context, IP string) (int64, error) {
return s.ticketStore.CountTicketByIP(ctx, IP)
}
func (s *Service) UpdateTicketOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error {
return s.ticketStore.UpdateTicketOutcomeStatus(ctx, id, status)
}
func (s *Service) DeleteTicket(ctx context.Context, id int64) error {
return s.ticketStore.DeleteTicket(ctx, id)
}
func (s *Service) DeleteOldTickets(ctx context.Context) error {
return s.ticketStore.DeleteOldTickets(ctx)
}