From bdf057e01df83673c1b2bc36582556fa8fb66822 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Sun, 22 Jun 2025 21:49:16 +0300 Subject: [PATCH] institution service + more PopOK callback --- cmd/main.go | 4 + db/migrations/000001_fortune.up.sql | 17 + db/query/institutions.sql | 60 + docs/docs.go | 1025 ++++++++++------- docs/swagger.json | 1025 ++++++++++------- docs/swagger.yaml | 667 ++++++----- gen/db/institutions.sql.go | 251 ++++ gen/db/models.go | 18 + internal/domain/chapa.go | 47 +- internal/domain/institutions.go | 21 + internal/domain/notification.go | 11 +- internal/domain/virtual_game.go | 9 + internal/repository/institutions.go | 139 +++ internal/services/chapa/client.go | 62 +- internal/services/chapa/service.go | 117 +- internal/services/institutions/port.go | 1 + internal/services/institutions/service.go | 44 + internal/services/issues/port.go | 1 + internal/services/issues/service.go | 1 + internal/services/virtualGame/port.go | 2 + internal/services/virtualGame/service.go | 161 +++ internal/web_server/app.go | 4 + internal/web_server/handlers/bet_handler.go | 14 +- internal/web_server/handlers/chapa.go | 10 +- internal/web_server/handlers/handlers.go | 4 + internal/web_server/handlers/institutions.go | 135 +++ .../web_server/handlers/recommendation.go | 27 +- .../web_server/handlers/transfer_handler.go | 70 +- .../handlers/virtual_games_hadlers.go | 42 + internal/web_server/routes.go | 5 +- 30 files changed, 2696 insertions(+), 1298 deletions(-) create mode 100644 db/query/institutions.sql create mode 100644 gen/db/institutions.sql.go create mode 100644 internal/domain/institutions.go create mode 100644 internal/repository/institutions.go create mode 100644 internal/services/institutions/port.go create mode 100644 internal/services/institutions/service.go create mode 100644 internal/services/issues/port.go create mode 100644 internal/services/issues/service.go create mode 100644 internal/web_server/handlers/institutions.go diff --git a/cmd/main.go b/cmd/main.go index ad29b18..9d04181 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -35,6 +35,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" @@ -162,6 +163,8 @@ func main() { go httpserver.SetupReportCronJobs(context.Background(), reportSvc) + bankRepository := repository.NewBankRepository(store) + instSvc := institutions.New(bankRepository) // Initialize report worker with CSV exporter // csvExporter := infrastructure.CSVExporter{ // ExportPath: cfg.ReportExportPath, // Make sure to add this to your config @@ -200,6 +203,7 @@ func main() { // Initialize and start HTTP server app := httpserver.NewApp( + instSvc, currSvc, cfg.Port, v, diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index b57d127..29f9c7f 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -112,6 +112,23 @@ CREATE TABLE IF NOT EXISTS ticket_outcomes ( status INT NOT NULL DEFAULT 0, expires TIMESTAMP NOT NULL ); +CREATE TABLE IF NOT EXISTS banks ( + id BIGSERIAL PRIMARY KEY, + slug VARCHAR(255) NOT NULL UNIQUE, + swift VARCHAR(20) NOT NULL, + name VARCHAR(255) NOT NULL, + acct_length INT NOT NULL, + country_id INT NOT NULL, + is_mobilemoney INT, -- nullable integer (0 or 1) + is_active INT NOT NULL, -- 0 or 1 + is_rtgs INT NOT NULL, -- 0 or 1 + active INT NOT NULL, -- 0 or 1 + is_24hrs INT, -- nullable integer (0 or 1) + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + currency VARCHAR(10) NOT NULL, + bank_logo TEXT -- URL or base64 string +); CREATE TABLE IF NOT EXISTS wallets ( id BIGSERIAL PRIMARY KEY, balance BIGINT NOT NULL DEFAULT 0, diff --git a/db/query/institutions.sql b/db/query/institutions.sql new file mode 100644 index 0000000..d6faada --- /dev/null +++ b/db/query/institutions.sql @@ -0,0 +1,60 @@ +-- name: CreateBank :one +INSERT INTO banks ( + slug, + swift, + name, + acct_length, + country_id, + is_mobilemoney, + is_active, + is_rtgs, + active, + is_24hrs, + created_at, + updated_at, + currency, + bank_logo +) +VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, $11, $12 +) +RETURNING *; + +-- name: GetBankByID :one +SELECT * +FROM banks +WHERE id = $1; + +-- name: GetAllBanks :many +SELECT * +FROM banks +WHERE ( + country_id = sqlc.narg('country_id') + OR sqlc.narg('country_id') IS NULL + ) + AND ( + is_active = sqlc.narg('is_active') + OR sqlc.narg('is_active') IS NULL + ); + +-- name: UpdateBank :one +UPDATE banks +SET slug = COALESCE(sqlc.narg(slug), slug), + swift = COALESCE(sqlc.narg(swift), swift), + name = COALESCE(sqlc.narg(name), name), + acct_length = COALESCE(sqlc.narg(acct_length), acct_length), + country_id = COALESCE(sqlc.narg(country_id), country_id), + is_mobilemoney = COALESCE(sqlc.narg(is_mobilemoney), is_mobilemoney), + is_active = COALESCE(sqlc.narg(is_active), is_active), + is_rtgs = COALESCE(sqlc.narg(is_rtgs), is_rtgs), + active = COALESCE(sqlc.narg(active), active), + is_24hrs = COALESCE(sqlc.narg(is_24hrs), is_24hrs), + updated_at = CURRENT_TIMESTAMP, + currency = COALESCE(sqlc.narg(currency), currency), + bank_logo = COALESCE(sqlc.narg(bank_logo), bank_logo) +WHERE id = $1 +RETURNING *; + +-- name: DeleteBank :exec +DELETE FROM banks +WHERE id = $1; diff --git a/docs/docs.go b/docs/docs.go index 754c307..1edc2a8 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -304,6 +304,217 @@ const docTemplate = `{ } } }, + "/api/v1/banks": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Institutions - Banks" + ], + "summary": "List all banks", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Bank" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Institutions - Banks" + ], + "summary": "Create a new bank", + "parameters": [ + { + "description": "Bank Info", + "name": "bank", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.Bank" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/domain.Bank" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/banks/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Institutions - Banks" + ], + "summary": "Get a bank by ID", + "parameters": [ + { + "type": "integer", + "description": "Bank ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Bank" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Institutions - Banks" + ], + "summary": "Update a bank", + "parameters": [ + { + "type": "integer", + "description": "Bank ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Bank Info", + "name": "bank", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.Bank" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Bank" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Institutions - Banks" + ], + "summary": "Delete a bank", + "parameters": [ + { + "type": "integer", + "description": "Bank ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "Deleted successfully", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/chapa/banks": { "get": { "description": "Get list of banks supported by Chapa", @@ -841,44 +1052,6 @@ const docTemplate = `{ } } }, - "/api/v1/virtual-games/recommendations/{userID}": { - "get": { - "description": "Returns a list of recommended virtual games for a specific user", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Recommendations" - ], - "summary": "Get virtual game recommendations", - "parameters": [ - { - "type": "string", - "description": "User ID", - "name": "userID", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Recommended games fetched successfully", - "schema": { - "$ref": "#/definitions/domain.RecommendationSuccessfulResponse" - } - }, - "500": { - "description": "Failed to fetch recommendations", - "schema": { - "$ref": "#/definitions/domain.RecommendationErrorResponse" - } - } - } - } - }, "/api/v1/webhooks/alea": { "post": { "description": "Handles webhook callbacks from Alea Play virtual games for bet settlement", @@ -1084,269 +1257,6 @@ const docTemplate = `{ } } }, - "/bet": { - "get": { - "description": "Gets all the bets", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Gets all bets", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.BetRes" - } - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - }, - "post": { - "description": "Creates a bet", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Create a bet", - "parameters": [ - { - "description": "Creates bet", - "name": "createBet", - "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" - } - } - } - } - }, - "/bet/cashout/{id}": { - "get": { - "description": "Gets a single bet by cashout id", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Gets bet by cashout id", - "parameters": [ - { - "type": "string", - "description": "cashout ID", - "name": "id", - "in": "path", - "required": true - } - ], - "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" - } - } - } - } - }, - "/bet/{id}": { - "get": { - "description": "Gets a single bet by id", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Gets bet by id", - "parameters": [ - { - "type": "integer", - "description": "Bet ID", - "name": "id", - "in": "path", - "required": true - } - ], - "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" - } - } - } - }, - "delete": { - "description": "Deletes bet by id", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Deletes bet by id", - "parameters": [ - { - "type": "integer", - "description": "Bet ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - }, - "patch": { - "description": "Updates the cashed out field", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Updates the cashed out field", - "parameters": [ - { - "type": "integer", - "description": "Bet ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Updates Cashed Out", - "name": "updateCashOut", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handlers.UpdateCashOutReq" - } - } - ], - "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" - } - } - } - } - }, "/branch": { "get": { "description": "Gets all branches", @@ -3083,52 +2993,6 @@ const docTemplate = `{ } } }, - "/random/bet": { - "post": { - "description": "Generate a random bet", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Generate a random bet", - "parameters": [ - { - "description": "Create Random bet", - "name": "createBet", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.RandomBetReq" - } - } - ], - "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" - } - } - } - } - }, "/referral/settings": { "get": { "security": [ @@ -3402,6 +3266,315 @@ const docTemplate = `{ } } }, + "/sport/bet": { + "get": { + "description": "Gets all the bets", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets all bets", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.BetRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "post": { + "description": "Creates a bet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Create a bet", + "parameters": [ + { + "description": "Creates bet", + "name": "createBet", + "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/cashout/{id}": { + "get": { + "description": "Gets a single bet by cashout id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets bet by cashout id", + "parameters": [ + { + "type": "string", + "description": "cashout ID", + "name": "id", + "in": "path", + "required": true + } + ], + "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", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets bet by id", + "parameters": [ + { + "type": "integer", + "description": "Bet ID", + "name": "id", + "in": "path", + "required": true + } + ], + "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" + } + } + } + }, + "delete": { + "description": "Deletes bet by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Deletes bet by id", + "parameters": [ + { + "type": "integer", + "description": "Bet ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "patch": { + "description": "Updates the cashed out field", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Updates the cashed out field", + "parameters": [ + { + "type": "integer", + "description": "Bet ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updates Cashed Out", + "name": "updateCashOut", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.UpdateCashOutReq" + } + } + ], + "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" + } + } + } + } + }, + "/sport/random/bet": { + "post": { + "description": "Generate a random bet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Generate a random bet", + "parameters": [ + { + "description": "Create Random bet", + "name": "createBet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.RandomBetReq" + } + } + ], + "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" + } + } + } + } + }, "/supportedOperation": { "get": { "description": "Gets all supported operations", @@ -4786,6 +4959,59 @@ const docTemplate = `{ } } }, + "domain.Bank": { + "type": "object", + "properties": { + "acct_length": { + "type": "integer" + }, + "active": { + "type": "integer" + }, + "bank_logo": { + "description": "URL or base64", + "type": "string" + }, + "country_id": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_24hrs": { + "description": "nullable", + "type": "integer" + }, + "is_active": { + "type": "integer" + }, + "is_mobilemoney": { + "description": "nullable", + "type": "integer" + }, + "is_rtgs": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "swift": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "domain.BetOutcome": { "type": "object", "properties": { @@ -5444,11 +5670,13 @@ const docTemplate = `{ "domain.PaymentStatus": { "type": "string", "enum": [ + "success", "pending", "completed", "failed" ], "x-enum-varnames": [ + "PaymentStatusSuccessful", "PaymentStatusPending", "PaymentStatusCompleted", "PaymentStatusFailed" @@ -5543,28 +5771,6 @@ const docTemplate = `{ } } }, - "domain.RecommendationErrorResponse": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } - }, - "domain.RecommendationSuccessfulResponse": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "recommended_games": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.VirtualGame" - } - } - } - }, "domain.ReferralSettings": { "type": "object", "properties": { @@ -5806,53 +6012,6 @@ const docTemplate = `{ } } }, - "domain.VirtualGame": { - "type": "object", - "properties": { - "category": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "is_active": { - "type": "boolean" - }, - "is_featured": { - "type": "boolean" - }, - "max_bet": { - "type": "number" - }, - "min_bet": { - "type": "number" - }, - "name": { - "type": "string" - }, - "popularity_score": { - "type": "integer" - }, - "provider": { - "type": "string" - }, - "rtp": { - "type": "number" - }, - "thumbnail_url": { - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "volatility": { - "type": "string" - } - } - }, "handlers.AdminRes": { "type": "object", "properties": { @@ -6670,44 +6829,38 @@ const docTemplate = `{ "type": "object", "properties": { "amount": { - "type": "number", - "example": 100 + "type": "number" }, "cashier_id": { - "type": "integer", - "example": 789 + "type": "integer" }, "created_at": { - "type": "string", - "example": "2025-04-08T12:00:00Z" + "type": "string" }, "id": { - "type": "integer", - "example": 1 + "type": "integer" }, "payment_method": { - "type": "string", - "example": "bank" + "type": "string" }, "receiver_wallet_id": { - "type": "integer", - "example": 1 + "type": "integer" + }, + "reference_number": { + "description": "← Add this", + "type": "string" }, "sender_wallet_id": { - "type": "integer", - "example": 1 + "type": "integer" }, "type": { - "type": "string", - "example": "transfer" + "type": "string" }, "updated_at": { - "type": "string", - "example": "2025-04-08T12:30:00Z" + "type": "string" }, "verified": { - "type": "boolean", - "example": true + "type": "boolean" } } }, diff --git a/docs/swagger.json b/docs/swagger.json index 0402648..6260c8a 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -296,6 +296,217 @@ } } }, + "/api/v1/banks": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Institutions - Banks" + ], + "summary": "List all banks", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Bank" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Institutions - Banks" + ], + "summary": "Create a new bank", + "parameters": [ + { + "description": "Bank Info", + "name": "bank", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.Bank" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/domain.Bank" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/banks/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Institutions - Banks" + ], + "summary": "Get a bank by ID", + "parameters": [ + { + "type": "integer", + "description": "Bank ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Bank" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Institutions - Banks" + ], + "summary": "Update a bank", + "parameters": [ + { + "type": "integer", + "description": "Bank ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Bank Info", + "name": "bank", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.Bank" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Bank" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Institutions - Banks" + ], + "summary": "Delete a bank", + "parameters": [ + { + "type": "integer", + "description": "Bank ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "Deleted successfully", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/chapa/banks": { "get": { "description": "Get list of banks supported by Chapa", @@ -833,44 +1044,6 @@ } } }, - "/api/v1/virtual-games/recommendations/{userID}": { - "get": { - "description": "Returns a list of recommended virtual games for a specific user", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Recommendations" - ], - "summary": "Get virtual game recommendations", - "parameters": [ - { - "type": "string", - "description": "User ID", - "name": "userID", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Recommended games fetched successfully", - "schema": { - "$ref": "#/definitions/domain.RecommendationSuccessfulResponse" - } - }, - "500": { - "description": "Failed to fetch recommendations", - "schema": { - "$ref": "#/definitions/domain.RecommendationErrorResponse" - } - } - } - } - }, "/api/v1/webhooks/alea": { "post": { "description": "Handles webhook callbacks from Alea Play virtual games for bet settlement", @@ -1076,269 +1249,6 @@ } } }, - "/bet": { - "get": { - "description": "Gets all the bets", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Gets all bets", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.BetRes" - } - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - }, - "post": { - "description": "Creates a bet", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Create a bet", - "parameters": [ - { - "description": "Creates bet", - "name": "createBet", - "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" - } - } - } - } - }, - "/bet/cashout/{id}": { - "get": { - "description": "Gets a single bet by cashout id", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Gets bet by cashout id", - "parameters": [ - { - "type": "string", - "description": "cashout ID", - "name": "id", - "in": "path", - "required": true - } - ], - "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" - } - } - } - } - }, - "/bet/{id}": { - "get": { - "description": "Gets a single bet by id", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Gets bet by id", - "parameters": [ - { - "type": "integer", - "description": "Bet ID", - "name": "id", - "in": "path", - "required": true - } - ], - "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" - } - } - } - }, - "delete": { - "description": "Deletes bet by id", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Deletes bet by id", - "parameters": [ - { - "type": "integer", - "description": "Bet ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - }, - "patch": { - "description": "Updates the cashed out field", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Updates the cashed out field", - "parameters": [ - { - "type": "integer", - "description": "Bet ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Updates Cashed Out", - "name": "updateCashOut", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handlers.UpdateCashOutReq" - } - } - ], - "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" - } - } - } - } - }, "/branch": { "get": { "description": "Gets all branches", @@ -3075,52 +2985,6 @@ } } }, - "/random/bet": { - "post": { - "description": "Generate a random bet", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Generate a random bet", - "parameters": [ - { - "description": "Create Random bet", - "name": "createBet", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.RandomBetReq" - } - } - ], - "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" - } - } - } - } - }, "/referral/settings": { "get": { "security": [ @@ -3394,6 +3258,315 @@ } } }, + "/sport/bet": { + "get": { + "description": "Gets all the bets", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets all bets", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.BetRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "post": { + "description": "Creates a bet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Create a bet", + "parameters": [ + { + "description": "Creates bet", + "name": "createBet", + "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/cashout/{id}": { + "get": { + "description": "Gets a single bet by cashout id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets bet by cashout id", + "parameters": [ + { + "type": "string", + "description": "cashout ID", + "name": "id", + "in": "path", + "required": true + } + ], + "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", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets bet by id", + "parameters": [ + { + "type": "integer", + "description": "Bet ID", + "name": "id", + "in": "path", + "required": true + } + ], + "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" + } + } + } + }, + "delete": { + "description": "Deletes bet by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Deletes bet by id", + "parameters": [ + { + "type": "integer", + "description": "Bet ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "patch": { + "description": "Updates the cashed out field", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Updates the cashed out field", + "parameters": [ + { + "type": "integer", + "description": "Bet ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updates Cashed Out", + "name": "updateCashOut", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.UpdateCashOutReq" + } + } + ], + "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" + } + } + } + } + }, + "/sport/random/bet": { + "post": { + "description": "Generate a random bet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Generate a random bet", + "parameters": [ + { + "description": "Create Random bet", + "name": "createBet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.RandomBetReq" + } + } + ], + "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" + } + } + } + } + }, "/supportedOperation": { "get": { "description": "Gets all supported operations", @@ -4778,6 +4951,59 @@ } } }, + "domain.Bank": { + "type": "object", + "properties": { + "acct_length": { + "type": "integer" + }, + "active": { + "type": "integer" + }, + "bank_logo": { + "description": "URL or base64", + "type": "string" + }, + "country_id": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_24hrs": { + "description": "nullable", + "type": "integer" + }, + "is_active": { + "type": "integer" + }, + "is_mobilemoney": { + "description": "nullable", + "type": "integer" + }, + "is_rtgs": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "swift": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "domain.BetOutcome": { "type": "object", "properties": { @@ -5436,11 +5662,13 @@ "domain.PaymentStatus": { "type": "string", "enum": [ + "success", "pending", "completed", "failed" ], "x-enum-varnames": [ + "PaymentStatusSuccessful", "PaymentStatusPending", "PaymentStatusCompleted", "PaymentStatusFailed" @@ -5535,28 +5763,6 @@ } } }, - "domain.RecommendationErrorResponse": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } - }, - "domain.RecommendationSuccessfulResponse": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "recommended_games": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.VirtualGame" - } - } - } - }, "domain.ReferralSettings": { "type": "object", "properties": { @@ -5798,53 +6004,6 @@ } } }, - "domain.VirtualGame": { - "type": "object", - "properties": { - "category": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "is_active": { - "type": "boolean" - }, - "is_featured": { - "type": "boolean" - }, - "max_bet": { - "type": "number" - }, - "min_bet": { - "type": "number" - }, - "name": { - "type": "string" - }, - "popularity_score": { - "type": "integer" - }, - "provider": { - "type": "string" - }, - "rtp": { - "type": "number" - }, - "thumbnail_url": { - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "volatility": { - "type": "string" - } - } - }, "handlers.AdminRes": { "type": "object", "properties": { @@ -6662,44 +6821,38 @@ "type": "object", "properties": { "amount": { - "type": "number", - "example": 100 + "type": "number" }, "cashier_id": { - "type": "integer", - "example": 789 + "type": "integer" }, "created_at": { - "type": "string", - "example": "2025-04-08T12:00:00Z" + "type": "string" }, "id": { - "type": "integer", - "example": 1 + "type": "integer" }, "payment_method": { - "type": "string", - "example": "bank" + "type": "string" }, "receiver_wallet_id": { - "type": "integer", - "example": 1 + "type": "integer" + }, + "reference_number": { + "description": "← Add this", + "type": "string" }, "sender_wallet_id": { - "type": "integer", - "example": 1 + "type": "integer" }, "type": { - "type": "string", - "example": "transfer" + "type": "string" }, "updated_at": { - "type": "string", - "example": "2025-04-08T12:30:00Z" + "type": "string" }, "verified": { - "type": "boolean", - "example": true + "type": "boolean" } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ffb24c6..63f93a8 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -31,6 +31,42 @@ definitions: user_id: type: string type: object + domain.Bank: + properties: + acct_length: + type: integer + active: + type: integer + bank_logo: + description: URL or base64 + type: string + country_id: + type: integer + created_at: + type: string + currency: + type: string + id: + type: integer + is_24hrs: + description: nullable + type: integer + is_active: + type: integer + is_mobilemoney: + description: nullable + type: integer + is_rtgs: + type: integer + name: + type: string + slug: + type: string + swift: + type: string + updated_at: + type: string + type: object domain.BetOutcome: properties: away_team_name: @@ -491,11 +527,13 @@ definitions: - BANK domain.PaymentStatus: enum: + - success - pending - completed - failed type: string x-enum-varnames: + - PaymentStatusSuccessful - PaymentStatusPending - PaymentStatusCompleted - PaymentStatusFailed @@ -559,20 +597,6 @@ definitions: items: {} type: array type: object - domain.RecommendationErrorResponse: - properties: - message: - type: string - type: object - domain.RecommendationSuccessfulResponse: - properties: - message: - type: string - recommended_games: - items: - $ref: '#/definitions/domain.VirtualGame' - type: array - type: object domain.ReferralSettings: properties: betReferralBonusPercentage: @@ -742,37 +766,6 @@ definitions: - $ref: '#/definitions/domain.EventStatus' description: Match Status for event type: object - domain.VirtualGame: - properties: - category: - type: string - created_at: - type: string - id: - type: integer - is_active: - type: boolean - is_featured: - type: boolean - max_bet: - type: number - min_bet: - type: number - name: - type: string - popularity_score: - type: integer - provider: - type: string - rtp: - type: number - thumbnail_url: - type: string - updated_at: - type: string - volatility: - type: string - type: object handlers.AdminRes: properties: created_at: @@ -1340,34 +1333,27 @@ definitions: handlers.TransferWalletRes: properties: amount: - example: 100 type: number cashier_id: - example: 789 type: integer created_at: - example: "2025-04-08T12:00:00Z" type: string id: - example: 1 type: integer payment_method: - example: bank type: string receiver_wallet_id: - example: 1 type: integer + reference_number: + description: ← Add this + type: string sender_wallet_id: - example: 1 type: integer type: - example: transfer type: string updated_at: - example: "2025-04-08T12:30:00Z" type: string verified: - example: true type: boolean type: object handlers.UpdateCashOutReq: @@ -1801,6 +1787,144 @@ paths: summary: Launch an Alea Play virtual game tags: - Alea Virtual Games + /api/v1/banks: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.Bank' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: List all banks + tags: + - Institutions - Banks + post: + consumes: + - application/json + parameters: + - description: Bank Info + in: body + name: bank + required: true + schema: + $ref: '#/definitions/domain.Bank' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/domain.Bank' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Create a new bank + tags: + - Institutions - Banks + /api/v1/banks/{id}: + delete: + parameters: + - description: Bank ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "204": + description: Deleted successfully + schema: + type: string + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Delete a bank + tags: + - Institutions - Banks + get: + parameters: + - description: Bank ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Bank' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get a bank by ID + tags: + - Institutions - Banks + put: + consumes: + - application/json + parameters: + - description: Bank ID + in: path + name: id + required: true + type: integer + - description: Bank Info + in: body + name: bank + required: true + schema: + $ref: '#/definitions/domain.Bank' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Bank' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Update a bank + tags: + - Institutions - Banks /api/v1/chapa/banks: get: consumes: @@ -2144,31 +2268,6 @@ paths: summary: Get dashboard report tags: - Reports - /api/v1/virtual-games/recommendations/{userID}: - get: - consumes: - - application/json - description: Returns a list of recommended virtual games for a specific user - parameters: - - description: User ID - in: path - name: userID - required: true - type: string - produces: - - application/json - responses: - "200": - description: Recommended games fetched successfully - schema: - $ref: '#/definitions/domain.RecommendationSuccessfulResponse' - "500": - description: Failed to fetch recommendations - schema: - $ref: '#/definitions/domain.RecommendationErrorResponse' - summary: Get virtual game recommendations - tags: - - Recommendations /api/v1/webhooks/alea: post: consumes: @@ -2301,180 +2400,6 @@ paths: summary: Refresh token tags: - auth - /bet: - get: - consumes: - - application/json - description: Gets all the bets - produces: - - application/json - responses: - "200": - description: OK - schema: - items: - $ref: '#/definitions/domain.BetRes' - type: array - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Gets all bets - tags: - - bet - post: - consumes: - - application/json - description: Creates a bet - parameters: - - description: Creates bet - in: body - name: createBet - 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 - tags: - - bet - /bet/{id}: - delete: - consumes: - - application/json - description: Deletes bet by id - parameters: - - description: Bet ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/response.APIResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Deletes bet by id - tags: - - bet - get: - consumes: - - application/json - description: Gets a single bet by id - parameters: - - description: Bet ID - in: path - name: id - required: true - type: integer - 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: Gets bet by id - tags: - - bet - patch: - consumes: - - application/json - description: Updates the cashed out field - parameters: - - description: Bet ID - in: path - name: id - required: true - type: integer - - description: Updates Cashed Out - in: body - name: updateCashOut - required: true - schema: - $ref: '#/definitions/handlers.UpdateCashOutReq' - 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 cashed out field - tags: - - bet - /bet/cashout/{id}: - get: - consumes: - - application/json - description: Gets a single bet by cashout id - parameters: - - description: cashout ID - in: path - name: id - required: true - type: string - 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: Gets bet by cashout id - tags: - - bet /branch: get: consumes: @@ -3622,36 +3547,6 @@ paths: summary: Recommend virtual games tags: - Virtual Games - PopOK - /random/bet: - post: - consumes: - - application/json - description: Generate a random bet - parameters: - - description: Create Random bet - in: body - name: createBet - required: true - schema: - $ref: '#/definitions/domain.RandomBetReq' - 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: Generate a random bet - tags: - - bet /referral/settings: get: consumes: @@ -3828,6 +3723,210 @@ paths: summary: Gets all companies tags: - company + /sport/bet: + get: + consumes: + - application/json + description: Gets all the bets + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.BetRes' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets all bets + tags: + - bet + post: + consumes: + - application/json + description: Creates a bet + parameters: + - description: Creates bet + in: body + name: createBet + 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 + tags: + - bet + /sport/bet/{id}: + delete: + consumes: + - application/json + description: Deletes bet by id + parameters: + - description: Bet ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Deletes bet by id + tags: + - bet + get: + consumes: + - application/json + description: Gets a single bet by id + parameters: + - description: Bet ID + in: path + name: id + required: true + type: integer + 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: Gets bet by id + tags: + - bet + patch: + consumes: + - application/json + description: Updates the cashed out field + parameters: + - description: Bet ID + in: path + name: id + required: true + type: integer + - description: Updates Cashed Out + in: body + name: updateCashOut + required: true + schema: + $ref: '#/definitions/handlers.UpdateCashOutReq' + 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 cashed out field + tags: + - bet + /sport/bet/cashout/{id}: + get: + consumes: + - application/json + description: Gets a single bet by cashout id + parameters: + - description: cashout ID + in: path + name: id + required: true + type: string + 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: Gets bet by cashout id + tags: + - bet + /sport/random/bet: + post: + consumes: + - application/json + description: Generate a random bet + parameters: + - description: Create Random bet + in: body + name: createBet + required: true + schema: + $ref: '#/definitions/domain.RandomBetReq' + 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: Generate a random bet + tags: + - bet /supportedOperation: get: consumes: diff --git a/gen/db/institutions.sql.go b/gen/db/institutions.sql.go new file mode 100644 index 0000000..b182933 --- /dev/null +++ b/gen/db/institutions.sql.go @@ -0,0 +1,251 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: institutions.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const CreateBank = `-- name: CreateBank :one +INSERT INTO banks ( + slug, + swift, + name, + acct_length, + country_id, + is_mobilemoney, + is_active, + is_rtgs, + active, + is_24hrs, + created_at, + updated_at, + currency, + bank_logo +) +VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, $11, $12 +) +RETURNING id, slug, swift, name, acct_length, country_id, is_mobilemoney, is_active, is_rtgs, active, is_24hrs, created_at, updated_at, currency, bank_logo +` + +type CreateBankParams struct { + Slug string `json:"slug"` + Swift string `json:"swift"` + Name string `json:"name"` + AcctLength int32 `json:"acct_length"` + CountryID int32 `json:"country_id"` + IsMobilemoney pgtype.Int4 `json:"is_mobilemoney"` + IsActive int32 `json:"is_active"` + IsRtgs int32 `json:"is_rtgs"` + Active int32 `json:"active"` + Is24hrs pgtype.Int4 `json:"is_24hrs"` + Currency string `json:"currency"` + BankLogo pgtype.Text `json:"bank_logo"` +} + +func (q *Queries) CreateBank(ctx context.Context, arg CreateBankParams) (Bank, error) { + row := q.db.QueryRow(ctx, CreateBank, + arg.Slug, + arg.Swift, + arg.Name, + arg.AcctLength, + arg.CountryID, + arg.IsMobilemoney, + arg.IsActive, + arg.IsRtgs, + arg.Active, + arg.Is24hrs, + arg.Currency, + arg.BankLogo, + ) + var i Bank + err := row.Scan( + &i.ID, + &i.Slug, + &i.Swift, + &i.Name, + &i.AcctLength, + &i.CountryID, + &i.IsMobilemoney, + &i.IsActive, + &i.IsRtgs, + &i.Active, + &i.Is24hrs, + &i.CreatedAt, + &i.UpdatedAt, + &i.Currency, + &i.BankLogo, + ) + return i, err +} + +const DeleteBank = `-- name: DeleteBank :exec +DELETE FROM banks +WHERE id = $1 +` + +func (q *Queries) DeleteBank(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeleteBank, id) + return err +} + +const GetAllBanks = `-- name: GetAllBanks :many +SELECT id, slug, swift, name, acct_length, country_id, is_mobilemoney, is_active, is_rtgs, active, is_24hrs, created_at, updated_at, currency, bank_logo +FROM banks +WHERE ( + country_id = $1 + OR $1 IS NULL + ) + AND ( + is_active = $2 + OR $2 IS NULL + ) +` + +type GetAllBanksParams struct { + CountryID pgtype.Int4 `json:"country_id"` + IsActive pgtype.Int4 `json:"is_active"` +} + +func (q *Queries) GetAllBanks(ctx context.Context, arg GetAllBanksParams) ([]Bank, error) { + rows, err := q.db.Query(ctx, GetAllBanks, arg.CountryID, arg.IsActive) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Bank + for rows.Next() { + var i Bank + if err := rows.Scan( + &i.ID, + &i.Slug, + &i.Swift, + &i.Name, + &i.AcctLength, + &i.CountryID, + &i.IsMobilemoney, + &i.IsActive, + &i.IsRtgs, + &i.Active, + &i.Is24hrs, + &i.CreatedAt, + &i.UpdatedAt, + &i.Currency, + &i.BankLogo, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetBankByID = `-- name: GetBankByID :one +SELECT id, slug, swift, name, acct_length, country_id, is_mobilemoney, is_active, is_rtgs, active, is_24hrs, created_at, updated_at, currency, bank_logo +FROM banks +WHERE id = $1 +` + +func (q *Queries) GetBankByID(ctx context.Context, id int64) (Bank, error) { + row := q.db.QueryRow(ctx, GetBankByID, id) + var i Bank + err := row.Scan( + &i.ID, + &i.Slug, + &i.Swift, + &i.Name, + &i.AcctLength, + &i.CountryID, + &i.IsMobilemoney, + &i.IsActive, + &i.IsRtgs, + &i.Active, + &i.Is24hrs, + &i.CreatedAt, + &i.UpdatedAt, + &i.Currency, + &i.BankLogo, + ) + return i, err +} + +const UpdateBank = `-- name: UpdateBank :one +UPDATE banks +SET slug = COALESCE($2, slug), + swift = COALESCE($3, swift), + name = COALESCE($4, name), + acct_length = COALESCE($5, acct_length), + country_id = COALESCE($6, country_id), + is_mobilemoney = COALESCE($7, is_mobilemoney), + is_active = COALESCE($8, is_active), + is_rtgs = COALESCE($9, is_rtgs), + active = COALESCE($10, active), + is_24hrs = COALESCE($11, is_24hrs), + updated_at = CURRENT_TIMESTAMP, + currency = COALESCE($12, currency), + bank_logo = COALESCE($13, bank_logo) +WHERE id = $1 +RETURNING id, slug, swift, name, acct_length, country_id, is_mobilemoney, is_active, is_rtgs, active, is_24hrs, created_at, updated_at, currency, bank_logo +` + +type UpdateBankParams struct { + ID int64 `json:"id"` + Slug pgtype.Text `json:"slug"` + Swift pgtype.Text `json:"swift"` + Name pgtype.Text `json:"name"` + AcctLength pgtype.Int4 `json:"acct_length"` + CountryID pgtype.Int4 `json:"country_id"` + IsMobilemoney pgtype.Int4 `json:"is_mobilemoney"` + IsActive pgtype.Int4 `json:"is_active"` + IsRtgs pgtype.Int4 `json:"is_rtgs"` + Active pgtype.Int4 `json:"active"` + Is24hrs pgtype.Int4 `json:"is_24hrs"` + Currency pgtype.Text `json:"currency"` + BankLogo pgtype.Text `json:"bank_logo"` +} + +func (q *Queries) UpdateBank(ctx context.Context, arg UpdateBankParams) (Bank, error) { + row := q.db.QueryRow(ctx, UpdateBank, + arg.ID, + arg.Slug, + arg.Swift, + arg.Name, + arg.AcctLength, + arg.CountryID, + arg.IsMobilemoney, + arg.IsActive, + arg.IsRtgs, + arg.Active, + arg.Is24hrs, + arg.Currency, + arg.BankLogo, + ) + var i Bank + err := row.Scan( + &i.ID, + &i.Slug, + &i.Swift, + &i.Name, + &i.AcctLength, + &i.CountryID, + &i.IsMobilemoney, + &i.IsActive, + &i.IsRtgs, + &i.Active, + &i.Is24hrs, + &i.CreatedAt, + &i.UpdatedAt, + &i.Currency, + &i.BankLogo, + ) + return i, err +} diff --git a/gen/db/models.go b/gen/db/models.go index ab7ecca..e052db4 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -55,6 +55,24 @@ func (ns NullReferralstatus) Value() (driver.Value, error) { return string(ns.Referralstatus), nil } +type Bank struct { + ID int64 `json:"id"` + Slug string `json:"slug"` + Swift string `json:"swift"` + Name string `json:"name"` + AcctLength int32 `json:"acct_length"` + CountryID int32 `json:"country_id"` + IsMobilemoney pgtype.Int4 `json:"is_mobilemoney"` + IsActive int32 `json:"is_active"` + IsRtgs int32 `json:"is_rtgs"` + Active int32 `json:"active"` + Is24hrs pgtype.Int4 `json:"is_24hrs"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + Currency string `json:"currency"` + BankLogo pgtype.Text `json:"bank_logo"` +} + type Bet struct { ID int64 `json:"id"` Amount int64 `json:"amount"` diff --git a/internal/domain/chapa.go b/internal/domain/chapa.go index 2a3b236..57a090f 100644 --- a/internal/domain/chapa.go +++ b/internal/domain/chapa.go @@ -16,6 +16,7 @@ type PaymentStatus string type WithdrawalStatus string const ( + WithdrawalStatusSuccessful WithdrawalStatus = "success" WithdrawalStatusPending WithdrawalStatus = "pending" WithdrawalStatusProcessing WithdrawalStatus = "processing" WithdrawalStatusCompleted WithdrawalStatus = "completed" @@ -23,9 +24,10 @@ const ( ) const ( - PaymentStatusPending PaymentStatus = "pending" - PaymentStatusCompleted PaymentStatus = "completed" - PaymentStatusFailed PaymentStatus = "failed" + PaymentStatusSuccessful PaymentStatus = "success" + PaymentStatusPending PaymentStatus = "pending" + PaymentStatusCompleted PaymentStatus = "completed" + PaymentStatusFailed PaymentStatus = "failed" ) type ChapaDepositRequest struct { @@ -70,22 +72,23 @@ type ChapaVerificationResponse struct { TxRef string `json:"tx_ref"` } -type Bank struct { - ID int `json:"id"` - Slug string `json:"slug"` - Swift string `json:"swift"` - Name string `json:"name"` - AcctLength int `json:"acct_length"` - CountryID int `json:"country_id"` - IsMobileMoney int `json:"is_mobilemoney"` // nullable - IsActive int `json:"is_active"` - IsRTGS int `json:"is_rtgs"` - Active int `json:"active"` - Is24Hrs int `json:"is_24hrs"` // nullable - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Currency string `json:"currency"` -} +// type Bank struct { +// ID int `json:"id"` +// Slug string `json:"slug"` +// Swift string `json:"swift"` +// Name string `json:"name"` +// AcctLength int `json:"acct_length"` +// CountryID int `json:"country_id"` +// IsMobileMoney int `json:"is_mobilemoney"` // nullable +// IsActive int `json:"is_active"` +// IsRTGS int `json:"is_rtgs"` +// Active int `json:"active"` +// Is24Hrs int `json:"is_24hrs"` // nullable +// CreatedAt time.Time `json:"created_at"` +// UpdatedAt time.Time `json:"updated_at"` +// Currency string `json:"currency"` +// BankLogo string `json:"bank_logo"` // URL or base64 +// } type BankResponse struct { Message string `json:"message"` @@ -142,11 +145,9 @@ type ChapaWithdrawalRequest struct { // } type ChapaWithdrawalResponse struct { - Status string `json:"status"` Message string `json:"message"` - Data struct { - Reference string `json:"reference"` - } `json:"data"` + Status string `json:"status"` + Data string `json:"data"` // Accepts string instead of struct } type ChapaTransactionType struct { diff --git a/internal/domain/institutions.go b/internal/domain/institutions.go new file mode 100644 index 0000000..0e09b57 --- /dev/null +++ b/internal/domain/institutions.go @@ -0,0 +1,21 @@ +package domain + +import "time" + +type Bank struct { + ID int `json:"id"` + Slug string `json:"slug"` + Swift string `json:"swift"` + Name string `json:"name"` + AcctLength int `json:"acct_length"` + CountryID int `json:"country_id"` + IsMobileMoney int `json:"is_mobilemoney"` // nullable + IsActive int `json:"is_active"` + IsRTGS int `json:"is_rtgs"` + Active int `json:"active"` + Is24Hrs int `json:"is_24hrs"` // nullable + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Currency string `json:"currency"` + BankLogo string `json:"bank_logo"` // URL or base64 +} \ No newline at end of file diff --git a/internal/domain/notification.go b/internal/domain/notification.go index 9351d68..db054c1 100644 --- a/internal/domain/notification.go +++ b/internal/domain/notification.go @@ -97,15 +97,16 @@ func FromJSON(data []byte) (*Notification, error) { func ReceiverFromRole(role Role) NotificationRecieverSide { - if role == RoleAdmin { + switch role { + case RoleAdmin: return NotificationRecieverSideAdmin - } else if role == RoleCashier { + case RoleCashier: return NotificationRecieverSideCashier - } else if role == RoleBranchManager { + case RoleBranchManager: return NotificationRecieverSideBranchManager - } else if role == RoleCustomer { + case RoleCustomer: return NotificationRecieverSideCustomer - } else { + default: return "" } } diff --git a/internal/domain/virtual_game.go b/internal/domain/virtual_game.go index ff35ead..cc96a46 100644 --- a/internal/domain/virtual_game.go +++ b/internal/domain/virtual_game.go @@ -159,6 +159,11 @@ type PopOKWinResponse struct { Balance float64 `json:"balance"` } +type PopOKGenerateTokenRequest struct { + GameID string `json:"newGameId"` + Token string `json:"token"` +} + type PopOKCancelRequest struct { ExternalToken string `json:"externalToken"` PlayerID string `json:"playerId"` @@ -172,6 +177,10 @@ type PopOKCancelResponse struct { Balance float64 `json:"balance"` } +type PopOKGenerateTokenResponse struct { + NewToken string `json:"newToken"` +} + type AleaPlayCallback struct { EventID string `json:"event_id"` TransactionID string `json:"transaction_id"` diff --git a/internal/repository/institutions.go b/internal/repository/institutions.go new file mode 100644 index 0000000..6cf72a4 --- /dev/null +++ b/internal/repository/institutions.go @@ -0,0 +1,139 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/jackc/pgx/v5/pgtype" +) + +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) + UpdateBank(ctx context.Context, bank *domain.Bank) error + DeleteBank(ctx context.Context, id int) error +} + +type BankRepo struct { + store *Store +} + +func NewBankRepository(store *Store) BankRepository { + return &BankRepo{store: store} +} + +func (r *BankRepo) CreateBank(ctx context.Context, bank *domain.Bank) error { + params := dbgen.CreateBankParams{ + Slug: bank.Slug, + Swift: bank.Swift, + Name: bank.Name, + AcctLength: int32(bank.AcctLength), + CountryID: int32(bank.CountryID), + IsMobilemoney: pgtype.Int4{Int32: int32(bank.IsMobileMoney), Valid: true}, + IsActive: int32(bank.IsActive), + IsRtgs: int32(bank.IsRTGS), + Active: int32(bank.Active), + Is24hrs: pgtype.Int4{Int32: int32(bank.Is24Hrs), Valid: true}, + Currency: bank.Currency, + BankLogo: pgtype.Text{String: bank.BankLogo, Valid: true}, + } + createdBank, err := r.store.queries.CreateBank(ctx, params) + if err != nil { + return err + } + // Update the ID and timestamps on the passed struct + bank.ID = int(createdBank.ID) + bank.CreatedAt = createdBank.CreatedAt.Time + bank.UpdatedAt = createdBank.UpdatedAt.Time + return nil +} + +func (r *BankRepo) GetBankByID(ctx context.Context, id int) (*domain.Bank, error) { + dbBank, err := r.store.queries.GetBankByID(ctx, int64(id)) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, err + } + 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{}, + } + if countryID != nil { + params.CountryID = pgtype.Int4{Int32: int32(*countryID), Valid: true} + } + if isActive != nil { + params.IsActive = pgtype.Int4{Int32: int32(*isActive), Valid: true} + } + + dbBanks, err := r.store.queries.GetAllBanks(ctx, params) + if err != nil { + return nil, err + } + + banks := make([]domain.Bank, len(dbBanks)) + for i, b := range dbBanks { + banks[i] = *mapDBBankToDomain(&b) + } + return banks, nil +} + +func (r *BankRepo) UpdateBank(ctx context.Context, bank *domain.Bank) error { + params := dbgen.UpdateBankParams{ + ID: int64(bank.ID), + Slug: pgtype.Text{String: bank.Slug, Valid: true}, + Swift: pgtype.Text{String: bank.Swift, Valid: true}, + Name: pgtype.Text{String: bank.Name, Valid: true}, + AcctLength: pgtype.Int4{Int32: int32(bank.AcctLength), Valid: true}, + CountryID: pgtype.Int4{Int32: int32(bank.CountryID), Valid: true}, + IsMobilemoney: pgtype.Int4{Int32: int32(bank.IsMobileMoney), Valid: true}, + IsActive: pgtype.Int4{Int32: int32(bank.IsActive), Valid: true}, + IsRtgs: pgtype.Int4{Int32: int32(bank.IsRTGS), Valid: true}, + Active: pgtype.Int4{Int32: int32(bank.Active), Valid: true}, + Is24hrs: pgtype.Int4{Int32: int32(bank.Is24Hrs), Valid: true}, + Currency: pgtype.Text{String: bank.Currency, Valid: true}, + BankLogo: pgtype.Text{String: bank.BankLogo, Valid: true}, + } + updatedBank, err := r.store.queries.UpdateBank(ctx, params) + if err != nil { + return err + } + + // update timestamps in domain struct + bank.UpdatedAt = updatedBank.UpdatedAt.Time + return nil +} + +func (r *BankRepo) DeleteBank(ctx context.Context, id int) error { + return r.store.queries.DeleteBank(ctx, int64(id)) +} + +// Helper to map DB struct to domain +func mapDBBankToDomain(dbBank *dbgen.Bank) *domain.Bank { + return &domain.Bank{ + ID: int(dbBank.ID), + Slug: dbBank.Slug, + Swift: dbBank.Swift, + Name: dbBank.Name, + AcctLength: int(dbBank.AcctLength), + CountryID: int(dbBank.CountryID), + IsMobileMoney: int(dbBank.IsMobilemoney.Int32), + IsActive: int(dbBank.IsActive), + IsRTGS: int(dbBank.IsRtgs), + Active: int(dbBank.Active), + Is24Hrs: int(dbBank.Is24hrs.Int32), + CreatedAt: dbBank.CreatedAt.Time, + UpdatedAt: dbBank.UpdatedAt.Time, + Currency: dbBank.Currency, + BankLogo: dbBank.BankLogo.String, + } +} diff --git a/internal/services/chapa/client.go b/internal/services/chapa/client.go index 748bc13..baac2fd 100644 --- a/internal/services/chapa/client.go +++ b/internal/services/chapa/client.go @@ -31,8 +31,8 @@ func NewClient(baseURL, secretKey string) *Client { func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error) { payload := map[string]interface{}{ - "amount": fmt.Sprintf("%.2f", float64(req.Amount)/100), - "currency": req.Currency, + "amount": fmt.Sprintf("%.2f", float64(req.Amount)/100), + "currency": req.Currency, // "email": req.Email, "first_name": req.FirstName, "last_name": req.LastName, @@ -175,6 +175,51 @@ func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain }, nil } +func (c *Client) ManualVerifyTransfer(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) { + url := fmt.Sprintf("%s/transfers/verify/%s", c.baseURL, txRef) + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+c.secretKey) + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var response struct { + Status string `json:"status"` + Amount float64 `json:"amount"` + Currency string `json:"currency"` + } + + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + var status domain.PaymentStatus + switch response.Status { + case "success": + status = domain.PaymentStatusCompleted + default: + status = domain.PaymentStatusFailed + } + + return &domain.ChapaVerificationResponse{ + Status: string(status), + Amount: response.Amount, + Currency: response.Currency, + }, nil +} + func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) { req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/banks", nil) if err != nil { @@ -223,10 +268,6 @@ func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) } func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawalRequest) (bool, error) { - // base, err := url.Parse(c.baseURL) - // if err != nil { - // return false, fmt.Errorf("invalid base URL: %w", err) - // } endpoint := c.baseURL + "/transfers" fmt.Printf("\n\nChapa withdrawal URL is %v\n\n", endpoint) @@ -240,7 +281,9 @@ func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawa return false, fmt.Errorf("failed to create request: %w", err) } - c.setHeaders(httpReq) + // Set headers here + httpReq.Header.Set("Authorization", "Bearer "+c.secretKey) + httpReq.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(httpReq) if err != nil { @@ -249,7 +292,8 @@ func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawa defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return false, fmt.Errorf("chapa api returned status: %d", resp.StatusCode) + body, _ := io.ReadAll(resp.Body) + return false, fmt.Errorf("chapa api returned status: %d - %s", resp.StatusCode, string(body)) } var response domain.ChapaWithdrawalResponse @@ -257,7 +301,7 @@ func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawa return false, fmt.Errorf("failed to decode response: %w", err) } - return response.Status == string(domain.WithdrawalStatusProcessing), nil + return response.Status == string(domain.WithdrawalStatusSuccessful), nil } func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain.ChapaVerificationResponse, error) { diff --git a/internal/services/chapa/service.go b/internal/services/chapa/service.go index 344f8ee..96d5145 100644 --- a/internal/services/chapa/service.go +++ b/internal/services/chapa/service.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "strconv" + "strings" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -60,7 +61,9 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma var senderWallet domain.Wallet // Generate unique reference - reference := uuid.New().String() + // reference := uuid.New().String() + reference := fmt.Sprintf("chapa-deposit-%d-%s", userID, uuid.New().String()) + senderWallets, err := s.walletStore.GetWalletsByUser(ctx, userID) if err != nil { return "", fmt.Errorf("failed to get sender wallets: %w", err) @@ -92,8 +95,7 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma Verified: false, } - // Initialize payment with Chapa - response, err := s.chapaClient.InitializePayment(ctx, domain.ChapaDepositRequest{ + payload := domain.ChapaDepositRequest{ Amount: amount, Currency: "ETB", Email: user.Email, @@ -102,7 +104,12 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma TxRef: reference, CallbackURL: s.cfg.CHAPA_CALLBACK_URL, ReturnURL: s.cfg.CHAPA_RETURN_URL, - }) + } + + // Initialize payment with Chapa + response, err := s.chapaClient.InitializePayment(ctx, payload) + + fmt.Printf("\n\nChapa payload is: %+v\n\n", payload) if err != nil { // Update payment status to failed @@ -189,12 +196,16 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma } success, err := s.chapaClient.InitiateTransfer(ctx, transferReq) - if err != nil || !success { - // Update withdrawal status to failed + if err != nil { _ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed)) return nil, fmt.Errorf("failed to initiate transfer: %w", err) } + if !success { + _ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed)) + return nil, errors.New("chapa rejected the transfer request") + } + // Update withdrawal status to processing if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusProcessing)); err != nil { return nil, fmt.Errorf("failed to update withdrawal status: %w", err) @@ -216,50 +227,68 @@ func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.Bank, error) return banks, nil } -func (s *Service) ManualVerifTransaction(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) { - // First check if we already have a verified record +func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) { + // Lookup transfer by reference transfer, err := s.transferStore.GetTransferByReference(ctx, txRef) - if err == nil && transfer.Verified { + if err != nil { + return nil, fmt.Errorf("transfer not found for reference %s: %w", txRef, err) + } + + if transfer.Verified { return &domain.ChapaVerificationResponse{ Status: string(domain.PaymentStatusCompleted), - Amount: float64(transfer.Amount) / 100, // Convert from cents/kobo + Amount: float64(transfer.Amount) / 100, Currency: "ETB", }, nil } - fmt.Printf("\n\nSender wallet ID is:%v\n\n", transfer.SenderWalletID.Value) - fmt.Printf("\n\nTransfer is:%v\n\n", transfer) - - // just making sure that the sender id is valid + // Validate sender wallet if !transfer.SenderWalletID.Valid { - return nil, fmt.Errorf("sender wallet id is invalid: %v \n", transfer.SenderWalletID) + return nil, fmt.Errorf("invalid sender wallet ID: %v", transfer.SenderWalletID) } - // If not verified or not found, verify with Chapa - verification, err := s.chapaClient.VerifyPayment(ctx, txRef) - if err != nil { - return nil, fmt.Errorf("failed to verify payment: %w", err) - } + var verification *domain.ChapaVerificationResponse - // Update our records if payment is successful - if verification.Status == domain.PaymentStatusCompleted { - err = s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true) + // Decide verification method based on type + switch strings.ToLower(string(transfer.Type)) { + case "deposit": + // Use Chapa Payment Verification + verification, err = s.chapaClient.ManualVerifyPayment(ctx, txRef) if err != nil { - return nil, fmt.Errorf("failed to update verification status: %w", err) + return nil, fmt.Errorf("failed to verify deposit with Chapa: %w", err) } - // Credit user's wallet - err = s.walletStore.UpdateBalance(ctx, transfer.SenderWalletID.Value, transfer.Amount) - if err != nil { - return nil, fmt.Errorf("failed to update wallet balance: %w", err) + if verification.Status == string(domain.PaymentStatusSuccessful) { + // Mark verified + if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil { + return nil, fmt.Errorf("failed to mark deposit transfer as verified: %w", err) + } + + // Credit wallet + if _, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value, transfer.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{}); err != nil { + return nil, fmt.Errorf("failed to credit wallet: %w", err) + } } + + case "withdraw": + // Use Chapa Transfer Verification + verification, err = s.chapaClient.ManualVerifyTransfer(ctx, txRef) + if err != nil { + return nil, fmt.Errorf("failed to verify withdrawal with Chapa: %w", err) + } + + if verification.Status == string(domain.PaymentStatusSuccessful) { + // Mark verified (withdraw doesn't affect balance) + if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil { + return nil, fmt.Errorf("failed to mark withdrawal transfer as verified: %w", err) + } + } + + default: + return nil, fmt.Errorf("unsupported transfer type: %s", transfer.Type) } - return &domain.ChapaVerificationResponse{ - Status: string(verification.Status), - Amount: float64(verification.Amount), - Currency: verification.Currency, - }, nil + return verification, nil } func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebHookTransfer) error { @@ -285,12 +314,13 @@ func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domai // verified = true // } - if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil { - return fmt.Errorf("failed to update payment status: %w", err) - } - // If payment is completed, credit user's wallet - if transfer.Status == string(domain.PaymentStatusCompleted) { + if transfer.Status == string(domain.PaymentStatusSuccessful) { + + if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil { + return fmt.Errorf("failed to update payment status: %w", err) + } + if _, err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID.Value, payment.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{ ReferenceNumber: domain.ValidString{ Value: transfer.Reference, @@ -326,12 +356,11 @@ func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domai // verified = true // } - if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil { - return fmt.Errorf("failed to update payment status: %w", err) - } - - // If payment is completed, credit user's wallet - if payment.Status == string(domain.PaymentStatusFailed) { + if payment.Status == string(domain.PaymentStatusSuccessful) { + if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil { + return fmt.Errorf("failed to update payment status: %w", err) + } // If payment is completed, credit user's walle + } else { if _, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value, transfer.Amount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil { return fmt.Errorf("failed to credit user wallet: %w", err) } diff --git a/internal/services/institutions/port.go b/internal/services/institutions/port.go new file mode 100644 index 0000000..b73a84a --- /dev/null +++ b/internal/services/institutions/port.go @@ -0,0 +1 @@ +package institutions diff --git a/internal/services/institutions/service.go b/internal/services/institutions/service.go new file mode 100644 index 0000000..9b54cd1 --- /dev/null +++ b/internal/services/institutions/service.go @@ -0,0 +1,44 @@ +package institutions + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" +) + +type Service struct { + repo repository.BankRepository +} + +func New(repo repository.BankRepository) *Service { + return &Service{repo: repo} +} + +func (s *Service) Create(ctx context.Context, bank *domain.Bank) error { + return s.repo.CreateBank(ctx, bank) +} + +func (s *Service) Update(ctx context.Context, bank *domain.Bank) error { + return s.repo.UpdateBank(ctx, bank) +} + +func (s *Service) GetByID(ctx context.Context, id int64) (*domain.Bank, error) { + return s.repo.GetBankByID(ctx, int(id)) +} + +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) + if err != nil { + return nil, err + } + result := make([]*domain.Bank, len(banks)) + for i := range banks { + result[i] = &banks[i] + } + return result, nil +} diff --git a/internal/services/issues/port.go b/internal/services/issues/port.go new file mode 100644 index 0000000..689aaa1 --- /dev/null +++ b/internal/services/issues/port.go @@ -0,0 +1 @@ +package issues \ No newline at end of file diff --git a/internal/services/issues/service.go b/internal/services/issues/service.go new file mode 100644 index 0000000..689aaa1 --- /dev/null +++ b/internal/services/issues/service.go @@ -0,0 +1 @@ +package issues \ No newline at end of file diff --git a/internal/services/virtualGame/port.go b/internal/services/virtualGame/port.go index 173598f..26d7816 100644 --- a/internal/services/virtualGame/port.go +++ b/internal/services/virtualGame/port.go @@ -13,6 +13,8 @@ type VirtualGameService interface { GetPlayerInfo(ctx context.Context, req *domain.PopOKPlayerInfoRequest) (*domain.PopOKPlayerInfoResponse, error) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequest) (*domain.PopOKCancelResponse, error) + ProcessTournamentWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error) + ProcessPromoWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) ListGames(ctx context.Context, currency string) ([]domain.PopOKGame, error) diff --git a/internal/services/virtualGame/service.go b/internal/services/virtualGame/service.go index b795b33..128364d 100644 --- a/internal/services/virtualGame/service.go +++ b/internal/services/virtualGame/service.go @@ -299,6 +299,167 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) ( }, nil } +func (s *service) ProcessTournamentWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error) { + // 1. Validate token and get user ID + claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey) + if err != nil { + s.logger.Error("Invalid token in tournament win request", "error", err) + return nil, fmt.Errorf("invalid token") + } + + // 2. Check for duplicate tournament win transaction + existingTx, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID) + if err != nil { + s.logger.Error("Failed to check existing tournament transaction", "error", err) + return nil, fmt.Errorf("transaction check failed") + } + if existingTx != nil && existingTx.TransactionType == "TOURNAMENT_WIN" { + s.logger.Warn("Duplicate tournament win", "transactionID", req.TransactionID) + wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) + balance := 0.0 + if len(wallets) > 0 { + balance = float64(wallets[0].Balance) / 100 + } + return &domain.PopOKWinResponse{ + TransactionID: req.TransactionID, + ExternalTrxID: fmt.Sprintf("%v", existingTx.ID), + Balance: balance, + }, nil + } + + // 3. Convert amount to cents + amountCents := int64(req.Amount * 100) + + // 4. Credit user wallet + if _, err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil { + s.logger.Error("Failed to credit wallet for tournament", "userID", claims.UserID, "error", err) + return nil, fmt.Errorf("wallet credit failed") + } + + // 5. Log tournament win transaction + tx := &domain.VirtualGameTransaction{ + UserID: claims.UserID, + TransactionType: "TOURNAMENT_WIN", + Amount: amountCents, + Currency: req.Currency, + ExternalTransactionID: req.TransactionID, + Status: "COMPLETED", + CreatedAt: time.Now(), + } + + if err := s.repo.CreateVirtualGameTransaction(ctx, tx); err != nil { + s.logger.Error("Failed to record tournament win transaction", "error", err) + return nil, fmt.Errorf("transaction recording failed") + } + + // 6. Fetch updated balance + wallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) + if err != nil { + return nil, fmt.Errorf("Failed to get wallet balance") + } + + return &domain.PopOKWinResponse{ + TransactionID: req.TransactionID, + ExternalTrxID: fmt.Sprintf("%v", tx.ID), + Balance: float64(wallets[0].Balance) / 100, + }, nil +} + +func (s *service) ProcessPromoWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error) { + claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey) + if err != nil { + s.logger.Error("Invalid token in promo win request", "error", err) + return nil, fmt.Errorf("invalid token") + } + + existingTx, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID) + if err != nil { + s.logger.Error("Failed to check existing promo transaction", "error", err) + return nil, fmt.Errorf("transaction check failed") + } + if existingTx != nil && existingTx.TransactionType == "PROMO_WIN" { + s.logger.Warn("Duplicate promo win", "transactionID", req.TransactionID) + wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) + balance := 0.0 + if len(wallets) > 0 { + balance = float64(wallets[0].Balance) / 100 + } + return &domain.PopOKWinResponse{ + TransactionID: req.TransactionID, + ExternalTrxID: fmt.Sprintf("%v", existingTx.ID), + Balance: balance, + }, nil + } + + amountCents := int64(req.Amount * 100) + + if _, err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil { + s.logger.Error("Failed to credit wallet for promo", "userID", claims.UserID, "error", err) + return nil, fmt.Errorf("wallet credit failed") + } + + tx := &domain.VirtualGameTransaction{ + UserID: claims.UserID, + TransactionType: "PROMO_WIN", + Amount: amountCents, + Currency: req.Currency, + ExternalTransactionID: req.TransactionID, + Status: "COMPLETED", + CreatedAt: time.Now(), + } + + if err := s.repo.CreateVirtualGameTransaction(ctx, tx); err != nil { + s.logger.Error("Failed to create promo win transaction", "error", err) + return nil, fmt.Errorf("transaction recording failed") + } + + wallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) + if err != nil { + return nil, fmt.Errorf("failed to read wallets") + } + + return &domain.PopOKWinResponse{ + TransactionID: req.TransactionID, + ExternalTrxID: fmt.Sprintf("%v", tx.ID), + Balance: float64(wallets[0].Balance) / 100, + }, nil +} + +// func (s *service) GenerateNewToken(ctx context.Context, req *domain.PopOKGenerateTokenRequest) (*domain.PopOKGenerateTokenResponse, error) { +// userID, err := strconv.ParseInt(req.PlayerID, 10, 64) +// if err != nil { +// s.logger.Error("Invalid player ID", "playerID", req.PlayerID, "error", err) +// return nil, fmt.Errorf("invalid player ID") +// } + +// user, err := s.store.GetUserByID(ctx, userID) +// if err != nil { +// s.logger.Error("Failed to find user for token refresh", "userID", userID, "error", err) +// return nil, fmt.Errorf("user not found") +// } + +// newSessionID := fmt.Sprintf("%d-%s-%d", userID, req.GameID, time.Now().UnixNano()) + +// token, err := jwtutil.CreatePopOKJwt( +// userID, +// user.FirstName, +// req.Currency, +// "en", +// req.Mode, +// newSessionID, +// s.config.PopOK.SecretKey, +// 24*time.Hour, +// ) +// if err != nil { +// s.logger.Error("Failed to generate new token", "userID", userID, "error", err) +// return nil, fmt.Errorf("token generation failed") +// } + +// return &domain.PopOKGenerateTokenResponse{ +// NewToken: token, +// }, nil +// } + func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequest) (*domain.PopOKCancelResponse, error) { // 1. Validate token and get user ID claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey) diff --git a/internal/web_server/app.go b/internal/web_server/app.go index 72926d8..be16c0c 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -12,6 +12,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation" @@ -36,6 +37,7 @@ import ( ) type App struct { + instSvc *institutions.Service currSvc *currency.Service fiber *fiber.App aleaVirtualGameService alea.AleaVirtualGameService @@ -68,6 +70,7 @@ type App struct { } func NewApp( + instSvc *institutions.Service, currSvc *currency.Service, port int, validator *customvalidator.CustomValidator, authSvc *authentication.Service, @@ -110,6 +113,7 @@ func NewApp( })) s := &App{ + instSvc: instSvc, currSvc: currSvc, fiber: app, port: port, diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index 335d07f..6ba2ec5 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -22,7 +22,7 @@ import ( // @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /bet [post] +// @Router /sport/bet [post] func (h *Handler) CreateBet(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) role := c.Locals("role").(domain.Role) @@ -82,7 +82,7 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error { // @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /random/bet [post] +// @Router /sport/random/bet [post] func (h *Handler) RandomBet(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) @@ -207,7 +207,7 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error { // @Success 200 {array} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /bet [get] +// @Router /sport/bet [get] func (h *Handler) GetAllBet(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) branchID := c.Locals("branch_id").(domain.ValidInt64) @@ -268,7 +268,7 @@ func (h *Handler) GetAllBet(c *fiber.Ctx) error { // @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /bet/{id} [get] +// @Router /sport/bet/{id} [get] func (h *Handler) GetBetByID(c *fiber.Ctx) error { betID := c.Params("id") id, err := strconv.ParseInt(betID, 10, 64) @@ -314,7 +314,7 @@ func (h *Handler) GetBetByID(c *fiber.Ctx) error { // @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /bet/cashout/{id} [get] +// @Router /sport/bet/cashout/{id} [get] func (h *Handler) GetBetByCashoutID(c *fiber.Ctx) error { cashoutID := c.Params("id") @@ -355,7 +355,7 @@ type UpdateCashOutReq struct { // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /bet/{id} [patch] +// @Router /sport/bet/{id} [patch] func (h *Handler) UpdateCashOut(c *fiber.Ctx) error { type UpdateCashOutReq struct { CashedOut bool `json:"cashed_out" validate:"required" example:"true"` @@ -418,7 +418,7 @@ func (h *Handler) UpdateCashOut(c *fiber.Ctx) error { // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /bet/{id} [delete] +// @Router /sport/bet/{id} [delete] func (h *Handler) DeleteBet(c *fiber.Ctx) error { betID := c.Params("id") id, err := strconv.ParseInt(betID, 10, 64) diff --git a/internal/web_server/handlers/chapa.go b/internal/web_server/handlers/chapa.go index 751f78c..5e9ad56 100644 --- a/internal/web_server/handlers/chapa.go +++ b/internal/web_server/handlers/chapa.go @@ -32,7 +32,7 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error { var req domain.ChapaDepositRequestPayload if err := c.BodyParser(&req); err != nil { - fmt.Sprintln("We first first are here init Chapa payment") + // fmt.Println("We first first are here init Chapa payment") return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Error: err.Error(), Message: "Failed to parse request body", @@ -41,7 +41,7 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error { amount := domain.Currency(req.Amount * 100) - fmt.Sprintln("We are here init Chapa payment") + fmt.Println("We are here init Chapa payment") checkoutURL, err := h.chapaSvc.InitiateDeposit(c.Context(), userID, amount) if err != nil { @@ -79,7 +79,7 @@ func (h *Handler) WebhookCallback(c *fiber.Ctx) error { } switch chapaTransactionType.Type { - case h.Cfg.CHAPA_TRANSFER_TYPE: + case h.Cfg.CHAPA_PAYMENT_TYPE: chapaTransferVerificationRequest := new(domain.ChapaWebHookTransfer) if err := c.BodyParser(chapaTransferVerificationRequest); err != nil { @@ -100,7 +100,7 @@ func (h *Handler) WebhookCallback(c *fiber.Ctx) error { Data: chapaTransferVerificationRequest, Success: true, }) - case h.Cfg.CHAPA_PAYMENT_TYPE: + case h.Cfg.CHAPA_TRANSFER_TYPE: chapaPaymentVerificationRequest := new(domain.ChapaWebHookPayment) if err := c.BodyParser(chapaPaymentVerificationRequest); err != nil { return domain.UnProcessableEntityResponse(c) @@ -147,7 +147,7 @@ func (h *Handler) ManualVerifyTransaction(c *fiber.Ctx) error { }) } - verification, err := h.chapaSvc.ManualVerifTransaction(c.Context(), txRef) + verification, err := h.chapaSvc.ManuallyVerify(c.Context(), txRef) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to verify Chapa transaction", diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index ad2fe3e..25615bc 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -11,6 +11,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" @@ -30,6 +31,7 @@ import ( ) type Handler struct { + instSvc *institutions.Service currSvc *currency.Service logger *slog.Logger notificationSvc *notificationservice.Service @@ -59,6 +61,7 @@ type Handler struct { } func New( + instSvc *institutions.Service, currSvc *currency.Service, logger *slog.Logger, notificationSvc *notificationservice.Service, @@ -87,6 +90,7 @@ func New( mongoLoggerSvc *zap.Logger, ) *Handler { return &Handler{ + instSvc: instSvc, currSvc: currSvc, logger: logger, notificationSvc: notificationSvc, diff --git a/internal/web_server/handlers/institutions.go b/internal/web_server/handlers/institutions.go new file mode 100644 index 0000000..bd723c0 --- /dev/null +++ b/internal/web_server/handlers/institutions.go @@ -0,0 +1,135 @@ +package handlers + +import ( + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/gofiber/fiber/v2" +) + +// @Summary Create a new bank +// @Tags Institutions - Banks +// @Accept json +// @Produce json +// @Param bank body domain.Bank true "Bank Info" +// @Success 201 {object} domain.Bank +// @Failure 400 {object} domain.ErrorResponse +// @Router /api/v1/banks [post] +func (h *Handler) CreateBank(c *fiber.Ctx) error { + var bank domain.Bank + if err := c.BodyParser(&bank); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid payload"}) + } + + err := h.instSvc.Create(c.Context(), &bank) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + return c.Status(fiber.StatusCreated).JSON(bank) +} + +// @Summary Get a bank by ID +// @Tags Institutions - Banks +// @Produce json +// @Param id path int true "Bank ID" +// @Success 200 {object} domain.Bank +// @Failure 400 {object} domain.ErrorResponse +// @Failure 404 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/banks/{id} [get] +func (h *Handler) GetBankByID(c *fiber.Ctx) error { + id, err := c.ParamsInt("id") + if err != nil || id <= 0 { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid bank ID"}) + } + + bank, err := h.instSvc.GetByID(c.Context(), int64(id)) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "bank not found"}) + } + + return c.JSON(bank) +} + +// @Summary Update a bank +// @Tags Institutions - Banks +// @Accept json +// @Produce json +// @Param id path int true "Bank ID" +// @Param bank body domain.Bank true "Bank Info" +// @Success 200 {object} domain.Bank +// @Failure 400 {object} domain.ErrorResponse +// @Failure 404 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/banks/{id} [put] +func (h *Handler) UpdateBank(c *fiber.Ctx) error { + id, err := c.ParamsInt("id") + if err != nil || id <= 0 { + // return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid bank ID"}) + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Failed to update bank", + Error: err.Error(), + }) + } + + var bank domain.Bank + if err := c.BodyParser(&bank); err != nil { + // return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid payload"}) + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Failed to update bank", + Error: err.Error(), + }) + } + bank.ID = id + + err = h.instSvc.Update(c.Context(), &bank) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to update bank", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Bank updated successfully", + StatusCode: fiber.StatusOK, + Success: true, + Data: bank, + }) + // return c.JSON(bank) +} + +// @Summary Delete a bank +// @Tags Institutions - Banks +// @Produce json +// @Param id path int true "Bank ID" +// @Success 204 {string} string "Deleted successfully" +// @Failure 400 {object} domain.ErrorResponse +// @Failure 404 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/banks/{id} [delete] +func (h *Handler) DeleteBank(c *fiber.Ctx) error { + id, err := c.ParamsInt("id") + if err != nil || id <= 0 { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid bank ID"}) + } + + err = h.instSvc.Delete(c.Context(), int64(id)) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + + return c.SendStatus(fiber.StatusNoContent) +} + +// @Summary List all banks +// @Tags Institutions - Banks +// @Produce json +// @Success 200 {array} domain.Bank +// @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()}) + } + return c.JSON(banks) +} diff --git a/internal/web_server/handlers/recommendation.go b/internal/web_server/handlers/recommendation.go index cdd8cdf..5d79f87 100644 --- a/internal/web_server/handlers/recommendation.go +++ b/internal/web_server/handlers/recommendation.go @@ -1,9 +1,5 @@ package handlers -import ( - "github.com/gofiber/fiber/v2" -) - // @Summary Get virtual game recommendations // @Description Returns a list of recommended virtual games for a specific user // @Tags Recommendations @@ -13,14 +9,15 @@ import ( // @Success 200 {object} domain.RecommendationSuccessfulResponse "Recommended games fetched successfully" // @Failure 500 {object} domain.RecommendationErrorResponse "Failed to fetch recommendations" // @Router /api/v1/virtual-games/recommendations/{userID} [get] -func (h *Handler) GetRecommendations(c *fiber.Ctx) error { - userID := c.Params("userID") // or from JWT - recommendations, err := h.recommendationSvc.GetRecommendations(c.Context(), userID) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch recommendations") - } - return c.JSON(fiber.Map{ - "message": "Recommended games fetched successfully", - "recommended_games": recommendations, - }) -} + +// func (h *Handler) GetRecommendations(c *fiber.Ctx) error { +// userID := c.Params("userID") // or from JWT +// recommendations, err := h.recommendationSvc.GetRecommendations(c.Context(), userID) +// if err != nil { +// return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch recommendations") +// } +// return c.JSON(fiber.Map{ +// "message": "Recommended games fetched successfully", +// "recommended_games": recommendations, +// }) +// } diff --git a/internal/web_server/handlers/transfer_handler.go b/internal/web_server/handlers/transfer_handler.go index 783022f..428ff5a 100644 --- a/internal/web_server/handlers/transfer_handler.go +++ b/internal/web_server/handlers/transfer_handler.go @@ -11,17 +11,19 @@ import ( ) type TransferWalletRes struct { - ID int64 `json:"id" example:"1"` - Amount float32 `json:"amount" example:"100.0"` - Verified bool `json:"verified" example:"true"` - Type string `json:"type" example:"transfer"` - PaymentMethod string `json:"payment_method" example:"bank"` - ReceiverWalletID *int64 `json:"receiver_wallet_id" example:"1"` - SenderWalletID *int64 `json:"sender_wallet_id" example:"1"` - CashierID *int64 `json:"cashier_id" example:"789"` - CreatedAt time.Time `json:"created_at" example:"2025-04-08T12:00:00Z"` - UpdatedAt time.Time `json:"updated_at" example:"2025-04-08T12:30:00Z"` + ID int64 `json:"id"` + Amount float32 `json:"amount"` + Verified bool `json:"verified"` + Type string `json:"type"` + PaymentMethod string `json:"payment_method"` + ReceiverWalletID *int64 `json:"receiver_wallet_id,omitempty"` + SenderWalletID *int64 `json:"sender_wallet_id,omitempty"` + CashierID *int64 `json:"cashier_id,omitempty"` + ReferenceNumber string `json:"reference_number"` // ← Add this + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } + type RefillRes struct { ID int64 `json:"id" example:"1"` Amount float32 `json:"amount" example:"100.0"` @@ -35,33 +37,34 @@ type RefillRes struct { UpdatedAt time.Time `json:"updated_at" example:"2025-04-08T12:30:00Z"` } -func convertTransfer(transfer domain.Transfer) TransferWalletRes { +func convertTransfer(t domain.Transfer) TransferWalletRes { + var receiverID *int64 + if t.ReceiverWalletID.Valid { + receiverID = &t.ReceiverWalletID.Value + } + + var senderID *int64 + if t.SenderWalletID.Valid { + senderID = &t.SenderWalletID.Value + } var cashierID *int64 - if transfer.CashierID.Valid { - cashierID = &transfer.CashierID.Value - } - var receiverID *int64 - if transfer.ReceiverWalletID.Valid { - receiverID = &transfer.ReceiverWalletID.Value - } - - var senderId *int64 - if transfer.SenderWalletID.Valid { - senderId = &transfer.SenderWalletID.Value + if t.CashierID.Valid { + cashierID = &t.CashierID.Value } return TransferWalletRes{ - ID: transfer.ID, - Amount: transfer.Amount.Float32(), - Verified: transfer.Verified, - Type: string(transfer.Type), - PaymentMethod: string(transfer.PaymentMethod), + ID: t.ID, + Amount: float32(t.Amount), + Verified: t.Verified, + Type: string(t.Type), + PaymentMethod: string(t.PaymentMethod), ReceiverWalletID: receiverID, - SenderWalletID: senderId, + SenderWalletID: senderID, CashierID: cashierID, - CreatedAt: transfer.CreatedAt, - UpdatedAt: transfer.UpdatedAt, + ReferenceNumber: t.ReferenceNumber, + CreatedAt: t.CreatedAt, + UpdatedAt: t.UpdatedAt, } } @@ -142,10 +145,11 @@ func (h *Handler) TransferToWallet(c *fiber.Ctx) error { var senderID int64 //TODO: check to make sure that the cashiers aren't transferring TO branch wallet - if role == domain.RoleCustomer { + switch role { + case domain.RoleCustomer: h.logger.Error("Unauthorized access", "userID", userID, "role", role) return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) - } else if role == domain.RoleBranchManager || role == domain.RoleAdmin || role == domain.RoleSuperAdmin { + case domain.RoleBranchManager, domain.RoleAdmin, domain.RoleSuperAdmin: company, err := h.companySvc.GetCompanyByID(c.Context(), companyID.Value) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ @@ -156,7 +160,7 @@ func (h *Handler) TransferToWallet(c *fiber.Ctx) error { } senderID = company.WalletID h.logger.Error("Will", "userID", userID, "role", role) - } else { + default: cashierBranch, err := h.branchSvc.GetBranchByCashier(c.Context(), userID) if err != nil { h.logger.Error("Failed to get branch", "user ID", userID, "error", err) diff --git a/internal/web_server/handlers/virtual_games_hadlers.go b/internal/web_server/handlers/virtual_games_hadlers.go index 3c48879..255d3a6 100644 --- a/internal/web_server/handlers/virtual_games_hadlers.go +++ b/internal/web_server/handlers/virtual_games_hadlers.go @@ -199,3 +199,45 @@ func (h *Handler) RecommendGames(c *fiber.Ctx) error { return c.JSON(recommendations) } + +func (h *Handler) HandleTournamentWin(c *fiber.Ctx) error { + var req domain.PopOKWinRequest + + if err := c.BodyParser(&req); err != nil { + h.logger.Error("Invalid tournament win request body", "error", err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request body", + }) + } + + resp, err := h.virtualGameSvc.ProcessTournamentWin(c.Context(), &req) + if err != nil { + h.logger.Error("Failed to process tournament win", "error", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return c.JSON(resp) +} + +func (h *Handler) HandlePromoWin(c *fiber.Ctx) error { + var req domain.PopOKWinRequest + + if err := c.BodyParser(&req); err != nil { + h.logger.Error("Invalid promo win request body", "error", err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request body", + }) + } + + resp, err := h.virtualGameSvc.ProcessPromoWin(c.Context(), &req) + if err != nil { + h.logger.Error("Failed to process promo win", "error", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return c.JSON(resp) +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index be73bfa..0523271 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -20,6 +20,7 @@ import ( func (a *App) initAppRoutes() { h := handlers.New( + a.instSvc, a.currSvc, a.logger, a.NotidicationStore, @@ -187,7 +188,7 @@ func (a *App) initAppRoutes() { a.fiber.Patch("/sport/bet/:id", a.authMiddleware, h.UpdateCashOut) a.fiber.Delete("/sport/bet/:id", a.authMiddleware, h.DeleteBet) - a.fiber.Post("/random/bet", a.authMiddleware, h.RandomBet) + a.fiber.Post("/sport/random/bet", a.authMiddleware, h.RandomBet) // Wallet a.fiber.Get("/wallet", h.GetAllWallets) @@ -274,6 +275,8 @@ func (a *App) initAppRoutes() { a.fiber.Post("/bet", h.HandleBet) a.fiber.Post("/win", h.HandleWin) a.fiber.Post("/cancel", h.HandleCancel) + a.fiber.Post("/promoWin ", h.HandlePromoWin) + a.fiber.Post("/tournamentWin ", h.HandleTournamentWin) a.fiber.Get("/popok/games", h.GetGameList) a.fiber.Get("/popok/games/recommend", a.authMiddleware, h.RecommendGames)