diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 1db8ddb..6e12d64 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -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; diff --git a/docs/docs.go b/docs/docs.go index 044c114..78486ff 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -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": { diff --git a/docs/swagger.json b/docs/swagger.json index d225501..948658c 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -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": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a0003a7..feaedda 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -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: diff --git a/go.mod b/go.mod index 1b6761d..32d9786 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 32967eb..69ce8cd 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/domain/chapa.go b/internal/domain/chapa.go index 4bd6d90..1dba8f9 100644 --- a/internal/domain/chapa.go +++ b/internal/domain/chapa.go @@ -221,3 +221,8 @@ type ChapaPaymentUrlResponseWrapper struct { Data ChapaPaymentUrlResponse `json:"data"` Response } + +type ChapaSupportedBanksResponseWrapper struct { + Data []ChapaSupportedBank `json:"data"` + Response +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 043836c..41e6bb4 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -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, }) } diff --git a/internal/services/chapa/client.go b/internal/services/chapa/client.go index ff5a888..8e0374f 100644 --- a/internal/services/chapa/client.go +++ b/internal/services/chapa/client.go @@ -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 +} diff --git a/internal/services/chapa/port.go b/internal/services/chapa/port.go index b1b181f..0cdb213 100644 --- a/internal/services/chapa/port.go +++ b/internal/services/chapa/port.go @@ -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) } diff --git a/internal/services/chapa/service.go b/internal/services/chapa/service.go index 69d5809..9c67ab4 100644 --- a/internal/services/chapa/service.go +++ b/internal/services/chapa/service.go @@ -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) +} diff --git a/internal/services/result/sports_eval.go b/internal/services/result/sports_eval.go index 48a837f..86e082f 100644 --- a/internal/services/result/sports_eval.go +++ b/internal/services/result/sports_eval.go @@ -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": diff --git a/internal/web_server/handlers/chapa.go b/internal/web_server/handlers/chapa.go index a2d9991..7c03183 100644 --- a/internal/web_server/handlers/chapa.go +++ b/internal/web_server/handlers/chapa.go @@ -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, + }, + }) +} diff --git a/internal/web_server/handlers/read_chapa_banks_handler_test.go b/internal/web_server/handlers/read_chapa_banks_handler_test.go new file mode 100644 index 0000000..73e785c --- /dev/null +++ b/internal/web_server/handlers/read_chapa_banks_handler_test.go @@ -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) +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 7d31a87..88e8a2f 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -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) diff --git a/makefile b/makefile index a842d04..303a8cc 100644 --- a/makefile +++ b/makefile @@ -19,7 +19,7 @@ build: .PHONY: run run: - @docker compose up -d + @docker compose up .PHONY: stop stop: