From 66a7affebad8af5a1d486de23812e52e7c8b0ba8 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Thu, 22 May 2025 18:35:20 +0300 Subject: [PATCH 1/7] chapa integration 1st phase --- cmd/main.go | 17 +- cmd/router.go | 9 + go.mod | 3 +- go.sum | 6 - internal/config/config.go | 21 +- internal/models/models.chapa.go | 2 + internal/router/router.chapa.go | 37 ++++ internal/services/wallet/chapa.go | 1 - internal/utils/utils.chapa.go | 19 ++ internal/web_server/app.go | 4 + internal/web_server/handlers/chapa.go | 250 +++++++++++++++++++++++ internal/web_server/handlers/handlers.go | 4 + internal/web_server/routes.go | 12 ++ makefile | 6 +- 14 files changed, 377 insertions(+), 14 deletions(-) create mode 100644 cmd/router.go create mode 100644 internal/models/models.chapa.go create mode 100644 internal/router/router.chapa.go delete mode 100644 internal/services/wallet/chapa.go create mode 100644 internal/utils/utils.chapa.go create mode 100644 internal/web_server/handlers/chapa.go diff --git a/cmd/main.go b/cmd/main.go index f6fd907..6eedda0 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -7,12 +7,15 @@ import ( "os" "github.com/go-playground/validator/v10" + // "github.com/gofiber/fiber/v2" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger" mockemail "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_email" mocksms "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_sms" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" + + // "github.com/SamuelTariku/FortuneBet-Backend/internal/router" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" @@ -27,6 +30,8 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" + + // "github.com/SamuelTariku/FortuneBet-Backend/internal/utils" httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" @@ -46,6 +51,16 @@ import ( // @name Authorization // @BasePath / func main() { + + // utils.Init() + + // fiberApp := fiber.New() + + // fiberApp.Get("/health", func(c *fiber.Ctx) error { + // return c.SendString("Betting service is up and running!") + // }) + // router.ChapaRoutes(fiberApp) + cfg, err := config.NewConfig() if err != nil { slog.Error(" Config error:", "err", err) @@ -92,7 +107,7 @@ func main() { JwtAccessKey: cfg.JwtKey, JwtAccessExpiry: cfg.AccessExpiry, }, userSvc, - ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc, referalSvc, virtualGameSvc, resultSvc) + ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc, referalSvc, virtualGameSvc, resultSvc, cfg) logger.Info("Starting server", "port", cfg.Port) if err := app.Run(); err != nil { diff --git a/cmd/router.go b/cmd/router.go new file mode 100644 index 0000000..5c95681 --- /dev/null +++ b/cmd/router.go @@ -0,0 +1,9 @@ +package main + +import "github.com/gofiber/fiber/v2" + +func SetupRoutes(app *fiber.App) { + app.Get("/health", func(c *fiber.Ctx) error { + return c.SendString("Betting service is up and running!") + }) +} diff --git a/go.mod b/go.mod index a510af6..5a8e5eb 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/andybalholm/brotli v1.1.1 // indirect + // github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cloudwego/base64x v0.1.5 // indirect github.com/fasthttp/websocket v1.5.8 // indirect @@ -31,7 +32,7 @@ require ( github.com/go-openapi/swag v0.23.1 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/gofiber/contrib/websocket v1.3.4 + // github.com/gofiber/contrib/websocket v1.3.4 github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect diff --git a/go.sum b/go.sum index 9e77972..87d14e2 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQtdek= -github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs= github.com/fasthttp/websocket v1.5.8 h1:k5DpirKkftIF/w1R8ZzjSgARJrs54Je9YJK37DL/Ah8= github.com/fasthttp/websocket v1.5.8/go.mod h1:d08g8WaT6nnyvg9uMm8K9zMYyDjfKyj3170AtPRuVU0= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= @@ -51,8 +49,6 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= -github.com/gofiber/contrib/websocket v1.3.4 h1:tWeBdbJ8q0WFQXariLN4dBIbGH9KBU75s0s7YXplOSg= -github.com/gofiber/contrib/websocket v1.3.4/go.mod h1:kTFBPC6YENCnKfKx0BoOFjgXxdz7E85/STdkmZPEmPs= github.com/gofiber/fiber/v2 v2.32.0/go.mod h1:CMy5ZLiXkn6qwthrl03YMyW1NLfj0rhxz2LKl4t7ZTY= github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= @@ -118,8 +114,6 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= -github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 h1:KanIMPX0QdEdB4R3CiimCAbxFrhB3j7h0/OvpYGVQa8= github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= diff --git a/internal/config/config.go b/internal/config/config.go index e58f153..9418707 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -21,7 +21,7 @@ var ( ErrInvalidLevel = errors.New("invalid log level") ErrInvalidEnv = errors.New("env not set or invalid") ErrInvalidSMSAPIKey = errors.New("SMS API key is invalid") - ErrMissingBetToken = errors.New("missing BET365_TOKEN in .env") + ErrMissingBetToken = errors.New("missing BET365_TOKEN in .env") ErrInvalidPopOKClientID = errors.New("PopOK client ID is invalid") ErrInvalidPopOKSecretKey = errors.New("PopOK secret key is invalid") ErrInvalidPopOKBaseURL = errors.New("PopOK base URL is invalid") @@ -40,7 +40,13 @@ type Config struct { AFRO_SMS_SENDER_NAME string AFRO_SMS_RECEIVER_PHONE_NUMBER string ADRO_SMS_HOST_URL string - Bet365Token string + CHAPA_SECRET_KEY string + CHAPA_PUBLIC_KEY string + CHAPA_BASE_URL string + CHAPA_ENCRYPTION_KEY string + CHAPA_CALLBACK_URL string + CHAPA_RETURN_URL string + Bet365Token string PopOK domain.PopOKConfig } @@ -115,6 +121,17 @@ func (c *Config) loadEnv() error { if !ok { return ErrInvalidLevel } + + c.CHAPA_SECRET_KEY = os.Getenv("CHAPA_SECRET_KEY") + c.CHAPA_PUBLIC_KEY = os.Getenv("CHAPA_PUBLIC_KEY") + c.CHAPA_ENCRYPTION_KEY = os.Getenv("CHAPA_ENCRYPTION_KEY") + c.CHAPA_BASE_URL = os.Getenv("CHAPA_BASE_URL") + if c.CHAPA_BASE_URL == "" { + c.CHAPA_BASE_URL = "https://api.chapa.co/v1" + } + c.CHAPA_CALLBACK_URL = os.Getenv("CHAPA_CALLBACK_URL") + c.CHAPA_RETURN_URL = os.Getenv("CHAPA_RETURN_URL") + c.LogLevel = lvl c.AFRO_SMS_API_KEY = os.Getenv("AFRO_SMS_API_KEY") diff --git a/internal/models/models.chapa.go b/internal/models/models.chapa.go new file mode 100644 index 0000000..d6a05a4 --- /dev/null +++ b/internal/models/models.chapa.go @@ -0,0 +1,2 @@ +package models + diff --git a/internal/router/router.chapa.go b/internal/router/router.chapa.go new file mode 100644 index 0000000..6b5a09e --- /dev/null +++ b/internal/router/router.chapa.go @@ -0,0 +1,37 @@ +package router + +// @title FortuneBet Chapa API + +// import ( +// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" +// "github.com/gofiber/fiber/v2" +// ) + +// func ChapaRoutes(app *fiber.App) { + +// chapaRouter := app.Group("/api/v1/chapa") + +// chapaRouter.Post("/payments/initialize", +// wallet.InitializePayment, +// ) + +// chapaRouter.Get("/payments/verify/:tx_ref", +// wallet.VerifyTransaction, +// ) + +// chapaRouter.Post("/payments/callback", +// wallet.ReceiveWebhook, +// ) + +// chapaRouter.Get("/banks", +// wallet.GetBanks, +// ) + +// chapaRouter.Post("/transfers", +// wallet.CreateTransfer, +// ) + +// chapaRouter.Get("/transfers/:transfer_ref", +// wallet.VerifyTransfer, +// ) +// } diff --git a/internal/services/wallet/chapa.go b/internal/services/wallet/chapa.go deleted file mode 100644 index 23a7507..0000000 --- a/internal/services/wallet/chapa.go +++ /dev/null @@ -1 +0,0 @@ -package wallet diff --git a/internal/utils/utils.chapa.go b/internal/utils/utils.chapa.go new file mode 100644 index 0000000..5005d3a --- /dev/null +++ b/internal/utils/utils.chapa.go @@ -0,0 +1,19 @@ +package utils + +// import ( +// "log" +// "os" + +// "github.com/SamuelTariku/FortuneBet-Backend/internal/models" +// ) + +// func Init() { +// if err != nil { +// log.Println("No .env file found") +// } +// models.ChapaSecret = os.Getenv("CHAPA_SECRET_KEY") +// models.ChapaBaseURL = os.Getenv("CHAPA_BASE_URL") +// if models.ChapaBaseURL == "" { +// models.ChapaBaseURL = "https://api.chapa.co/v1" +// } +// } diff --git a/internal/web_server/app.go b/internal/web_server/app.go index f3e50bd..c41d9f0 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -4,6 +4,7 @@ import ( "fmt" "log/slog" + "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" @@ -47,6 +48,7 @@ type App struct { prematchSvc *odds.ServiceImpl eventSvc event.Service resultSvc *result.Service + cfg *config.Config } func NewApp( @@ -67,6 +69,7 @@ func NewApp( referralSvc referralservice.ReferralStore, virtualGameSvc virtualgameservice.VirtualGameService, resultSvc *result.Service, + cfg *config.Config, ) *App { app := fiber.New(fiber.Config{ CaseSensitive: true, @@ -103,6 +106,7 @@ func NewApp( eventSvc: eventSvc, virtualGameSvc: virtualGameSvc, resultSvc: resultSvc, + cfg: cfg, } s.initAppRoutes() diff --git a/internal/web_server/handlers/chapa.go b/internal/web_server/handlers/chapa.go new file mode 100644 index 0000000..9165226 --- /dev/null +++ b/internal/web_server/handlers/chapa.go @@ -0,0 +1,250 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + // "github.com/SamuelTariku/FortuneBet-Backend/internal/config" + "github.com/gofiber/fiber/v2" +) + +var ( + ChapaSecret string + ChapaBaseURL string +) + +type InitPaymentRequest struct { + Amount string `json:"amount"` + Currency string `json:"currency"` + Email string `json:"email"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + TxRef string `json:"tx_ref"` + CallbackURL string `json:"callback_url"` + ReturnURL string `json:"return_url"` +} + +type TransferRequest struct { + AccountNumber string `json:"account_number"` + BankCode string `json:"bank_code"` + Amount string `json:"amount"` + Currency string `json:"currency"` + Reference string `json:"reference"` + Reason string `json:"reason"` + RecipientName string `json:"recipient_name"` +} + +func (h *Handler) GetBanks(c *fiber.Ctx) error { + httpReq, err := http.NewRequest("GET", h.Cfg.CHAPA_BASE_URL+"/banks", nil) + // log.Printf("\n\nbase url is: %v\n\n", h.Cfg.CHAPA_BASE_URL) + if err != nil { + return c.Status(500).JSON(fiber.Map{"error": "Failed to create request", "details": err.Error()}) + } + httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY) + + resp, err := http.DefaultClient.Do(httpReq) + if err != nil { + return c.Status(500).JSON(fiber.Map{"error": "Failed to fetch banks", "details": err.Error()}) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return c.Status(500).JSON(fiber.Map{"error": "Failed to read response", "details": err.Error()}) + } + + return c.Status(resp.StatusCode).Send(body) +} + +func (h *Handler) InitializePayment(c *fiber.Ctx) error { + var req InitPaymentRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request body", + "details": err.Error(), + }) + } + + payload, err := json.Marshal(req) + if err != nil { + return c.Status(500).JSON(fiber.Map{ + "error": "Failed to serialize request", + "details": err.Error(), + }) + } + + httpReq, err := http.NewRequest("POST", h.Cfg.CHAPA_BASE_URL+"/transaction/initialize", bytes.NewBuffer(payload)) + if err != nil { + return c.Status(500).JSON(fiber.Map{ + "error": "Failed to create request", + "details": err.Error(), + }) + } + httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY) + httpReq.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(httpReq) + if err != nil { + return c.Status(500).JSON(fiber.Map{ + "error": "Failed to initialize payment", + "details": err.Error(), + }) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return c.Status(500).JSON(fiber.Map{ + "error": "Failed to read response", + "details": err.Error(), + }) + } + + return c.Status(resp.StatusCode).Send(body) +} + +func (h *Handler) VerifyTransaction(c *fiber.Ctx) error { + txRef := c.Params("tx_ref") + if txRef == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Missing transaction reference", + }) + } + + url := fmt.Sprintf("%s/transaction/verify/%s", h.Cfg.CHAPA_BASE_URL, txRef) + + httpReq, err := http.NewRequest("GET", url, nil) + if err != nil { + return c.Status(500).JSON(fiber.Map{ + "error": "Failed to create request", + "details": err.Error(), + }) + } + httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY) + + resp, err := http.DefaultClient.Do(httpReq) + if err != nil { + return c.Status(500).JSON(fiber.Map{ + "error": "Failed to verify transaction", + "details": err.Error(), + }) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return c.Status(500).JSON(fiber.Map{ + "error": "Failed to read response", + "details": err.Error(), + }) + } + + return c.Status(resp.StatusCode).Send(body) +} + +func (h *Handler) ReceiveWebhook(c *fiber.Ctx) error { + var payload map[string]interface{} + if err := c.BodyParser(&payload); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid webhook data", + "details": err.Error(), + }) + } + + h.logger.Info("Chapa webhook received", "payload", payload) + + // Optional: you can verify tx_ref here again if needed + + return c.SendStatus(fiber.StatusOK) +} + +func (h *Handler) CreateTransfer(c *fiber.Ctx) error { + var req TransferRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request", + "details": err.Error(), + }) + } + + payload, err := json.Marshal(req) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Failed to serialize request", + "details": err.Error(), + }) + } + + httpReq, err := http.NewRequest("POST", h.Cfg.CHAPA_BASE_URL+"/transfers", bytes.NewBuffer(payload)) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Failed to create HTTP request", + "details": err.Error(), + }) + } + + httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY) + httpReq.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(httpReq) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Transfer request failed", + "details": err.Error(), + }) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Failed to read response", + "details": err.Error(), + }) + } + + return c.Status(resp.StatusCode).Send(body) +} + +func (h *Handler) VerifyTransfer(c *fiber.Ctx) error { + transferRef := c.Params("transfer_ref") + if transferRef == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Missing transfer reference in URL", + }) + } + + url := fmt.Sprintf("%s/transfers/%s", h.Cfg.CHAPA_BASE_URL, transferRef) + + httpReq, err := http.NewRequest("GET", url, nil) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Failed to create HTTP request", + "details": err.Error(), + }) + } + + httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY) + + resp, err := http.DefaultClient.Do(httpReq) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Verification request failed", + "details": err.Error(), + }) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Failed to read response body", + "details": err.Error(), + }) + } + + return c.Status(resp.StatusCode).Send(body) +} diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index b5f811d..2b4b836 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -3,6 +3,7 @@ package handlers import ( "log/slog" + "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" @@ -37,6 +38,7 @@ type Handler struct { authSvc *authentication.Service jwtConfig jwtutil.JwtConfig validator *customvalidator.CustomValidator + Cfg *config.Config } func New( @@ -56,6 +58,7 @@ func New( companySvc *company.Service, prematchSvc *odds.ServiceImpl, eventSvc event.Service, + cfg *config.Config, ) *Handler { return &Handler{ logger: logger, @@ -74,5 +77,6 @@ func New( virtualGameSvc: virtualGameSvc, authSvc: authSvc, jwtConfig: jwtConfig, + Cfg: cfg, } } diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 7b7e22a..6319bc6 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -5,7 +5,10 @@ import ( "strconv" _ "github.com/SamuelTariku/FortuneBet-Backend/docs" + // "github.com/SamuelTariku/FortuneBet-Backend/internal/config" + // "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + // "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/handlers" "github.com/gofiber/fiber/v2" @@ -30,6 +33,7 @@ func (a *App) initAppRoutes() { a.companySvc, a.prematchSvc, a.eventSvc, + a.cfg, ) a.fiber.Get("/", func(c *fiber.Ctx) error { @@ -162,6 +166,14 @@ func (a *App) initAppRoutes() { a.fiber.Get("/transfer/wallet/:id", a.authMiddleware, h.GetTransfersByWallet) a.fiber.Post("/transfer/refill/:id", a.authMiddleware, h.RefillWallet) + //Chapa Routes + a.fiber.Post("/api/v1/chapa/payments/initialize", a.authMiddleware, h.InitializePayment) + a.fiber.Get("/api/v1/chapa/payments/verify/:tx_ref", a.authMiddleware, h.VerifyTransaction) + a.fiber.Post("/api/v1/chapa/payments/callback", a.authMiddleware, h.ReceiveWebhook) + a.fiber.Get("/api/v1/chapa/banks", a.authMiddleware, h.GetBanks) + a.fiber.Post("/api/v1/chapa/transfers", a.authMiddleware, h.CreateTransfer) + a.fiber.Get("/api/v1/chapa/transfers/:transfer_ref", a.authMiddleware, h.VerifyTransfer) + // Transactions /transactions a.fiber.Post("/transaction", a.authMiddleware, h.CreateTransaction) a.fiber.Get("/transaction", a.authMiddleware, h.GetAllTransactions) diff --git a/makefile b/makefile index 79017cf..ebf6b14 100644 --- a/makefile +++ b/makefile @@ -9,7 +9,7 @@ coverage: @go tool cover -func=coverage.out -o coverage/coverage.txt .PHONY: build build: - @go build -ldflags="-s" -o ./bin/web ./ + @go build -ldflags="-s" -o ./bin/web ./cmd/main.go .PHONY: run run: @echo "Running Go application" @@ -18,7 +18,7 @@ run: air: @echo "Running air" @air -c .air.toml -.PHONY: migrations/up +.PHONY: migrations/new migrations/new: @echo 'Creating migration files for DB_URL' @migrate create -seq -ext=.sql -dir=./db/migrations $(name) @@ -38,4 +38,4 @@ db-down: docker compose -f compose.db.yaml down .PHONY: sqlc-gen sqlc-gen: - @sqlc generate + @sqlc generate \ No newline at end of file From 991359f8059a27730b7cd023ac2a51bbf9ecf982 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Fri, 23 May 2025 16:26:32 +0300 Subject: [PATCH 2/7] chapa models+swagger+unit_test --- cmd/main.go | 9 - cmd/router.go | 9 - docs/docs.go | 506 +++++++++++++++++++ docs/swagger.json | 506 +++++++++++++++++++ docs/swagger.yaml | 331 ++++++++++++ internal/models/models.chapa.go | 2 - internal/utils/utils.chapa.go | 19 - internal/web_server/handlers/chapa.go | 107 ++-- internal/web_server/handlers/models.chapa.go | 107 ++++ internal/web_server/routes.go | 12 +- 10 files changed, 1530 insertions(+), 78 deletions(-) delete mode 100644 cmd/router.go delete mode 100644 internal/models/models.chapa.go delete mode 100644 internal/utils/utils.chapa.go create mode 100644 internal/web_server/handlers/models.chapa.go diff --git a/cmd/main.go b/cmd/main.go index 6eedda0..0045fca 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -52,15 +52,6 @@ import ( // @BasePath / func main() { - // utils.Init() - - // fiberApp := fiber.New() - - // fiberApp.Get("/health", func(c *fiber.Ctx) error { - // return c.SendString("Betting service is up and running!") - // }) - // router.ChapaRoutes(fiberApp) - cfg, err := config.NewConfig() if err != nil { slog.Error(" Config error:", "err", err) diff --git a/cmd/router.go b/cmd/router.go deleted file mode 100644 index 5c95681..0000000 --- a/cmd/router.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -import "github.com/gofiber/fiber/v2" - -func SetupRoutes(app *fiber.App) { - app.Get("/health", func(c *fiber.Ctx) error { - return c.SendString("Betting service is up and running!") - }) -} diff --git a/docs/docs.go b/docs/docs.go index 72da21c..f9b5d61 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -129,6 +129,267 @@ const docTemplate = `{ } } }, + "/api/v1/chapa/banks": { + "get": { + "description": "Fetch all supported banks from Chapa", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Get list of banks", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.ChapaSupportedBanksResponse" + } + } + } + } + }, + "/api/v1/chapa/payments/callback": { + "post": { + "description": "Endpoint to receive webhook payloads from Chapa", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Receive Chapa webhook", + "parameters": [ + { + "description": "Webhook Payload (dynamic)", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "type": "object" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/chapa/payments/initialize": { + "post": { + "description": "Initiate a payment through Chapa", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Initialize a payment transaction", + "parameters": [ + { + "description": "Payment initialization request", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.InitPaymentRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.InitPaymentResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/chapa/payments/verify/{tx_ref}": { + "get": { + "description": "Verify the transaction status from Chapa using tx_ref", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Verify a payment transaction", + "parameters": [ + { + "type": "string", + "description": "Transaction Reference", + "name": "tx_ref", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.VerifyTransactionResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/chapa/transfers": { + "post": { + "description": "Initiate a transfer request via Chapa", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Create a money transfer", + "parameters": [ + { + "description": "Transfer request body", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.TransferRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CreateTransferResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/chapa/transfers/verify/{transfer_ref}": { + "get": { + "description": "Check the status of a money transfer via reference", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Verify a transfer", + "parameters": [ + { + "type": "string", + "description": "Transfer Reference", + "name": "transfer_ref", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.VerifyTransferResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/auth/login": { "post": { "description": "Login customer", @@ -3903,6 +4164,73 @@ const docTemplate = `{ } } }, + "handlers.ChapaSupportedBank": { + "type": "object", + "properties": { + "acct_length": { + "type": "integer" + }, + "acct_number_regex": { + "type": "string" + }, + "active": { + "type": "integer" + }, + "country_id": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "example_value": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_24hrs": { + "type": "integer" + }, + "is_active": { + "type": "integer" + }, + "is_mobilemoney": { + "type": "integer" + }, + "is_rtgs": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "swift": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "handlers.ChapaSupportedBanksResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ChapaSupportedBank" + } + }, + "message": { + "type": "string" + } + } + }, "handlers.CheckPhoneEmailExistReq": { "type": "object", "properties": { @@ -4286,6 +4614,20 @@ const docTemplate = `{ } } }, + "handlers.CreateTransferResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.TransferData" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "handlers.CustomerWalletRes": { "type": "object", "properties": { @@ -4328,6 +4670,62 @@ const docTemplate = `{ } } }, + "handlers.InitPaymentData": { + "type": "object", + "properties": { + "checkout_url": { + "type": "string" + }, + "tx_ref": { + "type": "string" + } + } + }, + "handlers.InitPaymentRequest": { + "type": "object", + "properties": { + "amount": { + "type": "string" + }, + "callback_url": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "return_url": { + "type": "string" + }, + "tx_ref": { + "type": "string" + } + } + }, + "handlers.InitPaymentResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.InitPaymentData" + }, + "message": { + "description": "e.g., \"Payment initialized\"", + "type": "string" + }, + "status": { + "description": "\"success\"", + "type": "string" + } + } + }, "handlers.ManagersRes": { "type": "object", "properties": { @@ -4505,6 +4903,26 @@ const docTemplate = `{ } } }, + "handlers.TransactionData": { + "type": "object", + "properties": { + "amount": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "email": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tx_ref": { + "type": "string" + } + } + }, "handlers.TransactionRes": { "type": "object", "properties": { @@ -4603,6 +5021,66 @@ const docTemplate = `{ } } }, + "handlers.TransferData": { + "type": "object", + "properties": { + "amount": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "handlers.TransferRequest": { + "type": "object", + "properties": { + "account_number": { + "type": "string" + }, + "amount": { + "type": "string" + }, + "bank_code": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "recipient_name": { + "type": "string" + }, + "reference": { + "type": "string" + } + } + }, + "handlers.TransferVerificationData": { + "type": "object", + "properties": { + "account_name": { + "type": "string" + }, + "bank_code": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "handlers.TransferWalletRes": { "type": "object", "properties": { @@ -4734,6 +5212,34 @@ const docTemplate = `{ } } }, + "handlers.VerifyTransactionResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.TransactionData" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "handlers.VerifyTransferResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.TransferVerificationData" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "handlers.WalletRes": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 123e78f..b01b99e 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -121,6 +121,267 @@ } } }, + "/api/v1/chapa/banks": { + "get": { + "description": "Fetch all supported banks from Chapa", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Get list of banks", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.ChapaSupportedBanksResponse" + } + } + } + } + }, + "/api/v1/chapa/payments/callback": { + "post": { + "description": "Endpoint to receive webhook payloads from Chapa", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Receive Chapa webhook", + "parameters": [ + { + "description": "Webhook Payload (dynamic)", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "type": "object" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/chapa/payments/initialize": { + "post": { + "description": "Initiate a payment through Chapa", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Initialize a payment transaction", + "parameters": [ + { + "description": "Payment initialization request", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.InitPaymentRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.InitPaymentResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/chapa/payments/verify/{tx_ref}": { + "get": { + "description": "Verify the transaction status from Chapa using tx_ref", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Verify a payment transaction", + "parameters": [ + { + "type": "string", + "description": "Transaction Reference", + "name": "tx_ref", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.VerifyTransactionResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/chapa/transfers": { + "post": { + "description": "Initiate a transfer request via Chapa", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Create a money transfer", + "parameters": [ + { + "description": "Transfer request body", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.TransferRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CreateTransferResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/chapa/transfers/verify/{transfer_ref}": { + "get": { + "description": "Check the status of a money transfer via reference", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Verify a transfer", + "parameters": [ + { + "type": "string", + "description": "Transfer Reference", + "name": "transfer_ref", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.VerifyTransferResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/auth/login": { "post": { "description": "Login customer", @@ -3895,6 +4156,73 @@ } } }, + "handlers.ChapaSupportedBank": { + "type": "object", + "properties": { + "acct_length": { + "type": "integer" + }, + "acct_number_regex": { + "type": "string" + }, + "active": { + "type": "integer" + }, + "country_id": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "example_value": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_24hrs": { + "type": "integer" + }, + "is_active": { + "type": "integer" + }, + "is_mobilemoney": { + "type": "integer" + }, + "is_rtgs": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "swift": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "handlers.ChapaSupportedBanksResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ChapaSupportedBank" + } + }, + "message": { + "type": "string" + } + } + }, "handlers.CheckPhoneEmailExistReq": { "type": "object", "properties": { @@ -4278,6 +4606,20 @@ } } }, + "handlers.CreateTransferResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.TransferData" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "handlers.CustomerWalletRes": { "type": "object", "properties": { @@ -4320,6 +4662,62 @@ } } }, + "handlers.InitPaymentData": { + "type": "object", + "properties": { + "checkout_url": { + "type": "string" + }, + "tx_ref": { + "type": "string" + } + } + }, + "handlers.InitPaymentRequest": { + "type": "object", + "properties": { + "amount": { + "type": "string" + }, + "callback_url": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "return_url": { + "type": "string" + }, + "tx_ref": { + "type": "string" + } + } + }, + "handlers.InitPaymentResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.InitPaymentData" + }, + "message": { + "description": "e.g., \"Payment initialized\"", + "type": "string" + }, + "status": { + "description": "\"success\"", + "type": "string" + } + } + }, "handlers.ManagersRes": { "type": "object", "properties": { @@ -4497,6 +4895,26 @@ } } }, + "handlers.TransactionData": { + "type": "object", + "properties": { + "amount": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "email": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tx_ref": { + "type": "string" + } + } + }, "handlers.TransactionRes": { "type": "object", "properties": { @@ -4595,6 +5013,66 @@ } } }, + "handlers.TransferData": { + "type": "object", + "properties": { + "amount": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "handlers.TransferRequest": { + "type": "object", + "properties": { + "account_number": { + "type": "string" + }, + "amount": { + "type": "string" + }, + "bank_code": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "recipient_name": { + "type": "string" + }, + "reference": { + "type": "string" + } + } + }, + "handlers.TransferVerificationData": { + "type": "object", + "properties": { + "account_name": { + "type": "string" + }, + "bank_code": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "handlers.TransferWalletRes": { "type": "object", "properties": { @@ -4726,6 +5204,34 @@ } } }, + "handlers.VerifyTransactionResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.TransactionData" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "handlers.VerifyTransferResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.TransferVerificationData" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "handlers.WalletRes": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e76a984..4219293 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -413,6 +413,50 @@ definitions: example: 1 type: integer type: object + handlers.ChapaSupportedBank: + properties: + acct_length: + type: integer + acct_number_regex: + type: string + active: + type: integer + country_id: + type: integer + created_at: + type: string + currency: + type: string + example_value: + type: string + id: + type: integer + is_24hrs: + type: integer + is_active: + type: integer + is_mobilemoney: + type: integer + is_rtgs: + type: integer + name: + type: string + slug: + type: string + swift: + type: string + updated_at: + type: string + type: object + handlers.ChapaSupportedBanksResponse: + properties: + data: + items: + $ref: '#/definitions/handlers.ChapaSupportedBank' + type: array + message: + type: string + type: object handlers.CheckPhoneEmailExistReq: properties: email: @@ -682,6 +726,15 @@ definitions: example: cash type: string type: object + handlers.CreateTransferResponse: + properties: + data: + $ref: '#/definitions/handlers.TransferData' + message: + type: string + status: + type: string + type: object handlers.CustomerWalletRes: properties: company_id: @@ -712,6 +765,43 @@ definitions: static_updated_at: type: string type: object + handlers.InitPaymentData: + properties: + checkout_url: + type: string + tx_ref: + type: string + type: object + handlers.InitPaymentRequest: + properties: + amount: + type: string + callback_url: + type: string + currency: + type: string + email: + type: string + first_name: + type: string + last_name: + type: string + return_url: + type: string + tx_ref: + type: string + type: object + handlers.InitPaymentResponse: + properties: + data: + $ref: '#/definitions/handlers.InitPaymentData' + message: + description: e.g., "Payment initialized" + type: string + status: + description: '"success"' + type: string + type: object handlers.ManagersRes: properties: created_at: @@ -835,6 +925,19 @@ definitions: example: 4.22 type: number type: object + handlers.TransactionData: + properties: + amount: + type: string + currency: + type: string + email: + type: string + status: + type: string + tx_ref: + type: string + type: object handlers.TransactionRes: properties: account_name: @@ -904,6 +1007,45 @@ definitions: example: true type: boolean type: object + handlers.TransferData: + properties: + amount: + type: string + currency: + type: string + reference: + type: string + status: + type: string + type: object + handlers.TransferRequest: + properties: + account_number: + type: string + amount: + type: string + bank_code: + type: string + currency: + type: string + reason: + type: string + recipient_name: + type: string + reference: + type: string + type: object + handlers.TransferVerificationData: + properties: + account_name: + type: string + bank_code: + type: string + reference: + type: string + status: + type: string + type: object handlers.TransferWalletRes: properties: amount: @@ -994,6 +1136,24 @@ definitions: updated_at: type: string type: object + handlers.VerifyTransactionResponse: + properties: + data: + $ref: '#/definitions/handlers.TransactionData' + message: + type: string + status: + type: string + type: object + handlers.VerifyTransferResponse: + properties: + data: + $ref: '#/definitions/handlers.TransferVerificationData' + message: + type: string + status: + type: string + type: object handlers.WalletRes: properties: amount: @@ -1206,6 +1366,177 @@ paths: summary: Create Admin tags: - admin + /api/v1/chapa/banks: + get: + consumes: + - application/json + description: Fetch all supported banks from Chapa + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.ChapaSupportedBanksResponse' + summary: Get list of banks + tags: + - Chapa + /api/v1/chapa/payments/callback: + post: + consumes: + - application/json + description: Endpoint to receive webhook payloads from Chapa + parameters: + - description: Webhook Payload (dynamic) + in: body + name: payload + required: true + schema: + type: object + produces: + - application/json + responses: + "200": + description: ok + schema: + type: string + summary: Receive Chapa webhook + tags: + - Chapa + /api/v1/chapa/payments/initialize: + post: + consumes: + - application/json + description: Initiate a payment through Chapa + parameters: + - description: Payment initialization request + in: body + name: payload + required: true + schema: + $ref: '#/definitions/handlers.InitPaymentRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.InitPaymentResponse' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Initialize a payment transaction + tags: + - Chapa + /api/v1/chapa/payments/verify/{tx_ref}: + get: + consumes: + - application/json + description: Verify the transaction status from Chapa using tx_ref + parameters: + - description: Transaction Reference + in: path + name: tx_ref + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.VerifyTransactionResponse' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Verify a payment transaction + tags: + - Chapa + /api/v1/chapa/transfers: + post: + consumes: + - application/json + description: Initiate a transfer request via Chapa + parameters: + - description: Transfer request body + in: body + name: payload + required: true + schema: + $ref: '#/definitions/handlers.TransferRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.CreateTransferResponse' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Create a money transfer + tags: + - Chapa + /api/v1/chapa/transfers/verify/{transfer_ref}: + get: + consumes: + - application/json + description: Check the status of a money transfer via reference + parameters: + - description: Transfer Reference + in: path + name: transfer_ref + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.VerifyTransferResponse' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Verify a transfer + tags: + - Chapa /auth/login: post: consumes: diff --git a/internal/models/models.chapa.go b/internal/models/models.chapa.go deleted file mode 100644 index d6a05a4..0000000 --- a/internal/models/models.chapa.go +++ /dev/null @@ -1,2 +0,0 @@ -package models - diff --git a/internal/utils/utils.chapa.go b/internal/utils/utils.chapa.go deleted file mode 100644 index 5005d3a..0000000 --- a/internal/utils/utils.chapa.go +++ /dev/null @@ -1,19 +0,0 @@ -package utils - -// import ( -// "log" -// "os" - -// "github.com/SamuelTariku/FortuneBet-Backend/internal/models" -// ) - -// func Init() { -// if err != nil { -// log.Println("No .env file found") -// } -// models.ChapaSecret = os.Getenv("CHAPA_SECRET_KEY") -// models.ChapaBaseURL = os.Getenv("CHAPA_BASE_URL") -// if models.ChapaBaseURL == "" { -// models.ChapaBaseURL = "https://api.chapa.co/v1" -// } -// } diff --git a/internal/web_server/handlers/chapa.go b/internal/web_server/handlers/chapa.go index 9165226..1f80787 100644 --- a/internal/web_server/handlers/chapa.go +++ b/internal/web_server/handlers/chapa.go @@ -7,36 +7,18 @@ import ( "io" "net/http" - // "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/gofiber/fiber/v2" + "github.com/google/uuid" ) -var ( - ChapaSecret string - ChapaBaseURL string -) - -type InitPaymentRequest struct { - Amount string `json:"amount"` - Currency string `json:"currency"` - Email string `json:"email"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - TxRef string `json:"tx_ref"` - CallbackURL string `json:"callback_url"` - ReturnURL string `json:"return_url"` -} - -type TransferRequest struct { - AccountNumber string `json:"account_number"` - BankCode string `json:"bank_code"` - Amount string `json:"amount"` - Currency string `json:"currency"` - Reference string `json:"reference"` - Reason string `json:"reason"` - RecipientName string `json:"recipient_name"` -} - +// GetBanks godoc +// @Summary Get list of banks +// @Description Fetch all supported banks from Chapa +// @Tags Chapa +// @Accept json +// @Produce json +// @Success 200 {object} ChapaSupportedBanksResponse +// @Router /api/v1/chapa/banks [get] func (h *Handler) GetBanks(c *fiber.Ctx) error { httpReq, err := http.NewRequest("GET", h.Cfg.CHAPA_BASE_URL+"/banks", nil) // log.Printf("\n\nbase url is: %v\n\n", h.Cfg.CHAPA_BASE_URL) @@ -56,9 +38,20 @@ func (h *Handler) GetBanks(c *fiber.Ctx) error { return c.Status(500).JSON(fiber.Map{"error": "Failed to read response", "details": err.Error()}) } - return c.Status(resp.StatusCode).Send(body) + return c.Status(resp.StatusCode).Type("json").Send(body) } +// InitializePayment godoc +// @Summary Initialize a payment transaction +// @Description Initiate a payment through Chapa +// @Tags Chapa +// @Accept json +// @Produce json +// @Param payload body InitPaymentRequest true "Payment initialization request" +// @Success 200 {object} InitPaymentResponse +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /api/v1/chapa/payments/initialize [post] func (h *Handler) InitializePayment(c *fiber.Ctx) error { var req InitPaymentRequest if err := c.BodyParser(&req); err != nil { @@ -68,6 +61,9 @@ func (h *Handler) InitializePayment(c *fiber.Ctx) error { }) } + // Generate and assign a unique transaction reference + req.TxRef = uuid.New().String() + payload, err := json.Marshal(req) if err != nil { return c.Status(500).JSON(fiber.Map{ @@ -103,9 +99,20 @@ func (h *Handler) InitializePayment(c *fiber.Ctx) error { }) } - return c.Status(resp.StatusCode).Send(body) + return c.Status(resp.StatusCode).Type("json").Send(body) } +// VerifyTransaction godoc +// @Summary Verify a payment transaction +// @Description Verify the transaction status from Chapa using tx_ref +// @Tags Chapa +// @Accept json +// @Produce json +// @Param tx_ref path string true "Transaction Reference" +// @Success 200 {object} VerifyTransactionResponse +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /api/v1/chapa/payments/verify/{tx_ref} [get] func (h *Handler) VerifyTransaction(c *fiber.Ctx) error { txRef := c.Params("tx_ref") if txRef == "" { @@ -142,9 +149,18 @@ func (h *Handler) VerifyTransaction(c *fiber.Ctx) error { }) } - return c.Status(resp.StatusCode).Send(body) + return c.Status(resp.StatusCode).Type("json").Send(body) } +// ReceiveWebhook godoc +// @Summary Receive Chapa webhook +// @Description Endpoint to receive webhook payloads from Chapa +// @Tags Chapa +// @Accept json +// @Produce json +// @Param payload body object true "Webhook Payload (dynamic)" +// @Success 200 {string} string "ok" +// @Router /api/v1/chapa/payments/callback [post] func (h *Handler) ReceiveWebhook(c *fiber.Ctx) error { var payload map[string]interface{} if err := c.BodyParser(&payload); err != nil { @@ -161,6 +177,17 @@ func (h *Handler) ReceiveWebhook(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) } +// CreateTransfer godoc +// @Summary Create a money transfer +// @Description Initiate a transfer request via Chapa +// @Tags Chapa +// @Accept json +// @Produce json +// @Param payload body TransferRequest true "Transfer request body" +// @Success 200 {object} CreateTransferResponse +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /api/v1/chapa/transfers [post] func (h *Handler) CreateTransfer(c *fiber.Ctx) error { var req TransferRequest if err := c.BodyParser(&req); err != nil { @@ -170,6 +197,9 @@ func (h *Handler) CreateTransfer(c *fiber.Ctx) error { }) } + // Inject unique transaction reference + req.Reference = uuid.New().String() + payload, err := json.Marshal(req) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ @@ -206,9 +236,20 @@ func (h *Handler) CreateTransfer(c *fiber.Ctx) error { }) } - return c.Status(resp.StatusCode).Send(body) + return c.Status(resp.StatusCode).Type("json").Send(body) } +// VerifyTransfer godoc +// @Summary Verify a transfer +// @Description Check the status of a money transfer via reference +// @Tags Chapa +// @Accept json +// @Produce json +// @Param transfer_ref path string true "Transfer Reference" +// @Success 200 {object} VerifyTransferResponse +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /api/v1/chapa/transfers/verify/{transfer_ref} [get] func (h *Handler) VerifyTransfer(c *fiber.Ctx) error { transferRef := c.Params("transfer_ref") if transferRef == "" { @@ -217,7 +258,7 @@ func (h *Handler) VerifyTransfer(c *fiber.Ctx) error { }) } - url := fmt.Sprintf("%s/transfers/%s", h.Cfg.CHAPA_BASE_URL, transferRef) + url := fmt.Sprintf("%s/transfers/verify/%s", h.Cfg.CHAPA_BASE_URL, transferRef) httpReq, err := http.NewRequest("GET", url, nil) if err != nil { @@ -246,5 +287,5 @@ func (h *Handler) VerifyTransfer(c *fiber.Ctx) error { }) } - return c.Status(resp.StatusCode).Send(body) + return c.Status(resp.StatusCode).Type("json").Send(body) } diff --git a/internal/web_server/handlers/models.chapa.go b/internal/web_server/handlers/models.chapa.go new file mode 100644 index 0000000..f829b53 --- /dev/null +++ b/internal/web_server/handlers/models.chapa.go @@ -0,0 +1,107 @@ +package handlers + +import "time" + +var ( + ChapaSecret string + ChapaBaseURL string +) + +type InitPaymentRequest struct { + Amount string `json:"amount"` + Currency string `json:"currency"` + Email string `json:"email"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + TxRef string `json:"tx_ref"` + CallbackURL string `json:"callback_url"` + ReturnURL string `json:"return_url"` +} + +type TransferRequest struct { + AccountNumber string `json:"account_number"` + BankCode string `json:"bank_code"` + Amount string `json:"amount"` + Currency string `json:"currency"` + Reference string `json:"reference"` + Reason string `json:"reason"` + RecipientName string `json:"recipient_name"` +} + +type ChapaSupportedBank struct { + Id int64 `json:"id"` + Slug string `json:"slug"` + Swift string `json:"swift"` + Name string `json:"name"` + AcctLength int `json:"acct_length"` + AcctNumberRegex string `json:"acct_number_regex"` + ExampleValue string `json:"example_value"` + CountryId int `json:"country_id"` + IsMobilemoney *int `json:"is_mobilemoney"` + + IsActive int `json:"is_active"` + IsRtgs *int `json:"is_rtgs"` + Active int `json:"active"` + Is24Hrs *int `json:"is_24hrs"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Currency string `json:"currency"` +} + +type ChapaSupportedBanksResponse struct { + Message string `json:"message"` + Data []ChapaSupportedBank `json:"data"` +} + +type InitPaymentData struct { + TxRef string `json:"tx_ref"` + CheckoutURL string `json:"checkout_url"` +} + +type InitPaymentResponse struct { + Status string `json:"status"` // "success" + Message string `json:"message"` // e.g., "Payment initialized" + Data InitPaymentData `json:"data"` +} + +type WebhookPayload map[string]interface{} + +type TransactionData struct { + TxRef string `json:"tx_ref"` + Status string `json:"status"` + Amount string `json:"amount"` + Currency string `json:"currency"` + CustomerEmail string `json:"email"` +} + +type VerifyTransactionResponse struct { + Status string `json:"status"` + Message string `json:"message"` + Data TransactionData `json:"data"` +} + +type TransferData struct { + Reference string `json:"reference"` + Status string `json:"status"` + Amount string `json:"amount"` + Currency string `json:"currency"` +} + +type CreateTransferResponse struct { + Status string `json:"status"` + Message string `json:"message"` + Data TransferData `json:"data"` +} + +type TransferVerificationData struct { + Reference string `json:"reference"` + Status string `json:"status"` + BankCode string `json:"bank_code"` + AccountName string `json:"account_name"` +} + +type VerifyTransferResponse struct { + Status string `json:"status"` + Message string `json:"message"` + Data TransferVerificationData `json:"data"` +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 6319bc6..20e84f5 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -167,12 +167,12 @@ func (a *App) initAppRoutes() { a.fiber.Post("/transfer/refill/:id", a.authMiddleware, h.RefillWallet) //Chapa Routes - a.fiber.Post("/api/v1/chapa/payments/initialize", a.authMiddleware, h.InitializePayment) - a.fiber.Get("/api/v1/chapa/payments/verify/:tx_ref", a.authMiddleware, h.VerifyTransaction) - a.fiber.Post("/api/v1/chapa/payments/callback", a.authMiddleware, h.ReceiveWebhook) - a.fiber.Get("/api/v1/chapa/banks", a.authMiddleware, h.GetBanks) - a.fiber.Post("/api/v1/chapa/transfers", a.authMiddleware, h.CreateTransfer) - a.fiber.Get("/api/v1/chapa/transfers/:transfer_ref", a.authMiddleware, h.VerifyTransfer) + a.fiber.Post("/api/v1/chapa/payments/initialize", h.InitializePayment) + a.fiber.Get("/api/v1/chapa/payments/verify/:tx_ref", h.VerifyTransaction) + a.fiber.Post("/api/v1/chapa/payments/callback", h.ReceiveWebhook) + a.fiber.Get("/api/v1/chapa/banks", h.GetBanks) + a.fiber.Post("/api/v1/chapa/transfers", h.CreateTransfer) + a.fiber.Get("/api/v1/chapa/transfers/verify/:transfer_ref", h.VerifyTransfer) // Transactions /transactions a.fiber.Post("/transaction", a.authMiddleware, h.CreateTransaction) From 9dd566417e7bc700eef1976d2e13ba99d47824b9 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Fri, 23 May 2025 20:11:26 +0300 Subject: [PATCH 3/7] Alea Play Service --- cmd/main.go | 9 +- docs/docs.go | 779 +++++++++++------- docs/swagger.json | 779 +++++++++++------- docs/swagger.yaml | 517 +++++++----- internal/config/config.go | 13 + internal/domain/chapa.go | 108 ++- internal/domain/virtual_game.go | 63 +- internal/middleware/alea.go | 34 + internal/repository/virtual_game.go | 1 + internal/router/router.chapa.go | 37 - internal/services/virtualGame/Alea/port.go | 12 + internal/services/virtualGame/Alea/service.go | 159 ++++ internal/services/virtualGame/port.go | 1 + internal/services/virtualGame/service.go | 3 +- internal/web_server/app.go | 88 +- internal/web_server/handlers/alea_games.go | 76 ++ internal/web_server/handlers/chapa.go | 22 +- internal/web_server/handlers/handlers.go | 72 +- internal/web_server/routes.go | 10 +- 19 files changed, 1823 insertions(+), 960 deletions(-) create mode 100644 internal/middleware/alea.go delete mode 100644 internal/router/router.chapa.go create mode 100644 internal/services/virtualGame/Alea/port.go create mode 100644 internal/services/virtualGame/Alea/service.go create mode 100644 internal/web_server/handlers/alea_games.go diff --git a/cmd/main.go b/cmd/main.go index 0045fca..c5eebd7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -29,6 +29,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" + alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" // "github.com/SamuelTariku/FortuneBet-Backend/internal/utils" @@ -91,6 +92,12 @@ func main() { notificationSvc := notificationservice.New(notificationRepo, logger, cfg) referalSvc := referralservice.New(referalRepo, *walletSvc, store, cfg, logger) virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger) + aleaService := alea.NewAleaPlayService( + vitualGameRepo, + *walletSvc, + cfg, + logger, + ) httpserver.StartDataFetchingCrons(eventSvc, oddsSvc, resultSvc) @@ -98,7 +105,7 @@ func main() { JwtAccessKey: cfg.JwtKey, JwtAccessExpiry: cfg.AccessExpiry, }, userSvc, - ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc, referalSvc, virtualGameSvc, resultSvc, cfg) + ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc, referalSvc, virtualGameSvc, aleaService, resultSvc, cfg) logger.Info("Starting server", "port", cfg.Port) if err := app.Run(); err != nil { diff --git a/docs/docs.go b/docs/docs.go index f9b5d61..235d7ff 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -129,6 +129,108 @@ const docTemplate = `{ } } }, + "/api/v1/alea-games/launch": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Generates an authenticated launch URL for Alea Play virtual games", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Alea Virtual Games" + ], + "summary": "Launch an Alea Play virtual game", + "parameters": [ + { + "type": "string", + "description": "Game identifier (e.g., 'aviator', 'plinko')", + "name": "game_id", + "in": "query", + "required": true + }, + { + "enum": [ + "USD", + "EUR", + "GBP" + ], + "type": "string", + "default": "USD", + "description": "Currency code (ISO 4217)", + "name": "currency", + "in": "query" + }, + { + "enum": [ + "real", + "demo" + ], + "type": "string", + "default": "real", + "description": "Game mode", + "name": "mode", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Returns authenticated game launch URL", + "schema": { + "type": "object", + "additionalProperties": { + "allOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "launch_url": { + "type": "string" + } + } + } + ] + } + } + }, + "400": { + "description": "Invalid request parameters", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/api/v1/chapa/banks": { "get": { "description": "Fetch all supported banks from Chapa", @@ -146,7 +248,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.ChapaSupportedBanksResponse" + "$ref": "#/definitions/domain.ChapaSupportedBanksResponse" } } } @@ -206,7 +308,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handlers.InitPaymentRequest" + "$ref": "#/definitions/domain.InitPaymentRequest" } } ], @@ -214,25 +316,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.InitPaymentResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } + "$ref": "#/definitions/domain.InitPaymentResponse" } } } @@ -264,25 +348,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.VerifyTransactionResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } + "$ref": "#/definitions/domain.VerifyTransactionResponse" } } } @@ -308,7 +374,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handlers.TransferRequest" + "$ref": "#/definitions/domain.TransferRequest" } } ], @@ -316,25 +382,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.CreateTransferResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } + "$ref": "#/definitions/domain.CreateTransferResponse" } } } @@ -366,11 +414,78 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.VerifyTransferResponse" + "$ref": "#/definitions/domain.VerifyTransferResponse" + } + } + } + } + }, + "/api/v1/webhooks/alea": { + "post": { + "description": "Handles webhook callbacks from Alea Play virtual games for bet settlement", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Alea Virtual Games" + ], + "summary": "Process Alea Play game callback", + "parameters": [ + { + "description": "Callback payload", + "name": "callback", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.AleaPlayCallback" + } + } + ], + "responses": { + "200": { + "description": "Callback processed successfully", + "schema": { + "type": "object", + "additionalProperties": { + "allOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + } + ] + } } }, "400": { - "description": "Bad Request", + "description": "Invalid callback format", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Invalid signature", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Duplicate transaction", "schema": { "type": "object", "additionalProperties": { @@ -379,7 +494,7 @@ const docTemplate = `{ } }, "500": { - "description": "Internal Server Error", + "description": "Internal processing error", "schema": { "type": "object", "additionalProperties": { @@ -3578,6 +3693,54 @@ const docTemplate = `{ } }, "definitions": { + "domain.AleaPlayCallback": { + "type": "object", + "properties": { + "amount": { + "type": "number" + }, + "currency": { + "type": "string" + }, + "event_id": { + "type": "string" + }, + "game_id": { + "type": "string" + }, + "is_free_round": { + "type": "boolean" + }, + "multiplier": { + "type": "number" + }, + "operator_id": { + "type": "string" + }, + "round_id": { + "type": "string" + }, + "session_id": { + "type": "string" + }, + "signature": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "transaction_id": { + "type": "string" + }, + "type": { + "description": "BET, WIN, CASHOUT, etc.", + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, "domain.BetOutcome": { "type": "object", "properties": { @@ -3647,6 +3810,143 @@ const docTemplate = `{ } } }, + "domain.ChapaSupportedBank": { + "type": "object", + "properties": { + "acct_length": { + "type": "integer" + }, + "acct_number_regex": { + "type": "string" + }, + "active": { + "type": "integer" + }, + "country_id": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "example_value": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_24hrs": { + "type": "integer" + }, + "is_active": { + "type": "integer" + }, + "is_mobilemoney": { + "type": "integer" + }, + "is_rtgs": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "swift": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "domain.ChapaSupportedBanksResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ChapaSupportedBank" + } + }, + "message": { + "type": "string" + } + } + }, + "domain.CreateTransferResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.TransferData" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "domain.InitPaymentData": { + "type": "object", + "properties": { + "checkout_url": { + "type": "string" + }, + "tx_ref": { + "type": "string" + } + } + }, + "domain.InitPaymentRequest": { + "type": "object", + "properties": { + "amount": { + "type": "string" + }, + "callback_url": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "return_url": { + "type": "string" + }, + "tx_ref": { + "type": "string" + } + } + }, + "domain.InitPaymentResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.InitPaymentData" + }, + "message": { + "description": "e.g., \"Payment initialized\"", + "type": "string" + }, + "status": { + "description": "\"success\"", + "type": "string" + } + } + }, "domain.Odd": { "type": "object", "properties": { @@ -3917,6 +4217,86 @@ const docTemplate = `{ } } }, + "domain.TransactionData": { + "type": "object", + "properties": { + "amount": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "email": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tx_ref": { + "type": "string" + } + } + }, + "domain.TransferData": { + "type": "object", + "properties": { + "amount": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "domain.TransferRequest": { + "type": "object", + "properties": { + "account_number": { + "type": "string" + }, + "amount": { + "type": "string" + }, + "bank_code": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "recipient_name": { + "type": "string" + }, + "reference": { + "type": "string" + } + } + }, + "domain.TransferVerificationData": { + "type": "object", + "properties": { + "account_name": { + "type": "string" + }, + "bank_code": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "domain.UpcomingEvent": { "type": "object", "properties": { @@ -3974,6 +4354,34 @@ const docTemplate = `{ } } }, + "domain.VerifyTransactionResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.TransactionData" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "domain.VerifyTransferResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.TransferVerificationData" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "handlers.AdminRes": { "type": "object", "properties": { @@ -4164,73 +4572,6 @@ const docTemplate = `{ } } }, - "handlers.ChapaSupportedBank": { - "type": "object", - "properties": { - "acct_length": { - "type": "integer" - }, - "acct_number_regex": { - "type": "string" - }, - "active": { - "type": "integer" - }, - "country_id": { - "type": "integer" - }, - "created_at": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "example_value": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "is_24hrs": { - "type": "integer" - }, - "is_active": { - "type": "integer" - }, - "is_mobilemoney": { - "type": "integer" - }, - "is_rtgs": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "slug": { - "type": "string" - }, - "swift": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, - "handlers.ChapaSupportedBanksResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/handlers.ChapaSupportedBank" - } - }, - "message": { - "type": "string" - } - } - }, "handlers.CheckPhoneEmailExistReq": { "type": "object", "properties": { @@ -4614,20 +4955,6 @@ const docTemplate = `{ } } }, - "handlers.CreateTransferResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/handlers.TransferData" - }, - "message": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, "handlers.CustomerWalletRes": { "type": "object", "properties": { @@ -4670,62 +4997,6 @@ const docTemplate = `{ } } }, - "handlers.InitPaymentData": { - "type": "object", - "properties": { - "checkout_url": { - "type": "string" - }, - "tx_ref": { - "type": "string" - } - } - }, - "handlers.InitPaymentRequest": { - "type": "object", - "properties": { - "amount": { - "type": "string" - }, - "callback_url": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "email": { - "type": "string" - }, - "first_name": { - "type": "string" - }, - "last_name": { - "type": "string" - }, - "return_url": { - "type": "string" - }, - "tx_ref": { - "type": "string" - } - } - }, - "handlers.InitPaymentResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/handlers.InitPaymentData" - }, - "message": { - "description": "e.g., \"Payment initialized\"", - "type": "string" - }, - "status": { - "description": "\"success\"", - "type": "string" - } - } - }, "handlers.ManagersRes": { "type": "object", "properties": { @@ -4903,26 +5174,6 @@ const docTemplate = `{ } } }, - "handlers.TransactionData": { - "type": "object", - "properties": { - "amount": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "email": { - "type": "string" - }, - "status": { - "type": "string" - }, - "tx_ref": { - "type": "string" - } - } - }, "handlers.TransactionRes": { "type": "object", "properties": { @@ -5021,66 +5272,6 @@ const docTemplate = `{ } } }, - "handlers.TransferData": { - "type": "object", - "properties": { - "amount": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "reference": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, - "handlers.TransferRequest": { - "type": "object", - "properties": { - "account_number": { - "type": "string" - }, - "amount": { - "type": "string" - }, - "bank_code": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "recipient_name": { - "type": "string" - }, - "reference": { - "type": "string" - } - } - }, - "handlers.TransferVerificationData": { - "type": "object", - "properties": { - "account_name": { - "type": "string" - }, - "bank_code": { - "type": "string" - }, - "reference": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, "handlers.TransferWalletRes": { "type": "object", "properties": { @@ -5212,34 +5403,6 @@ const docTemplate = `{ } } }, - "handlers.VerifyTransactionResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/handlers.TransactionData" - }, - "message": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, - "handlers.VerifyTransferResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/handlers.TransferVerificationData" - }, - "message": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, "handlers.WalletRes": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index b01b99e..1b046b1 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -121,6 +121,108 @@ } } }, + "/api/v1/alea-games/launch": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Generates an authenticated launch URL for Alea Play virtual games", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Alea Virtual Games" + ], + "summary": "Launch an Alea Play virtual game", + "parameters": [ + { + "type": "string", + "description": "Game identifier (e.g., 'aviator', 'plinko')", + "name": "game_id", + "in": "query", + "required": true + }, + { + "enum": [ + "USD", + "EUR", + "GBP" + ], + "type": "string", + "default": "USD", + "description": "Currency code (ISO 4217)", + "name": "currency", + "in": "query" + }, + { + "enum": [ + "real", + "demo" + ], + "type": "string", + "default": "real", + "description": "Game mode", + "name": "mode", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Returns authenticated game launch URL", + "schema": { + "type": "object", + "additionalProperties": { + "allOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "launch_url": { + "type": "string" + } + } + } + ] + } + } + }, + "400": { + "description": "Invalid request parameters", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/api/v1/chapa/banks": { "get": { "description": "Fetch all supported banks from Chapa", @@ -138,7 +240,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.ChapaSupportedBanksResponse" + "$ref": "#/definitions/domain.ChapaSupportedBanksResponse" } } } @@ -198,7 +300,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handlers.InitPaymentRequest" + "$ref": "#/definitions/domain.InitPaymentRequest" } } ], @@ -206,25 +308,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.InitPaymentResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } + "$ref": "#/definitions/domain.InitPaymentResponse" } } } @@ -256,25 +340,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.VerifyTransactionResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } + "$ref": "#/definitions/domain.VerifyTransactionResponse" } } } @@ -300,7 +366,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handlers.TransferRequest" + "$ref": "#/definitions/domain.TransferRequest" } } ], @@ -308,25 +374,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.CreateTransferResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } + "$ref": "#/definitions/domain.CreateTransferResponse" } } } @@ -358,11 +406,78 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.VerifyTransferResponse" + "$ref": "#/definitions/domain.VerifyTransferResponse" + } + } + } + } + }, + "/api/v1/webhooks/alea": { + "post": { + "description": "Handles webhook callbacks from Alea Play virtual games for bet settlement", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Alea Virtual Games" + ], + "summary": "Process Alea Play game callback", + "parameters": [ + { + "description": "Callback payload", + "name": "callback", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.AleaPlayCallback" + } + } + ], + "responses": { + "200": { + "description": "Callback processed successfully", + "schema": { + "type": "object", + "additionalProperties": { + "allOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + } + ] + } } }, "400": { - "description": "Bad Request", + "description": "Invalid callback format", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Invalid signature", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Duplicate transaction", "schema": { "type": "object", "additionalProperties": { @@ -371,7 +486,7 @@ } }, "500": { - "description": "Internal Server Error", + "description": "Internal processing error", "schema": { "type": "object", "additionalProperties": { @@ -3570,6 +3685,54 @@ } }, "definitions": { + "domain.AleaPlayCallback": { + "type": "object", + "properties": { + "amount": { + "type": "number" + }, + "currency": { + "type": "string" + }, + "event_id": { + "type": "string" + }, + "game_id": { + "type": "string" + }, + "is_free_round": { + "type": "boolean" + }, + "multiplier": { + "type": "number" + }, + "operator_id": { + "type": "string" + }, + "round_id": { + "type": "string" + }, + "session_id": { + "type": "string" + }, + "signature": { + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "transaction_id": { + "type": "string" + }, + "type": { + "description": "BET, WIN, CASHOUT, etc.", + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, "domain.BetOutcome": { "type": "object", "properties": { @@ -3639,6 +3802,143 @@ } } }, + "domain.ChapaSupportedBank": { + "type": "object", + "properties": { + "acct_length": { + "type": "integer" + }, + "acct_number_regex": { + "type": "string" + }, + "active": { + "type": "integer" + }, + "country_id": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "example_value": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_24hrs": { + "type": "integer" + }, + "is_active": { + "type": "integer" + }, + "is_mobilemoney": { + "type": "integer" + }, + "is_rtgs": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "swift": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "domain.ChapaSupportedBanksResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ChapaSupportedBank" + } + }, + "message": { + "type": "string" + } + } + }, + "domain.CreateTransferResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.TransferData" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "domain.InitPaymentData": { + "type": "object", + "properties": { + "checkout_url": { + "type": "string" + }, + "tx_ref": { + "type": "string" + } + } + }, + "domain.InitPaymentRequest": { + "type": "object", + "properties": { + "amount": { + "type": "string" + }, + "callback_url": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "return_url": { + "type": "string" + }, + "tx_ref": { + "type": "string" + } + } + }, + "domain.InitPaymentResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.InitPaymentData" + }, + "message": { + "description": "e.g., \"Payment initialized\"", + "type": "string" + }, + "status": { + "description": "\"success\"", + "type": "string" + } + } + }, "domain.Odd": { "type": "object", "properties": { @@ -3909,6 +4209,86 @@ } } }, + "domain.TransactionData": { + "type": "object", + "properties": { + "amount": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "email": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tx_ref": { + "type": "string" + } + } + }, + "domain.TransferData": { + "type": "object", + "properties": { + "amount": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "domain.TransferRequest": { + "type": "object", + "properties": { + "account_number": { + "type": "string" + }, + "amount": { + "type": "string" + }, + "bank_code": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "recipient_name": { + "type": "string" + }, + "reference": { + "type": "string" + } + } + }, + "domain.TransferVerificationData": { + "type": "object", + "properties": { + "account_name": { + "type": "string" + }, + "bank_code": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "domain.UpcomingEvent": { "type": "object", "properties": { @@ -3966,6 +4346,34 @@ } } }, + "domain.VerifyTransactionResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.TransactionData" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "domain.VerifyTransferResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.TransferVerificationData" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "handlers.AdminRes": { "type": "object", "properties": { @@ -4156,73 +4564,6 @@ } } }, - "handlers.ChapaSupportedBank": { - "type": "object", - "properties": { - "acct_length": { - "type": "integer" - }, - "acct_number_regex": { - "type": "string" - }, - "active": { - "type": "integer" - }, - "country_id": { - "type": "integer" - }, - "created_at": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "example_value": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "is_24hrs": { - "type": "integer" - }, - "is_active": { - "type": "integer" - }, - "is_mobilemoney": { - "type": "integer" - }, - "is_rtgs": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "slug": { - "type": "string" - }, - "swift": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, - "handlers.ChapaSupportedBanksResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/handlers.ChapaSupportedBank" - } - }, - "message": { - "type": "string" - } - } - }, "handlers.CheckPhoneEmailExistReq": { "type": "object", "properties": { @@ -4606,20 +4947,6 @@ } } }, - "handlers.CreateTransferResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/handlers.TransferData" - }, - "message": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, "handlers.CustomerWalletRes": { "type": "object", "properties": { @@ -4662,62 +4989,6 @@ } } }, - "handlers.InitPaymentData": { - "type": "object", - "properties": { - "checkout_url": { - "type": "string" - }, - "tx_ref": { - "type": "string" - } - } - }, - "handlers.InitPaymentRequest": { - "type": "object", - "properties": { - "amount": { - "type": "string" - }, - "callback_url": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "email": { - "type": "string" - }, - "first_name": { - "type": "string" - }, - "last_name": { - "type": "string" - }, - "return_url": { - "type": "string" - }, - "tx_ref": { - "type": "string" - } - } - }, - "handlers.InitPaymentResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/handlers.InitPaymentData" - }, - "message": { - "description": "e.g., \"Payment initialized\"", - "type": "string" - }, - "status": { - "description": "\"success\"", - "type": "string" - } - } - }, "handlers.ManagersRes": { "type": "object", "properties": { @@ -4895,26 +5166,6 @@ } } }, - "handlers.TransactionData": { - "type": "object", - "properties": { - "amount": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "email": { - "type": "string" - }, - "status": { - "type": "string" - }, - "tx_ref": { - "type": "string" - } - } - }, "handlers.TransactionRes": { "type": "object", "properties": { @@ -5013,66 +5264,6 @@ } } }, - "handlers.TransferData": { - "type": "object", - "properties": { - "amount": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "reference": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, - "handlers.TransferRequest": { - "type": "object", - "properties": { - "account_number": { - "type": "string" - }, - "amount": { - "type": "string" - }, - "bank_code": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "recipient_name": { - "type": "string" - }, - "reference": { - "type": "string" - } - } - }, - "handlers.TransferVerificationData": { - "type": "object", - "properties": { - "account_name": { - "type": "string" - }, - "bank_code": { - "type": "string" - }, - "reference": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, "handlers.TransferWalletRes": { "type": "object", "properties": { @@ -5204,34 +5395,6 @@ } } }, - "handlers.VerifyTransactionResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/handlers.TransactionData" - }, - "message": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, - "handlers.VerifyTransferResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/handlers.TransferVerificationData" - }, - "message": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, "handlers.WalletRes": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 4219293..cc038bf 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,4 +1,36 @@ definitions: + domain.AleaPlayCallback: + properties: + amount: + type: number + currency: + type: string + event_id: + type: string + game_id: + type: string + is_free_round: + type: boolean + multiplier: + type: number + operator_id: + type: string + round_id: + type: string + session_id: + type: string + signature: + type: string + timestamp: + type: integer + transaction_id: + type: string + type: + description: BET, WIN, CASHOUT, etc. + type: string + user_id: + type: string + type: object domain.BetOutcome: properties: away_team_name: @@ -48,6 +80,96 @@ definitions: - $ref: '#/definitions/domain.OutcomeStatus' example: 1 type: object + domain.ChapaSupportedBank: + properties: + acct_length: + type: integer + acct_number_regex: + type: string + active: + type: integer + country_id: + type: integer + created_at: + type: string + currency: + type: string + example_value: + type: string + id: + type: integer + is_24hrs: + type: integer + is_active: + type: integer + is_mobilemoney: + type: integer + is_rtgs: + type: integer + name: + type: string + slug: + type: string + swift: + type: string + updated_at: + type: string + type: object + domain.ChapaSupportedBanksResponse: + properties: + data: + items: + $ref: '#/definitions/domain.ChapaSupportedBank' + type: array + message: + type: string + type: object + domain.CreateTransferResponse: + properties: + data: + $ref: '#/definitions/domain.TransferData' + message: + type: string + status: + type: string + type: object + domain.InitPaymentData: + properties: + checkout_url: + type: string + tx_ref: + type: string + type: object + domain.InitPaymentRequest: + properties: + amount: + type: string + callback_url: + type: string + currency: + type: string + email: + type: string + first_name: + type: string + last_name: + type: string + return_url: + type: string + tx_ref: + type: string + type: object + domain.InitPaymentResponse: + properties: + data: + $ref: '#/definitions/domain.InitPaymentData' + message: + description: e.g., "Payment initialized" + type: string + status: + description: '"success"' + type: string + type: object domain.Odd: properties: category: @@ -238,6 +360,58 @@ definitions: example: 1 type: integer type: object + domain.TransactionData: + properties: + amount: + type: string + currency: + type: string + email: + type: string + status: + type: string + tx_ref: + type: string + type: object + domain.TransferData: + properties: + amount: + type: string + currency: + type: string + reference: + type: string + status: + type: string + type: object + domain.TransferRequest: + properties: + account_number: + type: string + amount: + type: string + bank_code: + type: string + currency: + type: string + reason: + type: string + recipient_name: + type: string + reference: + type: string + type: object + domain.TransferVerificationData: + properties: + account_name: + type: string + bank_code: + type: string + reference: + type: string + status: + type: string + type: object domain.UpcomingEvent: properties: awayKitImage: @@ -280,6 +454,24 @@ definitions: description: Converted from "time" field in UNIX format type: string type: object + domain.VerifyTransactionResponse: + properties: + data: + $ref: '#/definitions/domain.TransactionData' + message: + type: string + status: + type: string + type: object + domain.VerifyTransferResponse: + properties: + data: + $ref: '#/definitions/domain.TransferVerificationData' + message: + type: string + status: + type: string + type: object handlers.AdminRes: properties: created_at: @@ -413,50 +605,6 @@ definitions: example: 1 type: integer type: object - handlers.ChapaSupportedBank: - properties: - acct_length: - type: integer - acct_number_regex: - type: string - active: - type: integer - country_id: - type: integer - created_at: - type: string - currency: - type: string - example_value: - type: string - id: - type: integer - is_24hrs: - type: integer - is_active: - type: integer - is_mobilemoney: - type: integer - is_rtgs: - type: integer - name: - type: string - slug: - type: string - swift: - type: string - updated_at: - type: string - type: object - handlers.ChapaSupportedBanksResponse: - properties: - data: - items: - $ref: '#/definitions/handlers.ChapaSupportedBank' - type: array - message: - type: string - type: object handlers.CheckPhoneEmailExistReq: properties: email: @@ -726,15 +874,6 @@ definitions: example: cash type: string type: object - handlers.CreateTransferResponse: - properties: - data: - $ref: '#/definitions/handlers.TransferData' - message: - type: string - status: - type: string - type: object handlers.CustomerWalletRes: properties: company_id: @@ -765,43 +904,6 @@ definitions: static_updated_at: type: string type: object - handlers.InitPaymentData: - properties: - checkout_url: - type: string - tx_ref: - type: string - type: object - handlers.InitPaymentRequest: - properties: - amount: - type: string - callback_url: - type: string - currency: - type: string - email: - type: string - first_name: - type: string - last_name: - type: string - return_url: - type: string - tx_ref: - type: string - type: object - handlers.InitPaymentResponse: - properties: - data: - $ref: '#/definitions/handlers.InitPaymentData' - message: - description: e.g., "Payment initialized" - type: string - status: - description: '"success"' - type: string - type: object handlers.ManagersRes: properties: created_at: @@ -925,19 +1027,6 @@ definitions: example: 4.22 type: number type: object - handlers.TransactionData: - properties: - amount: - type: string - currency: - type: string - email: - type: string - status: - type: string - tx_ref: - type: string - type: object handlers.TransactionRes: properties: account_name: @@ -1007,45 +1096,6 @@ definitions: example: true type: boolean type: object - handlers.TransferData: - properties: - amount: - type: string - currency: - type: string - reference: - type: string - status: - type: string - type: object - handlers.TransferRequest: - properties: - account_number: - type: string - amount: - type: string - bank_code: - type: string - currency: - type: string - reason: - type: string - recipient_name: - type: string - reference: - type: string - type: object - handlers.TransferVerificationData: - properties: - account_name: - type: string - bank_code: - type: string - reference: - type: string - status: - type: string - type: object handlers.TransferWalletRes: properties: amount: @@ -1136,24 +1186,6 @@ definitions: updated_at: type: string type: object - handlers.VerifyTransactionResponse: - properties: - data: - $ref: '#/definitions/handlers.TransactionData' - message: - type: string - status: - type: string - type: object - handlers.VerifyTransferResponse: - properties: - data: - $ref: '#/definitions/handlers.TransferVerificationData' - message: - type: string - status: - type: string - type: object handlers.WalletRes: properties: amount: @@ -1366,6 +1398,71 @@ paths: summary: Create Admin tags: - admin + /api/v1/alea-games/launch: + get: + consumes: + - application/json + description: Generates an authenticated launch URL for Alea Play virtual games + parameters: + - description: Game identifier (e.g., 'aviator', 'plinko') + in: query + name: game_id + required: true + type: string + - default: USD + description: Currency code (ISO 4217) + enum: + - USD + - EUR + - GBP + in: query + name: currency + type: string + - default: real + description: Game mode + enum: + - real + - demo + in: query + name: mode + type: string + produces: + - application/json + responses: + "200": + description: Returns authenticated game launch URL + schema: + additionalProperties: + allOf: + - type: string + - properties: + launch_url: + type: string + type: object + type: object + "400": + description: Invalid request parameters + schema: + additionalProperties: + type: string + type: object + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal server error + schema: + additionalProperties: + type: string + type: object + security: + - BearerAuth: [] + summary: Launch an Alea Play virtual game + tags: + - Alea Virtual Games /api/v1/chapa/banks: get: consumes: @@ -1377,7 +1474,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/handlers.ChapaSupportedBanksResponse' + $ref: '#/definitions/domain.ChapaSupportedBanksResponse' summary: Get list of banks tags: - Chapa @@ -1414,26 +1511,14 @@ paths: name: payload required: true schema: - $ref: '#/definitions/handlers.InitPaymentRequest' + $ref: '#/definitions/domain.InitPaymentRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/handlers.InitPaymentResponse' - "400": - description: Bad Request - schema: - additionalProperties: - type: string - type: object - "500": - description: Internal Server Error - schema: - additionalProperties: - type: string - type: object + $ref: '#/definitions/domain.InitPaymentResponse' summary: Initialize a payment transaction tags: - Chapa @@ -1454,19 +1539,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/handlers.VerifyTransactionResponse' - "400": - description: Bad Request - schema: - additionalProperties: - type: string - type: object - "500": - description: Internal Server Error - schema: - additionalProperties: - type: string - type: object + $ref: '#/definitions/domain.VerifyTransactionResponse' summary: Verify a payment transaction tags: - Chapa @@ -1481,26 +1554,14 @@ paths: name: payload required: true schema: - $ref: '#/definitions/handlers.TransferRequest' + $ref: '#/definitions/domain.TransferRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/handlers.CreateTransferResponse' - "400": - description: Bad Request - schema: - additionalProperties: - type: string - type: object - "500": - description: Internal Server Error - schema: - additionalProperties: - type: string - type: object + $ref: '#/definitions/domain.CreateTransferResponse' summary: Create a money transfer tags: - Chapa @@ -1521,22 +1582,64 @@ paths: "200": description: OK schema: - $ref: '#/definitions/handlers.VerifyTransferResponse' + $ref: '#/definitions/domain.VerifyTransferResponse' + summary: Verify a transfer + tags: + - Chapa + /api/v1/webhooks/alea: + post: + consumes: + - application/json + description: Handles webhook callbacks from Alea Play virtual games for bet + settlement + parameters: + - description: Callback payload + in: body + name: callback + required: true + schema: + $ref: '#/definitions/domain.AleaPlayCallback' + produces: + - application/json + responses: + "200": + description: Callback processed successfully + schema: + additionalProperties: + allOf: + - type: string + - properties: + status: + type: string + type: object + type: object "400": - description: Bad Request + description: Invalid callback format + schema: + additionalProperties: + type: string + type: object + "401": + description: Invalid signature + schema: + additionalProperties: + type: string + type: object + "409": + description: Duplicate transaction schema: additionalProperties: type: string type: object "500": - description: Internal Server Error + description: Internal processing error schema: additionalProperties: type: string type: object - summary: Verify a transfer + summary: Process Alea Play game callback tags: - - Chapa + - Alea Virtual Games /auth/login: post: consumes: diff --git a/internal/config/config.go b/internal/config/config.go index 9418707..5187325 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -28,6 +28,18 @@ var ( ErrInvalidPopOKCallbackURL = errors.New("PopOK callback URL is invalid") ) +type AleaPlayConfig struct { + Enabled bool `mapstructure:"enabled"` + BaseURL string `mapstructure:"base_url"` // "https://api.aleaplay.com" + OperatorID string `mapstructure:"operator_id"` // Your operator ID with Alea + SecretKey string `mapstructure:"secret_key"` // API secret for signatures + GameListURL string `mapstructure:"game_list_url"` // Endpoint to fetch available games + + // Optional settings + DefaultCurrency string `mapstructure:"default_currency"` // "USD", "EUR", etc. + SessionTimeout int `mapstructure:"session_timeout"` // In hours +} + type Config struct { Port int DbUrl string @@ -48,6 +60,7 @@ type Config struct { CHAPA_RETURN_URL string Bet365Token string PopOK domain.PopOKConfig + AleaPlay AleaPlayConfig `mapstructure:"alea_play"` } func NewConfig() (*Config, error) { diff --git a/internal/domain/chapa.go b/internal/domain/chapa.go index 7d0427a..f630a6d 100644 --- a/internal/domain/chapa.go +++ b/internal/domain/chapa.go @@ -1 +1,107 @@ -package domain \ No newline at end of file +package domain + +import "time" + +var ( + ChapaSecret string + ChapaBaseURL string +) + +type InitPaymentRequest struct { + Amount string `json:"amount"` + Currency string `json:"currency"` + Email string `json:"email"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + TxRef string `json:"tx_ref"` + CallbackURL string `json:"callback_url"` + ReturnURL string `json:"return_url"` +} + +type TransferRequest struct { + AccountNumber string `json:"account_number"` + BankCode string `json:"bank_code"` + Amount string `json:"amount"` + Currency string `json:"currency"` + Reference string `json:"reference"` + Reason string `json:"reason"` + RecipientName string `json:"recipient_name"` +} + +type ChapaSupportedBank struct { + Id int64 `json:"id"` + Slug string `json:"slug"` + Swift string `json:"swift"` + Name string `json:"name"` + AcctLength int `json:"acct_length"` + AcctNumberRegex string `json:"acct_number_regex"` + ExampleValue string `json:"example_value"` + CountryId int `json:"country_id"` + IsMobilemoney *int `json:"is_mobilemoney"` + + IsActive int `json:"is_active"` + IsRtgs *int `json:"is_rtgs"` + Active int `json:"active"` + Is24Hrs *int `json:"is_24hrs"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Currency string `json:"currency"` +} + +type ChapaSupportedBanksResponse struct { + Message string `json:"message"` + Data []ChapaSupportedBank `json:"data"` +} + +type InitPaymentData struct { + TxRef string `json:"tx_ref"` + CheckoutURL string `json:"checkout_url"` +} + +type InitPaymentResponse struct { + Status string `json:"status"` // "success" + Message string `json:"message"` // e.g., "Payment initialized" + Data InitPaymentData `json:"data"` +} + +type WebhookPayload map[string]interface{} + +type TransactionData struct { + TxRef string `json:"tx_ref"` + Status string `json:"status"` + Amount string `json:"amount"` + Currency string `json:"currency"` + CustomerEmail string `json:"email"` +} + +type VerifyTransactionResponse struct { + Status string `json:"status"` + Message string `json:"message"` + Data TransactionData `json:"data"` +} + +type TransferData struct { + Reference string `json:"reference"` + Status string `json:"status"` + Amount string `json:"amount"` + Currency string `json:"currency"` +} + +type CreateTransferResponse struct { + Status string `json:"status"` + Message string `json:"message"` + Data TransferData `json:"data"` +} + +type TransferVerificationData struct { + Reference string `json:"reference"` + Status string `json:"status"` + BankCode string `json:"bank_code"` + AccountName string `json:"account_name"` +} + +type VerifyTransferResponse struct { + Status string `json:"status"` + Message string `json:"message"` + Data TransferVerificationData `json:"data"` +} diff --git a/internal/domain/virtual_game.go b/internal/domain/virtual_game.go index 8b981af..1866519 100644 --- a/internal/domain/virtual_game.go +++ b/internal/domain/virtual_game.go @@ -14,6 +14,11 @@ type VirtualGameSession struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` ExpiresAt time.Time `json:"expires_at"` + + // Alea Play specific fields + ExternalSessionID string `json:"external_session_id"` // Alea's session reference + OperatorID string `json:"operator_id"` // Your operator ID with Alea + GameMode string `json:"game_mode"` // real, demo, tournament } type VirtualGameTransaction struct { @@ -21,15 +26,35 @@ type VirtualGameTransaction struct { SessionID int64 `json:"session_id"` UserID int64 `json:"user_id"` WalletID int64 `json:"wallet_id"` - TransactionType string `json:"transaction_type"` // BET, WIN, REFUND, JACKPOT_WIN - Amount int64 `json:"amount"` + TransactionType string `json:"transaction_type"` // BET, WIN, REFUND, CASHOUT, etc. + Amount int64 `json:"amount"` // Always in cents Currency string `json:"currency"` ExternalTransactionID string `json:"external_transaction_id"` Status string `json:"status"` // PENDING, COMPLETED, FAILED CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` + + // Alea Play specific fields + GameRoundID string `json:"game_round_id"` // Round identifier + Multiplier float64 `json:"multiplier"` // For games like Aviator + IsFreeRound bool `json:"is_free_round"` // For bonus play + OperatorID string `json:"operator_id"` // Your operator ID } +// type VirtualGameTransaction struct { +// ID int64 `json:"id"` +// SessionID int64 `json:"session_id"` +// UserID int64 `json:"user_id"` +// WalletID int64 `json:"wallet_id"` +// TransactionType string `json:"transaction_type"` // BET, WIN, REFUND, JACKPOT_WIN +// Amount int64 `json:"amount"` +// Currency string `json:"currency"` +// ExternalTransactionID string `json:"external_transaction_id"` +// Status string `json:"status"` // PENDING, COMPLETED, FAILED +// CreatedAt time.Time `json:"created_at"` +// UpdatedAt time.Time `json:"updated_at"` +// } + type CreateVirtualGameSession struct { UserID int64 GameID string @@ -53,3 +78,37 @@ type PopOKCallback struct { Timestamp int64 `json:"timestamp"` Signature string `json:"signature"` // HMAC-SHA256 signature for verification } + +type AleaPlayCallback struct { + EventID string `json:"event_id"` + TransactionID string `json:"transaction_id"` + SessionID string `json:"session_id"` + UserID string `json:"user_id"` + GameID string `json:"game_id"` + Type string `json:"type"` // BET, WIN, CASHOUT, etc. + Amount float64 `json:"amount"` + Currency string `json:"currency"` + RoundID string `json:"round_id"` + Multiplier float64 `json:"multiplier"` + IsFreeRound bool `json:"is_free_round"` + OperatorID string `json:"operator_id"` + Timestamp int64 `json:"timestamp"` + Signature string `json:"signature"` +} + +// // Extend VirtualGameTransaction for Alea compatibility +// type VirtualGameTransaction struct { +// ID string `json:"id"` +// SessionID string `json:"session_id"` +// UserID int64 `json:"user_id"` +// WalletID int64 `json:"wallet_id"` +// TransactionType string `json:"transaction_type"` // Matches Alea's types +// Amount int64 `json:"amount"` // In cents +// Currency string `json:"currency"` +// ExternalTransactionID string `json:"external_transaction_id"` +// Status string `json:"status"` +// GameID string `json:"game_id"` // Track which game this was for +// RoundID string `json:"round_id,omitempty"` +// CreatedAt time.Time `json:"created_at"` +// UpdatedAt time.Time `json:"updated_at"` +// } diff --git a/internal/middleware/alea.go b/internal/middleware/alea.go new file mode 100644 index 0000000..36e3f1b --- /dev/null +++ b/internal/middleware/alea.go @@ -0,0 +1,34 @@ +package middleware + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + + "github.com/gofiber/fiber/v2" +) + +func AleaWebhookMiddleware(secretKey string) fiber.Handler { + return func(c *fiber.Ctx) error { + // Verify IP comes from Alea's allowed IPs + // OR verify a signature header + + // Example signature verification: + receivedSig := c.Get("X-Alea-Signature") + body := c.Body() + + h := hmac.New(sha256.New, []byte(secretKey)) + h.Write(body) + expectedSig := hex.EncodeToString(h.Sum(nil)) + + if receivedSig != expectedSig { + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ + "error": "invalid signature", + }) + } + + return c.Next() + } +} + +// Then update your route: diff --git a/internal/repository/virtual_game.go b/internal/repository/virtual_game.go index cfa6fee..0fa5429 100644 --- a/internal/repository/virtual_game.go +++ b/internal/repository/virtual_game.go @@ -14,6 +14,7 @@ type VirtualGameRepository interface { CreateVirtualGameSession(ctx context.Context, session *domain.VirtualGameSession) error GetVirtualGameSessionByToken(ctx context.Context, token string) (*domain.VirtualGameSession, error) UpdateVirtualGameSessionStatus(ctx context.Context, id int64, status string) error + // UpdateVirtualGameSessionStatus(ctx context.Context, id int64, status string) error CreateVirtualGameTransaction(ctx context.Context, tx *domain.VirtualGameTransaction) error GetVirtualGameTransactionByExternalID(ctx context.Context, externalID string) (*domain.VirtualGameTransaction, error) UpdateVirtualGameTransactionStatus(ctx context.Context, id int64, status string) error diff --git a/internal/router/router.chapa.go b/internal/router/router.chapa.go deleted file mode 100644 index 6b5a09e..0000000 --- a/internal/router/router.chapa.go +++ /dev/null @@ -1,37 +0,0 @@ -package router - -// @title FortuneBet Chapa API - -// import ( -// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" -// "github.com/gofiber/fiber/v2" -// ) - -// func ChapaRoutes(app *fiber.App) { - -// chapaRouter := app.Group("/api/v1/chapa") - -// chapaRouter.Post("/payments/initialize", -// wallet.InitializePayment, -// ) - -// chapaRouter.Get("/payments/verify/:tx_ref", -// wallet.VerifyTransaction, -// ) - -// chapaRouter.Post("/payments/callback", -// wallet.ReceiveWebhook, -// ) - -// chapaRouter.Get("/banks", -// wallet.GetBanks, -// ) - -// chapaRouter.Post("/transfers", -// wallet.CreateTransfer, -// ) - -// chapaRouter.Get("/transfers/:transfer_ref", -// wallet.VerifyTransfer, -// ) -// } diff --git a/internal/services/virtualGame/Alea/port.go b/internal/services/virtualGame/Alea/port.go new file mode 100644 index 0000000..c5d4ac0 --- /dev/null +++ b/internal/services/virtualGame/Alea/port.go @@ -0,0 +1,12 @@ +package alea + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type AleaVirtualGameService interface { + GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error) + HandleCallback(ctx context.Context, callback *domain.AleaPlayCallback) error +} diff --git a/internal/services/virtualGame/Alea/service.go b/internal/services/virtualGame/Alea/service.go new file mode 100644 index 0000000..aadd179 --- /dev/null +++ b/internal/services/virtualGame/Alea/service.go @@ -0,0 +1,159 @@ +package alea + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "log/slog" + "net/url" + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/config" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" +) + +type AleaPlayService struct { + repo repository.VirtualGameRepository + walletSvc wallet.Service + config *config.AleaPlayConfig + logger *slog.Logger +} + +func NewAleaPlayService( + repo repository.VirtualGameRepository, + walletSvc wallet.Service, + cfg *config.Config, + logger *slog.Logger, +) *AleaPlayService { + return &AleaPlayService{ + repo: repo, + walletSvc: walletSvc, + config: &cfg.AleaPlay, + logger: logger, + } +} + +func (s *AleaPlayService) GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error) { + session := &domain.VirtualGameSession{ + UserID: userID, + GameID: gameID, + SessionToken: generateSessionToken(userID), + Currency: currency, + Status: "ACTIVE", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + ExpiresAt: time.Now().Add(24 * time.Hour), + } + + if err := s.repo.CreateVirtualGameSession(ctx, session); err != nil { + return "", fmt.Errorf("failed to create game session: %w", err) + } + + params := url.Values{ + "operator_id": []string{s.config.OperatorID}, + "user_id": []string{fmt.Sprintf("%d", userID)}, + "game_id": []string{gameID}, + "currency": []string{currency}, + "session_token": []string{session.SessionToken}, + "mode": []string{mode}, + "timestamp": []string{fmt.Sprintf("%d", time.Now().Unix())}, + } + + signature := s.generateSignature(params.Encode()) + params.Add("signature", signature) + + return fmt.Sprintf("%s/launch?%s", s.config.BaseURL, params.Encode()), nil +} + +func (s *AleaPlayService) HandleCallback(ctx context.Context, callback *domain.AleaPlayCallback) error { + if !s.verifyCallbackSignature(callback) { + return errors.New("invalid callback signature") + } + + if existing, _ := s.repo.GetVirtualGameTransactionByExternalID(ctx, callback.TransactionID); existing != nil { + s.logger.Warn("duplicate transaction detected", "tx_id", callback.TransactionID) + return nil + } + + session, err := s.repo.GetVirtualGameSessionByToken(ctx, callback.SessionID) + if err != nil { + return fmt.Errorf("failed to get game session: %w", err) + } + + tx := &domain.VirtualGameTransaction{ + SessionID: session.ID, + UserID: session.UserID, + TransactionType: callback.Type, + Amount: convertAmount(callback.Amount, callback.Type), + Currency: callback.Currency, + ExternalTransactionID: callback.TransactionID, + Status: "COMPLETED", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + if err := s.processTransaction(ctx, tx, session.UserID); err != nil { + return fmt.Errorf("failed to process transaction: %w", err) + } + + // Update session status using the proper repository method + if callback.Type == "SESSION_END" { + if err := s.repo.UpdateVirtualGameSessionStatus(ctx, session.ID, "COMPLETED"); err != nil { + s.logger.Error("failed to update session status", + "sessionID", session.ID, + "error", err) + } + } + + return nil +} + +func convertAmount(amount float64, txType string) int64 { + cents := int64(amount * 100) + if txType == "BET" { + return -cents + } + return cents +} + +func (s *AleaPlayService) processTransaction(ctx context.Context, tx *domain.VirtualGameTransaction, userID int64) error { + wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID) + if err != nil || len(wallets) == 0 { + return errors.New("no wallet available for user") + } + tx.WalletID = wallets[0].ID + + if err := s.walletSvc.AddToWallet(ctx, tx.WalletID, domain.Currency(tx.Amount)); err != nil { + return fmt.Errorf("wallet update failed: %w", err) + } + + return s.repo.CreateVirtualGameTransaction(ctx, tx) +} + +func (s *AleaPlayService) generateSignature(data string) string { + h := hmac.New(sha256.New, []byte(s.config.SecretKey)) + h.Write([]byte(data)) + return hex.EncodeToString(h.Sum(nil)) +} + +func (s *AleaPlayService) verifyCallbackSignature(cb *domain.AleaPlayCallback) bool { + signData := fmt.Sprintf("%s%s%s%.2f%s%d", + cb.TransactionID, + cb.SessionID, + cb.Type, + cb.Amount, + cb.Currency, + cb.Timestamp, + ) + expectedSig := s.generateSignature(signData) + return expectedSig == cb.Signature +} + +func generateSessionToken(userID int64) string { + return fmt.Sprintf("alea-%d-%d", userID, time.Now().UnixNano()) +} diff --git a/internal/services/virtualGame/port.go b/internal/services/virtualGame/port.go index d473355..0814a07 100644 --- a/internal/services/virtualGame/port.go +++ b/internal/services/virtualGame/port.go @@ -10,3 +10,4 @@ type VirtualGameService interface { GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error) HandleCallback(ctx context.Context, callback *domain.PopOKCallback) error } + diff --git a/internal/services/virtualGame/service.go b/internal/services/virtualGame/service.go index 1b5824e..688c8ef 100644 --- a/internal/services/virtualGame/service.go +++ b/internal/services/virtualGame/service.go @@ -32,8 +32,7 @@ func New(repo repository.VirtualGameRepository, walletSvc wallet.Service, store walletSvc: walletSvc, store: store, config: cfg, - logger: logger, - } + logger: logger} } func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error) { diff --git a/internal/web_server/app.go b/internal/web_server/app.go index c41d9f0..e036a6b 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -17,6 +17,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" + alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" @@ -28,27 +29,28 @@ import ( ) type App struct { - fiber *fiber.App - logger *slog.Logger - NotidicationStore notificationservice.NotificationStore - referralSvc referralservice.ReferralStore - port int - authSvc *authentication.Service - userSvc *user.Service - betSvc *bet.Service - virtualGameSvc virtualgameservice.VirtualGameService - walletSvc *wallet.Service - transactionSvc *transaction.Service - ticketSvc *ticket.Service - branchSvc *branch.Service - companySvc *company.Service - validator *customvalidator.CustomValidator - JwtConfig jwtutil.JwtConfig - Logger *slog.Logger - prematchSvc *odds.ServiceImpl - eventSvc event.Service - resultSvc *result.Service - cfg *config.Config + fiber *fiber.App + logger *slog.Logger + NotidicationStore notificationservice.NotificationStore + referralSvc referralservice.ReferralStore + port int + authSvc *authentication.Service + userSvc *user.Service + betSvc *bet.Service + virtualGameSvc virtualgameservice.VirtualGameService + aleaVirtualGameService alea.AleaVirtualGameService + walletSvc *wallet.Service + transactionSvc *transaction.Service + ticketSvc *ticket.Service + branchSvc *branch.Service + companySvc *company.Service + validator *customvalidator.CustomValidator + JwtConfig jwtutil.JwtConfig + Logger *slog.Logger + prematchSvc *odds.ServiceImpl + eventSvc event.Service + resultSvc *result.Service + cfg *config.Config } func NewApp( @@ -68,6 +70,7 @@ func NewApp( eventSvc event.Service, referralSvc referralservice.ReferralStore, virtualGameSvc virtualgameservice.VirtualGameService, + aleaVirtualGameService alea.AleaVirtualGameService, resultSvc *result.Service, cfg *config.Config, ) *App { @@ -86,27 +89,28 @@ func NewApp( })) s := &App{ - fiber: app, - port: port, - authSvc: authSvc, - validator: validator, - logger: logger, - JwtConfig: JwtConfig, - userSvc: userSvc, - ticketSvc: ticketSvc, - betSvc: betSvc, - walletSvc: walletSvc, - transactionSvc: transactionSvc, - branchSvc: branchSvc, - companySvc: companySvc, - NotidicationStore: notidicationStore, - referralSvc: referralSvc, - Logger: logger, - prematchSvc: prematchSvc, - eventSvc: eventSvc, - virtualGameSvc: virtualGameSvc, - resultSvc: resultSvc, - cfg: cfg, + fiber: app, + port: port, + authSvc: authSvc, + validator: validator, + logger: logger, + JwtConfig: JwtConfig, + userSvc: userSvc, + ticketSvc: ticketSvc, + betSvc: betSvc, + walletSvc: walletSvc, + transactionSvc: transactionSvc, + branchSvc: branchSvc, + companySvc: companySvc, + NotidicationStore: notidicationStore, + referralSvc: referralSvc, + Logger: logger, + prematchSvc: prematchSvc, + eventSvc: eventSvc, + virtualGameSvc: virtualGameSvc, + aleaVirtualGameService: aleaVirtualGameService, + resultSvc: resultSvc, + cfg: cfg, } s.initAppRoutes() diff --git a/internal/web_server/handlers/alea_games.go b/internal/web_server/handlers/alea_games.go new file mode 100644 index 0000000..01e4bb6 --- /dev/null +++ b/internal/web_server/handlers/alea_games.go @@ -0,0 +1,76 @@ +package handlers + +import ( + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/gofiber/fiber/v2" +) + +// LaunchAleaGame godoc +// @Summary Launch an Alea Play virtual game +// @Description Generates an authenticated launch URL for Alea Play virtual games +// @Tags Alea Virtual Games +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param game_id query string true "Game identifier (e.g., 'aviator', 'plinko')" +// @Param currency query string false "Currency code (ISO 4217)" Enums(USD, EUR, GBP) default(USD) +// @Param mode query string false "Game mode" Enums(real, demo) default(real) +// @Success 200 {object} map[string]string{launch_url=string} "Returns authenticated game launch URL" +// @Failure 400 {object} map[string]string "Invalid request parameters" +// @Failure 401 {object} map[string]string "Unauthorized" +// @Failure 500 {object} map[string]string "Internal server error" +// @Router /api/v1/alea-games/launch [get] +func (h *Handler) LaunchAleaGame(c *fiber.Ctx) error { + userID := c.Locals("userID").(int64) + gameID := c.Query("game_id") + currency := c.Query("currency", "USD") + mode := c.Query("mode", "real") // real or demo + + launchURL, err := h.aleaVirtualGameSvc.GenerateGameLaunchURL(c.Context(), userID, gameID, currency, mode) + if err != nil { + h.logger.Error("failed to generate Alea launch URL", "error", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "failed to launch game", + }) + } + + return c.JSON(fiber.Map{ + "launch_url": launchURL, + }) +} + +// HandleAleaCallback godoc +// @Summary Process Alea Play game callback +// @Description Handles webhook callbacks from Alea Play virtual games for bet settlement +// @Tags Alea Virtual Games +// @Accept json +// @Produce json +// @Param callback body domain.AleaPlayCallback true "Callback payload" +// @Success 200 {object} map[string]string{status=string} "Callback processed successfully" +// @Failure 400 {object} map[string]string "Invalid callback format" +// @Failure 401 {object} map[string]string "Invalid signature" +// @Failure 409 {object} map[string]string "Duplicate transaction" +// @Failure 500 {object} map[string]string "Internal processing error" +// @Router /api/v1/webhooks/alea [post] +func (h *Handler) HandleAleaCallback(c *fiber.Ctx) error { + var cb domain.AleaPlayCallback + if err := c.BodyParser(&cb); err != nil { + h.logger.Error("invalid Alea callback format", "error", err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "invalid callback format", + }) + } + + if err := h.aleaVirtualGameSvc.HandleCallback(c.Context(), &cb); err != nil { + h.logger.Error("failed to process Alea callback", + "transactionID", cb.TransactionID, + "error", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "failed to process callback", + }) + } + + return c.JSON(fiber.Map{ + "status": "processed", + }) +} diff --git a/internal/web_server/handlers/chapa.go b/internal/web_server/handlers/chapa.go index 1f80787..3fc66c0 100644 --- a/internal/web_server/handlers/chapa.go +++ b/internal/web_server/handlers/chapa.go @@ -17,7 +17,7 @@ import ( // @Tags Chapa // @Accept json // @Produce json -// @Success 200 {object} ChapaSupportedBanksResponse +// @Success 200 {object} domain.ChapaSupportedBanksResponse // @Router /api/v1/chapa/banks [get] func (h *Handler) GetBanks(c *fiber.Ctx) error { httpReq, err := http.NewRequest("GET", h.Cfg.CHAPA_BASE_URL+"/banks", nil) @@ -47,10 +47,8 @@ func (h *Handler) GetBanks(c *fiber.Ctx) error { // @Tags Chapa // @Accept json // @Produce json -// @Param payload body InitPaymentRequest true "Payment initialization request" -// @Success 200 {object} InitPaymentResponse -// @Failure 400 {object} map[string]string -// @Failure 500 {object} map[string]string +// @Param payload body domain.InitPaymentRequest true "Payment initialization request" +// @Success 200 {object} domain.InitPaymentResponse // @Router /api/v1/chapa/payments/initialize [post] func (h *Handler) InitializePayment(c *fiber.Ctx) error { var req InitPaymentRequest @@ -109,9 +107,7 @@ func (h *Handler) InitializePayment(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param tx_ref path string true "Transaction Reference" -// @Success 200 {object} VerifyTransactionResponse -// @Failure 400 {object} map[string]string -// @Failure 500 {object} map[string]string +// @Success 200 {object} domain.VerifyTransactionResponse // @Router /api/v1/chapa/payments/verify/{tx_ref} [get] func (h *Handler) VerifyTransaction(c *fiber.Ctx) error { txRef := c.Params("tx_ref") @@ -183,10 +179,8 @@ func (h *Handler) ReceiveWebhook(c *fiber.Ctx) error { // @Tags Chapa // @Accept json // @Produce json -// @Param payload body TransferRequest true "Transfer request body" -// @Success 200 {object} CreateTransferResponse -// @Failure 400 {object} map[string]string -// @Failure 500 {object} map[string]string +// @Param payload body domain.TransferRequest true "Transfer request body" +// @Success 200 {object} domain.CreateTransferResponse // @Router /api/v1/chapa/transfers [post] func (h *Handler) CreateTransfer(c *fiber.Ctx) error { var req TransferRequest @@ -246,9 +240,7 @@ func (h *Handler) CreateTransfer(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param transfer_ref path string true "Transfer Reference" -// @Success 200 {object} VerifyTransferResponse -// @Failure 400 {object} map[string]string -// @Failure 500 {object} map[string]string +// @Success 200 {object} domain.VerifyTransferResponse // @Router /api/v1/chapa/transfers/verify/{transfer_ref} [get] func (h *Handler) VerifyTransfer(c *fiber.Ctx) error { transferRef := c.Params("transfer_ref") diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index 2b4b836..ac4d495 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -16,29 +16,31 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" + alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" ) type Handler struct { - logger *slog.Logger - notificationSvc notificationservice.NotificationStore - userSvc *user.Service - referralSvc referralservice.ReferralStore - walletSvc *wallet.Service - transactionSvc *transaction.Service - ticketSvc *ticket.Service - betSvc *bet.Service - branchSvc *branch.Service - companySvc *company.Service - prematchSvc *odds.ServiceImpl - eventSvc event.Service - virtualGameSvc virtualgameservice.VirtualGameService - authSvc *authentication.Service - jwtConfig jwtutil.JwtConfig - validator *customvalidator.CustomValidator - Cfg *config.Config + logger *slog.Logger + notificationSvc notificationservice.NotificationStore + userSvc *user.Service + referralSvc referralservice.ReferralStore + walletSvc *wallet.Service + transactionSvc *transaction.Service + ticketSvc *ticket.Service + betSvc *bet.Service + branchSvc *branch.Service + companySvc *company.Service + prematchSvc *odds.ServiceImpl + eventSvc event.Service + virtualGameSvc virtualgameservice.VirtualGameService + aleaVirtualGameSvc alea.AleaVirtualGameService + authSvc *authentication.Service + jwtConfig jwtutil.JwtConfig + validator *customvalidator.CustomValidator + Cfg *config.Config } func New( @@ -48,6 +50,7 @@ func New( walletSvc *wallet.Service, referralSvc referralservice.ReferralStore, virtualGameSvc virtualgameservice.VirtualGameService, + aleaVirtualGameSvc alea.AleaVirtualGameService, userSvc *user.Service, transactionSvc *transaction.Service, ticketSvc *ticket.Service, @@ -61,22 +64,23 @@ func New( cfg *config.Config, ) *Handler { return &Handler{ - logger: logger, - notificationSvc: notificationSvc, - walletSvc: walletSvc, - referralSvc: referralSvc, - validator: validator, - userSvc: userSvc, - transactionSvc: transactionSvc, - ticketSvc: ticketSvc, - betSvc: betSvc, - branchSvc: branchSvc, - companySvc: companySvc, - prematchSvc: prematchSvc, - eventSvc: eventSvc, - virtualGameSvc: virtualGameSvc, - authSvc: authSvc, - jwtConfig: jwtConfig, - Cfg: cfg, + logger: logger, + notificationSvc: notificationSvc, + walletSvc: walletSvc, + referralSvc: referralSvc, + validator: validator, + userSvc: userSvc, + transactionSvc: transactionSvc, + ticketSvc: ticketSvc, + betSvc: betSvc, + branchSvc: branchSvc, + companySvc: companySvc, + prematchSvc: prematchSvc, + eventSvc: eventSvc, + virtualGameSvc: virtualGameSvc, + aleaVirtualGameSvc: aleaVirtualGameSvc, + authSvc: authSvc, + jwtConfig: jwtConfig, + Cfg: cfg, } } diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 20e84f5..fad2586 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -5,10 +5,8 @@ import ( "strconv" _ "github.com/SamuelTariku/FortuneBet-Backend/docs" - // "github.com/SamuelTariku/FortuneBet-Backend/internal/config" - // "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - // "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" + "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/handlers" "github.com/gofiber/fiber/v2" @@ -23,6 +21,7 @@ func (a *App) initAppRoutes() { a.walletSvc, a.referralSvc, a.virtualGameSvc, + a.aleaVirtualGameService, a.userSvc, a.transactionSvc, a.ticketSvc, @@ -174,6 +173,11 @@ func (a *App) initAppRoutes() { a.fiber.Post("/api/v1/chapa/transfers", h.CreateTransfer) a.fiber.Get("/api/v1/chapa/transfers/verify/:transfer_ref", h.VerifyTransfer) + //Alea Play Virtual Game Routes + a.fiber.Get("/api/v1/alea-games/launch", h.LaunchAleaGame) + a.fiber.Post("/api/v1/webhooks/alea", h.HandleAleaCallback) + // a.fiber.Post("/webhooks/alea", middleware.AleaWebhookMiddleware(a.cfg.AleaPlay.SecretKey), h.HandleAleaCallback) + // Transactions /transactions a.fiber.Post("/transaction", a.authMiddleware, h.CreateTransaction) a.fiber.Get("/transaction", a.authMiddleware, h.GetAllTransactions) From 97938545966c7bae4a084e01f6a404a2cc3eb9f8 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Fri, 23 May 2025 20:37:29 +0300 Subject: [PATCH 4/7] authmiddleware fix --- docs/docs.go | 63 ---------------------- docs/swagger.json | 63 ---------------------- docs/swagger.yaml | 42 --------------- internal/web_server/handlers/alea_games.go | 10 +--- internal/web_server/routes.go | 4 +- 5 files changed, 4 insertions(+), 178 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 235d7ff..875d103 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -200,33 +200,6 @@ const docTemplate = `{ ] } } - }, - "400": { - "description": "Invalid request parameters", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal server error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } } } } @@ -465,42 +438,6 @@ const docTemplate = `{ ] } } - }, - "400": { - "description": "Invalid callback format", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "401": { - "description": "Invalid signature", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "409": { - "description": "Duplicate transaction", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal processing error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } } } } diff --git a/docs/swagger.json b/docs/swagger.json index 1b046b1..4c54116 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -192,33 +192,6 @@ ] } } - }, - "400": { - "description": "Invalid request parameters", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal server error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } } } } @@ -457,42 +430,6 @@ ] } } - }, - "400": { - "description": "Invalid callback format", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "401": { - "description": "Invalid signature", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "409": { - "description": "Duplicate transaction", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal processing error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } } } } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index cc038bf..33afae3 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1440,24 +1440,6 @@ paths: type: string type: object type: object - "400": - description: Invalid request parameters - schema: - additionalProperties: - type: string - type: object - "401": - description: Unauthorized - schema: - additionalProperties: - type: string - type: object - "500": - description: Internal server error - schema: - additionalProperties: - type: string - type: object security: - BearerAuth: [] summary: Launch an Alea Play virtual game @@ -1613,30 +1595,6 @@ paths: type: string type: object type: object - "400": - description: Invalid callback format - schema: - additionalProperties: - type: string - type: object - "401": - description: Invalid signature - schema: - additionalProperties: - type: string - type: object - "409": - description: Duplicate transaction - schema: - additionalProperties: - type: string - type: object - "500": - description: Internal processing error - schema: - additionalProperties: - type: string - type: object summary: Process Alea Play game callback tags: - Alea Virtual Games diff --git a/internal/web_server/handlers/alea_games.go b/internal/web_server/handlers/alea_games.go index 01e4bb6..0fc8ea3 100644 --- a/internal/web_server/handlers/alea_games.go +++ b/internal/web_server/handlers/alea_games.go @@ -16,12 +16,9 @@ import ( // @Param currency query string false "Currency code (ISO 4217)" Enums(USD, EUR, GBP) default(USD) // @Param mode query string false "Game mode" Enums(real, demo) default(real) // @Success 200 {object} map[string]string{launch_url=string} "Returns authenticated game launch URL" -// @Failure 400 {object} map[string]string "Invalid request parameters" -// @Failure 401 {object} map[string]string "Unauthorized" -// @Failure 500 {object} map[string]string "Internal server error" // @Router /api/v1/alea-games/launch [get] func (h *Handler) LaunchAleaGame(c *fiber.Ctx) error { - userID := c.Locals("userID").(int64) + userID := c.Locals("user_id").(int64) gameID := c.Query("game_id") currency := c.Query("currency", "USD") mode := c.Query("mode", "real") // real or demo @@ -36,6 +33,7 @@ func (h *Handler) LaunchAleaGame(c *fiber.Ctx) error { return c.JSON(fiber.Map{ "launch_url": launchURL, + "message": "Game launched successfully", }) } @@ -47,10 +45,6 @@ func (h *Handler) LaunchAleaGame(c *fiber.Ctx) error { // @Produce json // @Param callback body domain.AleaPlayCallback true "Callback payload" // @Success 200 {object} map[string]string{status=string} "Callback processed successfully" -// @Failure 400 {object} map[string]string "Invalid callback format" -// @Failure 401 {object} map[string]string "Invalid signature" -// @Failure 409 {object} map[string]string "Duplicate transaction" -// @Failure 500 {object} map[string]string "Internal processing error" // @Router /api/v1/webhooks/alea [post] func (h *Handler) HandleAleaCallback(c *fiber.Ctx) error { var cb domain.AleaPlayCallback diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index fad2586..7a18943 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -174,8 +174,8 @@ func (a *App) initAppRoutes() { a.fiber.Get("/api/v1/chapa/transfers/verify/:transfer_ref", h.VerifyTransfer) //Alea Play Virtual Game Routes - a.fiber.Get("/api/v1/alea-games/launch", h.LaunchAleaGame) - a.fiber.Post("/api/v1/webhooks/alea", h.HandleAleaCallback) + a.fiber.Get("/api/v1/alea-games/launch", a.authMiddleware, h.LaunchAleaGame) + a.fiber.Post("/api/v1/webhooks/alea", a.authMiddleware, h.HandleAleaCallback) // a.fiber.Post("/webhooks/alea", middleware.AleaWebhookMiddleware(a.cfg.AleaPlay.SecretKey), h.HandleAleaCallback) // Transactions /transactions From ee07d469ebcc3c26521095800ce574845a43cc13 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Sat, 24 May 2025 19:39:24 +0300 Subject: [PATCH 5/7] Veli Games --- cmd/main.go | 10 +- docs/docs.go | 179 ++++++++++++++++++ docs/swagger.json | 179 ++++++++++++++++++ docs/swagger.yaml | 121 ++++++++++++ internal/config/config.go | 109 ++++++++++- internal/domain/virtual_game.go | 37 ++-- internal/services/virtualGame/veli/port.go | 13 ++ internal/services/virtualGame/veli/service.go | 161 ++++++++++++++++ internal/web_server/app.go | 4 + internal/web_server/handlers/handlers.go | 4 + internal/web_server/handlers/veli_games.go | 75 ++++++++ internal/web_server/routes.go | 20 +- 12 files changed, 878 insertions(+), 34 deletions(-) create mode 100644 internal/services/virtualGame/veli/port.go create mode 100644 internal/services/virtualGame/veli/service.go create mode 100644 internal/web_server/handlers/veli_games.go diff --git a/cmd/main.go b/cmd/main.go index c5eebd7..a474956 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -30,6 +30,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" // "github.com/SamuelTariku/FortuneBet-Backend/internal/utils" @@ -99,13 +100,20 @@ func main() { logger, ) + veliService := veli.NewVeliPlayService( + vitualGameRepo, + *walletSvc, + cfg, + logger, + ) + httpserver.StartDataFetchingCrons(eventSvc, oddsSvc, resultSvc) app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{ JwtAccessKey: cfg.JwtKey, JwtAccessExpiry: cfg.AccessExpiry, }, userSvc, - ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc, referalSvc, virtualGameSvc, aleaService, resultSvc, cfg) + ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc, referalSvc, virtualGameSvc, aleaService, veliService, resultSvc, cfg) logger.Info("Starting server", "port", cfg.Port) if err := app.Run(); err != nil { diff --git a/docs/docs.go b/docs/docs.go index 875d103..47695ae 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -442,6 +442,76 @@ const docTemplate = `{ } } }, + "/api/veli/launch/{game_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Generates authenticated launch URL for Veli games", + "tags": [ + "Veli Games" + ], + "summary": "Launch a Veli game", + "parameters": [ + { + "type": "string", + "description": "Game ID (e.g., veli_aviator_v1)", + "name": "game_id", + "in": "path", + "required": true + }, + { + "type": "string", + "default": "USD", + "description": "Currency code", + "name": "currency", + "in": "query" + }, + { + "enum": [ + "real", + "demo" + ], + "type": "string", + "default": "real", + "description": "Game mode", + "name": "mode", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Returns launch URL", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Invalid request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/auth/login": { "post": { "description": "Login customer", @@ -3627,6 +3697,70 @@ const docTemplate = `{ } } } + }, + "/webhooks/veli": { + "post": { + "description": "Processes game round settlements from Veli", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Virtual Games" + ], + "summary": "Veli Games webhook handler", + "parameters": [ + { + "description": "Callback payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.VeliCallback" + } + } + ], + "responses": { + "200": { + "description": "Callback processed", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Invalid payload", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Invalid signature", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Processing error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } } }, "definitions": { @@ -4291,6 +4425,51 @@ const docTemplate = `{ } } }, + "domain.VeliCallback": { + "type": "object", + "properties": { + "amount": { + "description": "Transaction amount", + "type": "number" + }, + "currency": { + "description": "e.g., \"USD\"", + "type": "string" + }, + "event_type": { + "description": "\"bet_placed\", \"game_result\", etc.", + "type": "string" + }, + "game_id": { + "description": "e.g., \"veli_aviator_v1\"", + "type": "string" + }, + "multiplier": { + "description": "For games with multipliers (Aviator/Plinko)", + "type": "number" + }, + "round_id": { + "description": "Unique round identifier (replaces transaction_id)", + "type": "string" + }, + "session_id": { + "description": "Matches VirtualGameSession.SessionToken", + "type": "string" + }, + "signature": { + "description": "HMAC-SHA256", + "type": "string" + }, + "timestamp": { + "description": "Unix timestamp", + "type": "integer" + }, + "user_id": { + "description": "Veli's user identifier", + "type": "string" + } + } + }, "domain.VerifyTransactionResponse": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 4c54116..a0fc42c 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -434,6 +434,76 @@ } } }, + "/api/veli/launch/{game_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Generates authenticated launch URL for Veli games", + "tags": [ + "Veli Games" + ], + "summary": "Launch a Veli game", + "parameters": [ + { + "type": "string", + "description": "Game ID (e.g., veli_aviator_v1)", + "name": "game_id", + "in": "path", + "required": true + }, + { + "type": "string", + "default": "USD", + "description": "Currency code", + "name": "currency", + "in": "query" + }, + { + "enum": [ + "real", + "demo" + ], + "type": "string", + "default": "real", + "description": "Game mode", + "name": "mode", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Returns launch URL", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Invalid request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/auth/login": { "post": { "description": "Login customer", @@ -3619,6 +3689,70 @@ } } } + }, + "/webhooks/veli": { + "post": { + "description": "Processes game round settlements from Veli", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Virtual Games" + ], + "summary": "Veli Games webhook handler", + "parameters": [ + { + "description": "Callback payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.VeliCallback" + } + } + ], + "responses": { + "200": { + "description": "Callback processed", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Invalid payload", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Invalid signature", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Processing error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } } }, "definitions": { @@ -4283,6 +4417,51 @@ } } }, + "domain.VeliCallback": { + "type": "object", + "properties": { + "amount": { + "description": "Transaction amount", + "type": "number" + }, + "currency": { + "description": "e.g., \"USD\"", + "type": "string" + }, + "event_type": { + "description": "\"bet_placed\", \"game_result\", etc.", + "type": "string" + }, + "game_id": { + "description": "e.g., \"veli_aviator_v1\"", + "type": "string" + }, + "multiplier": { + "description": "For games with multipliers (Aviator/Plinko)", + "type": "number" + }, + "round_id": { + "description": "Unique round identifier (replaces transaction_id)", + "type": "string" + }, + "session_id": { + "description": "Matches VirtualGameSession.SessionToken", + "type": "string" + }, + "signature": { + "description": "HMAC-SHA256", + "type": "string" + }, + "timestamp": { + "description": "Unix timestamp", + "type": "integer" + }, + "user_id": { + "description": "Veli's user identifier", + "type": "string" + } + } + }, "domain.VerifyTransactionResponse": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 33afae3..2efae74 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -454,6 +454,39 @@ definitions: description: Converted from "time" field in UNIX format type: string type: object + domain.VeliCallback: + properties: + amount: + description: Transaction amount + type: number + currency: + description: e.g., "USD" + type: string + event_type: + description: '"bet_placed", "game_result", etc.' + type: string + game_id: + description: e.g., "veli_aviator_v1" + type: string + multiplier: + description: For games with multipliers (Aviator/Plinko) + type: number + round_id: + description: Unique round identifier (replaces transaction_id) + type: string + session_id: + description: Matches VirtualGameSession.SessionToken + type: string + signature: + description: HMAC-SHA256 + type: string + timestamp: + description: Unix timestamp + type: integer + user_id: + description: Veli's user identifier + type: string + type: object domain.VerifyTransactionResponse: properties: data: @@ -1598,6 +1631,52 @@ paths: summary: Process Alea Play game callback tags: - Alea Virtual Games + /api/veli/launch/{game_id}: + get: + description: Generates authenticated launch URL for Veli games + parameters: + - description: Game ID (e.g., veli_aviator_v1) + in: path + name: game_id + required: true + type: string + - default: USD + description: Currency code + in: query + name: currency + type: string + - default: real + description: Game mode + enum: + - real + - demo + in: query + name: mode + type: string + responses: + "200": + description: Returns launch URL + schema: + additionalProperties: + type: string + type: object + "400": + description: Invalid request + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal server error + schema: + additionalProperties: + type: string + type: object + security: + - BearerAuth: [] + summary: Launch a Veli game + tags: + - Veli Games /auth/login: post: consumes: @@ -3689,6 +3768,48 @@ paths: summary: Activate and Deactivate Wallet tags: - wallet + /webhooks/veli: + post: + consumes: + - application/json + description: Processes game round settlements from Veli + parameters: + - description: Callback payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/domain.VeliCallback' + produces: + - application/json + responses: + "200": + description: Callback processed + schema: + additionalProperties: + type: string + type: object + "400": + description: Invalid payload + schema: + additionalProperties: + type: string + type: object + "403": + description: Invalid signature + schema: + additionalProperties: + type: string + type: object + "500": + description: Processing error + schema: + additionalProperties: + type: string + type: object + summary: Veli Games webhook handler + tags: + - Virtual Games securityDefinitions: Bearer: in: header diff --git a/internal/config/config.go b/internal/config/config.go index 5187325..eba9702 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,6 +2,7 @@ package config import ( "errors" + "fmt" "log/slog" "os" "strconv" @@ -26,20 +27,32 @@ var ( ErrInvalidPopOKSecretKey = errors.New("PopOK secret key is invalid") ErrInvalidPopOKBaseURL = errors.New("PopOK base URL is invalid") ErrInvalidPopOKCallbackURL = errors.New("PopOK callback URL is invalid") + ErrInvalidVeliAPIURL = errors.New("Veli API URL is invalid") + ErrInvalidVeliOperatorKey = errors.New("Veli operator key is invalid") + ErrInvalidVeliSecretKey = errors.New("Veli secret key is invalid") ) type AleaPlayConfig struct { - Enabled bool `mapstructure:"enabled"` - BaseURL string `mapstructure:"base_url"` // "https://api.aleaplay.com" - OperatorID string `mapstructure:"operator_id"` // Your operator ID with Alea - SecretKey string `mapstructure:"secret_key"` // API secret for signatures - GameListURL string `mapstructure:"game_list_url"` // Endpoint to fetch available games - - // Optional settings + Enabled bool `mapstructure:"enabled"` + BaseURL string `mapstructure:"base_url"` // "https://api.aleaplay.com" + OperatorID string `mapstructure:"operator_id"` // Your operator ID with Alea + SecretKey string `mapstructure:"secret_key"` // API secret for signatures + GameListURL string `mapstructure:"game_list_url"` // Endpoint to fetch available games DefaultCurrency string `mapstructure:"default_currency"` // "USD", "EUR", etc. SessionTimeout int `mapstructure:"session_timeout"` // In hours } +type VeliGamesConfig struct { + Enabled bool `mapstructure:"enabled"` + APIURL string `mapstructure:"api_url"` + OperatorKey string `mapstructure:"operator_key"` + SecretKey string `mapstructure:"secret_key"` + DefaultCurrency string `mapstructure:"default_currency"` + GameIDs struct { + Aviator string `mapstructure:"aviator"` + } `mapstructure:"game_ids"` +} + type Config struct { Port int DbUrl string @@ -60,7 +73,8 @@ type Config struct { CHAPA_RETURN_URL string Bet365Token string PopOK domain.PopOKConfig - AleaPlay AleaPlayConfig `mapstructure:"alea_play"` + AleaPlay AleaPlayConfig `mapstructure:"alea_play"` + VeliGames VeliGamesConfig `mapstructure:"veli_games"` } func NewConfig() (*Config, error) { @@ -135,6 +149,7 @@ func (c *Config) loadEnv() error { return ErrInvalidLevel } + //Chapa c.CHAPA_SECRET_KEY = os.Getenv("CHAPA_SECRET_KEY") c.CHAPA_PUBLIC_KEY = os.Getenv("CHAPA_PUBLIC_KEY") c.CHAPA_ENCRYPTION_KEY = os.Getenv("CHAPA_ENCRYPTION_KEY") @@ -145,6 +160,84 @@ func (c *Config) loadEnv() error { c.CHAPA_CALLBACK_URL = os.Getenv("CHAPA_CALLBACK_URL") c.CHAPA_RETURN_URL = os.Getenv("CHAPA_RETURN_URL") + //Alea Play + aleaEnabled := os.Getenv("ALEA_ENABLED") + if aleaEnabled == "" { + aleaEnabled = "false" // Default disabled + } + + if enabled, err := strconv.ParseBool(aleaEnabled); err != nil { + return fmt.Errorf("invalid ALEA_ENABLED value: %w", err) + } else { + c.AleaPlay.Enabled = enabled + } + + c.AleaPlay.BaseURL = os.Getenv("ALEA_BASE_URL") + if c.AleaPlay.BaseURL == "" && c.AleaPlay.Enabled { + return errors.New("ALEA_BASE_URL is required when Alea is enabled") + } + + c.AleaPlay.OperatorID = os.Getenv("ALEA_OPERATOR_ID") + if c.AleaPlay.OperatorID == "" && c.AleaPlay.Enabled { + return errors.New("ALEA_OPERATOR_ID is required when Alea is enabled") + } + + c.AleaPlay.SecretKey = os.Getenv("ALEA_SECRET_KEY") + if c.AleaPlay.SecretKey == "" && c.AleaPlay.Enabled { + return errors.New("ALEA_SECRET_KEY is required when Alea is enabled") + } + + c.AleaPlay.GameListURL = os.Getenv("ALEA_GAME_LIST_URL") + c.AleaPlay.DefaultCurrency = os.Getenv("ALEA_DEFAULT_CURRENCY") + if c.AleaPlay.DefaultCurrency == "" { + c.AleaPlay.DefaultCurrency = "USD" + } + + sessionTimeoutStr := os.Getenv("ALEA_SESSION_TIMEOUT") + if sessionTimeoutStr != "" { + timeout, err := strconv.Atoi(sessionTimeoutStr) + if err == nil { + c.AleaPlay.SessionTimeout = timeout + } + } + + //Veli Games + veliEnabled := os.Getenv("VELI_ENABLED") + if veliEnabled == "" { + veliEnabled = "false" // Default to disabled if not specified + } + + if enabled, err := strconv.ParseBool(veliEnabled); err != nil { + return fmt.Errorf("invalid VELI_ENABLED value: %w", err) + } else { + c.VeliGames.Enabled = enabled + } + + apiURL := os.Getenv("VELI_API_URL") + if apiURL == "" { + apiURL = "https://api.velitech.games" // Default production URL + } + c.VeliGames.APIURL = apiURL + + operatorKey := os.Getenv("VELI_OPERATOR_KEY") + if operatorKey == "" && c.VeliGames.Enabled { + return ErrInvalidVeliOperatorKey + } + c.VeliGames.OperatorKey = operatorKey + + secretKey := os.Getenv("VELI_SECRET_KEY") + if secretKey == "" && c.VeliGames.Enabled { + return ErrInvalidVeliSecretKey + } + c.VeliGames.SecretKey = secretKey + c.VeliGames.GameIDs.Aviator = os.Getenv("VELI_GAME_ID_AVIATOR") + + defaultCurrency := os.Getenv("VELI_DEFAULT_CURRENCY") + if defaultCurrency == "" { + defaultCurrency = "USD" // Default currency + } + c.VeliGames.DefaultCurrency = defaultCurrency + c.LogLevel = lvl c.AFRO_SMS_API_KEY = os.Getenv("AFRO_SMS_API_KEY") diff --git a/internal/domain/virtual_game.go b/internal/domain/virtual_game.go index 1866519..3663aee 100644 --- a/internal/domain/virtual_game.go +++ b/internal/domain/virtual_game.go @@ -39,6 +39,9 @@ type VirtualGameTransaction struct { Multiplier float64 `json:"multiplier"` // For games like Aviator IsFreeRound bool `json:"is_free_round"` // For bonus play OperatorID string `json:"operator_id"` // Your operator ID + + // Veli specific fields + GameSpecificData GameSpecificData `json:"game_specific_data"` } // type VirtualGameTransaction struct { @@ -96,19 +99,21 @@ type AleaPlayCallback struct { Signature string `json:"signature"` } -// // Extend VirtualGameTransaction for Alea compatibility -// type VirtualGameTransaction struct { -// ID string `json:"id"` -// SessionID string `json:"session_id"` -// UserID int64 `json:"user_id"` -// WalletID int64 `json:"wallet_id"` -// TransactionType string `json:"transaction_type"` // Matches Alea's types -// Amount int64 `json:"amount"` // In cents -// Currency string `json:"currency"` -// ExternalTransactionID string `json:"external_transaction_id"` -// Status string `json:"status"` -// GameID string `json:"game_id"` // Track which game this was for -// RoundID string `json:"round_id,omitempty"` -// CreatedAt time.Time `json:"created_at"` -// UpdatedAt time.Time `json:"updated_at"` -// } +type VeliCallback struct { + EventType string `json:"event_type"` // "bet_placed", "game_result", etc. + RoundID string `json:"round_id"` // Unique round identifier (replaces transaction_id) + SessionID string `json:"session_id"` // Matches VirtualGameSession.SessionToken + UserID string `json:"user_id"` // Veli's user identifier + GameID string `json:"game_id"` // e.g., "veli_aviator_v1" + Amount float64 `json:"amount"` // Transaction amount + Multiplier float64 `json:"multiplier"` // For games with multipliers (Aviator/Plinko) + Currency string `json:"currency"` // e.g., "USD" + Timestamp int64 `json:"timestamp"` // Unix timestamp + Signature string `json:"signature"` // HMAC-SHA256 +} + +type GameSpecificData struct { + Multiplier float64 `json:"multiplier,omitempty"` + RiskLevel string `json:"risk_level,omitempty"` // For Mines + BucketIndex int `json:"bucket_index,omitempty"` // For Plinko +} diff --git a/internal/services/virtualGame/veli/port.go b/internal/services/virtualGame/veli/port.go new file mode 100644 index 0000000..c2e7277 --- /dev/null +++ b/internal/services/virtualGame/veli/port.go @@ -0,0 +1,13 @@ +// services/veli/service.go +package veli + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type VeliVirtualGameService interface { + GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error) + HandleCallback(ctx context.Context, callback *domain.VeliCallback) error +} diff --git a/internal/services/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go new file mode 100644 index 0000000..33adb25 --- /dev/null +++ b/internal/services/virtualGame/veli/service.go @@ -0,0 +1,161 @@ +package veli + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "log/slog" + "net/url" + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/config" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" +) + +type VeliPlayService struct { + repo repository.VirtualGameRepository + walletSvc wallet.Service + config *config.VeliGamesConfig + logger *slog.Logger +} + +func NewVeliPlayService( + repo repository.VirtualGameRepository, + walletSvc wallet.Service, + cfg *config.Config, + logger *slog.Logger, +) *VeliPlayService { + return &VeliPlayService{ + repo: repo, + walletSvc: walletSvc, + config: &cfg.VeliGames, + logger: logger, + } +} + +// GenerateGameLaunchURL mirrors Alea's pattern but uses Veli's auth requirements +func (s *VeliPlayService) GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error) { + session := &domain.VirtualGameSession{ + UserID: userID, + GameID: gameID, + SessionToken: generateSessionToken(userID), + Currency: currency, + Status: "ACTIVE", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + ExpiresAt: time.Now().Add(24 * time.Hour), + } + + if err := s.repo.CreateVirtualGameSession(ctx, session); err != nil { + return "", fmt.Errorf("failed to create game session: %w", err) + } + + // Veli-specific parameters + params := url.Values{ + "operator_key": []string{s.config.OperatorKey}, // Different from Alea's operator_id + "user_id": []string{fmt.Sprintf("%d", userID)}, + "game_id": []string{gameID}, + "currency": []string{currency}, + "mode": []string{mode}, + "timestamp": []string{fmt.Sprintf("%d", time.Now().Unix())}, + } + + signature := s.generateSignature(params.Encode()) + params.Add("signature", signature) + + return fmt.Sprintf("%s/launch?%s", s.config.APIURL, params.Encode()), nil +} + +// HandleCallback processes Veli's webhooks (similar structure to Alea) +func (s *VeliPlayService) HandleCallback(ctx context.Context, callback *domain.VeliCallback) error { + if !s.verifyCallbackSignature(callback) { + return errors.New("invalid callback signature") + } + + // Veli uses round_id instead of transaction_id for idempotency + existing, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, callback.RoundID) + if err != nil || existing != nil { + s.logger.Warn("duplicate round detected", "round_id", callback.RoundID) + return nil + } + + session, err := s.repo.GetVirtualGameSessionByToken(ctx, callback.SessionID) + if err != nil { + return fmt.Errorf("failed to get game session: %w", err) + } + + // Convert amount based on event type (BET, WIN, etc.) + amount := convertAmount(callback.Amount, callback.EventType) + + tx := &domain.VirtualGameTransaction{ + SessionID: session.ID, + UserID: session.UserID, + TransactionType: callback.EventType, // e.g., "bet_placed", "game_result" + Amount: amount, + Currency: callback.Currency, + ExternalTransactionID: callback.RoundID, // Veli uses round_id as the unique identifier + Status: "COMPLETED", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + GameSpecificData: domain.GameSpecificData{ + Multiplier: callback.Multiplier, // Used for Aviator/Plinko + }, + } + + if err := s.processTransaction(ctx, tx, session.UserID); err != nil { + return fmt.Errorf("failed to process transaction: %w", err) + } + + return nil +} + +// Shared helper methods (same pattern as Alea) +func (s *VeliPlayService) generateSignature(data string) string { + h := hmac.New(sha256.New, []byte(s.config.SecretKey)) + h.Write([]byte(data)) + return hex.EncodeToString(h.Sum(nil)) +} + +func (s *VeliPlayService) verifyCallbackSignature(cb *domain.VeliCallback) bool { + signData := fmt.Sprintf("%s%s%s%.2f%s%d", + cb.RoundID, // Veli uses round_id instead of transaction_id + cb.SessionID, + cb.EventType, + cb.Amount, + cb.Currency, + cb.Timestamp, + ) + expectedSig := s.generateSignature(signData) + return expectedSig == cb.Signature +} + +func convertAmount(amount float64, eventType string) int64 { + cents := int64(amount * 100) + if eventType == "bet_placed" { + return -cents // Debit for bets + } + return cents // Credit for wins/results +} + +func generateSessionToken(userID int64) string { + return fmt.Sprintf("veli-%d-%d", userID, time.Now().UnixNano()) +} + +func (s *VeliPlayService) processTransaction(ctx context.Context, tx *domain.VirtualGameTransaction, userID int64) error { + wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID) + if err != nil || len(wallets) == 0 { + return errors.New("no wallet available for user") + } + tx.WalletID = wallets[0].ID + + if err := s.walletSvc.AddToWallet(ctx, tx.WalletID, domain.Currency(tx.Amount)); err != nil { + return fmt.Errorf("wallet update failed: %w", err) + } + + return s.repo.CreateVirtualGameTransaction(ctx, tx) +} diff --git a/internal/web_server/app.go b/internal/web_server/app.go index e036a6b..2c7c9d2 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -18,6 +18,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" @@ -39,6 +40,7 @@ type App struct { betSvc *bet.Service virtualGameSvc virtualgameservice.VirtualGameService aleaVirtualGameService alea.AleaVirtualGameService + veliVirtualGameService veli.VeliVirtualGameService walletSvc *wallet.Service transactionSvc *transaction.Service ticketSvc *ticket.Service @@ -71,6 +73,7 @@ func NewApp( referralSvc referralservice.ReferralStore, virtualGameSvc virtualgameservice.VirtualGameService, aleaVirtualGameService alea.AleaVirtualGameService, + veliVirtualGameService veli.VeliVirtualGameService, resultSvc *result.Service, cfg *config.Config, ) *App { @@ -109,6 +112,7 @@ func NewApp( eventSvc: eventSvc, virtualGameSvc: virtualGameSvc, aleaVirtualGameService: aleaVirtualGameService, + veliVirtualGameService: veliVirtualGameService, resultSvc: resultSvc, cfg: cfg, } diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index ac4d495..c81b43f 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -17,6 +17,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" @@ -37,6 +38,7 @@ type Handler struct { eventSvc event.Service virtualGameSvc virtualgameservice.VirtualGameService aleaVirtualGameSvc alea.AleaVirtualGameService + veliVirtualGameSvc veli.VeliVirtualGameService authSvc *authentication.Service jwtConfig jwtutil.JwtConfig validator *customvalidator.CustomValidator @@ -51,6 +53,7 @@ func New( referralSvc referralservice.ReferralStore, virtualGameSvc virtualgameservice.VirtualGameService, aleaVirtualGameSvc alea.AleaVirtualGameService, + veliVirtualGameSvc veli.VeliVirtualGameService, userSvc *user.Service, transactionSvc *transaction.Service, ticketSvc *ticket.Service, @@ -79,6 +82,7 @@ func New( eventSvc: eventSvc, virtualGameSvc: virtualGameSvc, aleaVirtualGameSvc: aleaVirtualGameSvc, + veliVirtualGameSvc: veliVirtualGameSvc, authSvc: authSvc, jwtConfig: jwtConfig, Cfg: cfg, diff --git a/internal/web_server/handlers/veli_games.go b/internal/web_server/handlers/veli_games.go new file mode 100644 index 0000000..a972bc6 --- /dev/null +++ b/internal/web_server/handlers/veli_games.go @@ -0,0 +1,75 @@ +package handlers + +import ( + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/gofiber/fiber/v2" +) + +// LaunchVeliGame godoc +// @Summary Launch a Veli game +// @Description Generates authenticated launch URL for Veli games +// @Tags Veli Games +// @Security BearerAuth +// @Param game_id path string true "Game ID (e.g., veli_aviator_v1)" +// @Param currency query string false "Currency code" default(USD) +// @Param mode query string false "Game mode" Enums(real, demo) default(real) +// @Success 200 {object} map[string]string "Returns launch URL" +// @Failure 400 {object} map[string]string "Invalid request" +// @Failure 500 {object} map[string]string "Internal server error" +// @Router /api/veli/launch/{game_id} [get] +func (h *Handler) LaunchVeliGame(c *fiber.Ctx) error { + userID := c.Locals("userID").(int64) + gameID := c.Params("game_id") + currency := c.Query("currency", "USD") + mode := c.Query("mode", "real") + + launchURL, err := h.veliVirtualGameSvc.GenerateGameLaunchURL(c.Context(), userID, gameID, currency, mode) + if err != nil { + h.logger.Error("failed to generate Veli launch URL", + "error", err, + "userID", userID, + "gameID", gameID) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "failed to launch game", + }) + } + + return c.JSON(fiber.Map{ + "launch_url": launchURL, + }) +} + +// HandleVeliCallback godoc +// @Summary Veli Games webhook handler +// @Description Processes game round settlements from Veli +// @Tags Virtual Games +// @Accept json +// @Produce json +// @Param payload body domain.VeliCallback true "Callback payload" +// @Success 200 {object} map[string]string "Callback processed" +// @Failure 400 {object} map[string]string "Invalid payload" +// @Failure 403 {object} map[string]string "Invalid signature" +// @Failure 500 {object} map[string]string "Processing error" +// @Router /webhooks/veli [post] +func (h *Handler) HandleVeliCallback(c *fiber.Ctx) error { + var cb domain.VeliCallback + if err := c.BodyParser(&cb); err != nil { + h.logger.Error("invalid Veli callback format", "error", err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "invalid payload format", + }) + } + + if err := h.veliVirtualGameSvc.HandleCallback(c.Context(), &cb); err != nil { + h.logger.Error("failed to process Veli callback", + "roundID", cb.RoundID, + "error", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "failed to process callback", + }) + } + + return c.JSON(fiber.Map{ + "status": "processed", + }) +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 7a18943..3fb78b5 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -22,6 +22,7 @@ func (a *App) initAppRoutes() { a.referralSvc, a.virtualGameSvc, a.aleaVirtualGameService, + a.veliVirtualGameService, a.userSvc, a.transactionSvc, a.ticketSvc, @@ -166,17 +167,18 @@ func (a *App) initAppRoutes() { a.fiber.Post("/transfer/refill/:id", a.authMiddleware, h.RefillWallet) //Chapa Routes - a.fiber.Post("/api/v1/chapa/payments/initialize", h.InitializePayment) - a.fiber.Get("/api/v1/chapa/payments/verify/:tx_ref", h.VerifyTransaction) - a.fiber.Post("/api/v1/chapa/payments/callback", h.ReceiveWebhook) - a.fiber.Get("/api/v1/chapa/banks", h.GetBanks) - a.fiber.Post("/api/v1/chapa/transfers", h.CreateTransfer) - a.fiber.Get("/api/v1/chapa/transfers/verify/:transfer_ref", h.VerifyTransfer) + group := a.fiber.Group("/api/v1") + + group.Post("/chapa/payments/initialize", h.InitializePayment) + group.Get("/chapa/payments/verify/:tx_ref", h.VerifyTransaction) + group.Post("/chapa/payments/callback", h.ReceiveWebhook) + group.Get("/chapa/banks", h.GetBanks) + group.Post("/chapa/transfers", h.CreateTransfer) + group.Get("/chapa/transfers/verify/:transfer_ref", h.VerifyTransfer) //Alea Play Virtual Game Routes - a.fiber.Get("/api/v1/alea-games/launch", a.authMiddleware, h.LaunchAleaGame) - a.fiber.Post("/api/v1/webhooks/alea", a.authMiddleware, h.HandleAleaCallback) - // a.fiber.Post("/webhooks/alea", middleware.AleaWebhookMiddleware(a.cfg.AleaPlay.SecretKey), h.HandleAleaCallback) + group.Get("/alea-games/launch", a.authMiddleware, h.LaunchAleaGame) + group.Post("/webhooks/alea-games", a.authMiddleware, h.HandleAleaCallback) // Transactions /transactions a.fiber.Post("/transaction", a.authMiddleware, h.CreateTransaction) From 69218ee8939f64802560f34ff40ed452d9191320 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Sat, 24 May 2025 19:51:21 +0300 Subject: [PATCH 6/7] veli routes --- internal/web_server/routes.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 3fb78b5..3172a06 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -177,8 +177,12 @@ func (a *App) initAppRoutes() { group.Get("/chapa/transfers/verify/:transfer_ref", h.VerifyTransfer) //Alea Play Virtual Game Routes - group.Get("/alea-games/launch", a.authMiddleware, h.LaunchAleaGame) - group.Post("/webhooks/alea-games", a.authMiddleware, h.HandleAleaCallback) + group.Get("/alea-play/launch", a.authMiddleware, h.LaunchAleaGame) + group.Post("/webhooks/alea-play", a.authMiddleware, h.HandleAleaCallback) + + //Veli Virtual Game Routes + group.Get("/veli-games/launch", a.authMiddleware, h.LaunchVeliGame) + group.Post("/webhooks/veli-games", a.authMiddleware, h.HandleVeliCallback) // Transactions /transactions a.fiber.Post("/transaction", a.authMiddleware, h.CreateTransaction) From 360bdd471fd8bce42581abe50a35d6bad6ea9751 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Sun, 25 May 2025 14:15:34 +0300 Subject: [PATCH 7/7] notification+wallet fixes --- internal/web_server/handlers/notification_handler.go | 2 +- internal/web_server/handlers/wallet_handler.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/web_server/handlers/notification_handler.go b/internal/web_server/handlers/notification_handler.go index 1e28543..3f6f6b8 100644 --- a/internal/web_server/handlers/notification_handler.go +++ b/internal/web_server/handlers/notification_handler.go @@ -268,7 +268,7 @@ func (h *Handler) GetNotifications(c *fiber.Ctx) error { } -func (h *Handler) getAllRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) { +func (h *Handler) GetAllRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) { return h.notificationSvc.ListRecipientIDs(ctx, receiver) } diff --git a/internal/web_server/handlers/wallet_handler.go b/internal/web_server/handlers/wallet_handler.go index f19fd9f..a7aa599 100644 --- a/internal/web_server/handlers/wallet_handler.go +++ b/internal/web_server/handlers/wallet_handler.go @@ -50,7 +50,7 @@ type CustomerWalletRes struct { CreatedAt time.Time `json:"created_at"` } -func convertCustomerWallet(wallet domain.GetCustomerWallet) CustomerWalletRes { +func ConvertCustomerWallet(wallet domain.GetCustomerWallet) CustomerWalletRes { return CustomerWalletRes{ ID: wallet.ID, RegularID: wallet.RegularID,