chapa integration fix
This commit is contained in:
parent
95eaed18ad
commit
aef5c4410d
1
go.mod
1
go.mod
|
|
@ -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
2
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
53
internal/domain/responses.go
Normal file
53
internal/domain/responses.go
Normal 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(),
|
||||
})
|
||||
}
|
||||
11
internal/services/chapa/port.go
Normal file
11
internal/services/chapa/port.go
Normal 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
|
||||
}
|
||||
128
internal/services/chapa/service.go
Normal file
128
internal/services/chapa/service.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user