wallet security+log file

This commit is contained in:
Yared Yemane 2025-06-02 17:22:07 +03:00
parent 49d9dafccb
commit f2ec267347
16 changed files with 617 additions and 1219 deletions

View File

@ -297,6 +297,7 @@ ADD CONSTRAINT fk_branch_operations_operations FOREIGN KEY (operation_id) REFERE
ALTER TABLE branch_cashiers
ADD CONSTRAINT fk_branch_cashiers_users FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
ADD CONSTRAINT fk_branch_cashiers_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE;
ALTER TABLE companies
ADD CONSTRAINT fk_companies_admin FOREIGN KEY (admin_id) REFERENCES users(id),
ADD CONSTRAINT fk_companies_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE;

View File

@ -306,7 +306,6 @@ const docTemplate = `{
},
"/api/v1/chapa/banks": {
"get": {
"description": "Fetch all supported banks from Chapa",
"consumes": [
"application/json"
],
@ -316,46 +315,42 @@ const docTemplate = `{
"tags": [
"Chapa"
],
"summary": "Get list of banks",
"summary": "fetches chapa supported banks",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.ChapaSupportedBanksResponse"
"$ref": "#/definitions/domain.ChapaSupportedBanksResponseWrapper"
}
}
}
}
},
"/api/v1/chapa/payments/callback": {
"post": {
"description": "Endpoint to receive webhook payloads from Chapa",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Receive Chapa webhook",
"parameters": [
{
"description": "Webhook Payload (dynamic)",
"name": "payload",
"in": "body",
"required": true,
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object"
"$ref": "#/definitions/domain.Response"
}
}
],
"responses": {
"200": {
"description": "ok",
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "string"
"$ref": "#/definitions/domain.Response"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"422": {
"description": "Unprocessable Entity",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.Response"
}
}
}
@ -363,11 +358,6 @@ const docTemplate = `{
},
"/api/v1/chapa/payments/deposit": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Deposits money into user wallet from user account using Chapa",
"consumes": [
"application/json"
@ -418,40 +408,6 @@ const docTemplate = `{
}
}
},
"/api/v1/chapa/payments/initialize": {
"post": {
"description": "Initiate a payment through Chapa",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Initialize a payment transaction",
"parameters": [
{
"description": "Payment initialization request",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.InitPaymentRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.InitPaymentResponse"
}
}
}
}
},
"/api/v1/chapa/payments/verify": {
"post": {
"consumes": [
@ -485,38 +441,6 @@ const docTemplate = `{
}
}
},
"/api/v1/chapa/payments/verify/{tx_ref}": {
"get": {
"description": "Verify the transaction status from Chapa using tx_ref",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Verify a payment transaction",
"parameters": [
{
"type": "string",
"description": "Transaction Reference",
"name": "tx_ref",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.VerifyTransactionResponse"
}
}
}
}
},
"/api/v1/chapa/payments/withdraw": {
"post": {
"description": "Initiates a withdrawal transaction using Chapa for the authenticated user.",
@ -587,72 +511,6 @@ const docTemplate = `{
}
}
},
"/api/v1/chapa/transfers": {
"post": {
"description": "Initiate a transfer request via Chapa",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Create a money transfer",
"parameters": [
{
"description": "Transfer request body",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.TransferRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.CreateTransferResponse"
}
}
}
}
},
"/api/v1/chapa/transfers/verify/{transfer_ref}": {
"get": {
"description": "Check the status of a money transfer via reference",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Verify a transfer",
"parameters": [
{
"type": "string",
"description": "Transfer Reference",
"name": "transfer_ref",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.VerifyTransferResponse"
}
}
}
}
},
"/api/v1/virtual-games/recommendations/{userID}": {
"get": {
"description": "Returns a list of recommended virtual games for a specific user",
@ -4666,17 +4524,18 @@ const docTemplate = `{
}
}
},
"domain.ChapaSupportedBanksResponse": {
"domain.ChapaSupportedBanksResponseWrapper": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.ChapaSupportedBank"
}
},
"data": {},
"message": {
"type": "string"
},
"status_code": {
"type": "integer"
},
"success": {
"type": "boolean"
}
}
},
@ -4770,76 +4629,6 @@ const docTemplate = `{
}
}
},
"domain.CreateTransferResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/domain.TransferData"
},
"message": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"domain.InitPaymentData": {
"type": "object",
"properties": {
"checkout_url": {
"type": "string"
},
"tx_ref": {
"type": "string"
}
}
},
"domain.InitPaymentRequest": {
"type": "object",
"properties": {
"amount": {
"type": "integer"
},
"callback_url": {
"type": "string"
},
"currency": {
"type": "string"
},
"email": {
"type": "string"
},
"first_name": {
"type": "string"
},
"last_name": {
"type": "string"
},
"return_url": {
"type": "string"
},
"tx_ref": {
"type": "string"
}
}
},
"domain.InitPaymentResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/domain.InitPaymentData"
},
"message": {
"description": "e.g., \"Payment initialized\"",
"type": "string"
},
"status": {
"description": "\"success\"",
"type": "string"
}
}
},
"domain.Odd": {
"type": "object",
"properties": {
@ -5167,86 +4956,6 @@ const docTemplate = `{
}
}
},
"domain.TransactionData": {
"type": "object",
"properties": {
"amount": {
"type": "string"
},
"currency": {
"type": "string"
},
"email": {
"type": "string"
},
"status": {
"type": "string"
},
"tx_ref": {
"type": "string"
}
}
},
"domain.TransferData": {
"type": "object",
"properties": {
"amount": {
"type": "string"
},
"currency": {
"type": "string"
},
"reference": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"domain.TransferRequest": {
"type": "object",
"properties": {
"account_number": {
"type": "string"
},
"amount": {
"type": "string"
},
"bank_code": {
"type": "string"
},
"currency": {
"type": "string"
},
"reason": {
"type": "string"
},
"recipient_name": {
"type": "string"
},
"reference": {
"type": "string"
}
}
},
"domain.TransferVerificationData": {
"type": "object",
"properties": {
"account_name": {
"type": "string"
},
"bank_code": {
"type": "string"
},
"reference": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"domain.UpcomingEvent": {
"type": "object",
"properties": {
@ -5353,34 +5062,6 @@ const docTemplate = `{
}
}
},
"domain.VerifyTransactionResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/domain.TransactionData"
},
"message": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"domain.VerifyTransferResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/domain.TransferVerificationData"
},
"message": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"domain.VirtualGame": {
"type": "object",
"properties": {

View File

@ -298,7 +298,6 @@
},
"/api/v1/chapa/banks": {
"get": {
"description": "Fetch all supported banks from Chapa",
"consumes": [
"application/json"
],
@ -308,46 +307,42 @@
"tags": [
"Chapa"
],
"summary": "Get list of banks",
"summary": "fetches chapa supported banks",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.ChapaSupportedBanksResponse"
"$ref": "#/definitions/domain.ChapaSupportedBanksResponseWrapper"
}
}
}
}
},
"/api/v1/chapa/payments/callback": {
"post": {
"description": "Endpoint to receive webhook payloads from Chapa",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Receive Chapa webhook",
"parameters": [
{
"description": "Webhook Payload (dynamic)",
"name": "payload",
"in": "body",
"required": true,
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object"
"$ref": "#/definitions/domain.Response"
}
}
],
"responses": {
"200": {
"description": "ok",
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "string"
"$ref": "#/definitions/domain.Response"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"422": {
"description": "Unprocessable Entity",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.Response"
}
}
}
@ -355,11 +350,6 @@
},
"/api/v1/chapa/payments/deposit": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Deposits money into user wallet from user account using Chapa",
"consumes": [
"application/json"
@ -410,40 +400,6 @@
}
}
},
"/api/v1/chapa/payments/initialize": {
"post": {
"description": "Initiate a payment through Chapa",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Initialize a payment transaction",
"parameters": [
{
"description": "Payment initialization request",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.InitPaymentRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.InitPaymentResponse"
}
}
}
}
},
"/api/v1/chapa/payments/verify": {
"post": {
"consumes": [
@ -477,38 +433,6 @@
}
}
},
"/api/v1/chapa/payments/verify/{tx_ref}": {
"get": {
"description": "Verify the transaction status from Chapa using tx_ref",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Verify a payment transaction",
"parameters": [
{
"type": "string",
"description": "Transaction Reference",
"name": "tx_ref",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.VerifyTransactionResponse"
}
}
}
}
},
"/api/v1/chapa/payments/withdraw": {
"post": {
"description": "Initiates a withdrawal transaction using Chapa for the authenticated user.",
@ -579,72 +503,6 @@
}
}
},
"/api/v1/chapa/transfers": {
"post": {
"description": "Initiate a transfer request via Chapa",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Create a money transfer",
"parameters": [
{
"description": "Transfer request body",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.TransferRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.CreateTransferResponse"
}
}
}
}
},
"/api/v1/chapa/transfers/verify/{transfer_ref}": {
"get": {
"description": "Check the status of a money transfer via reference",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Verify a transfer",
"parameters": [
{
"type": "string",
"description": "Transfer Reference",
"name": "transfer_ref",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.VerifyTransferResponse"
}
}
}
}
},
"/api/v1/virtual-games/recommendations/{userID}": {
"get": {
"description": "Returns a list of recommended virtual games for a specific user",
@ -4658,17 +4516,18 @@
}
}
},
"domain.ChapaSupportedBanksResponse": {
"domain.ChapaSupportedBanksResponseWrapper": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.ChapaSupportedBank"
}
},
"data": {},
"message": {
"type": "string"
},
"status_code": {
"type": "integer"
},
"success": {
"type": "boolean"
}
}
},
@ -4762,76 +4621,6 @@
}
}
},
"domain.CreateTransferResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/domain.TransferData"
},
"message": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"domain.InitPaymentData": {
"type": "object",
"properties": {
"checkout_url": {
"type": "string"
},
"tx_ref": {
"type": "string"
}
}
},
"domain.InitPaymentRequest": {
"type": "object",
"properties": {
"amount": {
"type": "integer"
},
"callback_url": {
"type": "string"
},
"currency": {
"type": "string"
},
"email": {
"type": "string"
},
"first_name": {
"type": "string"
},
"last_name": {
"type": "string"
},
"return_url": {
"type": "string"
},
"tx_ref": {
"type": "string"
}
}
},
"domain.InitPaymentResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/domain.InitPaymentData"
},
"message": {
"description": "e.g., \"Payment initialized\"",
"type": "string"
},
"status": {
"description": "\"success\"",
"type": "string"
}
}
},
"domain.Odd": {
"type": "object",
"properties": {
@ -5159,86 +4948,6 @@
}
}
},
"domain.TransactionData": {
"type": "object",
"properties": {
"amount": {
"type": "string"
},
"currency": {
"type": "string"
},
"email": {
"type": "string"
},
"status": {
"type": "string"
},
"tx_ref": {
"type": "string"
}
}
},
"domain.TransferData": {
"type": "object",
"properties": {
"amount": {
"type": "string"
},
"currency": {
"type": "string"
},
"reference": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"domain.TransferRequest": {
"type": "object",
"properties": {
"account_number": {
"type": "string"
},
"amount": {
"type": "string"
},
"bank_code": {
"type": "string"
},
"currency": {
"type": "string"
},
"reason": {
"type": "string"
},
"recipient_name": {
"type": "string"
},
"reference": {
"type": "string"
}
}
},
"domain.TransferVerificationData": {
"type": "object",
"properties": {
"account_name": {
"type": "string"
},
"bank_code": {
"type": "string"
},
"reference": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"domain.UpcomingEvent": {
"type": "object",
"properties": {
@ -5345,34 +5054,6 @@
}
}
},
"domain.VerifyTransactionResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/domain.TransactionData"
},
"message": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"domain.VerifyTransferResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/domain.TransferVerificationData"
},
"message": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"domain.VirtualGame": {
"type": "object",
"properties": {

View File

@ -185,14 +185,15 @@ definitions:
updated_at:
type: string
type: object
domain.ChapaSupportedBanksResponse:
domain.ChapaSupportedBanksResponseWrapper:
properties:
data:
items:
$ref: '#/definitions/domain.ChapaSupportedBank'
type: array
data: {}
message:
type: string
status_code:
type: integer
success:
type: boolean
type: object
domain.ChapaTransactionType:
properties:
@ -254,52 +255,6 @@ definitions:
- $ref: '#/definitions/domain.OutcomeStatus'
example: 1
type: object
domain.CreateTransferResponse:
properties:
data:
$ref: '#/definitions/domain.TransferData'
message:
type: string
status:
type: string
type: object
domain.InitPaymentData:
properties:
checkout_url:
type: string
tx_ref:
type: string
type: object
domain.InitPaymentRequest:
properties:
amount:
type: integer
callback_url:
type: string
currency:
type: string
email:
type: string
first_name:
type: string
last_name:
type: string
return_url:
type: string
tx_ref:
type: string
type: object
domain.InitPaymentResponse:
properties:
data:
$ref: '#/definitions/domain.InitPaymentData'
message:
description: e.g., "Payment initialized"
type: string
status:
description: '"success"'
type: string
type: object
domain.Odd:
properties:
category:
@ -529,58 +484,6 @@ definitions:
example: 1
type: integer
type: object
domain.TransactionData:
properties:
amount:
type: string
currency:
type: string
email:
type: string
status:
type: string
tx_ref:
type: string
type: object
domain.TransferData:
properties:
amount:
type: string
currency:
type: string
reference:
type: string
status:
type: string
type: object
domain.TransferRequest:
properties:
account_number:
type: string
amount:
type: string
bank_code:
type: string
currency:
type: string
reason:
type: string
recipient_name:
type: string
reference:
type: string
type: object
domain.TransferVerificationData:
properties:
account_name:
type: string
bank_code:
type: string
reference:
type: string
status:
type: string
type: object
domain.UpcomingEvent:
properties:
awayKitImage:
@ -659,24 +562,6 @@ definitions:
description: Veli's user identifier
type: string
type: object
domain.VerifyTransactionResponse:
properties:
data:
$ref: '#/definitions/domain.TransactionData'
message:
type: string
status:
type: string
type: object
domain.VerifyTransferResponse:
properties:
data:
$ref: '#/definitions/domain.TransferVerificationData'
message:
type: string
status:
type: string
type: object
domain.VirtualGame:
properties:
category:
@ -1752,37 +1637,34 @@ paths:
get:
consumes:
- application/json
description: Fetch all supported banks from Chapa
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.ChapaSupportedBanksResponse'
summary: Get list of banks
tags:
- Chapa
/api/v1/chapa/payments/callback:
post:
consumes:
- application/json
description: Endpoint to receive webhook payloads from Chapa
parameters:
- description: Webhook Payload (dynamic)
in: body
name: payload
required: true
schema:
type: object
produces:
- application/json
responses:
"200":
description: ok
$ref: '#/definitions/domain.ChapaSupportedBanksResponseWrapper'
"400":
description: Bad Request
schema:
type: string
summary: Receive Chapa webhook
$ref: '#/definitions/domain.Response'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/domain.Response'
"404":
description: Not Found
schema:
$ref: '#/definitions/domain.Response'
"422":
description: Unprocessable Entity
schema:
$ref: '#/definitions/domain.Response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.Response'
summary: fetches chapa supported banks
tags:
- Chapa
/api/v1/chapa/payments/deposit:
@ -1816,33 +1698,9 @@ paths:
description: Internal server error
schema:
$ref: '#/definitions/domain.Response'
security:
- ApiKeyAuth: []
summary: Deposit money into user wallet using Chapa
tags:
- Chapa
/api/v1/chapa/payments/initialize:
post:
consumes:
- application/json
description: Initiate a payment through Chapa
parameters:
- description: Payment initialization request
in: body
name: payload
required: true
schema:
$ref: '#/definitions/domain.InitPaymentRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.InitPaymentResponse'
summary: Initialize a payment transaction
tags:
- Chapa
/api/v1/chapa/payments/verify:
post:
consumes:
@ -1864,27 +1722,6 @@ paths:
summary: Verifies Chapa webhook transaction
tags:
- Chapa
/api/v1/chapa/payments/verify/{tx_ref}:
get:
consumes:
- application/json
description: Verify the transaction status from Chapa using tx_ref
parameters:
- description: Transaction Reference
in: path
name: tx_ref
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.VerifyTransactionResponse'
summary: Verify a payment transaction
tags:
- Chapa
/api/v1/chapa/payments/withdraw:
post:
consumes:
@ -1929,49 +1766,6 @@ paths:
summary: Withdraw using Chapa
tags:
- Chapa
/api/v1/chapa/transfers:
post:
consumes:
- application/json
description: Initiate a transfer request via Chapa
parameters:
- description: Transfer request body
in: body
name: payload
required: true
schema:
$ref: '#/definitions/domain.TransferRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.CreateTransferResponse'
summary: Create a money transfer
tags:
- Chapa
/api/v1/chapa/transfers/verify/{transfer_ref}:
get:
consumes:
- application/json
description: Check the status of a money transfer via reference
parameters:
- description: Transfer Reference
in: path
name: transfer_ref
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.VerifyTransferResponse'
summary: Verify a transfer
tags:
- Chapa
/api/v1/virtual-games/recommendations/{userID}:
get:
consumes:

19
go.mod
View File

@ -9,28 +9,28 @@ require (
github.com/gofiber/fiber/v2 v2.52.6
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/jackc/pgx/v5 v5.7.4
github.com/joho/godotenv v1.5.1
github.com/robfig/cron/v3 v3.0.1
github.com/shopspring/decimal v1.4.0
github.com/stretchr/testify v1.10.0
// github.com/stretchr/testify v1.10.0
github.com/swaggo/fiber-swagger v1.3.0
github.com/swaggo/swag v1.16.4
github.com/valyala/fasthttp v1.59.0
golang.org/x/crypto v0.36.0
)
require github.com/gorilla/websocket v1.5.3 // indirect
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
)
require (
// github.com/davecgh/go-spew v1.1.1 // indirect
// github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
// github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
@ -38,7 +38,6 @@ require (
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/gorilla/websocket v1.5.3
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
@ -50,13 +49,13 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/shopspring/decimal v1.4.0
github.com/stretchr/objx v0.5.2 // indirect
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.59.0
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sync v0.12.0 // indirect

2
go.sum
View File

@ -120,6 +120,8 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View File

@ -221,3 +221,8 @@ type ChapaPaymentUrlResponseWrapper struct {
Data ChapaPaymentUrlResponse `json:"data"`
Response
}
type ChapaSupportedBanksResponseWrapper struct {
Data []ChapaSupportedBank `json:"data"`
Response
}

View File

@ -18,13 +18,24 @@ var Environment = map[string]string{
func NewLogger(env string, lvl slog.Level) *slog.Logger {
var logHandler slog.Handler
err := os.MkdirAll("logs", os.ModePerm)
if err != nil {
panic("Failed to create log directory: " + err.Error())
}
file, err := os.OpenFile("logs/app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
panic("Failed to open log file: " + err.Error())
}
switch env {
case "development":
logHandler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
logHandler = slog.NewTextHandler(file, &slog.HandlerOptions{
Level: lvl,
})
default:
logHandler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
logHandler = slog.NewJSONHandler(file, &slog.HandlerOptions{
Level: lvl,
})
}

View File

@ -14,12 +14,14 @@ import (
type ChapaClient interface {
IssuePayment(ctx context.Context, payload domain.ChapaTransferPayload) (bool, error)
InitPayment(ctx context.Context, req domain.InitPaymentRequest) (string, error)
FetchBanks() ([]domain.ChapaSupportedBank, error)
}
type Client struct {
BaseURL string
SecretKey string
HTTPClient *http.Client
UserAgent string
}
func NewClient(baseURL, secretKey string) *Client {
@ -27,6 +29,7 @@ func NewClient(baseURL, secretKey string) *Client {
BaseURL: baseURL,
SecretKey: secretKey,
HTTPClient: http.DefaultClient,
UserAgent: "FortuneBet/1.0",
}
}
@ -96,3 +99,28 @@ func (c *Client) InitPayment(ctx context.Context, req domain.InitPaymentRequest)
return response.Data.CheckoutURL, nil
}
func (c *Client) FetchBanks() ([]domain.ChapaSupportedBank, error) {
req, _ := http.NewRequest("GET", c.BaseURL+"/banks", nil)
req.Header.Set("Authorization", "Bearer "+c.SecretKey)
fmt.Printf("\n\nbase URL is: %s\n\n", c.BaseURL)
res, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var resp struct {
Message string `json:"message"`
Data []domain.ChapaSupportedBank `json:"data"`
}
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return nil, err
}
fmt.Printf("\n\nclient fetched banks: %+v\n\n", resp.Data)
return resp.Data, nil
}

View File

@ -11,4 +11,5 @@ type ChapaPort interface {
HandleChapaPaymentWebhook(ctx context.Context, req domain.ChapaWebHookPayment) error
WithdrawUsingChapa(ctx context.Context, userID int64, req domain.ChapaWithdrawRequest) error
DepositUsingChapa(ctx context.Context, userID int64, req domain.ChapaDepositRequest) (string, error)
GetSupportedBanks() ([]domain.ChapaSupportedBank, error)
}

View File

@ -256,7 +256,7 @@ func (s *Service) DepositUsingChapa(ctx context.Context, userID int64, req domai
return "", err
}
defer tx.Rollback(ctx)
user, err := s.userStore.GetUserByID(ctx, userID)
if err != nil {
return "", err
@ -315,3 +315,37 @@ func (s *Service) DepositUsingChapa(ctx context.Context, userID int64, req domai
return paymentURL, nil
}
func (s *Service) GetSupportedBanks() ([]domain.ChapaSupportedBank, error) {
banks, err := s.chapaClient.FetchBanks()
fmt.Printf("\n\nfetched banks: %+v\n\n", banks)
if err != nil {
return nil, err
}
// Add formatting logic (same as in original controller)
for i := range banks {
if banks[i].IsMobilemoney != nil && *(banks[i].IsMobilemoney) == 1 {
banks[i].AcctNumberRegex = "/^09[0-9]{8}$/"
banks[i].ExampleValue = "0952097177"
} else {
switch banks[i].AcctLength {
case 8:
banks[i].ExampleValue = "16967608"
case 13:
banks[i].ExampleValue = "1000222215735"
case 14:
banks[i].ExampleValue = "01320089280800"
case 16:
banks[i].ExampleValue = "1000222215735123"
}
banks[i].AcctNumberRegex = formatRegex(banks[i].AcctLength)
}
}
return banks, nil
}
func formatRegex(length int) string {
return fmt.Sprintf("/^[0-9]{%d}$/", length)
}

View File

@ -8,7 +8,7 @@ import (
)
// NFL evaluations
func evaluateNFLMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
func EvaluateNFLMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddHeader {
case "1":
if score.Home > score.Away {
@ -25,7 +25,7 @@ func evaluateNFLMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away in
}
}
func evaluateNFLSpread(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
func EvaluateNFLSpread(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
handicap, err := strconv.ParseFloat(outcome.OddHandicap, 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid handicap: %s", outcome.OddHandicap)
@ -56,7 +56,7 @@ func evaluateNFLSpread(outcome domain.BetOutcome, score struct{ Home, Away int }
return domain.OUTCOME_STATUS_VOID, nil
}
func evaluateNFLTotalPoints(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
func EvaluateNFLTotalPoints(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalPoints := float64(score.Home + score.Away)
threshold, err := strconv.ParseFloat(outcome.OddName, 64)
if err != nil {
@ -81,8 +81,8 @@ func evaluateNFLTotalPoints(outcome domain.BetOutcome, score struct{ Home, Away
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
// evaluateRugbyMoneyLine evaluates Rugby money line bets
func evaluateRugbyMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
// EvaluateRugbyMoneyLine Evaluates Rugby money line bets
func EvaluateRugbyMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddHeader {
case "1":
if score.Home > score.Away {
@ -99,8 +99,8 @@ func evaluateRugbyMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away
}
}
// evaluateRugbySpread evaluates Rugby spread bets
func evaluateRugbySpread(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
// EvaluateRugbySpread Evaluates Rugby spread bets
func EvaluateRugbySpread(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
handicap, err := strconv.ParseFloat(outcome.OddHandicap, 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid handicap: %s", outcome.OddHandicap)
@ -131,8 +131,8 @@ func evaluateRugbySpread(outcome domain.BetOutcome, score struct{ Home, Away int
return domain.OUTCOME_STATUS_VOID, nil
}
// evaluateRugbyTotalPoints evaluates Rugby total points bets
func evaluateRugbyTotalPoints(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
// EvaluateRugbyTotalPoints Evaluates Rugby total points bets
func EvaluateRugbyTotalPoints(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalPoints := float64(score.Home + score.Away)
threshold, err := strconv.ParseFloat(outcome.OddName, 64)
if err != nil {
@ -157,8 +157,8 @@ func evaluateRugbyTotalPoints(outcome domain.BetOutcome, score struct{ Home, Awa
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
// evaluateBaseballMoneyLine evaluates Baseball money line bets
func evaluateBaseballMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
// EvaluateBaseballMoneyLine Evaluates Baseball money line bets
func EvaluateBaseballMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddHeader {
case "1":
if score.Home > score.Away {
@ -175,8 +175,8 @@ func evaluateBaseballMoneyLine(outcome domain.BetOutcome, score struct{ Home, Aw
}
}
// evaluateBaseballSpread evaluates Baseball spread bets
func evaluateBaseballSpread(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
// EvaluateBaseballSpread Evaluates Baseball spread bets
func EvaluateBaseballSpread(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
handicap, err := strconv.ParseFloat(outcome.OddHandicap, 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid handicap: %s", outcome.OddHandicap)
@ -207,8 +207,8 @@ func evaluateBaseballSpread(outcome domain.BetOutcome, score struct{ Home, Away
return domain.OUTCOME_STATUS_VOID, nil
}
// evaluateBaseballTotalRuns evaluates Baseball total runs bets
func evaluateBaseballTotalRuns(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
// EvaluateBaseballTotalRuns Evaluates Baseball total runs bets
func EvaluateBaseballTotalRuns(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalRuns := float64(score.Home + score.Away)
threshold, err := strconv.ParseFloat(outcome.OddName, 64)
if err != nil {
@ -233,7 +233,7 @@ func evaluateBaseballTotalRuns(outcome domain.BetOutcome, score struct{ Home, Aw
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
// evaluateBaseballFirstInning evaluates Baseball first inning bets
// EvaluateBaseballFirstInning Evaluates Baseball first inning bets
func EvaluateBaseballFirstInning(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddHeader {
case "1":
@ -256,7 +256,7 @@ func EvaluateBaseballFirstInning(outcome domain.BetOutcome, score struct{ Home,
}
}
// evaluateBaseballFirst5Innings evaluates Baseball first 5 innings bets
// EvaluateBaseballFirst5Innings Evaluates Baseball first 5 innings bets
func EvaluateBaseballFirst5Innings(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddHeader {
case "1":

View File

@ -1,287 +1,288 @@
package handlers
import (
"bytes"
"encoding/json"
// "bytes"
// "encoding/json"
// "fmt"
// "io"
// "net/http"
"fmt"
"io"
"net/http"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
// GetBanks godoc
// @Summary Get list of banks
// @Description Fetch all supported banks from Chapa
// @Tags Chapa
// @Accept json
// @Produce json
// @Success 200 {object} domain.ChapaSupportedBanksResponse
// @Router /api/v1/chapa/banks [get]
func (h *Handler) GetBanks(c *fiber.Ctx) error {
httpReq, err := http.NewRequest("GET", h.Cfg.CHAPA_BASE_URL+"/banks", nil)
// log.Printf("\n\nbase url is: %v\n\n", h.Cfg.CHAPA_BASE_URL)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "Failed to create request", "details": err.Error()})
}
httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
// // GetBanks godoc
// // @Summary Get list of banks
// // @Description Fetch all supported banks from Chapa
// // @Tags Chapa
// // @Accept json
// // @Produce json
// // @Success 200 {object} domain.ChapaSupportedBanksResponse
// // @Router /api/v1/chapa/banks [get]
// func (h *Handler) GetBanks(c *fiber.Ctx) error {
// httpReq, err := http.NewRequest("GET", h.Cfg.CHAPA_BASE_URL+"/banks", nil)
// // log.Printf("\n\nbase url is: %v\n\n", h.Cfg.CHAPA_BASE_URL)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{"error": "Failed to create request", "details": err.Error()})
// }
// httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "Failed to fetch banks", "details": err.Error()})
}
defer resp.Body.Close()
// resp, err := http.DefaultClient.Do(httpReq)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{"error": "Failed to fetch banks", "details": err.Error()})
// }
// defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "Failed to read response", "details": err.Error()})
}
// body, err := io.ReadAll(resp.Body)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{"error": "Failed to read response", "details": err.Error()})
// }
return c.Status(resp.StatusCode).Type("json").Send(body)
}
// return c.Status(resp.StatusCode).Type("json").Send(body)
// }
// InitializePayment godoc
// @Summary Initialize a payment transaction
// @Description Initiate a payment through Chapa
// @Tags Chapa
// @Accept json
// @Produce json
// @Param payload body domain.InitPaymentRequest true "Payment initialization request"
// @Success 200 {object} domain.InitPaymentResponse
// @Router /api/v1/chapa/payments/initialize [post]
func (h *Handler) InitializePayment(c *fiber.Ctx) error {
var req InitPaymentRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request body",
"details": err.Error(),
})
}
// // InitializePayment godoc
// // @Summary Initialize a payment transaction
// // @Description Initiate a payment through Chapa
// // @Tags Chapa
// // @Accept json
// // @Produce json
// // @Param payload body domain.InitPaymentRequest true "Payment initialization request"
// // @Success 200 {object} domain.InitPaymentResponse
// // @Router /api/v1/chapa/payments/initialize [post]
// func (h *Handler) InitializePayment(c *fiber.Ctx) error {
// var req InitPaymentRequest
// if err := c.BodyParser(&req); err != nil {
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
// "error": "Invalid request body",
// "details": err.Error(),
// })
// }
// Generate and assign a unique transaction reference
req.TxRef = uuid.New().String()
// // Generate and assign a unique transaction reference
// req.TxRef = uuid.New().String()
payload, err := json.Marshal(req)
if err != nil {
return c.Status(500).JSON(fiber.Map{
"error": "Failed to serialize request",
"details": err.Error(),
})
}
// payload, err := json.Marshal(req)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{
// "error": "Failed to serialize request",
// "details": err.Error(),
// })
// }
httpReq, err := http.NewRequest("POST", h.Cfg.CHAPA_BASE_URL+"/transaction/initialize", bytes.NewBuffer(payload))
if err != nil {
return c.Status(500).JSON(fiber.Map{
"error": "Failed to create request",
"details": err.Error(),
})
}
httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
httpReq.Header.Set("Content-Type", "application/json")
// httpReq, err := http.NewRequest("POST", h.Cfg.CHAPA_BASE_URL+"/transaction/initialize", bytes.NewBuffer(payload))
// if err != nil {
// return c.Status(500).JSON(fiber.Map{
// "error": "Failed to create request",
// "details": err.Error(),
// })
// }
// httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
// httpReq.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return c.Status(500).JSON(fiber.Map{
"error": "Failed to initialize payment",
"details": err.Error(),
})
}
defer resp.Body.Close()
// resp, err := http.DefaultClient.Do(httpReq)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{
// "error": "Failed to initialize payment",
// "details": err.Error(),
// })
// }
// defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return c.Status(500).JSON(fiber.Map{
"error": "Failed to read response",
"details": err.Error(),
})
}
// body, err := io.ReadAll(resp.Body)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{
// "error": "Failed to read response",
// "details": err.Error(),
// })
// }
return c.Status(resp.StatusCode).Type("json").Send(body)
}
// return c.Status(resp.StatusCode).Type("json").Send(body)
// }
// VerifyTransaction godoc
// @Summary Verify a payment transaction
// @Description Verify the transaction status from Chapa using tx_ref
// @Tags Chapa
// @Accept json
// @Produce json
// @Param tx_ref path string true "Transaction Reference"
// @Success 200 {object} domain.VerifyTransactionResponse
// @Router /api/v1/chapa/payments/verify/{tx_ref} [get]
func (h *Handler) VerifyTransaction(c *fiber.Ctx) error {
txRef := c.Params("tx_ref")
if txRef == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Missing transaction reference",
})
}
// // VerifyTransaction godoc
// // @Summary Verify a payment transaction
// // @Description Verify the transaction status from Chapa using tx_ref
// // @Tags Chapa
// // @Accept json
// // @Produce json
// // @Param tx_ref path string true "Transaction Reference"
// // @Success 200 {object} domain.VerifyTransactionResponse
// // @Router /api/v1/chapa/payments/verify/{tx_ref} [get]
// func (h *Handler) VerifyTransaction(c *fiber.Ctx) error {
// txRef := c.Params("tx_ref")
// if txRef == "" {
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
// "error": "Missing transaction reference",
// })
// }
url := fmt.Sprintf("%s/transaction/verify/%s", h.Cfg.CHAPA_BASE_URL, txRef)
// url := fmt.Sprintf("%s/transaction/verify/%s", h.Cfg.CHAPA_BASE_URL, txRef)
httpReq, err := http.NewRequest("GET", url, nil)
if err != nil {
return c.Status(500).JSON(fiber.Map{
"error": "Failed to create request",
"details": err.Error(),
})
}
httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
// httpReq, err := http.NewRequest("GET", url, nil)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{
// "error": "Failed to create request",
// "details": err.Error(),
// })
// }
// httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return c.Status(500).JSON(fiber.Map{
"error": "Failed to verify transaction",
"details": err.Error(),
})
}
defer resp.Body.Close()
// resp, err := http.DefaultClient.Do(httpReq)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{
// "error": "Failed to verify transaction",
// "details": err.Error(),
// })
// }
// defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return c.Status(500).JSON(fiber.Map{
"error": "Failed to read response",
"details": err.Error(),
})
}
// body, err := io.ReadAll(resp.Body)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{
// "error": "Failed to read response",
// "details": err.Error(),
// })
// }
return c.Status(resp.StatusCode).Type("json").Send(body)
}
// return c.Status(resp.StatusCode).Type("json").Send(body)
// }
// ReceiveWebhook godoc
// @Summary Receive Chapa webhook
// @Description Endpoint to receive webhook payloads from Chapa
// @Tags Chapa
// @Accept json
// @Produce json
// @Param payload body object true "Webhook Payload (dynamic)"
// @Success 200 {string} string "ok"
// @Router /api/v1/chapa/payments/callback [post]
func (h *Handler) ReceiveWebhook(c *fiber.Ctx) error {
var payload map[string]interface{}
if err := c.BodyParser(&payload); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid webhook data",
"details": err.Error(),
})
}
// // ReceiveWebhook godoc
// // @Summary Receive Chapa webhook
// // @Description Endpoint to receive webhook payloads from Chapa
// // @Tags Chapa
// // @Accept json
// // @Produce json
// // @Param payload body object true "Webhook Payload (dynamic)"
// // @Success 200 {string} string "ok"
// // @Router /api/v1/chapa/payments/callback [post]
// func (h *Handler) ReceiveWebhook(c *fiber.Ctx) error {
// var payload map[string]interface{}
// if err := c.BodyParser(&payload); err != nil {
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
// "error": "Invalid webhook data",
// "details": err.Error(),
// })
// }
h.logger.Info("Chapa webhook received", "payload", payload)
// h.logger.Info("Chapa webhook received", "payload", payload)
// Optional: you can verify tx_ref here again if needed
// // Optional: you can verify tx_ref here again if needed
return c.SendStatus(fiber.StatusOK)
}
// return c.SendStatus(fiber.StatusOK)
// }
// CreateTransfer godoc
// @Summary Create a money transfer
// @Description Initiate a transfer request via Chapa
// @Tags Chapa
// @Accept json
// @Produce json
// @Param payload body domain.TransferRequest true "Transfer request body"
// @Success 200 {object} domain.CreateTransferResponse
// @Router /api/v1/chapa/transfers [post]
func (h *Handler) CreateTransfer(c *fiber.Ctx) error {
var req TransferRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request",
"details": err.Error(),
})
}
// // CreateTransfer godoc
// // @Summary Create a money transfer
// // @Description Initiate a transfer request via Chapa
// // @Tags Chapa
// // @Accept json
// // @Produce json
// // @Param payload body domain.TransferRequest true "Transfer request body"
// // @Success 200 {object} domain.CreateTransferResponse
// // @Router /api/v1/chapa/transfers [post]
// func (h *Handler) CreateTransfer(c *fiber.Ctx) error {
// var req TransferRequest
// if err := c.BodyParser(&req); err != nil {
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
// "error": "Invalid request",
// "details": err.Error(),
// })
// }
// Inject unique transaction reference
req.Reference = uuid.New().String()
// // Inject unique transaction reference
// req.Reference = uuid.New().String()
payload, err := json.Marshal(req)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Failed to serialize request",
"details": err.Error(),
})
}
// payload, err := json.Marshal(req)
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
// "error": "Failed to serialize request",
// "details": err.Error(),
// })
// }
httpReq, err := http.NewRequest("POST", h.Cfg.CHAPA_BASE_URL+"/transfers", bytes.NewBuffer(payload))
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Failed to create HTTP request",
"details": err.Error(),
})
}
// httpReq, err := http.NewRequest("POST", h.Cfg.CHAPA_BASE_URL+"/transfers", bytes.NewBuffer(payload))
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
// "error": "Failed to create HTTP request",
// "details": err.Error(),
// })
// }
httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
httpReq.Header.Set("Content-Type", "application/json")
// httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
// httpReq.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Transfer request failed",
"details": err.Error(),
})
}
defer resp.Body.Close()
// resp, err := http.DefaultClient.Do(httpReq)
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
// "error": "Transfer request failed",
// "details": err.Error(),
// })
// }
// defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Failed to read response",
"details": err.Error(),
})
}
// body, err := io.ReadAll(resp.Body)
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
// "error": "Failed to read response",
// "details": err.Error(),
// })
// }
return c.Status(resp.StatusCode).Type("json").Send(body)
}
// return c.Status(resp.StatusCode).Type("json").Send(body)
// }
// VerifyTransfer godoc
// @Summary Verify a transfer
// @Description Check the status of a money transfer via reference
// @Tags Chapa
// @Accept json
// @Produce json
// @Param transfer_ref path string true "Transfer Reference"
// @Success 200 {object} domain.VerifyTransferResponse
// @Router /api/v1/chapa/transfers/verify/{transfer_ref} [get]
func (h *Handler) VerifyTransfer(c *fiber.Ctx) error {
transferRef := c.Params("transfer_ref")
if transferRef == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Missing transfer reference in URL",
})
}
// // VerifyTransfer godoc
// // @Summary Verify a transfer
// // @Description Check the status of a money transfer via reference
// // @Tags Chapa
// // @Accept json
// // @Produce json
// // @Param transfer_ref path string true "Transfer Reference"
// // @Success 200 {object} domain.VerifyTransferResponse
// // @Router /api/v1/chapa/transfers/verify/{transfer_ref} [get]
// func (h *Handler) VerifyTransfer(c *fiber.Ctx) error {
// transferRef := c.Params("transfer_ref")
// if transferRef == "" {
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
// "error": "Missing transfer reference in URL",
// })
// }
url := fmt.Sprintf("%s/transfers/verify/%s", h.Cfg.CHAPA_BASE_URL, transferRef)
// url := fmt.Sprintf("%s/transfers/verify/%s", h.Cfg.CHAPA_BASE_URL, transferRef)
httpReq, err := http.NewRequest("GET", url, nil)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Failed to create HTTP request",
"details": err.Error(),
})
}
// httpReq, err := http.NewRequest("GET", url, nil)
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
// "error": "Failed to create HTTP request",
// "details": err.Error(),
// })
// }
httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
// httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Verification request failed",
"details": err.Error(),
})
}
defer resp.Body.Close()
// resp, err := http.DefaultClient.Do(httpReq)
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
// "error": "Verification request failed",
// "details": err.Error(),
// })
// }
// defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Failed to read response body",
"details": err.Error(),
})
}
// body, err := io.ReadAll(resp.Body)
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
// "error": "Failed to read response body",
// "details": err.Error(),
// })
// }
return c.Status(resp.StatusCode).Type("json").Send(body)
}
// return c.Status(resp.StatusCode).Type("json").Send(body)
// }
// VerifyChapaPayment godoc
// @Summary Verifies Chapa webhook transaction
@ -384,7 +385,6 @@ func (h *Handler) WithdrawUsingChapa(c *fiber.Ctx) error {
// @Tags Chapa
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param payload body domain.ChapaDepositRequest true "Deposit request payload"
// @Success 200 {object} domain.ChapaPaymentUrlResponseWrapper
// @Failure 400 {object} domain.Response "Invalid request"
@ -407,7 +407,7 @@ func (h *Handler) DepositUsingChapa(c *fiber.Ctx) error {
return domain.UnProcessableEntityResponse(c)
}
// Validate input in domain/model (you may have a Validate method)
// Validate input in domain/domain (you may have a Validate method)
if err := req.Validate(); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.Response{
Message: err.Error(),
@ -433,3 +433,32 @@ func (h *Handler) DepositUsingChapa(c *fiber.Ctx) error {
},
})
}
// ReadChapaBanks godoc
// @Summary fetches chapa supported banks
// @Tags Chapa
// @Accept json
// @Produce json
// @Success 200 {object} domain.ChapaSupportedBanksResponseWrapper
// @Failure 400,401,404,422,500 {object} domain.Response
// @Router /api/v1/chapa/banks [get]
func (h *Handler) ReadChapaBanks(c *fiber.Ctx) error {
banks, err := h.chapaSvc.GetSupportedBanks()
fmt.Printf("\n\nhandler fetched banks: %+v\n\n", banks)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.Response{
Message: "Internal server error",
Success: false,
StatusCode: fiber.StatusInternalServerError,
})
}
return c.Status(fiber.StatusOK).JSON(domain.ResponseWDataFactory[[]domain.ChapaSupportedBank]{
Data: banks,
Response: domain.Response{
Message: "read successful on chapa supported banks",
Success: true,
StatusCode: fiber.StatusOK,
},
})
}

