From 1c7e076be56de20b6a30f38a5e6892e6914845f0 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Mon, 17 Nov 2025 18:45:18 +0300 Subject: [PATCH] direct depost and veli games fix --- cmd/main.go | 12 +- db/migrations/000001_fortune.up.sql | 31 +- .../000004_virtual_game_Session.up.sql | 6 +- db/query/direct_deposit.sql | 84 +- db/query/virtual_games.sql | 18 +- docker-compose.yml | 2 +- docs/docs.go | 1030 ++++++++++++++--- docs/swagger.json | 1030 ++++++++++++++--- docs/swagger.yaml | 684 ++++++++--- gen/db/direct_deposit.sql.go | 240 ++-- gen/db/models.go | 26 +- gen/db/virtual_games.sql.go | 16 +- internal/domain/common.go | 5 + internal/domain/direct_deposit.go | 126 +- internal/ports/wallet.go | 14 +- internal/repository/direct_deposit.go | 210 +++- internal/repository/virtual_game.go | 6 +- internal/services/direct_deposit/service.go | 305 +++++ internal/services/virtualGame/service.go | 2 +- internal/services/wallet/direct_deposit.go | 216 ---- internal/services/wallet/service.go | 6 +- internal/web_server/app.go | 4 + .../web_server/handlers/direct_deposit.go | 307 +++-- internal/web_server/handlers/handlers.go | 4 + .../handlers/virtual_games_hadlers.go | 14 +- internal/web_server/routes.go | 10 +- 26 files changed, 3271 insertions(+), 1137 deletions(-) create mode 100644 internal/services/direct_deposit/service.go delete mode 100644 internal/services/wallet/direct_deposit.go diff --git a/cmd/main.go b/cmd/main.go index f974c5a..051cff2 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -37,6 +37,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency" + directdeposit "github.com/SamuelTariku/FortuneBet-Backend/internal/services/direct_deposit" enetpulse "github.com/SamuelTariku/FortuneBet-Backend/internal/services/enet_pulse" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions" @@ -182,7 +183,7 @@ func main() { walletSvc := wallet.NewService( repository.NewWalletStore(store), repository.NewTransferStore(store), - repository.NewDirectDepositStore(store), + // repository.NewDirectDepositStore(store), notificationSvc, userSvc, domain.MongoDBLogger, @@ -326,6 +327,14 @@ func main() { // Start cron jobs for automated reporting + directdeposit := directdeposit.NewService( + *walletSvc, + repository.NewTransferStore(store), + repository.NewDirectDepositRepository(store), + notificationSvc, + userSvc, + ) + enetPulseSvc := enetpulse.New( *cfg, store, @@ -373,6 +382,7 @@ func main() { // Initialize and start HTTP server app := httpserver.NewApp( + directdeposit, enetPulseSvc, atlasVirtualGameService, veliVirtualGameService, diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 2529dc5..ef77807 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -26,9 +26,7 @@ CREATE TABLE IF NOT EXISTS virtual_game_providers ( provider_id VARCHAR(100) UNIQUE NOT NULL, -- providerId from Veli Games provider_name VARCHAR(255) NOT NULL, - -- providerName logo_dark TEXT, - -- logoForDark (URL) logo_light TEXT, -- logoForLight (URL) enabled BOOLEAN NOT NULL DEFAULT TRUE, @@ -618,20 +616,23 @@ CREATE TABLE flags ( ); CREATE TABLE direct_deposits ( id BIGSERIAL PRIMARY KEY, - customer_id BIGINT NOT NULL REFERENCES users (id), - wallet_id BIGINT NOT NULL REFERENCES wallets (id), - amount NUMERIC(15, 2) NOT NULL, - bank_reference TEXT NOT NULL, - sender_account TEXT NOT NULL, - status TEXT NOT NULL CHECK (status IN ('pending', 'completed', 'rejected')), - created_at TIMESTAMP NOT NULL DEFAULT NOW (), - verified_by BIGINT REFERENCES users (id), - verification_notes TEXT, - verified_at TIMESTAMP + customer_id BIGINT REFERENCES users(id), + wallet_id BIGINT REFERENCES wallets(id), + bank_name TEXT, + account_number TEXT, + account_holder TEXT, + amount NUMERIC(18,2), + reference_number TEXT, + transfer_screenshot TEXT, + status TEXT CHECK(status IN ('PENDING', 'APPROVED', 'REJECTED')), + created_at TIMESTAMPTZ, + approved_by BIGINT NULL REFERENCES users(id), + approved_at TIMESTAMPTZ NULL, + rejection_reason TEXT NULL ); -CREATE INDEX idx_direct_deposits_status ON direct_deposits (status); -CREATE INDEX idx_direct_deposits_customer ON direct_deposits (customer_id); -CREATE INDEX idx_direct_deposits_reference ON direct_deposits (bank_reference); +-- CREATE INDEX idx_direct_deposits_status ON direct_deposits (status); +-- CREATE INDEX idx_direct_deposits_customer ON direct_deposits (customer_id); +-- CREATE INDEX idx_direct_deposits_reference ON direct_deposits (bank_reference); CREATE TABLE IF NOT EXISTS raffles ( id SERIAL PRIMARY KEY, company_id INT NOT NULL, diff --git a/db/migrations/000004_virtual_game_Session.up.sql b/db/migrations/000004_virtual_game_Session.up.sql index b9fbd25..de60ae0 100644 --- a/db/migrations/000004_virtual_game_Session.up.sql +++ b/db/migrations/000004_virtual_game_Session.up.sql @@ -9,7 +9,7 @@ CREATE TABLE virtual_game_sessions ( CREATE TABLE virtual_game_transactions ( id BIGSERIAL PRIMARY KEY, - session_id BIGINT NOT NULL REFERENCES virtual_game_sessions(id), + -- session_id BIGINT NOT NULL REFERENCES virtual_game_sessions(id), user_id BIGINT NOT NULL REFERENCES users(id), company_id BIGINT, provider VARCHAR(100), @@ -26,7 +26,7 @@ CREATE TABLE virtual_game_transactions ( CREATE TABLE virtual_game_histories ( id BIGSERIAL PRIMARY KEY, - session_id VARCHAR(100), -- nullable + -- session_id VARCHAR(100), -- nullable user_id BIGINT NOT NULL, company_id BIGINT, provider VARCHAR(100), @@ -56,7 +56,7 @@ CREATE INDEX idx_virtual_game_game_id ON virtual_game_histories(game_id); CREATE INDEX idx_virtual_game_external_transaction_id ON virtual_game_histories(external_transaction_id); CREATE INDEX idx_virtual_game_sessions_user_id ON virtual_game_sessions(user_id); -CREATE INDEX idx_virtual_game_transactions_session_id ON virtual_game_transactions(session_id); +-- CREATE INDEX idx_virtual_game_transactions_session_id ON virtual_game_transactions(session_id); CREATE INDEX idx_virtual_game_transactions_user_id ON virtual_game_transactions(user_id); ALTER TABLE favorite_games diff --git a/db/query/direct_deposit.sql b/db/query/direct_deposit.sql index 96c52f5..f0b6e6a 100644 --- a/db/query/direct_deposit.sql +++ b/db/query/direct_deposit.sql @@ -1,30 +1,64 @@ -- name: CreateDirectDeposit :one INSERT INTO direct_deposits ( - customer_id, - wallet_id, - amount, - bank_reference, - sender_account, - status -) VALUES ( - $1, $2, $3, $4, $5, $6 -) RETURNING *; - --- name: GetDirectDeposit :one -SELECT * FROM direct_deposits WHERE id = $1; - --- name: UpdateDirectDeposit :one -UPDATE direct_deposits -SET - status = $2, - verified_by = $3, - verification_notes = $4, - verified_at = $5 -WHERE id = $1 + customer_id, wallet_id, bank_name, account_number, + account_holder, amount, reference_number, + transfer_screenshot, status +) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'PENDING') RETURNING *; --- name: GetDirectDepositsByStatus :many -SELECT * FROM direct_deposits WHERE status = $1 ORDER BY created_at DESC; +-- name: GetDirectDepositByID :one +SELECT * +FROM direct_deposits +WHERE id = $1; + +-- name: DeleteDirectDeposit :exec +DELETE FROM direct_deposits +WHERE id = $1; + +-- name: GetDirectDepositsByStatus :many +SELECT + id, + customer_id, + wallet_id, + bank_name, + account_number, + account_holder, + amount, + reference_number, + transfer_screenshot, + status, + created_at, + approved_by, + approved_at, + rejection_reason +FROM direct_deposits +WHERE status = $1 +ORDER BY created_at DESC +LIMIT $2 OFFSET $3; + +-- name: CountDirectDepositsByStatus :one +SELECT COUNT(*) +FROM direct_deposits +WHERE status = $1; + +-- name: ApproveDirectDeposit :exec +UPDATE direct_deposits +SET + status = 'APPROVED', + approved_by = $2, + approved_at = NOW() +WHERE + id = $1 + AND status = 'PENDING'; + +-- name: RejectDirectDeposit :exec +UPDATE direct_deposits +SET + status = 'REJECTED', + approved_by = $2, -- still track the admin who took final action + approved_at = NOW(), + rejection_reason = $3 +WHERE + id = $1 + AND status = 'PENDING'; --- name: GetCustomerDirectDeposits :many -SELECT * FROM direct_deposits WHERE customer_id = $1 ORDER BY created_at DESC; \ No newline at end of file diff --git a/db/query/virtual_games.sql b/db/query/virtual_games.sql index 08110e5..1d00622 100644 --- a/db/query/virtual_games.sql +++ b/db/query/virtual_games.sql @@ -97,7 +97,7 @@ WHERE session_token = $1; -- name: CreateVirtualGameTransaction :one INSERT INTO virtual_game_transactions ( - session_id, + -- session_id, user_id, company_id, provider, @@ -108,9 +108,9 @@ INSERT INTO virtual_game_transactions ( external_transaction_id, status ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, - session_id, + -- session_id, user_id, company_id, provider, @@ -124,7 +124,7 @@ RETURNING id, updated_at; -- name: CreateVirtualGameHistory :one INSERT INTO virtual_game_histories ( - session_id, + -- session_id, user_id, company_id, provider, @@ -148,11 +148,11 @@ VALUES ( $8, $9, $10, - $11, - $12 + $11 + -- $12 ) RETURNING id, - session_id, + -- session_id, user_id, company_id, provider, @@ -169,7 +169,7 @@ RETURNING id, -- name: GetVirtualGameTransactionByExternalID :one SELECT id, - session_id, + -- session_id, user_id, wallet_id, transaction_type, @@ -193,7 +193,7 @@ SELECT c.name AS company_name, COUNT(vgt.id) AS number_of_bets, COALESCE(SUM(vgt.amount), 0) AS total_transaction_sum FROM virtual_game_transactions vgt - JOIN virtual_game_sessions vgs ON vgt.session_id = vgs.id + -- JOIN virtual_game_sessions vgs ON vgt.session_id = vgs.id JOIN virtual_games vg ON vgs.game_id = vg.id JOIN companies c ON vgt.company_id = c.id WHERE vgt.transaction_type = 'BET' diff --git a/docker-compose.yml b/docker-compose.yml index 68f35c7..01d5f6f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,7 +26,7 @@ services: image: mongo:7.0.11 restart: always ports: - - "27020:27017" + - "27021:27017" environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: secret diff --git a/docs/docs.go b/docs/docs.go index 361e99b..dd1898f 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -3230,81 +3230,9 @@ const docTemplate = `{ } } }, - "/api/v1/direct_deposit": { - "post": { - "description": "Customer initiates a direct deposit from mobile banking", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Direct Deposits" - ], - "summary": "Initiate a direct deposit", - "parameters": [ - { - "description": "Deposit details", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.DirectDepositRequest" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/domain.Response" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - } - } - } - }, - "/api/v1/direct_deposit/pending": { + "/api/v1/direct-deposits": { "get": { - "description": "Get list of direct deposits needing verification", - "produces": [ - "application/json" - ], - "tags": [ - "Direct Deposits" - ], - "summary": "Get pending direct deposits", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/domain.Response" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - } - } - } - }, - "/api/v1/direct_deposit/verify": { - "post": { - "description": "Cashier verifies a direct deposit transaction", + "description": "Fetches direct deposits filtered by status with pagination", "consumes": [ "application/json" ], @@ -3312,25 +3240,50 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Direct Deposits" + "DirectDeposit" ], - "summary": "Verify a direct deposit", + "summary": "Get direct deposits by status", "parameters": [ { - "description": "Verification details", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.VerifyDirectDepositRequest" - } + "type": "string", + "description": "Deposit status (e.g., PENDING, APPROVED, REJECTED)", + "name": "status", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "pageSize", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/domain.Response" + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.DirectDeposit" + } + } + } + } + ] } }, "400": { @@ -3339,14 +3292,307 @@ const docTemplate = `{ "$ref": "#/definitions/domain.ErrorResponse" } }, - "401": { - "description": "Unauthorized", + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "post": { + "description": "Creates a direct deposit for a customer and notifies both the customer and admins", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DirectDeposit" + ], + "summary": "Create a new direct deposit", + "parameters": [ + { + "description": "Direct deposit details", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CreateDirectDeposit" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.DirectDeposit" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", "schema": { "$ref": "#/definitions/domain.ErrorResponse" } }, - "500": { - "description": "Internal Server Error", + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/direct-deposits/{depositID}": { + "get": { + "description": "Fetches a single direct deposit by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DirectDeposit" + ], + "summary": "Get a direct deposit by ID", + "parameters": [ + { + "type": "integer", + "description": "Deposit ID", + "name": "depositID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.DirectDeposit" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Deletes a direct deposit by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DirectDeposit" + ], + "summary": "Delete a direct deposit", + "parameters": [ + { + "type": "integer", + "description": "Deposit ID", + "name": "depositID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/direct-deposits/{depositID}/approve": { + "post": { + "description": "Approves a direct deposit by admin and credits customer wallet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DirectDeposit" + ], + "summary": "Approve a direct deposit", + "parameters": [ + { + "type": "integer", + "description": "Deposit ID", + "name": "depositID", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Admin ID performing the approval", + "name": "adminID", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/direct-deposits/{depositID}/reject": { + "post": { + "description": "Rejects a direct deposit by admin and notifies the customer", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DirectDeposit" + ], + "summary": "Reject a direct deposit", + "parameters": [ + { + "type": "integer", + "description": "Deposit ID", + "name": "depositID", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Admin ID performing the rejection", + "name": "adminID", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Reason for rejection", + "name": "reason", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "502": { + "description": "Bad Gateway", "schema": { "$ref": "#/definitions/domain.ErrorResponse" } @@ -4662,6 +4908,58 @@ const docTemplate = `{ } } }, + "/api/v1/market-settings": { + "get": { + "description": "Get all market settings that apply globally", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "market_settings" + ], + "summary": "Retrieve all global market settings", + "parameters": [ + { + "type": "integer", + "description": "Number of results to return (default 10)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Number of results to skip (default 0)", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.MarketSettings" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/odds": { "get": { "description": "Retrieve all odds from the database", @@ -8241,51 +8539,6 @@ const docTemplate = `{ } } }, - "/api/v1/win": { - "post": { - "description": "Processes win callbacks from either Veli or PopOK providers by auto-detecting the format", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Wins" - ], - "summary": "Handle win callback (Veli or PopOK)", - "responses": { - "200": { - "description": "Win processing result", - "schema": {} - }, - "400": { - "description": "Invalid request format", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - }, - "401": { - "description": "Authentication failed", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - }, - "409": { - "description": "Duplicate transaction", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - } - } - } - }, "/api/v1/{tenant_slug}/admin-login": { "post": { "description": "Login customer", @@ -8610,6 +8863,179 @@ const docTemplate = `{ } } }, + "/api/v1/{tenant_slug}/market-settings": { + "get": { + "description": "Get all market settings overridden for a specific tenant", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "market_settings" + ], + "summary": "Retrieve all market settings for a tenant", + "parameters": [ + { + "type": "integer", + "description": "Number of results to return (default 10)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Number of results to skip (default 0)", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.CompanyMarketSettings" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "post": { + "description": "Insert new market settings for a specific tenant/company", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "market_settings" + ], + "summary": "Insert company-specific market settings", + "parameters": [ + { + "description": "Market Settings", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CreateCompanyMarketSettings" + } + } + ], + "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" + } + } + } + }, + "delete": { + "description": "Remove all overridden market settings for a specific tenant", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "market_settings" + ], + "summary": "Delete all market settings for a tenant", + "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" + } + } + } + } + }, + "/api/v1/{tenant_slug}/market-settings/{id}": { + "delete": { + "description": "Remove a specific overridden market setting for a tenant", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "market_settings" + ], + "summary": "Delete a specific market setting for a tenant", + "parameters": [ + { + "type": "integer", + "description": "Market 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" + } + } + } + } + }, "/api/v1/{tenant_slug}/odds": { "get": { "description": "Retrieve all odds from the database", @@ -10166,6 +10592,51 @@ const docTemplate = `{ } } } + }, + "/win": { + "post": { + "description": "Processes win callbacks from either Veli or PopOK providers by auto-detecting the format", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Wins" + ], + "summary": "Handle win callback (Veli or PopOK)", + "responses": { + "200": { + "description": "Win processing result", + "schema": {} + }, + "400": { + "description": "Invalid request format", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "401": { + "description": "Authentication failed", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "409": { + "description": "Duplicate transaction", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } } }, "definitions": { @@ -10434,6 +10905,9 @@ const docTemplate = `{ "addedTime": { "$ref": "#/definitions/domain.ValidInt" }, + "avgBetAmount": { + "type": "integer" + }, "awayTeam": { "type": "string" }, @@ -10491,6 +10965,9 @@ const docTemplate = `{ "matchPeriod": { "$ref": "#/definitions/domain.ValidInt" }, + "numberOfBets": { + "type": "integer" + }, "score": { "$ref": "#/definitions/domain.ValidString" }, @@ -10512,8 +10989,14 @@ const docTemplate = `{ "timerStatus": { "$ref": "#/definitions/domain.ValidString" }, + "totalAmount": { + "type": "integer" + }, "totalOddOutcomes": { "type": "integer" + }, + "totalPotentialWinnings": { + "type": "integer" } } }, @@ -10689,6 +11172,13 @@ const docTemplate = `{ "type": "integer", "example": 1 }, + "company_name": { + "type": "string", + "example": "fortune" + }, + "deducted_stake": { + "type": "number" + }, "id": { "type": "integer", "example": 1 @@ -10721,10 +11211,34 @@ const docTemplate = `{ "type": "string", "example": "4-kilo Branch" }, + "number_of_unsettled": { + "type": "integer" + }, "profit_percentage": { "type": "number", "example": 0.1 }, + "stats_updated_at": { + "type": "string" + }, + "total_bets": { + "type": "integer" + }, + "total_cash_backs": { + "type": "number" + }, + "total_cash_out": { + "type": "number" + }, + "total_cashiers": { + "type": "integer" + }, + "total_stake": { + "type": "number" + }, + "total_unsettled_amount": { + "type": "number" + }, "wallet_id": { "type": "integer", "example": 1 @@ -11073,6 +11587,26 @@ const docTemplate = `{ } } }, + "domain.CompanyMarketSettings": { + "type": "object", + "properties": { + "companyID": { + "type": "integer" + }, + "isActive": { + "$ref": "#/definitions/domain.ValidBool" + }, + "marketID": { + "type": "integer" + }, + "marketName": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, "domain.CompanyRes": { "type": "object", "properties": { @@ -11218,6 +11752,23 @@ const docTemplate = `{ } } }, + "domain.CreateCompanyMarketSettings": { + "type": "object", + "properties": { + "companyID": { + "type": "integer" + }, + "isActive": { + "$ref": "#/definitions/domain.ValidBool" + }, + "marketID": { + "type": "integer" + }, + "marketName": { + "type": "string" + } + } + }, "domain.CreateCompanyReq": { "type": "object", "properties": { @@ -11241,6 +11792,35 @@ const docTemplate = `{ } } }, + "domain.CreateDirectDeposit": { + "type": "object", + "properties": { + "accountHolder": { + "type": "string" + }, + "accountNumber": { + "type": "string" + }, + "amount": { + "type": "number" + }, + "bankName": { + "type": "string" + }, + "customerID": { + "type": "integer" + }, + "referenceNumber": { + "type": "string" + }, + "transferScreenshot": { + "type": "string" + }, + "walletID": { + "type": "integer" + } + } + }, "domain.CreateSupportedOperationReq": { "type": "object", "properties": { @@ -11433,9 +12013,6 @@ const docTemplate = `{ "brandId": { "type": "string" }, - "country": { - "type": "string" - }, "deviceType": { "type": "string" }, @@ -11448,34 +12025,55 @@ const docTemplate = `{ "language": { "type": "string" }, - "playerId": { - "type": "string" - }, "providerId": { "type": "string" } } }, - "domain.DirectDepositRequest": { + "domain.DirectDeposit": { "type": "object", - "required": [ - "amount", - "bank_reference", - "customer_id", - "sender_account" - ], "properties": { + "accountHolder": { + "type": "string" + }, + "accountNumber": { + "type": "string" + }, "amount": { - "type": "integer" + "type": "number" }, - "bank_reference": { + "approvedAt": { "type": "string" }, - "customer_id": { + "approvedBy": { "type": "integer" }, - "sender_account": { + "bankName": { "type": "string" + }, + "createdAt": { + "type": "string" + }, + "customerID": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "referenceNumber": { + "type": "string" + }, + "rejectionReason": { + "type": "string" + }, + "status": { + "type": "string" + }, + "transferScreenshot": { + "type": "string" + }, + "walletID": { + "type": "integer" } } }, @@ -12030,6 +12628,9 @@ const docTemplate = `{ "added_time": { "type": "integer" }, + "average_bet_amount": { + "type": "number" + }, "away_team": { "type": "string" }, @@ -12093,6 +12694,9 @@ const docTemplate = `{ "match_period": { "type": "integer" }, + "number_of_bets": { + "type": "integer" + }, "score": { "type": "string" }, @@ -12114,9 +12718,15 @@ const docTemplate = `{ "timer_status": { "type": "string" }, + "total_amount": { + "type": "number" + }, "total_odd_outcomes": { "type": "integer" }, + "total_potential_winnings": { + "type": "number" + }, "updated_at": { "type": "string" }, @@ -12257,9 +12867,6 @@ const docTemplate = `{ "brandId": { "type": "string" }, - "cashierUrl": { - "type": "string" - }, "country": { "type": "string" }, @@ -12278,23 +12885,14 @@ const docTemplate = `{ "language": { "type": "string" }, - "lobbyUrl": { - "type": "string" - }, "playerId": { "type": "string" }, - "playerName": { - "type": "string" - }, "providerId": { "type": "string" }, "sessionId": { "type": "string" - }, - "userAgent": { - "type": "string" } } }, @@ -12473,6 +13071,9 @@ const docTemplate = `{ "type": "number", "example": 0.1 }, + "deducted_stake": { + "type": "number" + }, "id": { "type": "integer", "example": 1 @@ -12489,10 +13090,49 @@ const docTemplate = `{ "type": "string", "example": "CompanyName" }, + "number_of_unsettled": { + "type": "integer" + }, "slug": { "type": "string", "example": "slug" }, + "stats_updated_at": { + "type": "string" + }, + "total_admins": { + "type": "integer" + }, + "total_approvers": { + "type": "integer" + }, + "total_bets": { + "type": "integer" + }, + "total_branches": { + "type": "integer" + }, + "total_cash_backs": { + "type": "number" + }, + "total_cash_out": { + "type": "number" + }, + "total_cashiers": { + "type": "integer" + }, + "total_customers": { + "type": "integer" + }, + "total_managers": { + "type": "integer" + }, + "total_stake": { + "type": "number" + }, + "total_unsettled_amount": { + "type": "number" + }, "wallet_id": { "type": "integer", "example": 1 @@ -12717,6 +13357,23 @@ const docTemplate = `{ } } }, + "domain.MarketSettings": { + "type": "object", + "properties": { + "isActive": { + "type": "boolean" + }, + "marketID": { + "type": "integer" + }, + "marketName": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, "domain.OddMarketFilter": { "type": "object", "properties": { @@ -12756,7 +13413,7 @@ const docTemplate = `{ 5 ], "x-enum-comments": { - "OUTCOME_STATUS_ERROR": "Half Win and Half Given Back", + "OUTCOME_STATUS_ERROR": "Error (Unsettled Bet)", "OUTCOME_STATUS_HALF": "Half Win and Half Given Back", "OUTCOME_STATUS_VOID": "Give Back" }, @@ -12781,6 +13438,7 @@ const docTemplate = `{ "message": { "type": "string" }, + "metadata": {}, "pagination": { "$ref": "#/definitions/domain.Pagination" }, @@ -13035,6 +13693,7 @@ const docTemplate = `{ "message": { "type": "string" }, + "metadata": {}, "status_code": { "type": "integer" }, @@ -13834,24 +14493,6 @@ const docTemplate = `{ } } }, - "domain.VerifyDirectDepositRequest": { - "type": "object", - "required": [ - "deposit_id", - "is_verified" - ], - "properties": { - "deposit_id": { - "type": "integer" - }, - "is_verified": { - "type": "boolean" - }, - "notes": { - "type": "string" - } - } - }, "domain.VirtualGameProvider": { "type": "object", "properties": { @@ -14271,6 +14912,18 @@ const docTemplate = `{ "type": "string", "example": "Smith" }, + "number_of_deposits": { + "type": "integer" + }, + "number_of_transactions": { + "type": "integer" + }, + "number_of_transfers": { + "type": "integer" + }, + "number_of_withdraws": { + "type": "integer" + }, "phone_number": { "type": "string", "example": "0911111111" @@ -14304,6 +14957,21 @@ const docTemplate = `{ }, "static_updated_at": { "type": "string" + }, + "total_deposits_amount": { + "type": "number" + }, + "total_transactions": { + "type": "number" + }, + "total_transfers_amount": { + "type": "number" + }, + "total_withdraws_amount": { + "type": "number" + }, + "updated_at": { + "type": "string" } } }, diff --git a/docs/swagger.json b/docs/swagger.json index fa4aba2..49602a2 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -3222,81 +3222,9 @@ } } }, - "/api/v1/direct_deposit": { - "post": { - "description": "Customer initiates a direct deposit from mobile banking", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Direct Deposits" - ], - "summary": "Initiate a direct deposit", - "parameters": [ - { - "description": "Deposit details", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.DirectDepositRequest" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/domain.Response" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - } - } - } - }, - "/api/v1/direct_deposit/pending": { + "/api/v1/direct-deposits": { "get": { - "description": "Get list of direct deposits needing verification", - "produces": [ - "application/json" - ], - "tags": [ - "Direct Deposits" - ], - "summary": "Get pending direct deposits", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/domain.Response" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - } - } - } - }, - "/api/v1/direct_deposit/verify": { - "post": { - "description": "Cashier verifies a direct deposit transaction", + "description": "Fetches direct deposits filtered by status with pagination", "consumes": [ "application/json" ], @@ -3304,25 +3232,50 @@ "application/json" ], "tags": [ - "Direct Deposits" + "DirectDeposit" ], - "summary": "Verify a direct deposit", + "summary": "Get direct deposits by status", "parameters": [ { - "description": "Verification details", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.VerifyDirectDepositRequest" - } + "type": "string", + "description": "Deposit status (e.g., PENDING, APPROVED, REJECTED)", + "name": "status", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "pageSize", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/domain.Response" + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.DirectDeposit" + } + } + } + } + ] } }, "400": { @@ -3331,14 +3284,307 @@ "$ref": "#/definitions/domain.ErrorResponse" } }, - "401": { - "description": "Unauthorized", + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "post": { + "description": "Creates a direct deposit for a customer and notifies both the customer and admins", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DirectDeposit" + ], + "summary": "Create a new direct deposit", + "parameters": [ + { + "description": "Direct deposit details", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CreateDirectDeposit" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.DirectDeposit" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", "schema": { "$ref": "#/definitions/domain.ErrorResponse" } }, - "500": { - "description": "Internal Server Error", + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/direct-deposits/{depositID}": { + "get": { + "description": "Fetches a single direct deposit by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DirectDeposit" + ], + "summary": "Get a direct deposit by ID", + "parameters": [ + { + "type": "integer", + "description": "Deposit ID", + "name": "depositID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.DirectDeposit" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Deletes a direct deposit by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DirectDeposit" + ], + "summary": "Delete a direct deposit", + "parameters": [ + { + "type": "integer", + "description": "Deposit ID", + "name": "depositID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/direct-deposits/{depositID}/approve": { + "post": { + "description": "Approves a direct deposit by admin and credits customer wallet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DirectDeposit" + ], + "summary": "Approve a direct deposit", + "parameters": [ + { + "type": "integer", + "description": "Deposit ID", + "name": "depositID", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Admin ID performing the approval", + "name": "adminID", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/direct-deposits/{depositID}/reject": { + "post": { + "description": "Rejects a direct deposit by admin and notifies the customer", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DirectDeposit" + ], + "summary": "Reject a direct deposit", + "parameters": [ + { + "type": "integer", + "description": "Deposit ID", + "name": "depositID", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Admin ID performing the rejection", + "name": "adminID", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Reason for rejection", + "name": "reason", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "502": { + "description": "Bad Gateway", "schema": { "$ref": "#/definitions/domain.ErrorResponse" } @@ -4654,6 +4900,58 @@ } } }, + "/api/v1/market-settings": { + "get": { + "description": "Get all market settings that apply globally", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "market_settings" + ], + "summary": "Retrieve all global market settings", + "parameters": [ + { + "type": "integer", + "description": "Number of results to return (default 10)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Number of results to skip (default 0)", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.MarketSettings" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/odds": { "get": { "description": "Retrieve all odds from the database", @@ -8233,51 +8531,6 @@ } } }, - "/api/v1/win": { - "post": { - "description": "Processes win callbacks from either Veli or PopOK providers by auto-detecting the format", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Wins" - ], - "summary": "Handle win callback (Veli or PopOK)", - "responses": { - "200": { - "description": "Win processing result", - "schema": {} - }, - "400": { - "description": "Invalid request format", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - }, - "401": { - "description": "Authentication failed", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - }, - "409": { - "description": "Duplicate transaction", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - } - } - } - }, "/api/v1/{tenant_slug}/admin-login": { "post": { "description": "Login customer", @@ -8602,6 +8855,179 @@ } } }, + "/api/v1/{tenant_slug}/market-settings": { + "get": { + "description": "Get all market settings overridden for a specific tenant", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "market_settings" + ], + "summary": "Retrieve all market settings for a tenant", + "parameters": [ + { + "type": "integer", + "description": "Number of results to return (default 10)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Number of results to skip (default 0)", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.CompanyMarketSettings" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "post": { + "description": "Insert new market settings for a specific tenant/company", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "market_settings" + ], + "summary": "Insert company-specific market settings", + "parameters": [ + { + "description": "Market Settings", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CreateCompanyMarketSettings" + } + } + ], + "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" + } + } + } + }, + "delete": { + "description": "Remove all overridden market settings for a specific tenant", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "market_settings" + ], + "summary": "Delete all market settings for a tenant", + "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" + } + } + } + } + }, + "/api/v1/{tenant_slug}/market-settings/{id}": { + "delete": { + "description": "Remove a specific overridden market setting for a tenant", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "market_settings" + ], + "summary": "Delete a specific market setting for a tenant", + "parameters": [ + { + "type": "integer", + "description": "Market 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" + } + } + } + } + }, "/api/v1/{tenant_slug}/odds": { "get": { "description": "Retrieve all odds from the database", @@ -10158,6 +10584,51 @@ } } } + }, + "/win": { + "post": { + "description": "Processes win callbacks from either Veli or PopOK providers by auto-detecting the format", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Wins" + ], + "summary": "Handle win callback (Veli or PopOK)", + "responses": { + "200": { + "description": "Win processing result", + "schema": {} + }, + "400": { + "description": "Invalid request format", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "401": { + "description": "Authentication failed", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "409": { + "description": "Duplicate transaction", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } } }, "definitions": { @@ -10426,6 +10897,9 @@ "addedTime": { "$ref": "#/definitions/domain.ValidInt" }, + "avgBetAmount": { + "type": "integer" + }, "awayTeam": { "type": "string" }, @@ -10483,6 +10957,9 @@ "matchPeriod": { "$ref": "#/definitions/domain.ValidInt" }, + "numberOfBets": { + "type": "integer" + }, "score": { "$ref": "#/definitions/domain.ValidString" }, @@ -10504,8 +10981,14 @@ "timerStatus": { "$ref": "#/definitions/domain.ValidString" }, + "totalAmount": { + "type": "integer" + }, "totalOddOutcomes": { "type": "integer" + }, + "totalPotentialWinnings": { + "type": "integer" } } }, @@ -10681,6 +11164,13 @@ "type": "integer", "example": 1 }, + "company_name": { + "type": "string", + "example": "fortune" + }, + "deducted_stake": { + "type": "number" + }, "id": { "type": "integer", "example": 1 @@ -10713,10 +11203,34 @@ "type": "string", "example": "4-kilo Branch" }, + "number_of_unsettled": { + "type": "integer" + }, "profit_percentage": { "type": "number", "example": 0.1 }, + "stats_updated_at": { + "type": "string" + }, + "total_bets": { + "type": "integer" + }, + "total_cash_backs": { + "type": "number" + }, + "total_cash_out": { + "type": "number" + }, + "total_cashiers": { + "type": "integer" + }, + "total_stake": { + "type": "number" + }, + "total_unsettled_amount": { + "type": "number" + }, "wallet_id": { "type": "integer", "example": 1 @@ -11065,6 +11579,26 @@ } } }, + "domain.CompanyMarketSettings": { + "type": "object", + "properties": { + "companyID": { + "type": "integer" + }, + "isActive": { + "$ref": "#/definitions/domain.ValidBool" + }, + "marketID": { + "type": "integer" + }, + "marketName": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, "domain.CompanyRes": { "type": "object", "properties": { @@ -11210,6 +11744,23 @@ } } }, + "domain.CreateCompanyMarketSettings": { + "type": "object", + "properties": { + "companyID": { + "type": "integer" + }, + "isActive": { + "$ref": "#/definitions/domain.ValidBool" + }, + "marketID": { + "type": "integer" + }, + "marketName": { + "type": "string" + } + } + }, "domain.CreateCompanyReq": { "type": "object", "properties": { @@ -11233,6 +11784,35 @@ } } }, + "domain.CreateDirectDeposit": { + "type": "object", + "properties": { + "accountHolder": { + "type": "string" + }, + "accountNumber": { + "type": "string" + }, + "amount": { + "type": "number" + }, + "bankName": { + "type": "string" + }, + "customerID": { + "type": "integer" + }, + "referenceNumber": { + "type": "string" + }, + "transferScreenshot": { + "type": "string" + }, + "walletID": { + "type": "integer" + } + } + }, "domain.CreateSupportedOperationReq": { "type": "object", "properties": { @@ -11425,9 +12005,6 @@ "brandId": { "type": "string" }, - "country": { - "type": "string" - }, "deviceType": { "type": "string" }, @@ -11440,34 +12017,55 @@ "language": { "type": "string" }, - "playerId": { - "type": "string" - }, "providerId": { "type": "string" } } }, - "domain.DirectDepositRequest": { + "domain.DirectDeposit": { "type": "object", - "required": [ - "amount", - "bank_reference", - "customer_id", - "sender_account" - ], "properties": { + "accountHolder": { + "type": "string" + }, + "accountNumber": { + "type": "string" + }, "amount": { - "type": "integer" + "type": "number" }, - "bank_reference": { + "approvedAt": { "type": "string" }, - "customer_id": { + "approvedBy": { "type": "integer" }, - "sender_account": { + "bankName": { "type": "string" + }, + "createdAt": { + "type": "string" + }, + "customerID": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "referenceNumber": { + "type": "string" + }, + "rejectionReason": { + "type": "string" + }, + "status": { + "type": "string" + }, + "transferScreenshot": { + "type": "string" + }, + "walletID": { + "type": "integer" } } }, @@ -12022,6 +12620,9 @@ "added_time": { "type": "integer" }, + "average_bet_amount": { + "type": "number" + }, "away_team": { "type": "string" }, @@ -12085,6 +12686,9 @@ "match_period": { "type": "integer" }, + "number_of_bets": { + "type": "integer" + }, "score": { "type": "string" }, @@ -12106,9 +12710,15 @@ "timer_status": { "type": "string" }, + "total_amount": { + "type": "number" + }, "total_odd_outcomes": { "type": "integer" }, + "total_potential_winnings": { + "type": "number" + }, "updated_at": { "type": "string" }, @@ -12249,9 +12859,6 @@ "brandId": { "type": "string" }, - "cashierUrl": { - "type": "string" - }, "country": { "type": "string" }, @@ -12270,23 +12877,14 @@ "language": { "type": "string" }, - "lobbyUrl": { - "type": "string" - }, "playerId": { "type": "string" }, - "playerName": { - "type": "string" - }, "providerId": { "type": "string" }, "sessionId": { "type": "string" - }, - "userAgent": { - "type": "string" } } }, @@ -12465,6 +13063,9 @@ "type": "number", "example": 0.1 }, + "deducted_stake": { + "type": "number" + }, "id": { "type": "integer", "example": 1 @@ -12481,10 +13082,49 @@ "type": "string", "example": "CompanyName" }, + "number_of_unsettled": { + "type": "integer" + }, "slug": { "type": "string", "example": "slug" }, + "stats_updated_at": { + "type": "string" + }, + "total_admins": { + "type": "integer" + }, + "total_approvers": { + "type": "integer" + }, + "total_bets": { + "type": "integer" + }, + "total_branches": { + "type": "integer" + }, + "total_cash_backs": { + "type": "number" + }, + "total_cash_out": { + "type": "number" + }, + "total_cashiers": { + "type": "integer" + }, + "total_customers": { + "type": "integer" + }, + "total_managers": { + "type": "integer" + }, + "total_stake": { + "type": "number" + }, + "total_unsettled_amount": { + "type": "number" + }, "wallet_id": { "type": "integer", "example": 1 @@ -12709,6 +13349,23 @@ } } }, + "domain.MarketSettings": { + "type": "object", + "properties": { + "isActive": { + "type": "boolean" + }, + "marketID": { + "type": "integer" + }, + "marketName": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, "domain.OddMarketFilter": { "type": "object", "properties": { @@ -12748,7 +13405,7 @@ 5 ], "x-enum-comments": { - "OUTCOME_STATUS_ERROR": "Half Win and Half Given Back", + "OUTCOME_STATUS_ERROR": "Error (Unsettled Bet)", "OUTCOME_STATUS_HALF": "Half Win and Half Given Back", "OUTCOME_STATUS_VOID": "Give Back" }, @@ -12773,6 +13430,7 @@ "message": { "type": "string" }, + "metadata": {}, "pagination": { "$ref": "#/definitions/domain.Pagination" }, @@ -13027,6 +13685,7 @@ "message": { "type": "string" }, + "metadata": {}, "status_code": { "type": "integer" }, @@ -13826,24 +14485,6 @@ } } }, - "domain.VerifyDirectDepositRequest": { - "type": "object", - "required": [ - "deposit_id", - "is_verified" - ], - "properties": { - "deposit_id": { - "type": "integer" - }, - "is_verified": { - "type": "boolean" - }, - "notes": { - "type": "string" - } - } - }, "domain.VirtualGameProvider": { "type": "object", "properties": { @@ -14263,6 +14904,18 @@ "type": "string", "example": "Smith" }, + "number_of_deposits": { + "type": "integer" + }, + "number_of_transactions": { + "type": "integer" + }, + "number_of_transfers": { + "type": "integer" + }, + "number_of_withdraws": { + "type": "integer" + }, "phone_number": { "type": "string", "example": "0911111111" @@ -14296,6 +14949,21 @@ }, "static_updated_at": { "type": "string" + }, + "total_deposits_amount": { + "type": "number" + }, + "total_transactions": { + "type": "number" + }, + "total_transfers_amount": { + "type": "number" + }, + "total_withdraws_amount": { + "type": "number" + }, + "updated_at": { + "type": "string" } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6dddb84..ca72302 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -174,6 +174,8 @@ definitions: properties: addedTime: $ref: '#/definitions/domain.ValidInt' + avgBetAmount: + type: integer awayTeam: type: string awayTeamID: @@ -212,6 +214,8 @@ definitions: type: string matchPeriod: $ref: '#/definitions/domain.ValidInt' + numberOfBets: + type: integer score: $ref: '#/definitions/domain.ValidString' source: @@ -226,8 +230,12 @@ definitions: $ref: '#/definitions/domain.EventStatus' timerStatus: $ref: '#/definitions/domain.ValidString' + totalAmount: + type: integer totalOddOutcomes: type: integer + totalPotentialWinnings: + type: integer type: object domain.BaseLeague: properties: @@ -349,6 +357,11 @@ definitions: company_id: example: 1 type: integer + company_name: + example: fortune + type: string + deducted_stake: + type: number id: example: 1 type: integer @@ -373,9 +386,25 @@ definitions: name: example: 4-kilo Branch type: string + number_of_unsettled: + type: integer profit_percentage: example: 0.1 type: number + stats_updated_at: + type: string + total_bets: + type: integer + total_cash_backs: + type: number + total_cash_out: + type: number + total_cashiers: + type: integer + total_stake: + type: number + total_unsettled_amount: + type: number wallet_id: example: 1 type: integer @@ -608,6 +637,19 @@ definitions: - customerEmail - customerPhone type: object + domain.CompanyMarketSettings: + properties: + companyID: + type: integer + isActive: + $ref: '#/definitions/domain.ValidBool' + marketID: + type: integer + marketName: + type: string + updatedAt: + type: string + type: object domain.CompanyRes: properties: admin_id: @@ -712,6 +754,17 @@ definitions: - name - operations type: object + domain.CreateCompanyMarketSettings: + properties: + companyID: + type: integer + isActive: + $ref: '#/definitions/domain.ValidBool' + marketID: + type: integer + marketName: + type: string + type: object domain.CreateCompanyReq: properties: admin_id: @@ -728,6 +781,25 @@ definitions: slug: type: string type: object + domain.CreateDirectDeposit: + properties: + accountHolder: + type: string + accountNumber: + type: string + amount: + type: number + bankName: + type: string + customerID: + type: integer + referenceNumber: + type: string + transferScreenshot: + type: string + walletID: + type: integer + type: object domain.CreateSupportedOperationReq: properties: description: @@ -857,8 +929,6 @@ definitions: properties: brandId: type: string - country: - type: string deviceType: type: string gameId: @@ -867,26 +937,39 @@ definitions: type: string language: type: string - playerId: - type: string providerId: type: string type: object - domain.DirectDepositRequest: + domain.DirectDeposit: properties: + accountHolder: + type: string + accountNumber: + type: string amount: - type: integer - bank_reference: + type: number + approvedAt: type: string - customer_id: + approvedBy: type: integer - sender_account: + bankName: type: string - required: - - amount - - bank_reference - - customer_id - - sender_account + createdAt: + type: string + customerID: + type: integer + id: + type: integer + referenceNumber: + type: string + rejectionReason: + type: string + status: + type: string + transferScreenshot: + type: string + walletID: + type: integer type: object domain.EnetpulseFixture: properties: @@ -1272,6 +1355,8 @@ definitions: properties: added_time: type: integer + average_bet_amount: + type: number away_team: type: string away_team_id: @@ -1314,6 +1399,8 @@ definitions: type: string match_period: type: integer + number_of_bets: + type: integer score: type: string source: @@ -1328,8 +1415,12 @@ definitions: $ref: '#/definitions/domain.EventStatus' timer_status: type: string + total_amount: + type: number total_odd_outcomes: type: integer + total_potential_winnings: + type: number updated_at: type: string winning_upper_limit: @@ -1422,8 +1513,6 @@ definitions: properties: brandId: type: string - cashierUrl: - type: string country: type: string currency: @@ -1436,18 +1525,12 @@ definitions: type: string language: type: string - lobbyUrl: - type: string playerId: type: string - playerName: - type: string providerId: type: string sessionId: type: string - userAgent: - type: string type: object domain.GameStartResponse: properties: @@ -1569,6 +1652,8 @@ definitions: deducted_percentage: example: 0.1 type: number + deducted_stake: + type: number id: example: 1 type: integer @@ -1581,9 +1666,35 @@ definitions: name: example: CompanyName type: string + number_of_unsettled: + type: integer slug: example: slug type: string + stats_updated_at: + type: string + total_admins: + type: integer + total_approvers: + type: integer + total_bets: + type: integer + total_branches: + type: integer + total_cash_backs: + type: number + total_cash_out: + type: number + total_cashiers: + type: integer + total_customers: + type: integer + total_managers: + type: integer + total_stake: + type: number + total_unsettled_amount: + type: number wallet_id: example: 1 type: integer @@ -1730,6 +1841,17 @@ definitions: pagination: $ref: '#/definitions/domain.Pagination' type: object + domain.MarketSettings: + properties: + isActive: + type: boolean + marketID: + type: integer + marketName: + type: string + updatedAt: + type: string + type: object domain.OddMarketFilter: properties: limit: @@ -1758,7 +1880,7 @@ definitions: - 5 type: integer x-enum-comments: - OUTCOME_STATUS_ERROR: Half Win and Half Given Back + OUTCOME_STATUS_ERROR: Error (Unsettled Bet) OUTCOME_STATUS_HALF: Half Win and Half Given Back OUTCOME_STATUS_VOID: Give Back x-enum-varnames: @@ -1776,6 +1898,7 @@ definitions: type: array message: type: string + metadata: {} pagination: $ref: '#/definitions/domain.Pagination' status_code: @@ -1946,6 +2069,7 @@ definitions: data: {} message: type: string + metadata: {} status_code: type: integer success: @@ -2497,18 +2621,6 @@ definitions: value: type: string type: object - domain.VerifyDirectDepositRequest: - properties: - deposit_id: - type: integer - is_verified: - type: boolean - notes: - type: string - required: - - deposit_id - - is_verified - type: object domain.VirtualGameProvider: properties: created_at: @@ -2795,6 +2907,14 @@ definitions: last_name: example: Smith type: string + number_of_deposits: + type: integer + number_of_transactions: + type: integer + number_of_transfers: + type: integer + number_of_withdraws: + type: integer phone_number: example: "0911111111" type: string @@ -2820,6 +2940,16 @@ definitions: type: boolean static_updated_at: type: string + total_deposits_amount: + type: number + total_transactions: + type: number + total_transfers_amount: + type: number + total_withdraws_amount: + type: number + updated_at: + type: string type: object handlers.CustomersRes: properties: @@ -3568,6 +3698,120 @@ paths: summary: Set the league to active tags: - leagues + /api/v1/{tenant_slug}/market-settings: + delete: + consumes: + - application/json + description: Remove all overridden market settings for a specific tenant + 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: Delete all market settings for a tenant + tags: + - market_settings + get: + consumes: + - application/json + description: Get all market settings overridden for a specific tenant + parameters: + - description: Number of results to return (default 10) + in: query + name: limit + type: integer + - description: Number of results to skip (default 0) + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.CompanyMarketSettings' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Retrieve all market settings for a tenant + tags: + - market_settings + post: + consumes: + - application/json + description: Insert new market settings for a specific tenant/company + parameters: + - description: Market Settings + in: body + name: body + required: true + schema: + $ref: '#/definitions/domain.CreateCompanyMarketSettings' + 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: Insert company-specific market settings + tags: + - market_settings + /api/v1/{tenant_slug}/market-settings/{id}: + delete: + consumes: + - application/json + description: Remove a specific overridden market setting for a tenant + parameters: + - description: Market 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: Delete a specific market setting for a tenant + tags: + - market_settings /api/v1/{tenant_slug}/odds: get: consumes: @@ -6385,87 +6629,235 @@ paths: summary: Get all customer wallets tags: - wallet - /api/v1/direct_deposit: - post: - consumes: - - application/json - description: Customer initiates a direct deposit from mobile banking - parameters: - - description: Deposit details - in: body - name: request - required: true - schema: - $ref: '#/definitions/domain.DirectDepositRequest' - produces: - - application/json - responses: - "201": - description: Created - schema: - $ref: '#/definitions/domain.Response' - "400": - description: Bad Request - schema: - $ref: '#/definitions/domain.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/domain.ErrorResponse' - summary: Initiate a direct deposit - tags: - - Direct Deposits - /api/v1/direct_deposit/pending: + /api/v1/direct-deposits: get: - description: Get list of direct deposits needing verification - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/domain.Response' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/domain.ErrorResponse' - summary: Get pending direct deposits - tags: - - Direct Deposits - /api/v1/direct_deposit/verify: - post: consumes: - application/json - description: Cashier verifies a direct deposit transaction + description: Fetches direct deposits filtered by status with pagination parameters: - - description: Verification details - in: body - name: request + - description: Deposit status (e.g., PENDING, APPROVED, REJECTED) + in: query + name: status required: true - schema: - $ref: '#/definitions/domain.VerifyDirectDepositRequest' + type: string + - description: Page number + in: query + name: page + type: integer + - description: Page size + in: query + name: pageSize + type: integer produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/domain.Response' + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + items: + $ref: '#/definitions/domain.DirectDeposit' + type: array + type: object "400": description: Bad Request schema: $ref: '#/definitions/domain.ErrorResponse' - "401": - description: Unauthorized + "502": + description: Bad Gateway schema: $ref: '#/definitions/domain.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/domain.ErrorResponse' - summary: Verify a direct deposit + summary: Get direct deposits by status tags: - - Direct Deposits + - DirectDeposit + post: + consumes: + - application/json + description: Creates a direct deposit for a customer and notifies both the customer + and admins + parameters: + - description: Direct deposit details + in: body + name: body + required: true + schema: + $ref: '#/definitions/domain.CreateDirectDeposit' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + $ref: '#/definitions/domain.DirectDeposit' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Create a new direct deposit + tags: + - DirectDeposit + /api/v1/direct-deposits/{depositID}: + delete: + consumes: + - application/json + description: Deletes a direct deposit by its ID + parameters: + - description: Deposit ID + in: path + name: depositID + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + type: string + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Delete a direct deposit + tags: + - DirectDeposit + get: + consumes: + - application/json + description: Fetches a single direct deposit by its ID + parameters: + - description: Deposit ID + in: path + name: depositID + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + $ref: '#/definitions/domain.DirectDeposit' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get a direct deposit by ID + tags: + - DirectDeposit + /api/v1/direct-deposits/{depositID}/approve: + post: + consumes: + - application/json + description: Approves a direct deposit by admin and credits customer wallet + parameters: + - description: Deposit ID + in: path + name: depositID + required: true + type: integer + - description: Admin ID performing the approval + in: query + name: adminID + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + type: string + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Approve a direct deposit + tags: + - DirectDeposit + /api/v1/direct-deposits/{depositID}/reject: + post: + consumes: + - application/json + description: Rejects a direct deposit by admin and notifies the customer + parameters: + - description: Deposit ID + in: path + name: depositID + required: true + type: integer + - description: Admin ID performing the rejection + in: query + name: adminID + required: true + type: integer + - description: Reason for rejection + in: query + name: reason + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + type: string + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Reject a direct deposit + tags: + - DirectDeposit /api/v1/enetpulse/betting-offers: get: consumes: @@ -7303,6 +7695,40 @@ paths: summary: Update Managers tags: - manager + /api/v1/market-settings: + get: + consumes: + - application/json + description: Get all market settings that apply globally + parameters: + - description: Number of results to return (default 10) + in: query + name: limit + type: integer + - description: Number of results to skip (default 0) + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.MarketSettings' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Retrieve all global market settings + tags: + - market_settings /api/v1/odds: get: consumes: @@ -9643,37 +10069,6 @@ paths: summary: Process Alea Play game callback tags: - Alea Virtual Games - /api/v1/win: - post: - consumes: - - application/json - description: Processes win callbacks from either Veli or PopOK providers by - auto-detecting the format - produces: - - application/json - responses: - "200": - description: Win processing result - schema: {} - "400": - description: Invalid request format - schema: - $ref: '#/definitions/domain.ErrorResponse' - "401": - description: Authentication failed - schema: - $ref: '#/definitions/domain.ErrorResponse' - "409": - description: Duplicate transaction - schema: - $ref: '#/definitions/domain.ErrorResponse' - "500": - description: Internal server error - schema: - $ref: '#/definitions/domain.ErrorResponse' - summary: Handle win callback (Veli or PopOK) - tags: - - Wins /betwin: post: consumes: @@ -9942,6 +10337,37 @@ paths: summary: Launch a PopOK virtual game tags: - Virtual Games - PopOK + /win: + post: + consumes: + - application/json + description: Processes win callbacks from either Veli or PopOK providers by + auto-detecting the format + produces: + - application/json + responses: + "200": + description: Win processing result + schema: {} + "400": + description: Invalid request format + schema: + $ref: '#/definitions/domain.ErrorResponse' + "401": + description: Authentication failed + schema: + $ref: '#/definitions/domain.ErrorResponse' + "409": + description: Duplicate transaction + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Handle win callback (Veli or PopOK) + tags: + - Wins securityDefinitions: Bearer: in: header diff --git a/gen/db/direct_deposit.sql.go b/gen/db/direct_deposit.sql.go index be02750..aab1d6b 100644 --- a/gen/db/direct_deposit.sql.go +++ b/gen/db/direct_deposit.sql.go @@ -11,119 +11,159 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const ApproveDirectDeposit = `-- name: ApproveDirectDeposit :exec +UPDATE direct_deposits +SET + status = 'APPROVED', + approved_by = $2, + approved_at = NOW() +WHERE + id = $1 + AND status = 'PENDING' +` + +type ApproveDirectDepositParams struct { + ID int64 `json:"id"` + ApprovedBy pgtype.Int8 `json:"approved_by"` +} + +func (q *Queries) ApproveDirectDeposit(ctx context.Context, arg ApproveDirectDepositParams) error { + _, err := q.db.Exec(ctx, ApproveDirectDeposit, arg.ID, arg.ApprovedBy) + return err +} + +const CountDirectDepositsByStatus = `-- name: CountDirectDepositsByStatus :one +SELECT COUNT(*) +FROM direct_deposits +WHERE status = $1 +` + +func (q *Queries) CountDirectDepositsByStatus(ctx context.Context, status pgtype.Text) (int64, error) { + row := q.db.QueryRow(ctx, CountDirectDepositsByStatus, status) + var count int64 + err := row.Scan(&count) + return count, err +} + const CreateDirectDeposit = `-- name: CreateDirectDeposit :one INSERT INTO direct_deposits ( - customer_id, - wallet_id, - amount, - bank_reference, - sender_account, - status -) VALUES ( - $1, $2, $3, $4, $5, $6 -) RETURNING id, customer_id, wallet_id, amount, bank_reference, sender_account, status, created_at, verified_by, verification_notes, verified_at + customer_id, wallet_id, bank_name, account_number, + account_holder, amount, reference_number, + transfer_screenshot, status +) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'PENDING') +RETURNING id, customer_id, wallet_id, bank_name, account_number, account_holder, amount, reference_number, transfer_screenshot, status, created_at, approved_by, approved_at, rejection_reason ` type CreateDirectDepositParams struct { - CustomerID int64 `json:"customer_id"` - WalletID int64 `json:"wallet_id"` - Amount pgtype.Numeric `json:"amount"` - BankReference string `json:"bank_reference"` - SenderAccount string `json:"sender_account"` - Status string `json:"status"` + CustomerID pgtype.Int8 `json:"customer_id"` + WalletID pgtype.Int8 `json:"wallet_id"` + BankName pgtype.Text `json:"bank_name"` + AccountNumber pgtype.Text `json:"account_number"` + AccountHolder pgtype.Text `json:"account_holder"` + Amount pgtype.Numeric `json:"amount"` + ReferenceNumber pgtype.Text `json:"reference_number"` + TransferScreenshot pgtype.Text `json:"transfer_screenshot"` } func (q *Queries) CreateDirectDeposit(ctx context.Context, arg CreateDirectDepositParams) (DirectDeposit, error) { row := q.db.QueryRow(ctx, CreateDirectDeposit, arg.CustomerID, arg.WalletID, + arg.BankName, + arg.AccountNumber, + arg.AccountHolder, arg.Amount, - arg.BankReference, - arg.SenderAccount, - arg.Status, + arg.ReferenceNumber, + arg.TransferScreenshot, ) var i DirectDeposit err := row.Scan( &i.ID, &i.CustomerID, &i.WalletID, + &i.BankName, + &i.AccountNumber, + &i.AccountHolder, &i.Amount, - &i.BankReference, - &i.SenderAccount, + &i.ReferenceNumber, + &i.TransferScreenshot, &i.Status, &i.CreatedAt, - &i.VerifiedBy, - &i.VerificationNotes, - &i.VerifiedAt, + &i.ApprovedBy, + &i.ApprovedAt, + &i.RejectionReason, ) return i, err } -const GetCustomerDirectDeposits = `-- name: GetCustomerDirectDeposits :many -SELECT id, customer_id, wallet_id, amount, bank_reference, sender_account, status, created_at, verified_by, verification_notes, verified_at FROM direct_deposits WHERE customer_id = $1 ORDER BY created_at DESC +const DeleteDirectDeposit = `-- name: DeleteDirectDeposit :exec +DELETE FROM direct_deposits +WHERE id = $1 ` -func (q *Queries) GetCustomerDirectDeposits(ctx context.Context, customerID int64) ([]DirectDeposit, error) { - rows, err := q.db.Query(ctx, GetCustomerDirectDeposits, customerID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []DirectDeposit - for rows.Next() { - var i DirectDeposit - if err := rows.Scan( - &i.ID, - &i.CustomerID, - &i.WalletID, - &i.Amount, - &i.BankReference, - &i.SenderAccount, - &i.Status, - &i.CreatedAt, - &i.VerifiedBy, - &i.VerificationNotes, - &i.VerifiedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil +func (q *Queries) DeleteDirectDeposit(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeleteDirectDeposit, id) + return err } -const GetDirectDeposit = `-- name: GetDirectDeposit :one -SELECT id, customer_id, wallet_id, amount, bank_reference, sender_account, status, created_at, verified_by, verification_notes, verified_at FROM direct_deposits WHERE id = $1 +const GetDirectDepositByID = `-- name: GetDirectDepositByID :one +SELECT id, customer_id, wallet_id, bank_name, account_number, account_holder, amount, reference_number, transfer_screenshot, status, created_at, approved_by, approved_at, rejection_reason +FROM direct_deposits +WHERE id = $1 ` -func (q *Queries) GetDirectDeposit(ctx context.Context, id int64) (DirectDeposit, error) { - row := q.db.QueryRow(ctx, GetDirectDeposit, id) +func (q *Queries) GetDirectDepositByID(ctx context.Context, id int64) (DirectDeposit, error) { + row := q.db.QueryRow(ctx, GetDirectDepositByID, id) var i DirectDeposit err := row.Scan( &i.ID, &i.CustomerID, &i.WalletID, + &i.BankName, + &i.AccountNumber, + &i.AccountHolder, &i.Amount, - &i.BankReference, - &i.SenderAccount, + &i.ReferenceNumber, + &i.TransferScreenshot, &i.Status, &i.CreatedAt, - &i.VerifiedBy, - &i.VerificationNotes, - &i.VerifiedAt, + &i.ApprovedBy, + &i.ApprovedAt, + &i.RejectionReason, ) return i, err } const GetDirectDepositsByStatus = `-- name: GetDirectDepositsByStatus :many -SELECT id, customer_id, wallet_id, amount, bank_reference, sender_account, status, created_at, verified_by, verification_notes, verified_at FROM direct_deposits WHERE status = $1 ORDER BY created_at DESC +SELECT + id, + customer_id, + wallet_id, + bank_name, + account_number, + account_holder, + amount, + reference_number, + transfer_screenshot, + status, + created_at, + approved_by, + approved_at, + rejection_reason +FROM direct_deposits +WHERE status = $1 +ORDER BY created_at DESC +LIMIT $2 OFFSET $3 ` -func (q *Queries) GetDirectDepositsByStatus(ctx context.Context, status string) ([]DirectDeposit, error) { - rows, err := q.db.Query(ctx, GetDirectDepositsByStatus, status) +type GetDirectDepositsByStatusParams struct { + Status pgtype.Text `json:"status"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) GetDirectDepositsByStatus(ctx context.Context, arg GetDirectDepositsByStatusParams) ([]DirectDeposit, error) { + rows, err := q.db.Query(ctx, GetDirectDepositsByStatus, arg.Status, arg.Limit, arg.Offset) if err != nil { return nil, err } @@ -135,14 +175,17 @@ func (q *Queries) GetDirectDepositsByStatus(ctx context.Context, status string) &i.ID, &i.CustomerID, &i.WalletID, + &i.BankName, + &i.AccountNumber, + &i.AccountHolder, &i.Amount, - &i.BankReference, - &i.SenderAccount, + &i.ReferenceNumber, + &i.TransferScreenshot, &i.Status, &i.CreatedAt, - &i.VerifiedBy, - &i.VerificationNotes, - &i.VerifiedAt, + &i.ApprovedBy, + &i.ApprovedAt, + &i.RejectionReason, ); err != nil { return nil, err } @@ -154,46 +197,25 @@ func (q *Queries) GetDirectDepositsByStatus(ctx context.Context, status string) return items, nil } -const UpdateDirectDeposit = `-- name: UpdateDirectDeposit :one +const RejectDirectDeposit = `-- name: RejectDirectDeposit :exec UPDATE direct_deposits -SET - status = $2, - verified_by = $3, - verification_notes = $4, - verified_at = $5 -WHERE id = $1 -RETURNING id, customer_id, wallet_id, amount, bank_reference, sender_account, status, created_at, verified_by, verification_notes, verified_at +SET + status = 'REJECTED', + approved_by = $2, -- still track the admin who took final action + approved_at = NOW(), + rejection_reason = $3 +WHERE + id = $1 + AND status = 'PENDING' ` -type UpdateDirectDepositParams struct { - ID int64 `json:"id"` - Status string `json:"status"` - VerifiedBy pgtype.Int8 `json:"verified_by"` - VerificationNotes pgtype.Text `json:"verification_notes"` - VerifiedAt pgtype.Timestamp `json:"verified_at"` +type RejectDirectDepositParams struct { + ID int64 `json:"id"` + ApprovedBy pgtype.Int8 `json:"approved_by"` + RejectionReason pgtype.Text `json:"rejection_reason"` } -func (q *Queries) UpdateDirectDeposit(ctx context.Context, arg UpdateDirectDepositParams) (DirectDeposit, error) { - row := q.db.QueryRow(ctx, UpdateDirectDeposit, - arg.ID, - arg.Status, - arg.VerifiedBy, - arg.VerificationNotes, - arg.VerifiedAt, - ) - var i DirectDeposit - err := row.Scan( - &i.ID, - &i.CustomerID, - &i.WalletID, - &i.Amount, - &i.BankReference, - &i.SenderAccount, - &i.Status, - &i.CreatedAt, - &i.VerifiedBy, - &i.VerificationNotes, - &i.VerifiedAt, - ) - return i, err +func (q *Queries) RejectDirectDeposit(ctx context.Context, arg RejectDirectDepositParams) error { + _, err := q.db.Exec(ctx, RejectDirectDeposit, arg.ID, arg.ApprovedBy, arg.RejectionReason) + return err } diff --git a/gen/db/models.go b/gen/db/models.go index e8872d6..1812828 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -312,17 +312,20 @@ type CustomerWalletDetail struct { } type DirectDeposit struct { - ID int64 `json:"id"` - CustomerID int64 `json:"customer_id"` - WalletID int64 `json:"wallet_id"` - Amount pgtype.Numeric `json:"amount"` - BankReference string `json:"bank_reference"` - SenderAccount string `json:"sender_account"` - Status string `json:"status"` - CreatedAt pgtype.Timestamp `json:"created_at"` - VerifiedBy pgtype.Int8 `json:"verified_by"` - VerificationNotes pgtype.Text `json:"verification_notes"` - VerifiedAt pgtype.Timestamp `json:"verified_at"` + ID int64 `json:"id"` + CustomerID pgtype.Int8 `json:"customer_id"` + WalletID pgtype.Int8 `json:"wallet_id"` + BankName pgtype.Text `json:"bank_name"` + AccountNumber pgtype.Text `json:"account_number"` + AccountHolder pgtype.Text `json:"account_holder"` + Amount pgtype.Numeric `json:"amount"` + ReferenceNumber pgtype.Text `json:"reference_number"` + TransferScreenshot pgtype.Text `json:"transfer_screenshot"` + Status pgtype.Text `json:"status"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + ApprovedBy pgtype.Int8 `json:"approved_by"` + ApprovedAt pgtype.Timestamptz `json:"approved_at"` + RejectionReason pgtype.Text `json:"rejection_reason"` } type EnetpulseFixture struct { @@ -1223,7 +1226,6 @@ type VirtualGameSession struct { type VirtualGameTransaction struct { ID int64 `json:"id"` - SessionID int64 `json:"session_id"` UserID int64 `json:"user_id"` CompanyID pgtype.Int8 `json:"company_id"` Provider pgtype.Text `json:"provider"` diff --git a/gen/db/virtual_games.sql.go b/gen/db/virtual_games.sql.go index 1e39f92..1db8306 100644 --- a/gen/db/virtual_games.sql.go +++ b/gen/db/virtual_games.sql.go @@ -445,7 +445,7 @@ func (q *Queries) CreateVirtualGameSession(ctx context.Context, arg CreateVirtua const CreateVirtualGameTransaction = `-- name: CreateVirtualGameTransaction :one INSERT INTO virtual_game_transactions ( - session_id, + -- session_id, user_id, company_id, provider, @@ -456,9 +456,9 @@ INSERT INTO virtual_game_transactions ( external_transaction_id, status ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, - session_id, + -- session_id, user_id, company_id, provider, @@ -473,7 +473,6 @@ RETURNING id, ` type CreateVirtualGameTransactionParams struct { - SessionID int64 `json:"session_id"` UserID int64 `json:"user_id"` CompanyID pgtype.Int8 `json:"company_id"` Provider pgtype.Text `json:"provider"` @@ -487,7 +486,6 @@ type CreateVirtualGameTransactionParams struct { type CreateVirtualGameTransactionRow struct { ID int64 `json:"id"` - SessionID int64 `json:"session_id"` UserID int64 `json:"user_id"` CompanyID pgtype.Int8 `json:"company_id"` Provider pgtype.Text `json:"provider"` @@ -503,7 +501,6 @@ type CreateVirtualGameTransactionRow struct { func (q *Queries) CreateVirtualGameTransaction(ctx context.Context, arg CreateVirtualGameTransactionParams) (CreateVirtualGameTransactionRow, error) { row := q.db.QueryRow(ctx, CreateVirtualGameTransaction, - arg.SessionID, arg.UserID, arg.CompanyID, arg.Provider, @@ -517,7 +514,6 @@ func (q *Queries) CreateVirtualGameTransaction(ctx context.Context, arg CreateVi var i CreateVirtualGameTransactionRow err := row.Scan( &i.ID, - &i.SessionID, &i.UserID, &i.CompanyID, &i.Provider, @@ -785,7 +781,7 @@ SELECT c.name AS company_name, COUNT(vgt.id) AS number_of_bets, COALESCE(SUM(vgt.amount), 0) AS total_transaction_sum FROM virtual_game_transactions vgt - JOIN virtual_game_sessions vgs ON vgt.session_id = vgs.id + -- JOIN virtual_game_sessions vgs ON vgt.session_id = vgs.id JOIN virtual_games vg ON vgs.game_id = vg.id JOIN companies c ON vgt.company_id = c.id WHERE vgt.transaction_type = 'BET' @@ -833,7 +829,7 @@ func (q *Queries) GetVirtualGameSummaryInRange(ctx context.Context, arg GetVirtu const GetVirtualGameTransactionByExternalID = `-- name: GetVirtualGameTransactionByExternalID :one SELECT id, - session_id, + -- session_id, user_id, wallet_id, transaction_type, @@ -849,7 +845,6 @@ WHERE external_transaction_id = $1 type GetVirtualGameTransactionByExternalIDRow struct { ID int64 `json:"id"` - SessionID int64 `json:"session_id"` UserID int64 `json:"user_id"` WalletID int64 `json:"wallet_id"` TransactionType string `json:"transaction_type"` @@ -866,7 +861,6 @@ func (q *Queries) GetVirtualGameTransactionByExternalID(ctx context.Context, ext var i GetVirtualGameTransactionByExternalIDRow err := row.Scan( &i.ID, - &i.SessionID, &i.UserID, &i.WalletID, &i.TransactionType, diff --git a/internal/domain/common.go b/internal/domain/common.go index dd5cc38..5856690 100644 --- a/internal/domain/common.go +++ b/internal/domain/common.go @@ -16,6 +16,11 @@ type Response struct { Data interface{} `json:"data,omitempty"` Success bool `json:"success"` StatusCode int `json:"status_code"` + MetaData interface{} `json:"metadata"` +} + +type CallbackErrorResponse struct { + Error string `json:"error,omitempty"` } func CalculateWinnings(amount Currency, totalOdds float32) Currency { diff --git a/internal/domain/direct_deposit.go b/internal/domain/direct_deposit.go index 87c73e1..7ee2408 100644 --- a/internal/domain/direct_deposit.go +++ b/internal/domain/direct_deposit.go @@ -1,113 +1,39 @@ package domain -import ( - "time" - "math/big" - dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" - "github.com/jackc/pgx/v5/pgtype" -) +import "time" type DirectDepositStatus string const ( - DepositStatusPending DirectDepositStatus = "pending" - DepositStatusCompleted DirectDepositStatus = "completed" - DepositStatusRejected DirectDepositStatus = "rejected" + DepositStatusPending DirectDepositStatus = "PENDING" + DepositStatusCompleted DirectDepositStatus = "COMPLETED" + DepositStatusRejected DirectDepositStatus = "REJECTED" ) type DirectDeposit struct { - ID int64 `json:"id"` - CustomerID int64 `json:"customer_id"` - WalletID int64 `json:"wallet_id"` - Wallet Wallet `json:"wallet"` - Amount Currency `json:"amount"` - BankReference string `json:"bank_reference"` - SenderAccount string `json:"sender_account"` - Status DirectDepositStatus `json:"status"` - CreatedAt time.Time `json:"created_at"` - VerifiedBy *int64 `json:"verified_by"` - VerificationNotes string `json:"verification_notes"` - VerifiedAt *time.Time `json:"verified_at"` + ID int + CustomerID int + WalletID int + BankName string + AccountNumber string + AccountHolder string + Amount float64 + ReferenceNumber string + TransferScreenshot string + Status string + CreatedAt time.Time + ApprovedBy *int + ApprovedAt *time.Time + RejectionReason *string } type CreateDirectDeposit struct { - CustomerID int64 - WalletID int64 - Amount Currency - BankReference string - SenderAccount string - Status DirectDepositStatus + CustomerID int + WalletID int + BankName string + AccountNumber string + AccountHolder string + Amount float64 + ReferenceNumber string + TransferScreenshot string } - -type UpdateDirectDeposit struct { - ID int64 - Status DirectDepositStatus - VerifiedBy int64 - VerificationNotes string - VerifiedAt time.Time -} - -type DirectDepositRequest struct { - CustomerID int64 `json:"customer_id" binding:"required"` - Amount Currency `json:"amount" binding:"required,gt=0"` - BankReference string `json:"bank_reference" binding:"required"` - SenderAccount string `json:"sender_account" binding:"required"` -} - -type VerifyDirectDepositRequest struct { - DepositID int64 `json:"deposit_id" binding:"required"` - IsVerified bool `json:"is_verified" binding:"required"` - Notes string `json:"notes"` -} - - -func ConvertDBDirectDeposit(deposit dbgen.DirectDeposit) DirectDeposit { - return DirectDeposit{ - ID: deposit.ID, - CustomerID: deposit.CustomerID, - WalletID: deposit.WalletID, - Amount: Currency(deposit.Amount.Int.Int64()), - BankReference: deposit.BankReference, - SenderAccount: deposit.SenderAccount, - Status: DirectDepositStatus(deposit.Status), - CreatedAt: deposit.CreatedAt.Time, - VerifiedBy: convertPgInt64ToPtr(deposit.VerifiedBy), - VerificationNotes: deposit.VerificationNotes.String, - VerifiedAt: convertPgTimeToPtr(deposit.VerifiedAt), - } -} - -func ConvertCreateDirectDeposit(deposit CreateDirectDeposit) dbgen.CreateDirectDepositParams { - return dbgen.CreateDirectDepositParams{ - CustomerID: deposit.CustomerID, - WalletID: deposit.WalletID, - Amount: pgtype.Numeric{Int: big.NewInt(int64(deposit.Amount)), Valid: true}, - BankReference: deposit.BankReference, - SenderAccount: deposit.SenderAccount, - Status: string(deposit.Status), - } -} - -func ConvertUpdateDirectDeposit(deposit UpdateDirectDeposit) dbgen.UpdateDirectDepositParams { - return dbgen.UpdateDirectDepositParams{ - ID: deposit.ID, - Status: string(deposit.Status), - VerifiedBy: pgtype.Int8{Int64: deposit.VerifiedBy, Valid: true}, - VerificationNotes: pgtype.Text{String: deposit.VerificationNotes, Valid: deposit.VerificationNotes != ""}, - VerifiedAt: pgtype.Timestamp{Time: deposit.VerifiedAt, Valid: true}, - } -} - -func convertPgInt64ToPtr(i pgtype.Int8) *int64 { - if i.Valid { - return &i.Int64 - } - return nil -} - -func convertPgTimeToPtr(t pgtype.Timestamp) *time.Time { - if t.Valid { - return &t.Time - } - return nil -} \ No newline at end of file diff --git a/internal/ports/wallet.go b/internal/ports/wallet.go index ef6e92c..c8eb97d 100644 --- a/internal/ports/wallet.go +++ b/internal/ports/wallet.go @@ -47,13 +47,13 @@ type ApprovalStore interface { GetPendingApprovals(ctx context.Context) ([]domain.TransferDetail, error) } -type DirectDepositStore interface { - CreateDirectDeposit(ctx context.Context, deposit domain.CreateDirectDeposit) (domain.DirectDeposit, error) - GetDirectDeposit(ctx context.Context, id int64) (domain.DirectDeposit, error) - UpdateDirectDeposit(ctx context.Context, deposit domain.UpdateDirectDeposit) (domain.DirectDeposit, error) - GetDirectDepositsByStatus(ctx context.Context, status domain.DirectDepositStatus) ([]domain.DirectDeposit, error) - GetCustomerDirectDeposits(ctx context.Context, customerID int64) ([]domain.DirectDeposit, error) -} +// type DirectDepositStore interface { +// CreateDirectDeposit(ctx context.Context, deposit domain.CreateDirectDeposit) (domain.DirectDeposit, error) +// GetDirectDeposit(ctx context.Context, id int64) (domain.DirectDeposit, error) +// UpdateDirectDeposit(ctx context.Context, deposit domain.UpdateDirectDeposit) (domain.DirectDeposit, error) +// GetDirectDepositsByStatus(ctx context.Context, status domain.DirectDepositStatus) ([]domain.DirectDeposit, error) +// GetCustomerDirectDeposits(ctx context.Context, customerID int64) ([]domain.DirectDeposit, error) +// } type WalletStatStore interface { UpdateWalletStats(ctx context.Context) error diff --git a/internal/repository/direct_deposit.go b/internal/repository/direct_deposit.go index 8d4a6d3..f9f160c 100644 --- a/internal/repository/direct_deposit.go +++ b/internal/repository/direct_deposit.go @@ -2,61 +2,195 @@ package repository import ( "context" + "time" + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - "github.com/SamuelTariku/FortuneBet-Backend/internal/ports" + "github.com/jackc/pgx/v5/pgtype" ) -// Interface for creating new wallet store -func NewDirectDepositStore(s *Store) ports.DirectDepositStore { return s } - - -func (s *Store) CreateDirectDeposit(ctx context.Context, deposit domain.CreateDirectDeposit) (domain.DirectDeposit, error) { - newDeposit, err := s.queries.CreateDirectDeposit(ctx, domain.ConvertCreateDirectDeposit(deposit)) - if err != nil { - return domain.DirectDeposit{}, err - } - return domain.ConvertDBDirectDeposit(newDeposit), nil +type DirectDepositRepository interface { + CreateDirectDeposit(ctx context.Context, deposit *domain.DirectDeposit) error + GetDirectDepositsByStatus(ctx context.Context, status string, page int, pageSize int) ([]domain.DirectDeposit, int64, error) + ApproveDirectDeposit(ctx context.Context, depositID int, adminID int) error + RejectDirectDeposit(ctx context.Context, depositID int, adminID int, reason string) error + DeleteDirectDeposit(ctx context.Context, id int) error + GetDirectDepositByID(ctx context.Context, id int) (*domain.DirectDeposit, error) } -func (s *Store) GetDirectDeposit(ctx context.Context, id int64) (domain.DirectDeposit, error) { - deposit, err := s.queries.GetDirectDeposit(ctx, id) - if err != nil { - return domain.DirectDeposit{}, err - } - return domain.ConvertDBDirectDeposit(deposit), nil +type DirectDepositRepo struct { + store *Store } -func (s *Store) UpdateDirectDeposit(ctx context.Context, deposit domain.UpdateDirectDeposit) (domain.DirectDeposit, error) { - updatedDeposit, err := s.queries.UpdateDirectDeposit(ctx, domain.ConvertUpdateDirectDeposit(deposit)) - if err != nil { - return domain.DirectDeposit{}, err - } - return domain.ConvertDBDirectDeposit(updatedDeposit), nil +func NewDirectDepositRepository(store *Store) DirectDepositRepository { + return &DirectDepositRepo{store: store} } -func (s *Store) GetDirectDepositsByStatus(ctx context.Context, status domain.DirectDepositStatus) ([]domain.DirectDeposit, error) { - deposits, err := s.queries.GetDirectDepositsByStatus(ctx, string(status)) +func (r *DirectDepositRepo) CreateDirectDeposit(ctx context.Context, deposit *domain.DirectDeposit) error { + params := dbgen.CreateDirectDepositParams{ + CustomerID: pgtype.Int8{Int64: int64(deposit.CustomerID)}, + WalletID: pgtype.Int8{Int64: int64(deposit.WalletID)}, + BankName: pgtype.Text{String: deposit.BankName}, + AccountNumber: pgtype.Text{String: deposit.AccountNumber}, + AccountHolder: pgtype.Text{String: deposit.AccountHolder}, + Amount: pgtype.Numeric{Exp: int32(deposit.Amount)}, + ReferenceNumber: pgtype.Text{String: deposit.ReferenceNumber}, + TransferScreenshot: pgtype.Text{String: deposit.TransferScreenshot}, + } + + dbDeposit, err := r.store.queries.CreateDirectDeposit(ctx, params) + if err != nil { + return err + } + + // Map back to domain struct + deposit.ID = int(dbDeposit.ID) + deposit.Status = dbDeposit.Status.String + deposit.CreatedAt = dbDeposit.CreatedAt.Time + + return nil +} + +func (r *DirectDepositRepo) GetDirectDepositsByStatus( + ctx context.Context, + status string, + page int, + pageSize int, +) ([]domain.DirectDeposit, int64, error) { + + // Default pagination rules + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 50 + } + offset := (page - 1) * pageSize + + params := dbgen.GetDirectDepositsByStatusParams{ + Status: pgtype.Text{String: status}, + Limit: int32(pageSize), + Offset: int32(offset), + } + + dbItems, err := r.store.queries.GetDirectDepositsByStatus(ctx, params) + if err != nil { + return nil, 0, err + } + + total, err := r.store.queries.CountDirectDepositsByStatus(ctx, pgtype.Text{String: status}) + if err != nil { + return nil, 0, err + } + + deposits := make([]domain.DirectDeposit, len(dbItems)) + for i, d := range dbItems { + deposits[i] = *mapDBDirectDepositToDomain(&d) + } + + return deposits, total, nil +} + +func (r *DirectDepositRepo) ApproveDirectDeposit( + ctx context.Context, + depositID int, + adminID int, +) error { + + params := dbgen.ApproveDirectDepositParams{ + ID: int64(depositID), + ApprovedBy: pgtype.Int8{Int64: int64(adminID)}, + } + + err := r.store.queries.ApproveDirectDeposit(ctx, params) + if err != nil { + return err + } + + return nil +} + +func (r *DirectDepositRepo) GetDirectDepositByID( + ctx context.Context, + id int, +) (*domain.DirectDeposit, error) { + + dbDeposit, err := r.store.queries.GetDirectDepositByID(ctx, int64(id)) if err != nil { return nil, err } - result := make([]domain.DirectDeposit, 0, len(deposits)) - for _, deposit := range deposits { - result = append(result, domain.ConvertDBDirectDeposit(deposit)) - } - return result, nil + deposit := mapDBDirectDepositToDomain(&dbDeposit) + return deposit, nil } -func (s *Store) GetCustomerDirectDeposits(ctx context.Context, customerID int64) ([]domain.DirectDeposit, error) { - deposits, err := s.queries.GetCustomerDirectDeposits(ctx, customerID) +func (r *DirectDepositRepo) DeleteDirectDeposit( + ctx context.Context, + id int, +) error { + + err := r.store.queries.DeleteDirectDeposit(ctx, int64(id)) if err != nil { - return nil, err + return err } - result := make([]domain.DirectDeposit, 0, len(deposits)) - for _, deposit := range deposits { - result = append(result, domain.ConvertDBDirectDeposit(deposit)) - } - return result, nil + return nil +} + +func (r *DirectDepositRepo) RejectDirectDeposit( + ctx context.Context, + depositID int, + adminID int, + reason string, +) error { + + params := dbgen.RejectDirectDepositParams{ + ID: int64(depositID), + ApprovedBy: pgtype.Int8{Int64: int64(adminID)}, + RejectionReason: pgtype.Text{String: reason}, + } + + err := r.store.queries.RejectDirectDeposit(ctx, params) + if err != nil { + return err + } + + return nil +} + +func mapDBDirectDepositToDomain(d *dbgen.DirectDeposit) *domain.DirectDeposit { + var approvedBy *int + if d.ApprovedBy.Valid { + v := int(d.ApprovedBy.Int64) + approvedBy = &v + } + + var approvedAt *time.Time + if d.ApprovedAt.Valid { + t := d.ApprovedAt.Time + approvedAt = &t + } + + var rejectionReason *string + if d.RejectionReason.Valid { + r := d.RejectionReason.String + rejectionReason = &r + } + + return &domain.DirectDeposit{ + ID: int(d.ID), + CustomerID: int(d.CustomerID.Int64), + WalletID: int(d.WalletID.Int64), + BankName: d.BankName.String, + AccountNumber: d.AccountNumber.String, + AccountHolder: d.AccountHolder.String, + Amount: float64(d.Amount.Exp), + ReferenceNumber: d.ReferenceNumber.String, + TransferScreenshot: d.TransferScreenshot.String, + Status: d.Status.String, + CreatedAt: d.CreatedAt.Time, + ApprovedBy: approvedBy, + ApprovedAt: approvedAt, + RejectionReason: rejectionReason, + } } diff --git a/internal/repository/virtual_game.go b/internal/repository/virtual_game.go index e7befef..2d455fa 100644 --- a/internal/repository/virtual_game.go +++ b/internal/repository/virtual_game.go @@ -224,7 +224,7 @@ func (r *VirtualGameRepo) GetVirtualGameSessionByToken(ctx context.Context, toke func (r *VirtualGameRepo) CreateVirtualGameTransaction(ctx context.Context, tx *domain.VirtualGameTransaction) error { params := dbgen.CreateVirtualGameTransactionParams{ - SessionID: tx.SessionID, + // SessionID: tx.SessionID, UserID: tx.UserID, WalletID: tx.WalletID, TransactionType: tx.TransactionType, @@ -239,7 +239,7 @@ func (r *VirtualGameRepo) CreateVirtualGameTransaction(ctx context.Context, tx * func (r *VirtualGameRepo) CreateVirtualGameHistory(ctx context.Context, his *domain.VirtualGameHistory) error { params := dbgen.CreateVirtualGameHistoryParams{ - SessionID: pgtype.Text{String: his.SessionID, Valid: true}, + // SessionID: pgtype.Text{String: his.SessionID, Valid: true}, UserID: his.UserID, // WalletID: pgtype.Int8{Int64: *his.WalletID, Valid: true}, TransactionType: his.TransactionType, @@ -262,7 +262,7 @@ func (r *VirtualGameRepo) GetVirtualGameTransactionByExternalID(ctx context.Cont } return &domain.VirtualGameTransaction{ ID: dbTx.ID, - SessionID: dbTx.SessionID, + // SessionID: dbTx.SessionID, UserID: dbTx.UserID, WalletID: dbTx.WalletID, TransactionType: dbTx.TransactionType, diff --git a/internal/services/direct_deposit/service.go b/internal/services/direct_deposit/service.go new file mode 100644 index 0000000..2b73c3d --- /dev/null +++ b/internal/services/direct_deposit/service.go @@ -0,0 +1,305 @@ +package directdeposit + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/ports" + "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" + notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" +) + +type Service struct { + walletSvc wallet.Service + transferStore ports.TransferStore + directDepositStore repository.DirectDepositRepository + notificationSvc *notificationservice.Service + userSvc *user.Service + // mongoLogger *zap.Logger + // logger *slog.Logger +} + +func NewService( + walletSvc wallet.Service, + transferStore ports.TransferStore, + directDepositStore repository.DirectDepositRepository, + notificationSvc *notificationservice.Service, + userSvc *user.Service, + // mongoLogger *zap.Logger, + // logger *slog.Logger, +) *Service { + return &Service{ + walletSvc: walletSvc, + transferStore: transferStore, + directDepositStore: directDepositStore, + notificationSvc: notificationSvc, + userSvc: userSvc, + // mongoLogger: mongoLogger, + // logger: logger, + } +} + +func (s *Service) CreateDirectDeposit( + ctx context.Context, + req domain.CreateDirectDeposit, +) (domain.DirectDeposit, error) { + + deposit := domain.DirectDeposit{ + CustomerID: req.CustomerID, + WalletID: req.WalletID, + BankName: req.BankName, + AccountNumber: req.AccountNumber, + AccountHolder: req.AccountHolder, + Amount: req.Amount, + ReferenceNumber: req.ReferenceNumber, + TransferScreenshot: req.TransferScreenshot, + Status: "PENDING", + } + + // Step 1: create the deposit in DB + if err := s.directDepositStore.CreateDirectDeposit(ctx, &deposit); err != nil { + return domain.DirectDeposit{}, err + } + + // Step 2: prepare common notification metadata + raw, _ := json.Marshal(map[string]any{ + "deposit_id": deposit.ID, + "customer_id": deposit.CustomerID, + "amount": deposit.Amount, + "status": deposit.Status, + "timestamp": time.Now(), + }) + + // ------------------------------- + // Step 3a: notify the customer + customerNotification := &domain.Notification{ + RecipientID: int64(deposit.CustomerID), + DeliveryChannel: domain.DeliveryChannelInApp, + Reciever: domain.NotificationRecieverSideCustomer, + Type: domain.NOTIFICATION_TYPE_WALLET, + DeliveryStatus: domain.DeliveryStatusPending, + IsRead: false, + Level: domain.NotificationLevelInfo, + Priority: 2, + Metadata: raw, + Payload: domain.NotificationPayload{ + Headline: "Direct Deposit Created", + Message: fmt.Sprintf("Your direct deposit of %.2f is now pending approval.", deposit.Amount), + }, + } + + if err := s.notificationSvc.SendNotification(ctx, customerNotification); err != nil { + return domain.DirectDeposit{}, err + } + + // ------------------------------- + // Step 3b: notify admins + adminNotification := &domain.Notification{ + RecipientID: 0, // 0 or special ID for admin-wide notifications + DeliveryChannel: domain.DeliveryChannelInApp, + Reciever: domain.NotificationRecieverSideAdmin, + Type: domain.NOTIFICATION_TYPE_APPROVAL_REQUIRED, + DeliveryStatus: domain.DeliveryStatusPending, + IsRead: false, + Level: domain.NotificationLevelInfo, + Priority: 2, + Metadata: raw, + Payload: domain.NotificationPayload{ + Headline: "New Direct Deposit Pending Approval", + Message: fmt.Sprintf("Customer #%d has created a direct deposit of %.2f that requires your approval.", deposit.CustomerID, deposit.Amount), + }, + } + + if err := s.notificationSvc.SendNotification(ctx, adminNotification); err != nil { + return domain.DirectDeposit{}, err + } + + return deposit, nil +} + +func (s *Service) GetDirectDepositsByStatus( + ctx context.Context, + status string, + page int, + pageSize int, +) ([]domain.DirectDeposit, int64, error) { + + deposits, total, err := s.directDepositStore.GetDirectDepositsByStatus( + ctx, + status, + page, + pageSize, + ) + if err != nil { + return nil, 0, err + } + + return deposits, total, nil +} + +func (s *Service) ApproveDirectDeposit( + ctx context.Context, + depositID int, + adminID int, +) error { + + // Step 1: fetch deposit (ensure it exists) + deposit, err := s.directDepositStore.GetDirectDepositByID(ctx, depositID) + if err != nil { + return err + } + + // Step 2: approve in DB + if err := s.directDepositStore.ApproveDirectDeposit(ctx, depositID, adminID); err != nil { + return err + } + + // Step 3: credit wallet balance + wallet, err := s.walletSvc.GetCustomerWallet(ctx, int64(deposit.CustomerID)) + if err != nil { + return err + } + if _, err := s.walletSvc.AddToWallet(ctx, + wallet.RegularID, + domain.Currency(deposit.Amount), + domain.ValidInt64{}, + domain.TRANSFER_DIRECT, + domain.PaymentDetails{}, + "", + ); err != nil { + return err + } + + // Step 4: record transfer + transfer := domain.CreateTransfer{ + Amount: domain.Currency(deposit.Amount), + Verified: true, + Message: "Direct deposit approved and credited", + Type: domain.DEPOSIT, + PaymentMethod: domain.TRANSFER_DIRECT, + ReceiverWalletID: domain.ValidInt64{Valid: true, Value: wallet.RegularID}, + ReferenceNumber: deposit.ReferenceNumber, + Status: string(domain.DepositStatusCompleted), + } + + if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil { + return err + } + + // Step 5: send customer notification + raw, _ := json.Marshal(map[string]any{ + "deposit_id": deposit.ID, + "amount": deposit.Amount, + "status": "APPROVED", + "timestamp": time.Now(), + }) + + notification := &domain.Notification{ + RecipientID: int64(deposit.CustomerID), + DeliveryChannel: domain.DeliveryChannelInApp, + Reciever: domain.NotificationRecieverSideCustomer, + Type: domain.NOTIFICATION_TYPE_TRANSFER_SUCCESS, + DeliveryStatus: domain.DeliveryStatusPending, + IsRead: false, + Level: domain.NotificationLevelInfo, + Priority: 2, + Metadata: raw, + Payload: domain.NotificationPayload{ + Headline: "Direct Deposit Approved", + Message: fmt.Sprintf("Your direct deposit of %.2f has been approved and credited to your wallet.", deposit.Amount), + }, + } + + if err := s.notificationSvc.SendNotification(ctx, notification); err != nil { + return err + } + + return nil +} + +func (s *Service) RejectDirectDeposit( + ctx context.Context, + depositID int, + adminID int, + reason string, +) error { + + // Step 1: fetch deposit to ensure it exists + deposit, err := s.directDepositStore.GetDirectDepositByID(ctx, depositID) + if err != nil { + return err + } + + // Step 2: reject operation + if err := s.directDepositStore.RejectDirectDeposit(ctx, depositID, adminID, reason); err != nil { + return err + } + + // Step 3: send customer notification + raw, _ := json.Marshal(map[string]any{ + "deposit_id": deposit.ID, + "amount": deposit.Amount, + "status": "REJECTED", + "reason": reason, + "timestamp": time.Now(), + }) + + notification := &domain.Notification{ + RecipientID: int64(deposit.CustomerID), + DeliveryChannel: domain.DeliveryChannelInApp, + Reciever: domain.NotificationRecieverSideCustomer, + Type: domain.NOTIFICATION_TYPE_TRANSFER_REJECTED, + DeliveryStatus: domain.DeliveryStatusPending, + IsRead: false, + Level: domain.NotificationLevelWarning, + Priority: 2, + Metadata: raw, + Payload: domain.NotificationPayload{ + Headline: "Direct Deposit Rejected", + Message: fmt.Sprintf("Your direct deposit of %.2f was rejected. Reason: %s", deposit.Amount, reason), + }, + } + + if err := s.notificationSvc.SendNotification(ctx, notification); err != nil { + return err + } + + return nil +} + +func (s *Service) GetDirectDepositByID( + ctx context.Context, + depositID int, +) (*domain.DirectDeposit, error) { + + deposit, err := s.directDepositStore.GetDirectDepositByID(ctx, depositID) + if err != nil { + return nil, err + } + + return deposit, nil +} + +func (s *Service) DeleteDirectDeposit( + ctx context.Context, + depositID int, +) error { + + // Optional: fetch first to ensure deposit exists + _, err := s.directDepositStore.GetDirectDepositByID(ctx, depositID) + if err != nil { + return err + } + + // Perform deletion + if err := s.directDepositStore.DeleteDirectDeposit(ctx, depositID); err != nil { + return err + } + + return nil +} diff --git a/internal/services/virtualGame/service.go b/internal/services/virtualGame/service.go index 89a0a68..95454f7 100644 --- a/internal/services/virtualGame/service.go +++ b/internal/services/virtualGame/service.go @@ -71,7 +71,7 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI // 3. Record virtual game history (optional but recommended) history := &domain.VirtualGameHistory{ - SessionID: sessionID, + // SessionID: sessionID, UserID: userID, CompanyID: user.CompanyID.Value, Provider: string(domain.PROVIDER_POPOK), diff --git a/internal/services/wallet/direct_deposit.go b/internal/services/wallet/direct_deposit.go deleted file mode 100644 index 11ef1bf..0000000 --- a/internal/services/wallet/direct_deposit.go +++ /dev/null @@ -1,216 +0,0 @@ -package wallet - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "time" - - "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" -) - -// InitiateDirectDeposit creates a pending deposit request -func (s *Service) InitiateDirectDeposit( - ctx context.Context, - customerID int64, - amount domain.Currency, - bankRef string, // Mobile banking transaction reference - senderAccount string, // Customer's account number -) (domain.DirectDeposit, error) { - // Get customer's betting wallet - customerWallet, err := s.GetCustomerWallet(ctx, customerID) - if err != nil { - return domain.DirectDeposit{}, fmt.Errorf("failed to get customer wallet: %w", err) - } - - // Create pending deposit record - deposit, err := s.directDepositStore.CreateDirectDeposit(ctx, domain.CreateDirectDeposit{ - CustomerID: customerID, - WalletID: customerWallet.ID, - Amount: amount, - BankReference: bankRef, - SenderAccount: senderAccount, - Status: domain.DepositStatusPending, - }) - if err != nil { - return domain.DirectDeposit{}, fmt.Errorf("failed to create deposit record: %w", err) - } - - // Notify cashiers for manual verification - go s.notifyCashiersForVerification(ctx, deposit.ID, customerID, amount) - - return deposit, nil -} - -// VerifyDirectDeposit verifies and processes the deposit -func (s *Service) VerifyDirectDeposit( - ctx context.Context, - depositID int64, - cashierID int64, - isVerified bool, - verificationNotes string, -) (domain.DirectDeposit, error) { - // Get the deposit record - deposit, err := s.directDepositStore.GetDirectDeposit(ctx, depositID) - if err != nil { - return domain.DirectDeposit{}, fmt.Errorf("failed to get deposit: %w", err) - } - - // Validate deposit status - if deposit.Status != domain.DepositStatusPending { - return domain.DirectDeposit{}, errors.New("only pending deposits can be verified") - } - - // Update based on verification result - if isVerified { - // Credit the wallet - err = s.walletStore.UpdateBalance(ctx, deposit.WalletID, - deposit.Wallet.Balance+deposit.Amount) - if err != nil { - return domain.DirectDeposit{}, fmt.Errorf("failed to update wallet balance: %w", err) - } - - // Publish wallet update event - // go s.publishWalletUpdate(ctx, deposit.WalletID, deposit.Wallet.UserID, - // deposit.Wallet.Balance+deposit.Amount, "direct_deposit_verified") - - // Update deposit status - deposit.Status = domain.DepositStatusCompleted - } else { - deposit.Status = domain.DepositStatusRejected - } - - // Update deposit record - updatedDeposit, err := s.directDepositStore.UpdateDirectDeposit(ctx, domain.UpdateDirectDeposit{ - ID: depositID, - Status: deposit.Status, - VerifiedBy: cashierID, - VerificationNotes: verificationNotes, - VerifiedAt: time.Now(), - }) - if err != nil { - return domain.DirectDeposit{}, fmt.Errorf("failed to update deposit: %w", err) - } - - // Notify customer of verification result - go s.notifyCustomerVerificationResult(ctx, updatedDeposit) - - return updatedDeposit, nil -} - -// GetPendingDirectDeposits returns deposits needing verification -func (s *Service) GetPendingDirectDeposits(ctx context.Context) ([]domain.DirectDeposit, error) { - return s.directDepositStore.GetDirectDepositsByStatus(ctx, domain.DepositStatusPending) -} - -// Helper functions -func (s *Service) notifyCashiersForVerification(ctx context.Context, depositID, customerID int64, amount domain.Currency) { - cashiers, _, err := s.userSvc.GetAllCashiers(ctx, domain.UserFilter{Role: string(domain.RoleCashier)}) - if err != nil { - s.logger.Error("failed to get cashiers for notification", - "error", err, - "deposit_id", depositID) - return - } - - customer, err := s.userSvc.GetUserByID(ctx, customerID) - if err != nil { - s.logger.Error("failed to get customer details", - "error", err, - "customer_id", customerID) - return - } - - for _, cashier := range cashiers { - metadataMap := map[string]interface{}{ - "deposit_id": depositID, - "customer_id": customerID, - "amount": amount.Float32(), - } - metadataJSON, err := json.Marshal(metadataMap) - if err != nil { - s.logger.Error("failed to marshal notification metadata", - "error", err, - "deposit_id", depositID) - continue - } - notification := &domain.Notification{ - RecipientID: cashier.ID, - Type: domain.NotificationTypeDepositVerification, - Level: domain.NotificationLevelInfo, - DeliveryChannel: domain.DeliveryChannelInApp, - Payload: domain.NotificationPayload{ - Headline: "Direct Deposit Requires Verification", - Message: fmt.Sprintf("Customer %s deposited %.2f - please verify", customer.FirstName+" "+customer.LastName, amount.Float32()), - }, - Metadata: metadataJSON, - } - - if err := s.notificationSvc.SendNotification(ctx, notification); err != nil { - s.logger.Error("failed to send verification notification", - "cashier_id", cashier.ID, - "error", err) - } - } -} - -func (s *Service) notifyCustomerVerificationResult(ctx context.Context, deposit domain.DirectDeposit) { - var ( - headline string - message string - level domain.NotificationLevel - ) - - if deposit.Status == domain.DepositStatusCompleted { - headline = "Deposit Verified" - message = fmt.Sprintf("Your deposit of %.2f has been credited to your wallet", deposit.Amount.Float32()) - level = domain.NotificationLevelSuccess - } else { - headline = "Deposit Rejected" - message = fmt.Sprintf("Your deposit of %.2f was not verified. Reason: %s", - deposit.Amount.Float32(), deposit.VerificationNotes) - level = domain.NotificationLevelError - } - - metadataMap := map[string]interface{}{ - "deposit_id": deposit.ID, - "amount": deposit.Amount.Float32(), - "status": string(deposit.Status), - } - metadataJSON, err := json.Marshal(metadataMap) - if err != nil { - s.logger.Error("failed to marshal notification metadata", - "error", err, - "deposit_id", deposit.ID) - return - } - - notification := &domain.Notification{ - RecipientID: deposit.CustomerID, - Type: domain.NotificationTypeDepositResult, - Level: level, - DeliveryChannel: domain.DeliveryChannelInApp, - Payload: domain.NotificationPayload{ - Headline: headline, - Message: message, - }, - Metadata: metadataJSON, - } - - if err := s.notificationSvc.SendNotification(ctx, notification); err != nil { - s.logger.Error("failed to send deposit result notification", - "customer_id", deposit.CustomerID, - "error", err) - } -} - -// func (s *Service) publishWalletUpdate(ctx context.Context, walletID, userID int64, newBalance domain.Currency, trigger string) { -// s.kafkaProducer.Publish(ctx, fmt.Sprint(walletID), event.WalletEvent{ -// EventType: event.WalletBalanceUpdated, -// WalletID: walletID, -// UserID: userID, -// Balance: newBalance, -// Trigger: trigger, -// }) -// } diff --git a/internal/services/wallet/service.go b/internal/services/wallet/service.go index 08d4e8f..1cf6bd9 100644 --- a/internal/services/wallet/service.go +++ b/internal/services/wallet/service.go @@ -14,7 +14,7 @@ type Service struct { // approvalStore ApprovalStore walletStore ports.WalletStore transferStore ports.TransferStore - directDepositStore ports.DirectDepositStore + // directDepositStore ports.DirectDepositStore notificationSvc *notificationservice.Service userSvc *user.Service mongoLogger *zap.Logger @@ -24,7 +24,7 @@ type Service struct { func NewService( walletStore ports.WalletStore, transferStore ports.TransferStore, - directDepositStore ports.DirectDepositStore, + // directDepositStore ports.DirectDepositStore, notificationSvc *notificationservice.Service, userSvc *user.Service, mongoLogger *zap.Logger, @@ -33,7 +33,7 @@ func NewService( return &Service{ walletStore: walletStore, transferStore: transferStore, - directDepositStore: directDepositStore, + // directDepositStore: directDepositStore, // approvalStore: approvalStore, notificationSvc: notificationSvc, userSvc: userSvc, diff --git a/internal/web_server/app.go b/internal/web_server/app.go index 288ffde..6b2f6a5 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -13,6 +13,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency" + directdeposit "github.com/SamuelTariku/FortuneBet-Backend/internal/services/direct_deposit" enetpulse "github.com/SamuelTariku/FortuneBet-Backend/internal/services/enet_pulse" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions" @@ -48,6 +49,7 @@ import ( ) type App struct { + directDepositSvc *directdeposit.Service enetPulseSvc *enetpulse.Service atlasVirtualGameService atlas.AtlasVirtualGameService veliVirtualGameService *veli.Service @@ -92,6 +94,7 @@ type App struct { } func NewApp( + directDepositSvc *directdeposit.Service, enetPulseSvc *enetpulse.Service, atlasVirtualGameService atlas.AtlasVirtualGameService, veliVirtualGameService *veli.Service, @@ -149,6 +152,7 @@ func NewApp( app.Static("/static", "./static") s := &App{ + directDepositSvc: directDepositSvc, enetPulseSvc: enetPulseSvc, atlasVirtualGameService: atlasVirtualGameService, veliVirtualGameService: veliVirtualGameService, diff --git a/internal/web_server/handlers/direct_deposit.go b/internal/web_server/handlers/direct_deposit.go index fa3a28f..d8bcd86 100644 --- a/internal/web_server/handlers/direct_deposit.go +++ b/internal/web_server/handlers/direct_deposit.go @@ -1,124 +1,267 @@ package handlers import ( + "fmt" + "math" + "strconv" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/gofiber/fiber/v2" ) -// InitiateDirectDeposit godoc -// @Summary Initiate a direct deposit -// @Description Customer initiates a direct deposit from mobile banking -// @Tags Direct Deposits -// @Accept json -// @Produce json -// @Param request body domain.DirectDepositRequest true "Deposit details" -// @Success 201 {object} domain.Response -// @Failure 400 {object} domain.ErrorResponse -// @Failure 500 {object} domain.ErrorResponse -// @Router /api/v1/direct_deposit [post] -func (h *Handler) InitiateDirectDeposit(c *fiber.Ctx) error { - var req domain.DirectDepositRequest +// CreateDirectDeposit godoc +// @Summary Create a new direct deposit +// @Description Creates a direct deposit for a customer and notifies both the customer and admins +// @Tags DirectDeposit +// @Accept json +// @Produce json +// @Param body body domain.CreateDirectDeposit true "Direct deposit details" +// @Success 200 {object} domain.Response{data=domain.DirectDeposit} +// @Failure 400 {object} domain.ErrorResponse +// @Failure 502 {object} domain.ErrorResponse +// @Router /api/v1/direct-deposits [post] +func (h *Handler) CreateDirectDeposit(c *fiber.Ctx) error { + var req domain.CreateDirectDeposit if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ - Error: err.Error(), Message: "Invalid request payload", - }) - } - - deposit, err := h.walletSvc.InitiateDirectDeposit( - c.Context(), - req.CustomerID, - req.Amount, - req.BankReference, - req.SenderAccount, - ) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Error: err.Error(), - Message: "Failed to initiate direct deposit", }) } - return c.Status(fiber.StatusCreated).JSON(domain.Response{ - Message: "Direct deposit initiated successfully", + // Call service + deposit, err := h.directDepositSvc.CreateDirectDeposit(c.Context(), req) + if err != nil { + h.logger.Error("CreateDirectDeposit error", err.Error()) + return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + Message: "Failed to create direct deposit", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Direct deposit created successfully", Data: deposit, + StatusCode: fiber.StatusOK, Success: true, - StatusCode: fiber.StatusCreated, }) } -// VerifyDirectDeposit godoc -// @Summary Verify a direct deposit -// @Description Cashier verifies a direct deposit transaction -// @Tags Direct Deposits -// @Accept json -// @Produce json -// @Param request body domain.VerifyDirectDepositRequest true "Verification details" -// @Success 200 {object} domain.Response -// @Failure 400 {object} domain.ErrorResponse -// @Failure 401 {object} domain.ErrorResponse -// @Failure 500 {object} domain.ErrorResponse -// @Router /api/v1/direct_deposit/verify [post] -func (h *Handler) VerifyDirectDeposit(c *fiber.Ctx) error { - var req domain.VerifyDirectDepositRequest - if err := c.BodyParser(&req); err != nil { +// GetDirectDepositsByStatus godoc +// @Summary Get direct deposits by status +// @Description Fetches direct deposits filtered by status with pagination +// @Tags DirectDeposit +// @Accept json +// @Produce json +// @Param status query string true "Deposit status (e.g., PENDING, APPROVED, REJECTED)" +// @Param page query int false "Page number" +// @Param pageSize query int false "Page size" +// @Success 200 {object} domain.Response{data=[]domain.DirectDeposit} +// @Failure 400 {object} domain.ErrorResponse +// @Failure 502 {object} domain.ErrorResponse +// @Router /api/v1/direct-deposits [get] +func (h *Handler) GetDirectDepositsByStatus(c *fiber.Ctx) error { + status := c.Query("status") + if status == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ - Error: err.Error(), - Message: "Invalid verification request", + Message: "status query parameter is required", }) } - cashierID := c.Locals("user_id") - if cashierID == nil { - return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ - Error: "missing user_id in context", - Message: "Unauthorized access", - }) - } + page, _ := strconv.Atoi(c.Query("page", "1")) + pageSize, _ := strconv.Atoi(c.Query("pageSize", "50")) - deposit, err := h.walletSvc.VerifyDirectDeposit( - c.Context(), - req.DepositID, - cashierID.(int64), - req.IsVerified, - req.Notes, - ) + deposits, total, err := h.directDepositSvc.GetDirectDepositsByStatus(c.Context(), status, page, pageSize) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + h.logger.Error("GetDirectDepositsByStatus error", err) + return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + Message: "Failed to fetch direct deposits", Error: err.Error(), - Message: "Failed to verify deposit", }) } return c.Status(fiber.StatusOK).JSON(domain.Response{ - Message: "Deposit verification processed successfully", - Data: deposit, - Success: true, + Message: fmt.Sprintf("Direct deposits with status '%s' fetched successfully", status), + Data: deposits, StatusCode: fiber.StatusOK, + Success: true, + // Optional: include pagination info + MetaData: map[string]any{ + "page": page, + "pageSize": pageSize, + "total": total, + "totalPage": int(math.Ceil(float64(total) / float64(pageSize))), + }, }) } -// GetPendingDeposits godoc -// @Summary Get pending direct deposits -// @Description Get list of direct deposits needing verification -// @Tags Direct Deposits -// @Produce json -// @Success 200 {object} domain.Response -// @Failure 500 {object} domain.ErrorResponse -// @Router /api/v1/direct_deposit/pending [get] -func (h *Handler) GetPendingDirectDeposits(c *fiber.Ctx) error { - deposits, err := h.walletSvc.GetPendingDirectDeposits(c.Context()) +// ApproveDirectDeposit godoc +// @Summary Approve a direct deposit +// @Description Approves a direct deposit by admin and credits customer wallet +// @Tags DirectDeposit +// @Accept json +// @Produce json +// @Param depositID path int true "Deposit ID" +// @Param adminID query int true "Admin ID performing the approval" +// @Success 200 {object} domain.Response{data=string} +// @Failure 400 {object} domain.ErrorResponse +// @Failure 502 {object} domain.ErrorResponse +// @Router /api/v1/direct-deposits/{depositID}/approve [post] +func (h *Handler) ApproveDirectDeposit(c *fiber.Ctx) error { + depositID, err := strconv.Atoi(c.Params("depositID")) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid deposit ID", + Error: err.Error(), + }) + } + + adminID, err := strconv.Atoi(c.Query("adminID")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid admin ID", + Error: err.Error(), + }) + } + + if err := h.directDepositSvc.ApproveDirectDeposit(c.Context(), depositID, adminID); err != nil { + h.logger.Error("ApproveDirectDeposit error", err) + return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + Message: "Failed to approve direct deposit", Error: err.Error(), - Message: "Failed to retrieve pending deposits", }) } return c.Status(fiber.StatusOK).JSON(domain.Response{ - Message: "Pending deposits retrieved successfully", - Data: deposits, - Success: true, + Message: fmt.Sprintf("Direct deposit #%d approved successfully", depositID), + Data: fmt.Sprintf("Deposit #%d approved", depositID), StatusCode: fiber.StatusOK, + Success: true, }) -} \ No newline at end of file +} + +// RejectDirectDeposit godoc +// @Summary Reject a direct deposit +// @Description Rejects a direct deposit by admin and notifies the customer +// @Tags DirectDeposit +// @Accept json +// @Produce json +// @Param depositID path int true "Deposit ID" +// @Param adminID query int true "Admin ID performing the rejection" +// @Param reason query string true "Reason for rejection" +// @Success 200 {object} domain.Response{data=string} +// @Failure 400 {object} domain.ErrorResponse +// @Failure 502 {object} domain.ErrorResponse +// @Router /api/v1/direct-deposits/{depositID}/reject [post] +func (h *Handler) RejectDirectDeposit(c *fiber.Ctx) error { + depositID, err := strconv.Atoi(c.Params("depositID")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid deposit ID", + Error: err.Error(), + }) + } + + adminID, err := strconv.Atoi(c.Query("adminID")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid admin ID", + Error: err.Error(), + }) + } + + reason := c.Query("reason") + if reason == "" { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Rejection reason is required", + }) + } + + if err := h.directDepositSvc.RejectDirectDeposit(c.Context(), depositID, adminID, reason); err != nil { + h.logger.Error("RejectDirectDeposit error", err) + return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + Message: "Failed to reject direct deposit", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: fmt.Sprintf("Direct deposit #%d rejected successfully", depositID), + Data: fmt.Sprintf("Deposit #%d rejected", depositID), + StatusCode: fiber.StatusOK, + Success: true, + }) +} + +// GetDirectDepositByID godoc +// @Summary Get a direct deposit by ID +// @Description Fetches a single direct deposit by its ID +// @Tags DirectDeposit +// @Accept json +// @Produce json +// @Param depositID path int true "Deposit ID" +// @Success 200 {object} domain.Response{data=domain.DirectDeposit} +// @Failure 400 {object} domain.ErrorResponse +// @Failure 502 {object} domain.ErrorResponse +// @Router /api/v1/direct-deposits/{depositID} [get] +func (h *Handler) GetDirectDepositByID(c *fiber.Ctx) error { + depositID, err := strconv.Atoi(c.Params("depositID")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid deposit ID", + Error: err.Error(), + }) + } + + deposit, err := h.directDepositSvc.GetDirectDepositByID(c.Context(), depositID) + if err != nil { + h.logger.Error("GetDirectDepositByID error", err) + return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + Message: "Failed to fetch direct deposit", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: fmt.Sprintf("Direct deposit #%d fetched successfully", depositID), + Data: deposit, + StatusCode: fiber.StatusOK, + Success: true, + }) +} + +// DeleteDirectDeposit godoc +// @Summary Delete a direct deposit +// @Description Deletes a direct deposit by its ID +// @Tags DirectDeposit +// @Accept json +// @Produce json +// @Param depositID path int true "Deposit ID" +// @Success 200 {object} domain.Response{data=string} +// @Failure 400 {object} domain.ErrorResponse +// @Failure 502 {object} domain.ErrorResponse +// @Router /api/v1/direct-deposits/{depositID} [delete] +func (h *Handler) DeleteDirectDeposit(c *fiber.Ctx) error { + depositID, err := strconv.Atoi(c.Params("depositID")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid deposit ID", + Error: err.Error(), + }) + } + + if err := h.directDepositSvc.DeleteDirectDeposit(c.Context(), depositID); err != nil { + h.logger.Error("DeleteDirectDeposit error", err) + return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + Message: "Failed to delete direct deposit", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: fmt.Sprintf("Direct deposit #%d deleted successfully", depositID), + Data: fmt.Sprintf("Deposit #%d deleted", depositID), + StatusCode: fiber.StatusOK, + Success: true, + }) +} diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index 6bf3104..ccf51e0 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -12,6 +12,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency" + directdeposit "github.com/SamuelTariku/FortuneBet-Backend/internal/services/direct_deposit" enetpulse "github.com/SamuelTariku/FortuneBet-Backend/internal/services/enet_pulse" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions" @@ -43,6 +44,7 @@ import ( ) type Handler struct { + directDepositSvc *directdeposit.Service orchestrationSvc *orchestration.Service enetPulseSvc *enetpulse.Service telebirrSvc *telebirr.TelebirrService @@ -84,6 +86,7 @@ type Handler struct { } func New( + directDepositSvc *directdeposit.Service, orchestrationSvc *orchestration.Service, enetPulseSvc *enetpulse.Service, telebirrSvc *telebirr.TelebirrService, @@ -124,6 +127,7 @@ func New( mongoLoggerSvc *zap.Logger, ) *Handler { return &Handler{ + directDepositSvc: directDepositSvc, orchestrationSvc: orchestrationSvc, enetPulseSvc: enetPulseSvc, telebirrSvc: telebirrSvc, diff --git a/internal/web_server/handlers/virtual_games_hadlers.go b/internal/web_server/handlers/virtual_games_hadlers.go index 1921ba6..dbee2f8 100644 --- a/internal/web_server/handlers/virtual_games_hadlers.go +++ b/internal/web_server/handlers/virtual_games_hadlers.go @@ -346,17 +346,17 @@ func (h *Handler) HandleBet(c *fiber.Ctx) error { res, err := h.veliVirtualGameSvc.ProcessBet(c.Context(), req) if err != nil { if strings.Contains(err.Error(), veli.ErrDuplicateTransaction.Error()) { - return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{ + return c.Status(fiber.StatusConflict).JSON(domain.CallbackErrorResponse{ // Message: "Duplicate transaction", Error: veli.ErrDuplicateTransaction.Error(), }) } else if strings.Contains(err.Error(), veli.ErrInsufficientBalance.Error()) { - return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{ + return c.Status(fiber.StatusConflict).JSON(domain.CallbackErrorResponse{ // Message: "Wallet balance is insufficient", Error: veli.ErrInsufficientBalance.Error(), }) } else if strings.Contains(err.Error(), veli.ErrPlayerNotFound.Error()) { - return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{ + return c.Status(fiber.StatusConflict).JSON(domain.CallbackErrorResponse{ // Message: "User not found", Error: veli.ErrPlayerNotFound.Error(), }) @@ -473,12 +473,12 @@ func (h *Handler) HandleWin(c *fiber.Ctx) error { res, err := h.veliVirtualGameSvc.ProcessWin(c.Context(), req) if err != nil { if errors.Is(err, veli.ErrDuplicateTransaction) { - return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{ + return c.Status(fiber.StatusConflict).JSON(domain.CallbackErrorResponse{ // Message: "Duplicate transaction", Error: veli.ErrDuplicateTransaction.Error(), }) } else if errors.Is(err, veli.ErrPlayerNotFound) { - return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{ + return c.Status(fiber.StatusConflict).JSON(domain.CallbackErrorResponse{ // Message: "Duplicate transaction", Error: veli.ErrPlayerNotFound.Error(), }) @@ -550,12 +550,12 @@ func (h *Handler) HandleCancel(c *fiber.Ctx) error { res, err := h.veliVirtualGameSvc.ProcessCancel(c.Context(), req) if err != nil { if strings.Contains(err.Error(), veli.ErrDuplicateTransaction.Error()) { - return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{ + return c.Status(fiber.StatusConflict).JSON(domain.CallbackErrorResponse{ // Message: "Duplicate transaction", Error: veli.ErrDuplicateTransaction.Error(), }) } else if strings.Contains(err.Error(), veli.ErrPlayerNotFound.Error()) { - return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{ + return c.Status(fiber.StatusConflict).JSON(domain.CallbackErrorResponse{ // Message: "User not found", Error: veli.ErrPlayerNotFound.Error(), }) diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 9ffc580..883f723 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.directDepositSvc, a.orchestrationSvc, a.enetPulseSvc, a.telebirrSvc, @@ -103,9 +104,12 @@ func (a *App) initAppRoutes() { groupV1.Get("/tenant", a.authMiddleware, h.GetTenantSlugByToken) //Direct_deposit - groupV1.Post("/direct_deposit", a.authMiddleware, h.InitiateDirectDeposit) - groupV1.Post("/direct_deposit/verify", a.authMiddleware, h.VerifyDirectDeposit) - groupV1.Get("/direct_deposit/pending", a.authMiddleware, h.GetPendingDirectDeposits) + groupV1.Post("/direct-deposits", a.authMiddleware, h.CreateDirectDeposit) + groupV1.Post("/direct-deposits/:depositID/approve", a.authMiddleware, h.ApproveDirectDeposit) + groupV1.Post("/direct-deposits/:depositID/reject", a.authMiddleware, h.RejectDirectDeposit) + groupV1.Get("/direct-deposits", a.authMiddleware, h.GetDirectDepositsByStatus) + groupV1.Get("/direct-deposits/:depositID", a.authMiddleware, h.GetDirectDepositByID) + groupV1.Delete("/direct-deposits/:depositID", a.authMiddleware, h.DeleteDirectDeposit) // Swagger a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler())