From a5ea52b993e0b2c5431de770e2245c48f352fc69 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Mon, 16 Jun 2025 17:54:42 +0300 Subject: [PATCH] multi-currency support --- .env | 7 + cmd/main.go | 24 +- db/migrations/000001_fortune.up.sql | 23 +- .../000004_virtual_game_Sessios.down.sql | 2 + db/migrations/000006_recommendation.up.sql | 30 +- db/query/transfer.sql | 9 +- docs/docs.go | 1094 ++++++++++++----- docs/swagger.json | 1094 ++++++++++++----- docs/swagger.yaml | 781 ++++++++---- gen/db/auth.sql.go | 2 +- gen/db/bet.sql.go | 2 +- gen/db/branch.sql.go | 2 +- gen/db/cashier.sql.go | 2 +- gen/db/company.sql.go | 2 +- gen/db/copyfrom.go | 2 +- gen/db/db.go | 2 +- gen/db/events.sql.go | 2 +- gen/db/leagues.sql.go | 2 +- gen/db/models.go | 22 +- gen/db/monitor.sql.go | 2 +- gen/db/notification.sql.go | 2 +- gen/db/odds.sql.go | 2 +- gen/db/otp.sql.go | 2 +- gen/db/referal.sql.go | 2 +- gen/db/result.sql.go | 2 +- gen/db/ticket.sql.go | 2 +- gen/db/transactions.sql.go | 2 +- gen/db/transfer.sql.go | 59 +- gen/db/user.sql.go | 2 +- gen/db/virtual_games.sql.go | 2 +- gen/db/wallet.sql.go | 2 +- internal/config/config.go | 12 + internal/domain/chapa.go | 103 +- internal/domain/currency.go | 88 ++ internal/domain/responses.go | 8 + internal/domain/transfer.go | 2 + internal/domain/wallet.go | 1 + internal/repository/currency.go | 96 ++ internal/repository/transfer.go | 36 +- internal/services/chapa/client.go | 93 +- internal/services/chapa/port.go | 7 +- internal/services/chapa/service.go | 191 ++- internal/services/currency/fetcher.go | 69 ++ internal/services/currency/service.go | 125 ++ internal/services/currency/worker.go | 55 + internal/services/wallet/port.go | 1 + internal/services/wallet/transfer.go | 6 +- internal/web_server/app.go | 4 + internal/web_server/handlers/chapa.go | 119 +- internal/web_server/handlers/currency.go | 57 + internal/web_server/handlers/handlers.go | 6 +- internal/web_server/handlers/report.go | 2 +- .../web_server/handlers/result_handler.go | 2 +- internal/web_server/routes.go | 8 +- 54 files changed, 3211 insertions(+), 1065 deletions(-) create mode 100644 internal/domain/currency.go create mode 100644 internal/repository/currency.go create mode 100644 internal/services/currency/fetcher.go create mode 100644 internal/services/currency/service.go create mode 100644 internal/services/currency/worker.go create mode 100644 internal/web_server/handlers/currency.go diff --git a/.env b/.env index ea411f2..92878d5 100644 --- a/.env +++ b/.env @@ -27,7 +27,14 @@ POPOK_BASE_URL=https://st.pokgaming.com/ #Staging POPOK_CALLBACK_URL=1 +#Muli-currency Support +FIXER_API_KEY=3b0f1eb30d-63c875026d-sxy9pl +BASE_CURRENCY=ETB +FIXER_BASE_URL=https://api.apilayer.com/fixer + # Chapa API Configuration +CHAPA_TRANSFER_TYPE="Payout" +CHAPA_PAYMENT_TYPE="API" CHAPA_BASE_URL="https://api.chapa.co/v1" CHAPA_ENCRYPTION_KEY=zLdYrjnBCknMvFikmP5jBfen CHAPA_PUBLIC_KEY=CHAPUBK_TEST-HJR0qhQRPLTkauNy9Q8UrmskPTOR31aC diff --git a/cmd/main.go b/cmd/main.go index 899b830..67eef77 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -4,6 +4,7 @@ import ( // "context" // "context" + "context" "fmt" "log" "log/slog" @@ -33,6 +34,7 @@ import ( "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/currency" "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" @@ -134,13 +136,18 @@ func main() { chapaSvc := chapa.NewService( wallet.TransferStore(store), - wallet.WalletStore(store), + *walletSvc, user.UserStore(store), chapaClient, ) - // Initialize reporting components reportRepo := repository.NewReportRepo(store) + currRepo := repository.NewCurrencyPostgresRepository(store) + + fixerFertcherSvc := currency.NewFixerFetcher( + cfg.FIXER_API_KEY, + cfg.FIXER_BASE_URL, + ) reportSvc := report.NewService( bet.BetStore(store), @@ -175,7 +182,17 @@ func main() { logger, 5*time.Minute, ) - walletMonitorSvc.Start() + + currSvc := currency.NewService( + currRepo, + cfg.BASE_CURRENCY, + fixerFertcherSvc, + ) + + exchangeWorker := currency.NewExchangeRateWorker(fixerFertcherSvc, logger, cfg) + exchangeWorker.Start(context.Background()) + defer exchangeWorker.Stop() + go walletMonitorSvc.Start() httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc) httpserver.StartTicketCrons(*ticketSvc) @@ -183,6 +200,7 @@ func main() { // Initialize and start HTTP server app := httpserver.NewApp( + currSvc, cfg.Port, v, authSvc, diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index c43a7b9..6a9157e 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -69,6 +69,15 @@ CREATE TABLE IF NOT EXISTS tickets ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); +CREATE TABLE exchange_rates ( + id SERIAL PRIMARY KEY, + from_currency VARCHAR(3) NOT NULL, + to_currency VARCHAR(3) NOT NULL, + rate DECIMAL(19, 6) NOT NULL, + valid_until TIMESTAMP NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + UNIQUE (from_currency, to_currency) +); CREATE TABLE IF NOT EXISTS bet_outcomes ( id BIGSERIAL PRIMARY KEY, bet_id BIGINT NOT NULL, @@ -123,14 +132,15 @@ CREATE TABLE IF NOT EXISTS customer_wallets ( ); CREATE TABLE IF NOT EXISTS wallet_transfer ( id BIGSERIAL PRIMARY KEY, - amount BIGINT NOT NULL, - type VARCHAR(255) NOT NULL, - receiver_wallet_id BIGINT NOT NULL, + amount BIGINT, + type VARCHAR(255), + receiver_wallet_id BIGINT, sender_wallet_id BIGINT, cashier_id BIGINT, - verified BOOLEAN NOT NULL DEFAULT false, - reference_number VARCHAR(255) NOT NULL, - payment_method VARCHAR(255) NOT NULL, + verified BOOLEAN DEFAULT false, + reference_number VARCHAR(255), + status VARCHAR(255), + payment_method VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); @@ -301,6 +311,7 @@ ALTER TABLE bets ADD CONSTRAINT fk_bets_branches FOREIGN KEY (branch_id) REFERENCES branches(id); ALTER TABLE wallets ADD CONSTRAINT fk_wallets_users FOREIGN KEY (user_id) REFERENCES users(id); + ADD COLUMN currency VARCHAR(3) NOT NULL DEFAULT 'ETB'; ALTER TABLE customer_wallets ADD CONSTRAINT fk_customer_wallets_customers FOREIGN KEY (customer_id) REFERENCES users(id), ADD CONSTRAINT fk_customer_wallets_regular_wallet FOREIGN KEY (regular_wallet_id) REFERENCES wallets(id), diff --git a/db/migrations/000004_virtual_game_Sessios.down.sql b/db/migrations/000004_virtual_game_Sessios.down.sql index 58a1d58..714c2e5 100644 --- a/db/migrations/000004_virtual_game_Sessios.down.sql +++ b/db/migrations/000004_virtual_game_Sessios.down.sql @@ -1,3 +1,5 @@ +DROP TABLE IF EXISTS vitrual_games; + DROP TABLE IF EXISTS virtual_game_transactions; DROP TABLE IF EXISTS virtual_game_sessions; diff --git a/db/migrations/000006_recommendation.up.sql b/db/migrations/000006_recommendation.up.sql index 6be9fc7..f7806c5 100644 --- a/db/migrations/000006_recommendation.up.sql +++ b/db/migrations/000006_recommendation.up.sql @@ -1,18 +1,18 @@ -CREATE TABLE virtual_games ( - id BIGSERIAL PRIMARY KEY, - name VARCHAR(255) NOT NULL, - provider VARCHAR(100) NOT NULL, - category VARCHAR(100) NOT NULL, - min_bet DECIMAL(15,2) NOT NULL, - max_bet DECIMAL(15,2) NOT NULL, - volatility VARCHAR(50) NOT NULL, - rtp DECIMAL(5,2) NOT NULL, - is_featured BOOLEAN DEFAULT false, - popularity_score INTEGER DEFAULT 0, - thumbnail_url TEXT, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP -); +-- CREATE TABLE virtual_games ( +-- id BIGSERIAL PRIMARY KEY, +-- name VARCHAR(255) NOT NULL, +-- provider VARCHAR(100) NOT NULL, +-- category VARCHAR(100) NOT NULL, +-- min_bet DECIMAL(15,2) NOT NULL, +-- max_bet DECIMAL(15,2) NOT NULL, +-- volatility VARCHAR(50) NOT NULL, +-- rtp DECIMAL(5,2) NOT NULL, +-- is_featured BOOLEAN DEFAULT false, +-- popularity_score INTEGER DEFAULT 0, +-- thumbnail_url TEXT, +-- created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, +-- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +-- ); CREATE TABLE user_game_interactions ( id BIGSERIAL PRIMARY KEY, diff --git a/db/query/transfer.sql b/db/query/transfer.sql index f998b43..ac8e7ed 100644 --- a/db/query/transfer.sql +++ b/db/query/transfer.sql @@ -7,9 +7,10 @@ INSERT INTO wallet_transfer ( cashier_id, verified, reference_number, + status, payment_method ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *; -- name: GetAllTransfers :many SELECT * @@ -31,4 +32,10 @@ WHERE reference_number = $1; UPDATE wallet_transfer SET verified = $1, updated_at = CURRENT_TIMESTAMP +WHERE id = $2; + +-- name: UpdateTransferStatus :exec +UPDATE wallet_transfer +SET status = $1, + updated_at = CURRENT_TIMESTAMP WHERE id = $2; \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index d6a2230..f8825e0 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -304,8 +304,42 @@ const docTemplate = `{ } } }, + "/api/v1/chapa/banks": { + "get": { + "description": "Get list of banks supported by Chapa", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Get supported banks", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/chapa/payments/deposit": { "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], "description": "Starts a new deposit process using Chapa payment gateway", "consumes": [ "application/json" @@ -441,6 +475,165 @@ const docTemplate = `{ } } }, + "/api/v1/chapa/payments/withdraw": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Initiates a withdrawal request to transfer funds to a bank account via Chapa", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Initiate a withdrawal", + "parameters": [ + { + "description": "Withdrawal request details", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.ChapaWithdrawalRequest" + } + } + ], + "responses": { + "201": { + "description": "Chapa withdrawal process initiated successfully", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "422": { + "description": "Unprocessable entity", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/currencies": { + "get": { + "description": "Returns list of supported currencies", + "produces": [ + "application/json" + ], + "tags": [ + "Multi-Currency" + ], + "summary": "Get supported currencies", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "integer" + } + } + } + } + ] + } + } + } + } + }, + "/api/v1/currencies/convert": { + "get": { + "description": "Converts amount from one currency to another", + "produces": [ + "application/json" + ], + "tags": [ + "Multi-Currency" + ], + "summary": "Convert currency", + "parameters": [ + { + "type": "string", + "description": "Source currency code (e.g., USD)", + "name": "from", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Target currency code (e.g., ETB)", + "name": "to", + "in": "query", + "required": true + }, + { + "type": "number", + "description": "Amount to convert", + "name": "amount", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "number" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/reports/dashboard": { "get": { "security": [ @@ -507,7 +700,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/report.DashboardSummary" + "$ref": "#/definitions/domain.DashboardSummary" } }, "400": { @@ -844,38 +1037,6 @@ const docTemplate = `{ } } }, - "/banks": { - "get": { - "description": "Get list of banks supported by Chapa", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Chapa" - ], - "summary": "Get supported banks", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.Bank" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - } - } - } - }, "/bet": { "get": { "description": "Gets all the bets", @@ -1541,6 +1702,50 @@ const docTemplate = `{ } } }, + "/branchCashier": { + "get": { + "description": "Gets branch for cahier", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Gets branch for cahier", + "parameters": [ + { + "type": "integer", + "description": "Branch ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BranchDetailRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/branchWallet": { "get": { "description": "Retrieve all branch wallets", @@ -1629,6 +1834,56 @@ const docTemplate = `{ } } }, + "/cashierWallet": { + "get": { + "description": "Get wallet for cashier", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "cashier" + ], + "summary": "Get wallet for cashier", + "parameters": [ + { + "type": "integer", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.UserProfileRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/cashiers": { "get": { "description": "Get all cashiers", @@ -2059,6 +2314,206 @@ const docTemplate = `{ } } }, + "/events": { + "get": { + "description": "Retrieve all upcoming events from the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve all upcoming events", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "League ID Filter", + "name": "league_id", + "in": "query" + }, + { + "type": "string", + "description": "Sport ID Filter", + "name": "sport_id", + "in": "query" + }, + { + "type": "string", + "description": "Country Code Filter", + "name": "cc", + "in": "query" + }, + { + "type": "string", + "description": "Start Time", + "name": "first_start_time", + "in": "query" + }, + { + "type": "string", + "description": "End Time", + "name": "last_start_time", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.UpcomingEvent" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/events/{id}": { + "get": { + "description": "Retrieve an upcoming event by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve an upcoming by ID", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.UpcomingEvent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "delete": { + "description": "Set the event status to removed", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "event" + ], + "summary": "Set the event status to removed", + "parameters": [ + { + "type": "integer", + "description": "Event ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/leagues": { + "get": { + "description": "Gets all leagues", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "leagues" + ], + "summary": "Gets all leagues", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.League" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/manager/{id}/branch": { "get": { "description": "Gets a branches by manager id", @@ -2311,167 +2766,7 @@ const docTemplate = `{ } } }, - "/operation": { - "post": { - "description": "Creates a operation", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "branch" - ], - "summary": "Create a operation", - "parameters": [ - { - "description": "Creates operation", - "name": "createBranchOperation", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handlers.CreateBranchOperationReq" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handlers.BranchOperationRes" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/prematch/events": { - "get": { - "description": "Retrieve all upcoming events from the database", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "prematch" - ], - "summary": "Retrieve all upcoming events", - "parameters": [ - { - "type": "integer", - "description": "Page number", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "Page size", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "League ID Filter", - "name": "league_id", - "in": "query" - }, - { - "type": "string", - "description": "Sport ID Filter", - "name": "sport_id", - "in": "query" - }, - { - "type": "string", - "description": "Start Time", - "name": "first_start_time", - "in": "query" - }, - { - "type": "string", - "description": "End Time", - "name": "last_start_time", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.UpcomingEvent" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/prematch/events/{id}": { - "get": { - "description": "Retrieve an upcoming event by ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "prematch" - ], - "summary": "Retrieve an upcoming by ID", - "parameters": [ - { - "type": "string", - "description": "ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/domain.UpcomingEvent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/prematch/odds": { + "/odds": { "get": { "description": "Retrieve all prematch odds from the database", "consumes": [ @@ -2503,7 +2798,7 @@ const docTemplate = `{ } } }, - "/prematch/odds/upcoming/{upcoming_id}": { + "/odds/upcoming/{upcoming_id}": { "get": { "description": "Retrieve prematch odds by upcoming event ID (FI from Bet365) with optional pagination", "consumes": [ @@ -2562,7 +2857,7 @@ const docTemplate = `{ } } }, - "/prematch/odds/upcoming/{upcoming_id}/market/{market_id}": { + "/odds/upcoming/{upcoming_id}/market/{market_id}": { "get": { "description": "Retrieve raw odds records using a Market ID", "consumes": [ @@ -2616,9 +2911,9 @@ const docTemplate = `{ } } }, - "/prematch/odds/{event_id}": { - "get": { - "description": "Retrieve prematch odds for a specific event by event ID", + "/operation": { + "post": { + "description": "Creates a operation", "consumes": [ "application/json" ], @@ -2626,26 +2921,25 @@ const docTemplate = `{ "application/json" ], "tags": [ - "prematch" + "branch" ], - "summary": "Retrieve prematch odds for an event", + "summary": "Create a operation", "parameters": [ { - "type": "string", - "description": "Event ID", - "name": "event_id", - "in": "path", - "required": true + "description": "Creates operation", + "name": "createBranchOperation", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateBranchOperationReq" + } } ], "responses": { "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.Odd" - } + "$ref": "#/definitions/handlers.BranchOperationRes" } }, "400": { @@ -2878,7 +3172,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/domain.BetOutcome" + "$ref": "#/definitions/handlers.ResultRes" } } }, @@ -4398,55 +4692,6 @@ const docTemplate = `{ } } }, - "domain.Bank": { - "type": "object", - "properties": { - "acct_length": { - "type": "integer" - }, - "active": { - "type": "integer" - }, - "country_id": { - "type": "integer" - }, - "created_at": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "is_24hrs": { - "description": "nullable", - "type": "integer" - }, - "is_active": { - "type": "integer" - }, - "is_mobilemoney": { - "description": "nullable", - "type": "integer" - }, - "is_rtgs": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "slug": { - "type": "string" - }, - "swift": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, "domain.BetOutcome": { "type": "object", "properties": { @@ -4635,6 +4880,30 @@ const docTemplate = `{ } } }, + "domain.ChapaWithdrawalRequest": { + "type": "object", + "properties": { + "account_name": { + "type": "string" + }, + "account_number": { + "type": "string" + }, + "amount": { + "description": "string because Chapa API uses string for monetary values", + "type": "string" + }, + "bank_code": { + "type": "integer" + }, + "currency": { + "type": "string" + }, + "reference": { + "type": "string" + } + } + }, "domain.CreateBetOutcomeReq": { "type": "object", "properties": { @@ -4687,6 +4956,119 @@ const docTemplate = `{ } } }, + "domain.DashboardSummary": { + "type": "object", + "properties": { + "active_admins": { + "type": "integer" + }, + "active_bets": { + "type": "integer" + }, + "active_branches": { + "type": "integer" + }, + "active_cashiers": { + "type": "integer" + }, + "active_companies": { + "type": "integer" + }, + "active_customers": { + "type": "integer" + }, + "active_games": { + "type": "integer" + }, + "active_managers": { + "type": "integer" + }, + "average_stake": { + "type": "integer" + }, + "branches_count": { + "type": "integer" + }, + "customer_count": { + "type": "integer" + }, + "inactive_admins": { + "type": "integer" + }, + "inactive_branches": { + "type": "integer" + }, + "inactive_cashiers": { + "type": "integer" + }, + "inactive_companies": { + "type": "integer" + }, + "inactive_customers": { + "type": "integer" + }, + "inactive_games": { + "type": "integer" + }, + "inactive_managers": { + "type": "integer" + }, + "profit": { + "type": "integer" + }, + "read_notifications": { + "type": "integer" + }, + "total_admins": { + "type": "integer" + }, + "total_bets": { + "type": "integer" + }, + "total_cashiers": { + "type": "integer" + }, + "total_companies": { + "type": "integer" + }, + "total_deposits": { + "type": "integer" + }, + "total_games": { + "type": "integer" + }, + "total_losses": { + "type": "integer" + }, + "total_managers": { + "type": "integer" + }, + "total_notifications": { + "type": "integer" + }, + "total_stakes": { + "type": "integer" + }, + "total_wallets": { + "type": "integer" + }, + "total_wins": { + "type": "integer" + }, + "total_withdrawals": { + "type": "integer" + }, + "unread_notifications": { + "type": "integer" + }, + "win_balance": { + "type": "integer" + }, + "win_rate": { + "type": "number" + } + } + }, "domain.ErrorResponse": { "type": "object", "properties": { @@ -4698,6 +5080,68 @@ const docTemplate = `{ } } }, + "domain.EventStatus": { + "type": "string", + "enum": [ + "upcoming", + "in_play", + "to_be_fixed", + "ended", + "postponed", + "cancelled", + "walkover", + "interrupted", + "abandoned", + "retired", + "suspended", + "decided_by_fa", + "removed" + ], + "x-enum-varnames": [ + "STATUS_PENDING", + "STATUS_IN_PLAY", + "STATUS_TO_BE_FIXED", + "STATUS_ENDED", + "STATUS_POSTPONED", + "STATUS_CANCELLED", + "STATUS_WALKOVER", + "STATUS_INTERRUPTED", + "STATUS_ABANDONED", + "STATUS_RETIRED", + "STATUS_SUSPENDED", + "STATUS_DECIDED_BY_FA", + "STATUS_REMOVED" + ] + }, + "domain.League": { + "type": "object", + "properties": { + "bet365_id": { + "type": "integer", + "example": 1121 + }, + "cc": { + "type": "string", + "example": "uk" + }, + "id": { + "type": "integer", + "example": 1 + }, + "is_active": { + "type": "boolean", + "example": false + }, + "name": { + "type": "string", + "example": "BPL" + }, + "sport_id": { + "type": "integer", + "example": 1 + } + } + }, "domain.Odd": { "type": "object", "properties": { @@ -4941,6 +5385,21 @@ const docTemplate = `{ } } }, + "domain.Response": { + "type": "object", + "properties": { + "data": {}, + "message": { + "type": "string" + }, + "status_code": { + "type": "integer" + }, + "success": { + "type": "boolean" + } + } + }, "domain.Role": { "type": "string", "enum": [ @@ -5026,27 +5485,27 @@ const docTemplate = `{ "domain.UpcomingEvent": { "type": "object", "properties": { - "awayKitImage": { + "away_kit_image": { "description": "Kit or image for away team (optional)", "type": "string" }, - "awayTeam": { + "away_team": { "description": "Away team name (can be empty/null)", "type": "string" }, - "awayTeamID": { + "away_team_id": { "description": "Away team ID (can be empty/null)", "type": "integer" }, - "homeKitImage": { + "home_kit_image": { "description": "Kit or image for home team (optional)", "type": "string" }, - "homeTeam": { + "home_team": { "description": "Home team name (if available)", "type": "string" }, - "homeTeamID": { + "home_team_id": { "description": "Home team ID", "type": "integer" }, @@ -5054,19 +5513,19 @@ const docTemplate = `{ "description": "Event ID", "type": "string" }, - "leagueCC": { + "league_cc": { "description": "League country code", "type": "string" }, - "leagueID": { + "league_id": { "description": "League ID", "type": "integer" }, - "leagueName": { + "league_name": { "description": "League name", "type": "string" }, - "matchName": { + "match_name": { "description": "Match or event name", "type": "string" }, @@ -5074,13 +5533,21 @@ const docTemplate = `{ "description": "bet api provider (bet365, betfair)", "type": "string" }, - "sportID": { + "sport_id": { "description": "Sport ID", "type": "integer" }, - "startTime": { + "start_time": { "description": "Converted from \"time\" field in UNIX format", "type": "string" + }, + "status": { + "description": "Match Status for event", + "allOf": [ + { + "$ref": "#/definitions/domain.EventStatus" + } + ] } } }, @@ -5141,6 +5608,9 @@ const docTemplate = `{ "id": { "type": "integer" }, + "is_active": { + "type": "boolean" + }, "is_featured": { "type": "boolean" }, @@ -5220,6 +5690,10 @@ const docTemplate = `{ "handlers.BranchDetailRes": { "type": "object", "properties": { + "balance": { + "type": "number", + "example": 100.5 + }, "branch_manager_id": { "type": "integer", "example": 1 @@ -5679,6 +6153,15 @@ const docTemplate = `{ "branch_id": { "type": "integer" }, + "branch_location": { + "type": "string" + }, + "branch_name": { + "type": "string" + }, + "branch_wallet": { + "type": "integer" + }, "created_at": { "type": "string" }, @@ -5849,6 +6332,17 @@ const docTemplate = `{ } } }, + "handlers.ResultRes": { + "type": "object", + "properties": { + "outcomes": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.BetOutcome" + } + } + } + }, "handlers.SearchUserByNameOrPhoneReq": { "type": "object", "properties": { @@ -6215,10 +6709,10 @@ const docTemplate = `{ "mode": { "type": "string", "enum": [ - "REAL", - "DEMO" + "fun", + "real" ], - "example": "REAL" + "example": "real" } } }, @@ -6352,56 +6846,6 @@ const docTemplate = `{ } } }, - "report.DashboardSummary": { - "type": "object", - "properties": { - "active_bets": { - "type": "integer" - }, - "active_branches": { - "type": "integer" - }, - "active_customers": { - "type": "integer" - }, - "average_stake": { - "type": "integer" - }, - "branches_count": { - "type": "integer" - }, - "customer_count": { - "type": "integer" - }, - "profit": { - "type": "integer" - }, - "total_bets": { - "type": "integer" - }, - "total_deposits": { - "type": "integer" - }, - "total_losses": { - "type": "integer" - }, - "total_stakes": { - "type": "integer" - }, - "total_wins": { - "type": "integer" - }, - "total_withdrawals": { - "type": "integer" - }, - "win_balance": { - "type": "integer" - }, - "win_rate": { - "type": "number" - } - } - }, "response.APIResponse": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 2438fb7..1bb4270 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -296,8 +296,42 @@ } } }, + "/api/v1/chapa/banks": { + "get": { + "description": "Get list of banks supported by Chapa", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Get supported banks", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/chapa/payments/deposit": { "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], "description": "Starts a new deposit process using Chapa payment gateway", "consumes": [ "application/json" @@ -433,6 +467,165 @@ } } }, + "/api/v1/chapa/payments/withdraw": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Initiates a withdrawal request to transfer funds to a bank account via Chapa", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Initiate a withdrawal", + "parameters": [ + { + "description": "Withdrawal request details", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.ChapaWithdrawalRequest" + } + } + ], + "responses": { + "201": { + "description": "Chapa withdrawal process initiated successfully", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Invalid request body", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "422": { + "description": "Unprocessable entity", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/currencies": { + "get": { + "description": "Returns list of supported currencies", + "produces": [ + "application/json" + ], + "tags": [ + "Multi-Currency" + ], + "summary": "Get supported currencies", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "integer" + } + } + } + } + ] + } + } + } + } + }, + "/api/v1/currencies/convert": { + "get": { + "description": "Converts amount from one currency to another", + "produces": [ + "application/json" + ], + "tags": [ + "Multi-Currency" + ], + "summary": "Convert currency", + "parameters": [ + { + "type": "string", + "description": "Source currency code (e.g., USD)", + "name": "from", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Target currency code (e.g., ETB)", + "name": "to", + "in": "query", + "required": true + }, + { + "type": "number", + "description": "Amount to convert", + "name": "amount", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "number" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/reports/dashboard": { "get": { "security": [ @@ -499,7 +692,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/report.DashboardSummary" + "$ref": "#/definitions/domain.DashboardSummary" } }, "400": { @@ -836,38 +1029,6 @@ } } }, - "/banks": { - "get": { - "description": "Get list of banks supported by Chapa", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Chapa" - ], - "summary": "Get supported banks", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.Bank" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - } - } - } - }, "/bet": { "get": { "description": "Gets all the bets", @@ -1533,6 +1694,50 @@ } } }, + "/branchCashier": { + "get": { + "description": "Gets branch for cahier", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Gets branch for cahier", + "parameters": [ + { + "type": "integer", + "description": "Branch ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BranchDetailRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/branchWallet": { "get": { "description": "Retrieve all branch wallets", @@ -1621,6 +1826,56 @@ } } }, + "/cashierWallet": { + "get": { + "description": "Get wallet for cashier", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "cashier" + ], + "summary": "Get wallet for cashier", + "parameters": [ + { + "type": "integer", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.UserProfileRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/cashiers": { "get": { "description": "Get all cashiers", @@ -2051,6 +2306,206 @@ } } }, + "/events": { + "get": { + "description": "Retrieve all upcoming events from the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve all upcoming events", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "League ID Filter", + "name": "league_id", + "in": "query" + }, + { + "type": "string", + "description": "Sport ID Filter", + "name": "sport_id", + "in": "query" + }, + { + "type": "string", + "description": "Country Code Filter", + "name": "cc", + "in": "query" + }, + { + "type": "string", + "description": "Start Time", + "name": "first_start_time", + "in": "query" + }, + { + "type": "string", + "description": "End Time", + "name": "last_start_time", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.UpcomingEvent" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/events/{id}": { + "get": { + "description": "Retrieve an upcoming event by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve an upcoming by ID", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.UpcomingEvent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "delete": { + "description": "Set the event status to removed", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "event" + ], + "summary": "Set the event status to removed", + "parameters": [ + { + "type": "integer", + "description": "Event ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/leagues": { + "get": { + "description": "Gets all leagues", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "leagues" + ], + "summary": "Gets all leagues", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.League" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/manager/{id}/branch": { "get": { "description": "Gets a branches by manager id", @@ -2303,167 +2758,7 @@ } } }, - "/operation": { - "post": { - "description": "Creates a operation", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "branch" - ], - "summary": "Create a operation", - "parameters": [ - { - "description": "Creates operation", - "name": "createBranchOperation", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handlers.CreateBranchOperationReq" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handlers.BranchOperationRes" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/prematch/events": { - "get": { - "description": "Retrieve all upcoming events from the database", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "prematch" - ], - "summary": "Retrieve all upcoming events", - "parameters": [ - { - "type": "integer", - "description": "Page number", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "Page size", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "League ID Filter", - "name": "league_id", - "in": "query" - }, - { - "type": "string", - "description": "Sport ID Filter", - "name": "sport_id", - "in": "query" - }, - { - "type": "string", - "description": "Start Time", - "name": "first_start_time", - "in": "query" - }, - { - "type": "string", - "description": "End Time", - "name": "last_start_time", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.UpcomingEvent" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/prematch/events/{id}": { - "get": { - "description": "Retrieve an upcoming event by ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "prematch" - ], - "summary": "Retrieve an upcoming by ID", - "parameters": [ - { - "type": "string", - "description": "ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/domain.UpcomingEvent" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/prematch/odds": { + "/odds": { "get": { "description": "Retrieve all prematch odds from the database", "consumes": [ @@ -2495,7 +2790,7 @@ } } }, - "/prematch/odds/upcoming/{upcoming_id}": { + "/odds/upcoming/{upcoming_id}": { "get": { "description": "Retrieve prematch odds by upcoming event ID (FI from Bet365) with optional pagination", "consumes": [ @@ -2554,7 +2849,7 @@ } } }, - "/prematch/odds/upcoming/{upcoming_id}/market/{market_id}": { + "/odds/upcoming/{upcoming_id}/market/{market_id}": { "get": { "description": "Retrieve raw odds records using a Market ID", "consumes": [ @@ -2608,9 +2903,9 @@ } } }, - "/prematch/odds/{event_id}": { - "get": { - "description": "Retrieve prematch odds for a specific event by event ID", + "/operation": { + "post": { + "description": "Creates a operation", "consumes": [ "application/json" ], @@ -2618,26 +2913,25 @@ "application/json" ], "tags": [ - "prematch" + "branch" ], - "summary": "Retrieve prematch odds for an event", + "summary": "Create a operation", "parameters": [ { - "type": "string", - "description": "Event ID", - "name": "event_id", - "in": "path", - "required": true + "description": "Creates operation", + "name": "createBranchOperation", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateBranchOperationReq" + } } ], "responses": { "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.Odd" - } + "$ref": "#/definitions/handlers.BranchOperationRes" } }, "400": { @@ -2870,7 +3164,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/domain.BetOutcome" + "$ref": "#/definitions/handlers.ResultRes" } } }, @@ -4390,55 +4684,6 @@ } } }, - "domain.Bank": { - "type": "object", - "properties": { - "acct_length": { - "type": "integer" - }, - "active": { - "type": "integer" - }, - "country_id": { - "type": "integer" - }, - "created_at": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "is_24hrs": { - "description": "nullable", - "type": "integer" - }, - "is_active": { - "type": "integer" - }, - "is_mobilemoney": { - "description": "nullable", - "type": "integer" - }, - "is_rtgs": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "slug": { - "type": "string" - }, - "swift": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, "domain.BetOutcome": { "type": "object", "properties": { @@ -4627,6 +4872,30 @@ } } }, + "domain.ChapaWithdrawalRequest": { + "type": "object", + "properties": { + "account_name": { + "type": "string" + }, + "account_number": { + "type": "string" + }, + "amount": { + "description": "string because Chapa API uses string for monetary values", + "type": "string" + }, + "bank_code": { + "type": "integer" + }, + "currency": { + "type": "string" + }, + "reference": { + "type": "string" + } + } + }, "domain.CreateBetOutcomeReq": { "type": "object", "properties": { @@ -4679,6 +4948,119 @@ } } }, + "domain.DashboardSummary": { + "type": "object", + "properties": { + "active_admins": { + "type": "integer" + }, + "active_bets": { + "type": "integer" + }, + "active_branches": { + "type": "integer" + }, + "active_cashiers": { + "type": "integer" + }, + "active_companies": { + "type": "integer" + }, + "active_customers": { + "type": "integer" + }, + "active_games": { + "type": "integer" + }, + "active_managers": { + "type": "integer" + }, + "average_stake": { + "type": "integer" + }, + "branches_count": { + "type": "integer" + }, + "customer_count": { + "type": "integer" + }, + "inactive_admins": { + "type": "integer" + }, + "inactive_branches": { + "type": "integer" + }, + "inactive_cashiers": { + "type": "integer" + }, + "inactive_companies": { + "type": "integer" + }, + "inactive_customers": { + "type": "integer" + }, + "inactive_games": { + "type": "integer" + }, + "inactive_managers": { + "type": "integer" + }, + "profit": { + "type": "integer" + }, + "read_notifications": { + "type": "integer" + }, + "total_admins": { + "type": "integer" + }, + "total_bets": { + "type": "integer" + }, + "total_cashiers": { + "type": "integer" + }, + "total_companies": { + "type": "integer" + }, + "total_deposits": { + "type": "integer" + }, + "total_games": { + "type": "integer" + }, + "total_losses": { + "type": "integer" + }, + "total_managers": { + "type": "integer" + }, + "total_notifications": { + "type": "integer" + }, + "total_stakes": { + "type": "integer" + }, + "total_wallets": { + "type": "integer" + }, + "total_wins": { + "type": "integer" + }, + "total_withdrawals": { + "type": "integer" + }, + "unread_notifications": { + "type": "integer" + }, + "win_balance": { + "type": "integer" + }, + "win_rate": { + "type": "number" + } + } + }, "domain.ErrorResponse": { "type": "object", "properties": { @@ -4690,6 +5072,68 @@ } } }, + "domain.EventStatus": { + "type": "string", + "enum": [ + "upcoming", + "in_play", + "to_be_fixed", + "ended", + "postponed", + "cancelled", + "walkover", + "interrupted", + "abandoned", + "retired", + "suspended", + "decided_by_fa", + "removed" + ], + "x-enum-varnames": [ + "STATUS_PENDING", + "STATUS_IN_PLAY", + "STATUS_TO_BE_FIXED", + "STATUS_ENDED", + "STATUS_POSTPONED", + "STATUS_CANCELLED", + "STATUS_WALKOVER", + "STATUS_INTERRUPTED", + "STATUS_ABANDONED", + "STATUS_RETIRED", + "STATUS_SUSPENDED", + "STATUS_DECIDED_BY_FA", + "STATUS_REMOVED" + ] + }, + "domain.League": { + "type": "object", + "properties": { + "bet365_id": { + "type": "integer", + "example": 1121 + }, + "cc": { + "type": "string", + "example": "uk" + }, + "id": { + "type": "integer", + "example": 1 + }, + "is_active": { + "type": "boolean", + "example": false + }, + "name": { + "type": "string", + "example": "BPL" + }, + "sport_id": { + "type": "integer", + "example": 1 + } + } + }, "domain.Odd": { "type": "object", "properties": { @@ -4933,6 +5377,21 @@ } } }, + "domain.Response": { + "type": "object", + "properties": { + "data": {}, + "message": { + "type": "string" + }, + "status_code": { + "type": "integer" + }, + "success": { + "type": "boolean" + } + } + }, "domain.Role": { "type": "string", "enum": [ @@ -5018,27 +5477,27 @@ "domain.UpcomingEvent": { "type": "object", "properties": { - "awayKitImage": { + "away_kit_image": { "description": "Kit or image for away team (optional)", "type": "string" }, - "awayTeam": { + "away_team": { "description": "Away team name (can be empty/null)", "type": "string" }, - "awayTeamID": { + "away_team_id": { "description": "Away team ID (can be empty/null)", "type": "integer" }, - "homeKitImage": { + "home_kit_image": { "description": "Kit or image for home team (optional)", "type": "string" }, - "homeTeam": { + "home_team": { "description": "Home team name (if available)", "type": "string" }, - "homeTeamID": { + "home_team_id": { "description": "Home team ID", "type": "integer" }, @@ -5046,19 +5505,19 @@ "description": "Event ID", "type": "string" }, - "leagueCC": { + "league_cc": { "description": "League country code", "type": "string" }, - "leagueID": { + "league_id": { "description": "League ID", "type": "integer" }, - "leagueName": { + "league_name": { "description": "League name", "type": "string" }, - "matchName": { + "match_name": { "description": "Match or event name", "type": "string" }, @@ -5066,13 +5525,21 @@ "description": "bet api provider (bet365, betfair)", "type": "string" }, - "sportID": { + "sport_id": { "description": "Sport ID", "type": "integer" }, - "startTime": { + "start_time": { "description": "Converted from \"time\" field in UNIX format", "type": "string" + }, + "status": { + "description": "Match Status for event", + "allOf": [ + { + "$ref": "#/definitions/domain.EventStatus" + } + ] } } }, @@ -5133,6 +5600,9 @@ "id": { "type": "integer" }, + "is_active": { + "type": "boolean" + }, "is_featured": { "type": "boolean" }, @@ -5212,6 +5682,10 @@ "handlers.BranchDetailRes": { "type": "object", "properties": { + "balance": { + "type": "number", + "example": 100.5 + }, "branch_manager_id": { "type": "integer", "example": 1 @@ -5671,6 +6145,15 @@ "branch_id": { "type": "integer" }, + "branch_location": { + "type": "string" + }, + "branch_name": { + "type": "string" + }, + "branch_wallet": { + "type": "integer" + }, "created_at": { "type": "string" }, @@ -5841,6 +6324,17 @@ } } }, + "handlers.ResultRes": { + "type": "object", + "properties": { + "outcomes": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.BetOutcome" + } + } + } + }, "handlers.SearchUserByNameOrPhoneReq": { "type": "object", "properties": { @@ -6207,10 +6701,10 @@ "mode": { "type": "string", "enum": [ - "REAL", - "DEMO" + "fun", + "real" ], - "example": "REAL" + "example": "real" } } }, @@ -6344,56 +6838,6 @@ } } }, - "report.DashboardSummary": { - "type": "object", - "properties": { - "active_bets": { - "type": "integer" - }, - "active_branches": { - "type": "integer" - }, - "active_customers": { - "type": "integer" - }, - "average_stake": { - "type": "integer" - }, - "branches_count": { - "type": "integer" - }, - "customer_count": { - "type": "integer" - }, - "profit": { - "type": "integer" - }, - "total_bets": { - "type": "integer" - }, - "total_deposits": { - "type": "integer" - }, - "total_losses": { - "type": "integer" - }, - "total_stakes": { - "type": "integer" - }, - "total_wins": { - "type": "integer" - }, - "total_withdrawals": { - "type": "integer" - }, - "win_balance": { - "type": "integer" - }, - "win_rate": { - "type": "number" - } - } - }, "response.APIResponse": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index bcdf305..df02b3c 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -31,39 +31,6 @@ definitions: user_id: type: string type: object - domain.Bank: - properties: - acct_length: - type: integer - active: - type: integer - country_id: - type: integer - created_at: - type: string - currency: - type: string - id: - type: integer - is_24hrs: - description: nullable - type: integer - is_active: - type: integer - is_mobilemoney: - description: nullable - type: integer - is_rtgs: - type: integer - name: - type: string - slug: - type: string - swift: - type: string - updated_at: - type: string - type: object domain.BetOutcome: properties: away_team_name: @@ -193,6 +160,22 @@ definitions: tx_ref: type: string type: object + domain.ChapaWithdrawalRequest: + properties: + account_name: + type: string + account_number: + type: string + amount: + description: string because Chapa API uses string for monetary values + type: string + bank_code: + type: integer + currency: + type: string + reference: + type: string + type: object domain.CreateBetOutcomeReq: properties: event_id: @@ -228,6 +211,81 @@ definitions: - $ref: '#/definitions/domain.OutcomeStatus' example: 1 type: object + domain.DashboardSummary: + properties: + active_admins: + type: integer + active_bets: + type: integer + active_branches: + type: integer + active_cashiers: + type: integer + active_companies: + type: integer + active_customers: + type: integer + active_games: + type: integer + active_managers: + type: integer + average_stake: + type: integer + branches_count: + type: integer + customer_count: + type: integer + inactive_admins: + type: integer + inactive_branches: + type: integer + inactive_cashiers: + type: integer + inactive_companies: + type: integer + inactive_customers: + type: integer + inactive_games: + type: integer + inactive_managers: + type: integer + profit: + type: integer + read_notifications: + type: integer + total_admins: + type: integer + total_bets: + type: integer + total_cashiers: + type: integer + total_companies: + type: integer + total_deposits: + type: integer + total_games: + type: integer + total_losses: + type: integer + total_managers: + type: integer + total_notifications: + type: integer + total_stakes: + type: integer + total_wallets: + type: integer + total_wins: + type: integer + total_withdrawals: + type: integer + unread_notifications: + type: integer + win_balance: + type: integer + win_rate: + type: number + type: object domain.ErrorResponse: properties: error: @@ -235,6 +293,57 @@ definitions: message: type: string type: object + domain.EventStatus: + enum: + - upcoming + - in_play + - to_be_fixed + - ended + - postponed + - cancelled + - walkover + - interrupted + - abandoned + - retired + - suspended + - decided_by_fa + - removed + type: string + x-enum-varnames: + - STATUS_PENDING + - STATUS_IN_PLAY + - STATUS_TO_BE_FIXED + - STATUS_ENDED + - STATUS_POSTPONED + - STATUS_CANCELLED + - STATUS_WALKOVER + - STATUS_INTERRUPTED + - STATUS_ABANDONED + - STATUS_RETIRED + - STATUS_SUSPENDED + - STATUS_DECIDED_BY_FA + - STATUS_REMOVED + domain.League: + properties: + bet365_id: + example: 1121 + type: integer + cc: + example: uk + type: string + id: + example: 1 + type: integer + is_active: + example: false + type: boolean + name: + example: BPL + type: string + sport_id: + example: 1 + type: integer + type: object domain.Odd: properties: category: @@ -404,6 +513,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 @@ -466,48 +585,52 @@ definitions: type: object domain.UpcomingEvent: properties: - awayKitImage: + away_kit_image: description: Kit or image for away team (optional) type: string - awayTeam: + away_team: description: Away team name (can be empty/null) type: string - awayTeamID: + away_team_id: description: Away team ID (can be empty/null) type: integer - homeKitImage: + home_kit_image: description: Kit or image for home team (optional) type: string - homeTeam: + home_team: description: Home team name (if available) type: string - homeTeamID: + home_team_id: description: Home team ID type: integer id: description: Event ID type: string - leagueCC: + league_cc: description: League country code type: string - leagueID: + league_id: description: League ID type: integer - leagueName: + league_name: description: League name type: string - matchName: + match_name: description: Match or event name type: string source: description: bet api provider (bet365, betfair) type: string - sportID: + sport_id: description: Sport ID type: integer - startTime: + start_time: description: Converted from "time" field in UNIX format type: string + status: + allOf: + - $ref: '#/definitions/domain.EventStatus' + description: Match Status for event type: object domain.VeliCallback: properties: @@ -550,6 +673,8 @@ definitions: type: string id: type: integer + is_active: + type: boolean is_featured: type: boolean max_bet: @@ -602,6 +727,9 @@ definitions: type: object handlers.BranchDetailRes: properties: + balance: + example: 100.5 + type: number branch_manager_id: example: 1 type: integer @@ -928,6 +1056,12 @@ definitions: properties: branch_id: type: integer + branch_location: + type: string + branch_name: + type: string + branch_wallet: + type: integer created_at: type: string email: @@ -1045,6 +1179,13 @@ definitions: - otp - password type: object + handlers.ResultRes: + properties: + outcomes: + items: + $ref: '#/definitions/domain.BetOutcome' + type: array + type: object handlers.SearchUserByNameOrPhoneReq: properties: query: @@ -1296,9 +1437,9 @@ definitions: type: string mode: enum: - - REAL - - DEMO - example: REAL + - fun + - real + example: real type: string required: - currency @@ -1395,39 +1536,6 @@ definitions: example: false type: boolean type: object - report.DashboardSummary: - properties: - active_bets: - type: integer - active_branches: - type: integer - active_customers: - type: integer - average_stake: - type: integer - branches_count: - type: integer - customer_count: - type: integer - profit: - type: integer - total_bets: - type: integer - total_deposits: - type: integer - total_losses: - type: integer - total_stakes: - type: integer - total_wins: - type: integer - total_withdrawals: - type: integer - win_balance: - type: integer - win_rate: - type: number - type: object response.APIResponse: properties: data: {} @@ -1646,6 +1754,25 @@ paths: summary: Launch an Alea Play virtual game tags: - Alea Virtual Games + /api/v1/chapa/banks: + get: + consumes: + - application/json + description: Get list of banks supported by Chapa + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get supported banks + tags: + - Chapa /api/v1/chapa/payments/deposit: post: consumes: @@ -1673,6 +1800,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.ErrorResponse' + security: + - ApiKeyAuth: [] summary: Initiate a deposit tags: - Chapa @@ -1736,6 +1865,105 @@ paths: summary: Chapa payment webhook callback (used by Chapa) tags: - Chapa + /api/v1/chapa/payments/withdraw: + post: + consumes: + - application/json + description: Initiates a withdrawal request to transfer funds to a bank account + via Chapa + parameters: + - description: Withdrawal request details + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.ChapaWithdrawalRequest' + produces: + - application/json + responses: + "201": + description: Chapa withdrawal process initiated successfully + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Invalid request body + schema: + $ref: '#/definitions/domain.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/domain.ErrorResponse' + "422": + description: Unprocessable entity + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/domain.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: Initiate a withdrawal + tags: + - Chapa + /api/v1/currencies: + get: + description: Returns list of supported currencies + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + items: + type: integer + type: array + type: object + summary: Get supported currencies + tags: + - Multi-Currency + /api/v1/currencies/convert: + get: + description: Converts amount from one currency to another + parameters: + - description: Source currency code (e.g., USD) + in: query + name: from + required: true + type: string + - description: Target currency code (e.g., ETB) + in: query + name: to + required: true + type: string + - description: Amount to convert + in: query + name: amount + required: true + type: number + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + type: number + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Convert currency + tags: + - Multi-Currency /api/v1/reports/dashboard: get: consumes: @@ -1776,7 +2004,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/report.DashboardSummary' + $ref: '#/definitions/domain.DashboardSummary' "400": description: Bad Request schema: @@ -1997,27 +2225,6 @@ paths: summary: Refresh token tags: - auth - /banks: - get: - consumes: - - application/json - description: Get list of banks supported by Chapa - produces: - - application/json - responses: - "200": - description: OK - schema: - items: - $ref: '#/definitions/domain.Bank' - type: array - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/domain.ErrorResponse' - summary: Get supported banks - tags: - - Chapa /bet: get: consumes: @@ -2458,6 +2665,35 @@ paths: summary: Delete the branch operation tags: - branch + /branchCashier: + get: + consumes: + - application/json + description: Gets branch for cahier + parameters: + - description: Branch ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.BranchDetailRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets branch for cahier + tags: + - branch /branchWallet: get: consumes: @@ -2516,6 +2752,39 @@ paths: summary: Get cashier by id tags: - cashier + /cashierWallet: + get: + consumes: + - application/json + description: Get wallet for cashier + parameters: + - description: User ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.UserProfileRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Get wallet for cashier + tags: + - cashier /cashiers: get: consumes: @@ -2800,6 +3069,138 @@ paths: summary: Gets branches by company id tags: - branch + /events: + get: + consumes: + - application/json + description: Retrieve all upcoming events from the database + parameters: + - description: Page number + in: query + name: page + type: integer + - description: Page size + in: query + name: page_size + type: integer + - description: League ID Filter + in: query + name: league_id + type: string + - description: Sport ID Filter + in: query + name: sport_id + type: string + - description: Country Code Filter + in: query + name: cc + type: string + - description: Start Time + in: query + name: first_start_time + type: string + - description: End Time + in: query + name: last_start_time + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.UpcomingEvent' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Retrieve all upcoming events + tags: + - prematch + /events/{id}: + delete: + consumes: + - application/json + description: Set the event status to removed + parameters: + - description: Event ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Set the event status to removed + tags: + - event + get: + consumes: + - application/json + description: Retrieve an upcoming event by ID + parameters: + - description: ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.UpcomingEvent' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Retrieve an upcoming by ID + tags: + - prematch + /leagues: + get: + consumes: + - application/json + description: Gets all leagues + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.League' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets all leagues + tags: + - leagues /manager/{id}/branch: get: consumes: @@ -2966,112 +3367,7 @@ paths: summary: Update Managers tags: - manager - /operation: - post: - consumes: - - application/json - description: Creates a operation - parameters: - - description: Creates operation - in: body - name: createBranchOperation - required: true - schema: - $ref: '#/definitions/handlers.CreateBranchOperationReq' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handlers.BranchOperationRes' - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Create a operation - tags: - - branch - /prematch/events: - get: - consumes: - - application/json - description: Retrieve all upcoming events from the database - parameters: - - description: Page number - in: query - name: page - type: integer - - description: Page size - in: query - name: page_size - type: integer - - description: League ID Filter - in: query - name: league_id - type: string - - description: Sport ID Filter - in: query - name: sport_id - type: string - - description: Start Time - in: query - name: first_start_time - type: string - - description: End Time - in: query - name: last_start_time - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - items: - $ref: '#/definitions/domain.UpcomingEvent' - type: array - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Retrieve all upcoming events - tags: - - prematch - /prematch/events/{id}: - get: - consumes: - - application/json - description: Retrieve an upcoming event by ID - parameters: - - description: ID - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/domain.UpcomingEvent' - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Retrieve an upcoming by ID - tags: - - prematch - /prematch/odds: + /odds: get: consumes: - application/json @@ -3092,38 +3388,7 @@ paths: summary: Retrieve all prematch odds tags: - prematch - /prematch/odds/{event_id}: - get: - consumes: - - application/json - description: Retrieve prematch odds for a specific event by event ID - parameters: - - description: Event ID - in: path - name: event_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - items: - $ref: '#/definitions/domain.Odd' - type: array - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Retrieve prematch odds for an event - tags: - - prematch - /prematch/odds/upcoming/{upcoming_id}: + /odds/upcoming/{upcoming_id}: get: consumes: - application/json @@ -3163,7 +3428,7 @@ paths: summary: Retrieve prematch odds by upcoming ID (FI) tags: - prematch - /prematch/odds/upcoming/{upcoming_id}/market/{market_id}: + /odds/upcoming/{upcoming_id}/market/{market_id}: get: consumes: - application/json @@ -3199,6 +3464,36 @@ paths: summary: Retrieve raw odds by Market ID tags: - prematch + /operation: + post: + consumes: + - application/json + description: Creates a operation + parameters: + - description: Creates operation + in: body + name: createBranchOperation + required: true + schema: + $ref: '#/definitions/handlers.CreateBranchOperationReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.BranchOperationRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Create a operation + tags: + - branch /random/bet: post: consumes: @@ -3336,7 +3631,7 @@ paths: description: OK schema: items: - $ref: '#/definitions/domain.BetOutcome' + $ref: '#/definitions/handlers.ResultRes' type: array "400": description: Bad Request diff --git a/gen/db/auth.sql.go b/gen/db/auth.sql.go index 527f25c..9c55b29 100644 --- a/gen/db/auth.sql.go +++ b/gen/db/auth.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: auth.sql package dbgen diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index 40182ae..e4cde1d 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: bet.sql package dbgen diff --git a/gen/db/branch.sql.go b/gen/db/branch.sql.go index d3ef2e5..92e7f80 100644 --- a/gen/db/branch.sql.go +++ b/gen/db/branch.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: branch.sql package dbgen diff --git a/gen/db/cashier.sql.go b/gen/db/cashier.sql.go index 27a1ffb..113771c 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/company.sql.go b/gen/db/company.sql.go index 3c5a6b1..449c8fd 100644 --- a/gen/db/company.sql.go +++ b/gen/db/company.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: company.sql package dbgen diff --git a/gen/db/copyfrom.go b/gen/db/copyfrom.go index 900af58..1212253 100644 --- a/gen/db/copyfrom.go +++ b/gen/db/copyfrom.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: copyfrom.go package dbgen diff --git a/gen/db/db.go b/gen/db/db.go index d892683..84de07c 100644 --- a/gen/db/db.go +++ b/gen/db/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 package dbgen diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index 0ce862a..bd84b8d 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: events.sql package dbgen diff --git a/gen/db/leagues.sql.go b/gen/db/leagues.sql.go index 8762f82..9db2644 100644 --- a/gen/db/leagues.sql.go +++ b/gen/db/leagues.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: leagues.sql package dbgen diff --git a/gen/db/models.go b/gen/db/models.go index 420586e..1da22f3 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 package dbgen @@ -421,12 +421,13 @@ type VirtualGame struct { ID int64 `json:"id"` Name string `json:"name"` Provider string `json:"provider"` - Category string `json:"category"` + Category pgtype.Text `json:"category"` MinBet pgtype.Numeric `json:"min_bet"` MaxBet pgtype.Numeric `json:"max_bet"` - Volatility string `json:"volatility"` + Volatility pgtype.Text `json:"volatility"` + IsActive bool `json:"is_active"` Rtp pgtype.Numeric `json:"rtp"` - IsFeatured pgtype.Bool `json:"is_featured"` + IsFeatured bool `json:"is_featured"` PopularityScore pgtype.Int4 `json:"popularity_score"` ThumbnailUrl pgtype.Text `json:"thumbnail_url"` CreatedAt pgtype.Timestamptz `json:"created_at"` @@ -481,14 +482,15 @@ type WalletThresholdNotification struct { type WalletTransfer struct { ID int64 `json:"id"` - Amount int64 `json:"amount"` - Type string `json:"type"` - ReceiverWalletID int64 `json:"receiver_wallet_id"` + Amount pgtype.Int8 `json:"amount"` + Type pgtype.Text `json:"type"` + ReceiverWalletID pgtype.Int8 `json:"receiver_wallet_id"` SenderWalletID pgtype.Int8 `json:"sender_wallet_id"` CashierID pgtype.Int8 `json:"cashier_id"` - Verified bool `json:"verified"` - ReferenceNumber string `json:"reference_number"` - PaymentMethod string `json:"payment_method"` + Verified pgtype.Bool `json:"verified"` + ReferenceNumber pgtype.Text `json:"reference_number"` + Status pgtype.Text `json:"status"` + PaymentMethod pgtype.Text `json:"payment_method"` CreatedAt pgtype.Timestamp `json:"created_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"` } diff --git a/gen/db/monitor.sql.go b/gen/db/monitor.sql.go index db8a9ba..a9a7ecb 100644 --- a/gen/db/monitor.sql.go +++ b/gen/db/monitor.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: monitor.sql package dbgen diff --git a/gen/db/notification.sql.go b/gen/db/notification.sql.go index 9d9b242..ba9882b 100644 --- a/gen/db/notification.sql.go +++ b/gen/db/notification.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: notification.sql package dbgen diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index 99c47b7..cb30007 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: odds.sql package dbgen diff --git a/gen/db/otp.sql.go b/gen/db/otp.sql.go index 99cdd4c..7dba175 100644 --- a/gen/db/otp.sql.go +++ b/gen/db/otp.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: otp.sql package dbgen diff --git a/gen/db/referal.sql.go b/gen/db/referal.sql.go index d0ab21e..3a7f337 100644 --- a/gen/db/referal.sql.go +++ b/gen/db/referal.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: referal.sql package dbgen diff --git a/gen/db/result.sql.go b/gen/db/result.sql.go index cb3fdd8..bff7b1e 100644 --- a/gen/db/result.sql.go +++ b/gen/db/result.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: result.sql package dbgen diff --git a/gen/db/ticket.sql.go b/gen/db/ticket.sql.go index 443b266..4140384 100644 --- a/gen/db/ticket.sql.go +++ b/gen/db/ticket.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: ticket.sql package dbgen diff --git a/gen/db/transactions.sql.go b/gen/db/transactions.sql.go index 80e6022..cbd5743 100644 --- a/gen/db/transactions.sql.go +++ b/gen/db/transactions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: transactions.sql package dbgen diff --git a/gen/db/transfer.sql.go b/gen/db/transfer.sql.go index 2c8e6f6..18b6243 100644 --- a/gen/db/transfer.sql.go +++ b/gen/db/transfer.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: transfer.sql package dbgen @@ -20,21 +20,23 @@ INSERT INTO wallet_transfer ( cashier_id, verified, reference_number, + status, payment_method ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8) -RETURNING id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, payment_method, created_at, updated_at +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) +RETURNING id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, status, payment_method, created_at, updated_at ` type CreateTransferParams struct { - Amount int64 `json:"amount"` - Type string `json:"type"` - ReceiverWalletID int64 `json:"receiver_wallet_id"` + Amount pgtype.Int8 `json:"amount"` + Type pgtype.Text `json:"type"` + ReceiverWalletID pgtype.Int8 `json:"receiver_wallet_id"` SenderWalletID pgtype.Int8 `json:"sender_wallet_id"` CashierID pgtype.Int8 `json:"cashier_id"` - Verified bool `json:"verified"` - ReferenceNumber string `json:"reference_number"` - PaymentMethod string `json:"payment_method"` + Verified pgtype.Bool `json:"verified"` + ReferenceNumber pgtype.Text `json:"reference_number"` + Status pgtype.Text `json:"status"` + PaymentMethod pgtype.Text `json:"payment_method"` } func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams) (WalletTransfer, error) { @@ -46,6 +48,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams) arg.CashierID, arg.Verified, arg.ReferenceNumber, + arg.Status, arg.PaymentMethod, ) var i WalletTransfer @@ -58,6 +61,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams) &i.CashierID, &i.Verified, &i.ReferenceNumber, + &i.Status, &i.PaymentMethod, &i.CreatedAt, &i.UpdatedAt, @@ -66,7 +70,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams) } const GetAllTransfers = `-- name: GetAllTransfers :many -SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, payment_method, created_at, updated_at +SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, status, payment_method, created_at, updated_at FROM wallet_transfer ` @@ -88,6 +92,7 @@ func (q *Queries) GetAllTransfers(ctx context.Context) ([]WalletTransfer, error) &i.CashierID, &i.Verified, &i.ReferenceNumber, + &i.Status, &i.PaymentMethod, &i.CreatedAt, &i.UpdatedAt, @@ -103,7 +108,7 @@ func (q *Queries) GetAllTransfers(ctx context.Context) ([]WalletTransfer, error) } const GetTransferByID = `-- name: GetTransferByID :one -SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, payment_method, created_at, updated_at +SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, status, payment_method, created_at, updated_at FROM wallet_transfer WHERE id = $1 ` @@ -120,6 +125,7 @@ func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer &i.CashierID, &i.Verified, &i.ReferenceNumber, + &i.Status, &i.PaymentMethod, &i.CreatedAt, &i.UpdatedAt, @@ -128,12 +134,12 @@ func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer } const GetTransferByReference = `-- name: GetTransferByReference :one -SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, payment_method, created_at, updated_at +SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, status, payment_method, created_at, updated_at FROM wallet_transfer WHERE reference_number = $1 ` -func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber string) (WalletTransfer, error) { +func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber pgtype.Text) (WalletTransfer, error) { row := q.db.QueryRow(ctx, GetTransferByReference, referenceNumber) var i WalletTransfer err := row.Scan( @@ -145,6 +151,7 @@ func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber st &i.CashierID, &i.Verified, &i.ReferenceNumber, + &i.Status, &i.PaymentMethod, &i.CreatedAt, &i.UpdatedAt, @@ -153,13 +160,13 @@ func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber st } const GetTransfersByWallet = `-- name: GetTransfersByWallet :many -SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, payment_method, created_at, updated_at +SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, status, payment_method, created_at, updated_at FROM wallet_transfer WHERE receiver_wallet_id = $1 OR sender_wallet_id = $1 ` -func (q *Queries) GetTransfersByWallet(ctx context.Context, receiverWalletID int64) ([]WalletTransfer, error) { +func (q *Queries) GetTransfersByWallet(ctx context.Context, receiverWalletID pgtype.Int8) ([]WalletTransfer, error) { rows, err := q.db.Query(ctx, GetTransfersByWallet, receiverWalletID) if err != nil { return nil, err @@ -177,6 +184,7 @@ func (q *Queries) GetTransfersByWallet(ctx context.Context, receiverWalletID int &i.CashierID, &i.Verified, &i.ReferenceNumber, + &i.Status, &i.PaymentMethod, &i.CreatedAt, &i.UpdatedAt, @@ -191,6 +199,23 @@ func (q *Queries) GetTransfersByWallet(ctx context.Context, receiverWalletID int return items, nil } +const UpdateTransferStatus = `-- name: UpdateTransferStatus :exec +UPDATE wallet_transfer +SET status = $1, + updated_at = CURRENT_TIMESTAMP +WHERE id = $2 +` + +type UpdateTransferStatusParams struct { + Status pgtype.Text `json:"status"` + ID int64 `json:"id"` +} + +func (q *Queries) UpdateTransferStatus(ctx context.Context, arg UpdateTransferStatusParams) error { + _, err := q.db.Exec(ctx, UpdateTransferStatus, arg.Status, arg.ID) + return err +} + const UpdateTransferVerification = `-- name: UpdateTransferVerification :exec UPDATE wallet_transfer SET verified = $1, @@ -199,8 +224,8 @@ WHERE id = $2 ` type UpdateTransferVerificationParams struct { - Verified bool `json:"verified"` - ID int64 `json:"id"` + Verified pgtype.Bool `json:"verified"` + ID int64 `json:"id"` } func (q *Queries) UpdateTransferVerification(ctx context.Context, arg UpdateTransferVerificationParams) error { diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index 2b440c2..89051b2 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: user.sql package dbgen diff --git a/gen/db/virtual_games.sql.go b/gen/db/virtual_games.sql.go index eb832e7..16034ee 100644 --- a/gen/db/virtual_games.sql.go +++ b/gen/db/virtual_games.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: virtual_games.sql package dbgen diff --git a/gen/db/wallet.sql.go b/gen/db/wallet.sql.go index 64c3359..e46ea0b 100644 --- a/gen/db/wallet.sql.go +++ b/gen/db/wallet.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: wallet.sql package dbgen diff --git a/internal/config/config.go b/internal/config/config.go index 4ba0848..802302e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -56,6 +56,9 @@ type VeliGamesConfig struct { } type Config struct { + FIXER_API_KEY string + FIXER_BASE_URL string + BASE_CURRENCY domain.IntCurrency Port int DbUrl string RefreshExpiry int @@ -68,6 +71,8 @@ type Config struct { AFRO_SMS_SENDER_NAME string AFRO_SMS_RECEIVER_PHONE_NUMBER string ADRO_SMS_HOST_URL string + CHAPA_TRANSFER_TYPE string + CHAPA_PAYMENT_TYPE string CHAPA_SECRET_KEY string CHAPA_PUBLIC_KEY string CHAPA_BASE_URL string @@ -104,6 +109,13 @@ func (c *Config) loadEnv() error { c.ReportExportPath = os.Getenv("REPORT_EXPORT_PATH") + c.CHAPA_TRANSFER_TYPE = os.Getenv("CHAPA_TRANSFER_TYPE") + c.CHAPA_PAYMENT_TYPE = os.Getenv("CHAPA_PAYMENT_TYPE") + + c.FIXER_API_KEY = os.Getenv("FIXER_API_KEY") + c.BASE_CURRENCY = domain.IntCurrency(os.Getenv("BASE_CURRENCY")) + c.FIXER_BASE_URL = os.Getenv("FIXER_BASE_URL") + portStr := os.Getenv("PORT") if portStr == "" { return ErrInvalidPort diff --git a/internal/domain/chapa.go b/internal/domain/chapa.go index 13e7a5f..2a3b236 100644 --- a/internal/domain/chapa.go +++ b/internal/domain/chapa.go @@ -1,9 +1,27 @@ package domain -import "time" +import ( + "errors" + "time" +) + +var ( + ErrInsufficientBalance = errors.New("insufficient balance") + ErrInvalidWithdrawalAmount = errors.New("invalid withdrawal amount") + ErrWithdrawalNotFound = errors.New("withdrawal not found") +) type PaymentStatus string +type WithdrawalStatus string + +const ( + WithdrawalStatusPending WithdrawalStatus = "pending" + WithdrawalStatusProcessing WithdrawalStatus = "processing" + WithdrawalStatusCompleted WithdrawalStatus = "completed" + WithdrawalStatusFailed WithdrawalStatus = "failed" +) + const ( PaymentStatusPending PaymentStatus = "pending" PaymentStatusCompleted PaymentStatus = "completed" @@ -91,3 +109,86 @@ type BankData struct { UpdatedAt time.Time `json:"updated_at"` Currency string `json:"currency"` } + +type ChapaWithdrawal struct { + ID string + UserID int64 + Amount Currency + AccountNumber string + BankCode string + Status WithdrawalStatus + Reference string + CreatedAt time.Time + UpdatedAt time.Time +} + +type ChapaWithdrawalRequest struct { + AccountName string `json:"account_name"` + AccountNumber string `json:"account_number"` + Amount string `json:"amount"` // string because Chapa API uses string for monetary values + Currency string `json:"currency"` + Reference string `json:"reference"` + BankCode int `json:"bank_code"` +} + +// type ChapaWithdrawalRequest struct { +// AccountName string `json:"account_name"` +// AccountNumber string `json:"account_number"` +// Amount Currency `json:"amount"` +// Currency string `json:"currency"` +// BeneficiaryName string `json:"beneficiary_name"` +// BankCode string `json:"bank_code"` +// PhoneNumber string `json:"phone_number"` +// } + +type ChapaWithdrawalResponse struct { + Status string `json:"status"` + Message string `json:"message"` + Data struct { + Reference string `json:"reference"` + } `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"` +} diff --git a/internal/domain/currency.go b/internal/domain/currency.go new file mode 100644 index 0000000..3a2eb49 --- /dev/null +++ b/internal/domain/currency.go @@ -0,0 +1,88 @@ +package domain + +import ( + "errors" + "fmt" + "time" +) + +type IntCurrency string + +const ( + ETB IntCurrency = "ETB" // Ethiopian Birr + NGN IntCurrency = "NGN" // Nigerian Naira + ZAR IntCurrency = "ZAR" // South African Rand + EGP IntCurrency = "EGP" // Egyptian Pound + KES IntCurrency = "KES" // Kenyan Shilling + UGX IntCurrency = "UGX" // Ugandan Shilling + TZS IntCurrency = "TZS" // Tanzanian Shilling + RWF IntCurrency = "RWF" // Rwandan Franc + BIF IntCurrency = "BIF" // Burundian Franc + XOF IntCurrency = "XOF" // West African CFA Franc (BCEAO) + XAF IntCurrency = "XAF" // Central African CFA Franc (BEAC) + GHS IntCurrency = "GHS" // Ghanaian Cedi + SDG IntCurrency = "SDG" // Sudanese Pound + SSP IntCurrency = "SSP" // South Sudanese Pound + DZD IntCurrency = "DZD" // Algerian Dinar + MAD IntCurrency = "MAD" // Moroccan Dirham + TND IntCurrency = "TND" // Tunisian Dinar + LYD IntCurrency = "LYD" // Libyan Dinar + MZN IntCurrency = "MZN" // Mozambican Metical + AOA IntCurrency = "AOA" // Angolan Kwanza + BWP IntCurrency = "BWP" // Botswana Pula + ZMW IntCurrency = "ZMW" // Zambian Kwacha + MWK IntCurrency = "MWK" // Malawian Kwacha + LSL IntCurrency = "LSL" // Lesotho Loti + NAD IntCurrency = "NAD" // Namibian Dollar + SZL IntCurrency = "SZL" // Swazi Lilangeni + CVE IntCurrency = "CVE" // Cape Verdean Escudo + GMD IntCurrency = "GMD" // Gambian Dalasi + SLL IntCurrency = "SLL" // Sierra Leonean Leone + LRD IntCurrency = "LRD" // Liberian Dollar + GNF IntCurrency = "GNF" // Guinean Franc + XCD IntCurrency = "XCD" // Eastern Caribbean Dollar (used in Saint Lucia) + MRU IntCurrency = "MRU" // Mauritanian Ouguiya + KMF IntCurrency = "KMF" // Comorian Franc + DJF IntCurrency = "DJF" // Djiboutian Franc + SOS IntCurrency = "SOS" // Somali Shilling + ERN IntCurrency = "ERN" // Eritrean Nakfa + MGA IntCurrency = "MGA" // Malagasy Ariary + SCR IntCurrency = "SCR" // Seychellois Rupee + MUR IntCurrency = "MUR" // Mauritian Rupee + + // International currencies (already listed) + USD IntCurrency = "USD" // US Dollar + EUR IntCurrency = "EUR" // Euro + GBP IntCurrency = "GBP" // British Pound +) + +var ( + ErrUnsupportedIntCurrency = errors.New("unsupported IntCurrency") + ErrIntCurrencyConversion = errors.New("IntCurrency conversion failed") +) + +// IntCurrencyRate represents exchange rate between two currencies +type IntCurrencyRate struct { + From IntCurrency + To IntCurrency + Rate float64 + ValidUntil time.Time +} + +// Convert converts amount from one IntCurrency to another +func (cr IntCurrencyRate) Convert(amount float64) (float64, error) { + if time.Now().After(cr.ValidUntil) { + return 0, fmt.Errorf("%w: rate expired", ErrIntCurrencyConversion) + } + return amount * cr.Rate, nil +} + +// ValidateIntCurrency checks if IntCurrency is supported +func ValidateIntCurrency(c IntCurrency) error { + switch c { + case ETB, USD, EUR, GBP: + return nil + default: + return fmt.Errorf("%w: %s", ErrUnsupportedIntCurrency, c) + } +} diff --git a/internal/domain/responses.go b/internal/domain/responses.go index 841ca37..4836dc0 100644 --- a/internal/domain/responses.go +++ b/internal/domain/responses.go @@ -14,6 +14,14 @@ func UnProcessableEntityResponse(c *fiber.Ctx) error { }) } +func UnExpectedErrorResponse(c *fiber.Ctx) error { + return c.Status(fiber.StatusInternalServerError).JSON(Response{ + Message: "Unexpected internal error", + StatusCode: fiber.StatusInternalServerError, + Success: false, + }) +} + func FiberErrorResponse(c *fiber.Ctx, err error) error { var statusCode int var message string diff --git a/internal/domain/transfer.go b/internal/domain/transfer.go index bf968d2..6e366a2 100644 --- a/internal/domain/transfer.go +++ b/internal/domain/transfer.go @@ -33,6 +33,7 @@ type Transfer struct { ReceiverWalletID int64 SenderWalletID int64 ReferenceNumber string + Status string CashierID ValidInt64 CreatedAt time.Time UpdatedAt time.Time @@ -42,6 +43,7 @@ type CreateTransfer struct { Amount Currency Verified bool ReferenceNumber string + Status string ReceiverWalletID int64 SenderWalletID int64 CashierID ValidInt64 diff --git a/internal/domain/wallet.go b/internal/domain/wallet.go index 387dbd7..5a90078 100644 --- a/internal/domain/wallet.go +++ b/internal/domain/wallet.go @@ -5,6 +5,7 @@ import "time" type Wallet struct { ID int64 Balance Currency + Currency IntCurrency IsWithdraw bool IsBettable bool IsTransferable bool diff --git a/internal/repository/currency.go b/internal/repository/currency.go new file mode 100644 index 0000000..4aea3eb --- /dev/null +++ b/internal/repository/currency.go @@ -0,0 +1,96 @@ +package repository + +import ( + "context" + "database/sql" + "fmt" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type CurrencyRepository interface { + GetExchangeRate(ctx context.Context, from, to domain.IntCurrency) (domain.IntCurrencyRate, error) + StoreExchangeRate(ctx context.Context, rate domain.IntCurrencyRate) error + GetSupportedCurrencies(ctx context.Context) ([]domain.IntCurrency, error) +} + +type CurrencyPostgresRepository struct { + store *Store +} + +func NewCurrencyPostgresRepository(store *Store) *CurrencyPostgresRepository { + return &CurrencyPostgresRepository{store: store} +} + +func (r *CurrencyPostgresRepository) GetExchangeRate(ctx context.Context, from, to domain.IntCurrency) (domain.IntCurrencyRate, error) { + const query = ` + SELECT from_currency, to_currency, rate, precision, valid_until + FROM exchange_rates + WHERE from_currency = $1 AND to_currency = $2 AND valid_until > NOW() + ORDER BY created_at DESC + LIMIT 1` + + var rate domain.IntCurrencyRate + err := r.store.conn.QueryRow(ctx, query, from, to).Scan( + &rate.From, + &rate.To, + &rate.Rate, + &rate.ValidUntil, + ) + if err != nil { + if err == sql.ErrNoRows { + return domain.IntCurrencyRate{}, fmt.Errorf("%w: no rate found for %s to %s", + domain.ErrIntCurrencyConversion, from, to) + } + return domain.IntCurrencyRate{}, fmt.Errorf("failed to get exchange rate: %w", err) + } + + return rate, nil +} + +func (r *CurrencyPostgresRepository) StoreExchangeRate(ctx context.Context, rate domain.IntCurrencyRate) error { + const query = ` + INSERT INTO exchange_rates (from_currency, to_currency, rate, precision, valid_until) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (from_currency, to_currency) + DO UPDATE SET + rate = EXCLUDED.rate, + precision = EXCLUDED.precision, + valid_until = EXCLUDED.valid_until, + created_at = NOW()` + + _, err := r.store.conn.Exec(ctx, query, + rate.From, + rate.To, + rate.Rate, + rate.ValidUntil) + if err != nil { + return fmt.Errorf("failed to store exchange rate: %w", err) + } + + return nil +} + +func (r *CurrencyPostgresRepository) GetSupportedCurrencies(ctx context.Context) ([]domain.IntCurrency, error) { + const query = `SELECT DISTINCT currency FROM supported_currencies ORDER BY currency` + + var currencies []domain.IntCurrency + rows, err := r.store.conn.Query(ctx, query) + if err != nil { + return nil, fmt.Errorf("failed to get supported currencies: %w", err) + } + defer rows.Close() + + for rows.Next() { + var currency domain.IntCurrency + if err := rows.Scan(¤cy); err != nil { + return nil, fmt.Errorf("failed to scan currency: %w", err) + } + currencies = append(currencies, currency) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("row iteration error: %w", err) + } + + return currencies, nil +} diff --git a/internal/repository/transfer.go b/internal/repository/transfer.go index 58d3b05..75c66ea 100644 --- a/internal/repository/transfer.go +++ b/internal/repository/transfer.go @@ -11,24 +11,27 @@ import ( func convertDBTransfer(transfer dbgen.WalletTransfer) domain.Transfer { return domain.Transfer{ ID: transfer.ID, - Amount: domain.Currency(transfer.Amount), - Type: domain.TransferType(transfer.Type), - Verified: transfer.Verified, - ReceiverWalletID: transfer.ReceiverWalletID, + Amount: domain.Currency(transfer.Amount.Int64), + Type: domain.TransferType(transfer.Type.String), + Verified: transfer.Verified.Bool, + ReceiverWalletID: transfer.ReceiverWalletID.Int64, SenderWalletID: transfer.SenderWalletID.Int64, CashierID: domain.ValidInt64{ Value: transfer.CashierID.Int64, Valid: transfer.CashierID.Valid, }, - PaymentMethod: domain.PaymentMethod(transfer.PaymentMethod), + PaymentMethod: domain.PaymentMethod(transfer.PaymentMethod.String), } } func convertCreateTransfer(transfer domain.CreateTransfer) dbgen.CreateTransferParams { return dbgen.CreateTransferParams{ - Amount: int64(transfer.Amount), - Type: string(transfer.Type), - ReceiverWalletID: transfer.ReceiverWalletID, + Amount: pgtype.Int8{Int64: int64(transfer.Amount), Valid: true}, + Type: pgtype.Text{String: string(transfer.Type), Valid: true}, + ReceiverWalletID: pgtype.Int8{ + Int64: transfer.ReceiverWalletID, + Valid: true, + }, SenderWalletID: pgtype.Int8{ Int64: transfer.SenderWalletID, Valid: true, @@ -37,7 +40,7 @@ func convertCreateTransfer(transfer domain.CreateTransfer) dbgen.CreateTransferP Int64: transfer.CashierID.Value, Valid: transfer.CashierID.Valid, }, - PaymentMethod: string(transfer.PaymentMethod), + PaymentMethod: pgtype.Text{String: string(transfer.PaymentMethod), Valid: true}, } } @@ -62,7 +65,7 @@ func (s *Store) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error) return result, nil } func (s *Store) GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.Transfer, error) { - transfers, err := s.queries.GetTransfersByWallet(ctx, walletID) + transfers, err := s.queries.GetTransfersByWallet(ctx, pgtype.Int8{Int64: walletID, Valid: true}) if err != nil { return nil, err } @@ -76,7 +79,7 @@ func (s *Store) GetTransfersByWallet(ctx context.Context, walletID int64) ([]dom } func (s *Store) GetTransferByReference(ctx context.Context, reference string) (domain.Transfer, error) { - transfer, err := s.queries.GetTransferByReference(ctx, reference) + transfer, err := s.queries.GetTransferByReference(ctx, pgtype.Text{String: reference, Valid: true}) if err != nil { return domain.Transfer{}, nil } @@ -94,7 +97,16 @@ func (s *Store) GetTransferByID(ctx context.Context, id int64) (domain.Transfer, func (s *Store) UpdateTransferVerification(ctx context.Context, id int64, verified bool) error { err := s.queries.UpdateTransferVerification(ctx, dbgen.UpdateTransferVerificationParams{ ID: id, - Verified: verified, + Verified: pgtype.Bool{Bool: verified, Valid: true}, + }) + + return err +} + +func (s *Store) UpdateTransferStatus(ctx context.Context, id int64, status string) error { + err := s.queries.UpdateTransferStatus(ctx, dbgen.UpdateTransferStatusParams{ + ID: id, + Status: pgtype.Text{String: status, Valid: true}, }) return err diff --git a/internal/services/chapa/client.go b/internal/services/chapa/client.go index dbf12b3..94e1573 100644 --- a/internal/services/chapa/client.go +++ b/internal/services/chapa/client.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -211,24 +212,78 @@ func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) return banks, nil } -// Helper method to generate account regex based on bank type -// func GetAccountRegex(bank domain.Bank) string { -// if bank.IsMobileMoney != nil && bank.IsMobileMoney == 1 { -// return `^09[0-9]{8}$` // Ethiopian mobile money pattern -// } -// return fmt.Sprintf(`^[0-9]{%d}$`, bank.AcctLength) -// } +func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawalRequest) (bool, error) { + // base, err := url.Parse(c.baseURL) + // if err != nil { + // return false, fmt.Errorf("invalid base URL: %w", err) + // } + endpoint := c.baseURL + "/transfers" + fmt.Printf("\n\nChapa withdrawal URL is %v\n\n", endpoint) -// // Helper method to generate example account number -// func GetExampleAccount(bank domain.Bank) string { -// if bank.IsMobileMoney != nil && *bank.IsMobileMoney { -// return "0912345678" // Ethiopian mobile number example -// } + reqBody, err := json.Marshal(req) + if err != nil { + return false, fmt.Errorf("failed to marshal request: %w", err) + } -// // Generate example based on length -// example := "1" -// for i := 1; i < bank.AcctLength; i++ { -// example += fmt.Sprintf("%d", i%10) -// } -// return example -// } + httpReq, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewBuffer(reqBody)) + if err != nil { + return false, fmt.Errorf("failed to create request: %w", err) + } + + c.setHeaders(httpReq) + + resp, err := c.httpClient.Do(httpReq) + if err != nil { + return false, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return false, fmt.Errorf("chapa api returned status: %d", resp.StatusCode) + } + + var response domain.ChapaWithdrawalResponse + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return false, fmt.Errorf("failed to decode response: %w", err) + } + + return response.Status == string(domain.WithdrawalStatusProcessing), nil +} + +func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain.ChapaVerificationResponse, error) { + base, err := url.Parse(c.baseURL) + if err != nil { + return nil, fmt.Errorf("invalid base URL: %w", err) + } + endpoint := base.ResolveReference(&url.URL{Path: fmt.Sprintf("/v1/transfers/%s/verify", reference)}) + + httpReq, err := http.NewRequestWithContext(ctx, "GET", endpoint.String(), nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + c.setHeaders(httpReq) + + resp, err := c.httpClient.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("chapa api returned status: %d", resp.StatusCode) + } + + var verification domain.ChapaVerificationResponse + if err := json.NewDecoder(resp.Body).Decode(&verification); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &verification, nil +} + +func (c *Client) setHeaders(req *http.Request) { + req.Header.Set("Authorization", "Bearer "+c.secretKey) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") +} diff --git a/internal/services/chapa/port.go b/internal/services/chapa/port.go index 581b53f..78ae033 100644 --- a/internal/services/chapa/port.go +++ b/internal/services/chapa/port.go @@ -16,7 +16,10 @@ import ( type ChapaStore interface { InitializePayment(request domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error) - VerifyPayment(reference string) (domain.ChapaDepositVerification, error) - ManualVerifyPayment(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) + // VerifyPayment(reference string) (domain.ChapaDepositVerification, error) + ManualVerifTransaction(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) + CreateWithdrawal(userID string, amount float64, accountNumber, bankCode string) (*domain.ChapaWithdrawal, error) + HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebHookTransfer) error + HandleVerifyWithdrawWebhook(ctx context.Context, payment domain.ChapaWebHookPayment) error } diff --git a/internal/services/chapa/service.go b/internal/services/chapa/service.go index cb9281a..31a537f 100644 --- a/internal/services/chapa/service.go +++ b/internal/services/chapa/service.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strconv" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -20,7 +21,7 @@ var ( type Service struct { transferStore wallet.TransferStore - walletStore wallet.WalletStore + walletStore wallet.Service userStore user.UserStore cfg *config.Config chapaClient *Client @@ -28,7 +29,7 @@ type Service struct { func NewService( transferStore wallet.TransferStore, - walletStore wallet.WalletStore, + walletStore wallet.Service, userStore user.UserStore, chapaClient *Client, @@ -110,42 +111,100 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma return response.CheckoutURL, nil } +func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req domain.ChapaWithdrawalRequest) (*domain.Transfer, error) { + // Parse and validate amount + amount, err := strconv.ParseInt(req.Amount, 10, 64) + if err != nil || amount <= 0 { + return nil, domain.ErrInvalidWithdrawalAmount + } -// VerifyDeposit handles payment verification from webhook -func (s *Service) VerifyDeposit(ctx context.Context, reference string) error { - // Find payment by reference - payment, err := s.transferStore.GetTransferByReference(ctx, reference) + // Get user details + // user, err := s.userStore.GetUserByID(ctx, userID) + // if err != nil { + // return nil, fmt.Errorf("failed to get user: %w", err) + // } + + // Get user's wallet + wallets, err := s.walletStore.GetWalletsByUser(ctx, userID) if err != nil { - return ErrPaymentNotFound + return nil, fmt.Errorf("failed to get user wallets: %w", err) } - // Skip if already completed - if payment.Verified { - return nil - } - - // Verify payment with Chapa - verification, err := s.chapaClient.VerifyPayment(ctx, reference) - if err != nil { - return fmt.Errorf("failed to verify payment: %w", err) - } - - // Update payment status - if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil { - return fmt.Errorf("failed to update payment status: %w", err) - } - - // If payment is completed, credit user's wallet - if verification.Status == domain.PaymentStatusCompleted { - if err := s.walletStore.UpdateBalance(ctx, payment.SenderWalletID, payment.Amount); err != nil { - return fmt.Errorf("failed to credit user wallet: %w", err) + var withdrawWallet domain.Wallet + for _, wallet := range wallets { + if wallet.IsWithdraw { + withdrawWallet = wallet + break } } - return nil + if withdrawWallet.ID == 0 { + return nil, errors.New("withdrawal wallet not found") + } + // Check balance + if withdrawWallet.Balance < domain.Currency(amount) { + return nil, domain.ErrInsufficientBalance + } + + // Generate unique reference + reference := uuid.New().String() + + createTransfer := domain.CreateTransfer{ + Amount: domain.Currency(amount), + Type: domain.WITHDRAW, + ReceiverWalletID: 1, + SenderWalletID: withdrawWallet.ID, + Status: string(domain.PaymentStatusPending), + Verified: false, + ReferenceNumber: reference, + PaymentMethod: domain.TRANSFER_CHAPA, + } + + transfer, err := s.transferStore.CreateTransfer(ctx, createTransfer) + + if err != nil { + return nil, fmt.Errorf("failed to create transfer record: %w", err) + } + // Initiate transfer with Chapa + transferReq := domain.ChapaWithdrawalRequest{ + AccountName: req.AccountName, + AccountNumber: req.AccountNumber, + Amount: fmt.Sprintf("%d", amount), + Currency: req.Currency, + Reference: reference, + // BeneficiaryName: fmt.Sprintf("%s %s", user.FirstName, user.LastName), + BankCode: req.BankCode, + } + + success, err := s.chapaClient.InitiateTransfer(ctx, transferReq) + if err != nil || !success { + // Update withdrawal status to failed + _ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed)) + return nil, fmt.Errorf("failed to initiate transfer: %w", err) + } + + // Update withdrawal status to processing + if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusProcessing)); err != nil { + return nil, fmt.Errorf("failed to update withdrawal status: %w", err) + } + // Deduct from wallet (or wait for webhook confirmation depending on your flow) + newBalance := withdrawWallet.Balance - domain.Currency(amount) + if err := s.walletStore.UpdateBalance(ctx, withdrawWallet.ID, newBalance); err != nil { + return nil, fmt.Errorf("failed to update wallet balance: %w", err) + } + + return &transfer, nil } -func (s *Service) ManualVerifyPayment(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) { +func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.Bank, error) { + banks, err := s.chapaClient.FetchSupportedBanks(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch banks: %w", err) + } + return banks, nil +} + +func (s *Service) ManualVerifTransaction(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) { // First check if we already have a verified record transfer, err := s.transferStore.GetTransferByReference(ctx, txRef) if err == nil && transfer.Verified { @@ -183,10 +242,76 @@ func (s *Service) ManualVerifyPayment(ctx context.Context, txRef string) (*domai }, nil } -func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.Bank, error) { - banks, err := s.chapaClient.FetchSupportedBanks(ctx) +func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebHookTransfer) error { + // Find payment by reference + payment, err := s.transferStore.GetTransferByReference(ctx, transfer.Reference) if err != nil { - return nil, fmt.Errorf("failed to fetch banks: %w", err) + return ErrPaymentNotFound } - return banks, nil + + if payment.Verified { + return nil + } + + // Verify payment with Chapa + // verification, err := s.chapaClient.VerifyPayment(ctx, transfer.Reference) + // if err != nil { + // return fmt.Errorf("failed to verify payment: %w", err) + // } + + // Update payment status + // verified := false + // if transfer.Status == string(domain.PaymentStatusCompleted) { + // verified = true + // } + + if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil { + return fmt.Errorf("failed to update payment status: %w", err) + } + + // If payment is completed, credit user's wallet + if transfer.Status == string(domain.PaymentStatusCompleted) { + if err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID, payment.Amount); err != nil { + return fmt.Errorf("failed to credit user wallet: %w", err) + } + } + + return nil +} + +func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domain.ChapaWebHookPayment) error { + // Find payment by reference + transfer, err := s.transferStore.GetTransferByReference(ctx, payment.Reference) + if err != nil { + return ErrPaymentNotFound + } + + if transfer.Verified { + return nil + } + + // Verify payment with Chapa + // verification, err := s.chapaClient.VerifyPayment(ctx, payment.Reference) + // if err != nil { + // return fmt.Errorf("failed to verify payment: %w", err) + // } + + // Update payment status + // verified := false + // if transfer.Status == string(domain.PaymentStatusCompleted) { + // verified = true + // } + + if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil { + return fmt.Errorf("failed to update payment status: %w", err) + } + + // If payment is completed, credit user's wallet + if payment.Status == string(domain.PaymentStatusFailed) { + if err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID, transfer.Amount); err != nil { + return fmt.Errorf("failed to credit user wallet: %w", err) + } + } + + return nil } diff --git a/internal/services/currency/fetcher.go b/internal/services/currency/fetcher.go new file mode 100644 index 0000000..76d4739 --- /dev/null +++ b/internal/services/currency/fetcher.go @@ -0,0 +1,69 @@ +package currency + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type FixerFetcher struct { + apiKey string + baseURL string + httpClient *http.Client +} + +func NewFixerFetcher(apiKey string, baseURL string) *FixerFetcher { + return &FixerFetcher{ + apiKey: apiKey, + baseURL: baseURL, + httpClient: &http.Client{Timeout: 10 * time.Second}, + } +} + +type fixerResponse struct { + Success bool `json:"success"` + Base string `json:"base"` + Date string `json:"date"` + Rates map[string]float64 `json:"rates"` +} + +func (f *FixerFetcher) FetchLatestRates(ctx context.Context, baseCurrency domain.IntCurrency) (map[domain.IntCurrency]float64, error) { + url := fmt.Sprintf("%s/latest?base=%s", f.baseURL, baseCurrency) + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("apikey", f.apiKey) + + resp, err := f.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to fetch rates: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var result fixerResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + if !result.Success { + return nil, fmt.Errorf("api returned unsuccessful response") + } + + rates := make(map[domain.IntCurrency]float64) + for currency, rate := range result.Rates { + rates[domain.IntCurrency(currency)] = rate + } + + return rates, nil +} diff --git a/internal/services/currency/service.go b/internal/services/currency/service.go new file mode 100644 index 0000000..5351b11 --- /dev/null +++ b/internal/services/currency/service.go @@ -0,0 +1,125 @@ +package currency + +import ( + "context" + "fmt" + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" +) + +type Service struct { + repo repository.CurrencyRepository + baseCurrency domain.IntCurrency + fixerFetcher *FixerFetcher +} + +func NewService(repo repository.CurrencyRepository, baseCurrency domain.IntCurrency, fixerFetcher *FixerFetcher) *Service { + return &Service{repo: repo} +} + +func (s *Service) Convert(ctx context.Context, amount float64, from, to domain.IntCurrency) (float64, error) { + if from == to { + return amount, nil + } + + rate, err := s.repo.GetExchangeRate(ctx, from, to) + if err != nil { + return 0, err + } + + return rate.Convert(amount) +} + +func (s *Service) GetSupportedCurrencies(ctx context.Context) ([]domain.IntCurrency, error) { + return s.repo.GetSupportedCurrencies(ctx) +} + +func (s *Service) UpdateRates(ctx context.Context) error { + // Implement fetching from external API (e.g., Fixer, Open Exchange Rates) + rates := map[domain.IntCurrency]map[domain.IntCurrency]float64{ + domain.ETB: { + domain.USD: 0.018, + domain.EUR: 0.016, + domain.GBP: 0.014, + }, + // Add other currencies... + } + + for from, toRates := range rates { + for to, rate := range toRates { + err := s.repo.StoreExchangeRate(ctx, domain.IntCurrencyRate{ + From: from, + To: to, + Rate: rate, + ValidUntil: time.Now().Add(24 * time.Hour), // Refresh daily + }) + if err != nil { + return err + } + } + } + + return nil +} + +func (s *Service) FetchAndStoreRates(ctx context.Context) error { + // s.logger.Info("Starting exchange rate update") + + rates, err := s.fixerFetcher.FetchLatestRates(ctx, s.baseCurrency) + if err != nil { + // s.logger.Error("Failed to fetch rates", "error", err) + return fmt.Errorf("failed to fetch rates: %w", err) + } + + // Convert to integer rates with precision + const precision = 6 // 1.000000 + for currency, rate := range rates { + if currency == s.baseCurrency { + continue + } + + intRate := domain.IntCurrencyRate{ + From: s.baseCurrency, + To: currency, + Rate: rate * float64(pow10(precision)), + ValidUntil: time.Now().Add(24 * time.Hour), // Rates valid for 24 hours + } + + if err := s.repo.StoreExchangeRate(ctx, intRate); err != nil { + // s.logger.Error("Failed to store rate", + // "from", s.baseCurrency, + // "to", currency, + // "error", err) + continue // Try to store other rates even if one fails + } + + // Also store the inverse rate + inverseRate := domain.IntCurrencyRate{ + From: currency, + To: s.baseCurrency, + Rate: (1 / rate) * float64(pow10(precision)), + ValidUntil: time.Now().Add(24 * time.Hour), + } + + if err := s.repo.StoreExchangeRate(ctx, inverseRate); err != nil { + // s.logger.Error("Failed to store inverse rate", + // "from", currency, + // "to", s.baseCurrency, + // "error", err) + return fmt.Errorf("Error storing exchange rates") + } + } + + // s.logger.Info("Exchange rates updated successfully") + return nil +} + +func pow10(n int) int64 { + result := int64(1) + for i := 0; i < n; i++ { + result *= 10 + } + return result +} diff --git a/internal/services/currency/worker.go b/internal/services/currency/worker.go new file mode 100644 index 0000000..c020e08 --- /dev/null +++ b/internal/services/currency/worker.go @@ -0,0 +1,55 @@ +package currency + +import ( + "context" + "fmt" + "log/slog" + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/config" + "github.com/go-co-op/gocron" +) + +type ExchangeRateWorker struct { + fetcherService *FixerFetcher + scheduler *gocron.Scheduler + logger *slog.Logger + cfg *config.Config +} + +func NewExchangeRateWorker( + fetcherService *FixerFetcher, logger *slog.Logger, cfg *config.Config, +) *ExchangeRateWorker { + return &ExchangeRateWorker{ + fetcherService: fetcherService, + scheduler: gocron.NewScheduler(time.UTC), + logger: logger, + cfg: cfg, + } +} + +func (w *ExchangeRateWorker) Start(ctx context.Context) { + _, err := w.scheduler.Every(6).Hours().Do(w.RunUpdate) + if err != nil { + return + } + + // Run immediately on startup + go w.RunUpdate() + + w.scheduler.StartAsync() +} + +func (w *ExchangeRateWorker) RunUpdate() { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if _, err := w.fetcherService.FetchLatestRates(ctx, w.cfg.BASE_CURRENCY); err != nil { + fmt.Println("Exchange rate update failed", "error", err) + } +} + +func (w *ExchangeRateWorker) Stop() { + w.scheduler.Stop() + w.logger.Info("Exchange rate worker stopped") +} diff --git a/internal/services/wallet/port.go b/internal/services/wallet/port.go index 66eabef..145f38f 100644 --- a/internal/services/wallet/port.go +++ b/internal/services/wallet/port.go @@ -28,4 +28,5 @@ type TransferStore interface { GetTransferByReference(ctx context.Context, reference string) (domain.Transfer, error) GetTransferByID(ctx context.Context, id int64) (domain.Transfer, error) UpdateTransferVerification(ctx context.Context, id int64, verified bool) error + UpdateTransferStatus(ctx context.Context, id int64, status string) error } diff --git a/internal/services/wallet/transfer.go b/internal/services/wallet/transfer.go index 7f71c4a..b4d5c67 100644 --- a/internal/services/wallet/transfer.go +++ b/internal/services/wallet/transfer.go @@ -116,6 +116,10 @@ func (s *Service) UpdateTransferVerification(ctx context.Context, id int64, veri return s.transferStore.UpdateTransferVerification(ctx, id, verified) } +func (s *Service) UpdateTransferStatus(ctx context.Context, id int64, status string) error { + return s.transferStore.UpdateTransferStatus(ctx, id, status) +} + func (s *Service) RefillWallet(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) { receiverWallet, err := s.GetWalletByID(ctx, transfer.ReceiverWalletID) if err != nil { @@ -192,7 +196,7 @@ func (s *Service) TransferToWallet(ctx context.Context, senderID int64, receiver // Log the transfer so that if there is a mistake, it can be reverted transfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ - SenderWalletID: senderID, + SenderWalletID: senderID, CashierID: cashierID, ReceiverWalletID: receiverID, Amount: amount, diff --git a/internal/web_server/app.go b/internal/web_server/app.go index 52cf531..d9ef3a2 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -10,6 +10,7 @@ import ( "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/currency" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" @@ -34,6 +35,7 @@ import ( ) type App struct { + currSvc *currency.Service fiber *fiber.App aleaVirtualGameService alea.AleaVirtualGameService veliVirtualGameService veli.VeliVirtualGameService @@ -64,6 +66,7 @@ type App struct { } func NewApp( + currSvc *currency.Service, port int, validator *customvalidator.CustomValidator, authSvc *authentication.Service, logger *slog.Logger, @@ -104,6 +107,7 @@ func NewApp( })) s := &App{ + currSvc: currSvc, fiber: app, port: port, authSvc: authSvc, diff --git a/internal/web_server/handlers/chapa.go b/internal/web_server/handlers/chapa.go index 16c8675..7d84ddf 100644 --- a/internal/web_server/handlers/chapa.go +++ b/internal/web_server/handlers/chapa.go @@ -13,6 +13,7 @@ import ( // @Tags Chapa // @Accept json // @Produce json +// @Security ApiKeyAuth // @Param request body domain.ChapaDepositRequestPayload true "Deposit request" // @Success 200 {object} domain.ChapaDepositResponse // @Failure 400 {object} domain.ErrorResponse @@ -65,38 +66,59 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error { // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/chapa/payments/webhook/verify [post] func (h *Handler) WebhookCallback(c *fiber.Ctx) error { - // Verify webhook signature first - // signature := c.Get("Chapa-Signature") - // if !verifySignature(signature, c.Body()) { - // return c.Status(fiber.StatusUnauthorized).JSON(ErrorResponse{ - // Error: "invalid signature", - // }) - // } - var payload struct { - TxRef string `json:"tx_ref"` - Amount float64 `json:"amount"` - Currency string `json:"currency"` - Status string `json:"status"` + chapaTransactionType := new(domain.ChapaTransactionType) + + if parseTypeErr := c.BodyParser(chapaTransactionType); parseTypeErr != nil { + return domain.UnProcessableEntityResponse(c) } - if err := c.BodyParser(&payload); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ - Error: err.Error(), + switch chapaTransactionType.Type { + case h.Cfg.CHAPA_TRANSFER_TYPE: + chapaTransferVerificationRequest := new(domain.ChapaWebHookTransfer) + + if err := c.BodyParser(chapaTransferVerificationRequest); err != nil { + return domain.UnProcessableEntityResponse(c) + } + + err := h.chapaSvc.HandleVerifyDepositWebhook(c.Context(), *chapaTransferVerificationRequest) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to verify Chapa depposit", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + StatusCode: 200, + Message: "Chapa deposit transaction verified successfully", + Data: chapaTransferVerificationRequest, + Success: true, }) - } + case h.Cfg.CHAPA_PAYMENT_TYPE: + chapaPaymentVerificationRequest := new(domain.ChapaWebHookPayment) + if err := c.BodyParser(chapaPaymentVerificationRequest); err != nil { + return domain.UnProcessableEntityResponse(c) + } - if err := h.chapaSvc.VerifyDeposit(c.Context(), payload.TxRef); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ - Error: err.Error(), + err := h.chapaSvc.HandleVerifyWithdrawWebhook(c.Context(), *chapaPaymentVerificationRequest) + if err != nil { + return domain.UnExpectedErrorResponse(c) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + StatusCode: 200, + Message: "Chapa withdrawal transaction verified successfully", + Data: chapaPaymentVerificationRequest, + Success: true, }) + } - return c.Status(fiber.StatusOK).JSON(domain.Response{ - StatusCode: 200, - Message: "payment verified successfully", - Data: payload.TxRef, - Success: true, + // Return a 400 Bad Request if the type does not match any known case + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid Chapa webhook type", + Error: "Unknown transaction type", }) } @@ -111,7 +133,7 @@ func (h *Handler) WebhookCallback(c *fiber.Ctx) error { // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/chapa/payments/manual/verify/{tx_ref} [get] -func (h *Handler) ManualVerifyPayment(c *fiber.Ctx) error { +func (h *Handler) ManualVerifyTransaction(c *fiber.Ctx) error { txRef := c.Params("tx_ref") if txRef == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ @@ -120,7 +142,7 @@ func (h *Handler) ManualVerifyPayment(c *fiber.Ctx) error { }) } - verification, err := h.chapaSvc.ManualVerifyPayment(c.Context(), txRef) + verification, err := h.chapaSvc.ManualVerifTransaction(c.Context(), txRef) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to verify Chapa transaction", @@ -142,9 +164,9 @@ func (h *Handler) ManualVerifyPayment(c *fiber.Ctx) error { // @Tags Chapa // @Accept json // @Produce json -// @Success 200 {array} domain.Bank +// @Success 200 {object} domain.Response // @Failure 500 {object} domain.ErrorResponse -// @Router /banks [get] +// @Router /api/v1/chapa/banks [get] func (h *Handler) GetSupportedBanks(c *fiber.Ctx) error { banks, err := h.chapaSvc.GetSupportedBanks(c.Context()) if err != nil { @@ -161,3 +183,44 @@ func (h *Handler) GetSupportedBanks(c *fiber.Ctx) error { Data: banks, }) } + +// InitiateWithdrawal initiates a withdrawal request via Chapa payment gateway +// @Summary Initiate a withdrawal +// @Description Initiates a withdrawal request to transfer funds to a bank account via Chapa +// @Tags Chapa +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param request body domain.ChapaWithdrawalRequest true "Withdrawal request details" +// @Success 201 {object} domain.Response "Chapa withdrawal process initiated successfully" +// @Failure 400 {object} domain.ErrorResponse "Invalid request body" +// @Failure 401 {object} domain.ErrorResponse "Unauthorized" +// @Failure 422 {object} domain.ErrorResponse "Unprocessable entity" +// @Failure 500 {object} domain.ErrorResponse "Internal server error" +// @Router /api/v1/chapa/payments/withdraw [post] +func (h *Handler) InitiateWithdrawal(c *fiber.Ctx) error { + userID, ok := c.Locals("user_id").(int64) + if !ok { + return domain.UnProcessableEntityResponse(c) + } + + var req domain.ChapaWithdrawalRequest + if err := c.BodyParser(&req); err != nil { + return domain.UnProcessableEntityResponse(c) + } + + withdrawal, err := h.chapaSvc.InitiateWithdrawal(c.Context(), userID, req) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to initiate Chapa withdrawal", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusCreated).JSON(domain.Response{ + Message: "Chapa withdrawal process initiated successfully", + StatusCode: 201, + Success: true, + Data: withdrawal, + }) +} diff --git a/internal/web_server/handlers/currency.go b/internal/web_server/handlers/currency.go new file mode 100644 index 0000000..dd9541f --- /dev/null +++ b/internal/web_server/handlers/currency.go @@ -0,0 +1,57 @@ +package handlers + +import ( + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/gofiber/fiber/v2" +) + +// @Summary Get supported currencies +// @Description Returns list of supported currencies +// @Tags Multi-Currency +// @Produce json +// @Success 200 {object} domain.Response{data=[]domain.Currency} +// @Router /api/v1/currencies [get] +func (h *Handler) GetSupportedCurrencies(c *fiber.Ctx) error { + currencies, err := h.currSvc.GetSupportedCurrencies(c.Context()) + if err != nil { + return domain.UnExpectedErrorResponse(c) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Success: true, + Message: "Supported currencies retrieved successfully", + Data: currencies, + StatusCode: fiber.StatusOK, + }) +} + +// @Summary Convert currency +// @Description Converts amount from one currency to another +// @Tags Multi-Currency +// @Produce json +// @Param from query string true "Source currency code (e.g., USD)" +// @Param to query string true "Target currency code (e.g., ETB)" +// @Param amount query number true "Amount to convert" +// @Success 200 {object} domain.Response{data=float64} +// @Failure 400 {object} domain.ErrorResponse +// @Router /api/v1/currencies/convert [get] +func (h *Handler) ConvertCurrency(c *fiber.Ctx) error { + from := domain.IntCurrency(c.Query("from")) + to := domain.IntCurrency(c.Query("to")) + amount := c.QueryFloat("amount", 0) + // if err != nil { + // return domain.BadRequestResponse(c) + // } + + converted, err := h.currSvc.Convert(c.Context(), amount, from, to) + if err != nil { + return domain.UnExpectedErrorResponse(c) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Success: true, + Message: "Currency converted successfully", + Data: converted, + StatusCode: fiber.StatusOK, + }) +} diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index 1bf2422..a5e40a0 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -9,14 +9,15 @@ import ( "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/currency" "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" referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/report" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" @@ -29,6 +30,7 @@ import ( ) type Handler struct { + currSvc *currency.Service logger *slog.Logger notificationSvc *notificationservice.Service userSvc *user.Service @@ -56,6 +58,7 @@ type Handler struct { } func New( + currSvc *currency.Service, logger *slog.Logger, notificationSvc *notificationservice.Service, validator *customvalidator.CustomValidator, @@ -82,6 +85,7 @@ func New( cfg *config.Config, ) *Handler { return &Handler{ + currSvc: currSvc, logger: logger, notificationSvc: notificationSvc, reportSvc: reportSvc, diff --git a/internal/web_server/handlers/report.go b/internal/web_server/handlers/report.go index cdd5153..c597763 100644 --- a/internal/web_server/handlers/report.go +++ b/internal/web_server/handlers/report.go @@ -24,7 +24,7 @@ import ( // @Param sport_id query string false "Sport ID filter" // @Param status query int false "Status filter (0=Pending, 1=Win, 2=Loss, 3=Half, 4=Void, 5=Error)" // @Security ApiKeyAuth -// @Success 200 {object} report.DashboardSummary +// @Success 200 {object} domain.DashboardSummary // @Failure 400 {object} domain.ErrorResponse // @Failure 401 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse diff --git a/internal/web_server/handlers/result_handler.go b/internal/web_server/handlers/result_handler.go index 05f752e..23bd466 100644 --- a/internal/web_server/handlers/result_handler.go +++ b/internal/web_server/handlers/result_handler.go @@ -9,7 +9,7 @@ import ( ) type ResultRes struct { - ResultData json.RawMessage `json:"result_data"` + ResultData json.RawMessage `json:"result_data" swaggerignore:"true"` Outcomes []domain.BetOutcome `json:"outcomes"` } diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index d40c932..784338a 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -20,6 +20,7 @@ import ( func (a *App) initAppRoutes() { h := handlers.New( + a.currSvc, a.logger, a.NotidicationStore, a.validator, @@ -199,10 +200,15 @@ func (a *App) initAppRoutes() { //Chapa Routes group.Post("/chapa/payments/webhook/verify", h.WebhookCallback) - group.Get("/chapa/payments/manual/verify/:tx_ref", h.ManualVerifyPayment) + group.Get("/chapa/payments/manual/verify/:tx_ref", h.ManualVerifyTransaction) group.Post("/chapa/payments/deposit", a.authMiddleware, h.InitiateDeposit) + group.Post("/chapa/payments/withdraw", a.authMiddleware, h.InitiateWithdrawal) group.Get("/chapa/banks", h.GetSupportedBanks) + // Currencies + group.Get("/currencies", h.GetSupportedCurrencies) + group.Get("/currencies/convert", h.ConvertCurrency) + //Report Routes group.Get("/reports/dashboard", h.GetDashboardReport)