chapa integration fix

This commit is contained in:
Yared Yemane 2025-05-30 21:54:22 +03:00
parent 95eaed18ad
commit aef5c4410d
11 changed files with 313 additions and 3 deletions

1
go.mod
View File

@ -50,6 +50,7 @@ require (
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/shopspring/decimal v1.4.0
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect

2
go.sum
View File

@ -112,6 +112,8 @@ 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/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=

View File

@ -105,3 +105,47 @@ type VerifyTransferResponse struct {
Message string `json:"message"`
Data TransferVerificationData `json:"data"`
}
type ChapaTransactionType struct {
Type string `json:"type"`
}
type ChapaWebHookTransfer struct {
AccountName string `json:"account_name"`
AccountNumber string `json:"account_number"`
BankId string `json:"bank_id"`
BankName string `json:"bank_name"`
Currency string `json:"currency"`
Amount string `json:"amount"`
Type string `json:"type"`
Status string `json:"status"`
Reference string `json:"reference"`
TxRef string `json:"tx_ref"`
ChapaReference string `json:"chapa_reference"`
CreatedAt time.Time `json:"created_at"`
}
type ChapaWebHookPayment struct {
Event string `json:"event"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
Mobile interface{} `json:"mobile"`
Currency string `json:"currency"`
Amount string `json:"amount"`
Charge string `json:"charge"`
Status string `json:"status"`
Mode string `json:"mode"`
Reference string `json:"reference"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Type string `json:"type"`
TxRef string `json:"tx_ref"`
PaymentMethod string `json:"payment_method"`
Customization struct {
Title interface{} `json:"title"`
Description interface{} `json:"description"`
Logo interface{} `json:"logo"`
} `json:"customization"`
Meta string `json:"meta"`
}

View File

@ -47,3 +47,12 @@ func (m Currency) String() string {
x = x / 100
return fmt.Sprintf("$%.2f", x)
}
type Response struct {
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
Success bool `json:"success"`
StatusCode int `json:"status_code"`
}

View File

@ -0,0 +1,53 @@
package domain
import (
"errors"
"github.com/gofiber/fiber/v2"
)
func UnProcessableEntityResponse(c *fiber.Ctx) error {
return c.Status(fiber.StatusUnprocessableEntity).JSON(Response{
Message: "failed to parse request body",
StatusCode: fiber.StatusUnprocessableEntity,
Success: false,
})
}
func FiberErrorResponse(c *fiber.Ctx, err error) error {
var statusCode int
var message string
switch {
case errors.Is(err, fiber.ErrNotFound):
statusCode = fiber.StatusNotFound
message = "resource not found"
case errors.Is(err, fiber.ErrBadRequest):
statusCode = fiber.StatusBadRequest
message = "bad request"
case errors.Is(err, fiber.ErrUnauthorized):
statusCode = fiber.StatusUnauthorized
message = "unauthorized"
case errors.Is(err, fiber.ErrForbidden):
statusCode = fiber.StatusForbidden
message = "forbidden"
case errors.Is(err, fiber.ErrConflict):
statusCode = fiber.StatusConflict
message = "conflict occurred"
default:
statusCode = fiber.StatusInternalServerError
message = "unexpected server error"
}
return c.Status(statusCode).JSON(fiber.Map{
"success": false,
"status_code": statusCode,
"error": message,
"details": err.Error(),
})
}

View File

@ -0,0 +1,11 @@
package chapa
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type ChapaPort interface {
HandleChapaTransferWebhook(ctx context.Context, req domain.ChapaWebHookTransfer) error
}

View File

@ -0,0 +1,128 @@
package chapa
import (
"context"
"fmt"
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/shopspring/decimal"
)
type Service struct {
transactionStore transaction.TransactionStore
walletStore wallet.WalletStore
store *repository.Store
}
func (s *Service) HandleChapaTransferWebhook(ctx context.Context, req domain.ChapaWebHookTransfer) error {
tx, err := s.store.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
// 1. Fetch transaction
referenceID, err := strconv.ParseInt(req.Reference, 10, 64)
txn, err := s.transactionStore.GetTransactionByID(ctx, referenceID)
if err != nil {
return err
}
if txn.Verified {
return nil // already processed
}
// 2. Compare amount
webhookAmount, _ := decimal.NewFromString(req.Amount)
storedAmount, _ := decimal.NewFromString(txn.Amount.String())
if !webhookAmount.Equal(storedAmount) {
return fmt.Errorf("amount mismatch")
}
// 3. Update transaction
txn.Verified = true
if err := s.transactionStore.UpdateTransactionVerified(ctx, txn.ID, txn.Verified, txn.ApprovedBy.Value, txn.ApproverName.Value); err != nil {
return err
}
return tx.Commit(ctx)
}
func (s *Service) HandleChapaPaymentWebhook(ctx context.Context, req domain.ChapaWebHookPayment) error {
tx, err := s.store.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if req.Status != "success" {
return fmt.Errorf("payment status not successful")
}
// 1. Parse reference ID
referenceID, err := strconv.ParseInt(req.TxRef, 10, 64)
if err != nil {
return fmt.Errorf("invalid tx_ref: %w", err)
}
// 2. Fetch transaction
txn, err := s.transactionStore.GetTransactionByID(ctx, referenceID)
if err != nil {
return err
}
if txn.Verified {
return nil // already processed
}
// 3. Amount validation
webhookAmount, _ := decimal.NewFromString(req.Amount)
storedAmount := txn.Amount // assuming it's domain.Currency (decimal.Decimal alias)
if webhookAmount.LessThan(storedAmount) {
return fmt.Errorf("webhook amount is less than expected")
}
// 4. Fetch wallet
wallet, err := s.walletStore.GetWalletByID(ctx, txn.ID)
if err != nil {
return err
}
// 5. Update wallet balance
newBalance := wallet.Balance.Add(storedAmount)
if err := s.walletStore.UpdateBalance(ctx, wallet.ID, newBalance); err != nil {
return err
}
// 6. Mark transaction as verified
if err := s.transactionStore.UpdateTransactionVerified(ctx, txn.ID, true, txn.ApprovedBy.Value, txn.ApproverName.Value); err != nil {
return err
}
// 7. Check & generate referral code
hasCode, err := s.userStore.HasReferralCode(ctx, wallet.UserID)
if err != nil {
return err
}
if !hasCode {
code := misc.GenerateReferralCode(req.FirstName)
if err := s.userStore.SetReferralCode(ctx, wallet.UserID, code); err != nil {
return err
}
if err := s.referralStore.CreateReferralCode(ctx, domain.ReferralCode{
Code: code,
Amount: config.ReferralRewardBase,
}); err != nil {
return err
}
}
return tx.Commit(ctx)
}

View File

@ -730,7 +730,7 @@ func (s *Service) evaluateIceHockeyOutcome(outcome domain.BetOutcome, res domain
return domain.OUTCOME_STATUS_PENDING, nil
}
func (s *Service) evaluateNFLOutcome(outcome domain.BetOutcome, finalScore struct{ Home, Away int }) (domain.OutcomeStatus, error) {
func (s *Service) EvaluateNFLOutcome(outcome domain.BetOutcome, finalScore struct{ Home, Away int }) (domain.OutcomeStatus, error) {
if !domain.SupportedMarkets[outcome.MarketID] {
s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName)
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName)

View File

@ -234,7 +234,7 @@ func evaluateBaseballTotalRuns(outcome domain.BetOutcome, score struct{ Home, Aw
}
// evaluateBaseballFirstInning evaluates Baseball first inning bets
func evaluateBaseballFirstInning(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
func EvaluateBaseballFirstInning(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddHeader {
case "1":
if score.Home > score.Away {
@ -257,7 +257,7 @@ func evaluateBaseballFirstInning(outcome domain.BetOutcome, score struct{ Home,
}
// evaluateBaseballFirst5Innings evaluates Baseball first 5 innings bets
func evaluateBaseballFirst5Innings(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
func EvaluateBaseballFirst5Innings(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddHeader {
case "1":
if score.Home > score.Away {

View File

@ -7,6 +7,8 @@ import (
"io"
"net/http"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
@ -281,3 +283,59 @@ func (h *Handler) VerifyTransfer(c *fiber.Ctx) error {
return c.Status(resp.StatusCode).Type("json").Send(body)
}
// VerifyChapaPayment godoc
// @Summary Verifies Chapa webhook transaction
// @Tags Chapa
// @Accept json
// @Produce json
// @Param payload body domain.ChapaTransactionType true "Webhook Payload"
// @Success 200 {object} domain.Response
// @Router /api/v1/chapa/payments/verify [post]
func (h *Handler) VerifyChapaPayment(c *fiber.Ctx) error {
var txType domain.ChapaTransactionType
if err := c.BodyParser(&txType); err != nil {
return domain.UnProcessableEntityResponse(c)
}
switch txType.Type {
case config.ChapaTransferType:
var payload domain.ChapaWebHookTransfer
if err := c.BodyParser(&payload); err != nil {
return domain.UnProcessableEntityResponse(c)
}
if err := h.chapaSvc.HandleChapaTransferWebhook(c.Context(), payload); err != nil {
return domain.FiberErrorResponse(c, err)
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa transfer verified successfully",
Success: true,
StatusCode: fiber.StatusOK,
})
case config.ChapaPaymentType:
var payload domain.ChapaWebHookPayment
if err := c.BodyParser(&payload); err != nil {
return domain.UnProcessableEntityResponse(c)
}
if err := h.chapaSvc.HandleChapaPaymentWebhook(c.Context(), payload); err != nil {
return domain.FiberErrorResponse(c, err)
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa payment verified successfully",
Success: true,
StatusCode: fiber.StatusOK,
})
default:
return c.Status(fiber.StatusBadRequest).JSON(domain.Response{
Message: "Invalid Chapa transaction type",
Success: false,
StatusCode: fiber.StatusBadRequest,
})
}
}

View File

@ -7,6 +7,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
@ -29,6 +30,7 @@ type Handler struct {
notificationSvc *notificationservice.Service
userSvc *user.Service
referralSvc referralservice.ReferralStore
chapaSvc chapa.ChapaPort
walletSvc *wallet.Service
transactionSvc *transaction.Service
ticketSvc *ticket.Service
@ -51,6 +53,7 @@ func New(
logger *slog.Logger,
notificationSvc *notificationservice.Service,
validator *customvalidator.CustomValidator,
chapaSvc chapa.ChapaPort,
walletSvc *wallet.Service,
referralSvc referralservice.ReferralStore,
virtualGameSvc virtualgameservice.VirtualGameService,
@ -72,6 +75,7 @@ func New(
return &Handler{
logger: logger,
notificationSvc: notificationSvc,
chapaSvc: chapaSvc,
walletSvc: walletSvc,
referralSvc: referralSvc,
validator: validator,