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), user_id BIGINT NOT NULL REFERENCES users(id),
game_id VARCHAR(50) NOT NULL, game_id VARCHAR(50) NOT NULL,
session_token VARCHAR(255) NOT NULL UNIQUE, 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, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_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
); );
CREATE TABLE virtual_game_transactions ( CREATE TABLE virtual_game_transactions (

View File

@ -63,38 +63,38 @@ RETURNING id,
INSERT INTO virtual_game_sessions ( INSERT INTO virtual_game_sessions (
user_id, user_id,
game_id, game_id,
session_token, session_token
currency, )
status, VALUES ($1, $2, $3)
expires_at RETURNING
) id,
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id,
user_id, user_id,
game_id, game_id,
session_token, session_token,
currency,
status,
created_at, created_at,
updated_at, updated_at;
expires_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 -- name: GetVirtualGameSessionByToken :one
SELECT id, SELECT id,
user_id, user_id,
game_id, game_id,
session_token, session_token,
currency,
status,
created_at, created_at,
updated_at, updated_at
expires_at
FROM virtual_game_sessions FROM virtual_game_sessions
WHERE session_token = $1; WHERE session_token = $1;
-- name: UpdateVirtualGameSessionStatus :exec
UPDATE virtual_game_sessions
SET status = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: CreateVirtualGameTransaction :one -- name: CreateVirtualGameTransaction :one
INSERT INTO virtual_game_transactions ( INSERT INTO virtual_game_transactions (
session_id, session_id,

View File

@ -458,7 +458,7 @@ const docTemplate = `{
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "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": { "post": {
"description": "Cancels a payment session using Arifpay before completion.", "description": "Cancels a payment session using Arifpay before completion.",
"consumes": [ "consumes": [
@ -2254,7 +2254,7 @@ const docTemplate = `{
}, },
"/api/v1/chapa/payments/webhook/verify": { "/api/v1/chapa/payments/webhook/verify": {
"post": { "post": {
"description": "Handles payment notifications from Chapa", "description": "Handles payment and transfer notifications from Chapa",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -2272,7 +2272,7 @@ const docTemplate = `{
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/domain.ChapaWebhookPayload" "$ref": "#/definitions/domain.ChapaWebhookPayment"
} }
} }
], ],
@ -2280,8 +2280,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/domain.Response"
"additionalProperties": true
} }
}, },
"400": { "400": {
@ -2329,7 +2328,7 @@ const docTemplate = `{
} }
], ],
"responses": { "responses": {
"201": { "200": {
"description": "Chapa withdrawal process initiated successfully", "description": "Chapa withdrawal process initiated successfully",
"schema": { "schema": {
"$ref": "#/definitions/domain.Response" "$ref": "#/definitions/domain.Response"
@ -2364,7 +2363,7 @@ const docTemplate = `{
}, },
"/api/v1/chapa/swap": { "/api/v1/chapa/swap": {
"post": { "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": [ "consumes": [
"application/json" "application/json"
], ],
@ -2374,11 +2373,11 @@ const docTemplate = `{
"tags": [ "tags": [
"Chapa" "Chapa"
], ],
"summary": "Initiate a currency swap", "summary": "Swap currency using Chapa API",
"parameters": [ "parameters": [
{ {
"description": "Swap Request Payload", "description": "Swap request payload",
"name": "payload", "name": "request",
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
@ -2536,7 +2535,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/domain.ChapaVerificationResponse" "$ref": "#/definitions/domain.Response"
} }
}, },
"400": { "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": { "domain.ArifpayVerifyByTransactionIDRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -10980,17 +10957,28 @@ const docTemplate = `{
} }
} }
}, },
"domain.ChapaVerificationResponse": { "domain.ChapaWebhookCustomization": {
"type": "object", "type": "object",
"properties": { "properties": {
"data": { "description": {
"type": "string"
},
"logo": {
"type": "string"
},
"title": {
"type": "string"
}
}
},
"domain.ChapaWebhookPayment": {
"type": "object", "type": "object",
"properties": { "properties": {
"amount": { "amount": {
"type": "number" "type": "string"
}, },
"charge": { "charge": {
"type": "number" "type": "string"
}, },
"created_at": { "created_at": {
"type": "string" "type": "string"
@ -10999,33 +10987,32 @@ const docTemplate = `{
"type": "string" "type": "string"
}, },
"customization": { "customization": {
"type": "object", "$ref": "#/definitions/domain.ChapaWebhookCustomization"
"properties": {
"description": {
"type": "string"
},
"logo": {},
"title": {
"type": "string"
}
}
}, },
"email": { "email": {
"type": "string" "type": "string"
}, },
"event": {
"type": "string"
},
"first_name": { "first_name": {
"type": "string" "type": "string"
}, },
"last_name": { "last_name": {
"type": "string" "type": "string"
}, },
"meta": {}, "meta": {
"method": { "description": "may vary in structure, so kept flexible"
},
"mobile": {
"type": "string" "type": "string"
}, },
"mode": { "mode": {
"type": "string" "type": "string"
}, },
"payment_method": {
"type": "string"
},
"reference": { "reference": {
"type": "string" "type": "string"
}, },
@ -11043,33 +11030,6 @@ const docTemplate = `{
} }
} }
}, },
"message": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"domain.ChapaWebhookPayload": {
"type": "object",
"properties": {
"amount": {
"type": "integer"
},
"status": {
"description": "Currency string ` + "`" + `json:\"currency\"` + "`" + `",
"allOf": [
{
"$ref": "#/definitions/domain.PaymentStatus"
}
]
},
"trx_ref": {
"type": "string"
}
}
},
"domain.ChapaWithdrawalRequest": { "domain.ChapaWithdrawalRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -12884,21 +12844,6 @@ const docTemplate = `{
"BANK" "BANK"
] ]
}, },
"domain.PaymentStatus": {
"type": "string",
"enum": [
"success",
"pending",
"completed",
"failed"
],
"x-enum-varnames": [
"PaymentStatusSuccessful",
"PaymentStatusPending",
"PaymentStatusCompleted",
"PaymentStatusFailed"
]
},
"domain.PopOKCallback": { "domain.PopOKCallback": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -450,7 +450,7 @@
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "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": { "post": {
"description": "Cancels a payment session using Arifpay before completion.", "description": "Cancels a payment session using Arifpay before completion.",
"consumes": [ "consumes": [
@ -2246,7 +2246,7 @@
}, },
"/api/v1/chapa/payments/webhook/verify": { "/api/v1/chapa/payments/webhook/verify": {
"post": { "post": {
"description": "Handles payment notifications from Chapa", "description": "Handles payment and transfer notifications from Chapa",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -2264,7 +2264,7 @@
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/domain.ChapaWebhookPayload" "$ref": "#/definitions/domain.ChapaWebhookPayment"
} }
} }
], ],
@ -2272,8 +2272,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "object", "$ref": "#/definitions/domain.Response"
"additionalProperties": true
} }
}, },
"400": { "400": {
@ -2321,7 +2320,7 @@
} }
], ],
"responses": { "responses": {
"201": { "200": {
"description": "Chapa withdrawal process initiated successfully", "description": "Chapa withdrawal process initiated successfully",
"schema": { "schema": {
"$ref": "#/definitions/domain.Response" "$ref": "#/definitions/domain.Response"
@ -2356,7 +2355,7 @@
}, },
"/api/v1/chapa/swap": { "/api/v1/chapa/swap": {
"post": { "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": [ "consumes": [
"application/json" "application/json"
], ],
@ -2366,11 +2365,11 @@
"tags": [ "tags": [
"Chapa" "Chapa"
], ],
"summary": "Initiate a currency swap", "summary": "Swap currency using Chapa API",
"parameters": [ "parameters": [
{ {
"description": "Swap Request Payload", "description": "Swap request payload",
"name": "payload", "name": "request",
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
@ -2528,7 +2527,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/domain.ChapaVerificationResponse" "$ref": "#/definitions/domain.Response"
} }
}, },
"400": { "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": { "domain.ArifpayVerifyByTransactionIDRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -10972,17 +10949,28 @@
} }
} }
}, },
"domain.ChapaVerificationResponse": { "domain.ChapaWebhookCustomization": {
"type": "object", "type": "object",
"properties": { "properties": {
"data": { "description": {
"type": "string"
},
"logo": {
"type": "string"
},
"title": {
"type": "string"
}
}
},
"domain.ChapaWebhookPayment": {
"type": "object", "type": "object",
"properties": { "properties": {
"amount": { "amount": {
"type": "number" "type": "string"
}, },
"charge": { "charge": {
"type": "number" "type": "string"
}, },
"created_at": { "created_at": {
"type": "string" "type": "string"
@ -10991,33 +10979,32 @@
"type": "string" "type": "string"
}, },
"customization": { "customization": {
"type": "object", "$ref": "#/definitions/domain.ChapaWebhookCustomization"
"properties": {
"description": {
"type": "string"
},
"logo": {},
"title": {
"type": "string"
}
}
}, },
"email": { "email": {
"type": "string" "type": "string"
}, },
"event": {
"type": "string"
},
"first_name": { "first_name": {
"type": "string" "type": "string"
}, },
"last_name": { "last_name": {
"type": "string" "type": "string"
}, },
"meta": {}, "meta": {
"method": { "description": "may vary in structure, so kept flexible"
},
"mobile": {
"type": "string" "type": "string"
}, },
"mode": { "mode": {
"type": "string" "type": "string"
}, },
"payment_method": {
"type": "string"
},
"reference": { "reference": {
"type": "string" "type": "string"
}, },
@ -11035,33 +11022,6 @@
} }
} }
}, },
"message": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"domain.ChapaWebhookPayload": {
"type": "object",
"properties": {
"amount": {
"type": "integer"
},
"status": {
"description": "Currency string `json:\"currency\"`",
"allOf": [
{
"$ref": "#/definitions/domain.PaymentStatus"
}
]
},
"trx_ref": {
"type": "string"
}
}
},
"domain.ChapaWithdrawalRequest": { "domain.ChapaWithdrawalRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -12876,21 +12836,6 @@
"BANK" "BANK"
] ]
}, },
"domain.PaymentStatus": {
"type": "string",
"enum": [
"success",
"pending",
"completed",
"failed"
],
"x-enum-varnames": [
"PaymentStatusSuccessful",
"PaymentStatusPending",
"PaymentStatusCompleted",
"PaymentStatusFailed"
]
},
"domain.PopOKCallback": { "domain.PopOKCallback": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -76,7 +76,7 @@ type ChapaDepositVerification struct {
Currency string Currency string
} }
type ChapaVerificationResponse struct { type ChapaPaymentVerificationResponse struct {
Message string `json:"message"` Message string `json:"message"`
Status string `json:"status"` Status string `json:"status"`
Data struct { Data struct {
@ -103,6 +103,31 @@ type ChapaVerificationResponse struct {
} `json:"data"` } `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 { type ChapaAllTransactionsResponse struct {
Message string `json:"message"` Message string `json:"message"`
Status string `json:"status"` Status string `json:"status"`
@ -182,6 +207,57 @@ type ChapaCustomer struct {
// BankLogo string `json:"bank_logo"` // URL or base64 // 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 { type BankResponse struct {
Message string `json:"message"` Message string `json:"message"`
Status string `json:"status"` Status string `json:"status"`
@ -246,44 +322,49 @@ type ChapaTransactionType struct {
Type string `json:"type"` Type string `json:"type"`
} }
type ChapaWebHookTransfer struct { type ChapaWebhookTransfer struct {
Event string `json:"event"`
Type string `json:"type"`
AccountName string `json:"account_name"` AccountName string `json:"account_name"`
AccountNumber string `json:"account_number"` AccountNumber string `json:"account_number"`
BankId string `json:"bank_id"` BankID int `json:"bank_id"`
BankName string `json:"bank_name"` BankName string `json:"bank_name"`
Currency string `json:"currency"`
Amount string `json:"amount"` Amount string `json:"amount"`
Type string `json:"type"` Charge string `json:"charge"`
Currency string `json:"currency"`
Status string `json:"status"` Status string `json:"status"`
Reference string `json:"reference"` Reference string `json:"reference"`
TxRef string `json:"tx_ref"`
ChapaReference string `json:"chapa_reference"` ChapaReference string `json:"chapa_reference"`
CreatedAt time.Time `json:"created_at"` BankReference string `json:"bank_reference"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
} }
type ChapaWebHookPayment struct { type ChapaWebhookPayment struct {
Event string `json:"event"` Event string `json:"event"`
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
Email string `json:"email"` Email *string `json:"email,omitempty"`
Mobile interface{} `json:"mobile"` Mobile string `json:"mobile"`
Currency string `json:"currency"` Currency string `json:"currency"`
Amount string `json:"amount"` Amount string `json:"amount"`
Charge string `json:"charge"` Charge string `json:"charge"`
Status string `json:"status"` Status string `json:"status"`
Mode string `json:"mode"` Mode string `json:"mode"`
Reference string `json:"reference"` Reference string `json:"reference"`
CreatedAt time.Time `json:"created_at"` CreatedAt string `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt string `json:"updated_at"`
Type string `json:"type"` Type string `json:"type"`
TxRef string `json:"tx_ref"` TxRef string `json:"tx_ref"`
PaymentMethod string `json:"payment_method"` PaymentMethod string `json:"payment_method"`
Customization struct { Customization ChapaWebhookCustomization `json:"customization"`
Title interface{} `json:"title"` Meta interface{} `json:"meta"` // may vary in structure, so kept flexible
Description interface{} `json:"description"` }
Logo interface{} `json:"logo"`
} `json:"customization"` type ChapaWebhookCustomization struct {
Meta string `json:"meta"` Title *string `json:"title,omitempty"`
Description *string `json:"description,omitempty"`
Logo *string `json:"logo,omitempty"`
} }
type Balance struct { type Balance struct {
@ -298,19 +379,6 @@ type SwapRequest struct {
Amount float64 `json:"amount"` 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 { type ChapaCancelResponse struct {
Message string `json:"message"` Message string `json:"message"`
Status string `json:"status"` Status string `json:"status"`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,9 @@ package chapa
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "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 // InitiateDeposit starts a new deposit process
func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount domain.Currency) (string, error) { func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount domain.Currency) (string, error) {
// Validate amount // Validate amount
@ -88,7 +116,7 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
ReferenceNumber: reference, ReferenceNumber: reference,
// ReceiverWalletID: 1, // ReceiverWalletID: 1,
SenderWalletID: domain.ValidInt64{ SenderWalletID: domain.ValidInt64{
Value: senderWallet.ID, Value: senderWallet.RegularID,
Valid: true, Valid: true,
}, },
Verified: false, Verified: false,
@ -135,9 +163,9 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
return response.CheckoutURL, nil 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 // Find payment by reference
payment, err := s.transferStore.GetTransferByReference(ctx, transfer.TxRef) payment, err := s.transferStore.GetTransferByReference(ctx, req.TxRef)
if err != nil { if err != nil {
return domain.ErrPaymentNotFound 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 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 { if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil {
return fmt.Errorf("failed to update is payment verified value: %w", err) 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{ if _, err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID.Value, payment.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{
ReferenceNumber: domain.ValidString{ ReferenceNumber: domain.ValidString{
Value: transfer.TxRef, Value: req.TxRef,
}, },
}, fmt.Sprintf("Added %v to wallet using Chapa", payment.Amount)); err != nil { }, fmt.Sprintf("Added %v to wallet using Chapa", payment.Amount)); err != nil {
return fmt.Errorf("failed to credit user wallet: %w", err) 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) { func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req domain.ChapaWithdrawalRequest) (*domain.Transfer, error) {
// Parse and validate amount // Parse and validate amount
amount, err := strconv.ParseInt(req.Amount, 10, 64) amount, err := strconv.ParseFloat(req.Amount, 64)
if err != nil || amount <= 0 { if err != nil || amount <= 0 {
return nil, domain.ErrInvalidWithdrawalAmount return nil, domain.ErrInvalidWithdrawalAmount
} }
@ -319,7 +347,7 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma
reference := uuid.New().String() reference := uuid.New().String()
createTransfer := domain.CreateTransfer{ 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), Amount: domain.Currency(amount),
Type: domain.WITHDRAW, Type: domain.WITHDRAW,
SenderWalletID: domain.ValidInt64{ SenderWalletID: domain.ValidInt64{
@ -341,40 +369,49 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma
transferReq := domain.ChapaWithdrawalRequest{ transferReq := domain.ChapaWithdrawalRequest{
AccountName: req.AccountName, AccountName: req.AccountName,
AccountNumber: req.AccountNumber, AccountNumber: req.AccountNumber,
Amount: fmt.Sprintf("%d", amount), Amount: fmt.Sprintf("%f", amount),
Currency: req.Currency, Currency: req.Currency,
Reference: reference, Reference: reference,
// BeneficiaryName: fmt.Sprintf("%s %s", user.FirstName, user.LastName), // BeneficiaryName: fmt.Sprintf("%s %s", user.FirstName, user.LastName),
BankCode: req.BankCode, 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) newBalance := float64(wallet.RegularBalance) - float64(amount)
if err := s.walletStore.UpdateBalance(ctx, wallet.RegularID, domain.Currency(newBalance)); err != nil { 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 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 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 // Find payment by reference
transfer, err := s.transferStore.GetTransferByReference(ctx, payment.Reference) transfer, err := s.transferStore.GetTransferByReference(ctx, req.Reference)
if err != nil { if err != nil {
return domain.ErrPaymentNotFound return domain.ErrPaymentNotFound
} }
@ -395,7 +432,7 @@ func (s *Service) ProcessVerifyWithdrawWebhook(ctx context.Context, payment doma
// verified = true // 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 { if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil {
return fmt.Errorf("failed to update payment status: %w", err) return fmt.Errorf("failed to update payment status: %w", err)
} // If payment is completed, credit user's walle } // If payment is completed, credit user's walle
@ -420,15 +457,7 @@ func (s *Service) GetPaymentReceiptURL(refId string) (string, error) {
return receiptURL, nil return receiptURL, nil
} }
func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.Bank, error) { func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (any, 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) {
// Lookup transfer by reference // Lookup transfer by reference
transfer, err := s.transferStore.GetTransferByReference(ctx, txRef) transfer, err := s.transferStore.GetTransferByReference(ctx, txRef)
if err != nil { 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 already verified, just return a completed response
if transfer.Verified { if transfer.Verified {
return &domain.ChapaVerificationResponse{}, errors.New("transfer already verified") return map[string]any{}, errors.New("transfer already verified")
} }
// Validate sender wallet // 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) return nil, fmt.Errorf("invalid sender wallet ID: %v", transfer.SenderWalletID)
} }
var verification *domain.ChapaVerificationResponse var verification any
switch strings.ToLower(string(transfer.Type)) { switch strings.ToLower(string(transfer.Type)) {
case string(domain.DEPOSIT): case string(domain.DEPOSIT):
// Verify Chapa payment // Verify Chapa payment
verification, err = s.chapaClient.ManualVerifyPayment(ctx, txRef) verification, err := s.chapaClient.ManualVerifyPayment(ctx, txRef)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to verify deposit with Chapa: %w", err) 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): case string(domain.WITHDRAW):
// Verify Chapa transfer // Verify Chapa transfer
verification, err = s.chapaClient.ManualVerifyTransfer(ctx, txRef) verification, err := s.chapaClient.ManualVerifyTransfer(ctx, txRef)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to verify withdrawal with Chapa: %w", err) 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 return verification, nil
} }
func (s *Service) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error) { func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.BankData, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.chapa.co/v1/transfers", nil) 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 { if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err) 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)) return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes))
} }
var result struct { var result domain.ChapaTransfersListResponse
Status string `json:"status"`
Message string `json:"message"`
Data []domain.Transfer `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err) 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) { 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 != "" { 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 { if err != nil {
return nil, fmt.Errorf("failed to create balance request: %w", err) 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 return result.Data, nil
} }
func (s *Service) InitiateSwap(ctx context.Context, amount float64, from, to string) (*domain.SwapResponse, error) { func (s *Service) SwapCurrency(ctx context.Context, reqBody domain.SwapRequest) (*domain.SwapResponse, error) {
if amount < 1 { URL := s.cfg.CHAPA_BASE_URL + "/swap"
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{ // Normalize currency codes
Amount: amount, reqBody.From = strings.ToUpper(reqBody.From)
From: strings.ToUpper(from), reqBody.To = strings.ToUpper(reqBody.To)
To: strings.ToUpper(to),
}
// payload := map[string]any{ // Marshal request body
// "amount": amount, body, err := json.Marshal(reqBody)
// "from": strings.ToUpper(from),
// "to": strings.ToUpper(to),
// }
body, err := json.Marshal(payload)
if err != nil { 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 { if err != nil {
return nil, fmt.Errorf("failed to create swap request: %w", err) 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("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) resp, err := s.chapaClient.httpClient.Do(req)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to execute swap request: %w", err) return nil, fmt.Errorf("failed to execute swap request: %w", err)
} }
defer resp.Body.Close() 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) bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes)) return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes))
} }
var result struct { // Decode response
Message string `json:"message"` var result domain.SwapResponse
Status string `json:"status"`
Data domain.SwapResponse `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode swap response: %w", err) 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 // Update session status using the proper repository method
if callback.Type == "SESSION_END" { // if callback.Type == "SESSION_END" {
if err := s.repo.UpdateVirtualGameSessionStatus(ctx, session.ID, "COMPLETED"); err != nil { // if err := s.repo.UpdateVirtualGameSessionStatus(ctx, session.ID, "COMPLETED"); err != nil {
s.logger.Error("failed to update session status", // s.logger.Error("failed to update session status",
"sessionID", session.ID, // "sessionID", session.ID,
"error", err) // "error", err)
} // }
} // }
return nil return nil
} }

View File

@ -15,6 +15,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/google/uuid"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -167,7 +168,7 @@ func (s *Service) StartGame(ctx context.Context, req domain.GameStartRequest) (*
"playerId": req.PlayerID, "playerId": req.PlayerID,
"currency": req.Currency, "currency": req.Currency,
"deviceType": req.DeviceType, "deviceType": req.DeviceType,
"country": "US", "country": req.Country,
"ip": req.IP, "ip": req.IP,
"brandId": req.BrandID, "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) 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 return &res, nil
} }

View File

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

View File

@ -1,7 +1,9 @@
package handlers package handlers
import ( import (
"encoding/json"
"fmt" "fmt"
"strings"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@ -61,33 +63,55 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error {
// WebhookCallback godoc // WebhookCallback godoc
// @Summary Chapa payment webhook callback (used by Chapa) // @Summary Chapa payment webhook callback (used by Chapa)
// @Description Handles payment notifications from Chapa // @Description Handles payment and transfer notifications from Chapa
// @Tags Chapa // @Tags Chapa
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param request body domain.ChapaWebhookPayload true "Webhook payload" // @Param request body domain.ChapaWebhookPayment true "Webhook payload"
// @Success 200 {object} map[string]interface{} // @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse // @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/payments/webhook/verify [post] // @Router /api/v1/chapa/payments/webhook/verify [post]
func (h *Handler) WebhookCallback(c *fiber.Ctx) error { 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 { // 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(),
})
}
// 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 := 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 webhook processed successfully",
// Data: transfer,
Success: true,
})
}
// Otherwise, try as payment webhook
var payment domain.ChapaWebhookPayment
if err := json.Unmarshal(body, &payment); err != nil {
return domain.UnProcessableEntityResponse(c) return domain.UnProcessableEntityResponse(c)
} }
switch chapaTransactionType.Type { if err := h.chapaSvc.ProcessVerifyDepositWebhook(c.Context(), payment); err != nil {
case h.Cfg.CHAPA_PAYMENT_TYPE:
chapaTransferVerificationRequest := new(domain.ChapaPaymentWebhookRequest)
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{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to verify Chapa deposit", Message: "Failed to verify Chapa deposit",
Error: err.Error(), Error: err.Error(),
@ -96,35 +120,10 @@ func (h *Handler) WebhookCallback(c *fiber.Ctx) error {
return c.Status(fiber.StatusOK).JSON(domain.Response{ return c.Status(fiber.StatusOK).JSON(domain.Response{
StatusCode: 200, StatusCode: 200,
Message: "Chapa deposit transaction verified successfully", Message: "Chapa deposit webhook processed successfully",
Data: chapaTransferVerificationRequest, // Data: payment,
Success: true, 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 {
return domain.UnExpectedErrorResponse(c)
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
StatusCode: 200,
Message: "Chapa withdrawal transaction verified successfully",
Data: chapaPaymentVerificationRequest,
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",
})
} }
// CancelDeposit godoc // CancelDeposit godoc
@ -248,7 +247,7 @@ func (h *Handler) GetTransactionEvents(c *fiber.Ctx) error {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param tx_ref path string true "Transaction Reference" // @Param tx_ref path string true "Transaction Reference"
// @Success 200 {object} domain.ChapaVerificationResponse // @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse // @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/transaction/manual/verify/{tx_ref} [get] // @Router /api/v1/chapa/transaction/manual/verify/{tx_ref} [get]
@ -311,7 +310,7 @@ func (h *Handler) GetSupportedBanks(c *fiber.Ctx) error {
// @Produce json // @Produce json
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Param request body domain.ChapaWithdrawalRequest true "Withdrawal request details" // @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 400 {object} domain.ErrorResponse "Invalid request body"
// @Failure 401 {object} domain.ErrorResponse "Unauthorized" // @Failure 401 {object} domain.ErrorResponse "Unauthorized"
// @Failure 422 {object} domain.ErrorResponse "Unprocessable entity" // @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", Message: "Chapa withdrawal process initiated successfully",
StatusCode: 201, StatusCode: 200,
Success: true, Success: true,
Data: withdrawal, Data: withdrawal,
}) })
@ -430,44 +429,56 @@ func (h *Handler) GetAccountBalance(c *fiber.Ctx) error {
} }
return c.Status(fiber.StatusOK).JSON(domain.Response{ return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa account balance retrieved successfully", Message: "Chapa account balances retrieved successfully",
Data: balances, Data: balances,
StatusCode: fiber.StatusOK, StatusCode: fiber.StatusOK,
Success: true, Success: true,
}) })
} }
// InitiateSwap godoc // SwapCurrency godoc
// @Summary Initiate a currency swap // @Summary Swap currency using Chapa API
// @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
// @Tags Chapa // @Tags Chapa
// @Accept json // @Accept json
// @Produce 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 // @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse // @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/swap [post] // @Router /api/v1/chapa/swap [post]
func (h *Handler) InitiateSwap(c *fiber.Ctx) error { func (h *Handler) SwapCurrency(c *fiber.Ctx) error {
var req domain.SwapRequest var reqBody domain.SwapRequest
if err := c.BodyParser(&req); err != nil {
// Parse request body
if err := c.BodyParser(&reqBody); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request payload", Message: "Invalid request payload",
Error: err.Error(), 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 { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to initiate currency swap", Message: "Failed to perform currency swap",
Error: err.Error(), Error: err.Error(),
}) })
} }
// Success response
return c.Status(fiber.StatusOK).JSON(domain.Response{ return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Currency swap initiated successfully", Message: "Currency swapped successfully",
Data: swapResult, Data: resp,
StatusCode: fiber.StatusOK, StatusCode: fiber.StatusOK,
Success: true, Success: true,
}) })

View File

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

View File

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