veli games fixes

This commit is contained in:
Yared Yemane 2025-11-06 16:37:41 +03:00
parent 4fdc76280a
commit d654d5f2ef
22 changed files with 967 additions and 816 deletions

View File

@ -3,11 +3,8 @@ CREATE TABLE virtual_game_sessions (
user_id BIGINT NOT NULL REFERENCES users(id),
game_id VARCHAR(50) NOT NULL,
session_token VARCHAR(255) NOT NULL UNIQUE,
currency VARCHAR(3) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', -- ACTIVE, COMPLETED, FAILED
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE virtual_game_transactions (

View File

@ -61,40 +61,40 @@ RETURNING id,
updated_at;
-- name: CreateVirtualGameSession :one
INSERT INTO virtual_game_sessions (
user_id,
game_id,
session_token,
currency,
status,
expires_at
)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id,
user_id,
game_id,
session_token
)
VALUES ($1, $2, $3)
RETURNING
id,
user_id,
game_id,
session_token,
currency,
status,
created_at,
updated_at,
expires_at;
updated_at;
-- name: GetVirtualGameSessionByUserID :one
SELECT
id,
user_id,
game_id,
session_token,
created_at,
updated_at
FROM virtual_game_sessions
WHERE user_id = $1;
-- name: GetVirtualGameSessionByToken :one
SELECT id,
user_id,
game_id,
session_token,
currency,
status,
created_at,
updated_at,
expires_at
updated_at
FROM virtual_game_sessions
WHERE session_token = $1;
-- name: UpdateVirtualGameSessionStatus :exec
UPDATE virtual_game_sessions
SET status = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: CreateVirtualGameTransaction :one
INSERT INTO virtual_game_transactions (
session_id,

View File

@ -458,7 +458,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ArifpayB2CRequest"
"$ref": "#/definitions/domain.CheckoutSessionClientRequest"
}
}
],
@ -585,7 +585,7 @@ const docTemplate = `{
}
}
},
"/api/v1/arifpay/checkout/{sessionId}/cancel": {
"/api/v1/arifpay/checkout/cancel/{sessionId}": {
"post": {
"description": "Cancels a payment session using Arifpay before completion.",
"consumes": [
@ -2254,7 +2254,7 @@ const docTemplate = `{
},
"/api/v1/chapa/payments/webhook/verify": {
"post": {
"description": "Handles payment notifications from Chapa",
"description": "Handles payment and transfer notifications from Chapa",
"consumes": [
"application/json"
],
@ -2272,7 +2272,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ChapaWebhookPayload"
"$ref": "#/definitions/domain.ChapaWebhookPayment"
}
}
],
@ -2280,8 +2280,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
"$ref": "#/definitions/domain.Response"
}
},
"400": {
@ -2329,7 +2328,7 @@ const docTemplate = `{
}
],
"responses": {
"201": {
"200": {
"description": "Chapa withdrawal process initiated successfully",
"schema": {
"$ref": "#/definitions/domain.Response"
@ -2364,7 +2363,7 @@ const docTemplate = `{
},
"/api/v1/chapa/swap": {
"post": {
"description": "Perform a USD to ETB currency swap using Chapa's API",
"description": "Convert an amount from one currency to another using Chapa's currency swap API",
"consumes": [
"application/json"
],
@ -2374,11 +2373,11 @@ const docTemplate = `{
"tags": [
"Chapa"
],
"summary": "Initiate a currency swap",
"summary": "Swap currency using Chapa API",
"parameters": [
{
"description": "Swap Request Payload",
"name": "payload",
"description": "Swap request payload",
"name": "request",
"in": "body",
"required": true,
"schema": {
@ -2536,7 +2535,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.ChapaVerificationResponse"
"$ref": "#/definitions/domain.Response"
}
},
"400": {
@ -10229,28 +10228,6 @@ const docTemplate = `{
}
}
},
"domain.ArifpayB2CRequest": {
"type": "object",
"required": [
"amount",
"customerEmail",
"customerPhone"
],
"properties": {
"Phonenumber": {
"type": "string"
},
"amount": {
"type": "number"
},
"customerEmail": {
"type": "string"
},
"customerPhone": {
"type": "string"
}
}
},
"domain.ArifpayVerifyByTransactionIDRequest": {
"type": "object",
"properties": {
@ -10980,92 +10957,75 @@ const docTemplate = `{
}
}
},
"domain.ChapaVerificationResponse": {
"domain.ChapaWebhookCustomization": {
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"amount": {
"type": "number"
},
"charge": {
"type": "number"
},
"created_at": {
"type": "string"
},
"currency": {
"type": "string"
},
"customization": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"logo": {},
"title": {
"type": "string"
}
}
},
"email": {
"type": "string"
},
"first_name": {
"type": "string"
},
"last_name": {
"type": "string"
},
"meta": {},
"method": {
"type": "string"
},
"mode": {
"type": "string"
},
"reference": {
"type": "string"
},
"status": {
"type": "string"
},
"tx_ref": {
"type": "string"
},
"type": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"message": {
"description": {
"type": "string"
},
"status": {
"logo": {
"type": "string"
},
"title": {
"type": "string"
}
}
},
"domain.ChapaWebhookPayload": {
"domain.ChapaWebhookPayment": {
"type": "object",
"properties": {
"amount": {
"type": "integer"
"type": "string"
},
"charge": {
"type": "string"
},
"created_at": {
"type": "string"
},
"currency": {
"type": "string"
},
"customization": {
"$ref": "#/definitions/domain.ChapaWebhookCustomization"
},
"email": {
"type": "string"
},
"event": {
"type": "string"
},
"first_name": {
"type": "string"
},
"last_name": {
"type": "string"
},
"meta": {
"description": "may vary in structure, so kept flexible"
},
"mobile": {
"type": "string"
},
"mode": {
"type": "string"
},
"payment_method": {
"type": "string"
},
"reference": {
"type": "string"
},
"status": {
"description": "Currency string ` + "`" + `json:\"currency\"` + "`" + `",
"allOf": [
{
"$ref": "#/definitions/domain.PaymentStatus"
}
]
"type": "string"
},
"trx_ref": {
"tx_ref": {
"type": "string"
},
"type": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
@ -12884,21 +12844,6 @@ const docTemplate = `{
"BANK"
]
},
"domain.PaymentStatus": {
"type": "string",
"enum": [
"success",
"pending",
"completed",
"failed"
],
"x-enum-varnames": [
"PaymentStatusSuccessful",
"PaymentStatusPending",
"PaymentStatusCompleted",
"PaymentStatusFailed"
]
},
"domain.PopOKCallback": {
"type": "object",
"properties": {

View File

@ -450,7 +450,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ArifpayB2CRequest"
"$ref": "#/definitions/domain.CheckoutSessionClientRequest"
}
}
],
@ -577,7 +577,7 @@
}
}
},
"/api/v1/arifpay/checkout/{sessionId}/cancel": {
"/api/v1/arifpay/checkout/cancel/{sessionId}": {
"post": {
"description": "Cancels a payment session using Arifpay before completion.",
"consumes": [
@ -2246,7 +2246,7 @@
},
"/api/v1/chapa/payments/webhook/verify": {
"post": {
"description": "Handles payment notifications from Chapa",
"description": "Handles payment and transfer notifications from Chapa",
"consumes": [
"application/json"
],
@ -2264,7 +2264,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ChapaWebhookPayload"
"$ref": "#/definitions/domain.ChapaWebhookPayment"
}
}
],
@ -2272,8 +2272,7 @@
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
"$ref": "#/definitions/domain.Response"
}
},
"400": {
@ -2321,7 +2320,7 @@
}
],
"responses": {
"201": {
"200": {
"description": "Chapa withdrawal process initiated successfully",
"schema": {
"$ref": "#/definitions/domain.Response"
@ -2356,7 +2355,7 @@
},
"/api/v1/chapa/swap": {
"post": {
"description": "Perform a USD to ETB currency swap using Chapa's API",
"description": "Convert an amount from one currency to another using Chapa's currency swap API",
"consumes": [
"application/json"
],
@ -2366,11 +2365,11 @@
"tags": [
"Chapa"
],
"summary": "Initiate a currency swap",
"summary": "Swap currency using Chapa API",
"parameters": [
{
"description": "Swap Request Payload",
"name": "payload",
"description": "Swap request payload",
"name": "request",
"in": "body",
"required": true,
"schema": {
@ -2528,7 +2527,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.ChapaVerificationResponse"
"$ref": "#/definitions/domain.Response"
}
},
"400": {
@ -10221,28 +10220,6 @@
}
}
},
"domain.ArifpayB2CRequest": {
"type": "object",
"required": [
"amount",
"customerEmail",
"customerPhone"
],
"properties": {
"Phonenumber": {
"type": "string"
},
"amount": {
"type": "number"
},
"customerEmail": {
"type": "string"
},
"customerPhone": {
"type": "string"
}
}
},
"domain.ArifpayVerifyByTransactionIDRequest": {
"type": "object",
"properties": {
@ -10972,92 +10949,75 @@
}
}
},
"domain.ChapaVerificationResponse": {
"domain.ChapaWebhookCustomization": {
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"amount": {
"type": "number"
},
"charge": {
"type": "number"
},
"created_at": {
"type": "string"
},
"currency": {
"type": "string"
},
"customization": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"logo": {},
"title": {
"type": "string"
}
}
},
"email": {
"type": "string"
},
"first_name": {
"type": "string"
},
"last_name": {
"type": "string"
},
"meta": {},
"method": {
"type": "string"
},
"mode": {
"type": "string"
},
"reference": {
"type": "string"
},
"status": {
"type": "string"
},
"tx_ref": {
"type": "string"
},
"type": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"message": {
"description": {
"type": "string"
},
"status": {
"logo": {
"type": "string"
},
"title": {
"type": "string"
}
}
},
"domain.ChapaWebhookPayload": {
"domain.ChapaWebhookPayment": {
"type": "object",
"properties": {
"amount": {
"type": "integer"
"type": "string"
},
"charge": {
"type": "string"
},
"created_at": {
"type": "string"
},
"currency": {
"type": "string"
},
"customization": {
"$ref": "#/definitions/domain.ChapaWebhookCustomization"
},
"email": {
"type": "string"
},
"event": {
"type": "string"
},
"first_name": {
"type": "string"
},
"last_name": {
"type": "string"
},
"meta": {
"description": "may vary in structure, so kept flexible"
},
"mobile": {
"type": "string"
},
"mode": {
"type": "string"
},
"payment_method": {
"type": "string"
},
"reference": {
"type": "string"
},
"status": {
"description": "Currency string `json:\"currency\"`",
"allOf": [
{
"$ref": "#/definitions/domain.PaymentStatus"
}
]
"type": "string"
},
"trx_ref": {
"tx_ref": {
"type": "string"
},
"type": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
@ -12876,21 +12836,6 @@
"BANK"
]
},
"domain.PaymentStatus": {
"type": "string",
"enum": [
"success",
"pending",
"completed",
"failed"
],
"x-enum-varnames": [
"PaymentStatusSuccessful",
"PaymentStatusPending",
"PaymentStatusCompleted",
"PaymentStatusFailed"
]
},
"domain.PopOKCallback": {
"type": "object",
"properties": {

View File

@ -38,21 +38,6 @@ definitions:
user_id:
type: string
type: object
domain.ArifpayB2CRequest:
properties:
Phonenumber:
type: string
amount:
type: number
customerEmail:
type: string
customerPhone:
type: string
required:
- amount
- customerEmail
- customerPhone
type: object
domain.ArifpayVerifyByTransactionIDRequest:
properties:
paymentType:
@ -546,62 +531,52 @@ definitions:
updated_at:
type: string
type: object
domain.ChapaVerificationResponse:
domain.ChapaWebhookCustomization:
properties:
data:
properties:
amount:
type: number
charge:
type: number
created_at:
type: string
currency:
type: string
customization:
properties:
description:
type: string
logo: {}
title:
type: string
type: object
email:
type: string
first_name:
type: string
last_name:
type: string
meta: {}
method:
type: string
mode:
type: string
reference:
type: string
status:
type: string
tx_ref:
type: string
type:
type: string
updated_at:
type: string
type: object
message:
description:
type: string
status:
logo:
type: string
title:
type: string
type: object
domain.ChapaWebhookPayload:
domain.ChapaWebhookPayment:
properties:
amount:
type: integer
type: string
charge:
type: string
created_at:
type: string
currency:
type: string
customization:
$ref: '#/definitions/domain.ChapaWebhookCustomization'
email:
type: string
event:
type: string
first_name:
type: string
last_name:
type: string
meta:
description: may vary in structure, so kept flexible
mobile:
type: string
mode:
type: string
payment_method:
type: string
reference:
type: string
status:
allOf:
- $ref: '#/definitions/domain.PaymentStatus'
description: Currency string `json:"currency"`
trx_ref:
type: string
tx_ref:
type: string
type:
type: string
updated_at:
type: string
type: object
domain.ChapaWithdrawalRequest:
@ -1844,18 +1819,6 @@ definitions:
- TELEBIRR_TRANSACTION
- ARIFPAY_TRANSACTION
- BANK
domain.PaymentStatus:
enum:
- success
- pending
- completed
- failed
type: string
x-enum-varnames:
- PaymentStatusSuccessful
- PaymentStatusPending
- PaymentStatusCompleted
- PaymentStatusFailed
domain.PopOKCallback:
properties:
amount:
@ -4608,7 +4571,7 @@ paths:
name: request
required: true
schema:
$ref: '#/definitions/domain.ArifpayB2CRequest'
$ref: '#/definitions/domain.CheckoutSessionClientRequest'
produces:
- application/json
responses:
@ -4695,7 +4658,7 @@ paths:
summary: Create Arifpay Checkout Session
tags:
- Arifpay
/api/v1/arifpay/checkout/{sessionId}/cancel:
/api/v1/arifpay/checkout/cancel/{sessionId}:
post:
consumes:
- application/json
@ -5789,22 +5752,21 @@ paths:
post:
consumes:
- application/json
description: Handles payment notifications from Chapa
description: Handles payment and transfer notifications from Chapa
parameters:
- description: Webhook payload
in: body
name: request
required: true
schema:
$ref: '#/definitions/domain.ChapaWebhookPayload'
$ref: '#/definitions/domain.ChapaWebhookPayment'
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
$ref: '#/definitions/domain.Response'
"400":
description: Bad Request
schema:
@ -5832,7 +5794,7 @@ paths:
produces:
- application/json
responses:
"201":
"200":
description: Chapa withdrawal process initiated successfully
schema:
$ref: '#/definitions/domain.Response'
@ -5861,11 +5823,12 @@ paths:
post:
consumes:
- application/json
description: Perform a USD to ETB currency swap using Chapa's API
description: Convert an amount from one currency to another using Chapa's currency
swap API
parameters:
- description: Swap Request Payload
- description: Swap request payload
in: body
name: payload
name: request
required: true
schema:
$ref: '#/definitions/domain.SwapRequest'
@ -5884,7 +5847,7 @@ paths:
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Initiate a currency swap
summary: Swap currency using Chapa API
tags:
- Chapa
/api/v1/chapa/transaction/cancel/{tx_ref}:
@ -5970,7 +5933,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/domain.ChapaVerificationResponse'
$ref: '#/definitions/domain.Response'
"400":
description: Bad Request
schema:

View File

@ -1093,11 +1093,8 @@ type VirtualGameSession struct {
UserID int64 `json:"user_id"`
GameID string `json:"game_id"`
SessionToken string `json:"session_token"`
Currency string `json:"currency"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
}
type VirtualGameTransaction struct {

View File

@ -409,54 +409,36 @@ func (q *Queries) CreateVirtualGameReport(ctx context.Context, arg CreateVirtual
const CreateVirtualGameSession = `-- name: CreateVirtualGameSession :one
INSERT INTO virtual_game_sessions (
user_id,
game_id,
session_token,
currency,
status,
expires_at
)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id,
user_id,
game_id,
session_token
)
VALUES ($1, $2, $3)
RETURNING
id,
user_id,
game_id,
session_token,
currency,
status,
created_at,
updated_at,
expires_at
updated_at
`
type CreateVirtualGameSessionParams struct {
UserID int64 `json:"user_id"`
GameID string `json:"game_id"`
SessionToken string `json:"session_token"`
Currency string `json:"currency"`
Status string `json:"status"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
UserID int64 `json:"user_id"`
GameID string `json:"game_id"`
SessionToken string `json:"session_token"`
}
func (q *Queries) CreateVirtualGameSession(ctx context.Context, arg CreateVirtualGameSessionParams) (VirtualGameSession, error) {
row := q.db.QueryRow(ctx, CreateVirtualGameSession,
arg.UserID,
arg.GameID,
arg.SessionToken,
arg.Currency,
arg.Status,
arg.ExpiresAt,
)
row := q.db.QueryRow(ctx, CreateVirtualGameSession, arg.UserID, arg.GameID, arg.SessionToken)
var i VirtualGameSession
err := row.Scan(
&i.ID,
&i.UserID,
&i.GameID,
&i.SessionToken,
&i.Currency,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
&i.ExpiresAt,
)
return i, err
}
@ -751,11 +733,8 @@ SELECT id,
user_id,
game_id,
session_token,
currency,
status,
created_at,
updated_at,
expires_at
updated_at
FROM virtual_game_sessions
WHERE session_token = $1
`
@ -768,11 +747,34 @@ func (q *Queries) GetVirtualGameSessionByToken(ctx context.Context, sessionToken
&i.UserID,
&i.GameID,
&i.SessionToken,
&i.Currency,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
&i.ExpiresAt,
)
return i, err
}
const GetVirtualGameSessionByUserID = `-- name: GetVirtualGameSessionByUserID :one
SELECT
id,
user_id,
game_id,
session_token,
created_at,
updated_at
FROM virtual_game_sessions
WHERE user_id = $1
`
func (q *Queries) GetVirtualGameSessionByUserID(ctx context.Context, userID int64) (VirtualGameSession, error) {
row := q.db.QueryRow(ctx, GetVirtualGameSessionByUserID, userID)
var i VirtualGameSession
err := row.Scan(
&i.ID,
&i.UserID,
&i.GameID,
&i.SessionToken,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
@ -1117,23 +1119,6 @@ func (q *Queries) UpdateVirtualGameProviderReportByDate(ctx context.Context, arg
return err
}
const UpdateVirtualGameSessionStatus = `-- name: UpdateVirtualGameSessionStatus :exec
UPDATE virtual_game_sessions
SET status = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`
type UpdateVirtualGameSessionStatusParams struct {
ID int64 `json:"id"`
Status string `json:"status"`
}
func (q *Queries) UpdateVirtualGameSessionStatus(ctx context.Context, arg UpdateVirtualGameSessionStatusParams) error {
_, err := q.db.Exec(ctx, UpdateVirtualGameSessionStatus, arg.ID, arg.Status)
return err
}
const UpdateVirtualGameTransactionStatus = `-- name: UpdateVirtualGameTransactionStatus :exec
UPDATE virtual_game_transactions
SET status = $2,

View File

@ -136,6 +136,7 @@ type Config struct {
AFRO_SMS_SENDER_NAME string
AFRO_SMS_RECEIVER_PHONE_NUMBER string
ADRO_SMS_HOST_URL string
CHAPA_WEBHOOK_SECRET string
CHAPA_TRANSFER_TYPE string
CHAPA_PAYMENT_TYPE string
CHAPA_SECRET_KEY string
@ -259,6 +260,7 @@ func (c *Config) loadEnv() error {
c.TELEBIRR.TelebirrCallbackURL = os.Getenv("TELEBIRR_CALLBACK_URL")
//Chapa
c.CHAPA_WEBHOOK_SECRET = os.Getenv("CHAPA_WEBHOOK_SECRET")
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")

View File

@ -59,12 +59,12 @@ type WebhookRequest struct {
SessionID string `json:"sessionId"`
}
type ArifpayB2CRequest struct{
PhoneNumber string `json:"Phonenumber"`
Amount float64 `json:"amount" binding:"required"`
CustomerEmail string `json:"customerEmail" binding:"required"`
CustomerPhone string `json:"customerPhone" binding:"required"`
}
// type ArifpayB2CRequest struct{
// PhoneNumber string `json:"Phonenumber"`
// Amount float64 `json:"amount" binding:"required"`
// CustomerEmail string `json:"customerEmail" binding:"required"`
// // CustomerPhone string `json:"customerPhone" binding:"required"`
// }
type ArifpayVerifyByTransactionIDRequest struct{
TransactionId string `json:"transactionId"`

View File

@ -76,7 +76,7 @@ type ChapaDepositVerification struct {
Currency string
}
type ChapaVerificationResponse struct {
type ChapaPaymentVerificationResponse struct {
Message string `json:"message"`
Status string `json:"status"`
Data struct {
@ -103,6 +103,31 @@ type ChapaVerificationResponse struct {
} `json:"data"`
}
type ChapaTransferVerificationResponse struct {
Message string `json:"message"`
Status string `json:"status"`
Data struct {
AccountName string `json:"account_name"`
AccountNumber string `json:"account_number"`
Mobile interface{} `json:"mobile"`
Currency string `json:"currency"`
Amount float64 `json:"amount"`
Charge float64 `json:"charge"`
Mode string `json:"mode"`
TransferMethod string `json:"transfer_method"`
Narration interface{} `json:"narration"`
ChapaTransferID string `json:"chapa_transfer_id"`
BankCode int `json:"bank_code"`
BankName string `json:"bank_name"`
CrossPartyReference interface{} `json:"cross_party_reference"`
IPAddress string `json:"ip_address"`
Status string `json:"status"`
TxRef string `json:"tx_ref"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
} `json:"data"`
}
type ChapaAllTransactionsResponse struct {
Message string `json:"message"`
Status string `json:"status"`
@ -182,6 +207,57 @@ type ChapaCustomer struct {
// BankLogo string `json:"bank_logo"` // URL or base64
// }
type SwapResponse struct {
Message string `json:"message"`
Status string `json:"status"`
Data struct {
Status string `json:"status"`
RefID string `json:"ref_id"`
FromCurrency string `json:"from_currency"`
ToCurrency string `json:"to_currency"`
Amount float64 `json:"amount"`
ExchangedAmount float64 `json:"exchanged_amount"`
Charge float64 `json:"charge"`
Rate float64 `json:"rate"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
} `json:"data"`
}
type ChapaTransfersListResponse struct {
Message string `json:"message"`
Status string `json:"status"`
Meta struct {
CurrentPage int `json:"current_page"`
FirstPageURL string `json:"first_page_url"`
LastPage int `json:"last_page"`
LastPageURL string `json:"last_page_url"`
NextPageURL string `json:"next_page_url"`
Path string `json:"path"`
PerPage int `json:"per_page"`
PrevPageURL interface{} `json:"prev_page_url"`
To int `json:"to"`
Total int `json:"total"`
Error []interface{} `json:"error"`
} `json:"meta"`
Data []struct {
AccountName string `json:"account_name"`
AccountNumber string `json:"account_number"`
Currency string `json:"currency"`
Amount float64 `json:"amount"`
Charge float64 `json:"charge"`
TransferType string `json:"transfer_type"`
ChapaReference string `json:"chapa_reference"`
BankCode int `json:"bank_code"`
BankName string `json:"bank_name"`
BankReference interface{} `json:"bank_reference"`
Status string `json:"status"`
Reference interface{} `json:"reference"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
} `json:"data"`
}
type BankResponse struct {
Message string `json:"message"`
Status string `json:"status"`
@ -246,44 +322,49 @@ 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 ChapaWebhookTransfer struct {
Event string `json:"event"`
Type string `json:"type"`
AccountName string `json:"account_name"`
AccountNumber string `json:"account_number"`
BankID int `json:"bank_id"`
BankName string `json:"bank_name"`
Amount string `json:"amount"`
Charge string `json:"charge"`
Currency string `json:"currency"`
Status string `json:"status"`
Reference string `json:"reference"`
ChapaReference string `json:"chapa_reference"`
BankReference string `json:"bank_reference"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_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"`
type ChapaWebhookPayment struct {
Event string `json:"event"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email *string `json:"email,omitempty"`
Mobile string `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 string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Type string `json:"type"`
TxRef string `json:"tx_ref"`
PaymentMethod string `json:"payment_method"`
Customization ChapaWebhookCustomization `json:"customization"`
Meta interface{} `json:"meta"` // may vary in structure, so kept flexible
}
type ChapaWebhookCustomization struct {
Title *string `json:"title,omitempty"`
Description *string `json:"description,omitempty"`
Logo *string `json:"logo,omitempty"`
}
type Balance struct {
@ -298,19 +379,6 @@ type SwapRequest struct {
Amount float64 `json:"amount"`
}
type SwapResponse struct {
Status string `json:"status"`
RefID string `json:"ref_id"`
FromCurrency string `json:"from_currency"`
ToCurrency string `json:"to_currency"`
Amount float64 `json:"amount"`
ExchangedAmount float64 `json:"exchanged_amount"`
Charge float64 `json:"charge"`
Rate float64 `json:"rate"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type ChapaCancelResponse struct {
Message string `json:"message"`
Status string `json:"status"`

View File

@ -58,10 +58,10 @@ type GameStartRequest struct {
Country string `json:"country"`
IP string `json:"ip"`
BrandID string `json:"brandId"`
UserAgent string `json:"userAgent,omitempty"`
LobbyURL string `json:"lobbyUrl,omitempty"`
CashierURL string `json:"cashierUrl,omitempty"`
PlayerName string `json:"playerName,omitempty"`
// UserAgent string `json:"userAgent,omitempty"`
// LobbyURL string `json:"lobbyUrl,omitempty"`
// CashierURL string `json:"cashierUrl,omitempty"`
// PlayerName string `json:"playerName,omitempty"`
}
type DemoGameRequest struct {
@ -71,8 +71,8 @@ type DemoGameRequest struct {
DeviceType string `json:"deviceType"`
IP string `json:"ip"`
BrandID string `json:"brandId"`
PlayerID string `json:"playerId,omitempty"`
Country string `json:"country,omitempty"`
// PlayerID string `json:"playerId,omitempty"`
// Country string `json:"country,omitempty"`
}
type GameStartResponse struct {

View File

@ -21,8 +21,9 @@ type VirtualGameRepository interface {
ListVirtualGameProviders(ctx context.Context, limit, offset int32) ([]dbgen.VirtualGameProvider, error)
UpdateVirtualGameProviderEnabled(ctx context.Context, providerID string, enabled bool) (dbgen.VirtualGameProvider, error)
CreateVirtualGameSession(ctx context.Context, session *domain.VirtualGameSession) error
GetVirtualGameSessionByUserID(ctx context.Context, userID int64) (*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
@ -166,14 +167,33 @@ func (r *VirtualGameRepo) CreateVirtualGameSession(ctx context.Context, session
UserID: session.UserID,
GameID: session.GameID,
SessionToken: session.SessionToken,
Currency: session.Currency,
Status: session.Status,
ExpiresAt: pgtype.Timestamptz{Time: session.ExpiresAt, Valid: true},
// Currency: session.Currency,
// Status: session.Status,
// ExpiresAt: pgtype.Timestamptz{Time: session.ExpiresAt, Valid: true},
}
_, err := r.store.queries.CreateVirtualGameSession(ctx, params)
return err
}
func (r *VirtualGameRepo) GetVirtualGameSessionByUserID(ctx context.Context, userID int64) (*domain.VirtualGameSession, error) {
dbSession, err := r.store.queries.GetVirtualGameSessionByUserID(ctx, userID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}
return &domain.VirtualGameSession{
ID: dbSession.ID,
UserID: dbSession.UserID,
GameID: dbSession.GameID,
SessionToken: dbSession.SessionToken,
CreatedAt: dbSession.CreatedAt.Time,
UpdatedAt: dbSession.UpdatedAt.Time,
}, nil
}
func (r *VirtualGameRepo) GetVirtualGameSessionByToken(ctx context.Context, token string) (*domain.VirtualGameSession, error) {
dbSession, err := r.store.queries.GetVirtualGameSessionByToken(ctx, token)
if err != nil {
@ -187,20 +207,20 @@ func (r *VirtualGameRepo) GetVirtualGameSessionByToken(ctx context.Context, toke
UserID: dbSession.UserID,
GameID: dbSession.GameID,
SessionToken: dbSession.SessionToken,
Currency: dbSession.Currency,
Status: dbSession.Status,
CreatedAt: dbSession.CreatedAt.Time,
UpdatedAt: dbSession.UpdatedAt.Time,
ExpiresAt: dbSession.ExpiresAt.Time,
// Currency: dbSession.Currency,
// Status: dbSession.Status,
CreatedAt: dbSession.CreatedAt.Time,
UpdatedAt: dbSession.UpdatedAt.Time,
// ExpiresAt: dbSession.ExpiresAt.Time,
}, nil
}
func (r *VirtualGameRepo) UpdateVirtualGameSessionStatus(ctx context.Context, id int64, status string) error {
return r.store.queries.UpdateVirtualGameSessionStatus(ctx, dbgen.UpdateVirtualGameSessionStatusParams{
ID: id,
Status: status,
})
}
// func (r *VirtualGameRepo) UpdateVirtualGameSessionStatus(ctx context.Context, id int64, status string) error {
// return r.store.queries.UpdateVirtualGameSessionStatus(ctx, dbgen.UpdateVirtualGameSessionStatusParams{
// ID: id,
// Status: status,
// })
// }
func (r *VirtualGameRepo) CreateVirtualGameTransaction(ctx context.Context, tx *domain.VirtualGameTransaction) error {
params := dbgen.CreateVirtualGameTransactionParams{

View File

@ -8,6 +8,7 @@ import (
"fmt"
"io"
"net/http"
"strings"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
@ -31,15 +32,15 @@ func NewArifpayService(cfg *config.Config, transferStore wallet.TransferStore, w
}
}
func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientRequest, isDeposit bool) (map[string]any, error) {
func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientRequest, isDeposit bool, userId int64) (map[string]any, error) {
// Generate unique nonce
nonce := uuid.NewString()
var NotifyURL string
if isDeposit{
if isDeposit {
NotifyURL = s.cfg.ARIFPAY.C2BNotifyUrl
}else{
} else {
NotifyURL = s.cfg.ARIFPAY.B2CNotifyUrl
}
@ -129,6 +130,10 @@ func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientR
ReferenceNumber: nonce,
SessionID: fmt.Sprintf("%v", data["sessionId"]),
Status: string(domain.PaymentStatusPending),
CashierID: domain.ValidInt64{
Value: userId,
Valid: true,
},
}
if _, err := s.transferStore.CreateTransfer(context.Background(), transfer); err != nil {
@ -138,7 +143,7 @@ func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientR
return data, nil
}
func (s *ArifpayService) CancelCheckoutSession(ctx context.Context, sessionID string) (*domain.CancelCheckoutSessionResponse, error) {
func (s *ArifpayService) CancelCheckoutSession(ctx context.Context, sessionID string) (any, error) {
// Build the cancel URL
url := fmt.Sprintf("%s/api/sandbox/checkout/session/%s", s.cfg.ARIFPAY.BaseURL, sessionID)
@ -176,17 +181,19 @@ func (s *ArifpayService) CancelCheckoutSession(ctx context.Context, sessionID st
return nil, fmt.Errorf("failed to unmarshal cancel response: %w", err)
}
return &cancelResp, nil
return cancelResp.Data, nil
}
func (s *ArifpayService) HandleWebhook(ctx context.Context, req domain.WebhookRequest, userId int64, isDepost bool) error {
func (s *ArifpayService) ProcessWebhook(ctx context.Context, req domain.WebhookRequest, isDeposit bool) error {
// 1. Get transfer by SessionID
transfer, err := s.transferStore.GetTransferByReference(ctx, req.Transaction.TransactionID)
transfer, err := s.transferStore.GetTransferByReference(ctx, req.Nonce)
if err != nil {
return err
}
wallets, err := s.walletSvc.GetWalletsByUser(ctx, userId)
userId := transfer.DepositorID.Value
wallet, err := s.walletSvc.GetCustomerWallet(ctx, userId)
if err != nil {
return err
}
@ -196,7 +203,7 @@ func (s *ArifpayService) HandleWebhook(ctx context.Context, req domain.WebhookRe
}
// 2. Update transfer status
newStatus := req.Transaction.TransactionStatus
newStatus := strings.ToLower(req.Transaction.TransactionStatus)
// if req.Transaction.TransactionStatus != "" {
// newStatus = req.Transaction.TransactionStatus
// }
@ -212,10 +219,10 @@ func (s *ArifpayService) HandleWebhook(ctx context.Context, req domain.WebhookRe
}
// 3. If SUCCESS -> update customer wallet balance
if (newStatus == "SUCCESS" && isDepost) || (newStatus == "FAILED" && !isDepost) {
_, err = s.walletSvc.AddToWallet(ctx, wallets[0].ID, domain.Currency(req.TotalAmount), domain.ValidInt64{}, transfer.PaymentMethod, domain.PaymentDetails{
if (newStatus == "success" && isDeposit) || (newStatus == "failed" && !isDeposit) {
_, err = s.walletSvc.AddToWallet(ctx, wallet.RegularID, domain.Currency(req.TotalAmount), domain.ValidInt64{}, transfer.PaymentMethod, domain.PaymentDetails{
ReferenceNumber: domain.ValidString{
Value: req.Transaction.TransactionID,
Value: req.Nonce,
Valid: true,
},
BankNumber: domain.ValidString{
@ -231,35 +238,94 @@ func (s *ArifpayService) HandleWebhook(ctx context.Context, req domain.WebhookRe
return nil
}
func (s *ArifpayService) ExecuteTelebirrB2CTransfer(ctx context.Context, req domain.ArifpayB2CRequest, userId int64) error {
func (s *ArifpayService) ExecuteTelebirrB2CTransfer(ctx context.Context, req domain.CheckoutSessionClientRequest, userId int64) error {
// Step 1: Create Session
userWallet, err := s.walletSvc.GetCustomerWallet(ctx, userId)
if err != nil {
return fmt.Errorf("failed to get user wallets: %w", err)
}
// if len(userWallets) == 0 {
// return fmt.Errorf("no wallet found for user %d", userId)
// }
_, err = s.walletSvc.DeductFromWallet(
ctx,
userWallet.RegularID,
domain.Currency(req.Amount),
domain.ValidInt64{},
domain.TRANSFER_ARIFPAY,
"",
)
if err != nil {
return fmt.Errorf("failed to deduct from wallet: %w", err)
}
referenceNum := uuid.NewString()
sessionReq := domain.CheckoutSessionClientRequest{
Amount: req.Amount,
CustomerEmail: req.CustomerEmail,
CustomerPhone: req.CustomerPhone,
CustomerPhone: "251" + req.CustomerPhone[:9],
}
sessionResp, err := s.CreateCheckoutSession(sessionReq, false)
sessionResp, err := s.CreateCheckoutSession(sessionReq, false, userId)
if err != nil {
_, err = s.walletSvc.AddToWallet(
ctx,
userWallet.RegularID,
domain.Currency(req.Amount),
domain.ValidInt64{},
domain.TRANSFER_ARIFPAY,
domain.PaymentDetails{},
"",
)
if err != nil {
return fmt.Errorf("failed to deduct from wallet: %w", err)
}
return fmt.Errorf("failed to create session: %w", err)
}
sessionRespData := sessionResp["data"].(map[string]any)
// Step 2: Execute Transfer
transferURL := fmt.Sprintf("%s/api/Telebirr/b2c/transfer", s.cfg.ARIFPAY.BaseURL)
reqBody := map[string]any{
"Sessionid": sessionResp["sessionId"],
"Phonenumber": req.PhoneNumber,
"Sessionid": sessionRespData["sessionId"],
"Phonenumber": "251" + req.CustomerPhone[:9],
}
payload, err := json.Marshal(reqBody)
if err != nil {
_, err = s.walletSvc.AddToWallet(
ctx,
userWallet.RegularID,
domain.Currency(req.Amount),
domain.ValidInt64{},
domain.TRANSFER_ARIFPAY,
domain.PaymentDetails{},
"",
)
if err != nil {
return fmt.Errorf("failed to deduct from wallet: %w", err)
}
return fmt.Errorf("failed to marshal transfer request: %w", err)
}
transferReq, err := http.NewRequestWithContext(ctx, http.MethodPost, transferURL, bytes.NewBuffer(payload))
if err != nil {
_, err = s.walletSvc.AddToWallet(
ctx,
userWallet.RegularID,
domain.Currency(req.Amount),
domain.ValidInt64{},
domain.TRANSFER_ARIFPAY,
domain.PaymentDetails{},
"",
)
if err != nil {
return fmt.Errorf("failed to deduct from wallet: %w", err)
}
return fmt.Errorf("failed to build transfer request: %w", err)
}
transferReq.Header.Set("Content-Type", "application/json")
@ -267,11 +333,35 @@ func (s *ArifpayService) ExecuteTelebirrB2CTransfer(ctx context.Context, req dom
transferResp, err := s.httpClient.Do(transferReq)
if err != nil {
_, err = s.walletSvc.AddToWallet(
ctx,
userWallet.RegularID,
domain.Currency(req.Amount),
domain.ValidInt64{},
domain.TRANSFER_ARIFPAY,
domain.PaymentDetails{},
"",
)
if err != nil {
return fmt.Errorf("failed to deduct from wallet: %w", err)
}
return fmt.Errorf("failed to execute transfer request: %w", err)
}
defer transferResp.Body.Close()
if transferResp.StatusCode >= 300 {
if transferResp.StatusCode != http.StatusOK {
_, err = s.walletSvc.AddToWallet(
ctx,
userWallet.RegularID,
domain.Currency(req.Amount),
domain.ValidInt64{},
domain.TRANSFER_ARIFPAY,
domain.PaymentDetails{},
"",
)
if err != nil {
return fmt.Errorf("failed to deduct from wallet: %w", err)
}
body, _ := io.ReadAll(transferResp.Body)
return fmt.Errorf("transfer failed with status %d: %s", transferResp.StatusCode, string(body))
}
@ -282,109 +372,33 @@ func (s *ArifpayService) ExecuteTelebirrB2CTransfer(ctx context.Context, req dom
Verified: false,
Type: domain.WITHDRAW, // B2C = payout
ReferenceNumber: referenceNum,
SessionID: fmt.Sprintf("%v", sessionResp["sessionId"]),
SessionID: fmt.Sprintf("%v", sessionRespData["sessionId"]),
Status: string(domain.PaymentStatusPending),
PaymentMethod: domain.TRANSFER_ARIFPAY,
CashierID: domain.ValidInt64{
Value: userId,
Valid: true,
},
}
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
return fmt.Errorf("failed to store transfer: %w", err)
}
// Step 4: Deduct from wallet
userWallets, err := s.walletSvc.GetWalletsByUser(ctx, userId)
if err != nil {
return fmt.Errorf("failed to get user wallets: %w", err)
}
if len(userWallets) == 0 {
return fmt.Errorf("no wallet found for user %d", userId)
}
_, err = s.walletSvc.DeductFromWallet(
ctx,
userWallets[0].ID,
domain.Currency(req.Amount),
domain.ValidInt64{},
domain.TRANSFER_ARIFPAY,
"",
)
if err != nil {
return fmt.Errorf("failed to deduct from wallet: %w", err)
}
return nil
}
func (s *ArifpayService) ExecuteCBEB2CTransfer(ctx context.Context, req domain.ArifpayB2CRequest, userId int64) error {
// Step 1: Create Session
referenceNum := uuid.NewString()
sessionReq := domain.CheckoutSessionClientRequest{
Amount: req.Amount,
CustomerEmail: req.CustomerEmail,
CustomerPhone: req.CustomerPhone,
}
sessionResp, err := s.CreateCheckoutSession(sessionReq, false)
func (s *ArifpayService) ExecuteCBEB2CTransfer(ctx context.Context, req domain.CheckoutSessionClientRequest, userId int64) error {
// Step 1: Deduct from user wallet first
userWallet, err := s.walletSvc.GetCustomerWallet(ctx, userId)
if err != nil {
return fmt.Errorf("cbebirr: failed to create session: %w", err)
}
// Step 2: Execute Transfer
transferURL := fmt.Sprintf("%s/api/Cbebirr/b2c/transfer", s.cfg.ARIFPAY.BaseURL)
reqBody := map[string]any{
"Sessionid": sessionResp["sessionId"],
"Phonenumber": req.PhoneNumber,
}
payload, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("cbebirr: failed to marshal transfer request: %w", err)
}
transferReq, err := http.NewRequestWithContext(ctx, http.MethodPost, transferURL, bytes.NewBuffer(payload))
if err != nil {
return fmt.Errorf("cbebirr: failed to build transfer request: %w", err)
}
transferReq.Header.Set("Content-Type", "application/json")
transferReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey)
transferResp, err := s.httpClient.Do(transferReq)
if err != nil {
return fmt.Errorf("cbebirr: failed to execute transfer request: %w", err)
}
defer transferResp.Body.Close()
if transferResp.StatusCode >= 300 {
body, _ := io.ReadAll(transferResp.Body)
return fmt.Errorf("cbebirr: transfer failed with status %d: %s", transferResp.StatusCode, string(body))
}
// Step 3: Store transfer in DB
transfer := domain.CreateTransfer{
Amount: domain.Currency(req.Amount),
Verified: false,
Type: domain.WITHDRAW, // B2C = payout
ReferenceNumber: referenceNum,
SessionID: fmt.Sprintf("%v", sessionResp["sessionId"]),
Status: string(domain.PaymentStatusPending),
PaymentMethod: domain.TRANSFER_ARIFPAY,
}
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
return fmt.Errorf("cbebirr: failed to store transfer: %w", err)
}
// Step 4: Deduct from user wallet
userWallets, err := s.walletSvc.GetWalletsByUser(ctx, userId)
if err != nil {
return fmt.Errorf("cbebirr: failed to get user wallets: %w", err)
}
if len(userWallets) == 0 {
return fmt.Errorf("cbebirr: no wallet found for user %d", userId)
return fmt.Errorf("cbebirr: failed to get user wallet: %w", err)
}
_, err = s.walletSvc.DeductFromWallet(
ctx,
userWallets[0].ID,
userWallet.RegularID,
domain.Currency(req.Amount),
domain.ValidInt64{},
domain.TRANSFER_ARIFPAY,
@ -394,55 +408,68 @@ func (s *ArifpayService) ExecuteCBEB2CTransfer(ctx context.Context, req domain.A
return fmt.Errorf("cbebirr: failed to deduct from wallet: %w", err)
}
return nil
}
func (s *ArifpayService) ExecuteMPesaB2CTransfer(ctx context.Context, req domain.ArifpayB2CRequest, userId int64) error {
// Step 1: Create Session
referenceNum := uuid.NewString()
// Step 2: Create Session
sessionReq := domain.CheckoutSessionClientRequest{
Amount: req.Amount,
CustomerEmail: req.CustomerEmail,
CustomerPhone: req.CustomerPhone,
CustomerPhone: "251" + req.CustomerPhone[:9],
}
sessionResp, err := s.CreateCheckoutSession(sessionReq, false)
sessionResp, err := s.CreateCheckoutSession(sessionReq, false, userId)
if err != nil {
return fmt.Errorf("Mpesa: failed to create session: %w", err)
// refund wallet if session creation fails
_, refundErr := s.walletSvc.AddToWallet(
ctx,
userWallet.RegularID,
domain.Currency(req.Amount),
domain.ValidInt64{},
domain.TRANSFER_ARIFPAY,
domain.PaymentDetails{},
"",
)
if refundErr != nil {
return fmt.Errorf("cbebirr: refund failed after session creation error: %v", refundErr)
}
return fmt.Errorf("cbebirr: failed to create session: %w", err)
}
// Step 2: Execute Transfer
transferURL := fmt.Sprintf("%s/api/Mpesa/b2c/transfer", s.cfg.ARIFPAY.BaseURL)
// Step 3: Execute Transfer
transferURL := fmt.Sprintf("%s/api/Cbebirr/b2c/transfer", s.cfg.ARIFPAY.BaseURL)
reqBody := map[string]any{
"Sessionid": sessionResp["sessionId"],
"Phonenumber": req.PhoneNumber,
"Phonenumber": "251" + req.CustomerPhone[:9],
}
payload, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("Mpesa: failed to marshal transfer request: %w", err)
s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
return fmt.Errorf("cbebirr: failed to marshal transfer request: %w", err)
}
transferReq, err := http.NewRequestWithContext(ctx, http.MethodPost, transferURL, bytes.NewBuffer(payload))
if err != nil {
return fmt.Errorf("Mpesa: failed to build transfer request: %w", err)
s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
return fmt.Errorf("cbebirr: failed to build transfer request: %w", err)
}
transferReq.Header.Set("Content-Type", "application/json")
transferReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey)
transferResp, err := s.httpClient.Do(transferReq)
if err != nil {
return fmt.Errorf("Mpesa: failed to execute transfer request: %w", err)
s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
return fmt.Errorf("cbebirr: failed to execute transfer request: %w", err)
}
defer transferResp.Body.Close()
if transferResp.StatusCode >= 300 {
if transferResp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(transferResp.Body)
return fmt.Errorf("Mpesa: transfer failed with status %d: %s", transferResp.StatusCode, string(body))
s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
return fmt.Errorf("cbebirr: transfer failed with status %d: %s", transferResp.StatusCode, string(body))
}
// Step 3: Store transfer in DB
// Step 4: Store transfer in DB
transfer := domain.CreateTransfer{
Amount: domain.Currency(req.Amount),
Verified: false,
@ -451,30 +478,116 @@ func (s *ArifpayService) ExecuteMPesaB2CTransfer(ctx context.Context, req domain
SessionID: fmt.Sprintf("%v", sessionResp["sessionId"]),
Status: string(domain.PaymentStatusPending),
PaymentMethod: domain.TRANSFER_ARIFPAY,
}
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
return fmt.Errorf("Mpesa: failed to store transfer: %w", err)
CashierID: domain.ValidInt64{
Value: userId,
Valid: true,
},
}
// Step 4: Deduct from user wallet
userWallets, err := s.walletSvc.GetWalletsByUser(ctx, userId)
if err != nil {
return fmt.Errorf("Mpesa: failed to get user wallets: %w", err)
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
return fmt.Errorf("cbebirr: failed to store transfer: %w", err)
}
if len(userWallets) == 0 {
return fmt.Errorf("Mpesa: no wallet found for user %d", userId)
return nil
}
func (s *ArifpayService) ExecuteMPesaB2CTransfer(ctx context.Context, req domain.CheckoutSessionClientRequest, userId int64) error {
// Step 1: Deduct from user wallet first
userWallet, err := s.walletSvc.GetCustomerWallet(ctx, userId)
if err != nil {
return fmt.Errorf("mpesa: failed to get user wallet: %w", err)
}
_, err = s.walletSvc.DeductFromWallet(
ctx,
userWallets[0].ID,
userWallet.RegularID,
domain.Currency(req.Amount),
domain.ValidInt64{},
domain.TRANSFER_ARIFPAY,
"",
)
if err != nil {
return fmt.Errorf("Mpesa: failed to deduct from wallet: %w", err)
return fmt.Errorf("mpesa: failed to deduct from wallet: %w", err)
}
referenceNum := uuid.NewString()
// Step 2: Create Session
sessionReq := domain.CheckoutSessionClientRequest{
Amount: req.Amount,
CustomerEmail: req.CustomerEmail,
CustomerPhone: "251" + req.CustomerPhone[:9],
}
sessionResp, err := s.CreateCheckoutSession(sessionReq, false, userId)
if err != nil {
// Refund wallet if session creation fails
_, refundErr := s.walletSvc.AddToWallet(
ctx,
userWallet.RegularID,
domain.Currency(req.Amount),
domain.ValidInt64{},
domain.TRANSFER_ARIFPAY,
domain.PaymentDetails{},
"",
)
if refundErr != nil {
return fmt.Errorf("mpesa: refund failed after session creation error: %v", refundErr)
}
return fmt.Errorf("mpesa: failed to create session: %w", err)
}
// Step 3: Execute Transfer
transferURL := fmt.Sprintf("%s/api/Mpesa/b2c/transfer", s.cfg.ARIFPAY.BaseURL)
reqBody := map[string]any{
"Sessionid": sessionResp["sessionId"],
"Phonenumber": "251" + req.CustomerPhone[:9],
}
payload, err := json.Marshal(reqBody)
if err != nil {
s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
return fmt.Errorf("mpesa: failed to marshal transfer request: %w", err)
}
transferReq, err := http.NewRequestWithContext(ctx, http.MethodPost, transferURL, bytes.NewBuffer(payload))
if err != nil {
s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
return fmt.Errorf("mpesa: failed to build transfer request: %w", err)
}
transferReq.Header.Set("Content-Type", "application/json")
transferReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey)
transferResp, err := s.httpClient.Do(transferReq)
if err != nil {
s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
return fmt.Errorf("mpesa: failed to execute transfer request: %w", err)
}
defer transferResp.Body.Close()
if transferResp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(transferResp.Body)
s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
return fmt.Errorf("mpesa: transfer failed with status %d: %s", transferResp.StatusCode, string(body))
}
// Step 4: Store transfer in DB
transfer := domain.CreateTransfer{
Amount: domain.Currency(req.Amount),
Verified: false,
Type: domain.WITHDRAW, // B2C = payout
ReferenceNumber: referenceNum,
SessionID: fmt.Sprintf("%v", sessionResp["sessionId"]),
Status: string(domain.PaymentStatusPending),
PaymentMethod: domain.TRANSFER_ARIFPAY,
CashierID: domain.ValidInt64{
Value: userId,
Valid: true,
},
}
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
return fmt.Errorf("mpesa: failed to store transfer: %w", err)
}
return nil

View File

@ -37,7 +37,7 @@ func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaInitDepo
"first_name": req.FirstName,
"last_name": req.LastName,
"tx_ref": req.TxRef,
"callback_url": req.CallbackURL,
// "callback_url": req.CallbackURL,
"return_url": req.ReturnURL,
"phone_number": req.PhoneNumber,
}
@ -131,7 +131,7 @@ func (c *Client) VerifyPayment(ctx context.Context, reference string) (domain.Ch
}, nil
}
func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain.ChapaPaymentVerificationResponse, error) {
url := fmt.Sprintf("%s/transaction/verify/%s", c.baseURL, txRef)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
@ -153,7 +153,7 @@ func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain
return nil, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body))
}
var verification domain.ChapaVerificationResponse
var verification domain.ChapaPaymentVerificationResponse
if err := json.NewDecoder(resp.Body).Decode(&verification); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
@ -169,7 +169,7 @@ func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain
return &verification, nil
}
func (c *Client) ManualVerifyTransfer(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
func (c *Client) ManualVerifyTransfer(ctx context.Context, txRef string) (*domain.ChapaTransferVerificationResponse, error) {
url := fmt.Sprintf("%s/transfers/verify/%s", c.baseURL, txRef)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
@ -207,8 +207,8 @@ func (c *Client) ManualVerifyTransfer(ctx context.Context, txRef string) (*domai
status = domain.PaymentStatusFailed
}
return &domain.ChapaVerificationResponse{
Status: string(status),
return &domain.ChapaTransferVerificationResponse{
Status: string(status),
// Amount: response.Amount,
// Currency: response.Currency,
}, nil
@ -277,7 +277,7 @@ func (c *Client) GetTransactionEvents(ctx context.Context, refId string) ([]doma
return response.Data, nil
}
func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) {
func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.BankData, error) {
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/banks", nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
@ -300,9 +300,9 @@ func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error)
return nil, fmt.Errorf("failed to decode response: %w", err)
}
var banks []domain.Bank
var banks []domain.BankData
for _, bankData := range bankResponse.Data {
bank := domain.Bank{
bank := domain.BankData{
ID: bankData.ID,
Slug: bankData.Slug,
Swift: bankData.Swift,
@ -324,7 +324,7 @@ func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error)
return banks, nil
}
func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawalRequest) (bool, error) {
func (c *Client) InitializeTransfer(ctx context.Context, req domain.ChapaWithdrawalRequest) (bool, error) {
endpoint := c.baseURL + "/transfers"
fmt.Printf("\n\nChapa withdrawal URL is %v\n\n", endpoint)
@ -361,7 +361,7 @@ func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawa
return response.Status == string(domain.WithdrawalStatusSuccessful), nil
}
func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain.ChapaVerificationResponse, error) {
func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain.ChapaTransferVerificationResponse, error) {
base, err := url.Parse(c.baseURL)
if err != nil {
return nil, fmt.Errorf("invalid base URL: %w", err)
@ -385,7 +385,7 @@ func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain.
return nil, fmt.Errorf("chapa api returned status: %d", resp.StatusCode)
}
var verification domain.ChapaVerificationResponse
var verification domain.ChapaTransferVerificationResponse
if err := json.NewDecoder(resp.Body).Decode(&verification); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}

View File

@ -16,11 +16,11 @@ import (
type ChapaStore interface {
InitializePayment(request domain.ChapaInitDepositRequest) (domain.ChapaDepositResponse, error)
ManualVerifTransaction(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error)
ManualVerifTransaction(ctx context.Context, txRef string) (*domain.ChapaTransferVerificationResponse, error)
FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error)
CreateWithdrawal(userID string, amount float64, accountNumber, bankCode string) (*domain.ChapaWithdrawal, error)
HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebHookTransfer) error
HandleVerifyWithdrawWebhook(ctx context.Context, payment domain.ChapaWebHookPayment) error
HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebhookTransfer) error
HandleVerifyWithdrawWebhook(ctx context.Context, payment domain.ChapaWebhookPayment) error
GetPaymentReceiptURL(ctx context.Context, chapaRef string) (string, error)
GetAllTransfers(ctx context.Context) ([]domain.Transfer, error)
GetAccountBalance(ctx context.Context, currencyCode string) ([]domain.Balance, error)

View File

@ -3,6 +3,9 @@ package chapa
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
@ -43,6 +46,31 @@ func NewService(
}
}
func (s *Service) VerifyWebhookSignature(ctx context.Context, payload []byte, chapaSignature, xChapaSignature string) (bool, error) {
secret := s.cfg.CHAPA_WEBHOOK_SECRET // or os.Getenv("CHAPA_SECRET_KEY")
if secret == "" {
return false, fmt.Errorf("missing Chapa secret key in configuration")
}
// Compute expected signature using HMAC SHA256
h := hmac.New(sha256.New, []byte(secret))
h.Write(payload)
expected := hex.EncodeToString(h.Sum(nil))
// Check either header
if chapaSignature == expected || xChapaSignature == expected {
return true, nil
}
// Optionally log for debugging
var pretty map[string]interface{}
_ = json.Unmarshal(payload, &pretty)
fmt.Printf("[Webhook Verification Failed]\nExpected: %s\nGot chapa-signature: %s\nGot x-chapa-signature: %s\nPayload: %+v\n",
expected, chapaSignature, xChapaSignature, pretty)
return false, fmt.Errorf("invalid webhook signature")
}
// InitiateDeposit starts a new deposit process
func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount domain.Currency) (string, error) {
// Validate amount
@ -88,7 +116,7 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
ReferenceNumber: reference,
// ReceiverWalletID: 1,
SenderWalletID: domain.ValidInt64{
Value: senderWallet.ID,
Value: senderWallet.RegularID,
Valid: true,
},
Verified: false,
@ -135,9 +163,9 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
return response.CheckoutURL, nil
}
func (s *Service) ProcessVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaPaymentWebhookRequest) error {
func (s *Service) ProcessVerifyDepositWebhook(ctx context.Context, req domain.ChapaWebhookPayment) error {
// Find payment by reference
payment, err := s.transferStore.GetTransferByReference(ctx, transfer.TxRef)
payment, err := s.transferStore.GetTransferByReference(ctx, req.TxRef)
if err != nil {
return domain.ErrPaymentNotFound
}
@ -159,7 +187,7 @@ func (s *Service) ProcessVerifyDepositWebhook(ctx context.Context, transfer doma
// }
// If payment is completed, credit user's wallet
if transfer.Status == domain.PaymentStatusSuccessful {
if req.Status == string(domain.PaymentStatusSuccessful) {
if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil {
return fmt.Errorf("failed to update is payment verified value: %w", err)
@ -171,7 +199,7 @@ func (s *Service) ProcessVerifyDepositWebhook(ctx context.Context, transfer doma
if _, err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID.Value, payment.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{
ReferenceNumber: domain.ValidString{
Value: transfer.TxRef,
Value: req.TxRef,
},
}, fmt.Sprintf("Added %v to wallet using Chapa", payment.Amount)); err != nil {
return fmt.Errorf("failed to credit user wallet: %w", err)
@ -288,7 +316,7 @@ func (s *Service) FetchTransactionEvents(ctx context.Context, refID string) ([]d
func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req domain.ChapaWithdrawalRequest) (*domain.Transfer, error) {
// Parse and validate amount
amount, err := strconv.ParseInt(req.Amount, 10, 64)
amount, err := strconv.ParseFloat(req.Amount, 64)
if err != nil || amount <= 0 {
return nil, domain.ErrInvalidWithdrawalAmount
}
@ -319,7 +347,7 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma
reference := uuid.New().String()
createTransfer := domain.CreateTransfer{
Message: fmt.Sprintf("Withdrawing %d into wallet using chapa. Reference Number %s", amount, reference),
Message: fmt.Sprintf("Withdrawing %f into wallet using chapa. Reference Number %s", amount, reference),
Amount: domain.Currency(amount),
Type: domain.WITHDRAW,
SenderWalletID: domain.ValidInt64{
@ -341,40 +369,49 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma
transferReq := domain.ChapaWithdrawalRequest{
AccountName: req.AccountName,
AccountNumber: req.AccountNumber,
Amount: fmt.Sprintf("%d", amount),
Amount: fmt.Sprintf("%f", amount),
Currency: req.Currency,
Reference: reference,
// BeneficiaryName: fmt.Sprintf("%s %s", user.FirstName, user.LastName),
BankCode: req.BankCode,
}
success, err := s.chapaClient.InitiateTransfer(ctx, transferReq)
if err != nil {
_ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed))
return nil, fmt.Errorf("failed to initiate transfer: %w", err)
}
if !success {
_ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed))
return nil, errors.New("chapa rejected the transfer request")
}
// Update withdrawal status to processing
if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusProcessing)); err != nil {
return nil, fmt.Errorf("failed to update withdrawal status: %w", err)
}
// Deduct from wallet (or wait for webhook confirmation depending on your flow)
newBalance := float64(wallet.RegularBalance) - float64(amount)
if err := s.walletStore.UpdateBalance(ctx, wallet.RegularID, domain.Currency(newBalance)); err != nil {
return nil, fmt.Errorf("failed to update wallet balance: %w", err)
}
success, err := s.chapaClient.InitializeTransfer(ctx, transferReq)
if err != nil {
_ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed))
newBalance := float64(wallet.RegularBalance) + float64(amount)
if err := s.walletStore.UpdateBalance(ctx, wallet.RegularID, domain.Currency(newBalance)); err != nil {
return nil, fmt.Errorf("failed to update wallet balance: %w", err)
}
return nil, fmt.Errorf("failed to initiate transfer: %w", err)
}
if !success {
_ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed))
newBalance := float64(wallet.RegularBalance) + float64(amount)
if err := s.walletStore.UpdateBalance(ctx, wallet.RegularID, domain.Currency(newBalance)); err != nil {
return nil, fmt.Errorf("failed to update wallet balance: %w", err)
}
return nil, errors.New("chapa rejected the transfer request")
}
// Update withdrawal status to processing
// if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusProcessing)); err != nil {
// return nil, fmt.Errorf("failed to update withdrawal status: %w", err)
// }
// Deduct from wallet (or wait for webhook confirmation depending on your flow)
return &transfer, nil
}
func (s *Service) ProcessVerifyWithdrawWebhook(ctx context.Context, payment domain.ChapaWebHookPayment) error {
func (s *Service) ProcessVerifyWithdrawWebhook(ctx context.Context, req domain.ChapaWebhookTransfer) error {
// Find payment by reference
transfer, err := s.transferStore.GetTransferByReference(ctx, payment.Reference)
transfer, err := s.transferStore.GetTransferByReference(ctx, req.Reference)
if err != nil {
return domain.ErrPaymentNotFound
}
@ -395,7 +432,7 @@ func (s *Service) ProcessVerifyWithdrawWebhook(ctx context.Context, payment doma
// verified = true
// }
if payment.Status == string(domain.PaymentStatusSuccessful) {
if req.Status == string(domain.PaymentStatusSuccessful) {
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil {
return fmt.Errorf("failed to update payment status: %w", err)
} // If payment is completed, credit user's walle
@ -420,15 +457,7 @@ func (s *Service) GetPaymentReceiptURL(refId string) (string, error) {
return receiptURL, nil
}
func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.Bank, error) {
banks, err := s.chapaClient.FetchSupportedBanks(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch banks: %w", err)
}
return banks, nil
}
func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (any, error) {
// Lookup transfer by reference
transfer, err := s.transferStore.GetTransferByReference(ctx, txRef)
if err != nil {
@ -437,7 +466,7 @@ func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (*domain.Cha
// If already verified, just return a completed response
if transfer.Verified {
return &domain.ChapaVerificationResponse{}, errors.New("transfer already verified")
return map[string]any{}, errors.New("transfer already verified")
}
// Validate sender wallet
@ -445,12 +474,12 @@ func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (*domain.Cha
return nil, fmt.Errorf("invalid sender wallet ID: %v", transfer.SenderWalletID)
}
var verification *domain.ChapaVerificationResponse
var verification any
switch strings.ToLower(string(transfer.Type)) {
case string(domain.DEPOSIT):
// Verify Chapa payment
verification, err = s.chapaClient.ManualVerifyPayment(ctx, txRef)
verification, err := s.chapaClient.ManualVerifyPayment(ctx, txRef)
if err != nil {
return nil, fmt.Errorf("failed to verify deposit with Chapa: %w", err)
}
@ -481,7 +510,7 @@ func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (*domain.Cha
case string(domain.WITHDRAW):
// Verify Chapa transfer
verification, err = s.chapaClient.ManualVerifyTransfer(ctx, txRef)
verification, err := s.chapaClient.ManualVerifyTransfer(ctx, txRef)
if err != nil {
return nil, fmt.Errorf("failed to verify withdrawal with Chapa: %w", err)
}
@ -516,8 +545,16 @@ func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (*domain.Cha
return verification, nil
}
func (s *Service) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.chapa.co/v1/transfers", nil)
func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.BankData, error) {
banks, err := s.chapaClient.FetchSupportedBanks(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch banks: %w", err)
}
return banks, nil
}
func (s *Service) GetAllTransfers(ctx context.Context) (*domain.ChapaTransfersListResponse, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.cfg.CHAPA_BASE_URL+"/transfers", nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
@ -535,26 +572,22 @@ func (s *Service) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes))
}
var result struct {
Status string `json:"status"`
Message string `json:"message"`
Data []domain.Transfer `json:"data"`
}
var result domain.ChapaTransfersListResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return result.Data, nil
// Return the decoded result directly; no intermediate dynamic map needed
return &result, nil
}
func (s *Service) GetAccountBalance(ctx context.Context, currencyCode string) ([]domain.Balance, error) {
baseURL := "https://api.chapa.co/v1/balances"
URL := s.cfg.CHAPA_BASE_URL + "/balances"
if currencyCode != "" {
baseURL = fmt.Sprintf("%s/%s", baseURL, strings.ToLower(currencyCode))
URL = fmt.Sprintf("%s/%s", URL, strings.ToLower(currencyCode))
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL, nil)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create balance request: %w", err)
}
@ -585,59 +618,103 @@ func (s *Service) GetAccountBalance(ctx context.Context, currencyCode string) ([
return result.Data, nil
}
func (s *Service) InitiateSwap(ctx context.Context, amount float64, from, to string) (*domain.SwapResponse, error) {
if amount < 1 {
return nil, fmt.Errorf("amount must be at least 1 USD")
}
if strings.ToUpper(from) != "USD" || strings.ToUpper(to) != "ETB" {
return nil, fmt.Errorf("only USD to ETB swap is supported")
}
func (s *Service) SwapCurrency(ctx context.Context, reqBody domain.SwapRequest) (*domain.SwapResponse, error) {
URL := s.cfg.CHAPA_BASE_URL + "/swap"
payload := domain.SwapRequest{
Amount: amount,
From: strings.ToUpper(from),
To: strings.ToUpper(to),
}
// Normalize currency codes
reqBody.From = strings.ToUpper(reqBody.From)
reqBody.To = strings.ToUpper(reqBody.To)
// payload := map[string]any{
// "amount": amount,
// "from": strings.ToUpper(from),
// "to": strings.ToUpper(to),
// }
body, err := json.Marshal(payload)
// Marshal request body
body, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("failed to encode swap payload: %w", err)
return nil, fmt.Errorf("failed to marshal swap payload: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.chapa.co/v1/swap", bytes.NewBuffer(body))
// Create HTTP request
req, err := http.NewRequestWithContext(ctx, http.MethodPost, URL, bytes.NewBuffer(body))
if err != nil {
return nil, fmt.Errorf("failed to create swap request: %w", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.cfg.CHAPA_SECRET_KEY))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.cfg.CHAPA_SECRET_KEY))
// Execute request
resp, err := s.chapaClient.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute swap request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
// Handle unexpected status
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes))
}
var result struct {
Message string `json:"message"`
Status string `json:"status"`
Data domain.SwapResponse `json:"data"`
}
// Decode response
var result domain.SwapResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode swap response: %w", err)
}
return &result.Data, nil
return &result, nil
}
// func (s *Service) InitiateSwap(ctx context.Context, amount float64, from, to string) (*domain.SwapResponse, error) {
// if amount < 1 {
// return nil, fmt.Errorf("amount must be at least 1 USD")
// }
// if strings.ToUpper(from) != "USD" || strings.ToUpper(to) != "ETB" {
// return nil, fmt.Errorf("only USD to ETB swap is supported")
// }
// payload := domain.SwapRequest{
// Amount: amount,
// From: strings.ToUpper(from),
// To: strings.ToUpper(to),
// }
// // payload := map[string]any{
// // "amount": amount,
// // "from": strings.ToUpper(from),
// // "to": strings.ToUpper(to),
// // }
// body, err := json.Marshal(payload)
// if err != nil {
// return nil, fmt.Errorf("failed to encode swap payload: %w", err)
// }
// req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.chapa.co/v1/swap", bytes.NewBuffer(body))
// if err != nil {
// return nil, fmt.Errorf("failed to create swap request: %w", err)
// }
// req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.cfg.CHAPA_SECRET_KEY))
// req.Header.Set("Content-Type", "application/json")
// resp, err := s.chapaClient.httpClient.Do(req)
// if err != nil {
// return nil, fmt.Errorf("failed to execute swap request: %w", err)
// }
// defer resp.Body.Close()
// if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
// bodyBytes, _ := io.ReadAll(resp.Body)
// return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes))
// }
// var result struct {
// Message string `json:"message"`
// Status string `json:"status"`
// Data domain.SwapResponse `json:"data"`
// }
// if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
// return nil, fmt.Errorf("failed to decode swap response: %w", err)
// }
// return &result.Data, nil
// }

View File

@ -102,13 +102,13 @@ func (s *AleaPlayService) HandleCallback(ctx context.Context, callback *domain.A
}
// 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)
}
}
// 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
}

View File

@ -15,6 +15,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/google/uuid"
"go.uber.org/zap"
)
@ -167,7 +168,7 @@ func (s *Service) StartGame(ctx context.Context, req domain.GameStartRequest) (*
"playerId": req.PlayerID,
"currency": req.Currency,
"deviceType": req.DeviceType,
"country": "US",
"country": req.Country,
"ip": req.IP,
"brandId": req.BrandID,
}
@ -178,6 +179,21 @@ func (s *Service) StartGame(ctx context.Context, req domain.GameStartRequest) (*
return nil, fmt.Errorf("failed to start game with provider %s: %w", req.ProviderID, err)
}
playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid PlayerID: %w", err)
}
session := &domain.VirtualGameSession{
UserID: playerIDInt64,
GameID: req.GameID,
SessionToken: uuid.NewString(),
}
if err := s.repo.CreateVirtualGameSession(ctx, session); err != nil {
return nil, fmt.Errorf("failed to create virtual game session: %w", err)
}
return &res, nil
}

View File

@ -18,6 +18,15 @@ import (
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/arifpay/checkout [post]
func (h *Handler) CreateCheckoutSessionHandler(c *fiber.Ctx) error {
userId, ok := c.Locals("user_id").(int64)
if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
Error: "missing user id",
Message: "Unauthorized",
})
}
var req domain.CheckoutSessionClientRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
@ -26,7 +35,7 @@ func (h *Handler) CreateCheckoutSessionHandler(c *fiber.Ctx) error {
})
}
data, err := h.arifpaySvc.CreateCheckoutSession(req, true)
data, err := h.arifpaySvc.CreateCheckoutSession(req, true, userId)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Error: err.Error(),
@ -53,7 +62,7 @@ func (h *Handler) CreateCheckoutSessionHandler(c *fiber.Ctx) error {
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/arifpay/checkout/{sessionId}/cancel [post]
// @Router /api/v1/arifpay/checkout/cancel/{sessionId} [post]
func (h *Handler) CancelCheckoutSessionHandler(c *fiber.Ctx) error {
sessionID := c.Params("sessionId")
if sessionID == "" {
@ -103,15 +112,15 @@ func (h *Handler) HandleArifpayC2BWebhook(c *fiber.Ctx) error {
// 🚨 Decide how to get userId:
// If you get it from auth context/middleware, extract it here.
// For now, let's assume userId comes from your auth claims:
userId, ok := c.Locals("user_id").(int64)
if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
Error: "missing user id",
Message: "Unauthorized",
})
}
// userId, ok := c.Locals("user_id").(int64)
// if !ok {
// return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
// Error: "missing user id",
// Message: "Unauthorized",
// })
// }
err := h.arifpaySvc.HandleWebhook(c.Context(), req, userId, true)
err := h.arifpaySvc.ProcessWebhook(c.Context(), req, true)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Error: err.Error(),
@ -150,15 +159,15 @@ func (h *Handler) HandleArifpayB2CWebhook(c *fiber.Ctx) error {
// 🚨 Decide how to get userId:
// If you get it from auth context/middleware, extract it here.
// For now, let's assume userId comes from your auth claims:
userId, ok := c.Locals("user_id").(int64)
if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
Error: "missing user id",
Message: "Unauthorized",
})
}
// userId, ok := c.Locals("user_id").(int64)
// if !ok {
// return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
// Error: "missing user id",
// Message: "Unauthorized",
// })
// }
err := h.arifpaySvc.HandleWebhook(c.Context(), req, userId, false)
err := h.arifpaySvc.ProcessWebhook(c.Context(), req, false)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Error: err.Error(),
@ -253,7 +262,7 @@ func (h *Handler) ArifpayVerifyBySessionIDHandler(c *fiber.Ctx) error {
// @Accept json
// @Produce json
// @Param type query string true "Transfer type (telebirr, cbe, mpesa)"
// @Param request body domain.ArifpayB2CRequest true "Transfer request payload"
// @Param request body domain.CheckoutSessionClientRequest true "Transfer request payload"
// @Success 200 {object} map[string]string "message: transfer executed successfully"
// @Failure 400 {object} map[string]string "error: invalid request or unsupported transfer type"
// @Failure 500 {object} map[string]string "error: internal server error"
@ -275,7 +284,7 @@ func (h *Handler) ExecuteArifpayB2CTransfer(c *fiber.Ctx) error {
})
}
var req domain.ArifpayB2CRequest
var req domain.CheckoutSessionClientRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to process your withdrawal request",

View File

@ -1,7 +1,9 @@
package handlers
import (
"encoding/json"
"fmt"
"strings"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2"
@ -61,69 +63,66 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error {
// WebhookCallback godoc
// @Summary Chapa payment webhook callback (used by Chapa)
// @Description Handles payment notifications from Chapa
// @Description Handles payment and transfer notifications from Chapa
// @Tags Chapa
// @Accept json
// @Produce json
// @Param request body domain.ChapaWebhookPayload true "Webhook payload"
// @Success 200 {object} map[string]interface{}
// @Param request body domain.ChapaWebhookPayment true "Webhook payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/payments/webhook/verify [post]
func (h *Handler) WebhookCallback(c *fiber.Ctx) error {
body := c.Body()
chapaTransactionType := new(domain.ChapaTransactionType)
// Retrieve signature headers
chapaSignature := c.Get("chapa-signature")
xChapaSignature := c.Get("x-chapa-signature")
if parseTypeErr := c.BodyParser(chapaTransactionType); parseTypeErr != nil {
return domain.UnProcessableEntityResponse(c)
// Verify webhook signature
valid, err := h.chapaSvc.VerifyWebhookSignature(c.Context(), body, chapaSignature, xChapaSignature)
if err != nil || !valid {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid Chapa webhook signature",
Error: err.Error(),
})
}
switch chapaTransactionType.Type {
case h.Cfg.CHAPA_PAYMENT_TYPE:
chapaTransferVerificationRequest := new(domain.ChapaPaymentWebhookRequest)
// Try parsing as transfer webhook first
var transfer domain.ChapaWebhookTransfer
if err := json.Unmarshal(body, &transfer); err == nil &&
strings.EqualFold(transfer.Type, "payout") {
if err := c.BodyParser(chapaTransferVerificationRequest); err != nil {
return domain.UnProcessableEntityResponse(c)
}
err := h.chapaSvc.ProcessVerifyDepositWebhook(c.Context(), *chapaTransferVerificationRequest)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to verify Chapa deposit",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
StatusCode: 200,
Message: "Chapa deposit transaction verified successfully",
Data: chapaTransferVerificationRequest,
Success: true,
})
case h.Cfg.CHAPA_TRANSFER_TYPE:
chapaPaymentVerificationRequest := new(domain.ChapaWebHookPayment)
if err := c.BodyParser(chapaPaymentVerificationRequest); err != nil {
return domain.UnProcessableEntityResponse(c)
}
err := h.chapaSvc.ProcessVerifyWithdrawWebhook(c.Context(), *chapaPaymentVerificationRequest)
if err != nil {
if err := h.chapaSvc.ProcessVerifyWithdrawWebhook(c.Context(), transfer); err != nil {
return domain.UnExpectedErrorResponse(c)
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
StatusCode: 200,
Message: "Chapa withdrawal transaction verified successfully",
Data: chapaPaymentVerificationRequest,
Message: "Chapa withdrawal webhook processed successfully",
// Data: transfer,
Success: true,
})
}
// Return a 400 Bad Request if the type does not match any known case
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid Chapa webhook type",
Error: "Unknown transaction type",
// Otherwise, try as payment webhook
var payment domain.ChapaWebhookPayment
if err := json.Unmarshal(body, &payment); err != nil {
return domain.UnProcessableEntityResponse(c)
}
if err := h.chapaSvc.ProcessVerifyDepositWebhook(c.Context(), payment); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to verify Chapa deposit",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
StatusCode: 200,
Message: "Chapa deposit webhook processed successfully",
// Data: payment,
Success: true,
})
}
@ -248,7 +247,7 @@ func (h *Handler) GetTransactionEvents(c *fiber.Ctx) error {
// @Accept json
// @Produce json
// @Param tx_ref path string true "Transaction Reference"
// @Success 200 {object} domain.ChapaVerificationResponse
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/transaction/manual/verify/{tx_ref} [get]
@ -311,7 +310,7 @@ func (h *Handler) GetSupportedBanks(c *fiber.Ctx) error {
// @Produce json
// @Security ApiKeyAuth
// @Param request body domain.ChapaWithdrawalRequest true "Withdrawal request details"
// @Success 201 {object} domain.Response "Chapa withdrawal process initiated successfully"
// @Success 200 {object} domain.Response "Chapa withdrawal process initiated successfully"
// @Failure 400 {object} domain.ErrorResponse "Invalid request body"
// @Failure 401 {object} domain.ErrorResponse "Unauthorized"
// @Failure 422 {object} domain.ErrorResponse "Unprocessable entity"
@ -336,9 +335,9 @@ func (h *Handler) InitiateWithdrawal(c *fiber.Ctx) error {
})
}
return c.Status(fiber.StatusCreated).JSON(domain.Response{
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa withdrawal process initiated successfully",
StatusCode: 201,
StatusCode: 200,
Success: true,
Data: withdrawal,
})
@ -430,44 +429,56 @@ func (h *Handler) GetAccountBalance(c *fiber.Ctx) error {
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa account balance retrieved successfully",
Message: "Chapa account balances retrieved successfully",
Data: balances,
StatusCode: fiber.StatusOK,
Success: true,
})
}
// InitiateSwap godoc
// @Summary Initiate a currency swap
// @Description Perform a USD to ETB currency swap using Chapa's API
// SwapCurrency godoc
// @Summary Swap currency using Chapa API
// @Description Convert an amount from one currency to another using Chapa's currency swap API
// @Tags Chapa
// @Accept json
// @Produce json
// @Param payload body domain.SwapRequest true "Swap Request Payload"
// @Param request body domain.SwapRequest true "Swap request payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/swap [post]
func (h *Handler) InitiateSwap(c *fiber.Ctx) error {
var req domain.SwapRequest
if err := c.BodyParser(&req); err != nil {
func (h *Handler) SwapCurrency(c *fiber.Ctx) error {
var reqBody domain.SwapRequest
// Parse request body
if err := c.BodyParser(&reqBody); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request payload",
Error: err.Error(),
})
}
swapResult, err := h.chapaSvc.InitiateSwap(c.Context(), req.Amount, req.From, req.To)
// Validate input
if reqBody.From == "" || reqBody.To == "" || reqBody.Amount <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Missing or invalid swap parameters",
Error: "from, to, and amount are required fields",
})
}
// Call service
resp, err := h.chapaSvc.SwapCurrency(c.Context(), reqBody)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to initiate currency swap",
Message: "Failed to perform currency swap",
Error: err.Error(),
})
}
// Success response
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Currency swap initiated successfully",
Data: swapResult,
Message: "Currency swapped successfully",
Data: resp,
StatusCode: fiber.StatusOK,
Success: true,
})

View File

@ -135,7 +135,10 @@ func (h *Handler) StartGame(c *fiber.Ctx) error {
req.BrandID = h.Cfg.VeliGames.BrandID
}
useId := c.Locals("user_id")
req.IP = c.IP()
req.PlayerID = useId.(string)
// 1⃣ Call StartGame service
res, err := h.veliVirtualGameSvc.StartGame(context.Background(), req)

View File

@ -150,9 +150,9 @@ func (a *App) initAppRoutes() {
//Arifpay
groupV1.Post("/arifpay/checkout", a.authMiddleware, h.CreateCheckoutSessionHandler)
groupV1.Post("/arifpay/checkout/cancel/:session_id", a.authMiddleware, h.CancelCheckoutSessionHandler)
groupV1.Post("/api/v1/arifpay/c2b-webhook", a.authMiddleware, h.HandleArifpayC2BWebhook)
groupV1.Post("/api/v1/arifpay/b2c-webhook", a.authMiddleware, h.HandleArifpayB2CWebhook)
groupV1.Post("/arifpay/checkout/cancel/:sessionId", a.authMiddleware, h.CancelCheckoutSessionHandler)
groupV1.Post("/api/v1/arifpay/c2b-webhook", h.HandleArifpayC2BWebhook)
groupV1.Post("/api/v1/arifpay/b2c-webhook", h.HandleArifpayB2CWebhook)
groupV1.Post("/arifpay/b2c/transfer", a.authMiddleware, h.ExecuteArifpayB2CTransfer)
groupV1.Post("/arifpay/transaction-id/verify-transaction", a.authMiddleware, h.ArifpayVerifyByTransactionIDHandler)
groupV1.Get("/arifpay/session-id/verify-transaction/:session_id", a.authMiddleware, h.ArifpayVerifyBySessionIDHandler)
@ -381,17 +381,17 @@ func (a *App) initAppRoutes() {
//Chapa Routes
groupV1.Post("/chapa/payments/webhook/verify", h.WebhookCallback)
groupV1.Get("/chapa/transaction/manual/verify/:tx_ref", h.ManualVerifyTransaction)
groupV1.Get("/chapa/transaction/manual/verify/:tx_ref", a.authMiddleware, h.ManualVerifyTransaction)
groupV1.Put("/chapa/transaction/cancel/:tx_ref", a.authMiddleware, h.CancelDeposit)
groupV1.Get("/chapa/transactions", h.FetchAllTransactions)
groupV1.Get("/chapa/transaction/events/:ref_id", h.GetTransactionEvents)
groupV1.Get("/chapa/transactions", a.authMiddleware, h.FetchAllTransactions)
groupV1.Get("/chapa/transaction/events/:ref_id", a.authMiddleware, h.GetTransactionEvents)
groupV1.Post("/chapa/payments/deposit", a.authMiddleware, h.InitiateDeposit)
groupV1.Post("/chapa/payments/withdraw", a.authMiddleware, h.InitiateWithdrawal)
groupV1.Get("/chapa/banks", h.GetSupportedBanks)
groupV1.Get("/chapa/payments/receipt/:chapa_ref", h.GetPaymentReceipt)
groupV1.Get("/chapa/transfers", h.GetAllTransfers)
groupV1.Get("/chapa/balance", h.GetAccountBalance)
groupV1.Post("/chapa/init-swap", h.InitiateSwap)
groupV1.Get("/chapa/payments/receipt/:chapa_ref", a.authMiddleware, h.GetPaymentReceipt)
groupV1.Get("/chapa/transfers", a.authMiddleware, h.GetAllTransfers)
groupV1.Get("/chapa/balance", a.authMiddleware, h.GetAccountBalance)
groupV1.Post("/chapa/swap", a.authMiddleware, h.SwapCurrency)
// Currencies
groupV1.Get("/currencies", h.GetSupportedCurrencies)
@ -409,7 +409,7 @@ func (a *App) initAppRoutes() {
//Veli Virtual Game Routes
groupV1.Post("/veli/providers", h.GetProviders)
groupV1.Post("/veli/games-list", h.GetGamesByProvider)
groupV1.Post("/veli/start-game", h.StartGame)
groupV1.Post("/veli/start-game", a.authMiddleware, h.StartGame)
groupV1.Post("/veli/start-demo-game", h.StartDemoGame)
a.fiber.Post("/balance", h.GetBalance)
groupV1.Post("/veli/gaming-activity", a.authMiddleware, h.GetGamingActivity)