From 208a2d74becb8bbf846768a9c909355ff9dc6faf Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Sat, 26 Apr 2025 14:48:45 +0300 Subject: [PATCH 1/2] integration fixes --- db/migrations/000001_fortune.up.sql | 7 +- db/migrations/000003_referal.up.sql | 19 +- db/query/auth.sql | 20 +- db/query/company.sql | 4 + db/query/user.sql | 71 +- docs/docs.go | 673 +++++++++++++++++- docs/swagger.json | 673 +++++++++++++++++- docs/swagger.yaml | 441 +++++++++++- gen/db/auth.sql.go | 34 +- gen/db/branch.sql.go | 6 +- gen/db/company.sql.go | 33 + gen/db/models.go | 36 +- gen/db/transactions.sql.go | 12 +- gen/db/user.sql.go | 147 +++- go.mod | 5 +- go.sum | 6 + internal/domain/transaction.go | 36 +- internal/domain/user.go | 16 +- internal/repository/auth.go | 43 +- internal/repository/company.go | 18 + internal/repository/transaction.go | 27 +- internal/repository/user.go | 158 ++-- internal/services/authentication/impl.go | 41 +- internal/services/authentication/port.go | 1 + internal/services/bet/service.go | 17 + internal/services/company/port.go | 1 + internal/services/company/service.go | 4 + internal/services/user/direct.go | 16 +- internal/services/user/port.go | 6 +- internal/services/user/register.go | 2 +- internal/web_server/app.go | 2 +- internal/web_server/handlers/admin.go | 156 ++++ internal/web_server/handlers/auth_handler.go | 52 +- internal/web_server/handlers/bet_handler.go | 50 +- internal/web_server/handlers/cashier.go | 40 +- .../web_server/handlers/company_handler.go | 31 + internal/web_server/handlers/manager.go | 93 ++- .../web_server/handlers/ticket_handler.go | 13 +- .../handlers/transaction_handler.go | 66 +- internal/web_server/handlers/user.go | 162 ++++- .../handlers/virtual_games_hadlers.go | 19 +- .../web_server/handlers/wallet_handler.go | 7 +- internal/web_server/jwt/jwt.go | 14 +- internal/web_server/middleware.go | 2 +- internal/web_server/routes.go | 30 +- 45 files changed, 2905 insertions(+), 405 deletions(-) create mode 100644 internal/web_server/handlers/admin.go diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 111aa73..5a8c725 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -10,8 +10,9 @@ CREATE TABLE IF NOT EXISTS users ( phone_verified BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ, - -- - suspended_at TIMESTAMPTZ NULL, -- this can be NULL if the user is not suspended + company_id BIGINT, + suspended_at TIMESTAMPTZ NULL, + -- this can be NULL if the user is not suspended suspended BOOLEAN NOT NULL DEFAULT FALSE, CHECK ( email IS NOT NULL @@ -53,6 +54,7 @@ CREATE TABLE IF NOT EXISTS bets ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, is_shop_bet BOOLEAN NOT NULL, + UNIQUE(cashout_id), CHECK ( user_id IS NOT NULL OR branch_id IS NOT NULL @@ -148,6 +150,7 @@ CREATE TABLE IF NOT EXISTS transactions ( branch_id BIGINT NOT NULL, cashier_id BIGINT NOT NULL, bet_id BIGINT NOT NULL, + number_of_outcomes BIGINT NOT NULL, type BIGINT NOT NULL, payment_option BIGINT NOT NULL, full_name VARCHAR(255) NOT NULL, diff --git a/db/migrations/000003_referal.up.sql b/db/migrations/000003_referal.up.sql index 4f8a181..521e3e3 100644 --- a/db/migrations/000003_referal.up.sql +++ b/db/migrations/000003_referal.up.sql @@ -1,5 +1,4 @@ CREATE TYPE ReferralStatus AS ENUM ('PENDING', 'COMPLETED', 'EXPIRED', 'CANCELLED'); - CREATE TABLE IF NOT EXISTS referral_settings ( id BIGSERIAL PRIMARY KEY, referral_reward_amount DECIMAL(15, 2) NOT NULL DEFAULT 0.00, @@ -17,7 +16,6 @@ CREATE TABLE IF NOT EXISTS referral_settings ( AND cashback_percentage <= 100 ) ); - CREATE TABLE IF NOT EXISTS referrals ( id BIGSERIAL PRIMARY KEY, referral_code VARCHAR(10) NOT NULL UNIQUE, @@ -29,25 +27,20 @@ CREATE TABLE IF NOT EXISTS referrals ( created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMPTZ NOT NULL, - FOREIGN KEY (referrer_id) REFERENCES users (id), - FOREIGN KEY (referred_id) REFERENCES users (id), + -- FOREIGN KEY (referrer_id) REFERENCES users (id), + -- FOREIGN KEY (referred_id) REFERENCES users (id), CONSTRAINT reward_amount_positive CHECK (reward_amount >= 0), CONSTRAINT cashback_amount_positive CHECK (cashback_amount >= 0) ); - CREATE INDEX idx_referrals_referral_code ON referrals (referral_code); - CREATE INDEX idx_referrals_referrer_id ON referrals (referrer_id); - CREATE INDEX idx_referrals_status ON referrals (status); - ALTER TABLE users ADD COLUMN IF NOT EXISTS referral_code VARCHAR(10) UNIQUE, -ADD COLUMN IF NOT EXISTS referred_by VARCHAR(10); - + ADD COLUMN IF NOT EXISTS referred_by VARCHAR(10); -- Modify wallet table to track bonus money separately ALTER TABLE wallets ADD COLUMN IF NOT EXISTS bonus_balance DECIMAL(15, 2) NOT NULL DEFAULT 0.00, -ADD COLUMN IF NOT EXISTS cash_balance DECIMAL(15, 2) NOT NULL DEFAULT 0.00, -ADD CONSTRAINT bonus_balance_positive CHECK (bonus_balance >= 0), -ADD CONSTRAINT cash_balance_positive CHECK (cash_balance >= 0); + ADD COLUMN IF NOT EXISTS cash_balance DECIMAL(15, 2) NOT NULL DEFAULT 0.00, + ADD CONSTRAINT bonus_balance_positive CHECK (bonus_balance >= 0), + ADD CONSTRAINT cash_balance_positive CHECK (cash_balance >= 0); \ No newline at end of file diff --git a/db/query/auth.sql b/db/query/auth.sql index 71e45ec..99366e8 100644 --- a/db/query/auth.sql +++ b/db/query/auth.sql @@ -1,16 +1,20 @@ -- name: GetUserByEmailPhone :one -SELECT * FROM users -WHERE email = $1 OR phone_number = $2; - +SELECT * +FROM users +WHERE email = $1 + OR phone_number = $2; -- name: CreateRefreshToken :exec INSERT INTO refresh_tokens (user_id, token, expires_at, created_at, revoked) VALUES ($1, $2, $3, $4, $5); - -- name: GetRefreshToken :one -SELECT * FROM refresh_tokens +SELECT * +FROM refresh_tokens WHERE token = $1; - +-- name: GetRefreshTokenByUserID :one +SELECT * +FROM refresh_tokens +WHERE user_id = $1; -- name: RevokeRefreshToken :exec -UPDATE refresh_tokens -SET revoked = TRUE +UPDATE refresh_tokens +SET revoked = TRUE WHERE token = $1; \ No newline at end of file diff --git a/db/query/company.sql b/db/query/company.sql index d82cb7a..6747a74 100644 --- a/db/query/company.sql +++ b/db/query/company.sql @@ -13,6 +13,10 @@ FROM companies; SELECT * FROM companies WHERE id = $1; +-- name: SearchCompanyByName :many +SELECT * +FROM companies +WHERE name ILIKE '%' || $1 || '%'; -- name: UpdateCompany :one UPDATE companies SET name = $1, diff --git a/db/query/user.sql b/db/query/user.sql index bee4713..1fd2029 100644 --- a/db/query/user.sql +++ b/db/query/user.sql @@ -9,9 +9,24 @@ INSERT INTO users ( email_verified, phone_verified, created_at, - updated_at + updated_at, + suspended, + company_id + ) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12 ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, first_name, last_name, @@ -21,7 +36,9 @@ RETURNING id, email_verified, phone_verified, created_at, - updated_at; + updated_at, + suspended, + company_id; -- name: GetUserByID :one SELECT * FROM users @@ -36,8 +53,31 @@ SELECT id, email_verified, phone_verified, created_at, - updated_at -FROM users; + updated_at, + suspended, + suspended_at, + company_id +FROM users +wHERE ( + role = $1 + OR $1 IS NULL + ) + AND ( + company_id = $2 + OR $2 IS NULL + ) +LIMIT $3 OFFSET $4; +-- name: GetTotalUsers :one +SELECT COUNT(*) +FROM users +wHERE ( + role = $1 + OR $1 IS NULL + ) + AND ( + company_id = $2 + OR $2 IS NULL + ); -- name: SearchUserByNameOrPhone :many SELECT id, first_name, @@ -48,7 +88,10 @@ SELECT id, email_verified, phone_verified, created_at, - updated_at + updated_at, + suspended, + suspended_at, + company_id FROM users WHERE first_name ILIKE '%' || $1 || '%' OR last_name ILIKE '%' || $1 || '%' @@ -62,6 +105,12 @@ SET first_name = $1, role = $5, updated_at = $6 WHERE id = $7; +-- name: SuspendUser :exec +UPDATE users +SET suspended = $1, + suspended_at = $2, + updated_at = CURRENT_TIMESTAMP +WHERE id = $3; -- name: DeleteUser :exec DELETE FROM users WHERE id = $1; @@ -88,7 +137,10 @@ SELECT id, email_verified, phone_verified, created_at, - updated_at + updated_at, + suspended, + suspended_at, + company_id FROM users WHERE email = $1; -- name: GetUserByPhone :one @@ -101,7 +153,10 @@ SELECT id, email_verified, phone_verified, created_at, - updated_at + updated_at, + suspended, + suspended_at, + company_id FROM users WHERE phone_number = $1; -- name: UpdatePassword :exec diff --git a/docs/docs.go b/docs/docs.go index cb1802c..d7a5bad 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -24,6 +24,111 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/admin": { + "get": { + "description": "Get all Admins", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get all Admins", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "post": { + "description": "Create Admin", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create Admin", + "parameters": [ + { + "description": "Create admin", + "name": "manger", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateAdminReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/auth/login": { "post": { "description": "Login customer", @@ -955,6 +1060,13 @@ const docTemplate = `{ ], "summary": "Update cashier", "parameters": [ + { + "type": "integer", + "description": "Cashier ID", + "name": "id", + "in": "path", + "required": true + }, { "description": "Update cashier", "name": "cashier", @@ -1361,7 +1473,7 @@ const docTemplate = `{ } }, "post": { - "description": "Create Managers", + "description": "Create Manager", "consumes": [ "application/json" ], @@ -1371,7 +1483,7 @@ const docTemplate = `{ "tags": [ "manager" ], - "summary": "Create Managers", + "summary": "Create Manager", "parameters": [ { "description": "Create manager", @@ -1803,6 +1915,147 @@ const docTemplate = `{ } } }, + "/referral/settings": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Retrieves current referral settings (admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "referral" + ], + "summary": "Get referral settings", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ReferralSettings" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Updates referral settings (admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "referral" + ], + "summary": "Update referral settings", + "parameters": [ + { + "description": "Referral settings", + "name": "settings", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.ReferralSettings" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/referral/stats": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Retrieves referral statistics for the authenticated user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "referral" + ], + "summary": "Get referral statistics", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ReferralStats" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/search/branch": { "get": { "description": "Search branches by name or location", @@ -1850,6 +2103,44 @@ const docTemplate = `{ } } }, + "/search/company": { + "get": { + "description": "Gets all companies", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "company" + ], + "summary": "Gets all companies", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.CompanyRes" + } + } + }, + "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", @@ -2184,7 +2475,7 @@ const docTemplate = `{ } }, "patch": { - "description": "Updates the cashed out field", + "description": "Updates the verified status of a transaction", "consumes": [ "application/json" ], @@ -2194,7 +2485,7 @@ const docTemplate = `{ "tags": [ "transaction" ], - "summary": "Updates the cashed out field", + "summary": "Updates the verified field of a transaction", "parameters": [ { "type": "integer", @@ -2205,7 +2496,7 @@ const docTemplate = `{ }, { "description": "Updates Transaction Verification", - "name": "updateCashOut", + "name": "updateVerified", "in": "body", "required": true, "schema": { @@ -2689,6 +2980,56 @@ const docTemplate = `{ } } }, + "/user/single/{id}": { + "get": { + "description": "Get a single user by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Get user by id", + "parameters": [ + { + "type": "integer", + "description": "User 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" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/user/wallet": { "get": { "security": [ @@ -2738,6 +3079,109 @@ const docTemplate = `{ } } }, + "/virtual-game/callback": { + "post": { + "description": "Processes callbacks from PopOK for game events", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "virtual-game" + ], + "summary": "Handle PopOK game callback", + "parameters": [ + { + "description": "Callback data", + "name": "callback", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.PopOKCallback" + } + } + ], + "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" + } + } + } + } + }, + "/virtual-game/launch": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Generates a URL to launch a PopOK game", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "virtual-game" + ], + "summary": "Launch a PopOK virtual game", + "parameters": [ + { + "description": "Game launch details", + "name": "launch", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.launchVirtualGameReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.launchVirtualGameRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/wallet": { "get": { "description": "Retrieve all wallets", @@ -3019,6 +3463,34 @@ const docTemplate = `{ "BANK" ] }, + "domain.PopOKCallback": { + "type": "object", + "properties": { + "amount": { + "type": "number" + }, + "currency": { + "type": "string" + }, + "session_id": { + "type": "string" + }, + "signature": { + "description": "HMAC-SHA256 signature for verification", + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "transaction_id": { + "type": "string" + }, + "type": { + "description": "BET, WIN, REFUND, JACKPOT_WIN", + "type": "string" + } + } + }, "domain.RawOddsByMarketID": { "type": "object", "properties": { @@ -3040,6 +3512,58 @@ const docTemplate = `{ } } }, + "domain.ReferralSettings": { + "type": "object", + "properties": { + "betReferralBonusPercentage": { + "type": "number" + }, + "cashbackPercentage": { + "type": "number" + }, + "createdAt": { + "type": "string" + }, + "expiresAfterDays": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "maxReferrals": { + "type": "integer" + }, + "referralRewardAmount": { + "type": "number" + }, + "updatedAt": { + "type": "string" + }, + "updatedBy": { + "type": "string" + }, + "version": { + "type": "integer" + } + } + }, + "domain.ReferralStats": { + "type": "object", + "properties": { + "completedReferrals": { + "type": "integer" + }, + "pendingRewards": { + "type": "number" + }, + "totalReferrals": { + "type": "integer" + }, + "totalRewardEarned": { + "type": "number" + } + } + }, "domain.Role": { "type": "string", "enum": [ @@ -3327,6 +3851,9 @@ const docTemplate = `{ }, "handlers.CheckPhoneEmailExistReq": { "type": "object", + "required": [ + "phone_number" + ], "properties": { "email": { "type": "string", @@ -3370,11 +3897,39 @@ const docTemplate = `{ } } }, + "handlers.CreateAdminReq": { + "type": "object", + "properties": { + "company_id": { + "type": "integer", + "example": 1 + }, + "email": { + "type": "string", + "example": "john.doe@example.com" + }, + "first_name": { + "type": "string", + "example": "John" + }, + "last_name": { + "type": "string", + "example": "Doe" + }, + "password": { + "type": "string", + "example": "password123" + }, + "phone_number": { + "type": "string", + "example": "1234567890" + } + } + }, "handlers.CreateBetOutcomeReq": { "type": "object", "properties": { "event_id": { - "description": "BetID int64 ` + "`" + `json:\"bet_id\" example:\"1\"` + "`" + `", "type": "integer", "example": 1 }, @@ -3399,10 +3954,6 @@ const docTemplate = `{ "type": "string", "example": "John" }, - "is_shop_bet": { - "type": "boolean", - "example": false - }, "outcomes": { "type": "array", "items": { @@ -3497,6 +4048,10 @@ const docTemplate = `{ "phone_number": { "type": "string", "example": "1234567890" + }, + "suspended": { + "type": "boolean", + "example": false } } }, @@ -3516,6 +4071,10 @@ const docTemplate = `{ "handlers.CreateManagerReq": { "type": "object", "properties": { + "company_id": { + "type": "integer", + "example": 1 + }, "email": { "type": "string", "example": "john.doe@example.com" @@ -3737,7 +4296,6 @@ const docTemplate = `{ "example": "Doe" }, "otp": { - "description": "Role string", "type": "string", "example": "123456" }, @@ -3770,18 +4328,27 @@ const docTemplate = `{ }, "handlers.ResetPasswordReq": { "type": "object", + "required": [ + "otp", + "password" + ], "properties": { "email": { - "type": "string" + "type": "string", + "example": "john.doe@example.com" }, "otp": { - "type": "string" + "type": "string", + "example": "123456" }, "password": { - "type": "string" + "type": "string", + "minLength": 8, + "example": "newpassword123" }, - "phoneNumber": { - "type": "string" + "phone_number": { + "type": "string", + "example": "1234567890" } } }, @@ -3872,6 +4439,10 @@ const docTemplate = `{ "type": "integer", "example": 1 }, + "number_of_outcomes": { + "type": "integer", + "example": 1 + }, "payment_option": { "allOf": [ { @@ -3952,17 +4523,25 @@ const docTemplate = `{ }, "handlers.UpdateTransactionVerifiedReq": { "type": "object", + "required": [ + "verified" + ], "properties": { "verified": { - "type": "boolean" + "type": "boolean", + "example": true } } }, "handlers.UpdateWalletActiveReq": { "type": "object", + "required": [ + "is_active" + ], "properties": { - "isActive": { - "type": "boolean" + "is_active": { + "type": "boolean", + "example": true } } }, @@ -3984,6 +4563,9 @@ const docTemplate = `{ "id": { "type": "integer" }, + "last_login": { + "type": "string" + }, "last_name": { "type": "string" }, @@ -4046,8 +4628,45 @@ const docTemplate = `{ } } }, + "handlers.launchVirtualGameReq": { + "type": "object", + "required": [ + "currency", + "game_id", + "mode" + ], + "properties": { + "currency": { + "type": "string", + "example": "USD" + }, + "game_id": { + "type": "string", + "example": "crash_001" + }, + "mode": { + "type": "string", + "enum": [ + "REAL", + "DEMO" + ], + "example": "REAL" + } + } + }, + "handlers.launchVirtualGameRes": { + "type": "object", + "properties": { + "launch_url": { + "type": "string" + } + } + }, "handlers.loginCustomerReq": { "type": "object", + "required": [ + "password" + ], "properties": { "email": { "type": "string", @@ -4079,20 +4698,30 @@ const docTemplate = `{ }, "handlers.logoutReq": { "type": "object", + "required": [ + "refresh_token" + ], "properties": { "refresh_token": { - "type": "string" + "type": "string", + "example": "\u003crefresh-token\u003e" } } }, "handlers.refreshToken": { "type": "object", + "required": [ + "access_token", + "refresh_token" + ], "properties": { "access_token": { - "type": "string" + "type": "string", + "example": "\u003cjwt-token\u003e" }, "refresh_token": { - "type": "string" + "type": "string", + "example": "\u003crefresh-token\u003e" } } }, diff --git a/docs/swagger.json b/docs/swagger.json index ad7007a..7be8f14 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -16,6 +16,111 @@ "version": "1.0" }, "paths": { + "/admin": { + "get": { + "description": "Get all Admins", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get all Admins", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "post": { + "description": "Create Admin", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create Admin", + "parameters": [ + { + "description": "Create admin", + "name": "manger", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateAdminReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/auth/login": { "post": { "description": "Login customer", @@ -947,6 +1052,13 @@ ], "summary": "Update cashier", "parameters": [ + { + "type": "integer", + "description": "Cashier ID", + "name": "id", + "in": "path", + "required": true + }, { "description": "Update cashier", "name": "cashier", @@ -1353,7 +1465,7 @@ } }, "post": { - "description": "Create Managers", + "description": "Create Manager", "consumes": [ "application/json" ], @@ -1363,7 +1475,7 @@ "tags": [ "manager" ], - "summary": "Create Managers", + "summary": "Create Manager", "parameters": [ { "description": "Create manager", @@ -1795,6 +1907,147 @@ } } }, + "/referral/settings": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Retrieves current referral settings (admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "referral" + ], + "summary": "Get referral settings", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ReferralSettings" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Updates referral settings (admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "referral" + ], + "summary": "Update referral settings", + "parameters": [ + { + "description": "Referral settings", + "name": "settings", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.ReferralSettings" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/referral/stats": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Retrieves referral statistics for the authenticated user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "referral" + ], + "summary": "Get referral statistics", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ReferralStats" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/search/branch": { "get": { "description": "Search branches by name or location", @@ -1842,6 +2095,44 @@ } } }, + "/search/company": { + "get": { + "description": "Gets all companies", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "company" + ], + "summary": "Gets all companies", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.CompanyRes" + } + } + }, + "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", @@ -2176,7 +2467,7 @@ } }, "patch": { - "description": "Updates the cashed out field", + "description": "Updates the verified status of a transaction", "consumes": [ "application/json" ], @@ -2186,7 +2477,7 @@ "tags": [ "transaction" ], - "summary": "Updates the cashed out field", + "summary": "Updates the verified field of a transaction", "parameters": [ { "type": "integer", @@ -2197,7 +2488,7 @@ }, { "description": "Updates Transaction Verification", - "name": "updateCashOut", + "name": "updateVerified", "in": "body", "required": true, "schema": { @@ -2681,6 +2972,56 @@ } } }, + "/user/single/{id}": { + "get": { + "description": "Get a single user by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Get user by id", + "parameters": [ + { + "type": "integer", + "description": "User 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" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/user/wallet": { "get": { "security": [ @@ -2730,6 +3071,109 @@ } } }, + "/virtual-game/callback": { + "post": { + "description": "Processes callbacks from PopOK for game events", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "virtual-game" + ], + "summary": "Handle PopOK game callback", + "parameters": [ + { + "description": "Callback data", + "name": "callback", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.PopOKCallback" + } + } + ], + "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" + } + } + } + } + }, + "/virtual-game/launch": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Generates a URL to launch a PopOK game", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "virtual-game" + ], + "summary": "Launch a PopOK virtual game", + "parameters": [ + { + "description": "Game launch details", + "name": "launch", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.launchVirtualGameReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.launchVirtualGameRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/wallet": { "get": { "description": "Retrieve all wallets", @@ -3011,6 +3455,34 @@ "BANK" ] }, + "domain.PopOKCallback": { + "type": "object", + "properties": { + "amount": { + "type": "number" + }, + "currency": { + "type": "string" + }, + "session_id": { + "type": "string" + }, + "signature": { + "description": "HMAC-SHA256 signature for verification", + "type": "string" + }, + "timestamp": { + "type": "integer" + }, + "transaction_id": { + "type": "string" + }, + "type": { + "description": "BET, WIN, REFUND, JACKPOT_WIN", + "type": "string" + } + } + }, "domain.RawOddsByMarketID": { "type": "object", "properties": { @@ -3032,6 +3504,58 @@ } } }, + "domain.ReferralSettings": { + "type": "object", + "properties": { + "betReferralBonusPercentage": { + "type": "number" + }, + "cashbackPercentage": { + "type": "number" + }, + "createdAt": { + "type": "string" + }, + "expiresAfterDays": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "maxReferrals": { + "type": "integer" + }, + "referralRewardAmount": { + "type": "number" + }, + "updatedAt": { + "type": "string" + }, + "updatedBy": { + "type": "string" + }, + "version": { + "type": "integer" + } + } + }, + "domain.ReferralStats": { + "type": "object", + "properties": { + "completedReferrals": { + "type": "integer" + }, + "pendingRewards": { + "type": "number" + }, + "totalReferrals": { + "type": "integer" + }, + "totalRewardEarned": { + "type": "number" + } + } + }, "domain.Role": { "type": "string", "enum": [ @@ -3319,6 +3843,9 @@ }, "handlers.CheckPhoneEmailExistReq": { "type": "object", + "required": [ + "phone_number" + ], "properties": { "email": { "type": "string", @@ -3362,11 +3889,39 @@ } } }, + "handlers.CreateAdminReq": { + "type": "object", + "properties": { + "company_id": { + "type": "integer", + "example": 1 + }, + "email": { + "type": "string", + "example": "john.doe@example.com" + }, + "first_name": { + "type": "string", + "example": "John" + }, + "last_name": { + "type": "string", + "example": "Doe" + }, + "password": { + "type": "string", + "example": "password123" + }, + "phone_number": { + "type": "string", + "example": "1234567890" + } + } + }, "handlers.CreateBetOutcomeReq": { "type": "object", "properties": { "event_id": { - "description": "BetID int64 `json:\"bet_id\" example:\"1\"`", "type": "integer", "example": 1 }, @@ -3391,10 +3946,6 @@ "type": "string", "example": "John" }, - "is_shop_bet": { - "type": "boolean", - "example": false - }, "outcomes": { "type": "array", "items": { @@ -3489,6 +4040,10 @@ "phone_number": { "type": "string", "example": "1234567890" + }, + "suspended": { + "type": "boolean", + "example": false } } }, @@ -3508,6 +4063,10 @@ "handlers.CreateManagerReq": { "type": "object", "properties": { + "company_id": { + "type": "integer", + "example": 1 + }, "email": { "type": "string", "example": "john.doe@example.com" @@ -3729,7 +4288,6 @@ "example": "Doe" }, "otp": { - "description": "Role string", "type": "string", "example": "123456" }, @@ -3762,18 +4320,27 @@ }, "handlers.ResetPasswordReq": { "type": "object", + "required": [ + "otp", + "password" + ], "properties": { "email": { - "type": "string" + "type": "string", + "example": "john.doe@example.com" }, "otp": { - "type": "string" + "type": "string", + "example": "123456" }, "password": { - "type": "string" + "type": "string", + "minLength": 8, + "example": "newpassword123" }, - "phoneNumber": { - "type": "string" + "phone_number": { + "type": "string", + "example": "1234567890" } } }, @@ -3864,6 +4431,10 @@ "type": "integer", "example": 1 }, + "number_of_outcomes": { + "type": "integer", + "example": 1 + }, "payment_option": { "allOf": [ { @@ -3944,17 +4515,25 @@ }, "handlers.UpdateTransactionVerifiedReq": { "type": "object", + "required": [ + "verified" + ], "properties": { "verified": { - "type": "boolean" + "type": "boolean", + "example": true } } }, "handlers.UpdateWalletActiveReq": { "type": "object", + "required": [ + "is_active" + ], "properties": { - "isActive": { - "type": "boolean" + "is_active": { + "type": "boolean", + "example": true } } }, @@ -3976,6 +4555,9 @@ "id": { "type": "integer" }, + "last_login": { + "type": "string" + }, "last_name": { "type": "string" }, @@ -4038,8 +4620,45 @@ } } }, + "handlers.launchVirtualGameReq": { + "type": "object", + "required": [ + "currency", + "game_id", + "mode" + ], + "properties": { + "currency": { + "type": "string", + "example": "USD" + }, + "game_id": { + "type": "string", + "example": "crash_001" + }, + "mode": { + "type": "string", + "enum": [ + "REAL", + "DEMO" + ], + "example": "REAL" + } + } + }, + "handlers.launchVirtualGameRes": { + "type": "object", + "properties": { + "launch_url": { + "type": "string" + } + } + }, "handlers.loginCustomerReq": { "type": "object", + "required": [ + "password" + ], "properties": { "email": { "type": "string", @@ -4071,20 +4690,30 @@ }, "handlers.logoutReq": { "type": "object", + "required": [ + "refresh_token" + ], "properties": { "refresh_token": { - "type": "string" + "type": "string", + "example": "\u003crefresh-token\u003e" } } }, "handlers.refreshToken": { "type": "object", + "required": [ + "access_token", + "refresh_token" + ], "properties": { "access_token": { - "type": "string" + "type": "string", + "example": "\u003cjwt-token\u003e" }, "refresh_token": { - "type": "string" + "type": "string", + "example": "\u003crefresh-token\u003e" } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index cfa3120..243a29a 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -103,6 +103,25 @@ definitions: - TELEBIRR_TRANSACTION - ARIFPAY_TRANSACTION - BANK + domain.PopOKCallback: + properties: + amount: + type: number + currency: + type: string + session_id: + type: string + signature: + description: HMAC-SHA256 signature for verification + type: string + timestamp: + type: integer + transaction_id: + type: string + type: + description: BET, WIN, REFUND, JACKPOT_WIN + type: string + type: object domain.RawOddsByMarketID: properties: fetched_at: @@ -117,6 +136,40 @@ definitions: items: {} type: array type: object + domain.ReferralSettings: + properties: + betReferralBonusPercentage: + type: number + cashbackPercentage: + type: number + createdAt: + type: string + expiresAfterDays: + type: integer + id: + type: integer + maxReferrals: + type: integer + referralRewardAmount: + type: number + updatedAt: + type: string + updatedBy: + type: string + version: + type: integer + type: object + domain.ReferralStats: + properties: + completedReferrals: + type: integer + pendingRewards: + type: number + totalReferrals: + type: integer + totalRewardEarned: + type: number + type: object domain.Role: enum: - super_admin @@ -331,6 +384,8 @@ definitions: phone_number: example: "1234567890" type: string + required: + - phone_number type: object handlers.CheckPhoneEmailExistRes: properties: @@ -354,10 +409,30 @@ definitions: example: 1 type: integer type: object + handlers.CreateAdminReq: + properties: + company_id: + example: 1 + type: integer + email: + example: john.doe@example.com + type: string + first_name: + example: John + type: string + last_name: + example: Doe + type: string + password: + example: password123 + type: string + phone_number: + example: "1234567890" + type: string + type: object handlers.CreateBetOutcomeReq: properties: event_id: - description: BetID int64 `json:"bet_id" example:"1"` example: 1 type: integer market_id: @@ -375,9 +450,6 @@ definitions: full_name: example: John type: string - is_shop_bet: - example: false - type: boolean outcomes: items: $ref: '#/definitions/handlers.CreateBetOutcomeReq' @@ -444,6 +516,9 @@ definitions: phone_number: example: "1234567890" type: string + suspended: + example: false + type: boolean type: object handlers.CreateCompanyReq: properties: @@ -456,6 +531,9 @@ definitions: type: object handlers.CreateManagerReq: properties: + company_id: + example: 1 + type: integer email: example: john.doe@example.com type: string @@ -611,7 +689,6 @@ definitions: example: Doe type: string otp: - description: Role string example: "123456" type: string password: @@ -636,13 +713,21 @@ definitions: handlers.ResetPasswordReq: properties: email: + example: john.doe@example.com type: string otp: + example: "123456" type: string password: + example: newpassword123 + minLength: 8 type: string - phoneNumber: + phone_number: + example: "1234567890" type: string + required: + - otp + - password type: object handlers.SearchUserByNameOrPhoneReq: properties: @@ -705,6 +790,9 @@ definitions: id: example: 1 type: integer + number_of_outcomes: + example: 1 + type: integer payment_option: allOf: - $ref: '#/definitions/domain.PaymentOption' @@ -762,12 +850,18 @@ definitions: handlers.UpdateTransactionVerifiedReq: properties: verified: + example: true type: boolean + required: + - verified type: object handlers.UpdateWalletActiveReq: properties: - isActive: + is_active: + example: true type: boolean + required: + - is_active type: object handlers.UserProfileRes: properties: @@ -781,6 +875,8 @@ definitions: type: string id: type: integer + last_login: + type: string last_name: type: string phone_number: @@ -824,6 +920,30 @@ definitions: example: 1 type: integer type: object + handlers.launchVirtualGameReq: + properties: + currency: + example: USD + type: string + game_id: + example: crash_001 + type: string + mode: + enum: + - REAL + - DEMO + example: REAL + type: string + required: + - currency + - game_id + - mode + type: object + handlers.launchVirtualGameRes: + properties: + launch_url: + type: string + type: object handlers.loginCustomerReq: properties: email: @@ -835,6 +955,8 @@ definitions: phone_number: example: "1234567890" type: string + required: + - password type: object handlers.loginCustomerRes: properties: @@ -848,14 +970,22 @@ definitions: handlers.logoutReq: properties: refresh_token: + example: type: string + required: + - refresh_token type: object handlers.refreshToken: properties: access_token: + example: type: string refresh_token: + example: type: string + required: + - access_token + - refresh_token type: object handlers.updateUserReq: properties: @@ -905,6 +1035,75 @@ info: title: FortuneBet API version: "1.0" paths: + /admin: + get: + consumes: + - application/json + description: Get all Admins + parameters: + - description: Page number + in: query + name: page + type: integer + - description: Page size + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Get all Admins + tags: + - admin + post: + consumes: + - application/json + description: Create Admin + parameters: + - description: Create admin + in: body + name: manger + required: true + schema: + $ref: '#/definitions/handlers.CreateAdminReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Create Admin + tags: + - admin /auth/login: post: consumes: @@ -1516,6 +1715,11 @@ paths: - application/json description: Update cashier parameters: + - description: Cashier ID + in: path + name: id + required: true + type: integer - description: Update cashier in: body name: cashier @@ -1790,7 +1994,7 @@ paths: post: consumes: - application/json - description: Create Managers + description: Create Manager parameters: - description: Create manager in: body @@ -1817,7 +2021,7 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/response.APIResponse' - summary: Create Managers + summary: Create Manager tags: - manager /managers/{id}: @@ -2079,6 +2283,95 @@ paths: summary: Retrieve raw odds by Market ID tags: - prematch + /referral/settings: + get: + consumes: + - application/json + description: Retrieves current referral settings (admin only) + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.ReferralSettings' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - Bearer: [] + summary: Get referral settings + tags: + - referral + put: + consumes: + - application/json + description: Updates referral settings (admin only) + parameters: + - description: Referral settings + in: body + name: settings + required: true + schema: + $ref: '#/definitions/domain.ReferralSettings' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - Bearer: [] + summary: Update referral settings + tags: + - referral + /referral/stats: + get: + consumes: + - application/json + description: Retrieves referral statistics for the authenticated user + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.ReferralStats' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - Bearer: [] + summary: Get referral statistics + tags: + - referral /search/branch: get: consumes: @@ -2110,6 +2403,31 @@ paths: summary: Search branches tags: - branch + /search/company: + get: + consumes: + - application/json + description: Gets all companies + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/handlers.CompanyRes' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets all companies + tags: + - company /supportedOperation: get: consumes: @@ -2333,7 +2651,7 @@ paths: patch: consumes: - application/json - description: Updates the cashed out field + description: Updates the verified status of a transaction parameters: - description: Transaction ID in: path @@ -2342,7 +2660,7 @@ paths: type: integer - description: Updates Transaction Verification in: body - name: updateCashOut + name: updateVerified required: true schema: $ref: '#/definitions/handlers.UpdateTransactionVerifiedReq' @@ -2361,7 +2679,7 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/response.APIResponse' - summary: Updates the cashed out field + summary: Updates the verified field of a transaction tags: - transaction /transfer/refill/:id: @@ -2659,6 +2977,39 @@ paths: summary: Send reset code tags: - user + /user/single/{id}: + get: + consumes: + - application/json + description: Get a single user by id + parameters: + - description: User 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' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Get user by id + tags: + - user /user/wallet: get: consumes: @@ -2690,6 +3041,72 @@ paths: summary: Get customer wallet tags: - wallet + /virtual-game/callback: + post: + consumes: + - application/json + description: Processes callbacks from PopOK for game events + parameters: + - description: Callback data + in: body + name: callback + required: true + schema: + $ref: '#/definitions/domain.PopOKCallback' + 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: Handle PopOK game callback + tags: + - virtual-game + /virtual-game/launch: + post: + consumes: + - application/json + description: Generates a URL to launch a PopOK game + parameters: + - description: Game launch details + in: body + name: launch + required: true + schema: + $ref: '#/definitions/handlers.launchVirtualGameReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.launchVirtualGameRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - Bearer: [] + summary: Launch a PopOK virtual game + tags: + - virtual-game /wallet: get: consumes: diff --git a/gen/db/auth.sql.go b/gen/db/auth.sql.go index 3b5854c..527f25c 100644 --- a/gen/db/auth.sql.go +++ b/gen/db/auth.sql.go @@ -36,7 +36,8 @@ func (q *Queries) CreateRefreshToken(ctx context.Context, arg CreateRefreshToken } const GetRefreshToken = `-- name: GetRefreshToken :one -SELECT id, user_id, token, expires_at, created_at, revoked FROM refresh_tokens +SELECT id, user_id, token, expires_at, created_at, revoked +FROM refresh_tokens WHERE token = $1 ` @@ -54,9 +55,31 @@ func (q *Queries) GetRefreshToken(ctx context.Context, token string) (RefreshTok return i, err } +const GetRefreshTokenByUserID = `-- name: GetRefreshTokenByUserID :one +SELECT id, user_id, token, expires_at, created_at, revoked +FROM refresh_tokens +WHERE user_id = $1 +` + +func (q *Queries) GetRefreshTokenByUserID(ctx context.Context, userID int64) (RefreshToken, error) { + row := q.db.QueryRow(ctx, GetRefreshTokenByUserID, userID) + var i RefreshToken + err := row.Scan( + &i.ID, + &i.UserID, + &i.Token, + &i.ExpiresAt, + &i.CreatedAt, + &i.Revoked, + ) + return i, err +} + const GetUserByEmailPhone = `-- name: GetUserByEmailPhone :one -SELECT id, first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at, suspended_at, suspended, referral_code, referred_by FROM users -WHERE email = $1 OR phone_number = $2 +SELECT id, first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at, company_id, suspended_at, suspended, referral_code, referred_by +FROM users +WHERE email = $1 + OR phone_number = $2 ` type GetUserByEmailPhoneParams struct { @@ -79,6 +102,7 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho &i.PhoneVerified, &i.CreatedAt, &i.UpdatedAt, + &i.CompanyID, &i.SuspendedAt, &i.Suspended, &i.ReferralCode, @@ -88,8 +112,8 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho } const RevokeRefreshToken = `-- name: RevokeRefreshToken :exec -UPDATE refresh_tokens -SET revoked = TRUE +UPDATE refresh_tokens +SET revoked = TRUE WHERE token = $1 ` diff --git a/gen/db/branch.sql.go b/gen/db/branch.sql.go index 7870509..439d76f 100644 --- a/gen/db/branch.sql.go +++ b/gen/db/branch.sql.go @@ -191,7 +191,7 @@ func (q *Queries) GetAllBranches(ctx context.Context) ([]BranchDetail, error) { } const GetAllCashiers = `-- name: GetAllCashiers :many -SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.suspended_at, users.suspended, users.referral_code, users.referred_by +SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended, users.referral_code, users.referred_by FROM branch_cashiers JOIN users ON branch_cashiers.user_id = users.id ` @@ -217,6 +217,7 @@ func (q *Queries) GetAllCashiers(ctx context.Context) ([]User, error) { &i.PhoneVerified, &i.CreatedAt, &i.UpdatedAt, + &i.CompanyID, &i.SuspendedAt, &i.Suspended, &i.ReferralCode, @@ -430,7 +431,7 @@ func (q *Queries) GetBranchOperations(ctx context.Context, branchID int64) ([]Ge } const GetCashiersByBranch = `-- name: GetCashiersByBranch :many -SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.suspended_at, users.suspended, users.referral_code, users.referred_by +SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended, users.referral_code, users.referred_by FROM branch_cashiers JOIN users ON branch_cashiers.user_id = users.id WHERE branch_cashiers.branch_id = $1 @@ -457,6 +458,7 @@ func (q *Queries) GetCashiersByBranch(ctx context.Context, branchID int64) ([]Us &i.PhoneVerified, &i.CreatedAt, &i.UpdatedAt, + &i.CompanyID, &i.SuspendedAt, &i.Suspended, &i.ReferralCode, diff --git a/gen/db/company.sql.go b/gen/db/company.sql.go index fb83066..d7f10e6 100644 --- a/gen/db/company.sql.go +++ b/gen/db/company.sql.go @@ -7,6 +7,8 @@ package dbgen import ( "context" + + "github.com/jackc/pgx/v5/pgtype" ) const CreateCompany = `-- name: CreateCompany :one @@ -95,6 +97,37 @@ func (q *Queries) GetCompanyByID(ctx context.Context, id int64) (Company, error) return i, err } +const SearchCompanyByName = `-- name: SearchCompanyByName :many +SELECT id, name, admin_id, wallet_id +FROM companies +WHERE name ILIKE '%' || $1 || '%' +` + +func (q *Queries) SearchCompanyByName(ctx context.Context, dollar_1 pgtype.Text) ([]Company, error) { + rows, err := q.db.Query(ctx, SearchCompanyByName, dollar_1) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Company + for rows.Next() { + var i Company + if err := rows.Scan( + &i.ID, + &i.Name, + &i.AdminID, + &i.WalletID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const UpdateCompany = `-- name: UpdateCompany :one UPDATE companies SET name = $1, diff --git a/gen/db/models.go b/gen/db/models.go index 0d2bea0..987b1ae 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -310,23 +310,24 @@ type TicketWithOutcome struct { } type Transaction struct { - ID int64 `json:"id"` - Amount int64 `json:"amount"` - BranchID int64 `json:"branch_id"` - CashierID int64 `json:"cashier_id"` - BetID int64 `json:"bet_id"` - Type int64 `json:"type"` - PaymentOption int64 `json:"payment_option"` - FullName string `json:"full_name"` - PhoneNumber string `json:"phone_number"` - BankCode string `json:"bank_code"` - BeneficiaryName string `json:"beneficiary_name"` - AccountName string `json:"account_name"` - AccountNumber string `json:"account_number"` - ReferenceNumber string `json:"reference_number"` - Verified bool `json:"verified"` - CreatedAt pgtype.Timestamp `json:"created_at"` - UpdatedAt pgtype.Timestamp `json:"updated_at"` + ID int64 `json:"id"` + Amount int64 `json:"amount"` + BranchID int64 `json:"branch_id"` + CashierID int64 `json:"cashier_id"` + BetID int64 `json:"bet_id"` + NumberOfOutcomes int64 `json:"number_of_outcomes"` + Type int64 `json:"type"` + PaymentOption int64 `json:"payment_option"` + FullName string `json:"full_name"` + PhoneNumber string `json:"phone_number"` + BankCode string `json:"bank_code"` + BeneficiaryName string `json:"beneficiary_name"` + AccountName string `json:"account_name"` + AccountNumber string `json:"account_number"` + ReferenceNumber string `json:"reference_number"` + Verified bool `json:"verified"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` } type User struct { @@ -341,6 +342,7 @@ type User struct { PhoneVerified bool `json:"phone_verified"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` + CompanyID pgtype.Int8 `json:"company_id"` SuspendedAt pgtype.Timestamptz `json:"suspended_at"` Suspended bool `json:"suspended"` ReferralCode pgtype.Text `json:"referral_code"` diff --git a/gen/db/transactions.sql.go b/gen/db/transactions.sql.go index 2865972..b4aec9d 100644 --- a/gen/db/transactions.sql.go +++ b/gen/db/transactions.sql.go @@ -10,7 +10,7 @@ import ( ) const CreateTransaction = `-- name: CreateTransaction :one -INSERT INTO transactions (amount, branch_id, cashier_id, bet_id, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id, amount, branch_id, cashier_id, bet_id, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, created_at, updated_at +INSERT INTO transactions (amount, branch_id, cashier_id, bet_id, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id, amount, branch_id, cashier_id, bet_id, number_of_outcomes, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, created_at, updated_at ` type CreateTransactionParams struct { @@ -52,6 +52,7 @@ func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionPa &i.BranchID, &i.CashierID, &i.BetID, + &i.NumberOfOutcomes, &i.Type, &i.PaymentOption, &i.FullName, @@ -69,7 +70,7 @@ func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionPa } const GetAllTransactions = `-- name: GetAllTransactions :many -SELECT id, amount, branch_id, cashier_id, bet_id, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, created_at, updated_at FROM transactions +SELECT id, amount, branch_id, cashier_id, bet_id, number_of_outcomes, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, created_at, updated_at FROM transactions ` func (q *Queries) GetAllTransactions(ctx context.Context) ([]Transaction, error) { @@ -87,6 +88,7 @@ func (q *Queries) GetAllTransactions(ctx context.Context) ([]Transaction, error) &i.BranchID, &i.CashierID, &i.BetID, + &i.NumberOfOutcomes, &i.Type, &i.PaymentOption, &i.FullName, @@ -111,7 +113,7 @@ func (q *Queries) GetAllTransactions(ctx context.Context) ([]Transaction, error) } const GetTransactionByBranch = `-- name: GetTransactionByBranch :many -SELECT id, amount, branch_id, cashier_id, bet_id, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, created_at, updated_at FROM transactions WHERE branch_id = $1 +SELECT id, amount, branch_id, cashier_id, bet_id, number_of_outcomes, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, created_at, updated_at FROM transactions WHERE branch_id = $1 ` func (q *Queries) GetTransactionByBranch(ctx context.Context, branchID int64) ([]Transaction, error) { @@ -129,6 +131,7 @@ func (q *Queries) GetTransactionByBranch(ctx context.Context, branchID int64) ([ &i.BranchID, &i.CashierID, &i.BetID, + &i.NumberOfOutcomes, &i.Type, &i.PaymentOption, &i.FullName, @@ -153,7 +156,7 @@ func (q *Queries) GetTransactionByBranch(ctx context.Context, branchID int64) ([ } const GetTransactionByID = `-- name: GetTransactionByID :one -SELECT id, amount, branch_id, cashier_id, bet_id, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, created_at, updated_at FROM transactions WHERE id = $1 +SELECT id, amount, branch_id, cashier_id, bet_id, number_of_outcomes, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, created_at, updated_at FROM transactions WHERE id = $1 ` func (q *Queries) GetTransactionByID(ctx context.Context, id int64) (Transaction, error) { @@ -165,6 +168,7 @@ func (q *Queries) GetTransactionByID(ctx context.Context, id int64) (Transaction &i.BranchID, &i.CashierID, &i.BetID, + &i.NumberOfOutcomes, &i.Type, &i.PaymentOption, &i.FullName, diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index 75dbd5e..7a6fcb8 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -54,9 +54,24 @@ INSERT INTO users ( email_verified, phone_verified, created_at, - updated_at + updated_at, + suspended, + company_id + ) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12 ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, first_name, last_name, @@ -66,7 +81,9 @@ RETURNING id, email_verified, phone_verified, created_at, - updated_at + updated_at, + suspended, + company_id ` type CreateUserParams struct { @@ -80,6 +97,8 @@ type CreateUserParams struct { PhoneVerified bool `json:"phone_verified"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` + Suspended bool `json:"suspended"` + CompanyID pgtype.Int8 `json:"company_id"` } type CreateUserRow struct { @@ -93,6 +112,8 @@ type CreateUserRow struct { PhoneVerified bool `json:"phone_verified"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` + Suspended bool `json:"suspended"` + CompanyID pgtype.Int8 `json:"company_id"` } func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) { @@ -107,6 +128,8 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateU arg.PhoneVerified, arg.CreatedAt, arg.UpdatedAt, + arg.Suspended, + arg.CompanyID, ) var i CreateUserRow err := row.Scan( @@ -120,6 +143,8 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateU &i.PhoneVerified, &i.CreatedAt, &i.UpdatedAt, + &i.Suspended, + &i.CompanyID, ) return i, err } @@ -144,10 +169,29 @@ SELECT id, email_verified, phone_verified, created_at, - updated_at + updated_at, + suspended, + suspended_at, + company_id FROM users +wHERE ( + role = $1 + OR $1 IS NULL + ) + AND ( + company_id = $2 + OR $2 IS NULL + ) +LIMIT $3 OFFSET $4 ` +type GetAllUsersParams struct { + Role string `json:"role"` + CompanyID pgtype.Int8 `json:"company_id"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + type GetAllUsersRow struct { ID int64 `json:"id"` FirstName string `json:"first_name"` @@ -159,10 +203,18 @@ type GetAllUsersRow struct { PhoneVerified bool `json:"phone_verified"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` + Suspended bool `json:"suspended"` + SuspendedAt pgtype.Timestamptz `json:"suspended_at"` + CompanyID pgtype.Int8 `json:"company_id"` } -func (q *Queries) GetAllUsers(ctx context.Context) ([]GetAllUsersRow, error) { - rows, err := q.db.Query(ctx, GetAllUsers) +func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]GetAllUsersRow, error) { + rows, err := q.db.Query(ctx, GetAllUsers, + arg.Role, + arg.CompanyID, + arg.Limit, + arg.Offset, + ) if err != nil { return nil, err } @@ -181,6 +233,9 @@ func (q *Queries) GetAllUsers(ctx context.Context) ([]GetAllUsersRow, error) { &i.PhoneVerified, &i.CreatedAt, &i.UpdatedAt, + &i.Suspended, + &i.SuspendedAt, + &i.CompanyID, ); err != nil { return nil, err } @@ -192,6 +247,31 @@ func (q *Queries) GetAllUsers(ctx context.Context) ([]GetAllUsersRow, error) { return items, nil } +const GetTotalUsers = `-- name: GetTotalUsers :one +SELECT COUNT(*) +FROM users +wHERE ( + role = $1 + OR $1 IS NULL + ) + AND ( + company_id = $2 + OR $2 IS NULL + ) +` + +type GetTotalUsersParams struct { + Role string `json:"role"` + CompanyID pgtype.Int8 `json:"company_id"` +} + +func (q *Queries) GetTotalUsers(ctx context.Context, arg GetTotalUsersParams) (int64, error) { + row := q.db.QueryRow(ctx, GetTotalUsers, arg.Role, arg.CompanyID) + var count int64 + err := row.Scan(&count) + return count, err +} + const GetUserByEmail = `-- name: GetUserByEmail :one SELECT id, first_name, @@ -202,7 +282,10 @@ SELECT id, email_verified, phone_verified, created_at, - updated_at + updated_at, + suspended, + suspended_at, + company_id FROM users WHERE email = $1 ` @@ -218,6 +301,9 @@ type GetUserByEmailRow struct { PhoneVerified bool `json:"phone_verified"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` + Suspended bool `json:"suspended"` + SuspendedAt pgtype.Timestamptz `json:"suspended_at"` + CompanyID pgtype.Int8 `json:"company_id"` } func (q *Queries) GetUserByEmail(ctx context.Context, email pgtype.Text) (GetUserByEmailRow, error) { @@ -234,12 +320,15 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email pgtype.Text) (GetUse &i.PhoneVerified, &i.CreatedAt, &i.UpdatedAt, + &i.Suspended, + &i.SuspendedAt, + &i.CompanyID, ) return i, err } const GetUserByID = `-- name: GetUserByID :one -SELECT id, first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at, suspended_at, suspended, referral_code, referred_by +SELECT id, first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at, company_id, suspended_at, suspended, referral_code, referred_by FROM users WHERE id = $1 ` @@ -259,6 +348,7 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) { &i.PhoneVerified, &i.CreatedAt, &i.UpdatedAt, + &i.CompanyID, &i.SuspendedAt, &i.Suspended, &i.ReferralCode, @@ -277,7 +367,10 @@ SELECT id, email_verified, phone_verified, created_at, - updated_at + updated_at, + suspended, + suspended_at, + company_id FROM users WHERE phone_number = $1 ` @@ -293,6 +386,9 @@ type GetUserByPhoneRow struct { PhoneVerified bool `json:"phone_verified"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` + Suspended bool `json:"suspended"` + SuspendedAt pgtype.Timestamptz `json:"suspended_at"` + CompanyID pgtype.Int8 `json:"company_id"` } func (q *Queries) GetUserByPhone(ctx context.Context, phoneNumber pgtype.Text) (GetUserByPhoneRow, error) { @@ -309,6 +405,9 @@ func (q *Queries) GetUserByPhone(ctx context.Context, phoneNumber pgtype.Text) ( &i.PhoneVerified, &i.CreatedAt, &i.UpdatedAt, + &i.Suspended, + &i.SuspendedAt, + &i.CompanyID, ) return i, err } @@ -323,7 +422,10 @@ SELECT id, email_verified, phone_verified, created_at, - updated_at + updated_at, + suspended, + suspended_at, + company_id FROM users WHERE first_name ILIKE '%' || $1 || '%' OR last_name ILIKE '%' || $1 || '%' @@ -341,6 +443,9 @@ type SearchUserByNameOrPhoneRow struct { PhoneVerified bool `json:"phone_verified"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` + Suspended bool `json:"suspended"` + SuspendedAt pgtype.Timestamptz `json:"suspended_at"` + CompanyID pgtype.Int8 `json:"company_id"` } func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, dollar_1 pgtype.Text) ([]SearchUserByNameOrPhoneRow, error) { @@ -363,6 +468,9 @@ func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, dollar_1 pgtype.T &i.PhoneVerified, &i.CreatedAt, &i.UpdatedAt, + &i.Suspended, + &i.SuspendedAt, + &i.CompanyID, ); err != nil { return nil, err } @@ -374,6 +482,25 @@ func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, dollar_1 pgtype.T return items, nil } +const SuspendUser = `-- name: SuspendUser :exec +UPDATE users +SET suspended = $1, + suspended_at = $2, + updated_at = CURRENT_TIMESTAMP +WHERE id = $3 +` + +type SuspendUserParams struct { + Suspended bool `json:"suspended"` + SuspendedAt pgtype.Timestamptz `json:"suspended_at"` + ID int64 `json:"id"` +} + +func (q *Queries) SuspendUser(ctx context.Context, arg SuspendUserParams) error { + _, err := q.db.Exec(ctx, SuspendUser, arg.Suspended, arg.SuspendedAt, arg.ID) + return err +} + const UpdatePassword = `-- name: UpdatePassword :exec UPDATE users SET password = $1, diff --git a/go.mod b/go.mod index 2c2e549..ee63ab3 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/andybalholm/brotli v1.1.1 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cloudwego/base64x v0.1.5 // indirect - github.com/fasthttp/websocket v1.5.3 // indirect + github.com/fasthttp/websocket v1.5.8 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect @@ -31,6 +31,7 @@ require ( github.com/go-openapi/swag v0.23.1 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/gofiber/contrib/websocket v1.3.4 github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect @@ -44,7 +45,7 @@ require ( github.com/mattn/go-runewidth v0.0.16 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect + github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 // indirect github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect diff --git a/go.sum b/go.sum index 67b5c0c..9e77972 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQtdek= github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs= +github.com/fasthttp/websocket v1.5.8 h1:k5DpirKkftIF/w1R8ZzjSgARJrs54Je9YJK37DL/Ah8= +github.com/fasthttp/websocket v1.5.8/go.mod h1:d08g8WaT6nnyvg9uMm8K9zMYyDjfKyj3170AtPRuVU0= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -49,6 +51,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/gofiber/contrib/websocket v1.3.4 h1:tWeBdbJ8q0WFQXariLN4dBIbGH9KBU75s0s7YXplOSg= +github.com/gofiber/contrib/websocket v1.3.4/go.mod h1:kTFBPC6YENCnKfKx0BoOFjgXxdz7E85/STdkmZPEmPs= github.com/gofiber/fiber/v2 v2.32.0/go.mod h1:CMy5ZLiXkn6qwthrl03YMyW1NLfj0rhxz2LKl4t7ZTY= github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= @@ -116,6 +120,8 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= +github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 h1:KanIMPX0QdEdB4R3CiimCAbxFrhB3j7h0/OvpYGVQa8= +github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= diff --git a/internal/domain/transaction.go b/internal/domain/transaction.go index f47c34a..8abd19b 100644 --- a/internal/domain/transaction.go +++ b/internal/domain/transaction.go @@ -19,15 +19,16 @@ const ( // Transaction only represents when the user cashes out a bet in the shop // It probably would be better to call it a CashOut or ShopWithdrawal type Transaction struct { - ID int64 - Amount Currency - BranchID int64 - CashierID int64 - BetID int64 - Type TransactionType - PaymentOption PaymentOption - FullName string - PhoneNumber string + ID int64 + Amount Currency + BranchID int64 + CashierID int64 + BetID int64 + NumberOfOutcomes int64 + Type TransactionType + PaymentOption PaymentOption + FullName string + PhoneNumber string // Payment Details for bank BankCode string BeneficiaryName string @@ -38,14 +39,15 @@ type Transaction struct { } type CreateTransaction struct { - Amount Currency - BranchID int64 - CashierID int64 - BetID int64 - Type TransactionType - PaymentOption PaymentOption - FullName string - PhoneNumber string + Amount Currency + BranchID int64 + CashierID int64 + BetID int64 + NumberOfOutcomes int64 + Type TransactionType + PaymentOption PaymentOption + FullName string + PhoneNumber string // Payment Details for bank BankCode string BeneficiaryName string diff --git a/internal/domain/user.go b/internal/domain/user.go index c215c5e..1cd27d6 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -27,15 +27,15 @@ type User struct { SuspendedAt time.Time Suspended bool // - BranchID int64 + CompanyID ValidInt64 } type RegisterUserReq struct { - FirstName string - LastName string - Email string - PhoneNumber string - Password string - Role string + FirstName string + LastName string + Email string + PhoneNumber string + Password string + Role string Otp string ReferralCode string `json:"referral_code"` OtpMedium OtpMedium @@ -47,6 +47,8 @@ type CreateUserReq struct { PhoneNumber string Password string Role string + Suspended bool + CompanyID ValidInt64 } type ResetPasswordReq struct { Email string diff --git a/internal/repository/auth.go b/internal/repository/auth.go index 99739e9..3f43d9e 100644 --- a/internal/repository/auth.go +++ b/internal/repository/auth.go @@ -29,13 +29,23 @@ func (s *Store) GetUserByEmailPhone(ctx context.Context, email, phone string) (d return domain.User{}, err } return domain.User{ - ID: user.ID, - FirstName: user.FirstName, - LastName: user.LastName, - Email: user.Email.String, - PhoneNumber: user.PhoneNumber.String, - Password: user.Password, - Role: domain.Role(user.Role), + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email.String, + PhoneNumber: user.PhoneNumber.String, + Password: user.Password, + Role: domain.Role(user.Role), + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + CreatedAt: user.CreatedAt.Time, + UpdatedAt: user.UpdatedAt.Time, + SuspendedAt: user.SuspendedAt.Time, + Suspended: user.Suspended, + CompanyID: domain.ValidInt64{ + Value: user.CompanyID.Int64, + Valid: user.CompanyID.Valid, + }, }, nil } @@ -72,6 +82,25 @@ func (s *Store) GetRefreshToken(ctx context.Context, token string) (domain.Refre Revoked: rf.Revoked, }, nil } + +func (s *Store) GetRefreshTokenByUserID(ctx context.Context, id int64) (domain.RefreshToken, error) { + rf, err := s.queries.GetRefreshTokenByUserID(ctx, id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return domain.RefreshToken{}, authentication.ErrRefreshTokenNotFound + } + return domain.RefreshToken{}, err + } + + return domain.RefreshToken{ + Token: rf.Token, + UserID: rf.UserID, + CreatedAt: rf.CreatedAt.Time, + ExpiresAt: rf.ExpiresAt.Time, + Revoked: rf.Revoked, + }, nil +} + func (s *Store) RevokeRefreshToken(ctx context.Context, token string) error { return s.queries.RevokeRefreshToken(ctx, token) } diff --git a/internal/repository/company.go b/internal/repository/company.go index 0e52cb6..d05a6f0 100644 --- a/internal/repository/company.go +++ b/internal/repository/company.go @@ -5,6 +5,7 @@ import ( dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/jackc/pgx/v5/pgtype" ) func convertCreateCompany(company domain.CreateCompany) dbgen.CreateCompanyParams { @@ -46,6 +47,23 @@ func (s *Store) GetAllCompanies(ctx context.Context) ([]domain.Company, error) { return companies, nil } +func (s *Store) SearchCompanyByName(ctx context.Context, name string) ([]domain.Company, error) { + dbCompanies, err := s.queries.SearchCompanyByName(ctx, pgtype.Text{ + String: name, + Valid: true, + }) + + if err != nil { + return nil, err + } + var companies []domain.Company = make([]domain.Company, 0, len(dbCompanies)) + + for _, dbCompany := range dbCompanies { + companies = append(companies, convertDBCompany(dbCompany)) + } + return companies, nil +} + func (s *Store) GetCompanyByID(ctx context.Context, id int64) (domain.Company, error) { dbCompany, err := s.queries.GetCompanyByID(ctx, id) diff --git a/internal/repository/transaction.go b/internal/repository/transaction.go index 24cf9e0..3526d74 100644 --- a/internal/repository/transaction.go +++ b/internal/repository/transaction.go @@ -9,19 +9,20 @@ import ( func convertDBTransaction(transaction dbgen.Transaction) domain.Transaction { return domain.Transaction{ - Amount: domain.Currency(transaction.Amount), - BranchID: transaction.BranchID, - CashierID: transaction.CashierID, - BetID: transaction.BetID, - Type: domain.TransactionType(transaction.Type), - PaymentOption: domain.PaymentOption(transaction.PaymentOption), - FullName: transaction.FullName, - PhoneNumber: transaction.PhoneNumber, - BankCode: transaction.BankCode, - BeneficiaryName: transaction.BeneficiaryName, - AccountName: transaction.AccountName, - AccountNumber: transaction.AccountNumber, - ReferenceNumber: transaction.ReferenceNumber, + Amount: domain.Currency(transaction.Amount), + BranchID: transaction.BranchID, + CashierID: transaction.CashierID, + BetID: transaction.BetID, + NumberOfOutcomes: transaction.NumberOfOutcomes, + Type: domain.TransactionType(transaction.Type), + PaymentOption: domain.PaymentOption(transaction.PaymentOption), + FullName: transaction.FullName, + PhoneNumber: transaction.PhoneNumber, + BankCode: transaction.BankCode, + BeneficiaryName: transaction.BeneficiaryName, + AccountName: transaction.AccountName, + AccountNumber: transaction.AccountNumber, + ReferenceNumber: transaction.ReferenceNumber, } } diff --git a/internal/repository/user.go b/internal/repository/user.go index 1b20834..dc30506 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -12,7 +12,7 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -func (s *Store) CreateUser(ctx context.Context, user domain.User, usedOtpId int64) (domain.User, error) { +func (s *Store) CreateUser(ctx context.Context, user domain.User, usedOtpId int64, is_company bool) (domain.User, error) { err := s.queries.MarkOtpAsUsed(ctx, dbgen.MarkOtpAsUsedParams{ ID: usedOtpId, UsedAt: pgtype.Timestamptz{ @@ -83,23 +83,44 @@ func (s *Store) GetUserByID(ctx context.Context, id int64) (domain.User, error) Suspended: user.Suspended, }, nil } -func (s *Store) GetAllUsers(ctx context.Context, filter user.Filter) ([]domain.User, error) { - users, err := s.queries.GetAllUsers(ctx) +func (s *Store) GetAllUsers(ctx context.Context, filter user.Filter) ([]domain.User, int64, error) { + users, err := s.queries.GetAllUsers(ctx, dbgen.GetAllUsersParams{ + Role: filter.Role, + CompanyID: pgtype.Int8{ + Int64: filter.CompanyID.Value, + Valid: filter.CompanyID.Valid, + }, + Limit: int32(filter.PageSize), + Offset: int32(filter.Page), + }) if err != nil { - return nil, err + return nil, 0, err } userList := make([]domain.User, len(users)) for i, user := range users { userList[i] = domain.User{ - ID: user.ID, - FirstName: user.FirstName, - LastName: user.LastName, - Email: user.Email.String, - PhoneNumber: user.PhoneNumber.String, - Role: domain.Role(user.Role), + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email.String, + EmailVerified: user.EmailVerified, + PhoneNumber: user.PhoneNumber.String, + Role: domain.Role(user.Role), + PhoneVerified: user.PhoneVerified, + CreatedAt: user.CreatedAt.Time, + UpdatedAt: user.UpdatedAt.Time, + SuspendedAt: user.SuspendedAt.Time, + Suspended: user.Suspended, } } - return userList, nil + totalCount, err := s.queries.GetTotalUsers(ctx, dbgen.GetTotalUsersParams{ + Role: filter.Role, + CompanyID: pgtype.Int8{ + Int64: filter.CompanyID.Value, + Valid: filter.CompanyID.Valid, + }, + }) + return userList, totalCount, nil } func (s *Store) GetAllCashiers(ctx context.Context) ([]domain.User, error) { @@ -110,12 +131,18 @@ func (s *Store) GetAllCashiers(ctx context.Context) ([]domain.User, error) { userList := make([]domain.User, len(users)) for i, user := range users { userList[i] = domain.User{ - ID: user.ID, - FirstName: user.FirstName, - LastName: user.LastName, - Email: user.Email.String, - PhoneNumber: user.PhoneNumber.String, - Role: domain.Role(user.Role), + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email.String, + PhoneNumber: user.PhoneNumber.String, + Role: domain.Role(user.Role), + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + CreatedAt: user.CreatedAt.Time, + UpdatedAt: user.UpdatedAt.Time, + SuspendedAt: user.SuspendedAt.Time, + Suspended: user.Suspended, } } return userList, nil @@ -129,12 +156,18 @@ func (s *Store) GetCashiersByBranch(ctx context.Context, branchID int64) ([]doma userList := make([]domain.User, len(users)) for i, user := range users { userList[i] = domain.User{ - ID: user.ID, - FirstName: user.FirstName, - LastName: user.LastName, - Email: user.Email.String, - PhoneNumber: user.PhoneNumber.String, - Role: domain.Role(user.Role), + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email.String, + PhoneNumber: user.PhoneNumber.String, + Role: domain.Role(user.Role), + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + CreatedAt: user.CreatedAt.Time, + UpdatedAt: user.UpdatedAt.Time, + SuspendedAt: user.SuspendedAt.Time, + Suspended: user.Suspended, } } return userList, nil @@ -152,12 +185,18 @@ func (s *Store) SearchUserByNameOrPhone(ctx context.Context, searchString string userList := make([]domain.User, 0, len(users)) for _, user := range users { userList = append(userList, domain.User{ - ID: user.ID, - FirstName: user.FirstName, - LastName: user.LastName, - Email: user.Email.String, - PhoneNumber: user.PhoneNumber.String, - Role: domain.Role(user.Role), + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email.String, + PhoneNumber: user.PhoneNumber.String, + Role: domain.Role(user.Role), + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + CreatedAt: user.CreatedAt.Time, + UpdatedAt: user.UpdatedAt.Time, + Suspended: user.Suspended, + SuspendedAt: user.SuspendedAt.Time, }) } return userList, nil @@ -216,12 +255,18 @@ func (s *Store) GetUserByEmail(ctx context.Context, email string) (domain.User, return domain.User{}, err } return domain.User{ - ID: user.ID, - FirstName: user.FirstName, - LastName: user.LastName, - Email: user.Email.String, - PhoneNumber: user.PhoneNumber.String, - Role: domain.Role(user.Role), + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email.String, + PhoneNumber: user.PhoneNumber.String, + Role: domain.Role(user.Role), + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + CreatedAt: user.CreatedAt.Time, + UpdatedAt: user.UpdatedAt.Time, + Suspended: user.Suspended, + SuspendedAt: user.SuspendedAt.Time, }, nil } func (s *Store) GetUserByPhone(ctx context.Context, phoneNum string) (domain.User, error) { @@ -235,13 +280,20 @@ func (s *Store) GetUserByPhone(ctx context.Context, phoneNum string) (domain.Use } return domain.User{}, err } + return domain.User{ - ID: user.ID, - FirstName: user.FirstName, - LastName: user.LastName, - Email: user.Email.String, - PhoneNumber: user.PhoneNumber.String, - Role: domain.Role(user.Role), + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email.String, + PhoneNumber: user.PhoneNumber.String, + Role: domain.Role(user.Role), + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + CreatedAt: user.CreatedAt.Time, + UpdatedAt: user.UpdatedAt.Time, + Suspended: user.Suspended, + SuspendedAt: user.SuspendedAt.Time, }, nil } @@ -272,7 +324,7 @@ func (s *Store) UpdatePassword(ctx context.Context, identifier string, password } return nil } -func (s *Store) CreateUserWithoutOtp(ctx context.Context, user domain.User) (domain.User, error) { +func (s *Store) CreateUserWithoutOtp(ctx context.Context, user domain.User, is_company bool) (domain.User, error) { userRes, err := s.queries.CreateUser(ctx, dbgen.CreateUserParams{ FirstName: user.FirstName, LastName: user.LastName, @@ -296,16 +348,26 @@ func (s *Store) CreateUserWithoutOtp(ctx context.Context, user domain.User) (dom Time: time.Now(), Valid: true, }, + Suspended: user.Suspended, + CompanyID: pgtype.Int8{ + Int64: user.CompanyID.Value, + Valid: user.CompanyID.Valid, + }, }) if err != nil { return domain.User{}, err } return domain.User{ - ID: userRes.ID, - FirstName: userRes.FirstName, - LastName: userRes.LastName, - Email: userRes.Email.String, - PhoneNumber: userRes.PhoneNumber.String, - Role: domain.Role(userRes.Role), + ID: userRes.ID, + FirstName: userRes.FirstName, + LastName: userRes.LastName, + Email: userRes.Email.String, + PhoneNumber: userRes.PhoneNumber.String, + Role: domain.Role(userRes.Role), + EmailVerified: userRes.EmailVerified, + PhoneVerified: userRes.PhoneVerified, + CreatedAt: userRes.CreatedAt.Time, + UpdatedAt: userRes.UpdatedAt.Time, + Suspended: userRes.Suspended, }, nil } diff --git a/internal/services/authentication/impl.go b/internal/services/authentication/impl.go index 2d6bb0b..7540047 100644 --- a/internal/services/authentication/impl.go +++ b/internal/services/authentication/impl.go @@ -16,13 +16,14 @@ var ( ErrUserNotFound = errors.New("user not found") ErrExpiredToken = errors.New("token expired") ErrRefreshTokenNotFound = errors.New("refresh token not found") // i.e login again + ErrUserSuspended = errors.New("user has been suspended") ) type LoginSuccess struct { - UserId int64 - Role domain.Role - RfToken string - BranchId int64 + UserId int64 + Role domain.Role + RfToken string + CompanyID domain.ValidInt64 } func (s *Service) Login(ctx context.Context, email, phone string, password string) (LoginSuccess, error) { @@ -34,6 +35,9 @@ func (s *Service) Login(ctx context.Context, email, phone string, password strin if err != nil { return LoginSuccess{}, err } + if user.Suspended { + return LoginSuccess{}, ErrUserSuspended + } refreshToken, err := generateRefreshToken() if err != nil { @@ -49,24 +53,24 @@ func (s *Service) Login(ctx context.Context, email, phone string, password strin return LoginSuccess{}, err } return LoginSuccess{ - UserId: user.ID, - Role: user.Role, - RfToken: refreshToken, - BranchId: user.BranchID, + UserId: user.ID, + Role: user.Role, + RfToken: refreshToken, + CompanyID: user.CompanyID, }, nil } -func (s *Service) RefreshToken(ctx context.Context, refToken string) error { +func (s *Service) RefreshToken(ctx context.Context, refToken string) (domain.RefreshToken, error) { token, err := s.tokenStore.GetRefreshToken(ctx, refToken) if err != nil { - return err + return domain.RefreshToken{}, err } if token.Revoked { - return ErrRefreshTokenNotFound + return domain.RefreshToken{}, ErrRefreshTokenNotFound } if token.ExpiresAt.Before(time.Now()) { - return ErrExpiredToken + return domain.RefreshToken{}, ErrExpiredToken } // newRefToken, err := generateRefreshToken() @@ -80,8 +84,19 @@ func (s *Service) RefreshToken(ctx context.Context, refToken string) error { // CreatedAt: time.Now(), // ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second), // }) - return nil + return token, nil } + +func (s *Service) GetLastLogin(ctx context.Context, user_id int64) (*time.Time, error) { + refreshToken, err := s.tokenStore.GetRefreshTokenByUserID(ctx, user_id) + if err != nil { + return nil, err + } + + return &refreshToken.CreatedAt, nil + +} + func (s *Service) Logout(ctx context.Context, refToken string) error { token, err := s.tokenStore.GetRefreshToken(ctx, refToken) if err != nil { diff --git a/internal/services/authentication/port.go b/internal/services/authentication/port.go index d177dbe..2728241 100644 --- a/internal/services/authentication/port.go +++ b/internal/services/authentication/port.go @@ -12,5 +12,6 @@ type UserStore interface { type TokenStore interface { CreateRefreshToken(ctx context.Context, rt domain.RefreshToken) error GetRefreshToken(ctx context.Context, token string) (domain.RefreshToken, error) + GetRefreshTokenByUserID(ctx context.Context, id int64) (domain.RefreshToken, error) RevokeRefreshToken(ctx context.Context, token string) error } diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index 1ff9565..1a2cb8d 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -2,6 +2,8 @@ package bet import ( "context" + "crypto/rand" + "math/big" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) @@ -16,6 +18,21 @@ func NewService(betStore BetStore) *Service { } } +func (s *Service) GenerateCashoutID() (string, error) { + const chars = "abcdefghijklmnopqrstuvwxyz0123456789" + const length int = 13 + charLen := big.NewInt(int64(len(chars))) + result := make([]byte, length) + for i := 0; i < length; i++ { + index, err := rand.Int(rand.Reader, charLen) + if err != nil { + return "", err + } + result[i] = chars[index.Int64()] + } + return string(result), nil +} + func (s *Service) CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) { return s.betStore.CreateBet(ctx, bet) diff --git a/internal/services/company/port.go b/internal/services/company/port.go index 5263ed1..a56e8a2 100644 --- a/internal/services/company/port.go +++ b/internal/services/company/port.go @@ -10,6 +10,7 @@ type CompanyStore interface { CreateCompany(ctx context.Context, company domain.CreateCompany) (domain.Company, error) GetAllCompanies(ctx context.Context) ([]domain.Company, error) GetCompanyByID(ctx context.Context, id int64) (domain.Company, error) + SearchCompanyByName(ctx context.Context, name string) ([]domain.Company, error) UpdateCompany(ctx context.Context, id int64, company domain.UpdateCompany) (domain.Company, error) DeleteCompany(ctx context.Context, id int64) error } diff --git a/internal/services/company/service.go b/internal/services/company/service.go index b5b6f7e..d6fa9a0 100644 --- a/internal/services/company/service.go +++ b/internal/services/company/service.go @@ -27,6 +27,10 @@ func (s *Service) GetCompanyByID(ctx context.Context, id int64) (domain.Company, return s.companyStore.GetCompanyByID(ctx, id) } +func (s *Service) SearchCompanyByName(ctx context.Context, name string) ([]domain.Company, error) { + return s.companyStore.SearchCompanyByName(ctx, name) +} + func (s *Service) UpdateCompany(ctx context.Context, id int64, company domain.UpdateCompany) (domain.Company, error) { return s.companyStore.UpdateCompany(ctx, id, company) } diff --git a/internal/services/user/direct.go b/internal/services/user/direct.go index f848792..8181822 100644 --- a/internal/services/user/direct.go +++ b/internal/services/user/direct.go @@ -6,7 +6,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) -func (s *Service) CreateUser(ctx context.Context, User domain.CreateUserReq) (domain.User, error) { +func (s *Service) CreateUser(ctx context.Context, User domain.CreateUserReq, is_company bool) (domain.User, error) { // Create User // creator, err := s.userStore.GetUserByID(ctx, createrUserId) // if err != nil { @@ -33,7 +33,9 @@ func (s *Service) CreateUser(ctx context.Context, User domain.CreateUserReq) (do Role: domain.Role(User.Role), EmailVerified: true, PhoneVerified: true, - }) + Suspended: User.Suspended, + CompanyID: User.CompanyID, + }, is_company) } func (s *Service) DeleteUser(ctx context.Context, id int64) error { @@ -42,10 +44,10 @@ func (s *Service) DeleteUser(ctx context.Context, id int64) error { } type Filter struct { - Role string - BranchId ValidBranchId - Page int - PageSize int + Role string + CompanyID domain.ValidInt64 + Page int + PageSize int } type ValidRole struct { Value domain.Role @@ -56,7 +58,7 @@ type ValidBranchId struct { Valid bool } -func (s *Service) GetAllUsers(ctx context.Context, filter Filter) ([]domain.User, error) { +func (s *Service) GetAllUsers(ctx context.Context, filter Filter) ([]domain.User, int64, error) { // Get all Users return s.userStore.GetAllUsers(ctx, filter) } diff --git a/internal/services/user/port.go b/internal/services/user/port.go index 9b8d06e..588787c 100644 --- a/internal/services/user/port.go +++ b/internal/services/user/port.go @@ -7,10 +7,10 @@ import ( ) type UserStore interface { - CreateUser(ctx context.Context, user domain.User, usedOtpId int64) (domain.User, error) - CreateUserWithoutOtp(ctx context.Context, user domain.User) (domain.User, error) + CreateUser(ctx context.Context, user domain.User, usedOtpId int64, is_company bool) (domain.User, error) + CreateUserWithoutOtp(ctx context.Context, user domain.User, is_company bool) (domain.User, error) GetUserByID(ctx context.Context, id int64) (domain.User, error) - GetAllUsers(ctx context.Context, filter Filter) ([]domain.User, error) + GetAllUsers(ctx context.Context, filter Filter) ([]domain.User, int64, error) GetAllCashiers(ctx context.Context) ([]domain.User, error) GetCashiersByBranch(ctx context.Context, branchID int64) ([]domain.User, error) UpdateUser(ctx context.Context, user domain.UpdateUserReq) error diff --git a/internal/services/user/register.go b/internal/services/user/register.go index dcefa99..3169b7b 100644 --- a/internal/services/user/register.go +++ b/internal/services/user/register.go @@ -70,7 +70,7 @@ func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterU PhoneVerified: registerReq.OtpMedium == domain.OtpMediumSms, } // create the user and mark otp as used - user, err := s.userStore.CreateUser(ctx, userR, otp.ID) + user, err := s.userStore.CreateUser(ctx, userR, otp.ID, false) if err != nil { return domain.User{}, err } diff --git a/internal/web_server/app.go b/internal/web_server/app.go index 3864b2f..f87685c 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -6,11 +6,11 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" - referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" + referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" diff --git a/internal/web_server/handlers/admin.go b/internal/web_server/handlers/admin.go new file mode 100644 index 0000000..26e2ca7 --- /dev/null +++ b/internal/web_server/handlers/admin.go @@ -0,0 +1,156 @@ +package handlers + +import ( + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" + "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" + "github.com/gofiber/fiber/v2" +) + +type CreateAdminReq struct { + FirstName string `json:"first_name" example:"John"` + LastName string `json:"last_name" example:"Doe"` + Email string `json:"email" example:"john.doe@example.com"` + PhoneNumber string `json:"phone_number" example:"1234567890"` + Password string `json:"password" example:"password123"` + CompanyID *int64 `json:"company_id,omitempty" example:"1"` +} + +// CreateAdmin godoc +// @Summary Create Admin +// @Description Create Admin +// @Tags admin +// @Accept json +// @Produce json +// @Param manger body CreateAdminReq true "Create admin" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 401 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /admin [post] +func (h *Handler) CreateAdmin(c *fiber.Ctx) error { + var companyID domain.ValidInt64 + var req CreateAdminReq + + if err := c.BodyParser(&req); err != nil { + h.logger.Error("RegisterUser failed", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) + } + valErrs, ok := h.validator.Validate(c, req) + if !ok { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + } + + // Admins can be created without company ids and can be assigned later + if req.CompanyID == nil { + companyID = domain.ValidInt64{ + Value: 0, + Valid: false, + } + } else { + companyID = domain.ValidInt64{ + Value: *req.CompanyID, + Valid: true, + } + } + + user := domain.CreateUserReq{ + FirstName: req.FirstName, + LastName: req.LastName, + Email: req.Email, + PhoneNumber: req.PhoneNumber, + Password: req.Password, + Role: string(domain.RoleAdmin), + CompanyID: companyID, + } + _, err := h.userSvc.CreateUser(c.Context(), user, true) + if err != nil { + h.logger.Error("CreateAdmin failed", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to create admin", nil, nil) + } + return response.WriteJSON(c, fiber.StatusOK, "Admin created successfully", nil, nil) +} + +type AdminRes struct { + ID int64 `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` + PhoneNumber string `json:"phone_number"` + Role domain.Role `json:"role"` + EmailVerified bool `json:"email_verified"` + PhoneVerified bool `json:"phone_verified"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LastLogin time.Time `json:"last_login"` + SuspendedAt time.Time `json:"suspended_at"` + Suspended bool `json:"suspended"` +} + +// GetAllAdmins godoc +// @Summary Get all Admins +// @Description Get all Admins +// @Tags admin +// @Accept json +// @Produce json +// @Param page query int false "Page number" +// @Param page_size query int false "Page size" +// @Success 200 {object} AdminRes +// @Failure 400 {object} response.APIResponse +// @Failure 401 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /admin [get] +func (h *Handler) GetAllAdmins(c *fiber.Ctx) error { + filter := user.Filter{ + Role: string(domain.RoleAdmin), + CompanyID: domain.ValidInt64{ + Value: int64(c.QueryInt("company_id")), + Valid: true, + }, + Page: c.QueryInt("page", 1) - 1, + PageSize: c.QueryInt("page_size", 10), + } + valErrs, ok := h.validator.Validate(c, filter) + if !ok { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + } + admins, total, err := h.userSvc.GetAllUsers(c.Context(), filter) + if err != nil { + h.logger.Error("GetAllAdmins failed", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get Admins", nil, nil) + } + + var result []AdminRes = make([]AdminRes, len(admins)) + for index, admin := range admins { + lastLogin, err := h.authSvc.GetLastLogin(c.Context(), admin.ID) + if err != nil { + if err == authentication.ErrRefreshTokenNotFound { + lastLogin = &admin.CreatedAt + } else { + h.logger.Error("Failed to get user last login", "userID", admin.ID, "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login") + } + } + result[index] = AdminRes{ + ID: admin.ID, + FirstName: admin.FirstName, + LastName: admin.LastName, + Email: admin.Email, + PhoneNumber: admin.PhoneNumber, + Role: admin.Role, + EmailVerified: admin.EmailVerified, + PhoneVerified: admin.PhoneVerified, + CreatedAt: admin.CreatedAt, + UpdatedAt: admin.UpdatedAt, + SuspendedAt: admin.SuspendedAt, + Suspended: admin.Suspended, + LastLogin: *lastLogin, + } + } + + return response.WritePaginatedJSON(c, fiber.StatusOK, "Admins retrieved successfully", result, nil, filter.Page, int(total)) + +} diff --git a/internal/web_server/handlers/auth_handler.go b/internal/web_server/handlers/auth_handler.go index db9e4e4..b117333 100644 --- a/internal/web_server/handlers/auth_handler.go +++ b/internal/web_server/handlers/auth_handler.go @@ -3,13 +3,22 @@ package handlers import ( "errors" - "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" ) +type loginCustomerReq struct { + Email string `json:"email" validate:"email" example:"john.doe@example.com"` + PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"` + Password string `json:"password" validate:"required" example:"password123"` +} +type loginCustomerRes struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + Role string `json:"role"` +} // LoginCustomer godoc // @Summary Login customer @@ -24,16 +33,6 @@ import ( // @Failure 500 {object} response.APIResponse // @Router /auth/login [post] func (h *Handler) LoginCustomer(c *fiber.Ctx) error { - type loginCustomerReq struct { - Email string `json:"email" validate:"email" example:"john.doe@example.com"` - PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"` - Password string `json:"password" validate:"required" example:"password123"` - } - type loginCustomerRes struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - Role string `json:"role"` - } var req loginCustomerReq if err := c.BodyParser(&req); err != nil { @@ -51,13 +50,15 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error { switch { case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound): return fiber.NewError(fiber.StatusUnauthorized, "Invalid credentials") + case errors.Is(err, authentication.ErrUserSuspended): + return fiber.NewError(fiber.StatusUnauthorized, "User login has been locked") default: h.logger.Error("Login failed", "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Internal server error") } } - accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, successRes.BranchId, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry) + accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, successRes.CompanyID, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry) if err != nil { h.logger.Error("Failed to create access token", "userID", successRes.UserId, "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token") @@ -71,6 +72,10 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Login successful", res, nil) } +type refreshToken struct { + AccessToken string `json:"access_token" validate:"required" example:""` + RefreshToken string `json:"refresh_token" validate:"required" example:""` +} // RefreshToken godoc // @Summary Refresh token @@ -85,16 +90,13 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error { // @Failure 500 {object} response.APIResponse // @Router /auth/refresh [post] func (h *Handler) RefreshToken(c *fiber.Ctx) error { - type refreshTokenReq struct { - AccessToken string `json:"access_token" validate:"required" example:""` - RefreshToken string `json:"refresh_token" validate:"required" example:""` - } + type loginCustomerRes struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` } - var req refreshTokenReq + var req refreshToken if err := c.BodyParser(&req); err != nil { h.logger.Error("Failed to parse RefreshToken request", "error", err) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") @@ -104,10 +106,7 @@ func (h *Handler) RefreshToken(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } - userId := c.Locals("user_id").(int64) - role := c.Locals("role").(string) - branchId := c.Locals("branch_id").(int64) - err := h.authSvc.RefreshToken(c.Context(), req.RefreshToken) + refreshToken, err := h.authSvc.RefreshToken(c.Context(), req.RefreshToken) if err != nil { h.logger.Info("Refresh token attempt failed", "refreshToken", req.RefreshToken, "error", err) switch { @@ -121,8 +120,10 @@ func (h *Handler) RefreshToken(c *fiber.Ctx) error { } } + user, err := h.userSvc.GetUserByID(c.Context(), refreshToken.UserID) + // Assuming the refreshed token includes userID and role info; adjust if needed - accessToken, err := jwtutil.CreateJwt(userId, domain.Role(role), branchId, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry) + accessToken, err := jwtutil.CreateJwt(user.ID, user.Role, user.CompanyID, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry) if err != nil { h.logger.Error("Failed to create new access token", "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token") @@ -135,6 +136,10 @@ func (h *Handler) RefreshToken(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Refresh successful", res, nil) } +type logoutReq struct { + RefreshToken string `json:"refresh_token" validate:"required" example:""` +} + // LogOutCustomer godoc // @Summary Logout customer // @Description Logout customer @@ -148,9 +153,6 @@ func (h *Handler) RefreshToken(c *fiber.Ctx) error { // @Failure 500 {object} response.APIResponse // @Router /auth/logout [post] func (h *Handler) LogOutCustomer(c *fiber.Ctx) error { - type logoutReq struct { - RefreshToken string `json:"refresh_token" validate:"required" example:""` - } var req logoutReq if err := c.BodyParser(&req); err != nil { diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index e968724..f638cd6 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -3,12 +3,10 @@ package handlers import ( "encoding/json" "strconv" - "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" - "github.com/google/uuid" ) type CreateBetOutcomeReq struct { @@ -24,7 +22,6 @@ type CreateBetReq struct { Status domain.OutcomeStatus `json:"status" example:"1"` FullName string `json:"full_name" example:"John"` PhoneNumber string `json:"phone_number" example:"1234567890"` - IsShopBet bool `json:"is_shop_bet" example:"false"` } type CreateBetRes struct { @@ -117,7 +114,15 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error { // Validating user by role // Differentiating between offline and online bets user, err := h.userSvc.GetUserByID(c.Context(), userID) - cashoutUUID := uuid.New() + if err != nil { + h.logger.Error("CreateBetReq failed, user id invalid") + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) + } + cashoutID, err := h.betSvc.GenerateCashoutID() + if err != nil { + h.logger.Error("CreateBetReq failed, unable to create cashout id") + return response.WriteJSON(c, fiber.StatusInternalServerError, "Invalid request", err, nil) + } var bet domain.Bet if user.Role == domain.RoleCashier { @@ -153,8 +158,27 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error { Value: userID, Valid: false, }, - IsShopBet: req.IsShopBet, - CashoutID: cashoutUUID.String(), + IsShopBet: true, + CashoutID: cashoutID, + }) + } else if user.Role == domain.RoleSuperAdmin { + // This is just for testing + bet, err = h.betSvc.CreateBet(c.Context(), domain.CreateBet{ + Amount: domain.ToCurrency(req.Amount), + TotalOdds: req.TotalOdds, + Status: req.Status, + FullName: req.FullName, + PhoneNumber: req.PhoneNumber, + BranchID: domain.ValidInt64{ + Value: 1, + Valid: true, + }, + UserID: domain.ValidInt64{ + Value: userID, + Valid: true, + }, + IsShopBet: true, + CashoutID: cashoutID, }) } else { // TODO if user is customer, get id from the token then get the wallet id from there and reduce the amount @@ -173,8 +197,8 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error { Value: userID, Valid: true, }, - IsShopBet: req.IsShopBet, - CashoutID: cashoutUUID.String(), + IsShopBet: false, + CashoutID: cashoutID, }) } @@ -200,10 +224,10 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error { } // Checking to make sure the event hasn't already started - currentTime := time.Now() - if event.StartTime.Before(currentTime) { - return response.WriteJSON(c, fiber.StatusBadRequest, "The event has already expired", nil, nil) - } + // currentTime := time.Now() + // if event.StartTime.Before(currentTime) { + // return response.WriteJSON(c, fiber.StatusBadRequest, "The event has already expired", nil, nil) + // } odds, err := h.prematchSvc.GetRawOddsByMarketID(c.Context(), marketIDStr, eventIDStr) @@ -224,7 +248,7 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error { rawBytes, err := json.Marshal(raw) err = json.Unmarshal(rawBytes, &rawOdd) if err != nil { - h.logger.Error("Failed to unmarshal raw odd:", err) + h.logger.Error("Failed to unmarshal raw odd", "error", err) continue } if rawOdd.ID == oddIDStr { diff --git a/internal/web_server/handlers/cashier.go b/internal/web_server/handlers/cashier.go index 5695e56..8a99d3b 100644 --- a/internal/web_server/handlers/cashier.go +++ b/internal/web_server/handlers/cashier.go @@ -1,10 +1,12 @@ package handlers import ( + "fmt" "strconv" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" ) @@ -16,6 +18,7 @@ type CreateCashierReq struct { PhoneNumber string `json:"phone_number" example:"1234567890"` Password string `json:"password" example:"password123"` BranchID int64 `json:"branch_id" example:"1"` + Suspended bool `json:"suspended" example:"false"` } // CreateCashier godoc @@ -31,6 +34,10 @@ type CreateCashierReq struct { // @Failure 500 {object} response.APIResponse // @Router /cashiers [post] func (h *Handler) CreateCashier(c *fiber.Ctx) error { + + // Get user_id from middleware + companyID := c.Locals("company_id").(domain.ValidInt64) + var req CreateCashierReq if err := c.BodyParser(&req); err != nil { h.logger.Error("RegisterUser failed", "error", err) @@ -47,8 +54,11 @@ func (h *Handler) CreateCashier(c *fiber.Ctx) error { PhoneNumber: req.PhoneNumber, Password: req.Password, Role: string(domain.RoleCashier), + Suspended: req.Suspended, + CompanyID: companyID, } - newUser, err := h.userSvc.CreateUser(c.Context(), userRequest) + fmt.Print(req.Suspended) + newUser, err := h.userSvc.CreateUser(c.Context(), userRequest, true) if err != nil { h.logger.Error("CreateCashier failed", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to create cashier", nil, nil) @@ -76,6 +86,7 @@ type GetCashierRes struct { UpdatedAt time.Time `json:"updated_at"` SuspendedAt time.Time `json:"suspended_at"` Suspended bool `json:"suspended"` + LastLogin time.Time `json:"last_login"` } // GetAllCashiers godoc @@ -113,9 +124,19 @@ func (h *Handler) GetAllCashiers(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get cashiers", nil, nil) } - var result []GetCashierRes + var result []GetCashierRes = make([]GetCashierRes, 0, len(cashiers)) for _, cashier := range cashiers { + lastLogin, err := h.authSvc.GetLastLogin(c.Context(), cashier.ID) + if err != nil { + if err == authentication.ErrRefreshTokenNotFound { + lastLogin = &cashier.CreatedAt + } else { + h.logger.Error("Failed to get user last login", "userID", cashier.ID, "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login") + } + } + result = append(result, GetCashierRes{ ID: cashier.ID, FirstName: cashier.FirstName, @@ -129,6 +150,7 @@ func (h *Handler) GetAllCashiers(c *fiber.Ctx) error { UpdatedAt: cashier.UpdatedAt, SuspendedAt: cashier.SuspendedAt, Suspended: cashier.Suspended, + LastLogin: *lastLogin, }) } @@ -148,6 +170,7 @@ type updateUserReq struct { // @Tags cashier // @Accept json // @Produce json +// @Param id path int true "Cashier ID" // @Param cashier body updateUserReq true "Update cashier" // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse @@ -155,6 +178,12 @@ type updateUserReq struct { // @Failure 500 {object} response.APIResponse // @Router /cashiers/{id} [put] func (h *Handler) UpdateCashier(c *fiber.Ctx) error { + cashierIdStr := c.Params("id") + cashierId, err := strconv.ParseInt(cashierIdStr, 10, 64) + if err != nil { + h.logger.Error("UpdateCashier failed", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid cashier ID", nil, nil) + } var req updateUserReq if err := c.BodyParser(&req); err != nil { h.logger.Error("UpdateCashier failed", "error", err) @@ -166,12 +195,7 @@ func (h *Handler) UpdateCashier(c *fiber.Ctx) error { if !ok { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } - cashierIdStr := c.Params("id") - cashierId, err := strconv.ParseInt(cashierIdStr, 10, 64) - if err != nil { - h.logger.Error("UpdateCashier failed", "error", err) - return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid cashier ID", nil, nil) - } + err = h.userSvc.UpdateUser(c.Context(), domain.UpdateUserReq{ UserId: cashierId, FirstName: domain.ValidString{ diff --git a/internal/web_server/handlers/company_handler.go b/internal/web_server/handlers/company_handler.go index 9c88215..f8cb663 100644 --- a/internal/web_server/handlers/company_handler.go +++ b/internal/web_server/handlers/company_handler.go @@ -143,6 +143,37 @@ func (h *Handler) GetCompanyByID(c *fiber.Ctx) error { } +// GetAllCompanies godoc +// @Summary Gets all companies +// @Description Gets all companies +// @Tags company +// @Accept json +// @Produce json +// @Success 200 {array} CompanyRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /search/company [get] +func (h *Handler) SearchCompany(c *fiber.Ctx) error { + searchQuery := c.Query("q") + if searchQuery == "" { + return response.WriteJSON(c, fiber.StatusBadRequest, "Search query is required", nil, nil) + } + companies, err := h.companySvc.SearchCompanyByName(c.Context(), searchQuery) + if err != nil { + h.logger.Error("Failed to get companies", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get companies", err, nil) + } + + var result []CompanyRes = make([]CompanyRes, 0, len(companies)) + + for _, company := range companies { + result = append(result, convertCompany(company)) + } + + return response.WriteJSON(c, fiber.StatusOK, "All Companies retrieved", result, nil) + +} + // UpdateCompany godoc // @Summary Updates a company // @Description Updates a company diff --git a/internal/web_server/handlers/manager.go b/internal/web_server/handlers/manager.go index 2520764..9edfbb6 100644 --- a/internal/web_server/handlers/manager.go +++ b/internal/web_server/handlers/manager.go @@ -2,8 +2,10 @@ package handlers import ( "strconv" + "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" @@ -15,11 +17,12 @@ type CreateManagerReq struct { Email string `json:"email" example:"john.doe@example.com"` PhoneNumber string `json:"phone_number" example:"1234567890"` Password string `json:"password" example:"password123"` + CompanyID *int64 `json:"company_id,omitempty" example:"1"` } -// CreateManagers godoc -// @Summary Create Managers -// @Description Create Managers +// CreateManager godoc +// @Summary Create Manager +// @Description Create Manager // @Tags manager // @Accept json // @Produce json @@ -30,6 +33,9 @@ type CreateManagerReq struct { // @Failure 500 {object} response.APIResponse // @Router /managers [post] func (h *Handler) CreateManager(c *fiber.Ctx) error { + + // Get user_id from middleware + var req CreateManagerReq if err := c.BodyParser(&req); err != nil { h.logger.Error("RegisterUser failed", "error", err) @@ -39,6 +45,22 @@ func (h *Handler) CreateManager(c *fiber.Ctx) error { if !ok { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } + + var companyID domain.ValidInt64 + role := c.Locals("role").(domain.Role) + if role == domain.RoleSuperAdmin { + if req.CompanyID == nil { + h.logger.Error("RegisterUser failed error: company id is required") + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", "Company ID is required", nil) + } + companyID = domain.ValidInt64{ + Value: *req.CompanyID, + Valid: true, + } + } else { + companyID = c.Locals("company_id").(domain.ValidInt64) + } + user := domain.CreateUserReq{ FirstName: req.FirstName, LastName: req.LastName, @@ -46,16 +68,33 @@ func (h *Handler) CreateManager(c *fiber.Ctx) error { PhoneNumber: req.PhoneNumber, Password: req.Password, Role: string(domain.RoleBranchManager), + CompanyID: companyID, } - _, err := h.userSvc.CreateUser(c.Context(), user) + _, err := h.userSvc.CreateUser(c.Context(), user, true) if err != nil { - h.logger.Error("CreateManagers failed", "error", err) - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to create Managers", nil, nil) + h.logger.Error("CreateManager failed", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to create manager", nil, nil) } - return response.WriteJSON(c, fiber.StatusOK, "Managers created successfully", nil, nil) + return response.WriteJSON(c, fiber.StatusOK, "Manager created successfully", nil, nil) } +type ManagersRes struct { + ID int64 `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` + PhoneNumber string `json:"phone_number"` + Role domain.Role `json:"role"` + EmailVerified bool `json:"email_verified"` + PhoneVerified bool `json:"phone_verified"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LastLogin time.Time `json:"last_login"` + SuspendedAt time.Time `json:"suspended_at"` + Suspended bool `json:"suspended"` +} + // GetAllManagers godoc // @Summary Get all Managers // @Description Get all Managers @@ -64,7 +103,7 @@ func (h *Handler) CreateManager(c *fiber.Ctx) error { // @Produce json // @Param page query int false "Page number" // @Param page_size query int false "Page size" -// @Success 200 {object} response.APIResponse +// @Success 200 {object} ManagersRes // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse @@ -72,24 +111,52 @@ func (h *Handler) CreateManager(c *fiber.Ctx) error { func (h *Handler) GetAllManagers(c *fiber.Ctx) error { filter := user.Filter{ Role: string(domain.RoleBranchManager), - BranchId: user.ValidBranchId{ - Value: int64(c.QueryInt("branch_id")), + CompanyID: domain.ValidInt64{ + Value: int64(c.QueryInt("company_id")), Valid: true, }, - Page: c.QueryInt("page", 1), + Page: c.QueryInt("page", 1) - 1, PageSize: c.QueryInt("page_size", 10), } valErrs, ok := h.validator.Validate(c, filter) if !ok { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } - Managers, err := h.userSvc.GetAllUsers(c.Context(), filter) + managers, total, err := h.userSvc.GetAllUsers(c.Context(), filter) if err != nil { h.logger.Error("GetAllManagers failed", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get Managers", nil, nil) } - return response.WriteJSON(c, fiber.StatusOK, "Managers retrieved successfully", Managers, nil) + var result []ManagersRes = make([]ManagersRes, len(managers)) + for index, manager := range managers { + lastLogin, err := h.authSvc.GetLastLogin(c.Context(), manager.ID) + if err != nil { + if err == authentication.ErrRefreshTokenNotFound { + lastLogin = &manager.CreatedAt + } else { + h.logger.Error("Failed to get user last login", "userID", manager.ID, "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login") + } + } + result[index] = ManagersRes{ + ID: manager.ID, + FirstName: manager.FirstName, + LastName: manager.LastName, + Email: manager.Email, + PhoneNumber: manager.PhoneNumber, + Role: manager.Role, + EmailVerified: manager.EmailVerified, + PhoneVerified: manager.PhoneVerified, + CreatedAt: manager.CreatedAt, + UpdatedAt: manager.UpdatedAt, + SuspendedAt: manager.SuspendedAt, + Suspended: manager.Suspended, + LastLogin: *lastLogin, + } + } + + return response.WritePaginatedJSON(c, fiber.StatusOK, "Managers retrieved successfully", result, nil, filter.Page, int(total)) } diff --git a/internal/web_server/handlers/ticket_handler.go b/internal/web_server/handlers/ticket_handler.go index a3a6998..84bf4a1 100644 --- a/internal/web_server/handlers/ticket_handler.go +++ b/internal/web_server/handlers/ticket_handler.go @@ -3,7 +3,6 @@ package handlers import ( "encoding/json" "strconv" - "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" @@ -77,10 +76,10 @@ func (h *Handler) CreateTicket(c *fiber.Ctx) error { } // Checking to make sure the event hasn't already started - currentTime := time.Now() - if event.StartTime.Before(currentTime) { - return response.WriteJSON(c, fiber.StatusBadRequest, "The event has already expired", nil, nil) - } + // currentTime := time.Now() + // if event.StartTime.Before(currentTime) { + // return response.WriteJSON(c, fiber.StatusBadRequest, "The event has already expired", nil, nil) + // } odds, err := h.prematchSvc.GetRawOddsByMarketID(c.Context(), marketIDStr, eventIDStr) @@ -184,8 +183,8 @@ func (h *Handler) GetTicketByID(c *fiber.Ctx) error { ticket, err := h.ticketSvc.GetTicketByID(c.Context(), id) if err != nil { - h.logger.Error("Failed to get ticket by ID", "ticketID", id, "error", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve ticket") + // h.logger.Error("Failed to get ticket by ID", "ticketID", id, "error", err) + return fiber.NewError(fiber.StatusNotFound, "Failed to retrieve ticket") } res := TicketRes{ diff --git a/internal/web_server/handlers/transaction_handler.go b/internal/web_server/handlers/transaction_handler.go index c00fa85..c0f7eb7 100644 --- a/internal/web_server/handlers/transaction_handler.go +++ b/internal/web_server/handlers/transaction_handler.go @@ -9,21 +9,22 @@ import ( ) type TransactionRes struct { - ID int64 `json:"id" example:"1"` - Amount float32 `json:"amount" example:"100.0"` - BranchID int64 `json:"branch_id" example:"1"` - CashierID int64 `json:"cashier_id" example:"1"` - BetID int64 `json:"bet_id" example:"1"` - Type int64 `json:"type" example:"1"` - PaymentOption domain.PaymentOption `json:"payment_option" example:"1"` - FullName string `json:"full_name" example:"John Smith"` - PhoneNumber string `json:"phone_number" example:"0911111111"` - BankCode string `json:"bank_code"` - BeneficiaryName string `json:"beneficiary_name"` - AccountName string `json:"account_name"` - AccountNumber string `json:"account_number"` - ReferenceNumber string `json:"reference_number"` - Verified bool `json:"verified" example:"true"` + ID int64 `json:"id" example:"1"` + Amount float32 `json:"amount" example:"100.0"` + BranchID int64 `json:"branch_id" example:"1"` + CashierID int64 `json:"cashier_id" example:"1"` + BetID int64 `json:"bet_id" example:"1"` + NumberOfOutcomes int64 `json:"number_of_outcomes" example:"1"` + Type int64 `json:"type" example:"1"` + PaymentOption domain.PaymentOption `json:"payment_option" example:"1"` + FullName string `json:"full_name" example:"John Smith"` + PhoneNumber string `json:"phone_number" example:"0911111111"` + BankCode string `json:"bank_code"` + BeneficiaryName string `json:"beneficiary_name"` + AccountName string `json:"account_name"` + AccountNumber string `json:"account_number"` + ReferenceNumber string `json:"reference_number"` + Verified bool `json:"verified" example:"true"` } type CreateTransactionReq struct { @@ -115,20 +116,22 @@ func (h *Handler) CreateTransaction(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } + // TODO: Validate the bet id and add the number of outcomes transaction, err := h.transactionSvc.CreateTransaction(c.Context(), domain.CreateTransaction{ - BranchID: branchID, - CashierID: userID, - Amount: domain.ToCurrency(req.Amount), - BetID: req.BetID, - Type: domain.TransactionType(req.Type), - PaymentOption: domain.PaymentOption(req.PaymentOption), - FullName: req.FullName, - PhoneNumber: req.PhoneNumber, - BankCode: req.BankCode, - BeneficiaryName: req.BeneficiaryName, - AccountName: req.AccountName, - AccountNumber: req.AccountNumber, - ReferenceNumber: req.ReferenceNumber, + BranchID: branchID, + CashierID: userID, + Amount: domain.ToCurrency(req.Amount), + BetID: req.BetID, + NumberOfOutcomes: 1, + Type: domain.TransactionType(req.Type), + PaymentOption: domain.PaymentOption(req.PaymentOption), + FullName: req.FullName, + PhoneNumber: req.PhoneNumber, + BankCode: req.BankCode, + BeneficiaryName: req.BeneficiaryName, + AccountName: req.AccountName, + AccountNumber: req.AccountNumber, + ReferenceNumber: req.ReferenceNumber, }) if err != nil { @@ -232,6 +235,10 @@ func (h *Handler) GetTransactionByID(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Transaction retrieved successfully", res, nil) } +type UpdateTransactionVerifiedReq struct { + Verified bool `json:"verified" validate:"required" example:"true"` +} + // UpdateTransactionVerified godoc // @Summary Updates the verified field of a transaction // @Description Updates the verified status of a transaction @@ -245,9 +252,6 @@ func (h *Handler) GetTransactionByID(c *fiber.Ctx) error { // @Failure 500 {object} response.APIResponse // @Router /transaction/{id} [patch] func (h *Handler) UpdateTransactionVerified(c *fiber.Ctx) error { - type UpdateTransactionVerifiedReq struct { - Verified bool `json:"verified" validate:"required" example:"true"` - } transactionID := c.Params("id") id, err := strconv.ParseInt(transactionID, 10, 64) diff --git a/internal/web_server/handlers/user.go b/internal/web_server/handlers/user.go index 3761518..7d3238d 100644 --- a/internal/web_server/handlers/user.go +++ b/internal/web_server/handlers/user.go @@ -2,14 +2,25 @@ package handlers import ( "errors" + "strconv" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" ) +type CheckPhoneEmailExistReq struct { + Email string `json:"email" validate:"email" example:"john.doe@example.com"` + PhoneNumber string `json:"phone_number" validate:"required" example:"1234567890"` +} +type CheckPhoneEmailExistRes struct { + EmailExist bool `json:"email_exist"` + PhoneNumberExist bool `json:"phone_number_exist"` +} + // CheckPhoneEmailExist godoc // @Summary Check if phone number or email exist // @Description Check if phone number or email exist @@ -22,14 +33,6 @@ import ( // @Failure 500 {object} response.APIResponse // @Router /user/checkPhoneEmailExist [post] func (h *Handler) CheckPhoneEmailExist(c *fiber.Ctx) error { - type CheckPhoneEmailExistReq struct { - Email string `json:"email" validate:"email" example:"john.doe@example.com"` - PhoneNumber string `json:"phone_number" validate:"required" example:"1234567890"` - } - type CheckPhoneEmailExistRes struct { - EmailExist bool `json:"email_exist"` - PhoneNumberExist bool `json:"phone_number_exist"` - } var req CheckPhoneEmailExistReq if err := c.BodyParser(&req); err != nil { @@ -54,6 +57,11 @@ func (h *Handler) CheckPhoneEmailExist(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Check successful", res, nil) } +type RegisterCodeReq struct { + Email string `json:"email" validate:"email" example:"john.doe@example.com"` + PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"` +} + // SendRegisterCode godoc // @Summary Send register code // @Description Send register code @@ -66,10 +74,6 @@ func (h *Handler) CheckPhoneEmailExist(c *fiber.Ctx) error { // @Failure 500 {object} response.APIResponse // @Router /user/sendRegisterCode [post] func (h *Handler) SendRegisterCode(c *fiber.Ctx) error { - type RegisterCodeReq struct { - Email string `json:"email" validate:"email" example:"john.doe@example.com"` - PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"` - } var req RegisterCodeReq if err := c.BodyParser(&req); err != nil { @@ -101,6 +105,16 @@ func (h *Handler) SendRegisterCode(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil) } +type RegisterUserReq struct { + FirstName string `json:"first_name" example:"John"` + LastName string `json:"last_name" example:"Doe"` + Email string `json:"email" example:"john.doe@example.com"` + PhoneNumber string `json:"phone_number" example:"1234567890"` + Password string `json:"password" example:"password123"` + Otp string `json:"otp" example:"123456"` + ReferalCode string `json:"referal_code" example:"ABC123"` +} + // RegisterUser godoc // @Summary Register user // @Description Register user @@ -113,15 +127,6 @@ func (h *Handler) SendRegisterCode(c *fiber.Ctx) error { // @Failure 500 {object} response.APIResponse // @Router /user/register [post] func (h *Handler) RegisterUser(c *fiber.Ctx) error { - type RegisterUserReq struct { - FirstName string `json:"first_name" example:"John"` - LastName string `json:"last_name" example:"Doe"` - Email string `json:"email" example:"john.doe@example.com"` - PhoneNumber string `json:"phone_number" example:"1234567890"` - Password string `json:"password" example:"password123"` - Otp string `json:"otp" example:"123456"` - ReferalCode string `json:"referal_code" example:"ABC123"` - } var req RegisterUserReq if err := c.BodyParser(&req); err != nil { @@ -192,6 +197,11 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Registration successful", nil, nil) } +type ResetCodeReq struct { + Email string `json:"email" validate:"email" example:"john.doe@example.com"` + PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"` +} + // SendResetCode godoc // @Summary Send reset code // @Description Send reset code @@ -204,10 +214,6 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error { // @Failure 500 {object} response.APIResponse // @Router /user/sendResetCode [post] func (h *Handler) SendResetCode(c *fiber.Ctx) error { - type ResetCodeReq struct { - Email string `json:"email" validate:"email" example:"john.doe@example.com"` - PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"` - } var req ResetCodeReq if err := c.BodyParser(&req); err != nil { @@ -239,6 +245,13 @@ func (h *Handler) SendResetCode(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil) } +type ResetPasswordReq struct { + Email string `json:"email" validate:"email" example:"john.doe@example.com"` + PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"` + Password string `json:"password" validate:"required,min=8" example:"newpassword123"` + Otp string `json:"otp" validate:"required" example:"123456"` +} + // ResetPassword godoc // @Summary Reset password // @Description Reset password @@ -251,12 +264,6 @@ func (h *Handler) SendResetCode(c *fiber.Ctx) error { // @Failure 500 {object} response.APIResponse // @Router /user/resetPassword [post] func (h *Handler) ResetPassword(c *fiber.Ctx) error { - type ResetPasswordReq struct { - Email string `json:"email" validate:"email" example:"john.doe@example.com"` - PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"` - Password string `json:"password" validate:"required,min=8" example:"newpassword123"` - Otp string `json:"otp" validate:"required" example:"123456"` - } var req ResetPasswordReq if err := c.BodyParser(&req); err != nil { @@ -301,6 +308,7 @@ type UserProfileRes struct { PhoneVerified bool `json:"phone_verified"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` + LastLogin time.Time `json:"last_login"` SuspendedAt time.Time `json:"suspended_at"` Suspended bool `json:"suspended"` } @@ -330,6 +338,15 @@ func (h *Handler) UserProfile(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user profile") } + lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID) + if err != nil { + if err != authentication.ErrRefreshTokenNotFound { + h.logger.Error("Failed to get user last login", "userID", userID, "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login") + } + + lastLogin = &user.CreatedAt + } res := UserProfileRes{ ID: user.ID, FirstName: user.FirstName, @@ -343,6 +360,7 @@ func (h *Handler) UserProfile(c *fiber.Ctx) error { UpdatedAt: user.UpdatedAt, SuspendedAt: user.SuspendedAt, Suspended: user.Suspended, + LastLogin: *lastLogin, } return response.WriteJSON(c, fiber.StatusOK, "User profile retrieved successfully", res, nil) } @@ -374,6 +392,7 @@ type SearchUserByNameOrPhoneReq struct { // @Failure 500 {object} response.APIResponse // @Router /user/search [post] func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error { + // TODO: Add filtering by role based on which user is calling this var req SearchUserByNameOrPhoneReq if err := c.BodyParser(&req); err != nil { h.logger.Error("SearchUserByNameOrPhone failed", "error", err) @@ -395,6 +414,15 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error { } var res []UserProfileRes = make([]UserProfileRes, 0, len(users)) for _, user := range users { + lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID) + if err != nil { + if err != authentication.ErrRefreshTokenNotFound { + h.logger.Error("Failed to get user last login", "userID", user.ID, "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login") + } + + lastLogin = &user.CreatedAt + } res = append(res, UserProfileRes{ ID: user.ID, FirstName: user.FirstName, @@ -408,8 +436,80 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error { UpdatedAt: user.UpdatedAt, SuspendedAt: user.SuspendedAt, Suspended: user.Suspended, + LastLogin: *lastLogin, }) } return response.WriteJSON(c, fiber.StatusOK, "Search Successful", res, nil) } + +// GetUserByID godoc +// @Summary Get user by id +// @Description Get a single user by id +// @Tags user +// @Accept json +// @Produce json +// @Param id path int true "User ID" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 401 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /user/single/{id} [get] +func (h *Handler) GetUserByID(c *fiber.Ctx) error { + // branchId := int64(12) //c.Locals("branch_id").(int64) + // filter := user.Filter{ + // Role: string(domain.RoleUser), + // BranchId: user.ValidBranchId{ + // Value: branchId, + // Valid: true, + // }, + // Page: c.QueryInt("page", 1), + // PageSize: c.QueryInt("page_size", 10), + // } + // valErrs, ok := validator.Validate(c, filter) + // if !ok { + // return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + // } + + userIDstr := c.Params("id") + userID, err := strconv.ParseInt(userIDstr, 10, 64) + if err != nil { + h.logger.Error("UpdateCashier failed", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid cashier ID", nil, nil) + } + + user, err := h.userSvc.GetUserByID(c.Context(), userID) + if err != nil { + h.logger.Error("GetAllCashiers failed", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get cashiers", nil, nil) + } + + lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID) + if err != nil { + if err != authentication.ErrRefreshTokenNotFound { + h.logger.Error("Failed to get user last login", "userID", user.ID, "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login") + } + + lastLogin = &user.CreatedAt + } + + res := UserProfileRes{ + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email, + PhoneNumber: user.PhoneNumber, + Role: user.Role, + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + SuspendedAt: user.SuspendedAt, + Suspended: user.Suspended, + LastLogin: *lastLogin, + } + + return response.WriteJSON(c, fiber.StatusOK, "Cashiers retrieved successfully", res, nil) + +} diff --git a/internal/web_server/handlers/virtual_games_hadlers.go b/internal/web_server/handlers/virtual_games_hadlers.go index ddd6bb5..9cf269b 100644 --- a/internal/web_server/handlers/virtual_games_hadlers.go +++ b/internal/web_server/handlers/virtual_games_hadlers.go @@ -6,6 +6,16 @@ import ( "github.com/gofiber/fiber/v2" ) +type launchVirtualGameReq struct { + GameID string `json:"game_id" validate:"required" example:"crash_001"` + Currency string `json:"currency" validate:"required,len=3" example:"USD"` + Mode string `json:"mode" validate:"required,oneof=REAL DEMO" example:"REAL"` +} + +type launchVirtualGameRes struct { + LaunchURL string `json:"launch_url"` +} + // LaunchVirtualGame godoc // @Summary Launch a PopOK virtual game // @Description Generates a URL to launch a PopOK game @@ -20,15 +30,6 @@ import ( // @Failure 500 {object} response.APIResponse // @Router /virtual-game/launch [post] func (h *Handler) LaunchVirtualGame(c *fiber.Ctx) error { - type launchVirtualGameReq struct { - GameID string `json:"game_id" validate:"required" example:"crash_001"` - Currency string `json:"currency" validate:"required,len=3" example:"USD"` - Mode string `json:"mode" validate:"required,oneof=REAL DEMO" example:"REAL"` - } - - type launchVirtualGameRes struct { - LaunchURL string `json:"launch_url"` - } userID, ok := c.Locals("user_id").(int64) if !ok || userID == 0 { diff --git a/internal/web_server/handlers/wallet_handler.go b/internal/web_server/handlers/wallet_handler.go index c15f5d0..d04d651 100644 --- a/internal/web_server/handlers/wallet_handler.go +++ b/internal/web_server/handlers/wallet_handler.go @@ -175,6 +175,10 @@ func (h *Handler) GetAllBranchWallets(c *fiber.Ctx) error { } +type UpdateWalletActiveReq struct { + IsActive bool `json:"is_active" validate:"required" example:"true"` +} + // UpdateWalletActive godoc // @Summary Activate and Deactivate Wallet // @Description Can activate and deactivate wallet @@ -188,9 +192,6 @@ func (h *Handler) GetAllBranchWallets(c *fiber.Ctx) error { // @Failure 500 {object} response.APIResponse // @Router /wallet/{id} [patch] func (h *Handler) UpdateWalletActive(c *fiber.Ctx) error { - type UpdateWalletActiveReq struct { - IsActive bool `json:"is_active" validate:"required" example:"true"` - } walletID := c.Params("id") id, err := strconv.ParseInt(walletID, 10, 64) diff --git a/internal/web_server/jwt/jwt.go b/internal/web_server/jwt/jwt.go index 22b76d6..2617873 100644 --- a/internal/web_server/jwt/jwt.go +++ b/internal/web_server/jwt/jwt.go @@ -17,9 +17,9 @@ var ( type UserClaim struct { jwt.RegisteredClaims - UserId int64 - Role domain.Role - BranchId int64 + UserId int64 + Role domain.Role + CompanyID domain.ValidInt64 } type PopOKClaim struct { @@ -37,7 +37,7 @@ type JwtConfig struct { JwtAccessExpiry int } -func CreateJwt(userId int64, Role domain.Role, BranchId int64, key string, expiry int) (string, error) { +func CreateJwt(userId int64, Role domain.Role, CompanyID domain.ValidInt64, key string, expiry int) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaim{ RegisteredClaims: jwt.RegisteredClaims{ Issuer: "github.com/lafetz/snippitstash", @@ -46,9 +46,9 @@ func CreateJwt(userId int64, Role domain.Role, BranchId int64, key string, expir NotBefore: jwt.NewNumericDate(time.Now()), ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expiry) * time.Second)), }, - UserId: userId, - Role: Role, - BranchId: BranchId, + UserId: userId, + Role: Role, + CompanyID: CompanyID, }) jwtToken, err := token.SignedString([]byte(key)) return jwtToken, err diff --git a/internal/web_server/middleware.go b/internal/web_server/middleware.go index 057295b..5b48396 100644 --- a/internal/web_server/middleware.go +++ b/internal/web_server/middleware.go @@ -44,7 +44,7 @@ func (a *App) authMiddleware(c *fiber.Ctx) error { } c.Locals("user_id", claim.UserId) c.Locals("role", claim.Role) - c.Locals("branch_id", claim.BranchId) + c.Locals("company_id", claim.CompanyID) c.Locals("refresh_token", refreshToken) return c.Next() diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 463e7aa..a70e19b 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -7,6 +7,7 @@ import ( _ "github.com/SamuelTariku/FortuneBet-Backend/docs" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/handlers" + "github.com/gofiber/fiber/v2" fiberSwagger "github.com/swaggo/fiber-swagger" ) @@ -33,7 +34,7 @@ func (a *App) initAppRoutes() { // Auth Routes a.fiber.Post("/auth/login", h.LoginCustomer) - a.fiber.Post("/auth/refresh", a.authMiddleware, h.RefreshToken) + a.fiber.Post("/auth/refresh", h.RefreshToken) a.fiber.Post("/auth/logout", a.authMiddleware, h.LogOutCustomer) a.fiber.Get("/auth/test", a.authMiddleware, func(c *fiber.Ctx) error { userID, ok := c.Locals("user_id").(int64) @@ -68,6 +69,7 @@ func (a *App) initAppRoutes() { a.fiber.Post("/user/sendRegisterCode", h.SendRegisterCode) a.fiber.Post("/user/checkPhoneEmailExist", h.CheckPhoneEmailExist) a.fiber.Get("/user/profile", a.authMiddleware, h.UserProfile) + a.fiber.Get("/user/single/:id", a.authMiddleware, h.GetUserByID) a.fiber.Get("/user/wallet", a.authMiddleware, h.GetCustomerWallet) a.fiber.Post("/user/search", a.authMiddleware, h.SearchUserByNameOrPhone) @@ -81,15 +83,15 @@ func (a *App) initAppRoutes() { a.fiber.Get("/cashiers", a.authMiddleware, h.GetAllCashiers) a.fiber.Post("/cashiers", a.authMiddleware, h.CreateCashier) a.fiber.Put("/cashiers/:id", a.authMiddleware, h.UpdateCashier) - // + + a.fiber.Get("/admin", a.authMiddleware, h.GetAllAdmins) + a.fiber.Post("/admin", a.authMiddleware, h.CreateAdmin) a.fiber.Get("/managers", a.authMiddleware, h.GetAllManagers) a.fiber.Post("/managers", a.authMiddleware, h.CreateManager) a.fiber.Put("/managers/:id", a.authMiddleware, h.UpdateManagers) a.fiber.Get("/manager/:id/branch", a.authMiddleware, h.GetBranchByManagerID) - a.fiber.Get("/company/:id/branch", a.authMiddleware, h.GetBranchByCompanyID) - a.fiber.Get("/prematch/odds/:event_id", h.GetPrematchOdds) a.fiber.Get("/prematch/odds", h.GetALLPrematchOdds) a.fiber.Get("/prematch/odds/upcoming/:upcoming_id/market/:market_id", h.GetRawOddsByMarketID) @@ -125,6 +127,8 @@ func (a *App) initAppRoutes() { a.fiber.Get("/company/:id", a.authMiddleware, a.SuperAdminOnly, h.GetCompanyByID) a.fiber.Put("/company/:id", a.authMiddleware, a.SuperAdminOnly, h.UpdateCompany) a.fiber.Delete("/company/:id", a.authMiddleware, a.SuperAdminOnly, h.DeleteCompany) + a.fiber.Get("/company/:id/branch", a.authMiddleware, h.GetBranchByCompanyID) + a.fiber.Get("/search/company", a.authMiddleware, h.SearchCompany) // Ticket Routes a.fiber.Post("/ticket", h.CreateTicket) @@ -132,12 +136,12 @@ func (a *App) initAppRoutes() { a.fiber.Get("/ticket/:id", h.GetTicketByID) // Bet Routes - a.fiber.Post("/bet", h.CreateBet) - a.fiber.Get("/bet", h.GetAllBet) - a.fiber.Get("/bet/:id", h.GetBetByID) + a.fiber.Post("/bet", a.authMiddleware, h.CreateBet) + a.fiber.Get("/bet", a.authMiddleware, h.GetAllBet) + a.fiber.Get("/bet/:id", a.authMiddleware, h.GetBetByID) a.fiber.Get("/bet/cashout/:id", a.authMiddleware, h.GetBetByCashoutID) - a.fiber.Patch("/bet/:id", h.UpdateCashOut) - a.fiber.Delete("/bet/:id", h.DeleteBet) + a.fiber.Patch("/bet/:id", a.authMiddleware, h.UpdateCashOut) + a.fiber.Delete("/bet/:id", a.authMiddleware, h.DeleteBet) // Wallet a.fiber.Get("/wallet", h.GetAllWallets) @@ -152,10 +156,10 @@ func (a *App) initAppRoutes() { a.fiber.Post("/transfer/refill/:id", a.authMiddleware, h.RefillWallet) // Transactions /transactions - a.fiber.Post("/transaction", h.CreateTransaction) - a.fiber.Get("/transaction", h.GetAllTransactions) - a.fiber.Get("/transaction/:id", h.GetTransactionByID) - a.fiber.Patch("/transaction/:id", h.UpdateTransactionVerified) + a.fiber.Post("/transaction", a.authMiddleware, h.CreateTransaction) + a.fiber.Get("/transaction", a.authMiddleware, h.GetAllTransactions) + a.fiber.Get("/transaction/:id", a.authMiddleware, h.GetTransactionByID) + a.fiber.Patch("/transaction/:id", a.authMiddleware, h.UpdateTransactionVerified) // Notification Routes a.fiber.Get("/notifications/ws/connect/:recipientID", h.ConnectSocket) From 69c571ac5a276b067d37b6e02b9ec733c6574433 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Mon, 28 Apr 2025 20:21:53 +0300 Subject: [PATCH 2/2] fix: show company wallet --- db/migrations/000001_fortune.up.sql | 9 ++ db/query/auth.sql | 1 + db/query/company.sql | 6 +- gen/db/company.sql.go | 34 +++-- gen/db/models.go | 9 ++ internal/domain/common.go | 4 +- internal/domain/company.go | 10 ++ internal/repository/company.go | 29 +++-- internal/services/authentication/impl.go | 15 +++ internal/services/company/port.go | 6 +- internal/services/company/service.go | 6 +- internal/services/event/service.go | 120 ++++++++++-------- internal/web_server/handlers/auth_handler.go | 2 + internal/web_server/handlers/bet_handler.go | 4 +- .../web_server/handlers/company_handler.go | 30 ++++- .../web_server/handlers/ticket_handler.go | 4 +- .../handlers/transaction_handler.go | 2 +- .../web_server/handlers/transfer_handler.go | 2 +- .../web_server/handlers/wallet_handler.go | 8 +- 19 files changed, 200 insertions(+), 101 deletions(-) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 5a8c725..d40a650 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -249,6 +249,12 @@ CREATE TABLE companies ( admin_id BIGINT NOT NULL, wallet_id BIGINT NOT NULL ); +CREATE VIEW companies_with_wallets AS +SELECT companies.*, + wallets.balance, + wallets.is_active +FROM companies + JOIN wallets ON wallets.id = companies.wallet_id; ALTER TABLE refresh_tokens ADD CONSTRAINT fk_refresh_tokens_users FOREIGN KEY (user_id) REFERENCES users(id); ALTER TABLE bets @@ -277,6 +283,9 @@ ADD CONSTRAINT fk_branch_operations_operations FOREIGN KEY (operation_id) REFERE ALTER TABLE branch_cashiers ADD CONSTRAINT fk_branch_cashiers_users FOREIGN KEY (user_id) REFERENCES users(id), ADD CONSTRAINT fk_branch_cashiers_branches FOREIGN KEY (branch_id) REFERENCES branches(id); +ALTER TABLE companies +ADD CONSTRAINT fk_companies_admin FOREIGN KEY (admin_id) REFERENCES users(id), + ADD CONSTRAINT fk_companies_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id); ----------------------------------------------seed data------------------------------------------------------------- -------------------------------------- DO NOT USE IN PRODUCTION------------------------------------------------- CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/db/query/auth.sql b/db/query/auth.sql index 99366e8..0444eff 100644 --- a/db/query/auth.sql +++ b/db/query/auth.sql @@ -6,6 +6,7 @@ WHERE email = $1 -- name: CreateRefreshToken :exec INSERT INTO refresh_tokens (user_id, token, expires_at, created_at, revoked) VALUES ($1, $2, $3, $4, $5); + -- name: GetRefreshToken :one SELECT * FROM refresh_tokens diff --git a/db/query/company.sql b/db/query/company.sql index 6747a74..d9ec3a8 100644 --- a/db/query/company.sql +++ b/db/query/company.sql @@ -8,14 +8,14 @@ VALUES ($1, $2, $3) RETURNING *; -- name: GetAllCompanies :many SELECT * -FROM companies; +FROM companies_with_wallets; -- name: GetCompanyByID :one SELECT * -FROM companies +FROM companies_with_wallets WHERE id = $1; -- name: SearchCompanyByName :many SELECT * -FROM companies +FROM companies_with_wallets WHERE name ILIKE '%' || $1 || '%'; -- name: UpdateCompany :one UPDATE companies diff --git a/gen/db/company.sql.go b/gen/db/company.sql.go index d7f10e6..f9db3aa 100644 --- a/gen/db/company.sql.go +++ b/gen/db/company.sql.go @@ -50,24 +50,26 @@ func (q *Queries) DeleteCompany(ctx context.Context, id int64) error { } const GetAllCompanies = `-- name: GetAllCompanies :many -SELECT id, name, admin_id, wallet_id -FROM companies +SELECT id, name, admin_id, wallet_id, balance, is_active +FROM companies_with_wallets ` -func (q *Queries) GetAllCompanies(ctx context.Context) ([]Company, error) { +func (q *Queries) GetAllCompanies(ctx context.Context) ([]CompaniesWithWallet, error) { rows, err := q.db.Query(ctx, GetAllCompanies) if err != nil { return nil, err } defer rows.Close() - var items []Company + var items []CompaniesWithWallet for rows.Next() { - var i Company + var i CompaniesWithWallet if err := rows.Scan( &i.ID, &i.Name, &i.AdminID, &i.WalletID, + &i.Balance, + &i.IsActive, ); err != nil { return nil, err } @@ -80,43 +82,47 @@ func (q *Queries) GetAllCompanies(ctx context.Context) ([]Company, error) { } const GetCompanyByID = `-- name: GetCompanyByID :one -SELECT id, name, admin_id, wallet_id -FROM companies +SELECT id, name, admin_id, wallet_id, balance, is_active +FROM companies_with_wallets WHERE id = $1 ` -func (q *Queries) GetCompanyByID(ctx context.Context, id int64) (Company, error) { +func (q *Queries) GetCompanyByID(ctx context.Context, id int64) (CompaniesWithWallet, error) { row := q.db.QueryRow(ctx, GetCompanyByID, id) - var i Company + var i CompaniesWithWallet err := row.Scan( &i.ID, &i.Name, &i.AdminID, &i.WalletID, + &i.Balance, + &i.IsActive, ) return i, err } const SearchCompanyByName = `-- name: SearchCompanyByName :many -SELECT id, name, admin_id, wallet_id -FROM companies +SELECT id, name, admin_id, wallet_id, balance, is_active +FROM companies_with_wallets WHERE name ILIKE '%' || $1 || '%' ` -func (q *Queries) SearchCompanyByName(ctx context.Context, dollar_1 pgtype.Text) ([]Company, error) { +func (q *Queries) SearchCompanyByName(ctx context.Context, dollar_1 pgtype.Text) ([]CompaniesWithWallet, error) { rows, err := q.db.Query(ctx, SearchCompanyByName, dollar_1) if err != nil { return nil, err } defer rows.Close() - var items []Company + var items []CompaniesWithWallet for rows.Next() { - var i Company + var i CompaniesWithWallet if err := rows.Scan( &i.ID, &i.Name, &i.AdminID, &i.WalletID, + &i.Balance, + &i.IsActive, ); err != nil { return nil, err } diff --git a/gen/db/models.go b/gen/db/models.go index 987b1ae..bf0b188 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -145,6 +145,15 @@ type BranchOperation struct { UpdatedAt pgtype.Timestamp `json:"updated_at"` } +type CompaniesWithWallet struct { + ID int64 `json:"id"` + Name string `json:"name"` + AdminID int64 `json:"admin_id"` + WalletID int64 `json:"wallet_id"` + Balance int64 `json:"balance"` + IsActive bool `json:"is_active"` +} + type Company struct { ID int64 `json:"id"` Name string `json:"name"` diff --git a/internal/domain/common.go b/internal/domain/common.go index e3a5e52..9ae3ad3 100644 --- a/internal/domain/common.go +++ b/internal/domain/common.go @@ -23,8 +23,8 @@ func ToCurrency(f float32) Currency { return Currency((f * 100) + 0.5) } -// Float64 converts a Currency to float32 -func (m Currency) Float64() float32 { +// Float32 converts a Currency to float32 +func (m Currency) Float32() float32 { x := float32(m) x = x / 100 return x diff --git a/internal/domain/company.go b/internal/domain/company.go index 989f306..60d738a 100644 --- a/internal/domain/company.go +++ b/internal/domain/company.go @@ -9,6 +9,16 @@ type Company struct { AdminID int64 WalletID int64 } + +type GetCompany struct { + ID int64 + Name string + AdminID int64 + WalletID int64 + WalletBalance Currency + IsWalletActive bool +} + type CreateCompany struct { Name string AdminID int64 diff --git a/internal/repository/company.go b/internal/repository/company.go index d05a6f0..0ed64ac 100644 --- a/internal/repository/company.go +++ b/internal/repository/company.go @@ -25,6 +25,17 @@ func convertDBCompany(dbCompany dbgen.Company) domain.Company { } } +func convertDBCompanyWithWallet(dbCompany dbgen.CompaniesWithWallet) domain.GetCompany { + return domain.GetCompany{ + ID: dbCompany.ID, + Name: dbCompany.Name, + AdminID: dbCompany.AdminID, + WalletID: dbCompany.WalletID, + WalletBalance: domain.Currency(dbCompany.Balance), + IsWalletActive: dbCompany.IsActive, + } +} + func (s *Store) CreateCompany(ctx context.Context, company domain.CreateCompany) (domain.Company, error) { dbCompany, err := s.queries.CreateCompany(ctx, convertCreateCompany(company)) if err != nil { @@ -33,21 +44,21 @@ func (s *Store) CreateCompany(ctx context.Context, company domain.CreateCompany) return convertDBCompany(dbCompany), nil } -func (s *Store) GetAllCompanies(ctx context.Context) ([]domain.Company, error) { +func (s *Store) GetAllCompanies(ctx context.Context) ([]domain.GetCompany, error) { dbCompanies, err := s.queries.GetAllCompanies(ctx) if err != nil { return nil, err } - var companies []domain.Company = make([]domain.Company, 0, len(dbCompanies)) + var companies []domain.GetCompany = make([]domain.GetCompany, 0, len(dbCompanies)) for _, dbCompany := range dbCompanies { - companies = append(companies, convertDBCompany(dbCompany)) + companies = append(companies, convertDBCompanyWithWallet(dbCompany)) } return companies, nil } -func (s *Store) SearchCompanyByName(ctx context.Context, name string) ([]domain.Company, error) { +func (s *Store) SearchCompanyByName(ctx context.Context, name string) ([]domain.GetCompany, error) { dbCompanies, err := s.queries.SearchCompanyByName(ctx, pgtype.Text{ String: name, Valid: true, @@ -56,21 +67,21 @@ func (s *Store) SearchCompanyByName(ctx context.Context, name string) ([]domain. if err != nil { return nil, err } - var companies []domain.Company = make([]domain.Company, 0, len(dbCompanies)) + var companies []domain.GetCompany = make([]domain.GetCompany, 0, len(dbCompanies)) for _, dbCompany := range dbCompanies { - companies = append(companies, convertDBCompany(dbCompany)) + companies = append(companies, convertDBCompanyWithWallet(dbCompany)) } return companies, nil } -func (s *Store) GetCompanyByID(ctx context.Context, id int64) (domain.Company, error) { +func (s *Store) GetCompanyByID(ctx context.Context, id int64) (domain.GetCompany, error) { dbCompany, err := s.queries.GetCompanyByID(ctx, id) if err != nil { - return domain.Company{}, err + return domain.GetCompany{}, err } - return convertDBCompany(dbCompany), nil + return convertDBCompanyWithWallet(dbCompany), nil } func (s *Store) UpdateCompany(ctx context.Context, id int64, company domain.UpdateCompany) (domain.Company, error) { diff --git a/internal/services/authentication/impl.go b/internal/services/authentication/impl.go index 7540047..e83fc7c 100644 --- a/internal/services/authentication/impl.go +++ b/internal/services/authentication/impl.go @@ -39,6 +39,20 @@ func (s *Service) Login(ctx context.Context, email, phone string, password strin return LoginSuccess{}, ErrUserSuspended } + oldRefreshToken, err := s.tokenStore.GetRefreshTokenByUserID(ctx, user.ID) + + if err != nil && err != ErrRefreshTokenNotFound { + return LoginSuccess{}, err + } + + // If old refresh token is not revoked, revoke it + if err == nil && !oldRefreshToken.Revoked { + err = s.tokenStore.RevokeRefreshToken(ctx, oldRefreshToken.Token) + if(err != nil) { + return LoginSuccess{}, err + } + } + refreshToken, err := generateRefreshToken() if err != nil { return LoginSuccess{}, err @@ -49,6 +63,7 @@ func (s *Service) Login(ctx context.Context, email, phone string, password strin CreatedAt: time.Now(), ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second), }) + if err != nil { return LoginSuccess{}, err } diff --git a/internal/services/company/port.go b/internal/services/company/port.go index a56e8a2..ad8fb74 100644 --- a/internal/services/company/port.go +++ b/internal/services/company/port.go @@ -8,9 +8,9 @@ import ( type CompanyStore interface { CreateCompany(ctx context.Context, company domain.CreateCompany) (domain.Company, error) - GetAllCompanies(ctx context.Context) ([]domain.Company, error) - GetCompanyByID(ctx context.Context, id int64) (domain.Company, error) - SearchCompanyByName(ctx context.Context, name string) ([]domain.Company, error) + GetAllCompanies(ctx context.Context) ([]domain.GetCompany, error) + SearchCompanyByName(ctx context.Context, name string) ([]domain.GetCompany, error) + GetCompanyByID(ctx context.Context, id int64) (domain.GetCompany, error) UpdateCompany(ctx context.Context, id int64, company domain.UpdateCompany) (domain.Company, error) DeleteCompany(ctx context.Context, id int64) error } diff --git a/internal/services/company/service.go b/internal/services/company/service.go index d6fa9a0..affc432 100644 --- a/internal/services/company/service.go +++ b/internal/services/company/service.go @@ -19,15 +19,15 @@ func NewService(companyStore CompanyStore) *Service { func (s *Service) CreateCompany(ctx context.Context, company domain.CreateCompany) (domain.Company, error) { return s.companyStore.CreateCompany(ctx, company) } -func (s *Service) GetAllCompanies(ctx context.Context) ([]domain.Company, error) { +func (s *Service) GetAllCompanies(ctx context.Context) ([]domain.GetCompany, error) { return s.companyStore.GetAllCompanies(ctx) } -func (s *Service) GetCompanyByID(ctx context.Context, id int64) (domain.Company, error) { +func (s *Service) GetCompanyByID(ctx context.Context, id int64) (domain.GetCompany, error) { return s.companyStore.GetCompanyByID(ctx, id) } -func (s *Service) SearchCompanyByName(ctx context.Context, name string) ([]domain.Company, error) { +func (s *Service) SearchCompanyByName(ctx context.Context, name string) ([]domain.GetCompany, error) { return s.companyStore.SearchCompanyByName(ctx, name) } diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 4eb5601..57dc930 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "log" "net/http" "strconv" "sync" @@ -98,63 +99,78 @@ func (s *service) FetchLiveEvents(ctx context.Context) error { func (s *service) FetchUpcomingEvents(ctx context.Context) error { sportIDs := []int{1} + var totalPages int = 1 + var page int = 0 + // var limit int = 5 + // var count int = 0 for _, sportID := range sportIDs { - url := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s", sportID, s.token) - resp, err := http.Get(url) - if err != nil { - continue - } - defer resp.Body.Close() + for page != totalPages { + page = page + 1 + url := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s&page=%d", sportID, s.token, page) + log.Printf("📡 Fetching data for event data page %d", page) + resp, err := http.Get(url) + if err != nil { + log.Printf("❌ Failed to fetch event data for page %d: %v", page, err) + continue + } + defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - var data struct { - Success int `json:"success"` - Results []struct { - ID string `json:"id"` - SportID string `json:"sport_id"` - Time string `json:"time"` - League struct { - ID string `json:"id"` - Name string `json:"name"` - } `json:"league"` - Home struct { - ID string `json:"id"` - Name string `json:"name"` - } `json:"home"` - Away *struct { - ID string `json:"id"` - Name string `json:"name"` - } `json:"away"` - } `json:"results"` - } - if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { - continue - } - - for _, ev := range data.Results { - startUnix, _ := strconv.ParseInt(ev.Time, 10, 64) - event := domain.UpcomingEvent{ - ID: ev.ID, - SportID: ev.SportID, - MatchName: ev.Home.Name, - HomeTeam: ev.Home.Name, - AwayTeam: "", // handle nil safely - HomeTeamID: ev.Home.ID, - AwayTeamID: "", - HomeKitImage: "", - AwayKitImage: "", - LeagueID: ev.League.ID, - LeagueName: ev.League.Name, - LeagueCC: "", - StartTime: time.Unix(startUnix, 0).UTC(), + body, _ := io.ReadAll(resp.Body) + var data struct { + Success int `json:"success"` + Pager struct { + Page int `json:"page"` + PerPage int `json:"per_page"` + Total int `json:"total"` + } + Results []struct { + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + League struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"league"` + Home struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"home"` + Away *struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"away"` + } `json:"results"` + } + if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { + continue } - if ev.Away != nil { - event.AwayTeam = ev.Away.Name - event.AwayTeamID = ev.Away.ID - } + for _, ev := range data.Results { + startUnix, _ := strconv.ParseInt(ev.Time, 10, 64) + event := domain.UpcomingEvent{ + ID: ev.ID, + SportID: ev.SportID, + MatchName: ev.Home.Name, + HomeTeam: ev.Home.Name, + AwayTeam: "", // handle nil safely + HomeTeamID: ev.Home.ID, + AwayTeamID: "", + HomeKitImage: "", + AwayKitImage: "", + LeagueID: ev.League.ID, + LeagueName: ev.League.Name, + LeagueCC: "", + StartTime: time.Unix(startUnix, 0).UTC(), + } - _ = s.store.SaveUpcomingEvent(ctx, event) + if ev.Away != nil { + event.AwayTeam = ev.Away.Name + event.AwayTeamID = ev.Away.ID + } + + _ = s.store.SaveUpcomingEvent(ctx, event) + } + totalPages = data.Pager.Total } } diff --git a/internal/web_server/handlers/auth_handler.go b/internal/web_server/handlers/auth_handler.go index b117333..7e1fdef 100644 --- a/internal/web_server/handlers/auth_handler.go +++ b/internal/web_server/handlers/auth_handler.go @@ -94,6 +94,7 @@ func (h *Handler) RefreshToken(c *fiber.Ctx) error { type loginCustomerRes struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` + Role string `json:"role"` } var req refreshToken @@ -132,6 +133,7 @@ func (h *Handler) RefreshToken(c *fiber.Ctx) error { res := loginCustomerRes{ AccessToken: accessToken, RefreshToken: req.RefreshToken, + Role: string(user.Role), } return response.WriteJSON(c, fiber.StatusOK, "Refresh successful", res, nil) } diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index f638cd6..a3c5df1 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -55,7 +55,7 @@ type BetRes struct { func convertCreateBet(bet domain.Bet, createdNumber int64) CreateBetRes { return CreateBetRes{ ID: bet.ID, - Amount: bet.Amount.Float64(), + Amount: bet.Amount.Float32(), TotalOdds: bet.TotalOdds, Status: bet.Status, FullName: bet.FullName, @@ -70,7 +70,7 @@ func convertCreateBet(bet domain.Bet, createdNumber int64) CreateBetRes { func convertBet(bet domain.GetBet) BetRes { return BetRes{ ID: bet.ID, - Amount: bet.Amount.Float64(), + Amount: bet.Amount.Float32(), TotalOdds: bet.TotalOdds, Status: bet.Status, FullName: bet.FullName, diff --git a/internal/web_server/handlers/company_handler.go b/internal/web_server/handlers/company_handler.go index f8cb663..863cbd7 100644 --- a/internal/web_server/handlers/company_handler.go +++ b/internal/web_server/handlers/company_handler.go @@ -20,6 +20,15 @@ type CompanyRes struct { WalletID int64 `json:"wallet_id" example:"1"` } +type GetCompanyRes struct { + ID int64 `json:"id" example:"1"` + Name string `json:"name" example:"CompanyName"` + AdminID int64 `json:"admin_id" example:"1"` + WalletID int64 `json:"wallet_id" example:"1"` + WalletBalance float32 `json:"balance" example:"1"` + IsActive bool `json:"is_active" example:"false"` +} + func convertCompany(company domain.Company) CompanyRes { return CompanyRes{ ID: company.ID, @@ -29,6 +38,17 @@ func convertCompany(company domain.Company) CompanyRes { } } +func convertGetCompany(company domain.GetCompany) GetCompanyRes { + return GetCompanyRes{ + ID: company.ID, + Name: company.Name, + AdminID: company.AdminID, + WalletID: company.WalletID, + WalletBalance: company.WalletBalance.Float32(), + IsActive: company.IsWalletActive, + } +} + // CreateCompany godoc // @Summary Create a company // @Description Creates a company @@ -100,10 +120,10 @@ func (h *Handler) GetAllCompanies(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get companies", err, nil) } - var result []CompanyRes = make([]CompanyRes, 0, len(companies)) + var result []GetCompanyRes = make([]GetCompanyRes, 0, len(companies)) for _, company := range companies { - result = append(result, convertCompany(company)) + result = append(result, convertGetCompany(company)) } return response.WriteJSON(c, fiber.StatusOK, "All Companies retrieved", result, nil) @@ -137,7 +157,7 @@ func (h *Handler) GetCompanyByID(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to company branch", err, nil) } - res := convertCompany(company) + res := convertGetCompany(company) return response.WriteJSON(c, fiber.StatusOK, "Company retrieved successfully", res, nil) @@ -164,10 +184,10 @@ func (h *Handler) SearchCompany(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get companies", err, nil) } - var result []CompanyRes = make([]CompanyRes, 0, len(companies)) + var result []GetCompanyRes = make([]GetCompanyRes, 0, len(companies)) for _, company := range companies { - result = append(result, convertCompany(company)) + result = append(result, convertGetCompany(company)) } return response.WriteJSON(c, fiber.StatusOK, "All Companies retrieved", result, nil) diff --git a/internal/web_server/handlers/ticket_handler.go b/internal/web_server/handlers/ticket_handler.go index 84bf4a1..f12ca8a 100644 --- a/internal/web_server/handlers/ticket_handler.go +++ b/internal/web_server/handlers/ticket_handler.go @@ -190,7 +190,7 @@ func (h *Handler) GetTicketByID(c *fiber.Ctx) error { res := TicketRes{ ID: ticket.ID, Outcomes: ticket.Outcomes, - Amount: ticket.Amount.Float64(), + Amount: ticket.Amount.Float32(), TotalOdds: ticket.TotalOdds, } return response.WriteJSON(c, fiber.StatusOK, "Ticket retrieved successfully", res, nil) @@ -219,7 +219,7 @@ func (h *Handler) GetAllTickets(c *fiber.Ctx) error { res[i] = TicketRes{ ID: ticket.ID, Outcomes: ticket.Outcomes, - Amount: ticket.Amount.Float64(), + Amount: ticket.Amount.Float32(), TotalOdds: ticket.TotalOdds, } } diff --git a/internal/web_server/handlers/transaction_handler.go b/internal/web_server/handlers/transaction_handler.go index c0f7eb7..99c323b 100644 --- a/internal/web_server/handlers/transaction_handler.go +++ b/internal/web_server/handlers/transaction_handler.go @@ -45,7 +45,7 @@ type CreateTransactionReq struct { func convertTransaction(transaction domain.Transaction) TransactionRes { return TransactionRes{ ID: transaction.ID, - Amount: transaction.Amount.Float64(), + Amount: transaction.Amount.Float32(), BranchID: transaction.BranchID, CashierID: transaction.CashierID, BetID: transaction.BetID, diff --git a/internal/web_server/handlers/transfer_handler.go b/internal/web_server/handlers/transfer_handler.go index a29fd2a..7cff933 100644 --- a/internal/web_server/handlers/transfer_handler.go +++ b/internal/web_server/handlers/transfer_handler.go @@ -47,7 +47,7 @@ func convertTransfer(transfer domain.Transfer) TransferWalletRes { return TransferWalletRes{ ID: transfer.ID, - Amount: transfer.Amount.Float64(), + Amount: transfer.Amount.Float32(), Verified: transfer.Verified, Type: string(transfer.Type), PaymentMethod: string(transfer.PaymentMethod), diff --git a/internal/web_server/handlers/wallet_handler.go b/internal/web_server/handlers/wallet_handler.go index d04d651..20e3288 100644 --- a/internal/web_server/handlers/wallet_handler.go +++ b/internal/web_server/handlers/wallet_handler.go @@ -24,7 +24,7 @@ type WalletRes struct { func convertWallet(wallet domain.Wallet) WalletRes { return WalletRes{ ID: wallet.ID, - Balance: wallet.Balance.Float64(), + Balance: wallet.Balance.Float32(), IsWithdraw: wallet.IsWithdraw, IsBettable: wallet.IsBettable, IsTransferable: wallet.IsTransferable, @@ -52,9 +52,9 @@ func convertCustomerWallet(wallet domain.GetCustomerWallet) CustomerWalletRes { return CustomerWalletRes{ ID: wallet.ID, RegularID: wallet.RegularID, - RegularBalance: wallet.RegularBalance.Float64(), + RegularBalance: wallet.RegularBalance.Float32(), StaticID: wallet.StaticID, - StaticBalance: wallet.StaticBalance.Float64(), + StaticBalance: wallet.StaticBalance.Float32(), CustomerID: wallet.CustomerID, CompanyID: wallet.CompanyID, RegularUpdatedAt: wallet.RegularUpdatedAt, @@ -159,7 +159,7 @@ func (h *Handler) GetAllBranchWallets(c *fiber.Ctx) error { for _, wallet := range wallets { res = append(res, BranchWalletRes{ ID: wallet.ID, - Balance: wallet.Balance.Float64(), + Balance: wallet.Balance.Float32(), IsActive: wallet.IsActive, Name: wallet.Name, Location: wallet.Location,