From 2b9302b10bf849c6175471239832dc72258b1aa8 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Fri, 11 Jul 2025 15:48:59 +0300 Subject: [PATCH] transaction maker-checker fixes --- cmd/main.go | 2 + db/query/institutions.sql | 27 + db/query/wallet.sql | 8 +- docs/docs.go | 1744 +++++++++++++---- docs/swagger.json | 1744 +++++++++++++---- docs/swagger.yaml | 1173 ++++++++--- gen/db/institutions.sql.go | 56 +- go.mod | 4 +- internal/domain/bank.go | 1 - internal/domain/chapa.go | 3 + internal/domain/common.go | 7 + internal/domain/institutions.go | 9 +- internal/domain/mongoLogs.go | 8 + internal/domain/notification.go | 31 +- internal/domain/report.go | 6 + internal/domain/transfer.go | 24 + internal/repository/institutions.go | 67 +- internal/repository/transfer.go | 4 + internal/services/chapa/service.go | 22 +- internal/services/institutions/service.go | 30 +- internal/services/report/service.go | 53 +- internal/services/result/eval.go | 90 +- internal/services/result/service.go | 6 +- internal/services/result/sports_eval.go | 36 +- internal/services/user/common.go | 2 +- internal/services/wallet/port.go | 26 +- internal/services/wallet/service.go | 9 +- internal/services/wallet/transfer.go | 211 ++ internal/web_server/handlers/institutions.go | 62 +- internal/web_server/handlers/mongoLogger.go | 93 +- internal/web_server/handlers/report.go | 99 +- internal/web_server/handlers/shop_handler.go | 16 +- .../handlers/virtual_games_hadlers.go | 354 +++- test.html | 112 ++ 34 files changed, 4833 insertions(+), 1306 deletions(-) delete mode 100644 internal/domain/bank.go create mode 100644 test.html diff --git a/cmd/main.go b/cmd/main.go index 497c02e..199f1f5 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -110,11 +110,13 @@ func main() { notificationSvc := notificationservice.New(notificationRepo, logger, cfg) var notificatioStore notificationservice.NotificationStore + // var userStore user.UserStore walletSvc := wallet.NewService( wallet.WalletStore(store), wallet.TransferStore(store), notificatioStore, + // userStore, notificationSvc, logger, ) diff --git a/db/query/institutions.sql b/db/query/institutions.sql index d6faada..865524b 100644 --- a/db/query/institutions.sql +++ b/db/query/institutions.sql @@ -35,6 +35,33 @@ WHERE ( AND ( is_active = sqlc.narg('is_active') OR sqlc.narg('is_active') IS NULL + ) + AND ( + name ILIKE '%' || sqlc.narg('search_term') || '%' + OR sqlc.narg('search_term') IS NULL + ) + AND ( + code ILIKE '%' || sqlc.narg('search_term') || '%' + OR sqlc.narg('search_term') IS NULL + ) +ORDER BY name ASC +LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); + +-- name: CountBanks :one +SELECT COUNT(*) +FROM banks +WHERE ( + country_id = $1 + OR $1 IS NULL + ) + AND ( + is_active = $2 + OR $2 IS NULL + ) + AND ( + name ILIKE '%' || $3 || '%' + OR code ILIKE '%' || $3 || '%' + OR $3 IS NULL ); -- name: UpdateBank :one diff --git a/db/query/wallet.sql b/db/query/wallet.sql index 79028e5..d22effe 100644 --- a/db/query/wallet.sql +++ b/db/query/wallet.sql @@ -65,4 +65,10 @@ LIMIT 1; SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at FROM branches WHERE wallet_id = $1 -LIMIT 1; \ No newline at end of file +LIMIT 1; + +-- -- name: GetCustomerByWalletID :one +-- SELECT id, first_name, last_name, email, phone_number,email_verified,phone_verified,company_id,suspended +-- FROM users +-- WHERE wallet_id = $1 +-- LIMIT 1; \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 5b797dd..860e921 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -129,6 +129,41 @@ const docTemplate = `{ } } }, + "/admin-company": { + "get": { + "description": "Gets a single company by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "company" + ], + "summary": "Gets company by id", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CompanyRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/admin/{id}": { "get": { "description": "Get a single admin by id", @@ -312,15 +347,53 @@ const docTemplate = `{ "tags": [ "Institutions - Banks" ], - "summary": "List all banks", + "summary": "List all banks with pagination and filtering", + "parameters": [ + { + "type": "integer", + "description": "Filter by country ID", + "name": "country_id", + "in": "query" + }, + { + "type": "boolean", + "description": "Filter by active status", + "name": "is_active", + "in": "query" + }, + { + "type": "string", + "description": "Search term for bank name or code", + "name": "search", + "in": "query" + }, + { + "type": "integer", + "default": 1, + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "maximum": 100, + "type": "integer", + "default": 50, + "description": "Items per page", + "name": "page_size", + "in": "query" + } + ], "responses": { "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.Bank" - } + "$ref": "#/definitions/domain.InstResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" } }, "500": { @@ -1071,22 +1144,53 @@ const docTemplate = `{ }, "/api/v1/logs": { "get": { - "description": "Fetches the 100 most recent application logs from MongoDB", + "description": "Fetches application logs from MongoDB with pagination, level filtering, and search", "produces": [ "application/json" ], "tags": [ "Logs" ], - "summary": "Retrieve latest application logs", + "summary": "Retrieve application logs with filtering and pagination", + "parameters": [ + { + "type": "string", + "description": "Filter logs by level (debug, info, warn, error, dpanic, panic, fatal)", + "name": "level", + "in": "query" + }, + { + "type": "string", + "description": "Search term to match against message or fields", + "name": "search", + "in": "query" + }, + { + "type": "integer", + "default": 1, + "description": "Page number for pagination (default: 1)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 50, + "description": "Number of items per page (default: 50, max: 100)", + "name": "limit", + "in": "query" + } + ], "responses": { "200": { - "description": "List of application logs", + "description": "Paginated list of application logs", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.LogEntry" - } + "$ref": "#/definitions/domain.LogResponse" + } + }, + "400": { + "description": "Invalid request parameters", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" } }, "500": { @@ -1147,7 +1251,7 @@ const docTemplate = `{ }, "/api/v1/report-files/list": { "get": { - "description": "Returns a list of all generated report CSV files available for download", + "description": "Returns a paginated list of generated report CSV files with search capability", "produces": [ "application/json" ], @@ -1155,26 +1259,39 @@ const docTemplate = `{ "Reports" ], "summary": "List available report CSV files", + "parameters": [ + { + "type": "string", + "description": "Search term to filter filenames", + "name": "search", + "in": "query" + }, + { + "type": "integer", + "default": 1, + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "Items per page (default: 20, max: 100)", + "name": "limit", + "in": "query" + } + ], "responses": { "200": { - "description": "List of CSV report filenames", + "description": "Paginated list of CSV report filenames", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - ] + "$ref": "#/definitions/domain.PaginatedFileResponse" + } + }, + "400": { + "description": "Invalid pagination parameters", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" } }, "500": { @@ -1726,6 +1843,51 @@ 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" + } + } + } + } + }, "/auth/login": { "post": { "description": "Login customer", @@ -2896,6 +3058,199 @@ const docTemplate = `{ } } }, + "/customer": { + "get": { + "description": "Get all Customers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "customer" + ], + "summary": "Get all Customers", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CustomersRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/customer/{id}": { + "get": { + "description": "Get a single customer by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "customer" + ], + "summary": "Get customer by id", + "parameters": [ + { + "type": "integer", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CustomersRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "description": "Update Customers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "customer" + ], + "summary": "Update Customers", + "parameters": [ + { + "description": "Update Customers", + "name": "Customers", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateCustomerReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/customerWallet": { + "get": { + "description": "Retrieve all customer wallets", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wallet" + ], + "summary": "Get all customer wallets", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.CustomerWalletRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/events": { "get": { "description": "Retrieve all upcoming events from the database", @@ -3571,8 +3926,8 @@ const docTemplate = `{ } } }, - "502": { - "description": "Bad Gateway", + "500": { + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/domain.ErrorResponse" } @@ -3891,6 +4246,459 @@ const docTemplate = `{ } } }, + "/shop/bet": { + "post": { + "description": "Create bet at branch", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transaction" + ], + "summary": "Create bet at branch", + "parameters": [ + { + "description": "create bet", + "name": "createBet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.ShopBetReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ShopTransactionRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/shop/bet/{id}": { + "get": { + "description": "Cashout bet at branch", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transaction" + ], + "summary": "Cashout bet at branch", + "parameters": [ + { + "description": "cashout bet", + "name": "createBet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CashoutReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ShopTransactionRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/shop/bet/{id}/cashout": { + "post": { + "description": "Cashout bet at branch", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transaction" + ], + "summary": "Cashout bet at branch", + "parameters": [ + { + "description": "cashout bet", + "name": "cashoutBet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CashoutReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ShopTransactionRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/shop/cashout": { + "post": { + "description": "Cashout bet by cashoutID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transaction" + ], + "summary": "Cashout bet by cashoutID", + "parameters": [ + { + "description": "cashout bet", + "name": "cashoutBet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CashoutReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ShopTransactionRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/shop/cashout/{id}": { + "get": { + "description": "Cashout bet at branch", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transaction" + ], + "summary": "Cashout bet at branch", + "parameters": [ + { + "description": "cashout bet", + "name": "createBet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CashoutReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ShopTransactionRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/shop/deposit": { + "post": { + "description": "Transfers money from branch wallet to customer wallet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transaction" + ], + "summary": "Shop deposit into customer wallet", + "parameters": [ + { + "description": "ShopDepositReq", + "name": "transferToWallet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.ShopDepositReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.TransferWalletRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/shop/transaction": { + "get": { + "description": "Gets all the transactions", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transaction" + ], + "summary": "Gets all transactions", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ShopTransactionRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/shop/transaction/{id}": { + "get": { + "description": "Gets a single transaction by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transaction" + ], + "summary": "Gets transaction by id", + "parameters": [ + { + "type": "integer", + "description": "Transaction ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ShopTransactionRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "description": "Updates the verified status of a transaction", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transaction" + ], + "summary": "Updates the verified field of a transaction", + "parameters": [ + { + "type": "integer", + "description": "Transaction ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updates Transaction Verification", + "name": "updateVerified", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.UpdateTransactionVerifiedReq" + } + } + ], + "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" + } + } + } + } + }, + "/shop/transaction/{id}/bet": { + "get": { + "description": "Gets a single shop bet by transaction id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transaction" + ], + "summary": "Gets shop bet by transaction id", + "parameters": [ + { + "type": "integer", + "description": "Transaction ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ShopTransactionRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/sport/bet": { "get": { "description": "Gets all the bets", @@ -4017,6 +4825,52 @@ const docTemplate = `{ } } }, + "/sport/bet/fastcode": { + "post": { + "description": "Creates a bet with fast code", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Create a bet with fast code", + "parameters": [ + { + "description": "Creates bet", + "name": "createBetWithFastCode", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CreateBetReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BetRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/sport/bet/{id}": { "get": { "description": "Gets a single bet by id", @@ -4427,7 +5281,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/domain.UpcomingEvent" + "$ref": "#/definitions/handlers.TopLeague" } } }, @@ -4440,183 +5294,6 @@ const docTemplate = `{ } } }, - "/transaction": { - "get": { - "description": "Gets all the transactions", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "transaction" - ], - "summary": "Gets all transactions", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/handlers.TransactionRes" - } - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - }, - "post": { - "description": "Creates a transaction", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "transaction" - ], - "summary": "Create a transaction", - "parameters": [ - { - "description": "Creates transaction", - "name": "createBet", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handlers.CreateTransactionReq" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handlers.TransactionRes" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/transaction/{id}": { - "get": { - "description": "Gets a single transaction by id", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "transaction" - ], - "summary": "Gets transaction by id", - "parameters": [ - { - "type": "integer", - "description": "Transaction ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handlers.TransactionRes" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - }, - "put": { - "description": "Updates the verified status of a transaction", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "transaction" - ], - "summary": "Updates the verified field of a transaction", - "parameters": [ - { - "type": "integer", - "description": "Transaction ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Updates Transaction Verification", - "name": "updateVerified", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handlers.UpdateTransactionVerifiedReq" - } - } - ], - "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" - } - } - } - } - }, "/transfer/refill/:id": { "post": { "description": "Super Admin route to refill a wallet", @@ -5769,6 +6446,42 @@ const docTemplate = `{ } } }, + "domain.CashoutReq": { + "type": "object", + "properties": { + "account_name": { + "type": "string" + }, + "account_number": { + "type": "string" + }, + "bank_code": { + "type": "string" + }, + "beneficiary_name": { + "type": "string" + }, + "branch_id": { + "type": "integer", + "example": 1 + }, + "cashout_id": { + "type": "string", + "example": "1234" + }, + "payment_option": { + "allOf": [ + { + "$ref": "#/definitions/domain.PaymentOption" + } + ], + "example": 1 + }, + "reference_number": { + "type": "string" + } + } + }, "domain.ChapaDepositRequestPayload": { "type": "object", "required": [ @@ -5890,14 +6603,6 @@ const docTemplate = `{ "phone_number": { "type": "string", "example": "1234567890" - }, - "status": { - "allOf": [ - { - "$ref": "#/definitions/domain.OutcomeStatus" - } - ], - "example": 1 } } }, @@ -6364,6 +7069,28 @@ const docTemplate = `{ } } }, + "domain.InstResponse": { + "type": "object", + "properties": { + "data": { + "description": "Changed to interface{} for flexibility" + }, + "message": { + "type": "string" + }, + "pagination": { + "description": "Made pointer and optional", + "allOf": [ + { + "$ref": "#/definitions/domain.Pagination" + } + ] + }, + "status": { + "type": "string" + } + } + }, "domain.League": { "type": "object", "properties": { @@ -6427,6 +7154,23 @@ const docTemplate = `{ } } }, + "domain.LogResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.LogEntry" + } + }, + "message": { + "type": "string" + }, + "pagination": { + "$ref": "#/definitions/domain.Pagination" + } + } + }, "domain.Odd": { "type": "object", "properties": { @@ -6478,17 +7222,6 @@ const docTemplate = `{ } } }, - "domain.OtpProvider": { - "type": "string", - "enum": [ - "twilio", - "aformessage" - ], - "x-enum-varnames": [ - "TwilioSms", - "AfroMessage" - ] - }, "domain.OutcomeStatus": { "type": "integer", "enum": [ @@ -6513,6 +7246,46 @@ const docTemplate = `{ "OUTCOME_STATUS_ERROR" ] }, + "domain.PaginatedFileResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "string" + } + }, + "message": { + "type": "string" + }, + "pagination": { + "$ref": "#/definitions/domain.Pagination" + }, + "status_code": { + "type": "integer" + }, + "success": { + "type": "boolean" + } + } + }, + "domain.Pagination": { + "type": "object", + "properties": { + "current_page": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "total_pages": { + "type": "integer" + } + } + }, "domain.PaginationMeta": { "type": "object", "properties": { @@ -6811,6 +7584,211 @@ const docTemplate = `{ "RoleCashier" ] }, + "domain.ShopBetReq": { + "type": "object", + "properties": { + "account_name": { + "type": "string" + }, + "account_number": { + "type": "string" + }, + "amount": { + "type": "number", + "example": 100 + }, + "bank_code": { + "type": "string" + }, + "beneficiary_name": { + "type": "string" + }, + "bet_id": { + "type": "integer", + "example": 1 + }, + "branch_id": { + "type": "integer", + "example": 1 + }, + "full_name": { + "type": "string", + "example": "John Smith" + }, + "outcomes": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.CreateBetOutcomeReq" + } + }, + "payment_option": { + "allOf": [ + { + "$ref": "#/definitions/domain.PaymentOption" + } + ], + "example": 1 + }, + "phone_number": { + "type": "string", + "example": "0911111111" + }, + "reference_number": { + "type": "string" + } + } + }, + "domain.ShopDepositReq": { + "type": "object", + "properties": { + "account_name": { + "type": "string" + }, + "account_number": { + "type": "string" + }, + "amount": { + "type": "number", + "example": 100 + }, + "bank_code": { + "description": "FullName string ` + "`" + `json:\"full_name\" example:\"John Smith\"` + "`" + `\nPhoneNumber string ` + "`" + `json:\"phone_number\" example:\"0911111111\"` + "`" + `", + "type": "string" + }, + "beneficiary_name": { + "type": "string" + }, + "branch_id": { + "type": "integer", + "example": 1 + }, + "customer_id": { + "type": "integer", + "example": 1 + }, + "payment_option": { + "allOf": [ + { + "$ref": "#/definitions/domain.PaymentOption" + } + ], + "example": 1 + }, + "reference_number": { + "type": "string" + } + } + }, + "domain.ShopTransactionRes": { + "type": "object", + "properties": { + "account_name": { + "type": "string" + }, + "account_number": { + "type": "string" + }, + "amount": { + "type": "number", + "example": 100 + }, + "approved_by": { + "type": "integer", + "example": 1 + }, + "approver_first_name": { + "type": "string", + "example": "John" + }, + "approver_last_name": { + "type": "string", + "example": "Smith" + }, + "approver_phone_number": { + "type": "string", + "example": "0911111111" + }, + "bank_code": { + "type": "string" + }, + "beneficiary_name": { + "type": "string" + }, + "branch_id": { + "type": "integer", + "example": 1 + }, + "branch_location": { + "type": "string", + "example": "Branch Location" + }, + "branch_name": { + "type": "string", + "example": "Branch Name" + }, + "cashier_name": { + "type": "string", + "example": "John Smith" + }, + "company_id": { + "type": "integer", + "example": 1 + }, + "created_at": { + "type": "string" + }, + "creator_first_name": { + "type": "string", + "example": "John" + }, + "creator_last_name": { + "type": "string", + "example": "Smith" + }, + "creator_phone_number": { + "type": "string", + "example": "0911111111" + }, + "full_name": { + "type": "string", + "example": "John Smith" + }, + "id": { + "type": "integer", + "example": 1 + }, + "payment_option": { + "allOf": [ + { + "$ref": "#/definitions/domain.PaymentOption" + } + ], + "example": 1 + }, + "phone_number": { + "type": "string", + "example": "0911111111" + }, + "reference_number": { + "type": "string" + }, + "type": { + "type": "integer", + "example": 1 + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "integer", + "example": 1 + }, + "verified": { + "type": "boolean", + "example": true + } + } + }, "domain.TicketOutcome": { "type": "object", "properties": { @@ -7031,10 +8009,18 @@ const docTemplate = `{ "type": "integer", "example": 1 }, + "is_active": { + "type": "boolean", + "example": false + }, "is_self_owned": { "type": "boolean", "example": false }, + "is_wallet_active": { + "type": "boolean", + "example": false + }, "location": { "type": "string", "example": "Addis Ababa" @@ -7085,6 +8071,10 @@ const docTemplate = `{ "type": "integer", "example": 1 }, + "is_active": { + "type": "boolean", + "example": false + }, "is_self_owned": { "type": "boolean", "example": false @@ -7319,62 +8309,6 @@ const docTemplate = `{ } } }, - "handlers.CreateTransactionReq": { - "type": "object", - "properties": { - "account_name": { - "type": "string" - }, - "account_number": { - "type": "string" - }, - "amount": { - "type": "number", - "example": 100 - }, - "bank_code": { - "type": "string" - }, - "beneficiary_name": { - "type": "string" - }, - "bet_id": { - "type": "integer", - "example": 1 - }, - "branch_id": { - "type": "integer", - "example": 1 - }, - "cashout_id": { - "type": "string", - "example": "191212" - }, - "full_name": { - "type": "string", - "example": "John Smith" - }, - "payment_option": { - "allOf": [ - { - "$ref": "#/definitions/domain.PaymentOption" - } - ], - "example": 1 - }, - "phone_number": { - "type": "string", - "example": "0911111111" - }, - "reference_number": { - "type": "string" - }, - "type": { - "type": "integer", - "example": 1 - } - } - }, "handlers.CreateTransferReq": { "type": "object", "properties": { @@ -7398,10 +8332,22 @@ const docTemplate = `{ "type": "integer", "example": 1 }, + "first_name": { + "type": "string", + "example": "John" + }, "id": { "type": "integer", "example": 1 }, + "last_name": { + "type": "string", + "example": "Smith" + }, + "phone_number": { + "type": "string", + "example": "0911111111" + }, "regular_balance": { "type": "number", "example": 100 @@ -7410,6 +8356,10 @@ const docTemplate = `{ "type": "integer", "example": 1 }, + "regular_is_active": { + "type": "boolean", + "example": true + }, "regular_updated_at": { "type": "string" }, @@ -7421,11 +8371,59 @@ const docTemplate = `{ "type": "integer", "example": 1 }, + "static_is_active": { + "type": "boolean", + "example": true + }, "static_updated_at": { "type": "string" } } }, + "handlers.CustomersRes": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "email_verified": { + "type": "boolean" + }, + "first_name": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_login": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "phone_number": { + "type": "string" + }, + "phone_verified": { + "type": "boolean" + }, + "role": { + "$ref": "#/definitions/domain.Role" + }, + "suspended": { + "type": "boolean" + }, + "suspended_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "handlers.GetCashierRes": { "type": "object", "properties": { @@ -7528,9 +8526,6 @@ const docTemplate = `{ }, "handlers.RegisterCodeReq": { "type": "object", - "required": [ - "provider" - ], "properties": { "email": { "type": "string", @@ -7539,22 +8534,11 @@ const docTemplate = `{ "phone_number": { "type": "string", "example": "1234567890" - }, - "provider": { - "allOf": [ - { - "$ref": "#/definitions/domain.OtpProvider" - } - ], - "example": "twilio" } } }, "handlers.RegisterUserReq": { "type": "object", - "required": [ - "provider" - ], "properties": { "email": { "type": "string", @@ -7580,14 +8564,6 @@ const docTemplate = `{ "type": "string", "example": "1234567890" }, - "provider": { - "allOf": [ - { - "$ref": "#/definitions/domain.OtpProvider" - } - ], - "example": "twilio" - }, "referal_code": { "type": "string", "example": "ABC123" @@ -7596,9 +8572,6 @@ const docTemplate = `{ }, "handlers.ResetCodeReq": { "type": "object", - "required": [ - "provider" - ], "properties": { "email": { "type": "string", @@ -7607,14 +8580,6 @@ const docTemplate = `{ "phone_number": { "type": "string", "example": "1234567890" - }, - "provider": { - "allOf": [ - { - "$ref": "#/definitions/domain.OtpProvider" - } - ], - "example": "twilio" } } }, @@ -7683,101 +8648,26 @@ const docTemplate = `{ } } }, - "handlers.TransactionRes": { + "handlers.TopLeague": { "type": "object", "properties": { - "account_name": { + "events": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.UpcomingEvent" + } + }, + "league_cc": { "type": "string" }, - "account_number": { + "league_id": { + "type": "integer" + }, + "league_name": { "type": "string" }, - "amount": { - "type": "number", - "example": 100 - }, - "approved_by": { - "type": "integer", - "example": 1 - }, - "approver_name": { - "type": "string", - "example": "John Smith" - }, - "bank_code": { - "type": "string" - }, - "beneficiary_name": { - "type": "string" - }, - "bet_id": { - "type": "integer", - "example": 1 - }, - "branch_id": { - "type": "integer", - "example": 1 - }, - "branch_location": { - "type": "string", - "example": "Branch Location" - }, - "branch_name": { - "type": "string", - "example": "Branch Name" - }, - "cashier_id": { - "type": "integer", - "example": 1 - }, - "cashier_name": { - "type": "string", - "example": "John Smith" - }, - "company_id": { - "type": "integer", - "example": 1 - }, - "created_at": { - "type": "string" - }, - "full_name": { - "type": "string", - "example": "John Smith" - }, - "id": { - "type": "integer", - "example": 1 - }, - "number_of_outcomes": { - "type": "integer", - "example": 1 - }, - "payment_option": { - "allOf": [ - { - "$ref": "#/definitions/domain.PaymentOption" - } - ], - "example": 1 - }, - "phone_number": { - "type": "string", - "example": "0911111111" - }, - "reference_number": { - "type": "string" - }, - "type": { - "type": "integer", - "example": 1 - }, - "updated_at": { - "type": "string" - }, - "verified": { - "type": "boolean", - "example": true + "league_sport_id": { + "type": "integer" } } }, @@ -7787,15 +8677,27 @@ const docTemplate = `{ "amount": { "type": "number" }, - "cashier_id": { + "created_at": { + "type": "string" + }, + "depositor_first_name": { + "type": "string" + }, + "depositor_id": { "type": "integer" }, - "created_at": { + "depositor_last_name": { + "type": "string" + }, + "depositor_phone_number": { "type": "string" }, "id": { "type": "integer" }, + "message": { + "type": "string" + }, "payment_method": { "type": "string" }, @@ -7853,7 +8755,6 @@ const docTemplate = `{ "handlers.UpdateUserSuspendReq": { "type": "object", "required": [ - "suspended", "user_id" ], "properties": { @@ -7987,7 +8888,7 @@ const docTemplate = `{ }, "game_id": { "type": "string", - "example": "crash_001" + "example": "1" }, "mode": { "type": "string", @@ -8108,6 +9009,27 @@ const docTemplate = `{ } } }, + "handlers.updateCustomerReq": { + "type": "object", + "properties": { + "company_id": { + "type": "integer", + "example": 1 + }, + "first_name": { + "type": "string", + "example": "John" + }, + "last_name": { + "type": "string", + "example": "Doe" + }, + "suspended": { + "type": "boolean", + "example": false + } + } + }, "handlers.updateManagerReq": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 6bc23d6..887186f 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -121,6 +121,41 @@ } } }, + "/admin-company": { + "get": { + "description": "Gets a single company by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "company" + ], + "summary": "Gets company by id", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CompanyRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/admin/{id}": { "get": { "description": "Get a single admin by id", @@ -304,15 +339,53 @@ "tags": [ "Institutions - Banks" ], - "summary": "List all banks", + "summary": "List all banks with pagination and filtering", + "parameters": [ + { + "type": "integer", + "description": "Filter by country ID", + "name": "country_id", + "in": "query" + }, + { + "type": "boolean", + "description": "Filter by active status", + "name": "is_active", + "in": "query" + }, + { + "type": "string", + "description": "Search term for bank name or code", + "name": "search", + "in": "query" + }, + { + "type": "integer", + "default": 1, + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "maximum": 100, + "type": "integer", + "default": 50, + "description": "Items per page", + "name": "page_size", + "in": "query" + } + ], "responses": { "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.Bank" - } + "$ref": "#/definitions/domain.InstResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" } }, "500": { @@ -1063,22 +1136,53 @@ }, "/api/v1/logs": { "get": { - "description": "Fetches the 100 most recent application logs from MongoDB", + "description": "Fetches application logs from MongoDB with pagination, level filtering, and search", "produces": [ "application/json" ], "tags": [ "Logs" ], - "summary": "Retrieve latest application logs", + "summary": "Retrieve application logs with filtering and pagination", + "parameters": [ + { + "type": "string", + "description": "Filter logs by level (debug, info, warn, error, dpanic, panic, fatal)", + "name": "level", + "in": "query" + }, + { + "type": "string", + "description": "Search term to match against message or fields", + "name": "search", + "in": "query" + }, + { + "type": "integer", + "default": 1, + "description": "Page number for pagination (default: 1)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 50, + "description": "Number of items per page (default: 50, max: 100)", + "name": "limit", + "in": "query" + } + ], "responses": { "200": { - "description": "List of application logs", + "description": "Paginated list of application logs", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.LogEntry" - } + "$ref": "#/definitions/domain.LogResponse" + } + }, + "400": { + "description": "Invalid request parameters", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" } }, "500": { @@ -1139,7 +1243,7 @@ }, "/api/v1/report-files/list": { "get": { - "description": "Returns a list of all generated report CSV files available for download", + "description": "Returns a paginated list of generated report CSV files with search capability", "produces": [ "application/json" ], @@ -1147,26 +1251,39 @@ "Reports" ], "summary": "List available report CSV files", + "parameters": [ + { + "type": "string", + "description": "Search term to filter filenames", + "name": "search", + "in": "query" + }, + { + "type": "integer", + "default": 1, + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "Items per page (default: 20, max: 100)", + "name": "limit", + "in": "query" + } + ], "responses": { "200": { - "description": "List of CSV report filenames", + "description": "Paginated list of CSV report filenames", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - ] + "$ref": "#/definitions/domain.PaginatedFileResponse" + } + }, + "400": { + "description": "Invalid pagination parameters", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" } }, "500": { @@ -1718,6 +1835,51 @@ } } }, + "/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" + } + } + } + } + }, "/auth/login": { "post": { "description": "Login customer", @@ -2888,6 +3050,199 @@ } } }, + "/customer": { + "get": { + "description": "Get all Customers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "customer" + ], + "summary": "Get all Customers", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CustomersRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/customer/{id}": { + "get": { + "description": "Get a single customer by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "customer" + ], + "summary": "Get customer by id", + "parameters": [ + { + "type": "integer", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CustomersRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "description": "Update Customers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "customer" + ], + "summary": "Update Customers", + "parameters": [ + { + "description": "Update Customers", + "name": "Customers", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateCustomerReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/customerWallet": { + "get": { + "description": "Retrieve all customer wallets", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wallet" + ], + "summary": "Get all customer wallets", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.CustomerWalletRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/events": { "get": { "description": "Retrieve all upcoming events from the database", @@ -3563,8 +3918,8 @@ } } }, - "502": { - "description": "Bad Gateway", + "500": { + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/domain.ErrorResponse" } @@ -3883,6 +4238,459 @@ } } }, + "/shop/bet": { + "post": { + "description": "Create bet at branch", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transaction" + ], + "summary": "Create bet at branch", + "parameters": [ + { + "description": "create bet", + "name": "createBet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.ShopBetReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ShopTransactionRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/shop/bet/{id}": { + "get": { + "description": "Cashout bet at branch", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transaction" + ], + "summary": "Cashout bet at branch", + "parameters": [ + { + "description": "cashout bet", + "name": "createBet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CashoutReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ShopTransactionRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/shop/bet/{id}/cashout": { + "post": { + "description": "Cashout bet at branch", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transaction" + ], + "summary": "Cashout bet at branch", + "parameters": [ + { + "description": "cashout bet", + "name": "cashoutBet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CashoutReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ShopTransactionRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/shop/cashout": { + "post": { + "description": "Cashout bet by cashoutID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transaction" + ], + "summary": "Cashout bet by cashoutID", + "parameters": [ + { + "description": "cashout bet", + "name": "cashoutBet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CashoutReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ShopTransactionRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/shop/cashout/{id}": { + "get": { + "description": "Cashout bet at branch", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transaction" + ], + "summary": "Cashout bet at branch", + "parameters": [ + { + "description": "cashout bet", + "name": "createBet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CashoutReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ShopTransactionRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/shop/deposit": { + "post": { + "description": "Transfers money from branch wallet to customer wallet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transaction" + ], + "summary": "Shop deposit into customer wallet", + "parameters": [ + { + "description": "ShopDepositReq", + "name": "transferToWallet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.ShopDepositReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.TransferWalletRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/shop/transaction": { + "get": { + "description": "Gets all the transactions", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transaction" + ], + "summary": "Gets all transactions", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ShopTransactionRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/shop/transaction/{id}": { + "get": { + "description": "Gets a single transaction by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transaction" + ], + "summary": "Gets transaction by id", + "parameters": [ + { + "type": "integer", + "description": "Transaction ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ShopTransactionRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "description": "Updates the verified status of a transaction", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transaction" + ], + "summary": "Updates the verified field of a transaction", + "parameters": [ + { + "type": "integer", + "description": "Transaction ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updates Transaction Verification", + "name": "updateVerified", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.UpdateTransactionVerifiedReq" + } + } + ], + "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" + } + } + } + } + }, + "/shop/transaction/{id}/bet": { + "get": { + "description": "Gets a single shop bet by transaction id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transaction" + ], + "summary": "Gets shop bet by transaction id", + "parameters": [ + { + "type": "integer", + "description": "Transaction ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ShopTransactionRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/sport/bet": { "get": { "description": "Gets all the bets", @@ -4009,6 +4817,52 @@ } } }, + "/sport/bet/fastcode": { + "post": { + "description": "Creates a bet with fast code", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Create a bet with fast code", + "parameters": [ + { + "description": "Creates bet", + "name": "createBetWithFastCode", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CreateBetReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BetRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/sport/bet/{id}": { "get": { "description": "Gets a single bet by id", @@ -4419,7 +5273,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/domain.UpcomingEvent" + "$ref": "#/definitions/handlers.TopLeague" } } }, @@ -4432,183 +5286,6 @@ } } }, - "/transaction": { - "get": { - "description": "Gets all the transactions", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "transaction" - ], - "summary": "Gets all transactions", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/handlers.TransactionRes" - } - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - }, - "post": { - "description": "Creates a transaction", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "transaction" - ], - "summary": "Create a transaction", - "parameters": [ - { - "description": "Creates transaction", - "name": "createBet", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handlers.CreateTransactionReq" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handlers.TransactionRes" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/transaction/{id}": { - "get": { - "description": "Gets a single transaction by id", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "transaction" - ], - "summary": "Gets transaction by id", - "parameters": [ - { - "type": "integer", - "description": "Transaction ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handlers.TransactionRes" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - }, - "put": { - "description": "Updates the verified status of a transaction", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "transaction" - ], - "summary": "Updates the verified field of a transaction", - "parameters": [ - { - "type": "integer", - "description": "Transaction ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Updates Transaction Verification", - "name": "updateVerified", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handlers.UpdateTransactionVerifiedReq" - } - } - ], - "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" - } - } - } - } - }, "/transfer/refill/:id": { "post": { "description": "Super Admin route to refill a wallet", @@ -5761,6 +6438,42 @@ } } }, + "domain.CashoutReq": { + "type": "object", + "properties": { + "account_name": { + "type": "string" + }, + "account_number": { + "type": "string" + }, + "bank_code": { + "type": "string" + }, + "beneficiary_name": { + "type": "string" + }, + "branch_id": { + "type": "integer", + "example": 1 + }, + "cashout_id": { + "type": "string", + "example": "1234" + }, + "payment_option": { + "allOf": [ + { + "$ref": "#/definitions/domain.PaymentOption" + } + ], + "example": 1 + }, + "reference_number": { + "type": "string" + } + } + }, "domain.ChapaDepositRequestPayload": { "type": "object", "required": [ @@ -5882,14 +6595,6 @@ "phone_number": { "type": "string", "example": "1234567890" - }, - "status": { - "allOf": [ - { - "$ref": "#/definitions/domain.OutcomeStatus" - } - ], - "example": 1 } } }, @@ -6356,6 +7061,28 @@ } } }, + "domain.InstResponse": { + "type": "object", + "properties": { + "data": { + "description": "Changed to interface{} for flexibility" + }, + "message": { + "type": "string" + }, + "pagination": { + "description": "Made pointer and optional", + "allOf": [ + { + "$ref": "#/definitions/domain.Pagination" + } + ] + }, + "status": { + "type": "string" + } + } + }, "domain.League": { "type": "object", "properties": { @@ -6419,6 +7146,23 @@ } } }, + "domain.LogResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.LogEntry" + } + }, + "message": { + "type": "string" + }, + "pagination": { + "$ref": "#/definitions/domain.Pagination" + } + } + }, "domain.Odd": { "type": "object", "properties": { @@ -6470,17 +7214,6 @@ } } }, - "domain.OtpProvider": { - "type": "string", - "enum": [ - "twilio", - "aformessage" - ], - "x-enum-varnames": [ - "TwilioSms", - "AfroMessage" - ] - }, "domain.OutcomeStatus": { "type": "integer", "enum": [ @@ -6505,6 +7238,46 @@ "OUTCOME_STATUS_ERROR" ] }, + "domain.PaginatedFileResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "string" + } + }, + "message": { + "type": "string" + }, + "pagination": { + "$ref": "#/definitions/domain.Pagination" + }, + "status_code": { + "type": "integer" + }, + "success": { + "type": "boolean" + } + } + }, + "domain.Pagination": { + "type": "object", + "properties": { + "current_page": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "total_pages": { + "type": "integer" + } + } + }, "domain.PaginationMeta": { "type": "object", "properties": { @@ -6803,6 +7576,211 @@ "RoleCashier" ] }, + "domain.ShopBetReq": { + "type": "object", + "properties": { + "account_name": { + "type": "string" + }, + "account_number": { + "type": "string" + }, + "amount": { + "type": "number", + "example": 100 + }, + "bank_code": { + "type": "string" + }, + "beneficiary_name": { + "type": "string" + }, + "bet_id": { + "type": "integer", + "example": 1 + }, + "branch_id": { + "type": "integer", + "example": 1 + }, + "full_name": { + "type": "string", + "example": "John Smith" + }, + "outcomes": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.CreateBetOutcomeReq" + } + }, + "payment_option": { + "allOf": [ + { + "$ref": "#/definitions/domain.PaymentOption" + } + ], + "example": 1 + }, + "phone_number": { + "type": "string", + "example": "0911111111" + }, + "reference_number": { + "type": "string" + } + } + }, + "domain.ShopDepositReq": { + "type": "object", + "properties": { + "account_name": { + "type": "string" + }, + "account_number": { + "type": "string" + }, + "amount": { + "type": "number", + "example": 100 + }, + "bank_code": { + "description": "FullName string `json:\"full_name\" example:\"John Smith\"`\nPhoneNumber string `json:\"phone_number\" example:\"0911111111\"`", + "type": "string" + }, + "beneficiary_name": { + "type": "string" + }, + "branch_id": { + "type": "integer", + "example": 1 + }, + "customer_id": { + "type": "integer", + "example": 1 + }, + "payment_option": { + "allOf": [ + { + "$ref": "#/definitions/domain.PaymentOption" + } + ], + "example": 1 + }, + "reference_number": { + "type": "string" + } + } + }, + "domain.ShopTransactionRes": { + "type": "object", + "properties": { + "account_name": { + "type": "string" + }, + "account_number": { + "type": "string" + }, + "amount": { + "type": "number", + "example": 100 + }, + "approved_by": { + "type": "integer", + "example": 1 + }, + "approver_first_name": { + "type": "string", + "example": "John" + }, + "approver_last_name": { + "type": "string", + "example": "Smith" + }, + "approver_phone_number": { + "type": "string", + "example": "0911111111" + }, + "bank_code": { + "type": "string" + }, + "beneficiary_name": { + "type": "string" + }, + "branch_id": { + "type": "integer", + "example": 1 + }, + "branch_location": { + "type": "string", + "example": "Branch Location" + }, + "branch_name": { + "type": "string", + "example": "Branch Name" + }, + "cashier_name": { + "type": "string", + "example": "John Smith" + }, + "company_id": { + "type": "integer", + "example": 1 + }, + "created_at": { + "type": "string" + }, + "creator_first_name": { + "type": "string", + "example": "John" + }, + "creator_last_name": { + "type": "string", + "example": "Smith" + }, + "creator_phone_number": { + "type": "string", + "example": "0911111111" + }, + "full_name": { + "type": "string", + "example": "John Smith" + }, + "id": { + "type": "integer", + "example": 1 + }, + "payment_option": { + "allOf": [ + { + "$ref": "#/definitions/domain.PaymentOption" + } + ], + "example": 1 + }, + "phone_number": { + "type": "string", + "example": "0911111111" + }, + "reference_number": { + "type": "string" + }, + "type": { + "type": "integer", + "example": 1 + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "integer", + "example": 1 + }, + "verified": { + "type": "boolean", + "example": true + } + } + }, "domain.TicketOutcome": { "type": "object", "properties": { @@ -7023,10 +8001,18 @@ "type": "integer", "example": 1 }, + "is_active": { + "type": "boolean", + "example": false + }, "is_self_owned": { "type": "boolean", "example": false }, + "is_wallet_active": { + "type": "boolean", + "example": false + }, "location": { "type": "string", "example": "Addis Ababa" @@ -7077,6 +8063,10 @@ "type": "integer", "example": 1 }, + "is_active": { + "type": "boolean", + "example": false + }, "is_self_owned": { "type": "boolean", "example": false @@ -7311,62 +8301,6 @@ } } }, - "handlers.CreateTransactionReq": { - "type": "object", - "properties": { - "account_name": { - "type": "string" - }, - "account_number": { - "type": "string" - }, - "amount": { - "type": "number", - "example": 100 - }, - "bank_code": { - "type": "string" - }, - "beneficiary_name": { - "type": "string" - }, - "bet_id": { - "type": "integer", - "example": 1 - }, - "branch_id": { - "type": "integer", - "example": 1 - }, - "cashout_id": { - "type": "string", - "example": "191212" - }, - "full_name": { - "type": "string", - "example": "John Smith" - }, - "payment_option": { - "allOf": [ - { - "$ref": "#/definitions/domain.PaymentOption" - } - ], - "example": 1 - }, - "phone_number": { - "type": "string", - "example": "0911111111" - }, - "reference_number": { - "type": "string" - }, - "type": { - "type": "integer", - "example": 1 - } - } - }, "handlers.CreateTransferReq": { "type": "object", "properties": { @@ -7390,10 +8324,22 @@ "type": "integer", "example": 1 }, + "first_name": { + "type": "string", + "example": "John" + }, "id": { "type": "integer", "example": 1 }, + "last_name": { + "type": "string", + "example": "Smith" + }, + "phone_number": { + "type": "string", + "example": "0911111111" + }, "regular_balance": { "type": "number", "example": 100 @@ -7402,6 +8348,10 @@ "type": "integer", "example": 1 }, + "regular_is_active": { + "type": "boolean", + "example": true + }, "regular_updated_at": { "type": "string" }, @@ -7413,11 +8363,59 @@ "type": "integer", "example": 1 }, + "static_is_active": { + "type": "boolean", + "example": true + }, "static_updated_at": { "type": "string" } } }, + "handlers.CustomersRes": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "email_verified": { + "type": "boolean" + }, + "first_name": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_login": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "phone_number": { + "type": "string" + }, + "phone_verified": { + "type": "boolean" + }, + "role": { + "$ref": "#/definitions/domain.Role" + }, + "suspended": { + "type": "boolean" + }, + "suspended_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "handlers.GetCashierRes": { "type": "object", "properties": { @@ -7520,9 +8518,6 @@ }, "handlers.RegisterCodeReq": { "type": "object", - "required": [ - "provider" - ], "properties": { "email": { "type": "string", @@ -7531,22 +8526,11 @@ "phone_number": { "type": "string", "example": "1234567890" - }, - "provider": { - "allOf": [ - { - "$ref": "#/definitions/domain.OtpProvider" - } - ], - "example": "twilio" } } }, "handlers.RegisterUserReq": { "type": "object", - "required": [ - "provider" - ], "properties": { "email": { "type": "string", @@ -7572,14 +8556,6 @@ "type": "string", "example": "1234567890" }, - "provider": { - "allOf": [ - { - "$ref": "#/definitions/domain.OtpProvider" - } - ], - "example": "twilio" - }, "referal_code": { "type": "string", "example": "ABC123" @@ -7588,9 +8564,6 @@ }, "handlers.ResetCodeReq": { "type": "object", - "required": [ - "provider" - ], "properties": { "email": { "type": "string", @@ -7599,14 +8572,6 @@ "phone_number": { "type": "string", "example": "1234567890" - }, - "provider": { - "allOf": [ - { - "$ref": "#/definitions/domain.OtpProvider" - } - ], - "example": "twilio" } } }, @@ -7675,101 +8640,26 @@ } } }, - "handlers.TransactionRes": { + "handlers.TopLeague": { "type": "object", "properties": { - "account_name": { + "events": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.UpcomingEvent" + } + }, + "league_cc": { "type": "string" }, - "account_number": { + "league_id": { + "type": "integer" + }, + "league_name": { "type": "string" }, - "amount": { - "type": "number", - "example": 100 - }, - "approved_by": { - "type": "integer", - "example": 1 - }, - "approver_name": { - "type": "string", - "example": "John Smith" - }, - "bank_code": { - "type": "string" - }, - "beneficiary_name": { - "type": "string" - }, - "bet_id": { - "type": "integer", - "example": 1 - }, - "branch_id": { - "type": "integer", - "example": 1 - }, - "branch_location": { - "type": "string", - "example": "Branch Location" - }, - "branch_name": { - "type": "string", - "example": "Branch Name" - }, - "cashier_id": { - "type": "integer", - "example": 1 - }, - "cashier_name": { - "type": "string", - "example": "John Smith" - }, - "company_id": { - "type": "integer", - "example": 1 - }, - "created_at": { - "type": "string" - }, - "full_name": { - "type": "string", - "example": "John Smith" - }, - "id": { - "type": "integer", - "example": 1 - }, - "number_of_outcomes": { - "type": "integer", - "example": 1 - }, - "payment_option": { - "allOf": [ - { - "$ref": "#/definitions/domain.PaymentOption" - } - ], - "example": 1 - }, - "phone_number": { - "type": "string", - "example": "0911111111" - }, - "reference_number": { - "type": "string" - }, - "type": { - "type": "integer", - "example": 1 - }, - "updated_at": { - "type": "string" - }, - "verified": { - "type": "boolean", - "example": true + "league_sport_id": { + "type": "integer" } } }, @@ -7779,15 +8669,27 @@ "amount": { "type": "number" }, - "cashier_id": { + "created_at": { + "type": "string" + }, + "depositor_first_name": { + "type": "string" + }, + "depositor_id": { "type": "integer" }, - "created_at": { + "depositor_last_name": { + "type": "string" + }, + "depositor_phone_number": { "type": "string" }, "id": { "type": "integer" }, + "message": { + "type": "string" + }, "payment_method": { "type": "string" }, @@ -7845,7 +8747,6 @@ "handlers.UpdateUserSuspendReq": { "type": "object", "required": [ - "suspended", "user_id" ], "properties": { @@ -7979,7 +8880,7 @@ }, "game_id": { "type": "string", - "example": "crash_001" + "example": "1" }, "mode": { "type": "string", @@ -8100,6 +9001,27 @@ } } }, + "handlers.updateCustomerReq": { + "type": "object", + "properties": { + "company_id": { + "type": "integer", + "example": 1 + }, + "first_name": { + "type": "string", + "example": "John" + }, + "last_name": { + "type": "string", + "example": "Doe" + }, + "suspended": { + "type": "boolean", + "example": false + } + } + }, "handlers.updateManagerReq": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e1968f5..5a6a398 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -160,6 +160,29 @@ definitions: example: 2 type: integer type: object + domain.CashoutReq: + properties: + account_name: + type: string + account_number: + type: string + bank_code: + type: string + beneficiary_name: + type: string + branch_id: + example: 1 + type: integer + cashout_id: + example: "1234" + type: string + payment_option: + allOf: + - $ref: '#/definitions/domain.PaymentOption' + example: 1 + reference_number: + type: string + type: object domain.ChapaDepositRequestPayload: properties: amount: @@ -242,10 +265,6 @@ definitions: phone_number: example: "1234567890" type: string - status: - allOf: - - $ref: '#/definitions/domain.OutcomeStatus' - example: 1 type: object domain.CreateTicketOutcomeReq: properties: @@ -565,6 +584,19 @@ definitions: meta: $ref: '#/definitions/domain.PaginationMeta' type: object + domain.InstResponse: + properties: + data: + description: Changed to interface{} for flexibility + message: + type: string + pagination: + allOf: + - $ref: '#/definitions/domain.Pagination' + description: Made pointer and optional + status: + type: string + type: object domain.League: properties: bet365_id: @@ -609,6 +641,17 @@ definitions: timestamp: type: string type: object + domain.LogResponse: + properties: + data: + items: + $ref: '#/definitions/domain.LogEntry' + type: array + message: + type: string + pagination: + $ref: '#/definitions/domain.Pagination' + type: object domain.Odd: properties: category: @@ -643,14 +686,6 @@ definitions: source: type: string type: object - domain.OtpProvider: - enum: - - twilio - - aformessage - type: string - x-enum-varnames: - - TwilioSms - - AfroMessage domain.OutcomeStatus: enum: - 0 @@ -671,6 +706,32 @@ definitions: - OUTCOME_STATUS_VOID - OUTCOME_STATUS_HALF - OUTCOME_STATUS_ERROR + domain.PaginatedFileResponse: + properties: + data: + items: + type: string + type: array + message: + type: string + pagination: + $ref: '#/definitions/domain.Pagination' + status_code: + type: integer + success: + type: boolean + type: object + domain.Pagination: + properties: + current_page: + type: integer + limit: + type: integer + total: + type: integer + total_pages: + type: integer + type: object domain.PaginationMeta: properties: currentPage: @@ -875,6 +936,149 @@ definitions: - RoleBranchManager - RoleCustomer - RoleCashier + domain.ShopBetReq: + properties: + account_name: + type: string + account_number: + type: string + amount: + example: 100 + type: number + bank_code: + type: string + beneficiary_name: + type: string + bet_id: + example: 1 + type: integer + branch_id: + example: 1 + type: integer + full_name: + example: John Smith + type: string + outcomes: + items: + $ref: '#/definitions/domain.CreateBetOutcomeReq' + type: array + payment_option: + allOf: + - $ref: '#/definitions/domain.PaymentOption' + example: 1 + phone_number: + example: "0911111111" + type: string + reference_number: + type: string + type: object + domain.ShopDepositReq: + properties: + account_name: + type: string + account_number: + type: string + amount: + example: 100 + type: number + bank_code: + description: |- + FullName string `json:"full_name" example:"John Smith"` + PhoneNumber string `json:"phone_number" example:"0911111111"` + type: string + beneficiary_name: + type: string + branch_id: + example: 1 + type: integer + customer_id: + example: 1 + type: integer + payment_option: + allOf: + - $ref: '#/definitions/domain.PaymentOption' + example: 1 + reference_number: + type: string + type: object + domain.ShopTransactionRes: + properties: + account_name: + type: string + account_number: + type: string + amount: + example: 100 + type: number + approved_by: + example: 1 + type: integer + approver_first_name: + example: John + type: string + approver_last_name: + example: Smith + type: string + approver_phone_number: + example: "0911111111" + type: string + bank_code: + type: string + beneficiary_name: + type: string + branch_id: + example: 1 + type: integer + branch_location: + example: Branch Location + type: string + branch_name: + example: Branch Name + type: string + cashier_name: + example: John Smith + type: string + company_id: + example: 1 + type: integer + created_at: + type: string + creator_first_name: + example: John + type: string + creator_last_name: + example: Smith + type: string + creator_phone_number: + example: "0911111111" + type: string + full_name: + example: John Smith + type: string + id: + example: 1 + type: integer + payment_option: + allOf: + - $ref: '#/definitions/domain.PaymentOption' + example: 1 + phone_number: + example: "0911111111" + type: string + reference_number: + type: string + type: + example: 1 + type: integer + updated_at: + type: string + user_id: + example: 1 + type: integer + verified: + example: true + type: boolean + type: object domain.TicketOutcome: properties: away_team_name: @@ -1029,9 +1233,15 @@ definitions: id: example: 1 type: integer + is_active: + example: false + type: boolean is_self_owned: example: false type: boolean + is_wallet_active: + example: false + type: boolean location: example: Addis Ababa type: string @@ -1068,6 +1278,9 @@ definitions: id: example: 1 type: integer + is_active: + example: false + type: boolean is_self_owned: example: false type: boolean @@ -1236,44 +1449,6 @@ definitions: example: SportsBook type: string type: object - handlers.CreateTransactionReq: - properties: - account_name: - type: string - account_number: - type: string - amount: - example: 100 - type: number - bank_code: - type: string - beneficiary_name: - type: string - bet_id: - example: 1 - type: integer - branch_id: - example: 1 - type: integer - cashout_id: - example: "191212" - type: string - full_name: - example: John Smith - type: string - payment_option: - allOf: - - $ref: '#/definitions/domain.PaymentOption' - example: 1 - phone_number: - example: "0911111111" - type: string - reference_number: - type: string - type: - example: 1 - type: integer - type: object handlers.CreateTransferReq: properties: amount: @@ -1290,15 +1465,27 @@ definitions: customer_id: example: 1 type: integer + first_name: + example: John + type: string id: example: 1 type: integer + last_name: + example: Smith + type: string + phone_number: + example: "0911111111" + type: string regular_balance: example: 100 type: number regular_id: example: 1 type: integer + regular_is_active: + example: true + type: boolean regular_updated_at: type: string static_balance: @@ -1307,9 +1494,41 @@ definitions: static_id: example: 1 type: integer + static_is_active: + example: true + type: boolean static_updated_at: type: string type: object + handlers.CustomersRes: + properties: + created_at: + type: string + email: + type: string + email_verified: + type: boolean + first_name: + type: string + id: + type: integer + last_login: + type: string + last_name: + type: string + phone_number: + type: string + phone_verified: + type: boolean + role: + $ref: '#/definitions/domain.Role' + suspended: + type: boolean + suspended_at: + type: string + updated_at: + type: string + type: object handlers.GetCashierRes: properties: branch_id: @@ -1384,12 +1603,6 @@ definitions: phone_number: example: "1234567890" type: string - provider: - allOf: - - $ref: '#/definitions/domain.OtpProvider' - example: twilio - required: - - provider type: object handlers.RegisterUserReq: properties: @@ -1411,15 +1624,9 @@ definitions: phone_number: example: "1234567890" type: string - provider: - allOf: - - $ref: '#/definitions/domain.OtpProvider' - example: twilio referal_code: example: ABC123 type: string - required: - - provider type: object handlers.ResetCodeReq: properties: @@ -1429,12 +1636,6 @@ definitions: phone_number: example: "1234567890" type: string - provider: - allOf: - - $ref: '#/definitions/domain.OtpProvider' - example: twilio - required: - - provider type: object handlers.ResetPasswordReq: properties: @@ -1481,85 +1682,39 @@ definitions: example: SportsBook type: string type: object - handlers.TransactionRes: + handlers.TopLeague: properties: - account_name: + events: + items: + $ref: '#/definitions/domain.UpcomingEvent' + type: array + league_cc: type: string - account_number: - type: string - amount: - example: 100 - type: number - approved_by: - example: 1 + league_id: type: integer - approver_name: - example: John Smith + league_name: type: string - bank_code: - type: string - beneficiary_name: - type: string - bet_id: - example: 1 + league_sport_id: type: integer - branch_id: - example: 1 - type: integer - branch_location: - example: Branch Location - type: string - branch_name: - example: Branch Name - type: string - cashier_id: - example: 1 - type: integer - cashier_name: - example: John Smith - type: string - company_id: - example: 1 - type: integer - created_at: - type: string - full_name: - example: John Smith - type: string - id: - example: 1 - type: integer - number_of_outcomes: - example: 1 - type: integer - payment_option: - allOf: - - $ref: '#/definitions/domain.PaymentOption' - example: 1 - phone_number: - example: "0911111111" - type: string - reference_number: - type: string - type: - example: 1 - type: integer - updated_at: - type: string - verified: - example: true - type: boolean type: object handlers.TransferWalletRes: properties: amount: type: number - cashier_id: - type: integer created_at: type: string + depositor_first_name: + type: string + depositor_id: + type: integer + depositor_last_name: + type: string + depositor_phone_number: + type: string id: type: integer + message: + type: string payment_method: type: string receiver_wallet_id: @@ -1605,7 +1760,6 @@ definitions: example: 123 type: integer required: - - suspended - user_id type: object handlers.UpdateUserSuspendRes: @@ -1686,7 +1840,7 @@ definitions: example: USD type: string game_id: - example: crash_001 + example: "1" type: string mode: enum: @@ -1774,6 +1928,21 @@ definitions: example: false type: boolean type: object + handlers.updateCustomerReq: + properties: + company_id: + example: 1 + type: integer + first_name: + example: John + type: string + last_name: + example: Doe + type: string + suspended: + example: false + type: boolean + type: object handlers.updateManagerReq: properties: company_id: @@ -1894,6 +2063,29 @@ paths: summary: Create Admin tags: - admin + /admin-company: + get: + consumes: + - application/json + description: Gets a single company by id + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.CompanyRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets company by id + tags: + - company /admin/{id}: get: consumes: @@ -2009,20 +2201,46 @@ paths: - Alea Virtual Games /api/v1/banks: get: + parameters: + - description: Filter by country ID + in: query + name: country_id + type: integer + - description: Filter by active status + in: query + name: is_active + type: boolean + - description: Search term for bank name or code + in: query + name: search + type: string + - default: 1 + description: Page number + in: query + name: page + type: integer + - default: 50 + description: Items per page + in: query + maximum: 100 + name: page_size + type: integer produces: - application/json responses: "200": description: OK schema: - items: - $ref: '#/definitions/domain.Bank' - type: array + $ref: '#/definitions/domain.InstResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/domain.ErrorResponse' - summary: List all banks + summary: List all banks with pagination and filtering tags: - Institutions - Banks post: @@ -2505,21 +2723,44 @@ paths: - Issues /api/v1/logs: get: - description: Fetches the 100 most recent application logs from MongoDB + description: Fetches application logs from MongoDB with pagination, level filtering, + and search + parameters: + - description: Filter logs by level (debug, info, warn, error, dpanic, panic, + fatal) + in: query + name: level + type: string + - description: Search term to match against message or fields + in: query + name: search + type: string + - default: 1 + description: 'Page number for pagination (default: 1)' + in: query + name: page + type: integer + - default: 50 + description: 'Number of items per page (default: 50, max: 100)' + in: query + name: limit + type: integer produces: - application/json responses: "200": - description: List of application logs + description: Paginated list of application logs schema: - items: - $ref: '#/definitions/domain.LogEntry' - type: array + $ref: '#/definitions/domain.LogResponse' + "400": + description: Invalid request parameters + schema: + $ref: '#/definitions/domain.ErrorResponse' "500": description: Internal server error schema: $ref: '#/definitions/domain.ErrorResponse' - summary: Retrieve latest application logs + summary: Retrieve application logs with filtering and pagination tags: - Logs /api/v1/report-files/download/{filename}: @@ -2555,22 +2796,34 @@ paths: - Reports /api/v1/report-files/list: get: - description: Returns a list of all generated report CSV files available for - download + description: Returns a paginated list of generated report CSV files with search + capability + parameters: + - description: Search term to filter filenames + in: query + name: search + type: string + - default: 1 + description: 'Page number (default: 1)' + in: query + name: page + type: integer + - default: 20 + description: 'Items per page (default: 20, max: 100)' + in: query + name: limit + type: integer produces: - application/json responses: "200": - description: List of CSV report filenames + description: Paginated list of CSV report filenames schema: - allOf: - - $ref: '#/definitions/domain.Response' - - properties: - data: - items: - type: string - type: array - type: object + $ref: '#/definitions/domain.PaginatedFileResponse' + "400": + description: Invalid pagination parameters + schema: + $ref: '#/definitions/domain.ErrorResponse' "500": description: Failed to read report directory schema: @@ -2920,6 +3173,37 @@ 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 /auth/login: post: consumes: @@ -3692,6 +3976,133 @@ paths: summary: Gets branches by company id tags: - branch + /customer: + get: + consumes: + - application/json + description: Get all Customers + parameters: + - description: Page number + in: query + name: page + type: integer + - description: Page size + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.CustomersRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Get all Customers + tags: + - customer + /customer/{id}: + get: + consumes: + - application/json + description: Get a single customer by id + parameters: + - description: User ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.CustomersRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Get customer by id + tags: + - customer + put: + consumes: + - application/json + description: Update Customers + parameters: + - description: Update Customers + in: body + name: Customers + required: true + schema: + $ref: '#/definitions/handlers.updateCustomerReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Update Customers + tags: + - customer + /customerWallet: + get: + consumes: + - application/json + description: Retrieve all customer wallets + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/handlers.CustomerWalletRes' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Get all customer wallets + tags: + - wallet /events: get: consumes: @@ -4137,8 +4548,8 @@ paths: items: $ref: '#/definitions/domain.PopOKGame' type: array - "502": - description: Bad Gateway + "500": + description: Internal Server Error schema: $ref: '#/definitions/domain.ErrorResponse' summary: Get PopOK Games List @@ -4345,6 +4756,303 @@ paths: summary: Gets all companies tags: - company + /shop/bet: + post: + consumes: + - application/json + description: Create bet at branch + parameters: + - description: create bet + in: body + name: createBet + required: true + schema: + $ref: '#/definitions/domain.ShopBetReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.ShopTransactionRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Create bet at branch + tags: + - transaction + /shop/bet/{id}: + get: + consumes: + - application/json + description: Cashout bet at branch + parameters: + - description: cashout bet + in: body + name: createBet + required: true + schema: + $ref: '#/definitions/domain.CashoutReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.ShopTransactionRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Cashout bet at branch + tags: + - transaction + /shop/bet/{id}/cashout: + post: + consumes: + - application/json + description: Cashout bet at branch + parameters: + - description: cashout bet + in: body + name: cashoutBet + required: true + schema: + $ref: '#/definitions/domain.CashoutReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.ShopTransactionRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Cashout bet at branch + tags: + - transaction + /shop/cashout: + post: + consumes: + - application/json + description: Cashout bet by cashoutID + parameters: + - description: cashout bet + in: body + name: cashoutBet + required: true + schema: + $ref: '#/definitions/domain.CashoutReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.ShopTransactionRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Cashout bet by cashoutID + tags: + - transaction + /shop/cashout/{id}: + get: + consumes: + - application/json + description: Cashout bet at branch + parameters: + - description: cashout bet + in: body + name: createBet + required: true + schema: + $ref: '#/definitions/domain.CashoutReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.ShopTransactionRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Cashout bet at branch + tags: + - transaction + /shop/deposit: + post: + consumes: + - application/json + description: Transfers money from branch wallet to customer wallet + parameters: + - description: ShopDepositReq + in: body + name: transferToWallet + required: true + schema: + $ref: '#/definitions/domain.ShopDepositReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.TransferWalletRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Shop deposit into customer wallet + tags: + - transaction + /shop/transaction: + get: + consumes: + - application/json + description: Gets all the transactions + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.ShopTransactionRes' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets all transactions + tags: + - transaction + /shop/transaction/{id}: + get: + consumes: + - application/json + description: Gets a single transaction by id + parameters: + - description: Transaction ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.ShopTransactionRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets transaction by id + tags: + - transaction + put: + consumes: + - application/json + description: Updates the verified status of a transaction + parameters: + - description: Transaction ID + in: path + name: id + required: true + type: integer + - description: Updates Transaction Verification + in: body + name: updateVerified + required: true + schema: + $ref: '#/definitions/handlers.UpdateTransactionVerifiedReq' + 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: Updates the verified field of a transaction + tags: + - transaction + /shop/transaction/{id}/bet: + get: + consumes: + - application/json + description: Gets a single shop bet by transaction id + parameters: + - description: Transaction ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.ShopTransactionRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets shop bet by transaction id + tags: + - transaction /sport/bet: get: consumes: @@ -4519,6 +5227,36 @@ paths: summary: Gets bet by cashout id tags: - bet + /sport/bet/fastcode: + post: + consumes: + - application/json + description: Creates a bet with fast code + parameters: + - description: Creates bet + in: body + name: createBetWithFastCode + required: true + schema: + $ref: '#/definitions/domain.CreateBetReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.BetRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Create a bet with fast code + tags: + - bet /sport/random/bet: post: consumes: @@ -4698,7 +5436,7 @@ paths: description: OK schema: items: - $ref: '#/definitions/domain.UpcomingEvent' + $ref: '#/definitions/handlers.TopLeague' type: array "500": description: Internal Server Error @@ -4707,123 +5445,6 @@ paths: summary: Retrieve all top leagues tags: - prematch - /transaction: - get: - consumes: - - application/json - description: Gets all the transactions - produces: - - application/json - responses: - "200": - description: OK - schema: - items: - $ref: '#/definitions/handlers.TransactionRes' - type: array - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Gets all transactions - tags: - - transaction - post: - consumes: - - application/json - description: Creates a transaction - parameters: - - description: Creates transaction - in: body - name: createBet - required: true - schema: - $ref: '#/definitions/handlers.CreateTransactionReq' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handlers.TransactionRes' - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Create a transaction - tags: - - transaction - /transaction/{id}: - get: - consumes: - - application/json - description: Gets a single transaction by id - parameters: - - description: Transaction ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handlers.TransactionRes' - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Gets transaction by id - tags: - - transaction - put: - consumes: - - application/json - description: Updates the verified status of a transaction - parameters: - - description: Transaction ID - in: path - name: id - required: true - type: integer - - description: Updates Transaction Verification - in: body - name: updateVerified - required: true - schema: - $ref: '#/definitions/handlers.UpdateTransactionVerifiedReq' - 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: Updates the verified field of a transaction - tags: - - transaction /transfer/refill/:id: post: consumes: diff --git a/gen/db/institutions.sql.go b/gen/db/institutions.sql.go index b182933..324ac3e 100644 --- a/gen/db/institutions.sql.go +++ b/gen/db/institutions.sql.go @@ -11,6 +11,37 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const CountBanks = `-- name: CountBanks :one +SELECT COUNT(*) +FROM banks +WHERE ( + country_id = $1 + OR $1 IS NULL + ) + AND ( + is_active = $2 + OR $2 IS NULL + ) + AND ( + name ILIKE '%' || $3 || '%' + OR code ILIKE '%' || $3 || '%' + OR $3 IS NULL + ) +` + +type CountBanksParams struct { + CountryID int32 `json:"country_id"` + IsActive int32 `json:"is_active"` + Column3 pgtype.Text `json:"column_3"` +} + +func (q *Queries) CountBanks(ctx context.Context, arg CountBanksParams) (int64, error) { + row := q.db.QueryRow(ctx, CountBanks, arg.CountryID, arg.IsActive, arg.Column3) + var count int64 + err := row.Scan(&count) + return count, err +} + const CreateBank = `-- name: CreateBank :one INSERT INTO banks ( slug, @@ -106,15 +137,34 @@ WHERE ( is_active = $2 OR $2 IS NULL ) + AND ( + name ILIKE '%' || $3 || '%' + OR $3 IS NULL + ) + AND ( + code ILIKE '%' || $3 || '%' + OR $3 IS NULL + ) +ORDER BY name ASC +LIMIT $5 OFFSET $4 ` type GetAllBanksParams struct { - CountryID pgtype.Int4 `json:"country_id"` - IsActive pgtype.Int4 `json:"is_active"` + CountryID pgtype.Int4 `json:"country_id"` + IsActive pgtype.Int4 `json:"is_active"` + SearchTerm pgtype.Text `json:"search_term"` + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` } func (q *Queries) GetAllBanks(ctx context.Context, arg GetAllBanksParams) ([]Bank, error) { - rows, err := q.db.Query(ctx, GetAllBanks, arg.CountryID, arg.IsActive) + rows, err := q.db.Query(ctx, GetAllBanks, + arg.CountryID, + arg.IsActive, + arg.SearchTerm, + arg.Offset, + arg.Limit, + ) if err != nil { return nil, err } diff --git a/go.mod b/go.mod index 7fe0d0c..60ae591 100644 --- a/go.mod +++ b/go.mod @@ -78,7 +78,7 @@ require ( ) require ( - github.com/go-resty/resty/v2 v2.16.5 + // github.com/go-resty/resty/v2 v2.16.5 github.com/twilio/twilio-go v1.26.3 ) @@ -87,6 +87,6 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/golang/mock v1.6.0 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/redis/go-redis/v9 v9.10.0 // indirect + github.com/redis/go-redis/v9 v9.10.0 // direct go.uber.org/atomic v1.9.0 // indirect ) diff --git a/internal/domain/bank.go b/internal/domain/bank.go deleted file mode 100644 index 7d0427a..0000000 --- a/internal/domain/bank.go +++ /dev/null @@ -1 +0,0 @@ -package domain \ No newline at end of file diff --git a/internal/domain/chapa.go b/internal/domain/chapa.go index 57a090f..d1451cd 100644 --- a/internal/domain/chapa.go +++ b/internal/domain/chapa.go @@ -9,6 +9,9 @@ var ( ErrInsufficientBalance = errors.New("insufficient balance") ErrInvalidWithdrawalAmount = errors.New("invalid withdrawal amount") ErrWithdrawalNotFound = errors.New("withdrawal not found") + ErrPaymentNotFound = errors.New("payment not found") + ErrPaymentAlreadyExists = errors.New("payment with this reference already exists") + ErrInvalidPaymentAmount = errors.New("invalid payment amount") ) type PaymentStatus string diff --git a/internal/domain/common.go b/internal/domain/common.go index 54433ab..b159b4c 100644 --- a/internal/domain/common.go +++ b/internal/domain/common.go @@ -79,5 +79,12 @@ func CalculateWinnings(amount Currency, totalOdds float32) Currency { } +type Pagination struct { + Total int `json:"total"` + TotalPages int `json:"total_pages"` + CurrentPage int `json:"current_page"` + Limit int `json:"limit"` +} + func PtrFloat64(v float64) *float64 { return &v } func PtrInt64(v int64) *int64 { return &v } diff --git a/internal/domain/institutions.go b/internal/domain/institutions.go index 0e09b57..431f64e 100644 --- a/internal/domain/institutions.go +++ b/internal/domain/institutions.go @@ -18,4 +18,11 @@ type Bank struct { UpdatedAt time.Time `json:"updated_at"` Currency string `json:"currency"` BankLogo string `json:"bank_logo"` // URL or base64 -} \ No newline at end of file +} + +type InstResponse struct { + Message string `json:"message"` + Status string `json:"status"` + Data interface{} `json:"data"` // Changed to interface{} for flexibility + Pagination *Pagination `json:"pagination,omitempty"` // Made pointer and optional +} diff --git a/internal/domain/mongoLogs.go b/internal/domain/mongoLogs.go index 6f1cc2c..4007023 100644 --- a/internal/domain/mongoLogs.go +++ b/internal/domain/mongoLogs.go @@ -10,3 +10,11 @@ type LogEntry struct { Service string `json:"service" bson:"service"` Env string `json:"env" bson:"env"` } + +type LogResponse struct { + Message string `json:"message" bson:"message"` + Data []LogEntry `json:"data"` + Pagination Pagination `json:"pagination"` +} + + diff --git a/internal/domain/notification.go b/internal/domain/notification.go index db054c1..d4a1a8e 100644 --- a/internal/domain/notification.go +++ b/internal/domain/notification.go @@ -14,22 +14,23 @@ type NotificationDeliveryStatus string type DeliveryChannel string const ( - NotificationTypeCashOutSuccess NotificationType = "cash_out_success" - NotificationTypeDepositSuccess NotificationType = "deposit_success" - NotificationTypeWithdrawSuccess NotificationType = "withdraw_success" - NotificationTypeBetPlaced NotificationType = "bet_placed" - NotificationTypeDailyReport NotificationType = "daily_report" - NotificationTypeHighLossOnBet NotificationType = "high_loss_on_bet" - NotificationTypeBetOverload NotificationType = "bet_overload" - NotificationTypeSignUpWelcome NotificationType = "signup_welcome" - NotificationTypeOTPSent NotificationType = "otp_sent" - NOTIFICATION_TYPE_WALLET NotificationType = "wallet_threshold" - NOTIFICATION_TYPE_TRANSFER_FAIL NotificationType = "transfer_failed" - NOTIFICATION_TYPE_TRANSFER_SUCCESS NotificationType = "transfer_success" - NOTIFICATION_TYPE_ADMIN_ALERT NotificationType = "admin_alert" - NOTIFICATION_TYPE_BET_RESULT NotificationType = "bet_result" + NotificationTypeCashOutSuccess NotificationType = "cash_out_success" + NotificationTypeDepositSuccess NotificationType = "deposit_success" + NotificationTypeWithdrawSuccess NotificationType = "withdraw_success" + NotificationTypeBetPlaced NotificationType = "bet_placed" + NotificationTypeDailyReport NotificationType = "daily_report" + NotificationTypeHighLossOnBet NotificationType = "high_loss_on_bet" + NotificationTypeBetOverload NotificationType = "bet_overload" + NotificationTypeSignUpWelcome NotificationType = "signup_welcome" + NotificationTypeOTPSent NotificationType = "otp_sent" + NOTIFICATION_TYPE_WALLET NotificationType = "wallet_threshold" + NOTIFICATION_TYPE_TRANSFER_FAIL NotificationType = "transfer_failed" + NOTIFICATION_TYPE_TRANSFER_SUCCESS NotificationType = "transfer_success" + NOTIFICATION_TYPE_ADMIN_ALERT NotificationType = "admin_alert" + NOTIFICATION_TYPE_BET_RESULT NotificationType = "bet_result" + NOTIFICATION_TYPE_TRANSFER_REJECTED NotificationType = "transfer_rejected" + NOTIFICATION_TYPE_APPROVAL_REQUIRED NotificationType = "approval_required" - NOTIFICATION_RECEIVER_ADMIN NotificationRecieverSide = "admin" NotificationRecieverSideAdmin NotificationRecieverSide = "admin" NotificationRecieverSideCustomer NotificationRecieverSide = "customer" NotificationRecieverSideCashier NotificationRecieverSide = "cashier" diff --git a/internal/domain/report.go b/internal/domain/report.go index bdca3d6..9c0c632 100644 --- a/internal/domain/report.go +++ b/internal/domain/report.go @@ -18,6 +18,12 @@ const ( ReportMonthly ReportFrequency = "monthly" ) +type PaginatedFileResponse struct { + Response `json:",inline"` + Data []string `json:"data"` + Pagination Pagination `json:"pagination"` +} + type ReportRequest struct { Frequency ReportFrequency StartDate time.Time diff --git a/internal/domain/transfer.go b/internal/domain/transfer.go index 4425ac4..370b8a1 100644 --- a/internal/domain/transfer.go +++ b/internal/domain/transfer.go @@ -25,6 +25,30 @@ const ( TRANSFER_OTHER PaymentMethod = "other" ) +type TransactionApproval struct { + ID int64 + TransferID int64 + ApprovedBy int64 // User ID of approver + Status string // "pending", "approved", "rejected" + Comments string + CreatedAt time.Time + UpdatedAt time.Time +} + +type ApprovalAction string + +const ( + ApprovalActionApprove ApprovalAction = "approve" + ApprovalActionReject ApprovalAction = "reject" +) + +type ApprovalRequest struct { + TransferID int64 + Action ApprovalAction + Comments string + ApproverID int64 +} + // Info for the payment providers type PaymentDetails struct { ReferenceNumber ValidString diff --git a/internal/repository/institutions.go b/internal/repository/institutions.go index 6cf72a4..f901957 100644 --- a/internal/repository/institutions.go +++ b/internal/repository/institutions.go @@ -13,7 +13,14 @@ import ( type BankRepository interface { CreateBank(ctx context.Context, bank *domain.Bank) error GetBankByID(ctx context.Context, id int) (*domain.Bank, error) - GetAllBanks(ctx context.Context, countryID *int, isActive *int) ([]domain.Bank, error) + GetAllBanks( + ctx context.Context, + countryID *int, + isActive *bool, + searchTerm *string, + page int, + pageSize int, + ) ([]domain.Bank, int64, error) UpdateBank(ctx context.Context, bank *domain.Bank) error DeleteBank(ctx context.Context, id int) error } @@ -63,28 +70,72 @@ func (r *BankRepo) GetBankByID(ctx context.Context, id int) (*domain.Bank, error return mapDBBankToDomain(&dbBank), nil } -func (r *BankRepo) GetAllBanks(ctx context.Context, countryID *int, isActive *int) ([]domain.Bank, error) { - params := dbgen.GetAllBanksParams{ - CountryID: pgtype.Int4{}, - IsActive: pgtype.Int4{}, +func (r *BankRepo) GetAllBanks( + ctx context.Context, + countryID *int, + isActive *bool, + searchTerm *string, + page int, + pageSize int, +) ([]domain.Bank, int64, error) { + // Set default pagination values if not provided + if page < 1 { + page = 1 } + if pageSize < 1 || pageSize > 100 { + pageSize = 50 + } + offset := (page - 1) * pageSize + + params := dbgen.GetAllBanksParams{ + CountryID: pgtype.Int4{}, + IsActive: pgtype.Int4{}, + SearchTerm: pgtype.Text{}, + Limit: pgtype.Int4{Int32: int32(pageSize), Valid: true}, + Offset: pgtype.Int4{Int32: int32(offset), Valid: true}, + } + if countryID != nil { params.CountryID = pgtype.Int4{Int32: int32(*countryID), Valid: true} } if isActive != nil { - params.IsActive = pgtype.Int4{Int32: int32(*isActive), Valid: true} + var activeInt int32 + if *isActive { + activeInt = 1 + } else { + activeInt = 0 + } + params.IsActive = pgtype.Int4{Int32: activeInt, Valid: true} + } + if searchTerm != nil { + params.SearchTerm = pgtype.Text{String: *searchTerm, Valid: true} } + // Get paginated results dbBanks, err := r.store.queries.GetAllBanks(ctx, params) if err != nil { - return nil, err + return nil, 0, err + } + + // Get total count for pagination + var countCountryID int32 + if params.CountryID.Valid { + countCountryID = params.CountryID.Int32 + } + total, err := r.store.queries.CountBanks(ctx, dbgen.CountBanksParams{ + CountryID: countCountryID, + IsActive: params.IsActive.Int32, + }) + if err != nil { + return nil, 0, err } banks := make([]domain.Bank, len(dbBanks)) for i, b := range dbBanks { banks[i] = *mapDBBankToDomain(&b) } - return banks, nil + + return banks, total, nil } func (r *BankRepo) UpdateBank(ctx context.Context, bank *domain.Bank) error { diff --git a/internal/repository/transfer.go b/internal/repository/transfer.go index 5816ce7..4324ddf 100644 --- a/internal/repository/transfer.go +++ b/internal/repository/transfer.go @@ -159,3 +159,7 @@ func (s *Store) UpdateTransferStatus(ctx context.Context, id int64, status strin return err } + +func ApproveTransfer(ctx context.Context, approval domain.ApprovalRequest) error { + return nil +} diff --git a/internal/services/chapa/service.go b/internal/services/chapa/service.go index 2f44258..5822cb7 100644 --- a/internal/services/chapa/service.go +++ b/internal/services/chapa/service.go @@ -14,12 +14,6 @@ import ( "github.com/google/uuid" ) -var ( - ErrPaymentNotFound = errors.New("payment not found") - ErrPaymentAlreadyExists = errors.New("payment with this reference already exists") - ErrInvalidPaymentAmount = errors.New("invalid payment amount") -) - type Service struct { transferStore wallet.TransferStore walletStore wallet.Service @@ -49,7 +43,7 @@ func NewService( func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount domain.Currency) (string, error) { // Validate amount if amount <= 0 { - return "", ErrInvalidPaymentAmount + return "", domain.ErrInvalidPaymentAmount } // Get user details @@ -136,12 +130,6 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma return nil, domain.ErrInvalidWithdrawalAmount } - // Get user details - // user, err := s.userStore.GetUserByID(ctx, userID) - // if err != nil { - // return nil, fmt.Errorf("failed to get user: %w", err) - // } - // Get user's wallet wallets, err := s.walletStore.GetWalletsByUser(ctx, userID) if err != nil { @@ -300,7 +288,7 @@ func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domai // Find payment by reference payment, err := s.transferStore.GetTransferByReference(ctx, transfer.Reference) if err != nil { - return ErrPaymentNotFound + return domain.ErrPaymentNotFound } if payment.Verified { @@ -330,7 +318,7 @@ func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domai ReferenceNumber: domain.ValidString{ Value: transfer.Reference, }, - }, fmt.Sprintf("Added %v to wallet using Chapa")); err != nil { + }, fmt.Sprintf("Added %v to wallet using Chapa", payment.Amount)); err != nil { return fmt.Errorf("failed to credit user wallet: %w", err) } } @@ -342,7 +330,7 @@ func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domai // Find payment by reference transfer, err := s.transferStore.GetTransferByReference(ctx, payment.Reference) if err != nil { - return ErrPaymentNotFound + return domain.ErrPaymentNotFound } if transfer.Verified { @@ -368,7 +356,7 @@ func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domai } else { _, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value, transfer.Amount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, - fmt.Sprintf("Added %v to wallet by system because chapa withdraw is unsuccessful")) + fmt.Sprintf("Added %v to wallet by system because chapa withdraw is unsuccessful", transfer.Amount)) if err != nil { return fmt.Errorf("failed to credit user wallet: %w", err) } diff --git a/internal/services/institutions/service.go b/internal/services/institutions/service.go index 9b54cd1..b27a4d4 100644 --- a/internal/services/institutions/service.go +++ b/internal/services/institutions/service.go @@ -31,14 +31,36 @@ func (s *Service) Delete(ctx context.Context, id int64) error { return s.repo.DeleteBank(ctx, int(id)) } -func (s *Service) List(ctx context.Context) ([]*domain.Bank, error) { - banks, err := s.repo.GetAllBanks(ctx, nil, nil) +func (s *Service) List( + ctx context.Context, + countryID *int, + isActive *bool, + searchTerm *string, + page int, + pageSize int, +) ([]*domain.Bank, *domain.Pagination, error) { + banks, total, err := s.repo.GetAllBanks(ctx, countryID, isActive, searchTerm, page, pageSize) if err != nil { - return nil, err + return nil, nil, err } + result := make([]*domain.Bank, len(banks)) for i := range banks { result[i] = &banks[i] } - return result, nil + + // Calculate pagination metadata + totalPages := int(total) / pageSize + if int(total)%pageSize != 0 { + totalPages++ + } + + pagination := &domain.Pagination{ + Total: int(total), + TotalPages: totalPages, + CurrentPage: page, + Limit: pageSize, + } + + return result, pagination, nil } diff --git a/internal/services/report/service.go b/internal/services/report/service.go index 6d4cb6a..90989f4 100644 --- a/internal/services/report/service.go +++ b/internal/services/report/service.go @@ -465,6 +465,11 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error { return fmt.Errorf("fetch data: %w", err) } + // Ensure the reports directory exists + if err := os.MkdirAll("reports", os.ModePerm); err != nil { + return fmt.Errorf("creating reports directory: %w", err) + } + filePath := fmt.Sprintf("reports/report_%s_%s.csv", period, time.Now().Format("2006-01-02_15-04")) file, err := os.Create(filePath) if err != nil { @@ -476,9 +481,13 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error { defer writer.Flush() // Summary section - writer.Write([]string{"Sports Betting Reports (Periodic)"}) - writer.Write([]string{"Period", "Total Bets", "Total Cash Made", "Total Cash Out", "Total Cash Backs", "Total Deposits", "Total Withdrawals", "Total Tickets"}) - writer.Write([]string{ + if err := writer.Write([]string{"Sports Betting Reports (Periodic)"}); err != nil { + return fmt.Errorf("write header: %w", err) + } + if err := writer.Write([]string{"Period", "Total Bets", "Total Cash Made", "Total Cash Out", "Total Cash Backs", "Total Deposits", "Total Withdrawals", "Total Tickets"}); err != nil { + return fmt.Errorf("write header row: %w", err) + } + if err := writer.Write([]string{ period, fmt.Sprintf("%d", data.TotalBets), fmt.Sprintf("%.2f", data.TotalCashIn), @@ -487,40 +496,50 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error { fmt.Sprintf("%.2f", data.Deposits), fmt.Sprintf("%.2f", data.Withdrawals), fmt.Sprintf("%d", data.TotalTickets), - }) + }); err != nil { + return fmt.Errorf("write summary row: %w", err) + } - writer.Write([]string{}) // Empty line for spacing + writer.Write([]string{}) // Empty line // Virtual Game Summary section writer.Write([]string{"Virtual Game Reports (Periodic)"}) writer.Write([]string{"Game Name", "Number of Bets", "Total Transaction Sum"}) for _, row := range data.VirtualGameStats { - writer.Write([]string{ + if err := writer.Write([]string{ row.GameName, fmt.Sprintf("%d", row.NumBets), fmt.Sprintf("%.2f", row.TotalTransaction), - }) + }); err != nil { + return fmt.Errorf("write virtual game row: %w", err) + } } writer.Write([]string{}) // Empty line + + // Company Reports writer.Write([]string{"Company Reports (Periodic)"}) writer.Write([]string{"Company ID", "Company Name", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"}) for _, cr := range data.CompanyReports { - writer.Write([]string{ + if err := writer.Write([]string{ fmt.Sprintf("%d", cr.CompanyID), cr.CompanyName, fmt.Sprintf("%d", cr.TotalBets), fmt.Sprintf("%.2f", cr.TotalCashIn), fmt.Sprintf("%.2f", cr.TotalCashOut), fmt.Sprintf("%.2f", cr.TotalCashBacks), - }) + }); err != nil { + return fmt.Errorf("write company row: %w", err) + } } writer.Write([]string{}) // Empty line + + // Branch Reports writer.Write([]string{"Branch Reports (Periodic)"}) writer.Write([]string{"Branch ID", "Branch Name", "Company ID", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"}) for _, br := range data.BranchReports { - writer.Write([]string{ + if err := writer.Write([]string{ fmt.Sprintf("%d", br.BranchID), br.BranchName, fmt.Sprintf("%d", br.CompanyID), @@ -528,9 +547,12 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error { fmt.Sprintf("%.2f", br.TotalCashIn), fmt.Sprintf("%.2f", br.TotalCashOut), fmt.Sprintf("%.2f", br.TotalCashBacks), - }) + }); err != nil { + return fmt.Errorf("write branch row: %w", err) + } } + // Total Summary var totalBets int64 var totalCashIn, totalCashOut, totalCashBacks float64 for _, cr := range data.CompanyReports { @@ -540,19 +562,22 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error { totalCashBacks += cr.TotalCashBacks } - writer.Write([]string{}) + writer.Write([]string{}) // Empty line writer.Write([]string{"Total Summary"}) writer.Write([]string{"Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"}) - writer.Write([]string{ + if err := writer.Write([]string{ fmt.Sprintf("%d", totalBets), fmt.Sprintf("%.2f", totalCashIn), fmt.Sprintf("%.2f", totalCashOut), fmt.Sprintf("%.2f", totalCashBacks), - }) + }); err != nil { + return fmt.Errorf("write total summary row: %w", err) + } return nil } + func (s *Service) fetchReportData(ctx context.Context, period string) (domain.ReportData, error) { from, to := getTimeRange(period) // companyID := int64(0) diff --git a/internal/services/result/eval.go b/internal/services/result/eval.go index 7352812..e7c1836 100644 --- a/internal/services/result/eval.go +++ b/internal/services/result/eval.go @@ -41,14 +41,15 @@ func evaluateGoalsOverUnder(outcome domain.BetOutcome, score struct{ Home, Away return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName) } - if outcome.OddHeader == "Over" { + switch outcome.OddHeader { + case "Over": if totalGoals > threshold { return domain.OUTCOME_STATUS_WIN, nil } else if totalGoals == threshold { return domain.OUTCOME_STATUS_VOID, nil } return domain.OUTCOME_STATUS_LOSS, nil - } else if outcome.OddHeader == "Under" { + case "Under": if totalGoals < threshold { return domain.OUTCOME_STATUS_WIN, nil } else if totalGoals == threshold { @@ -91,46 +92,48 @@ func checkMultiOutcome(outcome domain.OutcomeStatus, secondOutcome domain.Outcom case domain.OUTCOME_STATUS_PENDING: return secondOutcome, nil case domain.OUTCOME_STATUS_WIN: - if secondOutcome == domain.OUTCOME_STATUS_WIN { + switch secondOutcome { + case domain.OUTCOME_STATUS_WIN: return domain.OUTCOME_STATUS_WIN, nil - } else if secondOutcome == domain.OUTCOME_STATUS_LOSS { + case domain.OUTCOME_STATUS_LOSS: return domain.OUTCOME_STATUS_LOSS, nil - } else if secondOutcome == domain.OUTCOME_STATUS_HALF { + case domain.OUTCOME_STATUS_HALF: return domain.OUTCOME_STATUS_VOID, nil - } else if secondOutcome == domain.OUTCOME_STATUS_VOID { + case domain.OUTCOME_STATUS_VOID: return domain.OUTCOME_STATUS_HALF, nil - } else { + default: fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String()) return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome") } case domain.OUTCOME_STATUS_LOSS: - if secondOutcome == domain.OUTCOME_STATUS_LOSS || - secondOutcome == domain.OUTCOME_STATUS_WIN || - secondOutcome == domain.OUTCOME_STATUS_HALF { + switch secondOutcome { + case domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_HALF: return domain.OUTCOME_STATUS_LOSS, nil - } else if secondOutcome == domain.OUTCOME_STATUS_VOID { + case domain.OUTCOME_STATUS_VOID: return domain.OUTCOME_STATUS_VOID, nil - } else { + default: fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String()) return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome") } case domain.OUTCOME_STATUS_VOID: - if secondOutcome == domain.OUTCOME_STATUS_WIN || secondOutcome == domain.OUTCOME_STATUS_LOSS { + switch secondOutcome { + case domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_LOSS: return domain.OUTCOME_STATUS_VOID, nil - } else if secondOutcome == domain.OUTCOME_STATUS_VOID || secondOutcome == domain.OUTCOME_STATUS_HALF { + case domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_HALF: return domain.OUTCOME_STATUS_VOID, nil - } else { + default: fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String()) return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome") } case domain.OUTCOME_STATUS_HALF: - if secondOutcome == domain.OUTCOME_STATUS_WIN || secondOutcome == domain.OUTCOME_STATUS_HALF { + switch secondOutcome { + case domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_HALF: return domain.OUTCOME_STATUS_VOID, nil - } else if secondOutcome == domain.OUTCOME_STATUS_LOSS { + case domain.OUTCOME_STATUS_LOSS: return domain.OUTCOME_STATUS_LOSS, nil - } else if secondOutcome == domain.OUTCOME_STATUS_VOID { + case domain.OUTCOME_STATUS_VOID: return domain.OUTCOME_STATUS_VOID, nil - } else { + default: fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String()) return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome") } @@ -168,11 +171,12 @@ func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away i } adjustedHomeScore := float64(score.Home) adjustedAwayScore := float64(score.Away) - if outcome.OddHeader == "1" { // Home team + switch outcome.OddHeader { + case "1": // Home team adjustedHomeScore += handicap - } else if outcome.OddHeader == "2" { // Away team + case "2": // Away team adjustedAwayScore += handicap - } else { + default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) } @@ -244,7 +248,8 @@ func evaluateGoalLine(outcome domain.BetOutcome, score struct{ Home, Away int }) } oddHeader := strings.TrimSpace(outcome.OddHeader) - if oddHeader == "Over" { + switch oddHeader { + case "Over": if totalGoals > threshold { newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN) if err != nil { @@ -261,7 +266,7 @@ func evaluateGoalLine(outcome domain.BetOutcome, score struct{ Home, Away int }) if err != nil { return domain.OUTCOME_STATUS_ERROR, err } - } else if oddHeader == "Under" { + case "Under": if totalGoals < threshold { newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN) if err != nil { @@ -280,7 +285,7 @@ func evaluateGoalLine(outcome domain.BetOutcome, score struct{ Home, Away int }) return domain.OUTCOME_STATUS_ERROR, err } - } else { + default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: '%s'", oddHeader) } @@ -321,31 +326,33 @@ func evaluateTeamOddEven(outcome domain.BetOutcome, score struct{ Home, Away int switch outcome.OddHeader { case "1": - if outcome.OddHandicap == "Odd" { + switch outcome.OddHandicap { + case "Odd": if score.Home%2 == 1 { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil - } else if outcome.OddHandicap == "Even" { + case "Even": if score.Home%2 == 0 { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil - } else { + default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd handicap: %s", outcome.OddHandicap) } case "2": - if outcome.OddHandicap == "Odd" { + switch outcome.OddHandicap { + case "Odd": if score.Away%2 == 1 { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil - } else if outcome.OddHandicap == "Even" { + case "Even": if score.Away%2 == 0 { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil - } else { + default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd handicap: %s", outcome.OddHandicap) } default: @@ -480,17 +487,18 @@ func evaluateTotalOverUnder(outcome domain.BetOutcome, score struct{ Home, Away // Since the threshold will come in a xx.5 format, there is no VOID for this kind of bet totalScore := float64(score.Home + score.Away) - if overUnderStr[0] == "O" { + switch overUnderStr[0] { + case "O": if totalScore > threshold { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil - } else if overUnderStr[0] == "U" { + case "U": if totalScore < threshold { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil - } else if overUnderStr[0] == "E" { + case "E": if totalScore == threshold { return domain.OUTCOME_STATUS_WIN, nil } @@ -633,11 +641,12 @@ func evaluateResultAndBTTSX(outcome domain.BetOutcome, score struct{ Home, Away scoreCheckSplit := oddNameSplit[len(oddNameSplit)-1] var isScorePoints bool - if scoreCheckSplit == "Yes" { + switch scoreCheckSplit { + case "Yes": isScorePoints = true - } else if scoreCheckSplit == "No" { + case "No": isScorePoints = false - } else { + default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) } @@ -853,15 +862,16 @@ func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Awa } total := float64(score.Home + score.Away) overUnder := nameSplit[len(nameSplit)-2] - if overUnder == "Over" { + switch overUnder { + case "Over": if total < threshold { return domain.OUTCOME_STATUS_LOSS, nil } - } else if overUnder == "Under" { + case "Under": if total > threshold { return domain.OUTCOME_STATUS_LOSS, nil } - } else { + default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over and under: %s", outcome.OddName) } diff --git a/internal/services/result/service.go b/internal/services/result/service.go index 3ceef62..a19ede7 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -73,7 +73,7 @@ func (s *Service) UpdateResultForOutcomes(ctx context.Context, eventID int64, re return fmt.Errorf("Outcome has not expired yet") } - parseResult, err := s.parseResult(ctx, resultRes, outcome, sportID) + parseResult, err := s.parseResult(resultRes, outcome, sportID) if err != nil { s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err) @@ -595,7 +595,7 @@ func (s *Service) GetResultsForEvent(ctx context.Context, eventID string) (json. // Get results for outcome for i, outcome := range outcomes { // Parse the result based on sport type - parsedResult, err := s.parseResult(ctx, result.Results[0], outcome, sportID) + parsedResult, err := s.parseResult(result.Results[0], outcome, sportID) if err != nil { s.logger.Error("Failed to parse result for outcome", "event_id", outcome.EventID, "error", err) return json.RawMessage{}, nil, fmt.Errorf("failed to parse result for outcome %d: %w", i, err) @@ -643,7 +643,7 @@ func (s *Service) fetchResult(ctx context.Context, eventID int64) (domain.BaseRe return resultResp, nil } -func (s *Service) parseResult(ctx context.Context, resultResp json.RawMessage, outcome domain.BetOutcome, sportID int64) (domain.CreateResult, error) { +func (s *Service) parseResult(resultResp json.RawMessage, outcome domain.BetOutcome, sportID int64) (domain.CreateResult, error) { var result domain.CreateResult var err error diff --git a/internal/services/result/sports_eval.go b/internal/services/result/sports_eval.go index 86e082f..2a4f013 100644 --- a/internal/services/result/sports_eval.go +++ b/internal/services/result/sports_eval.go @@ -34,11 +34,12 @@ func EvaluateNFLSpread(outcome domain.BetOutcome, score struct{ Home, Away int } adjustedHomeScore := float64(score.Home) adjustedAwayScore := float64(score.Away) - if outcome.OddHeader == "1" { + switch outcome.OddHeader { + case "1": adjustedHomeScore += handicap - } else if outcome.OddHeader == "2" { + case "2": adjustedAwayScore += handicap - } else { + default: return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) } @@ -63,14 +64,15 @@ func EvaluateNFLTotalPoints(outcome domain.BetOutcome, score struct{ Home, Away return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName) } - if outcome.OddHeader == "Over" { + switch outcome.OddHeader { + case "Over": if totalPoints > threshold { return domain.OUTCOME_STATUS_WIN, nil } else if totalPoints == threshold { return domain.OUTCOME_STATUS_VOID, nil } return domain.OUTCOME_STATUS_LOSS, nil - } else if outcome.OddHeader == "Under" { + case "Under": if totalPoints < threshold { return domain.OUTCOME_STATUS_WIN, nil } else if totalPoints == threshold { @@ -109,11 +111,12 @@ func EvaluateRugbySpread(outcome domain.BetOutcome, score struct{ Home, Away int adjustedHomeScore := float64(score.Home) adjustedAwayScore := float64(score.Away) - if outcome.OddHeader == "1" { + switch outcome.OddHeader { + case "1": adjustedHomeScore += handicap - } else if outcome.OddHeader == "2" { + case "2": adjustedAwayScore += handicap - } else { + default: return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) } @@ -139,14 +142,15 @@ func EvaluateRugbyTotalPoints(outcome domain.BetOutcome, score struct{ Home, Awa return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName) } - if outcome.OddHeader == "Over" { + switch outcome.OddHeader { + case "Over": if totalPoints > threshold { return domain.OUTCOME_STATUS_WIN, nil } else if totalPoints == threshold { return domain.OUTCOME_STATUS_VOID, nil } return domain.OUTCOME_STATUS_LOSS, nil - } else if outcome.OddHeader == "Under" { + case "Under": if totalPoints < threshold { return domain.OUTCOME_STATUS_WIN, nil } else if totalPoints == threshold { @@ -185,11 +189,12 @@ func EvaluateBaseballSpread(outcome domain.BetOutcome, score struct{ Home, Away adjustedHomeScore := float64(score.Home) adjustedAwayScore := float64(score.Away) - if outcome.OddHeader == "1" { + switch outcome.OddHeader { + case "1": adjustedHomeScore += handicap - } else if outcome.OddHeader == "2" { + case "2": adjustedAwayScore += handicap - } else { + default: return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) } @@ -215,14 +220,15 @@ func EvaluateBaseballTotalRuns(outcome domain.BetOutcome, score struct{ Home, Aw return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName) } - if outcome.OddHeader == "Over" { + switch outcome.OddHeader { + case "Over": if totalRuns > threshold { return domain.OUTCOME_STATUS_WIN, nil } else if totalRuns == threshold { return domain.OUTCOME_STATUS_VOID, nil } return domain.OUTCOME_STATUS_LOSS, nil - } else if outcome.OddHeader == "Under" { + case "Under": if totalRuns < threshold { return domain.OUTCOME_STATUS_WIN, nil } else if totalRuns == threshold { diff --git a/internal/services/user/common.go b/internal/services/user/common.go index 6cab064..0094210 100644 --- a/internal/services/user/common.go +++ b/internal/services/user/common.go @@ -107,7 +107,7 @@ func (s *Service) SendTwilioSMSOTP(ctx context.Context, receiverPhone, message s _, err := client.Api.CreateMessage(params) if err != nil { - return fmt.Errorf("Error sending SMS message: %s" + err.Error()) + return fmt.Errorf("%s", "Error sending SMS message: %s" + err.Error()) } return nil diff --git a/internal/services/wallet/port.go b/internal/services/wallet/port.go index e47bc65..d3a2954 100644 --- a/internal/services/wallet/port.go +++ b/internal/services/wallet/port.go @@ -25,11 +25,23 @@ type WalletStore interface { } type TransferStore interface { - CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) - GetAllTransfers(ctx context.Context) ([]domain.TransferDetail, error) - GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.TransferDetail, error) - GetTransferByReference(ctx context.Context, reference string) (domain.TransferDetail, error) - GetTransferByID(ctx context.Context, id int64) (domain.TransferDetail, error) - UpdateTransferVerification(ctx context.Context, id int64, verified bool) error - UpdateTransferStatus(ctx context.Context, id int64, status string) error + CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) + GetAllTransfers(ctx context.Context) ([]domain.TransferDetail, error) + GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.TransferDetail, error) + GetTransferByReference(ctx context.Context, reference string) (domain.TransferDetail, error) + GetTransferByID(ctx context.Context, id int64) (domain.TransferDetail, error) + UpdateTransferVerification(ctx context.Context, id int64, verified bool) error + UpdateTransferStatus(ctx context.Context, id int64, status string) error + // InitiateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) + // ApproveTransfer(ctx context.Context, approval domain.ApprovalRequest) error + // RejectTransfer(ctx context.Context, approval domain.ApprovalRequest) error + // GetPendingApprovals(ctx context.Context) ([]domain.TransferDetail, error) + // GetTransferApprovalHistory(ctx context.Context, transferID int64) ([]domain.TransactionApproval, error) +} + +type ApprovalStore interface { + CreateApproval(ctx context.Context, approval domain.TransactionApproval) error + UpdateApprovalStatus(ctx context.Context, approvalID int64, status string, comments string) error + GetApprovalsByTransfer(ctx context.Context, transferID int64) ([]domain.TransactionApproval, error) + GetPendingApprovals(ctx context.Context) ([]domain.TransferDetail, error) } diff --git a/internal/services/wallet/service.go b/internal/services/wallet/service.go index 4d0bd76..773918d 100644 --- a/internal/services/wallet/service.go +++ b/internal/services/wallet/service.go @@ -7,19 +7,24 @@ import ( ) type Service struct { + // approvalStore ApprovalStore walletStore WalletStore transferStore TransferStore notificationStore notificationservice.NotificationStore notificationSvc *notificationservice.Service logger *slog.Logger + // userStore user.UserStore } func NewService(walletStore WalletStore, transferStore TransferStore, notificationStore notificationservice.NotificationStore, notificationSvc *notificationservice.Service, logger *slog.Logger) *Service { return &Service{ - walletStore: walletStore, - transferStore: transferStore, + walletStore: walletStore, + transferStore: transferStore, + // approvalStore: approvalStore, notificationStore: notificationStore, notificationSvc: notificationSvc, logger: logger, + // userStore: userStore, + // userStore users } } diff --git a/internal/services/wallet/transfer.go b/internal/services/wallet/transfer.go index 4a65926..ad3ef1d 100644 --- a/internal/services/wallet/transfer.go +++ b/internal/services/wallet/transfer.go @@ -172,3 +172,214 @@ func (s *Service) SendTransferNotification(ctx context.Context, senderWallet dom return nil } + +func ApproveTransfer(ctx context.Context, approval domain.ApprovalRequest) error { + return nil +} + +/////////////////////////////////Transaction Make-Cheker///////////////////////////////////////////////// + +// func (s *Service) InitiateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) { +// // Set transfer as unverified by default +// transfer.Verified = false + +// // Create the transfer record +// newTransfer, err := s.transferStore.CreateTransfer(ctx, transfer) +// if err != nil { +// return domain.Transfer{}, err +// } + +// // Create approval record +// approval := domain.TransactionApproval{ +// TransferID: newTransfer.ID, +// Status: "pending", +// } + +// if err := s.approvalStore.CreateApproval(ctx, approval); err != nil { +// return domain.Transfer{}, err +// } + +// // Notify approvers +// go s.notifyApprovers(ctx, newTransfer.ID) + +// return newTransfer, nil +// } + +// func (s *Service) ApproveTransfer(ctx context.Context, approval domain.ApprovalRequest) error { +// // Get the transfer +// transfer, err := s.transferStore.GetTransferByID(ctx, approval.TransferID) +// if err != nil { +// return err +// } + +// // Only allow approval of pending transfers +// if transfer.Status != "pending" { +// return errors.New("only pending transfers can be approved") +// } + +// // Update approval record +// if err := s.approvalStore.UpdateApprovalStatus(ctx, approval.TransferID, "approved", approval.Comments); err != nil { +// return err +// } + +// // Execute the actual transfer +// if transfer.Type == domain.WALLET { +// _, err = s.executeWalletTransfer(ctx, transfer) +// return err +// } + +// // For other transfer types, implement similar execution logic + +// return nil +// } + +// func (s *Service) executeWalletTransfer(ctx context.Context, transfer domain.TransferDetail) (domain.Transfer, error) { +// // Original transfer logic from TransferToWallet, but now guaranteed to be approved +// senderWallet, err := s.GetWalletByID(ctx, transfer.SenderWalletID.Value) +// if err != nil { +// return domain.Transfer{}, err +// } + +// receiverWallet, err := s.GetWalletByID(ctx, transfer.ReceiverWalletID.Value) +// if err != nil { +// return domain.Transfer{}, err +// } + +// // Deduct from sender +// if senderWallet.Balance < transfer.Amount { +// return domain.Transfer{}, ErrBalanceInsufficient +// } + +// err = s.walletStore.UpdateBalance(ctx, transfer.SenderWalletID.Value, senderWallet.Balance-transfer.Amount) +// if err != nil { +// return domain.Transfer{}, err +// } + +// // Add to receiver +// err = s.walletStore.UpdateBalance(ctx, transfer.ReceiverWalletID.Value, receiverWallet.Balance+transfer.Amount) +// if err != nil { +// return domain.Transfer{}, err +// } + +// // Mark transfer as completed +// if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, "completed"); err != nil { +// return domain.Transfer{}, err +// } + +// // Send notifications +// go s.SendTransferNotification(ctx, senderWallet, receiverWallet, +// domain.RoleFromString(transfer.SenderWalletID.Value), +// domain.RoleFromString(transfer.ReceiverWalletID.Value), +// transfer.Amount) + +// return transfer, nil +// } + +// func (s *Service) RejectTransfer(ctx context.Context, approval domain.ApprovalRequest) error { +// // Update approval record +// if err := s.approvalStore.UpdateApprovalStatus(ctx, approval.TransferID, "rejected", approval.Comments); err != nil { +// return err +// } + +// // Update transfer status +// if err := s.transferStore.UpdateTransferStatus(ctx, approval.TransferID, "rejected"); err != nil { +// return err +// } + +// // Notify the initiator +// go s.notifyInitiator(ctx, approval.TransferID, approval.Comments) + +// return nil +// } + +// func (s *Service) GetPendingApprovals(ctx context.Context) ([]domain.TransferDetail, error) { +// return s.approvalStore.GetPendingApprovals(ctx) +// } + +// func (s *Service) GetTransferApprovalHistory(ctx context.Context, transferID int64) ([]domain.TransactionApproval, error) { +// return s.approvalStore.GetApprovalsByTransfer(ctx, transferID) +// } + +// func (s *Service) notifyApprovers(ctx context.Context, transferID int64) { +// // Get approvers (could be from a config or role-based) +// approvers, err := s.userStore.GetUsersByRole(ctx, "approver") +// if err != nil { +// s.logger.Error("failed to get approvers", zap.Error(err)) +// return +// } + +// transfer, err := s.transferStore.GetTransferByID(ctx, transferID) +// if err != nil { +// s.logger.Error("failed to get transfer for notification", zap.Error(err)) +// return +// } + +// for _, approver := range approvers { +// notification := &domain.Notification{ +// RecipientID: approver.ID, +// Type: domain.NOTIFICATION_TYPE_APPROVAL_REQUIRED, +// Level: domain.NotificationLevelWarning, +// Reciever: domain.NotificationRecieverSideAdmin, +// DeliveryChannel: domain.DeliveryChannelInApp, +// Payload: domain.NotificationPayload{ +// Headline: "Transfer Approval Required", +// Message: fmt.Sprintf("Transfer #%d requires your approval", transfer.ID), +// }, +// Priority: 1, +// Metadata: []byte(fmt.Sprintf(`{ +// "transfer_id": %d, +// "amount": %d, +// "notification_type": "approval_request" +// }`, transfer.ID, transfer.Amount)), +// } + +// if err := s.notificationStore.SendNotification(ctx, notification); err != nil { +// s.logger.Error("failed to send approval notification", +// zap.Int64("approver_id", approver.ID), +// zap.Error(err)) +// } +// } +// } + +// func (s *Service) notifyInitiator(ctx context.Context, transferID int64, comments string) { +// transfer, err := s.transferStore.GetTransferByID(ctx, transferID) +// if err != nil { +// s.logger.Error("failed to get transfer for notification", zap.Error(err)) +// return +// } + +// // Determine who initiated the transfer (could be cashier or user) +// recipientID := transfer.CashierID.Value +// if !transfer.CashierID.Valid { +// // If no cashier, assume user initiated +// wallet, err := s.GetWalletByID(ctx, transfer.SenderWalletID.Value) +// if err != nil { +// s.logger.Error("failed to get wallet for notification", zap.Error(err)) +// return +// } +// recipientID = wallet.UserID +// } + +// notification := &domain.Notification{ +// RecipientID: recipientID, +// Type: domain.NOTIFICATION_TYPE_TRANSFER_REJECTED, +// Level: domain.NotificationLevelWarning, +// Reciever: domain.NotificationRecieverSideCustomer, +// DeliveryChannel: domain.DeliveryChannelInApp, +// Payload: domain.NotificationPayload{ +// Headline: "Transfer Rejected", +// Message: fmt.Sprintf("Your transfer #%d was rejected. Comments: %s", transfer.ID, comments), +// }, +// Priority: 2, +// Metadata: []byte(fmt.Sprintf(`{ +// "transfer_id": %d, +// "notification_type": "transfer_rejected" +// }`, transfer.ID)), +// } + +// if err := s.notificationStore.SendNotification(ctx, notification); err != nil { +// s.logger.Error("failed to send rejection notification", +// zap.Int64("recipient_id", recipientID), +// zap.Error(err)) +// } +// } diff --git a/internal/web_server/handlers/institutions.go b/internal/web_server/handlers/institutions.go index bd723c0..8e171d7 100644 --- a/internal/web_server/handlers/institutions.go +++ b/internal/web_server/handlers/institutions.go @@ -1,8 +1,11 @@ package handlers import ( + "strconv" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/gofiber/fiber/v2" + "go.uber.org/zap" ) // @Summary Create a new bank @@ -120,16 +123,63 @@ func (h *Handler) DeleteBank(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusNoContent) } -// @Summary List all banks +// @Summary List all banks with pagination and filtering // @Tags Institutions - Banks // @Produce json -// @Success 200 {array} domain.Bank +// @Param country_id query integer false "Filter by country ID" +// @Param is_active query boolean false "Filter by active status" +// @Param search query string false "Search term for bank name or code" +// @Param page query integer false "Page number" default(1) +// @Param page_size query integer false "Items per page" default(50) maximum(100) +// @Success 200 {object} domain.InstResponse +// @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/banks [get] func (h *Handler) ListBanks(c *fiber.Ctx) error { - banks, err := h.instSvc.List(c.Context()) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + // Parse query parameters + countryID, _ := strconv.Atoi(c.Query("country_id")) + var countryIDPtr *int + if c.Query("country_id") != "" { + countryIDPtr = &countryID } - return c.JSON(banks) + + isActive, _ := strconv.ParseBool(c.Query("is_active")) + var isActivePtr *bool + if c.Query("is_active") != "" { + isActivePtr = &isActive + } + + var searchTermPtr *string + if searchTerm := c.Query("search"); searchTerm != "" { + searchTermPtr = &searchTerm + } + + page, _ := strconv.Atoi(c.Query("page", "1")) + pageSize, _ := strconv.Atoi(c.Query("page_size", "50")) + + banks, pagination, err := h.instSvc.List( + c.Context(), + countryIDPtr, + isActivePtr, + searchTermPtr, + page, + pageSize, + ) + if err != nil { + h.mongoLoggerSvc.Error("failed to list banks", + zap.Error(err), + zap.Any("query_params", c.Queries()), + ) + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to retrieve banks", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.InstResponse{ + Message: "Banks retrieved successfully", + Status: "success", + Data: banks, + Pagination: pagination, + }) } diff --git a/internal/web_server/handlers/mongoLogger.go b/internal/web_server/handlers/mongoLogger.go index 1a139f8..2ccc7a2 100644 --- a/internal/web_server/handlers/mongoLogger.go +++ b/internal/web_server/handlers/mongoLogger.go @@ -12,24 +12,87 @@ import ( ) // GetLogsHandler godoc -// @Summary Retrieve latest application logs -// @Description Fetches the 100 most recent application logs from MongoDB +// @Summary Retrieve application logs with filtering and pagination +// @Description Fetches application logs from MongoDB with pagination, level filtering, and search // @Tags Logs // @Produce json -// @Success 200 {array} domain.LogEntry "List of application logs" +// @Param level query string false "Filter logs by level (debug, info, warn, error, dpanic, panic, fatal)" +// @Param search query string false "Search term to match against message or fields" +// @Param page query int false "Page number for pagination (default: 1)" default(1) +// @Param limit query int false "Number of items per page (default: 50, max: 100)" default(50) +// @Success 200 {object} domain.LogResponse "Paginated list of application logs" +// @Failure 400 {object} domain.ErrorResponse "Invalid request parameters" // @Failure 500 {object} domain.ErrorResponse "Internal server error" // @Router /api/v1/logs [get] func GetLogsHandler(appCtx context.Context) fiber.Handler { return func(c *fiber.Ctx) error { + // Get query parameters + levelFilter := c.Query("level") + searchTerm := c.Query("search") + page := c.QueryInt("page", 1) + limit := c.QueryInt("limit", 50) + + // Validate pagination parameters + if page < 1 { + page = 1 + } + if limit < 1 || limit > 100 { + limit = 50 + } + + // Calculate skip value for pagination + skip := (page - 1) * limit + client, err := mongo.Connect(appCtx, options.Client().ApplyURI(os.Getenv("MONGODB_URL"))) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "MongoDB connection failed: "+err.Error()) } + defer client.Disconnect(appCtx) collection := client.Database("logdb").Collection("applogs") - filter := bson.M{} - opts := options.Find().SetSort(bson.D{{Key: "timestamp", Value: -1}}).SetLimit(100) + // Build filter + filter := bson.M{} + + // Add level filter if specified + if levelFilter != "" { + validLevels := map[string]bool{ + "debug": true, + "info": true, + "warn": true, + "error": true, + "dpanic": true, + "panic": true, + "fatal": true, + } + + if !validLevels[levelFilter] { + return fiber.NewError(fiber.StatusBadRequest, "Invalid log level specified") + } + filter["level"] = levelFilter + } + + // Add search filter if specified + if searchTerm != "" { + filter["$or"] = []bson.M{ + {"message": bson.M{"$regex": searchTerm, "$options": "i"}}, + {"fields": bson.M{"$elemMatch": bson.M{"$regex": searchTerm, "$options": "i"}}}, + } + } + + // Find options with pagination and sorting + opts := options.Find(). + SetSort(bson.D{{Key: "timestamp", Value: -1}}). + SetSkip(int64(skip)). + SetLimit(int64(limit)) + + // Get total count for pagination metadata + total, err := collection.CountDocuments(appCtx, filter) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to count logs: "+err.Error()) + } + + // Find logs cursor, err := collection.Find(appCtx, filter, opts) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch logs: "+err.Error()) @@ -41,6 +104,24 @@ func GetLogsHandler(appCtx context.Context) fiber.Handler { return fiber.NewError(fiber.StatusInternalServerError, "Cursor decoding error: "+err.Error()) } - return c.JSON(logs) + // Calculate pagination metadata + totalPages := int(total) / limit + if int(total)%limit != 0 { + totalPages++ + } + + // Prepare response + response := domain.LogResponse{ + Message: "Logs fetched successfully", + Data: logs, + Pagination: domain.Pagination{ + Total: int(total), + TotalPages: totalPages, + CurrentPage: page, + Limit: limit, + }, + } + + return c.JSON(response) } } diff --git a/internal/web_server/handlers/report.go b/internal/web_server/handlers/report.go index 3e68773..e15faae 100644 --- a/internal/web_server/handlers/report.go +++ b/internal/web_server/handlers/report.go @@ -3,13 +3,16 @@ package handlers import ( "context" "fmt" + "math" "os" + "sort" "strconv" "strings" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/gofiber/fiber/v2" + "go.uber.org/zap" ) // GetDashboardReport returns a comprehensive dashboard report @@ -185,18 +188,45 @@ func (h *Handler) DownloadReportFile(c *fiber.Ctx) error { // ListReportFiles godoc // @Summary List available report CSV files -// @Description Returns a list of all generated report CSV files available for download +// @Description Returns a paginated list of generated report CSV files with search capability // @Tags Reports // @Produce json -// @Success 200 {object} domain.Response{data=[]string} "List of CSV report filenames" +// @Param search query string false "Search term to filter filenames" +// @Param page query int false "Page number (default: 1)" default(1) +// @Param limit query int false "Items per page (default: 20, max: 100)" default(20) +// @Success 200 {object} domain.PaginatedFileResponse "Paginated list of CSV report filenames" +// @Failure 400 {object} domain.ErrorResponse "Invalid pagination parameters" // @Failure 500 {object} domain.ErrorResponse "Failed to read report directory" // @Router /api/v1/report-files/list [get] func (h *Handler) ListReportFiles(c *fiber.Ctx) error { reportDir := "reports" + searchTerm := c.Query("search") + page := c.QueryInt("page", 1) + limit := c.QueryInt("limit", 20) + + // Validate pagination parameters + if page < 1 { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid page number", + Error: "Page must be greater than 0", + }) + } + + if limit < 1 || limit > 100 { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid limit value", + Error: "Limit must be between 1 and 100", + }) + } // Create the reports directory if it doesn't exist if _, err := os.Stat(reportDir); os.IsNotExist(err) { if err := os.MkdirAll(reportDir, os.ModePerm); err != nil { + h.mongoLoggerSvc.Error("failed to create report directory", + zap.Int64("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to create report directory", Error: err.Error(), @@ -206,23 +236,74 @@ func (h *Handler) ListReportFiles(c *fiber.Ctx) error { files, err := os.ReadDir(reportDir) if err != nil { + h.mongoLoggerSvc.Error("failed to read report directory", + zap.Int64("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to read report directory", Error: err.Error(), }) } - var reportFiles []string + var allFiles []string for _, file := range files { if !file.IsDir() && strings.HasSuffix(file.Name(), ".csv") { - reportFiles = append(reportFiles, file.Name()) + // Apply search filter if provided + if searchTerm == "" || strings.Contains(strings.ToLower(file.Name()), strings.ToLower(searchTerm)) { + allFiles = append(allFiles, file.Name()) + } } } - return c.Status(fiber.StatusOK).JSON(domain.Response{ - StatusCode: 200, - Message: "Report files retrieved successfully", - Data: reportFiles, - Success: true, + // Sort files by name (descending to show newest first) + sort.Slice(allFiles, func(i, j int) bool { + return allFiles[i] > allFiles[j] + }) + + // Calculate pagination values + total := len(allFiles) + startIdx := (page - 1) * limit + endIdx := startIdx + limit + + // Adjust end index if it exceeds the slice length + if endIdx > total { + endIdx = total + } + + // Handle case where start index is beyond available items + if startIdx >= total { + return c.Status(fiber.StatusOK).JSON(domain.PaginatedFileResponse{ + Response: domain.Response{ + StatusCode: fiber.StatusOK, + Message: "No files found for the requested page", + Success: true, + }, + Data: []string{}, + Pagination: domain.Pagination{ + Total: total, + TotalPages: int(math.Ceil(float64(total) / float64(limit))), + CurrentPage: page, + Limit: limit, + }, + }) + } + + paginatedFiles := allFiles[startIdx:endIdx] + + return c.Status(fiber.StatusOK).JSON(domain.PaginatedFileResponse{ + Response: domain.Response{ + StatusCode: fiber.StatusOK, + Message: "Report files retrieved successfully", + Success: true, + }, + Data: paginatedFiles, + Pagination: domain.Pagination{ + Total: total, + TotalPages: int(math.Ceil(float64(total) / float64(limit))), + CurrentPage: page, + Limit: limit, + }, }) } diff --git a/internal/web_server/handlers/shop_handler.go b/internal/web_server/handlers/shop_handler.go index af2b2d5..46884cd 100644 --- a/internal/web_server/handlers/shop_handler.go +++ b/internal/web_server/handlers/shop_handler.go @@ -18,7 +18,7 @@ import ( // @Accept json // @Produce json // @Param createBet body domain.ShopBetReq true "create bet" -// @Success 200 {object} TransactionRes +// @Success 200 {object} domain.ShopTransactionRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /shop/bet [post] @@ -57,7 +57,7 @@ func (h *Handler) CreateShopBet(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param createBet body domain.CashoutReq true "cashout bet" -// @Success 200 {object} TransactionRes +// @Success 200 {object} domain.ShopTransactionRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /shop/bet/{id} [get] @@ -91,7 +91,7 @@ func (h *Handler) GetShopBetByBetID(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param cashoutBet body domain.CashoutReq true "cashout bet" -// @Success 200 {object} TransactionRes +// @Success 200 {object} domain.ShopTransactionRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /shop/bet/{id}/cashout [post] @@ -139,7 +139,7 @@ func (h *Handler) CashoutBet(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param cashoutBet body domain.CashoutReq true "cashout bet" -// @Success 200 {object} TransactionRes +// @Success 200 {object} domain.ShopTransactionRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /shop/cashout [post] @@ -185,7 +185,7 @@ func (h *Handler) CashoutByCashoutID(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param createBet body domain.CashoutReq true "cashout bet" -// @Success 200 {object} TransactionRes +// @Success 200 {object} domain.ShopTransactionRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /shop/cashout/{id} [get] @@ -262,7 +262,7 @@ func (h *Handler) DepositForCustomer(c *fiber.Ctx) error { // @Tags transaction // @Accept json // @Produce json -// @Success 200 {array} TransactionRes +// @Success 200 {array} domain.ShopTransactionRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /shop/transaction [get] @@ -337,7 +337,7 @@ func (h *Handler) GetAllTransactions(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param id path int true "Transaction ID" -// @Success 200 {object} TransactionRes +// @Success 200 {object} domain.ShopTransactionRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /shop/transaction/{id} [get] @@ -366,7 +366,7 @@ func (h *Handler) GetTransactionByID(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param id path int true "Transaction ID" -// @Success 200 {object} TransactionRes +// @Success 200 {object} domain.ShopTransactionRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /shop/transaction/{id}/bet [get] diff --git a/internal/web_server/handlers/virtual_games_hadlers.go b/internal/web_server/handlers/virtual_games_hadlers.go index f3627b2..3a603bc 100644 --- a/internal/web_server/handlers/virtual_games_hadlers.go +++ b/internal/web_server/handlers/virtual_games_hadlers.go @@ -1,9 +1,13 @@ package handlers import ( + "encoding/json" + "errors" + "fmt" "strconv" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" ) @@ -106,66 +110,243 @@ func (h *Handler) HandlePlayerInfo(c *fiber.Ctx) error { } func (h *Handler) HandleBet(c *fiber.Ctx) error { - var req domain.PopOKBetRequest - if err := c.BodyParser(&req); err != nil { + // Read the raw body to avoid parsing issues + body := c.Body() + if len(body) == 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ - Message: "Invalid bet request", - Error: err.Error(), + Message: "Empty request body", + Error: "Request body cannot be empty", }) - // return fiber.NewError(fiber.StatusBadRequest, "Invalid bet request") } - resp, _ := h.virtualGameSvc.ProcessBet(c.Context(), &req) - // if err != nil { - // code := fiber.StatusInternalServerError - // // if err.Error() == "invalid token" { - // // code = fiber.StatusUnauthorized - // // } else if err.Error() == "insufficient balance" { - // // code = fiber.StatusBadRequest - // // } - // return fiber.NewError(code, err.Error()) - // } + // Try to identify the provider based on the request structure + provider, err := identifyBetProvider(body) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Unrecognized request format", + Error: err.Error(), + }) + } - return response.WriteJSON(c, fiber.StatusOK, "Bet processed", resp, nil) + switch provider { + case "veli": + var req domain.BetRequest + if err := json.Unmarshal(body, &req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid Veli bet request", + Error: err.Error(), + }) + } + + res, err := h.veliVirtualGameSvc.ProcessBet(c.Context(), req) + if err != nil { + if errors.Is(err, veli.ErrDuplicateTransaction) { + return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{ + Message: "Duplicate transaction", + Error: "DUPLICATE_TRANSACTION", + }) + } + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Veli bet processing failed", + Error: err.Error(), + }) + } + return c.JSON(res) + + case "popok": + var req domain.PopOKBetRequest + if err := json.Unmarshal(body, &req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid PopOK bet request", + Error: err.Error(), + }) + } + + resp, err := h.virtualGameSvc.ProcessBet(c.Context(), &req) + if err != nil { + code := fiber.StatusInternalServerError + switch err.Error() { + case "invalid token": + code = fiber.StatusUnauthorized + case "insufficient balance": + code = fiber.StatusBadRequest + } + return c.Status(code).JSON(domain.ErrorResponse{ + Message: "PopOK bet processing failed", + Error: err.Error(), + }) + } + return c.JSON(resp) + + default: + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Unsupported provider", + Error: "Request format doesn't match any supported provider", + }) + } } +// identifyProvider examines the request body to determine the provider + +// WinHandler godoc +// @Summary Handle win callback (Veli or PopOK) +// @Description Processes win callbacks from either Veli or PopOK providers by auto-detecting the format +// @Tags Wins +// @Accept json +// @Produce json +// @Success 200 {object} interface{} "Win processing result" +// @Failure 400 {object} domain.ErrorResponse "Invalid request format" +// @Failure 401 {object} domain.ErrorResponse "Authentication failed" +// @Failure 409 {object} domain.ErrorResponse "Duplicate transaction" +// @Failure 500 {object} domain.ErrorResponse "Internal server error" +// @Router /api/v1/win [post] func (h *Handler) HandleWin(c *fiber.Ctx) error { - var req domain.PopOKWinRequest - if err := c.BodyParser(&req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid win request") + // Read the raw body to avoid parsing issues + body := c.Body() + if len(body) == 0 { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Empty request body", + Error: "Request body cannot be empty", + }) } - resp, _ := h.virtualGameSvc.ProcessWin(c.Context(), &req) - // if err != nil { - // code := fiber.StatusInternalServerError - // if err.Error() == "invalid token" { - // code = fiber.StatusUnauthorized - // } - // return fiber.NewError(code, err.Error()) - // } + // Try to identify the provider based on the request structure + provider, err := identifyWinProvider(body) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Unrecognized request format", + Error: err.Error(), + }) + } - return response.WriteJSON(c, fiber.StatusOK, "Win processed", resp, nil) + switch provider { + case "veli": + var req domain.WinRequest + if err := json.Unmarshal(body, &req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid Veli win request", + Error: err.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{ + Message: "Duplicate transaction", + Error: "DUPLICATE_TRANSACTION", + }) + } + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Veli win processing failed", + Error: err.Error(), + }) + } + return c.JSON(res) + + case "popok": + var req domain.PopOKWinRequest + if err := json.Unmarshal(body, &req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid PopOK win request", + Error: err.Error(), + }) + } + + resp, err := h.virtualGameSvc.ProcessWin(c.Context(), &req) + if err != nil { + code := fiber.StatusInternalServerError + if err.Error() == "invalid token" { + code = fiber.StatusUnauthorized + } + return c.Status(code).JSON(domain.ErrorResponse{ + Message: "PopOK win processing failed", + Error: err.Error(), + }) + } + return c.JSON(resp) + + default: + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Unsupported provider", + Error: "Request format doesn't match any supported provider", + }) + } } func (h *Handler) HandleCancel(c *fiber.Ctx) error { - var req domain.PopOKCancelRequest - if err := c.BodyParser(&req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid cancel request") + body := c.Body() + if len(body) == 0 { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Empty request body", + Error: "Request body cannot be empty", + }) } - resp, _ := h.virtualGameSvc.ProcessCancel(c.Context(), &req) - // if err != nil { - // code := fiber.StatusInternalServerError - // switch err.Error() { - // case "invalid token": - // code = fiber.StatusUnauthorized - // case "original bet not found", "invalid original transaction": - // code = fiber.StatusBadRequest - // } - // return fiber.NewError(code, err.Error()) - // } + provider, err := identifyCancelProvider(body) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Unrecognized request format", + Error: err.Error(), + }) + } - return response.WriteJSON(c, fiber.StatusOK, "Cancel processed", resp, nil) + switch provider { + case "veli": + var req domain.CancelRequest + if err := json.Unmarshal(body, &req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid Veli cancel request", + Error: err.Error(), + }) + } + + res, err := h.veliVirtualGameSvc.ProcessCancel(c.Context(), req) + if err != nil { + if errors.Is(err, veli.ErrDuplicateTransaction) { + return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{ + Message: "Duplicate transaction", + Error: "DUPLICATE_TRANSACTION", + }) + } + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Veli cancel processing failed", + Error: err.Error(), + }) + } + return c.JSON(res) + + case "popok": + var req domain.PopOKCancelRequest + if err := json.Unmarshal(body, &req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid PopOK cancel request", + Error: err.Error(), + }) + } + + resp, err := h.virtualGameSvc.ProcessCancel(c.Context(), &req) + if err != nil { + code := fiber.StatusInternalServerError + switch err.Error() { + case "invalid token": + code = fiber.StatusUnauthorized + case "original bet not found", "invalid original transaction": + code = fiber.StatusBadRequest + } + return c.Status(code).JSON(domain.ErrorResponse{ + Message: "PopOK cancel processing failed", + Error: err.Error(), + }) + } + return c.JSON(resp) + + default: + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Unsupported provider", + Error: "Request format doesn't match any supported provider", + }) + } } // GetGameList godoc @@ -176,14 +357,14 @@ func (h *Handler) HandleCancel(c *fiber.Ctx) error { // @Produce json // @Param currency query string false "Currency (e.g. USD, ETB)" default(USD) // @Success 200 {array} domain.PopOKGame -// @Failure 502 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse // @Router /popok/games [get] func (h *Handler) GetGameList(c *fiber.Ctx) error { currency := c.Query("currency", "ETB") // fallback default games, err := h.virtualGameSvc.ListGames(c.Context(), currency) if err != nil { - return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Falied to fetch games", Error: err.Error(), }) @@ -331,3 +512,86 @@ func (h *Handler) ListFavorites(c *fiber.Ctx) error { } return c.Status(fiber.StatusOK).JSON(games) } + +func identifyBetProvider(body []byte) (string, error) { + // Check for Veli signature fields + var veliCheck struct { + TransactionID string `json:"transaction_id"` + GameID string `json:"game_id"` + } + if json.Unmarshal(body, &veliCheck) == nil { + if veliCheck.TransactionID != "" && veliCheck.GameID != "" { + return "veli", nil + } + } + + // Check for PopOK signature fields + var popokCheck struct { + Token string `json:"token"` + PlayerID string `json:"player_id"` + BetAmount float64 `json:"bet_amount"` + } + if json.Unmarshal(body, &popokCheck) == nil { + if popokCheck.Token != "" && popokCheck.PlayerID != "" { + return "popok", nil + } + } + + return "", fmt.Errorf("could not identify provider from request structure") +} + +// identifyWinProvider examines the request body to determine the provider for win callbacks +func identifyWinProvider(body []byte) (string, error) { + // Check for Veli signature fields + var veliCheck struct { + TransactionID string `json:"transaction_id"` + WinAmount float64 `json:"win_amount"` + } + if json.Unmarshal(body, &veliCheck) == nil { + if veliCheck.TransactionID != "" && veliCheck.WinAmount > 0 { + return "veli", nil + } + } + + // Check for PopOK signature fields + var popokCheck struct { + Token string `json:"token"` + PlayerID string `json:"player_id"` + WinAmount float64 `json:"win_amount"` + } + if json.Unmarshal(body, &popokCheck) == nil { + if popokCheck.Token != "" && popokCheck.PlayerID != "" { + return "popok", nil + } + } + + return "", fmt.Errorf("could not identify provider from request structure") +} + +func identifyCancelProvider(body []byte) (string, error) { + // Check for Veli cancel signature + var veliCheck struct { + TransactionID string `json:"transaction_id"` + OriginalTxID string `json:"original_transaction_id"` + CancelReason string `json:"cancel_reason"` + } + if json.Unmarshal(body, &veliCheck) == nil { + if veliCheck.TransactionID != "" && veliCheck.OriginalTxID != "" { + return "veli", nil + } + } + + // Check for PopOK cancel signature + var popokCheck struct { + Token string `json:"token"` + PlayerID string `json:"player_id"` + OriginalTxID string `json:"original_transaction_id"` + } + if json.Unmarshal(body, &popokCheck) == nil { + if popokCheck.Token != "" && popokCheck.PlayerID != "" && popokCheck.OriginalTxID != "" { + return "popok", nil + } + } + + return "", fmt.Errorf("could not identify cancel provider from request structure") +} diff --git a/test.html b/test.html new file mode 100644 index 0000000..554893a --- /dev/null +++ b/test.html @@ -0,0 +1,112 @@ + + + + + + + Attention Required! | Cloudflare + + + + + + + + + + + +
+ + +
+
+

Sorry, you have been blocked

+

+ You are unable to access pokgaming.com +

+
+ +
+
+
+ +
+
+
+ +
+
+
+

Why have I been blocked?

+

+ This website is using a security service to protect itself from online attacks. + The action you just performed triggered the security solution. + There are several actions that could trigger this block including submitting a certain word or phrase, + a SQL command or malformed data. +

+
+ +
+

What can I do to resolve this?

+

+ You can email the site owner to let them know you were blocked. + Please include what you were doing when this page came up and the Cloudflare Ray ID + found at the bottom of this page. +

+
+
+
+ + +
+
+ + +