diff --git a/cmd/main.go b/cmd/main.go index 8f07d13..cc41c96 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -20,8 +20,10 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation" @@ -85,6 +87,7 @@ func main() { transactionSvc := transaction.NewService(store) branchSvc := branch.NewService(store) companySvc := company.NewService(store) + leagueSvc := league.New(store) betSvc := bet.NewService(store, eventSvc, oddsSvc, *walletSvc, *branchSvc, logger) resultSvc := result.NewService(store, cfg, logger, *betSvc, oddsSvc, eventSvc) notificationRepo := repository.NewNotificationRepository(store) @@ -108,6 +111,17 @@ func main() { logger, ) recommendationSvc := recommendation.NewService(recommendationRepo) + chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY) + + chapaSvc := chapa.NewService( + transaction.TransactionStore(store), + wallet.WalletStore(store), + user.UserStore(store), + referalSvc, + branch.BranchStore(store), + chapaClient, + store, + ) httpserver.StartDataFetchingCrons(eventSvc, oddsSvc, resultSvc) httpserver.StartTicketCrons(*ticketSvc) @@ -116,7 +130,7 @@ func main() { JwtAccessKey: cfg.JwtKey, JwtAccessExpiry: cfg.AccessExpiry, }, userSvc, - ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc, referalSvc, virtualGameSvc, aleaService, veliService, recommendationSvc, resultSvc, cfg) + ticketSvc, betSvc, chapaSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc, leagueSvc, referalSvc, virtualGameSvc, aleaService, veliService, recommendationSvc, resultSvc, cfg) logger.Info("Starting server", "port", cfg.Port) if err := app.Run(); err != nil { diff --git a/db/migrations/000001_fortune.down.sql b/db/migrations/000001_fortune.down.sql index 82d488d..2724f06 100644 --- a/db/migrations/000001_fortune.down.sql +++ b/db/migrations/000001_fortune.down.sql @@ -75,4 +75,5 @@ DROP TABLE IF EXISTS supported_operations; DROP TABLE IF EXISTS refresh_tokens; DROP TABLE IF EXISTS otps; DROP TABLE IF EXISTS odds; -DROP TABLE IF EXISTS events; \ No newline at end of file +DROP TABLE IF EXISTS events; +DROP TABLE IF EXISTS leagues; \ No newline at end of file diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 1db8ddb..30a006b 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -184,15 +184,15 @@ CREATE TABLE IF NOT EXISTS branch_cashiers ( ); CREATE TABLE events ( id TEXT PRIMARY KEY, - sport_id TEXT, + sport_id INT, match_name TEXT, home_team TEXT, away_team TEXT, - home_team_id TEXT, - away_team_id TEXT, + home_team_id INT, + away_team_id INT, home_kit_image TEXT, away_kit_image TEXT, - league_id TEXT, + league_id INT, league_name TEXT, league_cc TEXT, start_time TIMESTAMP, @@ -233,6 +233,20 @@ CREATE TABLE companies ( admin_id BIGINT NOT NULL, wallet_id BIGINT NOT NULL ); +CREATE TABLE leagues ( + id BIGINT PRIMARY KEY, + name TEXT NOT NULL, + country_code TEXT, + bet365_id INT, + is_active BOOLEAN DEFAULT true +); +CREATE TABLE teams ( + id TEXT PRIMARY KEY, + team_name TEXT NOT NULL, + country TEXT, + bet365_id INT, + logo_url TEXT +); -- Views CREATE VIEW companies_details AS SELECT companies.*, @@ -297,6 +311,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/db/query/leagues.sql b/db/query/leagues.sql new file mode 100644 index 0000000..b9c0e02 --- /dev/null +++ b/db/query/leagues.sql @@ -0,0 +1,48 @@ +-- name: InsertLeague :exec +INSERT INTO leagues ( + id, + name, + country_code, + bet365_id, + is_active +) VALUES ( + $1, $2, $3, $4, $5 +) +ON CONFLICT (id) DO UPDATE +SET name = EXCLUDED.name, + country_code = EXCLUDED.country_code, + bet365_id = EXCLUDED.bet365_id, + is_active = EXCLUDED.is_active; +-- name: GetSupportedLeagues :many +SELECT id, + name, + country_code, + bet365_id, + is_active +FROM leagues +WHERE is_active = true; +-- name: GetAllLeagues :many +SELECT id, + name, + country_code, + bet365_id, + is_active +FROM leagues; +-- name: CheckLeagueSupport :one +SELECT EXISTS( + SELECT 1 + FROM leagues + WHERE id = $1 + AND is_active = true +); +-- name: UpdateLeague :exec +UPDATE leagues +SET name = $1, + country_code = $2, + bet365_id = $3, + is_active = $4 +WHERE id = $5; +-- name: SetLeagueActive :exec +UPDATE leagues +SET is_active = true +WHERE id = $1; \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 5685aa7..6c27d0b 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,20 +315,50 @@ 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" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$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" } } } } }, - "/api/v1/chapa/payments/callback": { + "/api/v1/chapa/payments/deposit": { "post": { - "description": "Endpoint to receive webhook payloads from Chapa", + "description": "Deposits money into user wallet from user account using Chapa", "consumes": [ "application/json" ], @@ -339,31 +368,48 @@ const docTemplate = `{ "tags": [ "Chapa" ], - "summary": "Receive Chapa webhook", + "summary": "Deposit money into user wallet using Chapa", "parameters": [ { - "description": "Webhook Payload (dynamic)", + "description": "Deposit request payload", "name": "payload", "in": "body", "required": true, "schema": { - "type": "object" + "$ref": "#/definitions/domain.ChapaDepositRequest" } } ], "responses": { "200": { - "description": "ok", + "description": "OK", "schema": { - "type": "string" + "$ref": "#/definitions/domain.ChapaPaymentUrlResponseWrapper" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "422": { + "description": "Validation error", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/domain.Response" } } } } }, - "/api/v1/chapa/payments/initialize": { + "/api/v1/chapa/payments/verify": { "post": { - "description": "Initiate a payment through Chapa", "consumes": [ "application/json" ], @@ -373,15 +419,15 @@ const docTemplate = `{ "tags": [ "Chapa" ], - "summary": "Initialize a payment transaction", + "summary": "Verifies Chapa webhook transaction", "parameters": [ { - "description": "Payment initialization request", + "description": "Webhook Payload", "name": "payload", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.InitPaymentRequest" + "$ref": "#/definitions/domain.ChapaTransactionType" } } ], @@ -389,47 +435,15 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/domain.InitPaymentResponse" + "$ref": "#/definitions/domain.Response" } } } } }, - "/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/transfers": { + "/api/v1/chapa/payments/withdraw": { "post": { - "description": "Initiate a transfer request via Chapa", + "description": "Initiates a withdrawal transaction using Chapa for the authenticated user.", "consumes": [ "application/json" ], @@ -439,55 +453,59 @@ const docTemplate = `{ "tags": [ "Chapa" ], - "summary": "Create a money transfer", + "summary": "Withdraw using Chapa", "parameters": [ { - "description": "Transfer request body", - "name": "payload", + "description": "Chapa Withdraw Request", + "name": "request", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.TransferRequest" + "$ref": "#/definitions/domain.ChapaWithdrawRequest" } } ], "responses": { "200": { - "description": "OK", + "description": "Withdrawal requested successfully", "schema": { - "$ref": "#/definitions/domain.CreateTransferResponse" + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] } - } - } - } - }, - "/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", + }, + "400": { + "description": "Invalid request", "schema": { - "$ref": "#/definitions/domain.VerifyTransferResponse" + "$ref": "#/definitions/domain.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.Response" } } } @@ -4460,6 +4478,46 @@ const docTemplate = `{ } } }, + "domain.ChapaDepositRequest": { + "type": "object", + "properties": { + "amount": { + "type": "integer" + }, + "branch_id": { + "type": "integer" + }, + "currency": { + "type": "string" + }, + "phone_number": { + "type": "string" + } + } + }, + "domain.ChapaPaymentUrlResponse": { + "type": "object", + "properties": { + "payment_url": { + "type": "string" + } + } + }, + "domain.ChapaPaymentUrlResponseWrapper": { + "type": "object", + "properties": { + "data": {}, + "message": { + "type": "string" + }, + "status_code": { + "type": "integer" + }, + "success": { + "type": "boolean" + } + } + }, "domain.ChapaSupportedBank": { "type": "object", "properties": { @@ -4513,17 +4571,56 @@ 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" + } + } + }, + "domain.ChapaTransactionType": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + } + }, + "domain.ChapaWithdrawRequest": { + "type": "object", + "properties": { + "account_name": { + "type": "string" + }, + "account_number": { + "type": "string" + }, + "amount": { + "type": "integer" + }, + "bank_code": { + "type": "string" + }, + "beneficiary_name": { + "type": "string" + }, + "branch_id": { + "type": "integer" + }, + "currency": { + "type": "string" + }, + "wallet_id": { + "description": "add this", + "type": "integer" } } }, @@ -4579,76 +4676,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": "string" - }, - "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": { @@ -4879,6 +4906,21 @@ const docTemplate = `{ } } }, + "domain.Response": { + "type": "object", + "properties": { + "data": {}, + "message": { + "type": "string" + }, + "status_code": { + "type": "integer" + }, + "success": { + "type": "boolean" + } + } + }, "domain.Role": { "type": "string", "enum": [ @@ -4961,86 +5003,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": { @@ -5147,34 +5109,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 23766a9..6119c3c 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,20 +307,50 @@ "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" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$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" } } } } }, - "/api/v1/chapa/payments/callback": { + "/api/v1/chapa/payments/deposit": { "post": { - "description": "Endpoint to receive webhook payloads from Chapa", + "description": "Deposits money into user wallet from user account using Chapa", "consumes": [ "application/json" ], @@ -331,31 +360,48 @@ "tags": [ "Chapa" ], - "summary": "Receive Chapa webhook", + "summary": "Deposit money into user wallet using Chapa", "parameters": [ { - "description": "Webhook Payload (dynamic)", + "description": "Deposit request payload", "name": "payload", "in": "body", "required": true, "schema": { - "type": "object" + "$ref": "#/definitions/domain.ChapaDepositRequest" } } ], "responses": { "200": { - "description": "ok", + "description": "OK", "schema": { - "type": "string" + "$ref": "#/definitions/domain.ChapaPaymentUrlResponseWrapper" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "422": { + "description": "Validation error", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/domain.Response" } } } } }, - "/api/v1/chapa/payments/initialize": { + "/api/v1/chapa/payments/verify": { "post": { - "description": "Initiate a payment through Chapa", "consumes": [ "application/json" ], @@ -365,15 +411,15 @@ "tags": [ "Chapa" ], - "summary": "Initialize a payment transaction", + "summary": "Verifies Chapa webhook transaction", "parameters": [ { - "description": "Payment initialization request", + "description": "Webhook Payload", "name": "payload", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.InitPaymentRequest" + "$ref": "#/definitions/domain.ChapaTransactionType" } } ], @@ -381,47 +427,15 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/domain.InitPaymentResponse" + "$ref": "#/definitions/domain.Response" } } } } }, - "/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/transfers": { + "/api/v1/chapa/payments/withdraw": { "post": { - "description": "Initiate a transfer request via Chapa", + "description": "Initiates a withdrawal transaction using Chapa for the authenticated user.", "consumes": [ "application/json" ], @@ -431,55 +445,59 @@ "tags": [ "Chapa" ], - "summary": "Create a money transfer", + "summary": "Withdraw using Chapa", "parameters": [ { - "description": "Transfer request body", - "name": "payload", + "description": "Chapa Withdraw Request", + "name": "request", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.TransferRequest" + "$ref": "#/definitions/domain.ChapaWithdrawRequest" } } ], "responses": { "200": { - "description": "OK", + "description": "Withdrawal requested successfully", "schema": { - "$ref": "#/definitions/domain.CreateTransferResponse" + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] } - } - } - } - }, - "/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", + }, + "400": { + "description": "Invalid request", "schema": { - "$ref": "#/definitions/domain.VerifyTransferResponse" + "$ref": "#/definitions/domain.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.Response" } } } @@ -4452,6 +4470,46 @@ } } }, + "domain.ChapaDepositRequest": { + "type": "object", + "properties": { + "amount": { + "type": "integer" + }, + "branch_id": { + "type": "integer" + }, + "currency": { + "type": "string" + }, + "phone_number": { + "type": "string" + } + } + }, + "domain.ChapaPaymentUrlResponse": { + "type": "object", + "properties": { + "payment_url": { + "type": "string" + } + } + }, + "domain.ChapaPaymentUrlResponseWrapper": { + "type": "object", + "properties": { + "data": {}, + "message": { + "type": "string" + }, + "status_code": { + "type": "integer" + }, + "success": { + "type": "boolean" + } + } + }, "domain.ChapaSupportedBank": { "type": "object", "properties": { @@ -4505,17 +4563,56 @@ } } }, - "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" + } + } + }, + "domain.ChapaTransactionType": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + } + }, + "domain.ChapaWithdrawRequest": { + "type": "object", + "properties": { + "account_name": { + "type": "string" + }, + "account_number": { + "type": "string" + }, + "amount": { + "type": "integer" + }, + "bank_code": { + "type": "string" + }, + "beneficiary_name": { + "type": "string" + }, + "branch_id": { + "type": "integer" + }, + "currency": { + "type": "string" + }, + "wallet_id": { + "description": "add this", + "type": "integer" } } }, @@ -4571,76 +4668,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": "string" - }, - "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": { @@ -4871,6 +4898,21 @@ } } }, + "domain.Response": { + "type": "object", + "properties": { + "data": {}, + "message": { + "type": "string" + }, + "status_code": { + "type": "integer" + }, + "success": { + "type": "boolean" + } + } + }, "domain.Role": { "type": "string", "enum": [ @@ -4953,86 +4995,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": { @@ -5139,34 +5101,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 7fe4781..4bf0ca1 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -124,6 +124,32 @@ definitions: example: 2 type: integer type: object + domain.ChapaDepositRequest: + properties: + amount: + type: integer + branch_id: + type: integer + currency: + type: string + phone_number: + type: string + type: object + domain.ChapaPaymentUrlResponse: + properties: + payment_url: + type: string + type: object + domain.ChapaPaymentUrlResponseWrapper: + properties: + data: {} + message: + type: string + status_code: + type: integer + success: + type: boolean + type: object domain.ChapaSupportedBank: properties: acct_length: @@ -159,14 +185,40 @@ 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: + type: + type: string + type: object + domain.ChapaWithdrawRequest: + properties: + account_name: + type: string + account_number: + type: string + amount: + type: integer + bank_code: + type: string + beneficiary_name: + type: string + branch_id: + type: integer + currency: + type: string + wallet_id: + description: add this + type: integer type: object domain.CreateBetOutcomeReq: properties: @@ -203,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: string - 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: @@ -408,6 +414,16 @@ definitions: totalRewardEarned: type: number type: object + domain.Response: + properties: + data: {} + message: + type: string + status_code: + type: integer + success: + type: boolean + type: object domain.Role: enum: - super_admin @@ -468,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: @@ -598,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: @@ -1691,123 +1637,133 @@ 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 + $ref: '#/definitions/domain.ChapaSupportedBanksResponseWrapper' + "400": + description: Bad Request + schema: + $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/callback: + /api/v1/chapa/payments/deposit: post: consumes: - application/json - description: Endpoint to receive webhook payloads from Chapa + description: Deposits money into user wallet from user account using Chapa parameters: - - description: Webhook Payload (dynamic) + - description: Deposit request payload in: body name: payload required: true schema: - type: object + $ref: '#/definitions/domain.ChapaDepositRequest' produces: - application/json responses: "200": - description: ok + description: OK schema: - type: string - summary: Receive Chapa webhook + $ref: '#/definitions/domain.ChapaPaymentUrlResponseWrapper' + "400": + description: Invalid request + schema: + $ref: '#/definitions/domain.Response' + "422": + description: Validation error + schema: + $ref: '#/definitions/domain.Response' + "500": + description: Internal server error + schema: + $ref: '#/definitions/domain.Response' + summary: Deposit money into user wallet using Chapa tags: - Chapa - /api/v1/chapa/payments/initialize: + /api/v1/chapa/payments/verify: post: consumes: - application/json - description: Initiate a payment through Chapa parameters: - - description: Payment initialization request + - description: Webhook Payload in: body name: payload required: true schema: - $ref: '#/definitions/domain.InitPaymentRequest' + $ref: '#/definitions/domain.ChapaTransactionType' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/domain.InitPaymentResponse' - summary: Initialize a payment transaction + $ref: '#/definitions/domain.Response' + 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/transfers: + /api/v1/chapa/payments/withdraw: post: consumes: - application/json - description: Initiate a transfer request via Chapa + description: Initiates a withdrawal transaction using Chapa for the authenticated + user. parameters: - - description: Transfer request body + - description: Chapa Withdraw Request in: body - name: payload + name: request required: true schema: - $ref: '#/definitions/domain.TransferRequest' + $ref: '#/definitions/domain.ChapaWithdrawRequest' produces: - application/json responses: "200": - description: OK + description: Withdrawal requested successfully 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 + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + type: string + type: object + "400": + description: Invalid request schema: - $ref: '#/definitions/domain.VerifyTransferResponse' - summary: Verify a transfer + $ref: '#/definitions/domain.Response' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/domain.Response' + "422": + description: Unprocessable Entity + schema: + $ref: '#/definitions/domain.Response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.Response' + summary: Withdraw using Chapa tags: - Chapa /api/v1/virtual-games/recommendations/{userID}: diff --git a/gen/db/cashier.sql.go b/gen/db/cashier.sql.go index d0f6768..bb71cb2 100644 --- a/gen/db/cashier.sql.go +++ b/gen/db/cashier.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: cashier.sql package dbgen diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index 26fa359..e5fc357 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -47,15 +47,15 @@ ORDER BY start_time ASC type GetAllUpcomingEventsRow struct { ID string `json:"id"` - SportID pgtype.Text `json:"sport_id"` + SportID pgtype.Int4 `json:"sport_id"` MatchName pgtype.Text `json:"match_name"` HomeTeam pgtype.Text `json:"home_team"` AwayTeam pgtype.Text `json:"away_team"` - HomeTeamID pgtype.Text `json:"home_team_id"` - AwayTeamID pgtype.Text `json:"away_team_id"` + HomeTeamID pgtype.Int4 `json:"home_team_id"` + AwayTeamID pgtype.Int4 `json:"away_team_id"` HomeKitImage pgtype.Text `json:"home_kit_image"` AwayKitImage pgtype.Text `json:"away_kit_image"` - LeagueID pgtype.Text `json:"league_id"` + LeagueID pgtype.Int4 `json:"league_id"` LeagueName pgtype.Text `json:"league_name"` LeagueCc pgtype.Text `json:"league_cc"` StartTime pgtype.Timestamp `json:"start_time"` @@ -132,15 +132,15 @@ ORDER BY start_time ASC type GetExpiredUpcomingEventsRow struct { ID string `json:"id"` - SportID pgtype.Text `json:"sport_id"` + SportID pgtype.Int4 `json:"sport_id"` MatchName pgtype.Text `json:"match_name"` HomeTeam pgtype.Text `json:"home_team"` AwayTeam pgtype.Text `json:"away_team"` - HomeTeamID pgtype.Text `json:"home_team_id"` - AwayTeamID pgtype.Text `json:"away_team_id"` + HomeTeamID pgtype.Int4 `json:"home_team_id"` + AwayTeamID pgtype.Int4 `json:"away_team_id"` HomeKitImage pgtype.Text `json:"home_kit_image"` AwayKitImage pgtype.Text `json:"away_kit_image"` - LeagueID pgtype.Text `json:"league_id"` + LeagueID pgtype.Int4 `json:"league_id"` LeagueName pgtype.Text `json:"league_name"` LeagueCc pgtype.Text `json:"league_cc"` StartTime pgtype.Timestamp `json:"start_time"` @@ -230,8 +230,8 @@ LIMIT $6 OFFSET $5 ` type GetPaginatedUpcomingEventsParams struct { - LeagueID pgtype.Text `json:"league_id"` - SportID pgtype.Text `json:"sport_id"` + LeagueID pgtype.Int4 `json:"league_id"` + SportID pgtype.Int4 `json:"sport_id"` LastStartTime pgtype.Timestamp `json:"last_start_time"` FirstStartTime pgtype.Timestamp `json:"first_start_time"` Offset pgtype.Int4 `json:"offset"` @@ -240,15 +240,15 @@ type GetPaginatedUpcomingEventsParams struct { type GetPaginatedUpcomingEventsRow struct { ID string `json:"id"` - SportID pgtype.Text `json:"sport_id"` + SportID pgtype.Int4 `json:"sport_id"` MatchName pgtype.Text `json:"match_name"` HomeTeam pgtype.Text `json:"home_team"` AwayTeam pgtype.Text `json:"away_team"` - HomeTeamID pgtype.Text `json:"home_team_id"` - AwayTeamID pgtype.Text `json:"away_team_id"` + HomeTeamID pgtype.Int4 `json:"home_team_id"` + AwayTeamID pgtype.Int4 `json:"away_team_id"` HomeKitImage pgtype.Text `json:"home_kit_image"` AwayKitImage pgtype.Text `json:"away_kit_image"` - LeagueID pgtype.Text `json:"league_id"` + LeagueID pgtype.Int4 `json:"league_id"` LeagueName pgtype.Text `json:"league_name"` LeagueCc pgtype.Text `json:"league_cc"` StartTime pgtype.Timestamp `json:"start_time"` @@ -327,8 +327,8 @@ WHERE is_live = false ` type GetTotalEventsParams struct { - LeagueID pgtype.Text `json:"league_id"` - SportID pgtype.Text `json:"sport_id"` + LeagueID pgtype.Int4 `json:"league_id"` + SportID pgtype.Int4 `json:"sport_id"` LastStartTime pgtype.Timestamp `json:"last_start_time"` FirstStartTime pgtype.Timestamp `json:"first_start_time"` } @@ -372,15 +372,15 @@ LIMIT 1 type GetUpcomingByIDRow struct { ID string `json:"id"` - SportID pgtype.Text `json:"sport_id"` + SportID pgtype.Int4 `json:"sport_id"` MatchName pgtype.Text `json:"match_name"` HomeTeam pgtype.Text `json:"home_team"` AwayTeam pgtype.Text `json:"away_team"` - HomeTeamID pgtype.Text `json:"home_team_id"` - AwayTeamID pgtype.Text `json:"away_team_id"` + HomeTeamID pgtype.Int4 `json:"home_team_id"` + AwayTeamID pgtype.Int4 `json:"away_team_id"` HomeKitImage pgtype.Text `json:"home_kit_image"` AwayKitImage pgtype.Text `json:"away_kit_image"` - LeagueID pgtype.Text `json:"league_id"` + LeagueID pgtype.Int4 `json:"league_id"` LeagueName pgtype.Text `json:"league_name"` LeagueCc pgtype.Text `json:"league_cc"` StartTime pgtype.Timestamp `json:"start_time"` @@ -488,15 +488,15 @@ SET sport_id = EXCLUDED.sport_id, type InsertEventParams struct { ID string `json:"id"` - SportID pgtype.Text `json:"sport_id"` + SportID pgtype.Int4 `json:"sport_id"` MatchName pgtype.Text `json:"match_name"` HomeTeam pgtype.Text `json:"home_team"` AwayTeam pgtype.Text `json:"away_team"` - HomeTeamID pgtype.Text `json:"home_team_id"` - AwayTeamID pgtype.Text `json:"away_team_id"` + HomeTeamID pgtype.Int4 `json:"home_team_id"` + AwayTeamID pgtype.Int4 `json:"away_team_id"` HomeKitImage pgtype.Text `json:"home_kit_image"` AwayKitImage pgtype.Text `json:"away_kit_image"` - LeagueID pgtype.Text `json:"league_id"` + LeagueID pgtype.Int4 `json:"league_id"` LeagueName pgtype.Text `json:"league_name"` LeagueCc pgtype.Text `json:"league_cc"` StartTime pgtype.Timestamp `json:"start_time"` @@ -595,15 +595,15 @@ SET sport_id = EXCLUDED.sport_id, type InsertUpcomingEventParams struct { ID string `json:"id"` - SportID pgtype.Text `json:"sport_id"` + SportID pgtype.Int4 `json:"sport_id"` MatchName pgtype.Text `json:"match_name"` HomeTeam pgtype.Text `json:"home_team"` AwayTeam pgtype.Text `json:"away_team"` - HomeTeamID pgtype.Text `json:"home_team_id"` - AwayTeamID pgtype.Text `json:"away_team_id"` + HomeTeamID pgtype.Int4 `json:"home_team_id"` + AwayTeamID pgtype.Int4 `json:"away_team_id"` HomeKitImage pgtype.Text `json:"home_kit_image"` AwayKitImage pgtype.Text `json:"away_kit_image"` - LeagueID pgtype.Text `json:"league_id"` + LeagueID pgtype.Int4 `json:"league_id"` LeagueName pgtype.Text `json:"league_name"` LeagueCc pgtype.Text `json:"league_cc"` StartTime pgtype.Timestamp `json:"start_time"` diff --git a/gen/db/leagues.sql.go b/gen/db/leagues.sql.go new file mode 100644 index 0000000..49c1555 --- /dev/null +++ b/gen/db/leagues.sql.go @@ -0,0 +1,174 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: leagues.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const CheckLeagueSupport = `-- name: CheckLeagueSupport :one +SELECT EXISTS( + SELECT 1 + FROM leagues + WHERE id = $1 + AND is_active = true +) +` + +func (q *Queries) CheckLeagueSupport(ctx context.Context, id int64) (bool, error) { + row := q.db.QueryRow(ctx, CheckLeagueSupport, id) + var exists bool + err := row.Scan(&exists) + return exists, err +} + +const GetAllLeagues = `-- name: GetAllLeagues :many +SELECT id, + name, + country_code, + bet365_id, + is_active +FROM leagues +` + +func (q *Queries) GetAllLeagues(ctx context.Context) ([]League, error) { + rows, err := q.db.Query(ctx, GetAllLeagues) + if err != nil { + return nil, err + } + defer rows.Close() + var items []League + for rows.Next() { + var i League + if err := rows.Scan( + &i.ID, + &i.Name, + &i.CountryCode, + &i.Bet365ID, + &i.IsActive, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetSupportedLeagues = `-- name: GetSupportedLeagues :many +SELECT id, + name, + country_code, + bet365_id, + is_active +FROM leagues +WHERE is_active = true +` + +func (q *Queries) GetSupportedLeagues(ctx context.Context) ([]League, error) { + rows, err := q.db.Query(ctx, GetSupportedLeagues) + if err != nil { + return nil, err + } + defer rows.Close() + var items []League + for rows.Next() { + var i League + if err := rows.Scan( + &i.ID, + &i.Name, + &i.CountryCode, + &i.Bet365ID, + &i.IsActive, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const InsertLeague = `-- name: InsertLeague :exec +INSERT INTO leagues ( + id, + name, + country_code, + bet365_id, + is_active +) VALUES ( + $1, $2, $3, $4, $5 +) +ON CONFLICT (id) DO UPDATE +SET name = EXCLUDED.name, + country_code = EXCLUDED.country_code, + bet365_id = EXCLUDED.bet365_id, + is_active = EXCLUDED.is_active +` + +type InsertLeagueParams struct { + ID int64 `json:"id"` + Name string `json:"name"` + CountryCode pgtype.Text `json:"country_code"` + Bet365ID pgtype.Int4 `json:"bet365_id"` + IsActive pgtype.Bool `json:"is_active"` +} + +func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) error { + _, err := q.db.Exec(ctx, InsertLeague, + arg.ID, + arg.Name, + arg.CountryCode, + arg.Bet365ID, + arg.IsActive, + ) + return err +} + +const SetLeagueActive = `-- name: SetLeagueActive :exec +UPDATE leagues +SET is_active = true +WHERE id = $1 +` + +func (q *Queries) SetLeagueActive(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, SetLeagueActive, id) + return err +} + +const UpdateLeague = `-- name: UpdateLeague :exec +UPDATE leagues +SET name = $1, + country_code = $2, + bet365_id = $3, + is_active = $4 +WHERE id = $5 +` + +type UpdateLeagueParams struct { + Name string `json:"name"` + CountryCode pgtype.Text `json:"country_code"` + Bet365ID pgtype.Int4 `json:"bet365_id"` + IsActive pgtype.Bool `json:"is_active"` + ID int64 `json:"id"` +} + +func (q *Queries) UpdateLeague(ctx context.Context, arg UpdateLeagueParams) error { + _, err := q.db.Exec(ctx, UpdateLeague, + arg.Name, + arg.CountryCode, + arg.Bet365ID, + arg.IsActive, + arg.ID, + ) + return err +} diff --git a/gen/db/models.go b/gen/db/models.go index 3633f75..40ec2ca 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -178,15 +178,15 @@ type CustomerWallet struct { type Event struct { ID string `json:"id"` - SportID pgtype.Text `json:"sport_id"` + SportID pgtype.Int4 `json:"sport_id"` MatchName pgtype.Text `json:"match_name"` HomeTeam pgtype.Text `json:"home_team"` AwayTeam pgtype.Text `json:"away_team"` - HomeTeamID pgtype.Text `json:"home_team_id"` - AwayTeamID pgtype.Text `json:"away_team_id"` + HomeTeamID pgtype.Int4 `json:"home_team_id"` + AwayTeamID pgtype.Int4 `json:"away_team_id"` HomeKitImage pgtype.Text `json:"home_kit_image"` AwayKitImage pgtype.Text `json:"away_kit_image"` - LeagueID pgtype.Text `json:"league_id"` + LeagueID pgtype.Int4 `json:"league_id"` LeagueName pgtype.Text `json:"league_name"` LeagueCc pgtype.Text `json:"league_cc"` StartTime pgtype.Timestamp `json:"start_time"` @@ -201,6 +201,14 @@ type Event struct { Source pgtype.Text `json:"source"` } +type League struct { + ID int64 `json:"id"` + Name string `json:"name"` + CountryCode pgtype.Text `json:"country_code"` + Bet365ID pgtype.Int4 `json:"bet365_id"` + IsActive pgtype.Bool `json:"is_active"` +} + type Notification struct { ID string `json:"id"` RecipientID int64 `json:"recipient_id"` diff --git a/go.mod b/go.mod index e8b3f60..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,12 +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/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 ab1ac26..69ce8cd 100644 --- a/go.sum +++ b/go.sum @@ -112,12 +112,16 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 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/config/config.go b/internal/config/config.go index eba9702..db606c8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -289,3 +289,8 @@ func (c *Config) loadEnv() error { c.Bet365Token = betToken return nil } + +type ChapaConfig struct { + ChapaPaymentType string `mapstructure:"chapa_payment_type"` + ChapaTransferType string `mapstructure:"chapa_transfer_type"` +} diff --git a/internal/domain/chapa.go b/internal/domain/chapa.go index f630a6d..1dba8f9 100644 --- a/internal/domain/chapa.go +++ b/internal/domain/chapa.go @@ -1,6 +1,9 @@ package domain -import "time" +import ( + "errors" + "time" +) var ( ChapaSecret string @@ -8,14 +11,14 @@ var ( ) type InitPaymentRequest struct { - Amount string `json:"amount"` - Currency string `json:"currency"` - Email string `json:"email"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - TxRef string `json:"tx_ref"` - CallbackURL string `json:"callback_url"` - ReturnURL string `json:"return_url"` + Amount Currency `json:"amount"` + Currency string `json:"currency"` + Email string `json:"email"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + TxRef string `json:"tx_ref"` + CallbackURL string `json:"callback_url"` + ReturnURL string `json:"return_url"` } type TransferRequest struct { @@ -105,3 +108,121 @@ type VerifyTransferResponse struct { Message string `json:"message"` Data TransferVerificationData `json:"data"` } + +type ChapaTransactionType struct { + Type string `json:"type"` +} + +type ChapaWebHookTransfer struct { + AccountName string `json:"account_name"` + AccountNumber string `json:"account_number"` + BankId string `json:"bank_id"` + BankName string `json:"bank_name"` + Currency string `json:"currency"` + Amount string `json:"amount"` + Type string `json:"type"` + Status string `json:"status"` + Reference string `json:"reference"` + TxRef string `json:"tx_ref"` + ChapaReference string `json:"chapa_reference"` + CreatedAt time.Time `json:"created_at"` +} + +type ChapaWebHookPayment struct { + Event string `json:"event"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` + Mobile interface{} `json:"mobile"` + Currency string `json:"currency"` + Amount string `json:"amount"` + Charge string `json:"charge"` + Status string `json:"status"` + Mode string `json:"mode"` + Reference string `json:"reference"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Type string `json:"type"` + TxRef string `json:"tx_ref"` + PaymentMethod string `json:"payment_method"` + Customization struct { + Title interface{} `json:"title"` + Description interface{} `json:"description"` + Logo interface{} `json:"logo"` + } `json:"customization"` + Meta string `json:"meta"` +} + +type ChapaWithdrawRequest struct { + WalletID int64 `json:"wallet_id"` // add this + AccountName string `json:"account_name"` + AccountNumber string `json:"account_number"` + Amount int64 `json:"amount"` + Currency string `json:"currency"` + BeneficiaryName string `json:"beneficiary_name"` + BankCode string `json:"bank_code"` + BranchID int64 `json:"branch_id"` +} + +type ChapaTransferPayload struct { + AccountName string + AccountNumber string + Amount string + Currency string + BeneficiaryName string + TxRef string + Reference string + BankCode string +} + +type ChapaDepositRequest struct { + Amount Currency `json:"amount"` + PhoneNumber string `json:"phone_number"` + Currency string `json:"currency"` + BranchID int64 `json:"branch_id"` +} + +func (r ChapaDepositRequest) Validate() error { + if r.Amount <= 0 { + return errors.New("amount must be greater than zero") + } + if r.Currency == "" { + return errors.New("currency is required") + } + if r.PhoneNumber == "" { + return errors.New("phone number is required") + } + if r.BranchID == 0 { + return errors.New("branch ID is required") + } + + return nil +} + +type AcceptChapaPaymentRequest struct { + Amount string `json:"amount"` + Currency string `json:"currency"` + Email string `json:"email"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + PhoneNumber string `json:"phone_number"` + TxRef string `json:"tx_ref"` + CallbackUrl string `json:"callback_url"` + ReturnUrl string `json:"return_url"` + CustomizationTitle string `json:"customization[title]"` + CustomizationDescription string `json:"customization[description]"` +} + +type ChapaPaymentUrlResponse struct { + PaymentURL string `json:"payment_url"` +} + +type ChapaPaymentUrlResponseWrapper struct { + Data ChapaPaymentUrlResponse `json:"data"` + Response +} + +type ChapaSupportedBanksResponseWrapper struct { + Data []ChapaSupportedBank `json:"data"` + Response +} diff --git a/internal/domain/common.go b/internal/domain/common.go index d90d9ce..556b98b 100644 --- a/internal/domain/common.go +++ b/internal/domain/common.go @@ -13,6 +13,10 @@ type ValidInt struct { Value int Valid bool } +type ValidInt32 struct { + Value int32 + Valid bool +} type ValidString struct { Value string @@ -48,6 +52,18 @@ func (m Currency) String() string { return fmt.Sprintf("$%.2f", x) } +type ResponseWDataFactory[T any] struct { + Data T `json:"data"` + Response +} + +type Response struct { + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` + Success bool `json:"success"` + StatusCode int `json:"status_code"` +} + func CalculateWinnings(amount Currency, totalOdds float32) Currency { vat := amount.Float32() * 0.15 diff --git a/internal/domain/event.go b/internal/domain/event.go index 516c305..4ea2d1a 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -37,15 +37,15 @@ const ( type Event struct { ID string - SportID string + SportID int32 MatchName string HomeTeam string AwayTeam string - HomeTeamID string - AwayTeamID string + HomeTeamID int32 + AwayTeamID int32 HomeKitImage string AwayKitImage string - LeagueID string + LeagueID int32 LeagueName string LeagueCC string StartTime string @@ -87,15 +87,15 @@ type BetResult struct { type UpcomingEvent struct { ID string // Event ID - SportID string // Sport ID + SportID int32 // Sport ID MatchName string // Match or event name HomeTeam string // Home team name (if available) AwayTeam string // Away team name (can be empty/null) - HomeTeamID string // Home team ID - AwayTeamID string // Away team ID (can be empty/null) + HomeTeamID int32 // Home team ID + AwayTeamID int32 // Away team ID (can be empty/null) HomeKitImage string // Kit or image for home team (optional) AwayKitImage string // Kit or image for away team (optional) - LeagueID string // League ID + LeagueID int32 // League ID LeagueName string // League name LeagueCC string // League country code StartTime time.Time // Converted from "time" field in UNIX format diff --git a/internal/domain/league.go b/internal/domain/league.go index fbe4bd0..f5ac35e 100644 --- a/internal/domain/league.go +++ b/internal/domain/league.go @@ -1,66 +1,9 @@ package domain -// TODO Will make this dynamic by moving into the database - -var SupportedLeagues = []int64{ - // Football - 10041282, //Premier League - 10083364, //La Liga - 10041095, //German Bundesliga - 10041100, //Ligue 1 - 10041809, //UEFA Champions League - 10041957, //UEFA Europa League - 10079560, //UEFA Conference League - 10047168, // US MLS - 10044469, // Ethiopian Premier League - 10050282, //UEFA Nations League - 10044685, //FIFA Club World Cup - 10082328, //Kings League World Cup - 10081269, //CONCACAF Champions Cup - 10040162, //Asia - World Cup Qualifying - 10067624, //South America - World Cup Qualifying - 10067913, // Europe - World Cup Qualifying - 10067624, // South America - World Cup Qualifying - - 10043156, //England FA Cup - 10042103, //France Cup - 10041088, //Premier League 2 - 10084250, //Turkiye Super League - 10041187, //Kenya Super League - 10041315, //Italian Serie A - 10041391, //Netherlands Eredivisie - 10036538, //Spain Segunda - 10041058, //Denmark Superligaen - 10077480, //Women’s International - 10046936, // USA NPSL - 10085159, //Baller League UK - 10040601, //Argentina Cup - 10037440, //Brazil Serie A - 10043205, //Copa Sudamericana - - 10037327, //Austria Landesliga - 10082020, //USA USL League One Cup - 10037075, //International Match - 10046648, //Kenya Cup - 10040485, //Kenya Super League - 10041369, //Norway Eliteserien - - // Basketball - 173998768, //NBA - 10041830, //NBA - 10049984, //WNBA - 10037165, //German Bundesliga - 10036608, //Italian Lega 1 - 10040795, //EuroLeague - 10084178, //Kenya Premier League - 10043548, //International Women - - // Ice Hockey - 10037477, //NHL - 10037447, //AHL - 10074238, // AIHL - 10069385, //IIHF World Championship - - // Cricket - +type League struct { + ID int64 + Name string + CountryCode string + Bet365ID int32 + IsActive bool } diff --git a/internal/domain/odds.go b/internal/domain/odds.go index f02885b..ea5a3e9 100644 --- a/internal/domain/odds.go +++ b/internal/domain/odds.go @@ -1,7 +1,6 @@ package domain import ( - "encoding/json" "time" ) @@ -15,10 +14,11 @@ type Market struct { MarketName string MarketID string UpdatedAt time.Time - Odds []json.RawMessage + Odds []map[string]interface{} Name string Handicap string OddsVal float64 + Source string } type Odd struct { diff --git a/internal/domain/responses.go b/internal/domain/responses.go new file mode 100644 index 0000000..841ca37 --- /dev/null +++ b/internal/domain/responses.go @@ -0,0 +1,53 @@ +package domain + +import ( + "errors" + + "github.com/gofiber/fiber/v2" +) + +func UnProcessableEntityResponse(c *fiber.Ctx) error { + return c.Status(fiber.StatusUnprocessableEntity).JSON(Response{ + Message: "failed to parse request body", + StatusCode: fiber.StatusUnprocessableEntity, + Success: false, + }) +} + +func FiberErrorResponse(c *fiber.Ctx, err error) error { + var statusCode int + var message string + + switch { + case errors.Is(err, fiber.ErrNotFound): + statusCode = fiber.StatusNotFound + message = "resource not found" + + case errors.Is(err, fiber.ErrBadRequest): + statusCode = fiber.StatusBadRequest + message = "bad request" + + case errors.Is(err, fiber.ErrUnauthorized): + statusCode = fiber.StatusUnauthorized + message = "unauthorized" + + case errors.Is(err, fiber.ErrForbidden): + statusCode = fiber.StatusForbidden + message = "forbidden" + + case errors.Is(err, fiber.ErrConflict): + statusCode = fiber.StatusConflict + message = "conflict occurred" + + default: + statusCode = fiber.StatusInternalServerError + message = "unexpected server error" + } + + return c.Status(statusCode).JSON(fiber.Map{ + "success": false, + "status_code": statusCode, + "error": message, + "details": err.Error(), + }) +} diff --git a/internal/domain/resultres.go b/internal/domain/resultres.go index d5115a2..f00b5a4 100644 --- a/internal/domain/resultres.go +++ b/internal/domain/resultres.go @@ -9,7 +9,7 @@ type BaseResultResponse struct { Results []json.RawMessage `json:"results"` } -type League struct { +type LeagueRes struct { ID string `json:"id"` Name string `json:"name"` CC string `json:"cc"` @@ -39,14 +39,14 @@ type CommonResultResponse struct { } type FootballResultResponse struct { - ID string `json:"id"` - SportID string `json:"sport_id"` - Time string `json:"time"` - TimeStatus string `json:"time_status"` - League League `json:"league"` - Home Team `json:"home"` - Away Team `json:"away"` - SS string `json:"ss"` + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + TimeStatus string `json:"time_status"` + League LeagueRes `json:"league"` + Home Team `json:"home"` + Away Team `json:"away"` + SS string `json:"ss"` Scores struct { FirstHalf Score `json:"1"` SecondHalf Score `json:"2"` @@ -78,14 +78,14 @@ type FootballResultResponse struct { } type BasketballResultResponse struct { - ID string `json:"id"` - SportID string `json:"sport_id"` - Time string `json:"time"` - TimeStatus string `json:"time_status"` - League League `json:"league"` - Home Team `json:"home"` - Away Team `json:"away"` - SS string `json:"ss"` + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + TimeStatus string `json:"time_status"` + League LeagueRes `json:"league"` + Home Team `json:"home"` + Away Team `json:"away"` + SS string `json:"ss"` Scores struct { FirstQuarter Score `json:"1"` SecondQuarter Score `json:"2"` @@ -125,14 +125,14 @@ type BasketballResultResponse struct { Bet365ID string `json:"bet365_id"` } type IceHockeyResultResponse struct { - ID string `json:"id"` - SportID string `json:"sport_id"` - Time string `json:"time"` - TimeStatus string `json:"time_status"` - League League `json:"league"` - Home Team `json:"home"` - Away Team `json:"away"` - SS string `json:"ss"` + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + TimeStatus string `json:"time_status"` + League LeagueRes `json:"league"` + Home Team `json:"home"` + Away Team `json:"away"` + SS string `json:"ss"` Scores struct { FirstPeriod Score `json:"1"` SecondPeriod Score `json:"2"` diff --git a/internal/domain/transaction.go b/internal/domain/transaction.go index b104348..427ec81 100644 --- a/internal/domain/transaction.go +++ b/internal/domain/transaction.go @@ -63,7 +63,6 @@ type CreateTransaction struct { PaymentOption PaymentOption FullName string PhoneNumber string - // Payment Details for bank BankCode string BeneficiaryName string AccountName string 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/repository/event.go b/internal/repository/event.go index a5b854c..2ae01dd 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -21,15 +21,15 @@ func (s *Store) SaveEvent(ctx context.Context, e domain.Event) error { return s.queries.InsertEvent(ctx, dbgen.InsertEventParams{ ID: e.ID, - SportID: pgtype.Text{String: e.SportID, Valid: true}, + SportID: pgtype.Int4{Int32: e.SportID, Valid: true}, MatchName: pgtype.Text{String: e.MatchName, Valid: true}, HomeTeam: pgtype.Text{String: e.HomeTeam, Valid: true}, AwayTeam: pgtype.Text{String: e.AwayTeam, Valid: true}, - HomeTeamID: pgtype.Text{String: e.HomeTeamID, Valid: true}, - AwayTeamID: pgtype.Text{String: e.AwayTeamID, Valid: true}, + HomeTeamID: pgtype.Int4{Int32: e.HomeTeamID, Valid: true}, + AwayTeamID: pgtype.Int4{Int32: e.AwayTeamID, Valid: true}, HomeKitImage: pgtype.Text{String: e.HomeKitImage, Valid: true}, AwayKitImage: pgtype.Text{String: e.AwayKitImage, Valid: true}, - LeagueID: pgtype.Text{String: e.LeagueID, Valid: true}, + LeagueID: pgtype.Int4{Int32: e.LeagueID, Valid: true}, LeagueName: pgtype.Text{String: e.LeagueName, Valid: true}, LeagueCc: pgtype.Text{String: e.LeagueCC, Valid: true}, StartTime: pgtype.Timestamp{Time: parsedTime, Valid: true}, @@ -46,15 +46,15 @@ func (s *Store) SaveEvent(ctx context.Context, e domain.Event) error { func (s *Store) SaveUpcomingEvent(ctx context.Context, e domain.UpcomingEvent) error { return s.queries.InsertUpcomingEvent(ctx, dbgen.InsertUpcomingEventParams{ ID: e.ID, - SportID: pgtype.Text{String: e.SportID, Valid: true}, + SportID: pgtype.Int4{Int32: e.SportID, Valid: true}, MatchName: pgtype.Text{String: e.MatchName, Valid: true}, HomeTeam: pgtype.Text{String: e.HomeTeam, Valid: true}, AwayTeam: pgtype.Text{String: e.AwayTeam, Valid: true}, - HomeTeamID: pgtype.Text{String: e.HomeTeamID, Valid: true}, - AwayTeamID: pgtype.Text{String: e.AwayTeamID, Valid: true}, + HomeTeamID: pgtype.Int4{Int32: e.HomeTeamID, Valid: true}, + AwayTeamID: pgtype.Int4{Int32: e.AwayTeamID, Valid: true}, HomeKitImage: pgtype.Text{String: e.HomeKitImage, Valid: true}, AwayKitImage: pgtype.Text{String: e.AwayKitImage, Valid: true}, - LeagueID: pgtype.Text{String: e.LeagueID, Valid: true}, + LeagueID: pgtype.Int4{Int32: e.LeagueID, Valid: true}, LeagueName: pgtype.Text{String: e.LeagueName, Valid: true}, LeagueCc: pgtype.Text{String: e.LeagueCC, Valid: true}, StartTime: pgtype.Timestamp{Time: e.StartTime, Valid: true}, @@ -75,15 +75,15 @@ func (s *Store) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEven for i, e := range events { upcomingEvents[i] = domain.UpcomingEvent{ ID: e.ID, - SportID: e.SportID.String, + SportID: e.SportID.Int32, MatchName: e.MatchName.String, HomeTeam: e.HomeTeam.String, AwayTeam: e.AwayTeam.String, - HomeTeamID: e.HomeTeamID.String, - AwayTeamID: e.AwayTeamID.String, + HomeTeamID: e.HomeTeamID.Int32, + AwayTeamID: e.AwayTeamID.Int32, HomeKitImage: e.HomeKitImage.String, AwayKitImage: e.AwayKitImage.String, - LeagueID: e.LeagueID.String, + LeagueID: e.LeagueID.Int32, LeagueName: e.LeagueName.String, LeagueCC: e.LeagueCc.String, StartTime: e.StartTime.Time.UTC(), @@ -106,15 +106,15 @@ func (s *Store) GetExpiredUpcomingEvents(ctx context.Context, filter domain.Even for i, e := range events { upcomingEvents[i] = domain.UpcomingEvent{ ID: e.ID, - SportID: e.SportID.String, + SportID: e.SportID.Int32, MatchName: e.MatchName.String, HomeTeam: e.HomeTeam.String, AwayTeam: e.AwayTeam.String, - HomeTeamID: e.HomeTeamID.String, - AwayTeamID: e.AwayTeamID.String, + HomeTeamID: e.HomeTeamID.Int32, + AwayTeamID: e.AwayTeamID.Int32, HomeKitImage: e.HomeKitImage.String, AwayKitImage: e.AwayKitImage.String, - LeagueID: e.LeagueID.String, + LeagueID: e.LeagueID.Int32, LeagueName: e.LeagueName.String, LeagueCC: e.LeagueCc.String, StartTime: e.StartTime.Time.UTC(), @@ -160,15 +160,15 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.Ev for i, e := range events { upcomingEvents[i] = domain.UpcomingEvent{ ID: e.ID, - SportID: e.SportID.String, + SportID: e.SportID.Int32, MatchName: e.MatchName.String, HomeTeam: e.HomeTeam.String, AwayTeam: e.AwayTeam.String, - HomeTeamID: e.HomeTeamID.String, - AwayTeamID: e.AwayTeamID.String, + HomeTeamID: e.HomeTeamID.Int32, + AwayTeamID: e.AwayTeamID.Int32, HomeKitImage: e.HomeKitImage.String, AwayKitImage: e.AwayKitImage.String, - LeagueID: e.LeagueID.String, + LeagueID: e.LeagueID.Int32, LeagueName: e.LeagueName.String, LeagueCC: e.LeagueCc.String, StartTime: e.StartTime.Time.UTC(), @@ -208,15 +208,15 @@ func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.Upc return domain.UpcomingEvent{ ID: event.ID, - SportID: event.SportID.String, + SportID: event.SportID.Int32, MatchName: event.MatchName.String, HomeTeam: event.HomeTeam.String, AwayTeam: event.AwayTeam.String, - HomeTeamID: event.HomeTeamID.String, - AwayTeamID: event.AwayTeamID.String, + HomeTeamID: event.HomeTeamID.Int32, + AwayTeamID: event.AwayTeamID.Int32, HomeKitImage: event.HomeKitImage.String, AwayKitImage: event.AwayKitImage.String, - LeagueID: event.LeagueID.String, + LeagueID: event.LeagueID.Int32, LeagueName: event.LeagueName.String, LeagueCC: event.LeagueCc.String, StartTime: event.StartTime.Time.UTC(), diff --git a/internal/repository/league.go b/internal/repository/league.go new file mode 100644 index 0000000..7e5205f --- /dev/null +++ b/internal/repository/league.go @@ -0,0 +1,75 @@ +package repository + +import ( + "context" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/jackc/pgx/v5/pgtype" +) + +func (s *Store) SaveLeague(ctx context.Context, l domain.League) error { + return s.queries.InsertLeague(ctx, dbgen.InsertLeagueParams{ + ID: l.ID, + Name: l.Name, + CountryCode: pgtype.Text{String: l.CountryCode, Valid: true}, + Bet365ID: pgtype.Int4{Int32: l.Bet365ID, Valid: true}, + IsActive: pgtype.Bool{Bool: l.IsActive, Valid: true}, + }) +} + +func (s *Store) GetSupportedLeagues(ctx context.Context) ([]domain.League, error) { + leagues, err := s.queries.GetSupportedLeagues(ctx) + if err != nil { + return nil, err + } + + supportedLeagues := make([]domain.League, len(leagues)) + for i, league := range leagues { + supportedLeagues[i] = domain.League{ + ID: league.ID, + Name: league.Name, + CountryCode: league.CountryCode.String, + Bet365ID: league.Bet365ID.Int32, + IsActive: league.IsActive.Bool, + } + } + return supportedLeagues, nil +} + +func (s *Store) GetAllLeagues(ctx context.Context) ([]domain.League, error) { + l, err := s.queries.GetAllLeagues(ctx) + if err != nil { + return nil, err + } + + leagues := make([]domain.League, len(l)) + for i, league := range l { + leagues[i] = domain.League{ + ID: league.ID, + Name: league.Name, + CountryCode: league.CountryCode.String, + Bet365ID: league.Bet365ID.Int32, + IsActive: league.IsActive.Bool, + } + } + return leagues, nil +} + +func (s *Store) CheckLeagueSupport(ctx context.Context, leagueID int64) (bool, error) { + return s.queries.CheckLeagueSupport(ctx, leagueID) +} + +func (s *Store) SetLeagueActive(ctx context.Context, leagueId int64) error { + return s.queries.SetLeagueActive(ctx, leagueId) +} + +// TODO: update based on id, no need for the entire league (same as the set active one) +func (s *Store) SetLeagueInActive(ctx context.Context, l domain.League) error { + return s.queries.UpdateLeague(ctx, dbgen.UpdateLeagueParams{ + Name: l.Name, + CountryCode: pgtype.Text{String: l.CountryCode, Valid: true}, + Bet365ID: pgtype.Int4{Int32: l.Bet365ID, Valid: true}, + IsActive: pgtype.Bool{Bool: false, Valid: true}, + }) +} diff --git a/internal/repository/odds.go b/internal/repository/odds.go index fd20d1c..875ea97 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -17,15 +17,19 @@ func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error { return nil } - for _, raw := range m.Odds { - var item map[string]interface{} - if err := json.Unmarshal(raw, &item); err != nil { - continue - } + for _, item := range m.Odds { + var name string + var oddsVal float64 - name := getString(item["name"]) + if m.Source == "bwin" { + nameValue := getMap(item["name"]) + name = getString(nameValue["value"]) + oddsVal = getFloat(item["odds"]) + } else { + name = getString(item["name"]) + oddsVal = getConvertedFloat(item["odds"]) + } handicap := getString(item["handicap"]) - oddsVal := getFloat(item["odds"]) rawOddsBytes, _ := json.Marshal(m.Odds) @@ -43,7 +47,7 @@ func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error { Category: pgtype.Text{Valid: false}, RawOdds: rawOddsBytes, IsActive: pgtype.Bool{Bool: true, Valid: true}, - Source: pgtype.Text{String: "b365api", Valid: true}, + Source: pgtype.Text{String: m.Source, Valid: true}, FetchedAt: pgtype.Timestamp{Time: time.Now(), Valid: true}, } @@ -85,23 +89,6 @@ func writeFailedMarketLog(m domain.Market, err error) error { return writeErr } -func getString(v interface{}) string { - if s, ok := v.(string); ok { - return s - } - return "" -} - -func getFloat(v interface{}) float64 { - if s, ok := v.(string); ok { - f, err := strconv.ParseFloat(s, 64) - if err == nil { - return f - } - } - return 0 -} - func (s *Store) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) { odds, err := s.queries.GetPrematchOdds(ctx) if err != nil { @@ -286,3 +273,34 @@ func (s *Store) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID stri return domainOdds, nil } + +func getString(v interface{}) string { + if s, ok := v.(string); ok { + return s + } + return "" +} + +func getConvertedFloat(v interface{}) float64 { + if s, ok := v.(string); ok { + f, err := strconv.ParseFloat(s, 64) + if err == nil { + return f + } + } + return 0 +} + +func getFloat(v interface{}) float64 { + if n, ok := v.(float64); ok { + return n + } + return 0 +} + +func getMap(v interface{}) map[string]interface{} { + if m, ok := v.(map[string]interface{}); ok { + return m + } + return nil +} diff --git a/internal/repository/store.go b/internal/repository/store.go index 02e1403..f3e7579 100644 --- a/internal/repository/store.go +++ b/internal/repository/store.go @@ -5,6 +5,7 @@ import ( "time" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) @@ -39,3 +40,12 @@ func OpenDB(url string) (*pgxpool.Pool, func(), error) { conn.Close() }, nil } + +func (s *Store) BeginTx(ctx context.Context) (*dbgen.Queries, pgx.Tx, error) { + tx, err := s.conn.Begin(ctx) + if err != nil { + return nil, nil, err + } + q := s.queries.WithTx(tx) + return q, tx, nil +} diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index e376d66..cd84525 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -121,15 +121,11 @@ func (s *Service) GenerateBetOutcome(ctx context.Context, eventID int64, marketI if err != nil { return domain.CreateBetOutcome{}, err } - sportID, err := strconv.ParseInt(event.SportID, 10, 64) - if err != nil { - return domain.CreateBetOutcome{}, err - } newOutcome := domain.CreateBetOutcome{ EventID: eventID, OddID: oddID, MarketID: marketID, - SportID: sportID, + SportID: int64(event.SportID), HomeTeamName: event.HomeTeam, AwayTeamName: event.AwayTeam, MarketName: odds.MarketName, @@ -287,7 +283,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID return res, nil } -func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID, sportID, HomeTeam, AwayTeam string, StartTime time.Time, numMarkets int) ([]domain.CreateBetOutcome, float32, error) { +func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string, sportID int32, HomeTeam, AwayTeam string, StartTime time.Time, numMarkets int) ([]domain.CreateBetOutcome, float32, error) { var newOdds []domain.CreateBetOutcome var totalOdds float32 = 1 @@ -337,11 +333,6 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID, sportI s.logger.Error("Failed to parse odd", "error", err) continue } - sportID, err := strconv.ParseInt(sportID, 10, 64) - if err != nil { - s.logger.Error("Failed to get sport id", "error", err) - continue - } eventID, err := strconv.ParseInt(eventID, 10, 64) if err != nil { s.logger.Error("Failed to get event id", "error", err) @@ -365,7 +356,7 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID, sportI EventID: eventID, OddID: oddID, MarketID: marketID, - SportID: sportID, + SportID: int64(sportID), HomeTeamName: HomeTeam, AwayTeamName: AwayTeam, MarketName: marketName, @@ -388,7 +379,7 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID, sportI return newOdds, totalOdds, nil } -func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, leagueID, sportID domain.ValidString, firstStartTime, lastStartTime domain.ValidTime) (domain.CreateBetRes, error) { +func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, leagueID, sportID domain.ValidInt32, firstStartTime, lastStartTime domain.ValidTime) (domain.CreateBetRes, error) { // Get a unexpired event id diff --git a/internal/services/chapa/client.go b/internal/services/chapa/client.go new file mode 100644 index 0000000..8e0374f --- /dev/null +++ b/internal/services/chapa/client.go @@ -0,0 +1,126 @@ +package chapa + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type ChapaClient interface { + IssuePayment(ctx context.Context, payload domain.ChapaTransferPayload) (bool, error) + InitPayment(ctx context.Context, req domain.InitPaymentRequest) (string, error) + FetchBanks() ([]domain.ChapaSupportedBank, error) +} + +type Client struct { + BaseURL string + SecretKey string + HTTPClient *http.Client + UserAgent string +} + +func NewClient(baseURL, secretKey string) *Client { + return &Client{ + BaseURL: baseURL, + SecretKey: secretKey, + HTTPClient: http.DefaultClient, + UserAgent: "FortuneBet/1.0", + } +} + +func (c *Client) IssuePayment(ctx context.Context, payload domain.ChapaTransferPayload) (bool, error) { + payloadBytes, err := json.Marshal(payload) + if err != nil { + return false, fmt.Errorf("failed to serialize payload: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "POST", c.BaseURL+"/transfers", bytes.NewBuffer(payloadBytes)) + if err != nil { + return false, fmt.Errorf("failed to create HTTP request: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+c.SecretKey) + req.Header.Set("Content-Type", "application/json") + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return false, fmt.Errorf("chapa HTTP request failed: %w", err) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + return true, nil + } + + return false, fmt.Errorf("chapa error: status %d, body: %s", resp.StatusCode, string(body)) +} + +// service/chapa_service.go +func (c *Client) InitPayment(ctx context.Context, req domain.InitPaymentRequest) (string, error) { + payloadBytes, err := json.Marshal(req) + if err != nil { + return "", fmt.Errorf("failed to serialize payload: %w", err) + } + + httpReq, err := http.NewRequestWithContext(ctx, "POST", c.BaseURL+"/transaction/initialize", bytes.NewBuffer(payloadBytes)) + if err != nil { + return "", fmt.Errorf("failed to create HTTP request: %w", err) + } + + httpReq.Header.Set("Authorization", "Bearer "+c.SecretKey) + httpReq.Header.Set("Content-Type", "application/json") + + resp, err := c.HTTPClient.Do(httpReq) + if err != nil { + return "", fmt.Errorf("chapa HTTP request failed: %w", err) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return "", fmt.Errorf("chapa error: status %d, body: %s", resp.StatusCode, string(body)) + } + + var response struct { + Data struct { + CheckoutURL string `json:"checkout_url"` + } `json:"data"` + } + + if err := json.Unmarshal(body, &response); err != nil { + return "", fmt.Errorf("failed to parse chapa response: %w", err) + } + + return response.Data.CheckoutURL, nil +} + +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 new file mode 100644 index 0000000..0cdb213 --- /dev/null +++ b/internal/services/chapa/port.go @@ -0,0 +1,15 @@ +package chapa + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type ChapaPort interface { + HandleChapaTransferWebhook(ctx context.Context, req domain.ChapaWebHookTransfer) error + HandleChapaPaymentWebhook(ctx context.Context, req domain.ChapaWebHookPayment) error + WithdrawUsingChapa(ctx context.Context, userID int64, req domain.ChapaWithdrawRequest) error + DepositUsingChapa(ctx context.Context, userID int64, req domain.ChapaDepositRequest) (string, error) + GetSupportedBanks() ([]domain.ChapaSupportedBank, error) +} diff --git a/internal/services/chapa/service.go b/internal/services/chapa/service.go new file mode 100644 index 0000000..9c67ab4 --- /dev/null +++ b/internal/services/chapa/service.go @@ -0,0 +1,351 @@ +package chapa + +import ( + "context" + "database/sql" + "errors" + "fmt" + + // "log/slog" + "strconv" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/config" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" + referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" + "github.com/google/uuid" + "github.com/shopspring/decimal" +) + +type Service struct { + transactionStore transaction.TransactionStore + walletStore wallet.WalletStore + userStore user.UserStore + referralStore referralservice.ReferralStore + branchStore branch.BranchStore + chapaClient ChapaClient + config *config.Config + // logger *slog.Logger + store *repository.Store +} + +func NewService( + txStore transaction.TransactionStore, + walletStore wallet.WalletStore, + userStore user.UserStore, + referralStore referralservice.ReferralStore, + branchStore branch.BranchStore, + chapaClient ChapaClient, + store *repository.Store, +) *Service { + return &Service{ + transactionStore: txStore, + walletStore: walletStore, + userStore: userStore, + referralStore: referralStore, + branchStore: branchStore, + chapaClient: chapaClient, + store: store, + } +} + +func (s *Service) HandleChapaTransferWebhook(ctx context.Context, req domain.ChapaWebHookTransfer) error { + _, tx, err := s.store.BeginTx(ctx) + if err != nil { + return err + } + defer tx.Rollback(ctx) + + // Use your services normally (they don’t use the transaction, unless you wire `q`) + referenceID, err := strconv.ParseInt(req.Reference, 10, 64) + if err != nil { + return fmt.Errorf("invalid reference ID: %w", err) + } + + txn, err := s.transactionStore.GetTransactionByID(ctx, referenceID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("transaction with ID %d not found", referenceID) + } + return err + } + if txn.Verified { + return nil + } + + webhookAmount, _ := decimal.NewFromString(req.Amount) + storedAmount, _ := decimal.NewFromString(txn.Amount.String()) + if !webhookAmount.Equal(storedAmount) { + return fmt.Errorf("amount mismatch") + } + + txn.Verified = true + if err := s.transactionStore.UpdateTransactionVerified(ctx, txn.ID, txn.Verified, txn.ApprovedBy.Value, txn.ApproverName.Value); err != nil { + return err + } + + return tx.Commit(ctx) +} + +func (s *Service) HandleChapaPaymentWebhook(ctx context.Context, req domain.ChapaWebHookPayment) error { + _, tx, err := s.store.BeginTx(ctx) + if err != nil { + return err + } + defer tx.Rollback(ctx) + + if req.Status != "success" { + return fmt.Errorf("payment status not successful") + } + + // 1. Parse reference ID + referenceID, err := strconv.ParseInt(req.TxRef, 10, 64) + if err != nil { + return fmt.Errorf("invalid tx_ref: %w", err) + } + + // 2. Fetch transaction + txn, err := s.transactionStore.GetTransactionByID(ctx, referenceID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("transaction with ID %d not found", referenceID) + } + return err + } + if txn.Verified { + return nil // already processed + } + + webhookAmount, _ := strconv.ParseFloat(req.Amount, 32) + if webhookAmount < float64(txn.Amount) { + return fmt.Errorf("webhook amount is less than expected") + } + + // 4. Fetch wallet + wallet, err := s.walletStore.GetWalletByID(ctx, txn.ID) + if err != nil { + return err + } + + // 5. Update wallet balance + newBalance := wallet.Balance + txn.Amount + if err := s.walletStore.UpdateBalance(ctx, wallet.ID, newBalance); err != nil { + return err + } + + // 6. Mark transaction as verified + if err := s.transactionStore.UpdateTransactionVerified(ctx, txn.ID, true, txn.ApprovedBy.Value, txn.ApproverName.Value); err != nil { + return err + } + + // 7. Check & Create Referral + stats, err := s.referralStore.GetReferralStats(ctx, strconv.FormatInt(wallet.UserID, 10)) + if err != nil { + return err + } + + if stats == nil { + if err := s.referralStore.CreateReferral(ctx, wallet.UserID); err != nil { + return err + } + } + + return tx.Commit(ctx) +} + +func (s *Service) WithdrawUsingChapa(ctx context.Context, userID int64, req domain.ChapaWithdrawRequest) error { + _, tx, err := s.store.BeginTx(ctx) + if err != nil { + return err + } + defer tx.Rollback(ctx) + + // Get the requesting user + user, err := s.userStore.GetUserByID(ctx, userID) + if err != nil { + return fmt.Errorf("user not found: %w", err) + } + + branch, err := s.branchStore.GetBranchByID(ctx, req.BranchID) + if err != nil { + return err + } + + wallets, err := s.walletStore.GetWalletsByUser(ctx, userID) + if err != nil { + return err + } + + var targetWallet *domain.Wallet + for _, w := range wallets { + if w.ID == req.WalletID { + targetWallet = &w + break + } + } + + if targetWallet == nil { + return fmt.Errorf("no wallet found with the specified ID") + } + + if !targetWallet.IsWithdraw || !targetWallet.IsActive { + return fmt.Errorf("wallet not eligible for withdrawal") + } + + if targetWallet.Balance < domain.Currency(req.Amount) { + return fmt.Errorf("insufficient balance") + } + + txID := uuid.New().String() + + payload := domain.ChapaTransferPayload{ + AccountName: req.AccountName, + AccountNumber: req.AccountNumber, + Amount: strconv.FormatInt(req.Amount, 10), + Currency: req.Currency, + BeneficiaryName: req.BeneficiaryName, + TxRef: txID, + Reference: txID, + BankCode: req.BankCode, + } + + ok, err := s.chapaClient.IssuePayment(ctx, payload) + if err != nil || !ok { + return fmt.Errorf("chapa transfer failed: %v", err) + } + + // Create transaction using user and wallet info + _, err = s.transactionStore.CreateTransaction(ctx, domain.CreateTransaction{ + Amount: domain.Currency(req.Amount), + Type: domain.TransactionType(domain.TRANSACTION_CASHOUT), + ReferenceNumber: txID, + AccountName: req.AccountName, + AccountNumber: req.AccountNumber, + BankCode: req.BankCode, + BeneficiaryName: req.BeneficiaryName, + PaymentOption: domain.PaymentOption(domain.BANK), + BranchID: req.BranchID, + BranchName: branch.Name, + BranchLocation: branch.Location, + // CashierID: user.ID, + // CashierName: user.FullName, + FullName: user.FirstName + " " + user.LastName, + PhoneNumber: user.PhoneNumber, + CompanyID: branch.CompanyID, + }) + if err != nil { + return fmt.Errorf("failed to create transaction: %w", err) + } + + newBalance := domain.Currency(req.Amount) + err = s.walletStore.UpdateBalance(ctx, targetWallet.ID, newBalance) + if err != nil { + return fmt.Errorf("failed to update wallet balance: %w", err) + } + + return tx.Commit(ctx) +} + +func (s *Service) DepositUsingChapa(ctx context.Context, userID int64, req domain.ChapaDepositRequest) (string, error) { + _, tx, err := s.store.BeginTx(ctx) + if err != nil { + return "", err + } + defer tx.Rollback(ctx) + + user, err := s.userStore.GetUserByID(ctx, userID) + if err != nil { + return "", err + } + + branch, err := s.branchStore.GetBranchByID(ctx, req.BranchID) + if err != nil { + return "", err + } + + txID := uuid.New().String() + + _, err = s.transactionStore.CreateTransaction(ctx, domain.CreateTransaction{ + Amount: req.Amount, + Type: domain.TransactionType(domain.TRANSACTION_DEPOSIT), + ReferenceNumber: txID, + BranchID: req.BranchID, + BranchName: branch.Name, + BranchLocation: branch.Location, + FullName: user.FirstName + " " + user.LastName, + PhoneNumber: user.PhoneNumber, + CompanyID: branch.CompanyID, + }) + if err != nil { + return "", err + } + + // Fetch user details for Chapa payment + userInfo, err := s.userStore.GetUserByID(ctx, userID) + if err != nil { + return "", err + } + + // Build Chapa InitPaymentRequest (matches Chapa API) + paymentReq := domain.InitPaymentRequest{ + Amount: req.Amount, + Currency: req.Currency, + Email: userInfo.Email, + FirstName: userInfo.FirstName, + LastName: userInfo.LastName, + TxRef: txID, + CallbackURL: s.config.CHAPA_CALLBACK_URL, + ReturnURL: s.config.CHAPA_RETURN_URL, + } + + // Call Chapa to initialize payment + paymentURL, err := s.chapaClient.InitPayment(ctx, paymentReq) + if err != nil { + return "", err + } + + // Commit DB transaction + if err := tx.Commit(ctx); err != nil { + return "", err + } + + return paymentURL, nil +} + +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/event/service.go b/internal/services/event/service.go index e839885..f1165dd 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -47,7 +47,6 @@ func (s *service) FetchLiveEvents(ctx context.Context) error { s.fetchLiveEvents(ctx, url.name, url.source) }() } - wg.Wait() return nil } @@ -75,7 +74,7 @@ func (s *service) fetchLiveEvents(ctx context.Context, url, source string) error events := []domain.Event{} switch source { case "bet365": - events = handleBet365prematch(body, sportID) + events = handleBet365prematch(body, sportID, source) case "betfair": events = handleBetfairprematch(body, sportID, source) case "1xbet": @@ -97,7 +96,7 @@ func (s *service) fetchLiveEvents(ctx context.Context, url, source string) error } -func handleBet365prematch(body []byte, sportID int) []domain.Event { +func handleBet365prematch(body []byte, sportID int, source string) []domain.Event { var data struct { Success int `json:"success"` Results [][]map[string]interface{} `json:"results"` @@ -105,7 +104,7 @@ func handleBet365prematch(body []byte, sportID int) []domain.Event { events := []domain.Event{} if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { - fmt.Printf(" Decode failed for sport_id=%d\nRaw: %s\n", sportID, string(body)) + fmt.Printf("%s: Decode failed for sport_id=%d\nRaw: %s\n", source, sportID, string(body)) return events } @@ -117,24 +116,24 @@ func handleBet365prematch(body []byte, sportID int) []domain.Event { event := domain.Event{ ID: getString(ev["ID"]), - SportID: fmt.Sprintf("%d", sportID), + SportID: int32(sportID), MatchName: getString(ev["NA"]), Score: getString(ev["SS"]), MatchMinute: getInt(ev["TM"]), TimerStatus: getString(ev["TT"]), - HomeTeamID: getString(ev["HT"]), - AwayTeamID: getString(ev["AT"]), + HomeTeamID: getInt32(ev["HT"]), + AwayTeamID: getInt32(ev["AT"]), HomeKitImage: getString(ev["K1"]), AwayKitImage: getString(ev["K2"]), LeagueName: getString(ev["CT"]), - LeagueID: getString(ev["C2"]), + LeagueID: getInt32(ev["C2"]), LeagueCC: getString(ev["CB"]), StartTime: time.Now().UTC().Format(time.RFC3339), IsLive: true, Status: "live", MatchPeriod: getInt(ev["MD"]), AddedTime: getInt(ev["TA"]), - Source: "bet365", + Source: source, } events = append(events, event) @@ -152,23 +151,20 @@ func handleBetfairprematch(body []byte, sportID int, source string) []domain.Eve events := []domain.Event{} if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { - fmt.Printf(" Decode failed for sport_id=%d\nRaw: %s\n", sportID, string(body)) + fmt.Printf("%s: Decode failed for sport_id=%d\nRaw: %s\n", source, sportID, string(body)) return events } for _, ev := range data.Results { homeRaw, _ := ev["home"].(map[string]interface{}) - homeId, _ := homeRaw["id"].(string) - awayRaw, _ := ev["home"].(map[string]interface{}) - awayId, _ := awayRaw["id"].(string) event := domain.Event{ ID: getString(ev["id"]), - SportID: fmt.Sprintf("%d", sportID), + SportID: int32(sportID), TimerStatus: getString(ev["time_status"]), - HomeTeamID: homeId, - AwayTeamID: awayId, + HomeTeamID: getInt32(homeRaw["id"]), + AwayTeamID: getInt32(awayRaw["id"]), StartTime: time.Now().UTC().Format(time.RFC3339), IsLive: true, Status: "live", @@ -221,8 +217,8 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour log.Printf("Sport ID %d", sportID) for page <= totalPages { page = page + 1 - url := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s&page=%d", sportID, s.token, page) - log.Printf("📡 Fetching data for sport %d at page %d", sportID, page) + url := fmt.Sprintf(url, sportID, s.token, page) + log.Printf("📡 Fetching data from %s - sport %d, for event data page %d", source, sportID, page) resp, err := http.Get(url) if err != nil { log.Printf("❌ Failed to fetch event data for page %d: %v", page, err) @@ -246,33 +242,35 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour // continue // } - // leagueID, err := strconv.ParseInt(ev.League.ID, 10, 64) - // if err != nil { - // log.Printf("❌ Invalid league id, leagueID %v", ev.League.ID) - // continue - // } + leagueID, err := strconv.ParseInt(ev.League.ID, 10, 64) + if err != nil { + log.Printf("❌ Invalid league id, leagueID %v", ev.League.ID) + continue + } - // if !slices.Contains(domain.SupportedLeagues, leagueID) { - // // fmt.Printf("⚠️ Skipping league %s (%d) as it is not supported\n", ev.League.Name, leagueID) - // _, err = fmt.Fprintf(b, "Skipped league %s (%d) in sport %d\n", ev.League.Name, leagueID, sportID) - // if err != nil { - // fmt.Printf(" Error while logging skipped league") - // } - // skippedLeague = append(skippedLeague, ev.League.Name) - // continue - // } + // doesn't make sense to save and check back to back, but for now it can be here + s.store.SaveLeague(ctx, domain.League{ + ID: leagueID, + Name: ev.League.Name, + IsActive: true, + }) + + if supported, err := s.store.CheckLeagueSupport(ctx, leagueID); !supported || err != nil { + skippedLeague = append(skippedLeague, ev.League.Name) + continue + } event := domain.UpcomingEvent{ ID: ev.ID, - SportID: ev.SportID, + SportID: convertInt32(ev.SportID), MatchName: "", HomeTeam: ev.Home.Name, AwayTeam: "", // handle nil safely - HomeTeamID: ev.Home.ID, - AwayTeamID: "", + HomeTeamID: convertInt32(ev.Home.ID), + AwayTeamID: 0, HomeKitImage: "", AwayKitImage: "", - LeagueID: ev.League.ID, + LeagueID: convertInt32(ev.League.ID), LeagueName: ev.League.Name, LeagueCC: "", StartTime: time.Unix(startUnix, 0).UTC(), @@ -281,7 +279,7 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour if ev.Away != nil { event.AwayTeam = ev.Away.Name - event.AwayTeamID = ev.Away.ID + event.AwayTeamID = convertInt32(ev.Away.ID) event.MatchName = ev.Home.Name + " vs " + ev.Away.Name } @@ -319,6 +317,20 @@ func getInt(v interface{}) int { } return 0 } + +func getInt32(v interface{}) int32 { + if n, err := strconv.Atoi(getString(v)); err == nil { + return int32(n) + } + return 0 +} + +func convertInt32(num string) int32 { + if n, err := strconv.Atoi(num); err == nil { + return int32(n) + } + return 0 +} func (s *service) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) { return s.store.GetAllUpcomingEvents(ctx) } diff --git a/internal/services/league/port.go b/internal/services/league/port.go new file mode 100644 index 0000000..7b71a48 --- /dev/null +++ b/internal/services/league/port.go @@ -0,0 +1,12 @@ +package league + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type Service interface { + GetAllLeagues(ctx context.Context) ([]domain.League, error) + SetLeagueActive(ctx context.Context, leagueId int64) error +} diff --git a/internal/services/league/service.go b/internal/services/league/service.go new file mode 100644 index 0000000..b1f05ed --- /dev/null +++ b/internal/services/league/service.go @@ -0,0 +1,26 @@ +package league + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" +) + +type service struct { + store *repository.Store +} + +func New(store *repository.Store) Service { + return &service{ + store: store, + } +} + +func (s *service) GetAllLeagues(ctx context.Context) ([]domain.League, error) { + return s.store.GetAllLeagues(ctx) +} + +func (s *service) SetLeagueActive(ctx context.Context, leagueId int64) error { + return s.store.SetLeagueActive(ctx, leagueId) +} diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 3d3a6d3..6bc3305 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -10,6 +10,7 @@ import ( "log/slog" "net/http" "strconv" + "sync" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" @@ -35,6 +36,38 @@ func New(store *repository.Store, cfg *config.Config, logger *slog.Logger) *Serv // TODO Add the optimization to get 10 events at the same time func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { + var wg sync.WaitGroup + errChan := make(chan error, 2) + // wg.Add(2) + wg.Add(1) + + // go func() { + // defer wg.Done() + // if err := s.fetchBet365Odds(ctx); err != nil { + // errChan <- fmt.Errorf("bet365 odds fetching error: %w", err) + // } + // }() + + go func() { + defer wg.Done() + if err := s.fetchBwinOdds(ctx); err != nil { + errChan <- fmt.Errorf("bwin odds fetching error: %w", err) + } + }() + + var errs []error + for err := range errChan { + errs = append(errs, err) + } + + if len(errs) > 0 { + return errors.Join(errs...) + } + + return nil +} + +func (s *ServiceImpl) fetchBet365Odds(ctx context.Context) error { eventIDs, err := s.store.GetAllUpcomingEvents(ctx) if err != nil { log.Printf("❌ Failed to fetch upcoming event IDs: %v", err) @@ -95,6 +128,180 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { return errors.Join(errs...) } +func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error { + // getting odds for a specific event is not possible for bwin, most specific we can get is fetch odds on a single sport + // so instead of having event and odds fetched separetly event will also be fetched along with the odds + sportIds := []int{4, 12, 7} + for _, sportId := range sportIds { + url := fmt.Sprintf("https://api.b365api.com/v1/bwin/prematch?sport_id=%d&token=%s", sportId, s.config.Bet365Token) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + log.Printf("❌ Failed to create request for sportId %d: %v", sportId, err) + continue + } + + resp, err := s.client.Do(req) + if err != nil { + log.Printf("❌ Failed to fetch request for sportId %d: %v", sportId, err) + continue + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("❌ Failed to read response body for sportId %d: %v", sportId, err) + continue + } + + var data struct { + Success int `json:"success"` + Results []map[string]interface{} `json:"results"` + } + + if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { + fmt.Printf("Decode failed for sport_id=%d\nRaw: %s\n", sportId, string(body)) + continue + } + + for _, res := range data.Results { + if getInt(res["Id"]) == -1 { + continue + } + + event := domain.Event{ + ID: strconv.Itoa(getInt(res["Id"])), + SportID: int32(getInt(res["SportId"])), + LeagueID: int32(getInt(res["LeagueId"])), + LeagueName: getString(res["Leaguename"]), + HomeTeam: getString(res["HomeTeam"]), + HomeTeamID: int32(getInt(res["HomeTeamId"])), + AwayTeam: getString(res["AwayTeam"]), + AwayTeamID: int32(getInt(res["AwayTeamId"])), + StartTime: time.Now().UTC().Format(time.RFC3339), + TimerStatus: "1", + IsLive: true, + Status: "live", + Source: "bwin", + } + + if err := s.store.SaveEvent(ctx, event); err != nil { + fmt.Printf("Could not store live event [id=%s]: %v\n", event.ID, err) + continue + } + + for _, market := range []string{"Markets, optionMarkets"} { + for _, m := range getMapArray(res[market]) { + name := getMap(m["name"]) + marketName := getString(name["value"]) + + market := domain.Market{ + EventID: event.ID, + MarketID: getString(m["id"]), + MarketCategory: getString(m["category"]), + MarketName: marketName, + Source: "bwin", + } + + results := getMapArray(m["results"]) + market.Odds = results + + s.store.SaveNonLiveMarket(ctx, market) + + } + } + } + + } + return nil +} + +func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error { + // getting odds for a specific event is not possible for bwin, most specific we can get is fetch odds on a single sport + // so instead of having event and odds fetched separetly event will also be fetched along with the odds + sportIds := []int{4, 12, 7} + for _, sportId := range sportIds { + url := fmt.Sprintf("https://api.b365api.com/v1/bwin/prematch?sport_id=%d&token=%s", sportId, s.config.Bet365Token) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + log.Printf("❌ Failed to create request for sportId %d: %v", sportId, err) + continue + } + + resp, err := s.client.Do(req) + if err != nil { + log.Printf("❌ Failed to fetch request for sportId %d: %v", sportId, err) + continue + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("❌ Failed to read response body for sportId %d: %v", sportId, err) + continue + } + + var data struct { + Success int `json:"success"` + Results []map[string]interface{} `json:"results"` + } + + if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { + fmt.Printf("Decode failed for sport_id=%d\nRaw: %s\n", sportId, string(body)) + continue + } + + for _, res := range data.Results { + if getInt(res["Id"]) == -1 { + continue + } + + event := domain.Event{ + ID: strconv.Itoa(getInt(res["Id"])), + SportID: int32(getInt(res["SportId"])), + LeagueID: int32(getInt(res["LeagueId"])), + LeagueName: getString(res["Leaguename"]), + HomeTeam: getString(res["HomeTeam"]), + HomeTeamID: int32(getInt(res["HomeTeamId"])), + AwayTeam: getString(res["AwayTeam"]), + AwayTeamID: int32(getInt(res["AwayTeamId"])), + StartTime: time.Now().UTC().Format(time.RFC3339), + TimerStatus: "1", + IsLive: true, + Status: "live", + Source: "bwin", + } + + if err := s.store.SaveEvent(ctx, event); err != nil { + fmt.Printf("Could not store live event [id=%s]: %v\n", event.ID, err) + continue + } + + for _, market := range []string{"Markets, optionMarkets"} { + for _, m := range getMapArray(res[market]) { + name := getMap(m["name"]) + marketName := getString(name["value"]) + + market := domain.Market{ + EventID: event.ID, + MarketID: getString(m["id"]), + MarketCategory: getString(m["category"]), + MarketName: marketName, + Source: "bwin", + } + + results := getMapArray(m["results"]) + market.Odds = results + + s.store.SaveNonLiveMarket(ctx, market) + + } + } + } + + } + return nil +} + func (s *ServiceImpl) FetchNonLiveOddsByEventID(ctx context.Context, eventIDStr string) (domain.BaseNonLiveOddResponse, error) { eventID, err := strconv.ParseInt(eventIDStr, 10, 64) @@ -336,6 +543,13 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName continue } + marketOdds, err := convertRawMessage(market.Odds) + if err != nil { + s.logger.Error("failed to conver json.RawMessage to []map[string]interface{} for market_id: ", market.ID) + errs = append(errs, err) + continue + } + marketRecord := domain.Market{ EventID: eventID, FI: fi, @@ -344,7 +558,9 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName MarketName: market.Name, MarketID: marketIDstr, UpdatedAt: updatedAt, - Odds: market.Odds, + Odds: marketOdds, + // bwin won't reach this code so bet365 is hardcoded for now + Source: "bet365", } err = s.store.SaveNonLiveMarket(ctx, marketRecord) @@ -385,3 +601,49 @@ func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingI func (s *ServiceImpl) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset domain.ValidInt64) ([]domain.Odd, error) { return s.store.GetPaginatedPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset) } + +func getString(v interface{}) string { + if str, ok := v.(string); ok { + return str + } + return "" +} + +func getInt(v interface{}) int { + if n, ok := v.(float64); ok { + return int(n) + } + return -1 +} + +func getMap(v interface{}) map[string]interface{} { + if m, ok := v.(map[string]interface{}); ok { + return m + } + return nil +} + +func getMapArray(v interface{}) []map[string]interface{} { + result := []map[string]interface{}{} + if arr, ok := v.([]interface{}); ok { + for _, item := range arr { + if m, ok := item.(map[string]interface{}); ok { + result = append(result, m) + } + } + } + return result +} + +func convertRawMessage(rawMessages []json.RawMessage) ([]map[string]interface{}, error) { + var result []map[string]interface{} + for _, raw := range rawMessages { + var m map[string]interface{} + if err := json.Unmarshal(raw, &m); err != nil { + return nil, err + } + result = append(result, m) + } + + return result, nil +} diff --git a/internal/services/result/sports_eval.go b/internal/services/result/sports_eval.go index eeb23f7..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,8 +233,8 @@ 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 -func evaluateBaseballFirstInning(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { +// EvaluateBaseballFirstInning Evaluates Baseball first inning bets +func EvaluateBaseballFirstInning(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { switch outcome.OddHeader { case "1": if score.Home > score.Away { @@ -256,8 +256,8 @@ func evaluateBaseballFirstInning(outcome domain.BetOutcome, score struct{ Home, } } -// evaluateBaseballFirst5Innings evaluates Baseball first 5 innings bets -func evaluateBaseballFirst5Innings(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { +// 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": if score.Home > score.Away { diff --git a/internal/services/result/sports_eval_test.go b/internal/services/result/sports_eval_test.go index 4810385..9132658 100644 --- a/internal/services/result/sports_eval_test.go +++ b/internal/services/result/sports_eval_test.go @@ -29,75 +29,75 @@ func TestNFLMarkets(t *testing.T) { case int64(domain.AMERICAN_FOOTBALL_MONEY_LINE): // Home win, away win, draw, and invalid OddHeader for Money Line t.Run("Home Win", func(t *testing.T) { - status, err := evaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 21, Away: 14}) + status, err := EvaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 21, Away: 14}) t.Logf("Market: %s, Scenario: Home Win", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) }) t.Run("Away Win", func(t *testing.T) { - status, err := evaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 14, Away: 21}) + status, err := EvaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 14, Away: 21}) t.Logf("Market: %s, Scenario: Away Win", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) }) t.Run("Draw", func(t *testing.T) { - status, err := evaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 17, Away: 17}) + status, err := EvaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 17, Away: 17}) t.Logf("Market: %s, Scenario: Draw", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_LOSS, status) }) t.Run("Invalid OddHeader", func(t *testing.T) { - status, err := evaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7}) + status, err := EvaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7}) t.Logf("Market: %s, Scenario: Invalid OddHeader", m.name) assert.Error(t, err) assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) }) case int64(domain.AMERICAN_FOOTBALL_SPREAD): t.Run("Home Win with Handicap", func(t *testing.T) { - status, err := evaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-3.5"}, struct{ Home, Away int }{Home: 24, Away: 20}) + status, err := EvaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-3.5"}, struct{ Home, Away int }{Home: 24, Away: 20}) t.Logf("Market: %s, Scenario: Home Win with Handicap", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) }) t.Run("Away Win with Handicap", func(t *testing.T) { - status, err := evaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+3.5"}, struct{ Home, Away int }{Home: 20, Away: 24}) + status, err := EvaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+3.5"}, struct{ Home, Away int }{Home: 20, Away: 24}) t.Logf("Market: %s, Scenario: Away Win with Handicap", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) }) t.Run("Push (Void)", func(t *testing.T) { - status, err := evaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 21, Away: 21}) + status, err := EvaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 21, Away: 21}) t.Logf("Market: %s, Scenario: Push (Void)", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) }) t.Run("Non-numeric Handicap", func(t *testing.T) { - status, err := evaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 21, Away: 14}) + status, err := EvaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 21, Away: 14}) t.Logf("Market: %s, Scenario: Non-numeric Handicap", m.name) assert.Error(t, err) assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) }) case int64(domain.AMERICAN_FOOTBALL_TOTAL_POINTS): t.Run("Over Win", func(t *testing.T) { - status, err := evaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "44.5"}, struct{ Home, Away int }{Home: 30, Away: 20}) + status, err := EvaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "44.5"}, struct{ Home, Away int }{Home: 30, Away: 20}) t.Logf("Market: %s, Scenario: Over Win", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) }) t.Run("Under Win", func(t *testing.T) { - status, err := evaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "44.5"}, struct{ Home, Away int }{Home: 20, Away: 17}) + status, err := EvaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "44.5"}, struct{ Home, Away int }{Home: 20, Away: 17}) t.Logf("Market: %s, Scenario: Under Win", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) }) t.Run("Push (Void)", func(t *testing.T) { - status, err := evaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "37"}, struct{ Home, Away int }{Home: 20, Away: 17}) + status, err := EvaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "37"}, struct{ Home, Away int }{Home: 20, Away: 17}) t.Logf("Market: %s, Scenario: Push (Void)", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) }) t.Run("Non-numeric OddName", func(t *testing.T) { - status, err := evaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 20, Away: 17}) + status, err := EvaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 20, Away: 17}) t.Logf("Market: %s, Scenario: Non-numeric OddName", m.name) assert.Error(t, err) assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) @@ -128,75 +128,75 @@ func TestRugbyMarkets(t *testing.T) { case int64(domain.RUGBY_MONEY_LINE): // Home win, away win, draw, and invalid OddHeader for Money Line t.Run("Home Win", func(t *testing.T) { - status, err := evaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 30, Away: 20}) + status, err := EvaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 30, Away: 20}) t.Logf("Market: %s, Scenario: Home Win", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) }) t.Run("Away Win", func(t *testing.T) { - status, err := evaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 20, Away: 30}) + status, err := EvaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 20, Away: 30}) t.Logf("Market: %s, Scenario: Away Win", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) }) t.Run("Draw", func(t *testing.T) { - status, err := evaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 25, Away: 25}) + status, err := EvaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 25, Away: 25}) t.Logf("Market: %s, Scenario: Draw", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_LOSS, status) }) t.Run("Invalid OddHeader", func(t *testing.T) { - status, err := evaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7}) + status, err := EvaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7}) t.Logf("Market: %s, Scenario: Invalid OddHeader", m.name) assert.Error(t, err) assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) }) case int64(domain.RUGBY_SPREAD), int64(domain.RUGBY_HANDICAP): t.Run("Home Win with Handicap", func(t *testing.T) { - status, err := evaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-6.5"}, struct{ Home, Away int }{Home: 28, Away: 20}) + status, err := EvaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-6.5"}, struct{ Home, Away int }{Home: 28, Away: 20}) t.Logf("Market: %s, Scenario: Home Win with Handicap", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) }) t.Run("Away Win with Handicap", func(t *testing.T) { - status, err := evaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+6.5"}, struct{ Home, Away int }{Home: 20, Away: 28}) + status, err := EvaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+6.5"}, struct{ Home, Away int }{Home: 20, Away: 28}) t.Logf("Market: %s, Scenario: Away Win with Handicap", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) }) t.Run("Push (Void)", func(t *testing.T) { - status, err := evaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 21, Away: 21}) + status, err := EvaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 21, Away: 21}) t.Logf("Market: %s, Scenario: Push (Void)", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) }) t.Run("Non-numeric Handicap", func(t *testing.T) { - status, err := evaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 21, Away: 14}) + status, err := EvaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 21, Away: 14}) t.Logf("Market: %s, Scenario: Non-numeric Handicap", m.name) assert.Error(t, err) assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) }) case int64(domain.RUGBY_TOTAL_POINTS): t.Run("Over Win", func(t *testing.T) { - status, err := evaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "40.5"}, struct{ Home, Away int }{Home: 25, Away: 20}) + status, err := EvaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "40.5"}, struct{ Home, Away int }{Home: 25, Away: 20}) t.Logf("Market: %s, Scenario: Over Win", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) }) t.Run("Under Win", func(t *testing.T) { - status, err := evaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "40.5"}, struct{ Home, Away int }{Home: 15, Away: 20}) + status, err := EvaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "40.5"}, struct{ Home, Away int }{Home: 15, Away: 20}) t.Logf("Market: %s, Scenario: Under Win", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) }) t.Run("Push (Void)", func(t *testing.T) { - status, err := evaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "35"}, struct{ Home, Away int }{Home: 20, Away: 15}) + status, err := EvaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "35"}, struct{ Home, Away int }{Home: 20, Away: 15}) t.Logf("Market: %s, Scenario: Push (Void)", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) }) t.Run("Non-numeric OddName", func(t *testing.T) { - status, err := evaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 20, Away: 15}) + status, err := EvaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 20, Away: 15}) t.Logf("Market: %s, Scenario: Non-numeric OddName", m.name) assert.Error(t, err) assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) @@ -226,75 +226,75 @@ func TestBaseballMarkets(t *testing.T) { case int64(domain.BASEBALL_MONEY_LINE): // Home win, away win, draw, and invalid OddHeader for Money Line t.Run("Home Win", func(t *testing.T) { - status, err := evaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 6, Away: 3}) + status, err := EvaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 6, Away: 3}) t.Logf("Market: %s, Scenario: Home Win", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) }) t.Run("Away Win", func(t *testing.T) { - status, err := evaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 2, Away: 5}) + status, err := EvaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 2, Away: 5}) t.Logf("Market: %s, Scenario: Away Win", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) }) t.Run("Draw", func(t *testing.T) { - status, err := evaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 4, Away: 4}) + status, err := EvaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 4, Away: 4}) t.Logf("Market: %s, Scenario: Draw", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_LOSS, status) }) t.Run("Invalid OddHeader", func(t *testing.T) { - status, err := evaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7}) + status, err := EvaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7}) t.Logf("Market: %s, Scenario: Invalid OddHeader", m.name) assert.Error(t, err) assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) }) case int64(domain.BASEBALL_SPREAD): t.Run("Home Win with Handicap", func(t *testing.T) { - status, err := evaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-1.5"}, struct{ Home, Away int }{Home: 5, Away: 3}) + status, err := EvaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-1.5"}, struct{ Home, Away int }{Home: 5, Away: 3}) t.Logf("Market: %s, Scenario: Home Win with Handicap", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) }) t.Run("Away Win with Handicap", func(t *testing.T) { - status, err := evaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+1.5"}, struct{ Home, Away int }{Home: 3, Away: 5}) + status, err := EvaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+1.5"}, struct{ Home, Away int }{Home: 3, Away: 5}) t.Logf("Market: %s, Scenario: Away Win with Handicap", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) }) t.Run("Push (Void)", func(t *testing.T) { - status, err := evaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 4, Away: 4}) + status, err := EvaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 4, Away: 4}) t.Logf("Market: %s, Scenario: Push (Void)", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) }) t.Run("Non-numeric Handicap", func(t *testing.T) { - status, err := evaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 5, Away: 3}) + status, err := EvaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 5, Away: 3}) t.Logf("Market: %s, Scenario: Non-numeric Handicap", m.name) assert.Error(t, err) assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) }) case int64(domain.BASEBALL_TOTAL_RUNS): t.Run("Over Win", func(t *testing.T) { - status, err := evaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "7.5"}, struct{ Home, Away int }{Home: 5, Away: 4}) + status, err := EvaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "7.5"}, struct{ Home, Away int }{Home: 5, Away: 4}) t.Logf("Market: %s, Scenario: Over Win", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) }) t.Run("Under Win", func(t *testing.T) { - status, err := evaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "7.5"}, struct{ Home, Away int }{Home: 2, Away: 3}) + status, err := EvaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "7.5"}, struct{ Home, Away int }{Home: 2, Away: 3}) t.Logf("Market: %s, Scenario: Under Win", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) }) t.Run("Push (Void)", func(t *testing.T) { - status, err := evaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "7"}, struct{ Home, Away int }{Home: 4, Away: 3}) + status, err := EvaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "7"}, struct{ Home, Away int }{Home: 4, Away: 3}) t.Logf("Market: %s, Scenario: Push (Void)", m.name) assert.NoError(t, err) assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) }) t.Run("Non-numeric OddName", func(t *testing.T) { - status, err := evaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 4, Away: 3}) + status, err := EvaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 4, Away: 3}) t.Logf("Market: %s, Scenario: Non-numeric OddName", m.name) assert.Error(t, err) assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) @@ -338,7 +338,7 @@ func TestEvaluateFootballOutcome(t *testing.T) { } // Act - status, _ := service.evaluateFootballOutcome(outcome, finalScore, firstHalfScore, secondHalfScore, corners, halfTimeCorners, events) + status, _ := service.EvaluateFootballOutcome(outcome, finalScore, firstHalfScore, secondHalfScore, corners, halfTimeCorners, events) fmt.Printf("\n\nBet Outcome: %v\n\n", &status) @@ -357,7 +357,7 @@ func TestEvaluateTotalLegs(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - status, _ := evaluateTotalLegs(tt.outcome, tt.score) + status, _ := EvaluateTotalLegs(tt.outcome, tt.score) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) } @@ -380,7 +380,7 @@ func TestEvaluateGameLines(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - status, _ := evaluateGameLines(tt.outcome, tt.score) + status, _ := EvaluateGameLines(tt.outcome, tt.score) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) } @@ -408,7 +408,7 @@ func TestEvaluateFirstTeamToScore(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - status, _ := evaluateFirstTeamToScore(tt.outcome, tt.events) + status, _ := EvaluateFirstTeamToScore(tt.outcome, tt.events) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) } @@ -430,7 +430,7 @@ func TestEvaluateGoalsOverUnder(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - status, _ := evaluateGoalsOverUnder(tt.outcome, tt.score) + status, _ := EvaluateGoalsOverUnder(tt.outcome, tt.score) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) } @@ -452,7 +452,7 @@ func TestEvaluateGoalsOddEven(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - status, _ := evaluateGoalsOddEven(tt.outcome, tt.score) + status, _ := EvaluateGoalsOddEven(tt.outcome, tt.score) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) } @@ -474,7 +474,7 @@ func TestEvaluateCorrectScore(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - status, _ := evaluateCorrectScore(tt.outcome, tt.score) + status, _ := EvaluateCorrectScore(tt.outcome, tt.score) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) } @@ -498,7 +498,7 @@ func TestEvaluateHighestScoringHalf(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - status, _ := evaluateHighestScoringHalf(tt.outcome, tt.firstScore, tt.secondScore) + status, _ := EvaluateHighestScoringHalf(tt.outcome, tt.firstScore, tt.secondScore) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) } @@ -551,7 +551,7 @@ func TestEvaluateHighestScoringQuarter(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - status, _ := evaluateHighestScoringQuarter(tt.outcome, tt.firstScore, tt.secondScore, tt.thirdScore, tt.fourthScore) + status, _ := EvaluateHighestScoringQuarter(tt.outcome, tt.firstScore, tt.secondScore, tt.thirdScore, tt.fourthScore) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) } @@ -577,7 +577,7 @@ func TestEvaluateWinningMargin(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - status, _ := evaluateWinningMargin(tt.outcome, tt.score) + status, _ := EvaluateWinningMargin(tt.outcome, tt.score) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) } @@ -605,7 +605,7 @@ func TestEvaluateDoubleResult(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - status, _ := evaluateDoubleResult(tt.outcome, tt.firstHalfScore, tt.fullTimeScore) + status, _ := EvaluateDoubleResult(tt.outcome, tt.firstHalfScore, tt.fullTimeScore) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) } @@ -632,7 +632,7 @@ func TestEvaluateHighestScoringPeriod(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - status, _ := evaluateHighestScoringPeriod(tt.outcome, tt.firstScore, tt.secondScore, tt.thirdScore) + status, _ := EvaluateHighestScoringPeriod(tt.outcome, tt.firstScore, tt.secondScore, tt.thirdScore) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) } @@ -656,7 +656,7 @@ func TestEvalauteTiedAfterRegulation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - status, _ := evaluateTiedAfterRegulation(tt.outcome, tt.score) + status, _ := EvaluateTiedAfterRegulation(tt.outcome, tt.score) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) } @@ -680,7 +680,7 @@ func TestEvaluateTeamTotal(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - status, _ := evaluateTeamTotal(tt.outcome, tt.score) + status, _ := EvaluateTeamTotal(tt.outcome, tt.score) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) } @@ -703,7 +703,7 @@ func TestDrawNoBet(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - status, _ := evaluateDrawNoBet(tt.outcome, tt.score) + status, _ := EvaluateDrawNoBet(tt.outcome, tt.score) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) } @@ -727,7 +727,7 @@ func TestEvaluateMoneyLine(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - status, _ := evaluateMoneyLine(tt.outcome, tt.score) + status, _ := EvaluateMoneyLine(tt.outcome, tt.score) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) } @@ -751,7 +751,7 @@ func TestEvaluateDoubleChance(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - status, _ := evaluateDoubleChance(tt.outcome, tt.score) + status, _ := EvaluateDoubleChance(tt.outcome, tt.score) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) } @@ -775,7 +775,7 @@ func TestEvaluateResultAndTotal(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - status, _ := evaluateResultAndTotal(tt.outcome, tt.score) + status, _ := EvaluateResultAndTotal(tt.outcome, tt.score) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) } @@ -826,7 +826,7 @@ func TestEvaluateBTTSX(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - status, _ := evaluateBTTSX(tt.outcome, tt.score) + status, _ := EvaluateBTTSX(tt.outcome, tt.score) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) } @@ -852,7 +852,7 @@ func TestEvaluateResultAndBTTSX(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - status, _ := evaluateResultAndBTTSX(tt.outcome, tt.score) + status, _ := EvaluateResultAndBTTSX(tt.outcome, tt.score) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) } @@ -876,7 +876,7 @@ func TestEvaluateMoneyLine3Way(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - status, _ := evaluateMoneyLine3Way(tt.outcome, tt.score) + status, _ := EvaluateMoneyLine3Way(tt.outcome, tt.score) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) } @@ -937,7 +937,7 @@ func TestEvaluateAsianHandicap(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - status, _ := evaluateAsianHandicap(tt.outcome, tt.score) + status, _ := EvaluateAsianHandicap(tt.outcome, tt.score) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) } @@ -971,7 +971,7 @@ func TestEvaluateHandicapAndTotal(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - status, _ := evaluateHandicapAndTotal(tt.outcome, tt.score) + status, _ := EvaluateHandicapAndTotal(tt.outcome, tt.score) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) } diff --git a/internal/web_server/app.go b/internal/web_server/app.go index 18e4411..0a50ef7 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -8,8 +8,10 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation" referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" @@ -44,6 +46,7 @@ type App struct { userSvc *user.Service betSvc *bet.Service virtualGameSvc virtualgameservice.VirtualGameService + chapaSvc *chapa.Service walletSvc *wallet.Service transactionSvc *transaction.Service ticketSvc *ticket.Service @@ -54,6 +57,7 @@ type App struct { Logger *slog.Logger prematchSvc *odds.ServiceImpl eventSvc event.Service + leagueSvc league.Service resultSvc *result.Service } @@ -65,6 +69,7 @@ func NewApp( userSvc *user.Service, ticketSvc *ticket.Service, betSvc *bet.Service, + chapaSvc *chapa.Service, walletSvc *wallet.Service, transactionSvc *transaction.Service, branchSvc *branch.Service, @@ -72,6 +77,7 @@ func NewApp( notidicationStore *notificationservice.Service, prematchSvc *odds.ServiceImpl, eventSvc event.Service, + leagueSvc league.Service, referralSvc referralservice.ReferralStore, virtualGameSvc virtualgameservice.VirtualGameService, aleaVirtualGameService alea.AleaVirtualGameService, @@ -104,6 +110,7 @@ func NewApp( userSvc: userSvc, ticketSvc: ticketSvc, betSvc: betSvc, + chapaSvc: chapaSvc, walletSvc: walletSvc, transactionSvc: transactionSvc, branchSvc: branchSvc, @@ -113,6 +120,7 @@ func NewApp( Logger: logger, prematchSvc: prematchSvc, eventSvc: eventSvc, + leagueSvc: leagueSvc, virtualGameSvc: virtualGameSvc, aleaVirtualGameService: aleaVirtualGameService, veliVirtualGameService: veliVirtualGameService, diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index 02dd822..18ccca6 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -72,18 +72,28 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) // role := c.Locals("role").(domain.Role) - leagueIDQuery := c.Query("league_id") - sportIDQuery := c.Query("sport_id") + leagueIDQuery, err := strconv.Atoi(c.Query("league_id")) + if err != nil { + h.logger.Error("invalid league id", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, nil) + } + + sportIDQuery, err := strconv.Atoi(c.Query("sport_id")) + if err != nil { + h.logger.Error("invalid sport id", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "invalid sport id", nil, nil) + } + firstStartTimeQuery := c.Query("first_start_time") lastStartTimeQuery := c.Query("last_start_time") - leagueID := domain.ValidString{ - Value: leagueIDQuery, - Valid: leagueIDQuery != "", + leagueID := domain.ValidInt32{ + Value: int32(leagueIDQuery), + Valid: leagueIDQuery != 0, } - sportID := domain.ValidString{ - Value: sportIDQuery, - Valid: sportIDQuery != "", + sportID := domain.ValidInt32{ + Value: int32(sportIDQuery), + Valid: sportIDQuery != 0, } var firstStartTime domain.ValidTime @@ -123,7 +133,6 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error { } var res domain.CreateBetRes - var err error for i := 0; i < int(req.NumberOfBets); i++ { res, err = h.betSvc.PlaceRandomBet(c.Context(), userID, req.BranchID, leagueID, sportID, firstStartTime, lastStartTime) diff --git a/internal/web_server/handlers/chapa.go b/internal/web_server/handlers/chapa.go index 3fc66c0..7c03183 100644 --- a/internal/web_server/handlers/chapa.go +++ b/internal/web_server/handlers/chapa.go @@ -1,283 +1,464 @@ package handlers import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" + // "bytes" + // "encoding/json" + // "fmt" + // "io" + // "net/http" + "fmt" + + "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 +// // 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) + +// // Optional: you can verify tx_ref here again if needed + +// 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(), +// }) +// } + +// // 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(), +// }) +// } + +// 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") + +// 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(), +// }) +// } + +// 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", +// }) +// } + +// 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.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() + +// 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) +// } + +// VerifyChapaPayment godoc +// @Summary Verifies Chapa webhook transaction // @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(), - }) +// @Param payload body domain.ChapaTransactionType true "Webhook Payload" +// @Success 200 {object} domain.Response +// @Router /api/v1/chapa/payments/verify [post] +func (h *Handler) VerifyChapaPayment(c *fiber.Ctx) error { + var txType domain.ChapaTransactionType + if err := c.BodyParser(&txType); err != nil { + return domain.UnProcessableEntityResponse(c) } - h.logger.Info("Chapa webhook received", "payload", payload) + switch txType.Type { + case "Payout": + var payload domain.ChapaWebHookTransfer + if err := c.BodyParser(&payload); err != nil { + return domain.UnProcessableEntityResponse(c) + } - // Optional: you can verify tx_ref here again if needed + if err := h.chapaSvc.HandleChapaTransferWebhook(c.Context(), payload); err != nil { + return domain.FiberErrorResponse(c, err) + } - return c.SendStatus(fiber.StatusOK) + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Chapa transfer verified successfully", + Success: true, + StatusCode: fiber.StatusOK, + }) + + case "API": + var payload domain.ChapaWebHookPayment + if err := c.BodyParser(&payload); err != nil { + return domain.UnProcessableEntityResponse(c) + } + + if err := h.chapaSvc.HandleChapaPaymentWebhook(c.Context(), payload); err != nil { + return domain.FiberErrorResponse(c, err) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Chapa payment verified successfully", + Success: true, + StatusCode: fiber.StatusOK, + }) + + default: + return c.Status(fiber.StatusBadRequest).JSON(domain.Response{ + Message: "Invalid Chapa transaction type", + Success: false, + StatusCode: fiber.StatusBadRequest, + }) + } } -// 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 +// WithdrawUsingChapa godoc +// @Summary Withdraw using Chapa +// @Description Initiates a withdrawal transaction using Chapa for the authenticated user. +// @Tags Chapa +// @Accept json +// @Produce json +// @Param request body domain.ChapaWithdrawRequest true "Chapa Withdraw Request" +// @Success 200 {object} domain.Response{data=string} "Withdrawal requested successfully" +// @Failure 400 {object} domain.Response "Invalid request" +// @Failure 401 {object} domain.Response "Unauthorized" +// @Failure 422 {object} domain.Response "Unprocessable Entity" +// @Failure 500 {object} domain.Response "Internal Server Error" +// @Router /api/v1/chapa/payments/withdraw [post] +func (h *Handler) WithdrawUsingChapa(c *fiber.Ctx) error { + var req domain.ChapaWithdrawRequest if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - "details": err.Error(), + return domain.UnProcessableEntityResponse(c) + } + + userID, ok := c.Locals("user_id").(int64) + if !ok || userID == 0 { + return c.Status(fiber.StatusUnauthorized).JSON(domain.Response{ + Message: "Unauthorized", + Success: false, + StatusCode: fiber.StatusUnauthorized, }) } - // 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(), - }) + if err := h.chapaSvc.WithdrawUsingChapa(c.Context(), userID, req); err != nil { + return domain.FiberErrorResponse(c, err) } - 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") - - 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(), - }) - } - - return c.Status(resp.StatusCode).Type("json").Send(body) + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Withdrawal requested successfully", + Success: true, + StatusCode: fiber.StatusOK, + }) } -// VerifyTransfer godoc -// @Summary Verify a transfer -// @Description Check the status of a money transfer via reference +// DepositUsingChapa godoc +// @Summary Deposit money into user wallet using Chapa +// @Description Deposits money into user wallet from user account using Chapa +// @Tags Chapa +// @Accept json +// @Produce json +// @Param payload body domain.ChapaDepositRequest true "Deposit request payload" +// @Success 200 {object} domain.ChapaPaymentUrlResponseWrapper +// @Failure 400 {object} domain.Response "Invalid request" +// @Failure 422 {object} domain.Response "Validation error" +// @Failure 500 {object} domain.Response "Internal server error" +// @Router /api/v1/chapa/payments/deposit [post] +func (h *Handler) DepositUsingChapa(c *fiber.Ctx) error { + // Extract user info from token (adjust as per your auth middleware) + userID, ok := c.Locals("user_id").(int64) + if !ok || userID == 0 { + return c.Status(fiber.StatusUnauthorized).JSON(domain.Response{ + Message: "Unauthorized", + Success: false, + StatusCode: fiber.StatusUnauthorized, + }) + } + + var req domain.ChapaDepositRequest + if err := c.BodyParser(&req); err != nil { + return domain.UnProcessableEntityResponse(c) + } + + // Validate input in domain/domain (you may have a Validate method) + if err := req.Validate(); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.Response{ + Message: err.Error(), + Success: false, + StatusCode: fiber.StatusBadRequest, + }) + } + + // Call service to handle the deposit logic and get payment URL + paymentUrl, svcErr := h.chapaSvc.DepositUsingChapa(c.Context(), userID, req) + if svcErr != nil { + return domain.FiberErrorResponse(c, svcErr) + } + + return c.Status(fiber.StatusOK).JSON(domain.ResponseWDataFactory[domain.ChapaPaymentUrlResponse]{ + Data: domain.ChapaPaymentUrlResponse{ + PaymentURL: paymentUrl, + }, + Response: domain.Response{ + Message: "Deposit process started on wallet, fulfill payment using the URL provided", + Success: true, + StatusCode: fiber.StatusOK, + }, + }) +} + +// ReadChapaBanks godoc +// @Summary fetches chapa supported banks // @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) - - httpReq, err := http.NewRequest("GET", url, nil) +// @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(fiber.Map{ - "error": "Failed to create HTTP request", - "details": err.Error(), + return c.Status(fiber.StatusInternalServerError).JSON(domain.Response{ + Message: "Internal server error", + Success: false, + StatusCode: fiber.StatusInternalServerError, }) } - 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() - - 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(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/handlers.go b/internal/web_server/handlers/handlers.go index 157663b..3dbf750 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -7,8 +7,10 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation" @@ -30,6 +32,7 @@ type Handler struct { notificationSvc *notificationservice.Service userSvc *user.Service referralSvc referralservice.ReferralStore + chapaSvc chapa.ChapaPort walletSvc *wallet.Service transactionSvc *transaction.Service ticketSvc *ticket.Service @@ -38,6 +41,7 @@ type Handler struct { companySvc *company.Service prematchSvc *odds.ServiceImpl eventSvc event.Service + leagueSvc league.Service virtualGameSvc virtualgameservice.VirtualGameService aleaVirtualGameSvc alea.AleaVirtualGameService veliVirtualGameSvc veli.VeliVirtualGameService @@ -53,6 +57,7 @@ func New( logger *slog.Logger, notificationSvc *notificationservice.Service, validator *customvalidator.CustomValidator, + chapaSvc chapa.ChapaPort, walletSvc *wallet.Service, referralSvc referralservice.ReferralStore, virtualGameSvc virtualgameservice.VirtualGameService, @@ -69,12 +74,14 @@ func New( companySvc *company.Service, prematchSvc *odds.ServiceImpl, eventSvc event.Service, + leagueSvc league.Service, resultSvc result.Service, cfg *config.Config, ) *Handler { return &Handler{ logger: logger, notificationSvc: notificationSvc, + chapaSvc: chapaSvc, walletSvc: walletSvc, referralSvc: referralSvc, validator: validator, @@ -86,6 +93,7 @@ func New( companySvc: companySvc, prematchSvc: prematchSvc, eventSvc: eventSvc, + leagueSvc: leagueSvc, virtualGameSvc: virtualGameSvc, aleaVirtualGameSvc: aleaVirtualGameSvc, veliVirtualGameSvc: veliVirtualGameSvc, diff --git a/internal/web_server/handlers/leagues.go b/internal/web_server/handlers/leagues.go new file mode 100644 index 0000000..d4f78ee --- /dev/null +++ b/internal/web_server/handlers/leagues.go @@ -0,0 +1,34 @@ +package handlers + +import ( + "strconv" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" + "github.com/gofiber/fiber/v2" +) + +func (h *Handler) GetAllLeagues(c *fiber.Ctx) error { + leagues, err := h.leagueSvc.GetAllLeagues(c.Context()) + if err != nil { + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get leagues", err, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "All leagues retrived", leagues, nil) +} + +func (h *Handler) SetLeagueActive(c *fiber.Ctx) error { + leagueIdStr := c.Params("id") + if leagueIdStr == "" { + response.WriteJSON(c, fiber.StatusBadRequest, "Missing league id", nil, nil) + } + leagueId, err := strconv.Atoi(leagueIdStr) + if err != nil { + response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, nil) + } + + if err := h.leagueSvc.SetLeagueActive(c.Context(), int64(leagueId)); err != nil { + response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update league", err, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil) +} diff --git a/internal/web_server/handlers/prematch.go b/internal/web_server/handlers/prematch.go index 5bcfafd..d325889 100644 --- a/internal/web_server/handlers/prematch.go +++ b/internal/web_server/handlers/prematch.go @@ -107,18 +107,27 @@ func (h *Handler) GetRawOddsByMarketID(c *fiber.Ctx) error { func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { page := c.QueryInt("page", 1) pageSize := c.QueryInt("page_size", 10) - leagueIDQuery := c.Query("league_id") - sportIDQuery := c.Query("sport_id") + leagueIDQuery, err := strconv.Atoi(c.Query("league_id")) + if err != nil { + h.logger.Error("invalid league id", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, nil) + } + + sportIDQuery, err := strconv.Atoi(c.Query("sport_id")) + if err != nil { + h.logger.Error("invalid sport id", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "invalid sport id", nil, nil) + } firstStartTimeQuery := c.Query("first_start_time") lastStartTimeQuery := c.Query("last_start_time") - leagueID := domain.ValidString{ - Value: leagueIDQuery, - Valid: leagueIDQuery != "", + leagueID := domain.ValidInt32{ + Value: int32(leagueIDQuery), + Valid: leagueIDQuery != 0, } - sportID := domain.ValidString{ - Value: sportIDQuery, - Valid: sportIDQuery != "", + sportID := domain.ValidInt32{ + Value: int32(sportIDQuery), + Valid: sportIDQuery != 0, } var firstStartTime domain.ValidTime 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 c15f0b3..e8f83e6 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -18,6 +18,7 @@ func (a *App) initAppRoutes() { a.logger, a.NotidicationStore, a.validator, + a.chapaSvc, a.walletSvc, a.referralSvc, a.virtualGameSvc, @@ -35,6 +36,7 @@ func (a *App) initAppRoutes() { a.prematchSvc, a.eventSvc, *a.resultSvc, + a.leagueSvc, a.cfg, ) @@ -114,13 +116,17 @@ func (a *App) initAppRoutes() { a.fiber.Put("/managers/:id", a.authMiddleware, h.UpdateManagers) a.fiber.Get("/manager/:id/branch", a.authMiddleware, h.GetBranchByManagerID) - a.fiber.Get("/prematch/odds/:event_id", h.GetPrematchOdds) - a.fiber.Get("/prematch/odds", h.GetALLPrematchOdds) - a.fiber.Get("/prematch/odds/upcoming/:upcoming_id/market/:market_id", h.GetRawOddsByMarketID) + a.fiber.Get("/events/odds/:event_id", h.GetPrematchOdds) + a.fiber.Get("/events/odds", h.GetALLPrematchOdds) + a.fiber.Get("/events/odds/upcoming/:upcoming_id/market/:market_id", h.GetRawOddsByMarketID) - a.fiber.Get("/prematch/events/:id", h.GetUpcomingEventByID) - a.fiber.Get("/prematch/events", h.GetAllUpcomingEvents) - a.fiber.Get("/prematch/odds/upcoming/:upcoming_id", h.GetPrematchOddsByUpcomingID) + a.fiber.Get("/events/:id", h.GetUpcomingEventByID) + a.fiber.Get("/events", h.GetAllUpcomingEvents) + a.fiber.Get("/events/odds/upcoming/:upcoming_id", h.GetPrematchOddsByUpcomingID) + + // Leagues + a.fiber.Get("/leagues", h.GetAllLeagues) + a.fiber.Get("/leagues/:id/set-active", h.SetLeagueActive) a.fiber.Get("/result/:id", h.GetResultsByEventID) @@ -184,13 +190,17 @@ func (a *App) initAppRoutes() { a.fiber.Post("/transfer/refill/:id", a.authMiddleware, h.RefillWallet) //Chapa Routes + 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) - group.Post("/chapa/payments/callback", h.ReceiveWebhook) - group.Get("/chapa/banks", h.GetBanks) - group.Post("/chapa/transfers", h.CreateTransfer) - group.Get("/chapa/transfers/verify/:transfer_ref", h.VerifyTransfer) + // group.Post("/chapa/payments/initialize", h.InitializePayment) + // group.Get("/chapa/payments/verify/:tx_ref", h.VerifyTransaction) + // group.Post("/chapa/payments/callback", h.ReceiveWebhook) + // group.Get("/chapa/banks", h.GetBanks) + // group.Post("/chapa/transfers", h.CreateTransfer) + // group.Get("/chapa/transfers/verify/:transfer_ref", h.VerifyTransfer) //Alea Play Virtual Game Routes group.Get("/alea-play/launch", a.authMiddleware, h.LaunchAleaGame) diff --git a/makefile b/makefile index a842d04..a40a255 100644 --- a/makefile +++ b/makefile @@ -19,7 +19,7 @@ build: .PHONY: run run: - @docker compose up -d + @docker compose up .PHONY: stop stop: @@ -56,8 +56,6 @@ db-up: db-down: @docker compose down @docker volume rm fortunebet-backend_postgres_data -postgres: - @docker exec -it fortunebet-backend-postgres-1 psql -U root -d gh .PHONY: sqlc-gen sqlc-gen: @sqlc generate \ No newline at end of file