createTransaction+createTransfer fix

This commit is contained in:
Yared Yemane 2025-05-31 21:29:39 +03:00
parent eb8abfc963
commit 75d469be8c
12 changed files with 1164 additions and 30 deletions

View File

@ -109,11 +109,15 @@ func main() {
logger,
)
recommendationSvc := recommendation.NewService(recommendationRepo)
chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY)
chapaSvc := chapa.NewService(
transaction.TransactionStore(store),
wallet.WalletStore(store),
user.UserStore(store),
referalSvc,
branch.BranchStore(store),
chapaClient,
store,
)

View File

@ -361,6 +361,63 @@ const docTemplate = `{
}
}
},
"/api/v1/chapa/payments/deposit": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Deposits money into user wallet from user account using Chapa",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Deposit money into user wallet using Chapa",
"parameters": [
{
"description": "Deposit request payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ChapaDepositRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.ChapaPaymentUrlResponseWrapper"
}
},
"400": {
"description": "Invalid request",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"422": {
"description": "Validation error",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/domain.Response"
}
}
}
}
},
"/api/v1/chapa/payments/initialize": {
"post": {
"description": "Initiate a payment through Chapa",
@ -395,6 +452,39 @@ const docTemplate = `{
}
}
},
"/api/v1/chapa/payments/verify": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Verifies Chapa webhook transaction",
"parameters": [
{
"description": "Webhook Payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ChapaTransactionType"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.Response"
}
}
}
}
},
"/api/v1/chapa/payments/verify/{tx_ref}": {
"get": {
"description": "Verify the transaction status from Chapa using tx_ref",
@ -427,6 +517,76 @@ const docTemplate = `{
}
}
},
"/api/v1/chapa/payments/withdraw": {
"post": {
"description": "Initiates a withdrawal transaction using Chapa for the authenticated user.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Withdraw using Chapa",
"parameters": [
{
"description": "Chapa Withdraw Request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ChapaWithdrawRequest"
}
}
],
"responses": {
"200": {
"description": "Withdrawal requested successfully",
"schema": {
"allOf": [
{
"$ref": "#/definitions/domain.Response"
},
{
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
]
}
},
"400": {
"description": "Invalid request",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"422": {
"description": "Unprocessable Entity",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.Response"
}
}
}
}
},
"/api/v1/chapa/transfers": {
"post": {
"description": "Initiate a transfer request via Chapa",
@ -4413,6 +4573,46 @@ const docTemplate = `{
}
}
},
"domain.ChapaDepositRequest": {
"type": "object",
"properties": {
"amount": {
"type": "integer"
},
"branch_id": {
"type": "integer"
},
"currency": {
"type": "string"
},
"phone_number": {
"type": "string"
}
}
},
"domain.ChapaPaymentUrlResponse": {
"type": "object",
"properties": {
"payment_url": {
"type": "string"
}
}
},
"domain.ChapaPaymentUrlResponseWrapper": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"status_code": {
"type": "integer"
},
"success": {
"type": "boolean"
}
}
},
"domain.ChapaSupportedBank": {
"type": "object",
"properties": {
@ -4480,6 +4680,44 @@ const docTemplate = `{
}
}
},
"domain.ChapaTransactionType": {
"type": "object",
"properties": {
"type": {
"type": "string"
}
}
},
"domain.ChapaWithdrawRequest": {
"type": "object",
"properties": {
"account_name": {
"type": "string"
},
"account_number": {
"type": "string"
},
"amount": {
"type": "integer"
},
"bank_code": {
"type": "string"
},
"beneficiary_name": {
"type": "string"
},
"branch_id": {
"type": "integer"
},
"currency": {
"type": "string"
},
"wallet_id": {
"description": "add this",
"type": "integer"
}
}
},
"domain.CreateBetOutcomeReq": {
"type": "object",
"properties": {
@ -4561,7 +4799,7 @@ const docTemplate = `{
"type": "object",
"properties": {
"amount": {
"type": "string"
"type": "integer"
},
"callback_url": {
"type": "string"
@ -4832,6 +5070,21 @@ const docTemplate = `{
}
}
},
"domain.Response": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"status_code": {
"type": "integer"
},
"success": {
"type": "boolean"
}
}
},
"domain.Role": {
"type": "string",
"enum": [
@ -5041,6 +5294,10 @@ const docTemplate = `{
"description": "Match or event name",
"type": "string"
},
"source": {
"description": "bet api provider (bet365, betfair)",
"type": "string"
},
"sportID": {
"description": "Sport ID",
"type": "string"

View File

@ -353,6 +353,63 @@
}
}
},
"/api/v1/chapa/payments/deposit": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Deposits money into user wallet from user account using Chapa",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Deposit money into user wallet using Chapa",
"parameters": [
{
"description": "Deposit request payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ChapaDepositRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.ChapaPaymentUrlResponseWrapper"
}
},
"400": {
"description": "Invalid request",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"422": {
"description": "Validation error",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/domain.Response"
}
}
}
}
},
"/api/v1/chapa/payments/initialize": {
"post": {
"description": "Initiate a payment through Chapa",
@ -387,6 +444,39 @@
}
}
},
"/api/v1/chapa/payments/verify": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Verifies Chapa webhook transaction",
"parameters": [
{
"description": "Webhook Payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ChapaTransactionType"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.Response"
}
}
}
}
},
"/api/v1/chapa/payments/verify/{tx_ref}": {
"get": {
"description": "Verify the transaction status from Chapa using tx_ref",
@ -419,6 +509,76 @@
}
}
},
"/api/v1/chapa/payments/withdraw": {
"post": {
"description": "Initiates a withdrawal transaction using Chapa for the authenticated user.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Withdraw using Chapa",
"parameters": [
{
"description": "Chapa Withdraw Request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ChapaWithdrawRequest"
}
}
],
"responses": {
"200": {
"description": "Withdrawal requested successfully",
"schema": {
"allOf": [
{
"$ref": "#/definitions/domain.Response"
},
{
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
]
}
},
"400": {
"description": "Invalid request",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"422": {
"description": "Unprocessable Entity",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.Response"
}
}
}
}
},
"/api/v1/chapa/transfers": {
"post": {
"description": "Initiate a transfer request via Chapa",
@ -4405,6 +4565,46 @@
}
}
},
"domain.ChapaDepositRequest": {
"type": "object",
"properties": {
"amount": {
"type": "integer"
},
"branch_id": {
"type": "integer"
},
"currency": {
"type": "string"
},
"phone_number": {
"type": "string"
}
}
},
"domain.ChapaPaymentUrlResponse": {
"type": "object",
"properties": {
"payment_url": {
"type": "string"
}
}
},
"domain.ChapaPaymentUrlResponseWrapper": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"status_code": {
"type": "integer"
},
"success": {
"type": "boolean"
}
}
},
"domain.ChapaSupportedBank": {
"type": "object",
"properties": {
@ -4472,6 +4672,44 @@
}
}
},
"domain.ChapaTransactionType": {
"type": "object",
"properties": {
"type": {
"type": "string"
}
}
},
"domain.ChapaWithdrawRequest": {
"type": "object",
"properties": {
"account_name": {
"type": "string"
},
"account_number": {
"type": "string"
},
"amount": {
"type": "integer"
},
"bank_code": {
"type": "string"
},
"beneficiary_name": {
"type": "string"
},
"branch_id": {
"type": "integer"
},
"currency": {
"type": "string"
},
"wallet_id": {
"description": "add this",
"type": "integer"
}
}
},
"domain.CreateBetOutcomeReq": {
"type": "object",
"properties": {
@ -4553,7 +4791,7 @@
"type": "object",
"properties": {
"amount": {
"type": "string"
"type": "integer"
},
"callback_url": {
"type": "string"
@ -4824,6 +5062,21 @@
}
}
},
"domain.Response": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"status_code": {
"type": "integer"
},
"success": {
"type": "boolean"
}
}
},
"domain.Role": {
"type": "string",
"enum": [
@ -5033,6 +5286,10 @@
"description": "Match or event name",
"type": "string"
},
"source": {
"description": "bet api provider (bet365, betfair)",
"type": "string"
},
"sportID": {
"description": "Sport ID",
"type": "string"

View File

@ -124,6 +124,32 @@ definitions:
example: 2
type: integer
type: object
domain.ChapaDepositRequest:
properties:
amount:
type: integer
branch_id:
type: integer
currency:
type: string
phone_number:
type: string
type: object
domain.ChapaPaymentUrlResponse:
properties:
payment_url:
type: string
type: object
domain.ChapaPaymentUrlResponseWrapper:
properties:
data: {}
message:
type: string
status_code:
type: integer
success:
type: boolean
type: object
domain.ChapaSupportedBank:
properties:
acct_length:
@ -168,6 +194,31 @@ definitions:
message:
type: string
type: object
domain.ChapaTransactionType:
properties:
type:
type: string
type: object
domain.ChapaWithdrawRequest:
properties:
account_name:
type: string
account_number:
type: string
amount:
type: integer
bank_code:
type: string
beneficiary_name:
type: string
branch_id:
type: integer
currency:
type: string
wallet_id:
description: add this
type: integer
type: object
domain.CreateBetOutcomeReq:
properties:
event_id:
@ -222,7 +273,7 @@ definitions:
domain.InitPaymentRequest:
properties:
amount:
type: string
type: integer
callback_url:
type: string
currency:
@ -408,6 +459,16 @@ definitions:
totalRewardEarned:
type: number
type: object
domain.Response:
properties:
data: {}
message:
type: string
status_code:
type: integer
success:
type: boolean
type: object
domain.Role:
enum:
- super_admin
@ -555,6 +616,9 @@ definitions:
matchName:
description: Match or event name
type: string
source:
description: bet api provider (bet365, betfair)
type: string
sportID:
description: Sport ID
type: string
@ -1721,6 +1785,42 @@ paths:
summary: Receive Chapa webhook
tags:
- Chapa
/api/v1/chapa/payments/deposit:
post:
consumes:
- application/json
description: Deposits money into user wallet from user account using Chapa
parameters:
- description: Deposit request payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/domain.ChapaDepositRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.ChapaPaymentUrlResponseWrapper'
"400":
description: Invalid request
schema:
$ref: '#/definitions/domain.Response'
"422":
description: Validation error
schema:
$ref: '#/definitions/domain.Response'
"500":
description: Internal server error
schema:
$ref: '#/definitions/domain.Response'
security:
- ApiKeyAuth: []
summary: Deposit money into user wallet using Chapa
tags:
- Chapa
/api/v1/chapa/payments/initialize:
post:
consumes:
@ -1743,6 +1843,27 @@ paths:
summary: Initialize a payment transaction
tags:
- Chapa
/api/v1/chapa/payments/verify:
post:
consumes:
- application/json
parameters:
- description: Webhook Payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/domain.ChapaTransactionType'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
summary: Verifies Chapa webhook transaction
tags:
- Chapa
/api/v1/chapa/payments/verify/{tx_ref}:
get:
consumes:
@ -1764,6 +1885,50 @@ paths:
summary: Verify a payment transaction
tags:
- Chapa
/api/v1/chapa/payments/withdraw:
post:
consumes:
- application/json
description: Initiates a withdrawal transaction using Chapa for the authenticated
user.
parameters:
- description: Chapa Withdraw Request
in: body
name: request
required: true
schema:
$ref: '#/definitions/domain.ChapaWithdrawRequest'
produces:
- application/json
responses:
"200":
description: Withdrawal requested successfully
schema:
allOf:
- $ref: '#/definitions/domain.Response'
- properties:
data:
type: string
type: object
"400":
description: Invalid request
schema:
$ref: '#/definitions/domain.Response'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/domain.Response'
"422":
description: Unprocessable Entity
schema:
$ref: '#/definitions/domain.Response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.Response'
summary: Withdraw using Chapa
tags:
- Chapa
/api/v1/chapa/transfers:
post:
consumes:

View File

@ -1,6 +1,9 @@
package domain
import "time"
import (
"errors"
"time"
)
var (
ChapaSecret string
@ -8,7 +11,7 @@ var (
)
type InitPaymentRequest struct {
Amount string `json:"amount"`
Amount Currency `json:"amount"`
Currency string `json:"currency"`
Email string `json:"email"`
FirstName string `json:"first_name"`
@ -149,3 +152,72 @@ type ChapaWebHookPayment struct {
} `json:"customization"`
Meta string `json:"meta"`
}
type ChapaWithdrawRequest struct {
WalletID int64 `json:"wallet_id"` // add this
AccountName string `json:"account_name"`
AccountNumber string `json:"account_number"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
BeneficiaryName string `json:"beneficiary_name"`
BankCode string `json:"bank_code"`
BranchID int64 `json:"branch_id"`
}
type ChapaTransferPayload struct {
AccountName string
AccountNumber string
Amount string
Currency string
BeneficiaryName string
TxRef string
Reference string
BankCode string
}
type ChapaDepositRequest struct {
Amount Currency `json:"amount"`
PhoneNumber string `json:"phone_number"`
Currency string `json:"currency"`
BranchID int64 `json:"branch_id"`
}
func (r ChapaDepositRequest) Validate() error {
if r.Amount <= 0 {
return errors.New("amount must be greater than zero")
}
if r.Currency == "" {
return errors.New("currency is required")
}
if r.PhoneNumber == "" {
return errors.New("phone number is required")
}
if r.BranchID == 0 {
return errors.New("branch ID is required")
}
return nil
}
type AcceptChapaPaymentRequest struct {
Amount string `json:"amount"`
Currency string `json:"currency"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
PhoneNumber string `json:"phone_number"`
TxRef string `json:"tx_ref"`
CallbackUrl string `json:"callback_url"`
ReturnUrl string `json:"return_url"`
CustomizationTitle string `json:"customization[title]"`
CustomizationDescription string `json:"customization[description]"`
}
type ChapaPaymentUrlResponse struct {
PaymentURL string `json:"payment_url"`
}
type ChapaPaymentUrlResponseWrapper struct {
Data ChapaPaymentUrlResponse `json:"data"`
Response
}

View File

@ -48,11 +48,14 @@ func (m Currency) String() string {
return fmt.Sprintf("$%.2f", x)
}
type ResponseWDataFactory[T any] struct {
Data T `json:"data"`
Response
}
type Response struct {
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
Success bool `json:"success"`
StatusCode int `json:"status_code"`
}

View File

@ -57,7 +57,6 @@ type CreateTransaction struct {
PaymentOption PaymentOption
FullName string
PhoneNumber string
// Payment Details for bank
BankCode string
BeneficiaryName string
AccountName string

View File

@ -0,0 +1,98 @@
package chapa
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type ChapaClient interface {
IssuePayment(ctx context.Context, payload domain.ChapaTransferPayload) (bool, error)
InitPayment(ctx context.Context, req domain.InitPaymentRequest) (string, error)
}
type Client struct {
BaseURL string
SecretKey string
HTTPClient *http.Client
}
func NewClient(baseURL, secretKey string) *Client {
return &Client{
BaseURL: baseURL,
SecretKey: secretKey,
HTTPClient: http.DefaultClient,
}
}
func (c *Client) IssuePayment(ctx context.Context, payload domain.ChapaTransferPayload) (bool, error) {
payloadBytes, err := json.Marshal(payload)
if err != nil {
return false, fmt.Errorf("failed to serialize payload: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", c.BaseURL+"/transfers", bytes.NewBuffer(payloadBytes))
if err != nil {
return false, fmt.Errorf("failed to create HTTP request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.SecretKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.HTTPClient.Do(req)
if err != nil {
return false, fmt.Errorf("chapa HTTP request failed: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return true, nil
}
return false, fmt.Errorf("chapa error: status %d, body: %s", resp.StatusCode, string(body))
}
// service/chapa_service.go
func (c *Client) InitPayment(ctx context.Context, req domain.InitPaymentRequest) (string, error) {
payloadBytes, err := json.Marshal(req)
if err != nil {
return "", fmt.Errorf("failed to serialize payload: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.BaseURL+"/transaction/initialize", bytes.NewBuffer(payloadBytes))
if err != nil {
return "", fmt.Errorf("failed to create HTTP request: %w", err)
}
httpReq.Header.Set("Authorization", "Bearer "+c.SecretKey)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.HTTPClient.Do(httpReq)
if err != nil {
return "", fmt.Errorf("chapa HTTP request failed: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return "", fmt.Errorf("chapa error: status %d, body: %s", resp.StatusCode, string(body))
}
var response struct {
Data struct {
CheckoutURL string `json:"checkout_url"`
} `json:"data"`
}
if err := json.Unmarshal(body, &response); err != nil {
return "", fmt.Errorf("failed to parse chapa response: %w", err)
}
return response.Data.CheckoutURL, nil
}

View File

@ -9,4 +9,6 @@ import (
type ChapaPort interface {
HandleChapaTransferWebhook(ctx context.Context, req domain.ChapaWebHookTransfer) error
HandleChapaPaymentWebhook(ctx context.Context, req domain.ChapaWebHookPayment) error
WithdrawUsingChapa(ctx context.Context, userID int64, req domain.ChapaWithdrawRequest) error
DepositUsingChapa(ctx context.Context, userID int64, req domain.ChapaDepositRequest) (string, error)
}

View File

@ -2,15 +2,22 @@ package chapa
import (
"context"
"database/sql"
"errors"
"fmt"
// "log/slog"
"strconv"
"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/branch"
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/google/uuid"
"github.com/shopspring/decimal"
)
@ -19,6 +26,10 @@ type Service struct {
walletStore wallet.WalletStore
userStore user.UserStore
referralStore referralservice.ReferralStore
branchStore branch.BranchStore
chapaClient ChapaClient
config *config.Config
// logger *slog.Logger
store *repository.Store
}
@ -27,6 +38,8 @@ func NewService(
walletStore wallet.WalletStore,
userStore user.UserStore,
referralStore referralservice.ReferralStore,
branchStore branch.BranchStore,
chapaClient ChapaClient,
store *repository.Store,
) *Service {
return &Service{
@ -34,6 +47,8 @@ func NewService(
walletStore: walletStore,
userStore: userStore,
referralStore: referralStore,
branchStore: branchStore,
chapaClient: chapaClient,
store: store,
}
}
@ -53,6 +68,9 @@ func (s *Service) HandleChapaTransferWebhook(ctx context.Context, req domain.Cha
txn, err := s.transactionStore.GetTransactionByID(ctx, referenceID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("transaction with ID %d not found", referenceID)
}
return err
}
if txn.Verified {
@ -93,6 +111,9 @@ func (s *Service) HandleChapaPaymentWebhook(ctx context.Context, req domain.Chap
// 2. Fetch transaction
txn, err := s.transactionStore.GetTransactionByID(ctx, referenceID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("transaction with ID %d not found", referenceID)
}
return err
}
if txn.Verified {
@ -122,7 +143,7 @@ func (s *Service) HandleChapaPaymentWebhook(ctx context.Context, req domain.Chap
}
// 7. Check & Create Referral
stats, err := s.referralStore.GetReferralStats(ctx, string(wallet.UserID))
stats, err := s.referralStore.GetReferralStats(ctx, strconv.FormatInt(wallet.UserID, 10))
if err != nil {
return err
}
@ -135,3 +156,162 @@ func (s *Service) HandleChapaPaymentWebhook(ctx context.Context, req domain.Chap
return tx.Commit(ctx)
}
func (s *Service) WithdrawUsingChapa(ctx context.Context, userID int64, req domain.ChapaWithdrawRequest) error {
_, tx, err := s.store.BeginTx(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
// Get the requesting user
user, err := s.userStore.GetUserByID(ctx, userID)
if err != nil {
return fmt.Errorf("user not found: %w", err)
}
branch, err := s.branchStore.GetBranchByID(ctx, req.BranchID)
if err != nil {
return err
}
wallets, err := s.walletStore.GetWalletsByUser(ctx, userID)
if err != nil {
return err
}
var targetWallet *domain.Wallet
for _, w := range wallets {
if w.ID == req.WalletID {
targetWallet = &w
break
}
}
if targetWallet == nil {
return fmt.Errorf("no wallet found with the specified ID")
}
if !targetWallet.IsWithdraw || !targetWallet.IsActive {
return fmt.Errorf("wallet not eligible for withdrawal")
}
if targetWallet.Balance < domain.Currency(req.Amount) {
return fmt.Errorf("insufficient balance")
}
txID := uuid.New().String()
payload := domain.ChapaTransferPayload{
AccountName: req.AccountName,
AccountNumber: req.AccountNumber,
Amount: strconv.FormatInt(req.Amount, 10),
Currency: req.Currency,
BeneficiaryName: req.BeneficiaryName,
TxRef: txID,
Reference: txID,
BankCode: req.BankCode,
}
ok, err := s.chapaClient.IssuePayment(ctx, payload)
if err != nil || !ok {
return fmt.Errorf("chapa transfer failed: %v", err)
}
// Create transaction using user and wallet info
_, err = s.transactionStore.CreateTransaction(ctx, domain.CreateTransaction{
Amount: domain.Currency(req.Amount),
Type: domain.TransactionType(domain.TRANSACTION_CASHOUT),
ReferenceNumber: txID,
AccountName: req.AccountName,
AccountNumber: req.AccountNumber,
BankCode: req.BankCode,
BeneficiaryName: req.BeneficiaryName,
PaymentOption: domain.PaymentOption(domain.BANK),
BranchID: req.BranchID,
BranchName: branch.Name,
BranchLocation: branch.Location,
// CashierID: user.ID,
// CashierName: user.FullName,
FullName: user.FirstName + " " + user.LastName,
PhoneNumber: user.PhoneNumber,
CompanyID: branch.CompanyID,
})
if err != nil {
return fmt.Errorf("failed to create transaction: %w", err)
}
newBalance := domain.Currency(req.Amount)
err = s.walletStore.UpdateBalance(ctx, targetWallet.ID, newBalance)
if err != nil {
return fmt.Errorf("failed to update wallet balance: %w", err)
}
return tx.Commit(ctx)
}
func (s *Service) DepositUsingChapa(ctx context.Context, userID int64, req domain.ChapaDepositRequest) (string, error) {
_, tx, err := s.store.BeginTx(ctx)
if err != nil {
return "", err
}
defer tx.Rollback(ctx)
user, err := s.userStore.GetUserByID(ctx, userID)
if err != nil {
return "", err
}
branch, err := s.branchStore.GetBranchByID(ctx, req.BranchID)
if err != nil {
return "", err
}
txID := uuid.New().String()
_, err = s.transactionStore.CreateTransaction(ctx, domain.CreateTransaction{
Amount: req.Amount,
Type: domain.TransactionType(domain.TRANSACTION_DEPOSIT),
ReferenceNumber: txID,
BranchID: req.BranchID,
BranchName: branch.Name,
BranchLocation: branch.Location,
FullName: user.FirstName + " " + user.LastName,
PhoneNumber: user.PhoneNumber,
CompanyID: branch.CompanyID,
})
if err != nil {
return "", err
}
// Fetch user details for Chapa payment
userInfo, err := s.userStore.GetUserByID(ctx, userID)
if err != nil {
return "", err
}
// Build Chapa InitPaymentRequest (matches Chapa API)
paymentReq := domain.InitPaymentRequest{
Amount: req.Amount,
Currency: req.Currency,
Email: userInfo.Email,
FirstName: userInfo.FirstName,
LastName: userInfo.LastName,
TxRef: txID,
CallbackURL: s.config.CHAPA_CALLBACK_URL,
ReturnURL: s.config.CHAPA_RETURN_URL,
}
// Call Chapa to initialize payment
paymentURL, err := s.chapaClient.InitPayment(ctx, paymentReq)
if err != nil {
return "", err
}
// Commit DB transaction
if err := tx.Commit(ctx); err != nil {
return "", err
}
return paymentURL, nil
}

View File

@ -7,7 +7,6 @@ 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"
@ -299,7 +298,7 @@ func (h *Handler) VerifyChapaPayment(c *fiber.Ctx) error {
}
switch txType.Type {
case config.ChapaConfig.ChapaTransferType:
case "Payout":
var payload domain.ChapaWebHookTransfer
if err := c.BodyParser(&payload); err != nil {
return domain.UnProcessableEntityResponse(c)
@ -315,7 +314,7 @@ func (h *Handler) VerifyChapaPayment(c *fiber.Ctx) error {
StatusCode: fiber.StatusOK,
})
case config.ChapaConfig.ChapaPaymentType:
case "API":
var payload domain.ChapaWebHookPayment
if err := c.BodyParser(&payload); err != nil {
return domain.UnProcessableEntityResponse(c)
@ -339,3 +338,98 @@ func (h *Handler) VerifyChapaPayment(c *fiber.Ctx) error {
})
}
}
// WithdrawUsingChapa godoc
// @Summary Withdraw using Chapa
// @Description Initiates a withdrawal transaction using Chapa for the authenticated user.
// @Tags Chapa
// @Accept json
// @Produce json
// @Param request body domain.ChapaWithdrawRequest true "Chapa Withdraw Request"
// @Success 200 {object} domain.Response{data=string} "Withdrawal requested successfully"
// @Failure 400 {object} domain.Response "Invalid request"
// @Failure 401 {object} domain.Response "Unauthorized"
// @Failure 422 {object} domain.Response "Unprocessable Entity"
// @Failure 500 {object} domain.Response "Internal Server Error"
// @Router /api/v1/chapa/payments/withdraw [post]
func (h *Handler) WithdrawUsingChapa(c *fiber.Ctx) error {
var req domain.ChapaWithdrawRequest
if err := c.BodyParser(&req); err != nil {
return domain.UnProcessableEntityResponse(c)
}
userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 {
return c.Status(fiber.StatusUnauthorized).JSON(domain.Response{
Message: "Unauthorized",
Success: false,
StatusCode: fiber.StatusUnauthorized,
})
}
if err := h.chapaSvc.WithdrawUsingChapa(c.Context(), userID, req); err != nil {
return domain.FiberErrorResponse(c, err)
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Withdrawal requested successfully",
Success: true,
StatusCode: fiber.StatusOK,
})
}
// DepositUsingChapa godoc
// @Summary Deposit money into user wallet using Chapa
// @Description Deposits money into user wallet from user account using Chapa
// @Tags Chapa
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param payload body domain.ChapaDepositRequest true "Deposit request payload"
// @Success 200 {object} domain.ChapaPaymentUrlResponseWrapper
// @Failure 400 {object} domain.Response "Invalid request"
// @Failure 422 {object} domain.Response "Validation error"
// @Failure 500 {object} domain.Response "Internal server error"
// @Router /api/v1/chapa/payments/deposit [post]
func (h *Handler) DepositUsingChapa(c *fiber.Ctx) error {
// Extract user info from token (adjust as per your auth middleware)
userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 {
return c.Status(fiber.StatusUnauthorized).JSON(domain.Response{
Message: "Unauthorized",
Success: false,
StatusCode: fiber.StatusUnauthorized,
})
}
var req domain.ChapaDepositRequest
if err := c.BodyParser(&req); err != nil {
return domain.UnProcessableEntityResponse(c)
}
// Validate input in domain/model (you may have a Validate method)
if err := req.Validate(); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.Response{
Message: err.Error(),
Success: false,
StatusCode: fiber.StatusBadRequest,
})
}
// Call service to handle the deposit logic and get payment URL
paymentUrl, svcErr := h.chapaSvc.DepositUsingChapa(c.Context(), userID, req)
if svcErr != nil {
return domain.FiberErrorResponse(c, svcErr)
}
return c.Status(fiber.StatusOK).JSON(domain.ResponseWDataFactory[domain.ChapaPaymentUrlResponse]{
Data: domain.ChapaPaymentUrlResponse{
PaymentURL: paymentUrl,
},
Response: domain.Response{
Message: "Deposit process started on wallet, fulfill payment using the URL provided",
Success: true,
StatusCode: fiber.StatusOK,
},
})
}

View File

@ -182,13 +182,16 @@ func (a *App) initAppRoutes() {
a.fiber.Post("/transfer/refill/:id", a.authMiddleware, h.RefillWallet)
//Chapa Routes
group.Post("/chapa/payments/verify", h.VerifyChapaPayment)
group.Post("/chapa/payments/withdraw", h.WithdrawUsingChapa)
group.Post("/chapa/payments/deposit", h.DepositUsingChapa)
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)
// 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
group.Get("/alea-play/launch", a.authMiddleware, h.LaunchAleaGame)