package handlers import ( "encoding/json" "errors" "fmt" "log" "strconv" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" ) type launchVirtualGameReq struct { GameID string `json:"game_id" validate:"required" example:"1"` Currency string `json:"currency" validate:"required,len=3" example:"USD"` Mode string `json:"mode" validate:"required,oneof=fun real" example:"real"` } type launchVirtualGameRes struct { LaunchURL string `json:"launch_url"` } // ListVirtualGames godoc // @Summary List all virtual games // @Description Returns all virtual games with optional filters (category, search, pagination) // @Tags VirtualGames - Orchestration // @Accept json // @Produce json // @Param category query string false "Filter by category" // @Param search query string false "Search by game name" // @Param limit query int false "Pagination limit" // @Param offset query int false "Pagination offset" // @Success 200 {object} domain.Response{data=[]domain.UnifiedGame} // @Failure 400 {object} domain.ErrorResponse // @Failure 502 {object} domain.ErrorResponse // @Router /api/v1/orchestrator/virtual-games [get] func (h *Handler) ListVirtualGames(c *fiber.Ctx) error { // --- Parse query parameters --- limit := c.QueryInt("limit", 100) if limit <= 0 { limit = 100 } offset := c.QueryInt("offset", 0) if offset < 0 { offset = 0 } category := c.Query("category", "") search := c.Query("search", "") params := dbgen.GetAllVirtualGamesParams{ Column1: category, Column2: search, Limit: int32(limit), Offset: int32(offset), } // --- Call service method --- games, err := h.veliVirtualGameSvc.GetAllVirtualGames(c.Context(), params) if err != nil { log.Println("ListVirtualGames error:", err) return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ Message: "Failed to fetch virtual games", Error: err.Error(), }) } // --- Return response --- return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Virtual games fetched successfully", Data: games, StatusCode: fiber.StatusOK, Success: true, }) } // RemoveProviderHandler // @Summary Remove a virtual game provider // @Description Deletes a provider by provider_id // @Tags VirtualGames - Orchestration // @Param provider_id path string true "Provider ID" // @Success 200 // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/virtual-game/providers/{provider_id} [delete] func (h *Handler) RemoveProvider(c *fiber.Ctx) error { providerID := c.Params("providerID") if err := h.virtualGameSvc.RemoveProvider(c.Context(), providerID); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Could not remove provider") } return c.SendStatus(fiber.StatusOK) } // GetProviderHandler // @Summary Get a virtual game provider // @Description Fetches a provider by provider_id // @Tags VirtualGames - Orchestration // @Param provider_id path string true "Provider ID" // @Produce json // @Success 200 {object} domain.VirtualGameProvider // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/virtual-game/providers/{provider_id} [get] func (h *Handler) GetProviderByID(c *fiber.Ctx) error { providerID := c.Params("providerID") provider, err := h.virtualGameSvc.GetProviderByID(c.Context(), providerID) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Could not fetch provider") } return c.Status(fiber.StatusOK).JSON(provider) } // ListProvidersHandler // @Summary List virtual game providers // @Description Lists all providers with pagination // @Tags VirtualGames - Orchestration // @Produce json // @Param limit query int false "Limit" // @Param offset query int false "Offset" // @Success 200 {object} map[string]interface{} // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/virtual-game/providers [get] func (h *Handler) ListProviders(c *fiber.Ctx) error { limit, _ := strconv.Atoi(c.Query("limit", "20")) offset, _ := strconv.Atoi(c.Query("offset", "0")) providers, total, err := h.virtualGameSvc.ListProviders(c.Context(), int32(limit), int32(offset)) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Could not list providers") } return c.Status(fiber.StatusOK).JSON(fiber.Map{ "total": total, "providers": providers, }) } // SetProviderEnabledHandler // @Summary Enable/Disable a provider // @Description Sets the enabled status of a provider // @Tags VirtualGames - Orchestration // @Param provider_id path string true "Provider ID" // @Param enabled query bool true "Enable or Disable" // @Produce json // @Success 200 {object} domain.VirtualGameProvider // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/virtual-game/orchestrator/providers/status [patch] func (h *Handler) SetProviderEnabled(c *fiber.Ctx) error { providerID := c.Params("providerID") enabled, _ := strconv.ParseBool(c.Query("enabled", "true")) provider, err := h.virtualGameSvc.SetProviderEnabled(c.Context(), providerID, enabled) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Could not update provider status") } return c.Status(fiber.StatusOK).JSON(provider) } // LaunchVirtualGame godoc // @Summary Launch a PopOK virtual game // @Description Generates a URL to launch a PopOK game // @Tags Virtual Games - PopOK // @Accept json // @Produce json // @Security Bearer // @Param launch body launchVirtualGameReq true "Game launch details" // @Success 200 {object} launchVirtualGameRes // @Failure 400 {object} domain.ErrorResponse // @Failure 401 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /virtual-game/launch [post] func (h *Handler) LaunchVirtualGame(c *fiber.Ctx) error { userID, ok := c.Locals("user_id").(int64) if !ok || userID == 0 { h.logger.Error("Invalid user ID in context") return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification") } // companyID, ok := c.Locals("company_id").(int64) // if !ok || companyID == 0 { // h.logger.Error("Invalid company ID in context") // return fiber.NewError(fiber.StatusUnauthorized, "Invalid company identification") // } var req launchVirtualGameReq if err := c.BodyParser(&req); err != nil { h.logger.Error("Failed to parse LaunchVirtualGame request", "error", err) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } if valErrs, ok := h.validator.Validate(c, req); !ok { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } url, err := h.virtualGameSvc.GenerateGameLaunchURL(c.Context(), userID, req.GameID, req.Currency, req.Mode) if err != nil { h.logger.Error("Failed to generate game launch URL", "userID", userID, "gameID", req.GameID, "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to launch game") } res := launchVirtualGameRes{LaunchURL: url} return response.WriteJSON(c, fiber.StatusOK, "Game launched successfully", res, nil) } // HandleVirtualGameCallback godoc // @Summary Handle PopOK game callback // @Description Processes callbacks from PopOK for game events // @Tags Virtual Games - PopOK // @Accept json // @Produce json // @Param callback body domain.PopOKCallback true "Callback data" // @Success 200 {object} domain.ErrorResponse // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /virtual-game/callback [post] func (h *Handler) HandleVirtualGameCallback(c *fiber.Ctx) error { var callback domain.PopOKCallback if err := c.BodyParser(&callback); err != nil { h.logger.Error("Failed to parse callback", "error", err) return fiber.NewError(fiber.StatusBadRequest, "Invalid callback data") } if err := h.virtualGameSvc.HandleCallback(c.Context(), &callback); err != nil { h.logger.Error("Failed to handle callback", "transactionID", callback.TransactionID, "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to process callback") } return response.WriteJSON(c, fiber.StatusOK, "Callback processed successfully", nil, nil) } func (h *Handler) HandlePlayerInfo(c *fiber.Ctx) error { var req domain.PopOKPlayerInfoRequest if err := c.BodyParser(&req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid request") } resp, err := h.virtualGameSvc.GetPlayerInfo(c.Context(), &req) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } return c.Status(fiber.StatusOK).JSON(resp) } func (h *Handler) HandleBet(c *fiber.Ctx) error { // Read the raw body body := c.Body() if len(body) == 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Empty request body", Error: "Request body cannot be empty", }) } // Identify the provider based on request structure provider, err := IdentifyBetProvider(body) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Unrecognized request format", Error: err.Error(), }) } switch provider { case "veli": var req domain.BetRequest if err := json.Unmarshal(body, &req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid Veli bet request", Error: err.Error(), }) } res, err := h.veliVirtualGameSvc.ProcessBet(c.Context(), req) if err != nil { if errors.Is(err, veli.ErrDuplicateTransaction) { return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{ Message: "Duplicate transaction", Error: "DUPLICATE_TRANSACTION", }) } return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Veli bet processing failed", Error: err.Error(), }) } return c.JSON(res) case "popok": var req domain.PopOKBetRequest if err := json.Unmarshal(body, &req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid PopOK bet request", Error: err.Error(), }) } resp, err := h.virtualGameSvc.ProcessBet(c.Context(), &req) if err != nil { code := fiber.StatusInternalServerError switch err.Error() { case "invalid token": code = fiber.StatusUnauthorized case "insufficient balance": code = fiber.StatusBadRequest } return c.Status(code).JSON(domain.ErrorResponse{ Message: "PopOK bet processing failed", Error: err.Error(), }) } return c.JSON(resp) case "atlas": var req domain.AtlasBetRequest if err := json.Unmarshal(body, &req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid Atlas bet request", Error: err.Error(), }) } resp, err := h.atlasVirtualGameSvc.ProcessBet(c.Context(), req) if err != nil { // code := fiber.StatusInternalServerError // if errors.Is(err, ErrDuplicateTransaction) { // code = fiber.StatusConflict // } return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Atlas bet processing failed", Error: err.Error(), }) } return c.JSON(resp) default: return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Unsupported provider", Error: "Request format doesn't match any supported provider", }) } } // identifyProvider examines the request body to determine the provider // WinHandler godoc // @Summary Handle win callback (Veli or PopOK) // @Description Processes win callbacks from either Veli or PopOK providers by auto-detecting the format // @Tags Wins // @Accept json // @Produce json // @Success 200 {object} interface{} "Win processing result" // @Failure 400 {object} domain.ErrorResponse "Invalid request format" // @Failure 401 {object} domain.ErrorResponse "Authentication failed" // @Failure 409 {object} domain.ErrorResponse "Duplicate transaction" // @Failure 500 {object} domain.ErrorResponse "Internal server error" // @Router /api/v1/win [post] func (h *Handler) HandleWin(c *fiber.Ctx) error { // Read the raw body to avoid parsing issues body := c.Body() if len(body) == 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Empty request body", Error: "Request body cannot be empty", }) } // Try to identify the provider based on the request structure provider, err := identifyWinProvider(body) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Unrecognized request format", Error: err.Error(), }) } switch provider { case "veli": var req domain.WinRequest if err := json.Unmarshal(body, &req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid Veli win request", Error: err.Error(), }) } res, err := h.veliVirtualGameSvc.ProcessWin(c.Context(), req) if err != nil { if errors.Is(err, veli.ErrDuplicateTransaction) { return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{ Message: "Duplicate transaction", Error: "DUPLICATE_TRANSACTION", }) } return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Veli win processing failed", Error: err.Error(), }) } return c.JSON(res) case "popok": var req domain.PopOKWinRequest if err := json.Unmarshal(body, &req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid PopOK win request", Error: err.Error(), }) } resp, err := h.virtualGameSvc.ProcessWin(c.Context(), &req) if err != nil { code := fiber.StatusInternalServerError if err.Error() == "invalid token" { code = fiber.StatusUnauthorized } return c.Status(code).JSON(domain.ErrorResponse{ Message: "PopOK win processing failed", Error: err.Error(), }) } return c.JSON(resp) default: return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Unsupported provider", Error: "Request format doesn't match any supported provider", }) } } func (h *Handler) HandleCancel(c *fiber.Ctx) error { body := c.Body() if len(body) == 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Empty request body", Error: "Request body cannot be empty", }) } provider, err := identifyCancelProvider(body) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Unrecognized request format", Error: err.Error(), }) } switch provider { case "veli": var req domain.CancelRequest if err := json.Unmarshal(body, &req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid Veli cancel request", Error: err.Error(), }) } res, err := h.veliVirtualGameSvc.ProcessCancel(c.Context(), req) if err != nil { if errors.Is(err, veli.ErrDuplicateTransaction) { return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{ Message: "Duplicate transaction", Error: "DUPLICATE_TRANSACTION", }) } return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Veli cancel processing failed", Error: err.Error(), }) } return c.JSON(res) case "popok": var req domain.PopOKCancelRequest if err := json.Unmarshal(body, &req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid PopOK cancel request", Error: err.Error(), }) } resp, err := h.virtualGameSvc.ProcessCancel(c.Context(), &req) if err != nil { code := fiber.StatusInternalServerError switch err.Error() { case "invalid token": code = fiber.StatusUnauthorized case "original bet not found", "invalid original transaction": code = fiber.StatusBadRequest } return c.Status(code).JSON(domain.ErrorResponse{ Message: "PopOK cancel processing failed", Error: err.Error(), }) } return c.JSON(resp) default: return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Unsupported provider", Error: "Request format doesn't match any supported provider", }) } } // GetGameList godoc // @Summary Get PopOK Games List // @Description Retrieves the list of available PopOK slot games // @Tags Virtual Games - PopOK // @Accept json // @Produce json // @Param currency query string false "Currency (e.g. USD, ETB)" default(USD) // @Success 200 {array} domain.PopOKGame // @Failure 500 {object} domain.ErrorResponse // @Router /popok/games [get] func (h *Handler) GetGameList(c *fiber.Ctx) error { currency := c.Query("currency", "ETB") // fallback default games, err := h.virtualGameSvc.ListGames(c.Context(), currency) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Falied to fetch games", Error: err.Error(), }) // return fiber.NewError(fiber.StatusBadGateway, "failed to fetch games") } return c.JSON(games) } // RecommendGames godoc // @Summary Recommend virtual games // @Description Recommends games based on user history or randomly // @Tags Virtual Games - PopOK // @Produce json // @Param user_id query int true "User ID" // @Success 200 {array} domain.GameRecommendation // @Failure 500 {object} domain.ErrorResponse // @Router /popok/games/recommend [get] func (h *Handler) RecommendGames(c *fiber.Ctx) error { userIDVal := c.Locals("user_id") userID, ok := userIDVal.(int64) if !ok || userID == 0 { return fiber.NewError(fiber.StatusBadRequest, "invalid user ID") } recommendations, err := h.virtualGameSvc.RecommendGames(c.Context(), userID) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "failed to recommend games") } return c.JSON(recommendations) } func (h *Handler) HandleTournamentWin(c *fiber.Ctx) error { var req domain.PopOKWinRequest if err := c.BodyParser(&req); err != nil { h.logger.Error("Invalid tournament win request body", "error", err) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Invalid request body", }) } resp, err := h.virtualGameSvc.ProcessTournamentWin(c.Context(), &req) if err != nil { h.logger.Error("Failed to process tournament win", "error", err) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": err.Error(), }) } return c.JSON(resp) } func (h *Handler) HandlePromoWin(c *fiber.Ctx) error { var req domain.PopOKWinRequest if err := c.BodyParser(&req); err != nil { h.logger.Error("Invalid promo win request body", "error", err) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Invalid request body", }) } resp, err := h.virtualGameSvc.ProcessPromoWin(c.Context(), &req) if err != nil { h.logger.Error("Failed to process promo win", "error", err) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": err.Error(), }) } return c.JSON(resp) } // AddFavoriteGame godoc // @Summary Add game to favorites // @Description Adds a game to the user's favorite games list // @Tags VirtualGames - Favourites // @Accept json // @Produce json // @Param body body domain.FavoriteGameRequest true "Game ID to add" // @Success 201 {string} domain.Response "created" // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/virtual-game/favorites [post] func (h *Handler) AddFavorite(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) var req domain.FavoriteGameRequest if err := c.BodyParser(&req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid request") } err := h.virtualGameSvc.AddFavoriteGame(c.Context(), userID, req.GameID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Could not add favorite", Error: err.Error(), }) // return fiber.NewError(fiber.StatusInternalServerError, "Could not add favorite") } return c.Status(fiber.StatusCreated).JSON(domain.Response{ Message: "Game added to favorites", StatusCode: fiber.StatusCreated, Success: true, }) // return c.SendStatus(fiber.StatusCreated) } // RemoveFavoriteGame godoc // @Summary Remove game from favorites // @Description Removes a game from the user's favorites // @Tags VirtualGames - Favourites // @Produce json // @Param gameID path int64 true "Game ID to remove" // @Success 200 {string} domain.Response "removed" // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/virtual-game/favorites/{gameID} [delete] func (h *Handler) RemoveFavorite(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) gameID, _ := strconv.ParseInt(c.Params("gameID"), 10, 64) err := h.virtualGameSvc.RemoveFavoriteGame(c.Context(), userID, gameID) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Could not remove favorite") } return c.SendStatus(fiber.StatusOK) } // ListFavoriteGames godoc // @Summary Get user's favorite games // @Description Lists the games that the user marked as favorite // @Tags VirtualGames - Favourites // @Produce json // @Success 200 {array} domain.GameRecommendation // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/virtual-game/favorites [get] func (h *Handler) ListFavorites(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) games, err := h.virtualGameSvc.ListFavoriteGames(c.Context(), userID) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Could not fetch favorites") } return c.Status(fiber.StatusOK).JSON(games) } func IdentifyBetProvider(body []byte) (string, error) { // Check for Veli signature fields var veliCheck struct { SessionID string `json:"sessionId"` BrandID string `json:"brandId"` } if json.Unmarshal(body, &veliCheck) == nil { if veliCheck.SessionID != "" && veliCheck.BrandID != "" { return "veli", nil } } // Check for PopOK signature fields var popokCheck struct { Token string `json:"externalToken"` } if json.Unmarshal(body, &popokCheck) == nil { if popokCheck.Token != "" { return "popok", nil } } var atlasCheck struct { CasinoID string `json:"casino_id"` SessionID string `json:"session_id"` } if json.Unmarshal(body, &atlasCheck) == nil { if atlasCheck.CasinoID != "" && atlasCheck.SessionID != "" { return "atlas", nil } } return "", fmt.Errorf("could not identify provider from request structure") } // identifyWinProvider examines the request body to determine the provider for win callbacks func identifyWinProvider(body []byte) (string, error) { // Check for Veli signature fields var veliCheck struct { SessionID string `json:"sessionId"` BrandID string `json:"brandId"` } if json.Unmarshal(body, &veliCheck) == nil { if veliCheck.SessionID != "" && veliCheck.BrandID != "" { return "veli", nil } } // Check for PopOK signature fields var popokCheck struct { Token string `json:"externalToken"` } if json.Unmarshal(body, &popokCheck) == nil { if popokCheck.Token != "" { return "popok", nil } } return "", fmt.Errorf("could not identify provider from request structure") } func identifyCancelProvider(body []byte) (string, error) { // Check for Veli cancel signature var veliCheck struct { SessionID string `json:"sessionId"` BrandID string `json:"brandId"` } if json.Unmarshal(body, &veliCheck) == nil { if veliCheck.SessionID != "" && veliCheck.BrandID != "" { return "veli", nil } } // Check for PopOK cancel signature var popokCheck struct { Token string `json:"externalToken"` } if json.Unmarshal(body, &popokCheck) == nil { if popokCheck.Token != "" { return "popok", nil } } return "", fmt.Errorf("could not identify cancel provider from request structure") }