View File

@ -0,0 +1,131 @@
package handlers
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
"testing"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
// --- Mock service ---
type MockChapaService struct {
mock.Mock
}
func (m *MockChapaService) GetSupportedBanks() ([]domain.ChapaSupportedBank, error) {
args := m.Called()
return args.Get(0).([]domain.ChapaSupportedBank), args.Error(1)
}
// --- Tests ---
func (h *Handler) TestReadChapaBanks_Success(t *testing.T) {
app := fiber.New()
mockService := new(MockChapaService)
now := time.Now()
isMobile := 1
isRtgs := 1
is24hrs := 1
mockBanks := []domain.ChapaSupportedBank{
{
Id: 101,
Slug: "bank-a",
Swift: "BKAETHAA",
Name: "Bank A",
AcctLength: 13,
AcctNumberRegex: "^[0-9]{13}$",
ExampleValue: "1000222215735",
CountryId: 1,
IsMobilemoney: &isMobile,
IsActive: 1,
IsRtgs: &isRtgs,
Active: 1,
Is24Hrs: &is24hrs,
CreatedAt: now,
UpdatedAt: now,
Currency: "ETB",
},
}
mockService.On("GetSupportedBanks").Return(mockBanks, nil)
// handler := handlers.NewChapaHandler(mockService)
app.Post("/chapa/banks", h.ReadChapaBanks)
req := createTestRequest(t, "POST", "/chapa/banks", nil)
resp, err := app.Test(req)
require.NoError(t, err)
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
var body domain.ResponseWDataFactory[[]domain.ChapaSupportedBank]
err = parseJSONBody(resp, &body)
require.NoError(t, err)
assert.True(t, body.Success)
assert.Equal(t, "read successful on chapa supported banks", body.Message)
require.Len(t, body.Data, 1)
assert.Equal(t, mockBanks[0].Name, body.Data[0].Name)
assert.Equal(t, mockBanks[0].AcctNumberRegex, body.Data[0].AcctNumberRegex)
mockService.AssertExpectations(t)
}
func (h *Handler) TestReadChapaBanks_Failure(t *testing.T) {
app := fiber.New()
mockService := new(MockChapaService)
mockService.On("GetSupportedBanks").Return(nil, errors.New("chapa service unavailable"))
// handler := handlers.NewChapaHandler(mockService)
app.Post("/chapa/banks", h.ReadChapaBanks)
req := createTestRequest(t, "POST", "/chapa/banks", nil)
resp, err := app.Test(req)
require.NoError(t, err)
assert.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)
var body domain.Response
err = parseJSONBody(resp, &body)
require.NoError(t, err)
assert.False(t, body.Success)
assert.Equal(t, "Internal server error", body.Message)
mockService.AssertExpectations(t)
}
func createTestRequest(t *testing.T, method, url string, body interface{}) *http.Request {
var buf io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
t.Fatal(err)
}
buf = bytes.NewBuffer(b)
}
req, err := http.NewRequest(method, url, buf)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
return req
}
func parseJSONBody(resp *http.Response, target interface{}) error {
return json.NewDecoder(resp.Body).Decode(target)
}

View File

@ -182,9 +182,10 @@ func (a *App) initAppRoutes() {
a.fiber.Post("/transfer/refill/:id", a.authMiddleware, h.RefillWallet)
//Chapa Routes
group.Post("/chapa/payments/verify", h.VerifyChapaPayment)
group.Post("/chapa/payments/withdraw", h.WithdrawUsingChapa)
group.Post("/chapa/payments/deposit", h.DepositUsingChapa)
group.Post("/chapa/payments/verify", a.authMiddleware, h.VerifyChapaPayment)
group.Post("/chapa/payments/withdraw", a.authMiddleware, h.WithdrawUsingChapa)
group.Post("/chapa/payments/deposit", a.authMiddleware, h.DepositUsingChapa)
group.Get("/chapa/banks", a.authMiddleware, h.ReadChapaBanks)
// group.Post("/chapa/payments/initialize", h.InitializePayment)
// group.Get("/chapa/payments/verify/:tx_ref", h.VerifyTransaction)

View File

@ -19,7 +19,7 @@ build:
.PHONY: run
run:
@docker compose up -d
@docker compose up
.PHONY: stop
stop: