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) }