chapa integration 1st phase

This commit is contained in:
Yared Yemane 2025-05-22 18:35:20 +03:00
parent 252bf04b1e
commit 66a7affeba
14 changed files with 377 additions and 14 deletions

View File

@ -7,12 +7,15 @@ import (
"os" "os"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
// "github.com/gofiber/fiber/v2"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/config"
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger" customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
mockemail "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_email" mockemail "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_email"
mocksms "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_sms" mocksms "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_sms"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "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/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
@ -27,6 +30,8 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/utils"
httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server" httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server"
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
@ -46,6 +51,16 @@ import (
// @name Authorization // @name Authorization
// @BasePath / // @BasePath /
func main() { 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() cfg, err := config.NewConfig()
if err != nil { if err != nil {
slog.Error(" Config error:", "err", err) slog.Error(" Config error:", "err", err)
@ -92,7 +107,7 @@ func main() {
JwtAccessKey: cfg.JwtKey, JwtAccessKey: cfg.JwtKey,
JwtAccessExpiry: cfg.AccessExpiry, JwtAccessExpiry: cfg.AccessExpiry,
}, userSvc, }, 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) logger.Info("Starting server", "port", cfg.Port)
if err := app.Run(); err != nil { if err := app.Run(); err != nil {

9
cmd/router.go Normal file
View File

@ -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!")
})
}

3
go.mod
View File

@ -21,6 +21,7 @@ require (
require ( require (
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/andybalholm/brotli v1.1.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/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/fasthttp/websocket v1.5.8 // 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-openapi/swag v0.23.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.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/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect

6
go.sum
View File

@ -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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:k5DpirKkftIF/w1R8ZzjSgARJrs54Je9YJK37DL/Ah8=
github.com/fasthttp/websocket v1.5.8/go.mod h1:d08g8WaT6nnyvg9uMm8K9zMYyDjfKyj3170AtPRuVU0= github.com/fasthttp/websocket v1.5.8/go.mod h1:d08g8WaT6nnyvg9uMm8K9zMYyDjfKyj3170AtPRuVU0=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= 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/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 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 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.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 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= 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 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 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/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 h1:KanIMPX0QdEdB4R3CiimCAbxFrhB3j7h0/OvpYGVQa8=
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= 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= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=

View File

@ -21,7 +21,7 @@ var (
ErrInvalidLevel = errors.New("invalid log level") ErrInvalidLevel = errors.New("invalid log level")
ErrInvalidEnv = errors.New("env not set or invalid") ErrInvalidEnv = errors.New("env not set or invalid")
ErrInvalidSMSAPIKey = errors.New("SMS API key is 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") ErrInvalidPopOKClientID = errors.New("PopOK client ID is invalid")
ErrInvalidPopOKSecretKey = errors.New("PopOK secret key is invalid") ErrInvalidPopOKSecretKey = errors.New("PopOK secret key is invalid")
ErrInvalidPopOKBaseURL = errors.New("PopOK base URL is invalid") ErrInvalidPopOKBaseURL = errors.New("PopOK base URL is invalid")
@ -40,7 +40,13 @@ type Config struct {
AFRO_SMS_SENDER_NAME string AFRO_SMS_SENDER_NAME string
AFRO_SMS_RECEIVER_PHONE_NUMBER string AFRO_SMS_RECEIVER_PHONE_NUMBER string
ADRO_SMS_HOST_URL 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 PopOK domain.PopOKConfig
} }
@ -115,6 +121,17 @@ func (c *Config) loadEnv() error {
if !ok { if !ok {
return ErrInvalidLevel 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.LogLevel = lvl
c.AFRO_SMS_API_KEY = os.Getenv("AFRO_SMS_API_KEY") c.AFRO_SMS_API_KEY = os.Getenv("AFRO_SMS_API_KEY")

View File

@ -0,0 +1,2 @@
package models

View File

@ -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,
// )
// }

View File

@ -1 +0,0 @@
package wallet

View File

@ -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"
// }
// }

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
@ -47,6 +48,7 @@ type App struct {
prematchSvc *odds.ServiceImpl prematchSvc *odds.ServiceImpl
eventSvc event.Service eventSvc event.Service
resultSvc *result.Service resultSvc *result.Service
cfg *config.Config
} }
func NewApp( func NewApp(
@ -67,6 +69,7 @@ func NewApp(
referralSvc referralservice.ReferralStore, referralSvc referralservice.ReferralStore,
virtualGameSvc virtualgameservice.VirtualGameService, virtualGameSvc virtualgameservice.VirtualGameService,
resultSvc *result.Service, resultSvc *result.Service,
cfg *config.Config,
) *App { ) *App {
app := fiber.New(fiber.Config{ app := fiber.New(fiber.Config{
CaseSensitive: true, CaseSensitive: true,
@ -103,6 +106,7 @@ func NewApp(
eventSvc: eventSvc, eventSvc: eventSvc,
virtualGameSvc: virtualGameSvc, virtualGameSvc: virtualGameSvc,
resultSvc: resultSvc, resultSvc: resultSvc,
cfg: cfg,
} }
s.initAppRoutes() s.initAppRoutes()

View File

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

View File

@ -3,6 +3,7 @@ package handlers
import ( import (
"log/slog" "log/slog"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
@ -37,6 +38,7 @@ type Handler struct {
authSvc *authentication.Service authSvc *authentication.Service
jwtConfig jwtutil.JwtConfig jwtConfig jwtutil.JwtConfig
validator *customvalidator.CustomValidator validator *customvalidator.CustomValidator
Cfg *config.Config
} }
func New( func New(
@ -56,6 +58,7 @@ func New(
companySvc *company.Service, companySvc *company.Service,
prematchSvc *odds.ServiceImpl, prematchSvc *odds.ServiceImpl,
eventSvc event.Service, eventSvc event.Service,
cfg *config.Config,
) *Handler { ) *Handler {
return &Handler{ return &Handler{
logger: logger, logger: logger,
@ -74,5 +77,6 @@ func New(
virtualGameSvc: virtualGameSvc, virtualGameSvc: virtualGameSvc,
authSvc: authSvc, authSvc: authSvc,
jwtConfig: jwtConfig, jwtConfig: jwtConfig,
Cfg: cfg,
} }
} }

View File

@ -5,7 +5,10 @@ import (
"strconv" "strconv"
_ "github.com/SamuelTariku/FortuneBet-Backend/docs" _ "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/domain"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/handlers" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/handlers"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@ -30,6 +33,7 @@ func (a *App) initAppRoutes() {
a.companySvc, a.companySvc,
a.prematchSvc, a.prematchSvc,
a.eventSvc, a.eventSvc,
a.cfg,
) )
a.fiber.Get("/", func(c *fiber.Ctx) error { 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.Get("/transfer/wallet/:id", a.authMiddleware, h.GetTransfersByWallet)
a.fiber.Post("/transfer/refill/:id", a.authMiddleware, h.RefillWallet) 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 // Transactions /transactions
a.fiber.Post("/transaction", a.authMiddleware, h.CreateTransaction) a.fiber.Post("/transaction", a.authMiddleware, h.CreateTransaction)
a.fiber.Get("/transaction", a.authMiddleware, h.GetAllTransactions) a.fiber.Get("/transaction", a.authMiddleware, h.GetAllTransactions)

View File

@ -9,7 +9,7 @@ coverage:
@go tool cover -func=coverage.out -o coverage/coverage.txt @go tool cover -func=coverage.out -o coverage/coverage.txt
.PHONY: build .PHONY: build
build: build:
@go build -ldflags="-s" -o ./bin/web ./ @go build -ldflags="-s" -o ./bin/web ./cmd/main.go
.PHONY: run .PHONY: run
run: run:
@echo "Running Go application" @echo "Running Go application"
@ -18,7 +18,7 @@ run:
air: air:
@echo "Running air" @echo "Running air"
@air -c .air.toml @air -c .air.toml
.PHONY: migrations/up .PHONY: migrations/new
migrations/new: migrations/new:
@echo 'Creating migration files for DB_URL' @echo 'Creating migration files for DB_URL'
@migrate create -seq -ext=.sql -dir=./db/migrations $(name) @migrate create -seq -ext=.sql -dir=./db/migrations $(name)
@ -38,4 +38,4 @@ db-down:
docker compose -f compose.db.yaml down docker compose -f compose.db.yaml down
.PHONY: sqlc-gen .PHONY: sqlc-gen
sqlc-gen: sqlc-gen:
@sqlc generate @sqlc generate