diff --git a/cmd/main.go b/cmd/main.go index a3768bf..00bc4a3 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -35,6 +35,8 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions" + issuereporting "github.com/SamuelTariku/FortuneBet-Backend/internal/services/issue_reporting" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" @@ -164,6 +166,8 @@ func main() { go httpserver.SetupReportCronJobs(context.Background(), reportSvc) + bankRepository := repository.NewBankRepository(store) + instSvc := institutions.New(bankRepository) // Initialize report worker with CSV exporter // csvExporter := infrastructure.CSVExporter{ // ExportPath: cfg.ReportExportPath, // Make sure to add this to your config @@ -198,10 +202,35 @@ func main() { httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc) httpserver.StartTicketCrons(*ticketSvc) + + // Fetch companies and branches for live wallet metrics update + ctx := context.Background() + + companies := []domain.GetCompany{ + {ID: 1, Name: "Company A", WalletBalance: 1000.0}, + } + + branches := []domain.BranchWallet{ + {ID: 10, Name: "Branch Z", CompanyID: 1, Balance: 500.0}, + } + + notificationSvc.UpdateLiveWalletMetrics(ctx, companies, branches) + if err != nil { + log.Println("Failed to update live metrics:", err) + } else { + log.Println("Live metrics broadcasted successfully") + } + + issueReportingRepo := repository.NewReportedIssueRepository(store) + + issueReportingSvc := issuereporting.New(issueReportingRepo) + // go httpserver.SetupReportCronJob(reportWorker) // Initialize and start HTTP server app := httpserver.NewApp( + issueReportingSvc, + instSvc, currSvc, cfg.Port, v, diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 699ad8a..6ed5000 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -112,6 +112,23 @@ CREATE TABLE IF NOT EXISTS ticket_outcomes ( status INT NOT NULL DEFAULT 0, expires TIMESTAMP NOT NULL ); +CREATE TABLE IF NOT EXISTS banks ( + id BIGSERIAL PRIMARY KEY, + slug VARCHAR(255) NOT NULL UNIQUE, + swift VARCHAR(20) NOT NULL, + name VARCHAR(255) NOT NULL, + acct_length INT NOT NULL, + country_id INT NOT NULL, + is_mobilemoney INT, -- nullable integer (0 or 1) + is_active INT NOT NULL, -- 0 or 1 + is_rtgs INT NOT NULL, -- 0 or 1 + active INT NOT NULL, -- 0 or 1 + is_24hrs INT, -- nullable integer (0 or 1) + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + currency VARCHAR(10) NOT NULL, + bank_logo TEXT -- URL or base64 string +); CREATE TABLE IF NOT EXISTS wallets ( id BIGSERIAL PRIMARY KEY, balance BIGINT NOT NULL DEFAULT 0, diff --git a/db/migrations/000004_virtual_game_Sessios.up.sql b/db/migrations/000004_virtual_game_Sessios.up.sql index 8ce89d0..fe47bac 100644 --- a/db/migrations/000004_virtual_game_Sessios.up.sql +++ b/db/migrations/000004_virtual_game_Sessios.up.sql @@ -30,6 +30,9 @@ CREATE TABLE virtual_game_transactions ( id BIGSERIAL PRIMARY KEY, session_id BIGINT NOT NULL REFERENCES virtual_game_sessions(id), user_id BIGINT NOT NULL REFERENCES users(id), + company_id BIGINT, + provider VARCHAR(100), + game_id VARCHAR(100), wallet_id BIGINT NOT NULL REFERENCES wallets(id), transaction_type VARCHAR(20) NOT NULL, amount BIGINT NOT NULL, @@ -44,6 +47,8 @@ CREATE TABLE virtual_game_histories ( id BIGSERIAL PRIMARY KEY, session_id VARCHAR(100), -- nullable user_id BIGINT NOT NULL, + company_id BIGINT, + provider VARCHAR(100), wallet_id BIGINT, -- nullable game_id BIGINT, -- nullable transaction_type VARCHAR(20) NOT NULL, -- e.g., BET, WIN, CANCEL @@ -56,6 +61,13 @@ CREATE TABLE virtual_game_histories ( updated_at TIMESTAMP NOT NULL DEFAULT NOW() ); +CREATE TABLE IF NOT EXISTS favorite_games ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + game_id BIGINT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + -- Optional: Indexes for performance CREATE INDEX idx_virtual_game_user_id ON virtual_game_histories(user_id); CREATE INDEX idx_virtual_game_transaction_type ON virtual_game_histories(transaction_type); @@ -65,3 +77,7 @@ CREATE INDEX idx_virtual_game_external_transaction_id ON virtual_game_histories( CREATE INDEX idx_virtual_game_sessions_user_id ON virtual_game_sessions(user_id); CREATE INDEX idx_virtual_game_transactions_session_id ON virtual_game_transactions(session_id); CREATE INDEX idx_virtual_game_transactions_user_id ON virtual_game_transactions(user_id); + +ALTER TABLE favorite_games +ADD CONSTRAINT unique_user_game_favorite UNIQUE (user_id, game_id); + diff --git a/db/migrations/000008_issue_reporting.down.sql b/db/migrations/000008_issue_reporting.down.sql new file mode 100644 index 0000000..59d3f24 --- /dev/null +++ b/db/migrations/000008_issue_reporting.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS reported_issues; + diff --git a/db/migrations/000008_issue_reporting.up.sql b/db/migrations/000008_issue_reporting.up.sql new file mode 100644 index 0000000..53ad252 --- /dev/null +++ b/db/migrations/000008_issue_reporting.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS reported_issues ( + id BIGSERIAL PRIMARY KEY, + customer_id BIGINT NOT NULL, + subject TEXT NOT NULL, + description TEXT NOT NULL, + issue_type TEXT NOT NULL, -- e.g., "deposit", "withdrawal", "bet", "technical" + status TEXT NOT NULL DEFAULT 'pending', -- pending, in_progress, resolved, rejected + metadata JSONB, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + diff --git a/db/query/institutions.sql b/db/query/institutions.sql new file mode 100644 index 0000000..d6faada --- /dev/null +++ b/db/query/institutions.sql @@ -0,0 +1,60 @@ +-- name: CreateBank :one +INSERT INTO banks ( + slug, + swift, + name, + acct_length, + country_id, + is_mobilemoney, + is_active, + is_rtgs, + active, + is_24hrs, + created_at, + updated_at, + currency, + bank_logo +) +VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, $11, $12 +) +RETURNING *; + +-- name: GetBankByID :one +SELECT * +FROM banks +WHERE id = $1; + +-- name: GetAllBanks :many +SELECT * +FROM banks +WHERE ( + country_id = sqlc.narg('country_id') + OR sqlc.narg('country_id') IS NULL + ) + AND ( + is_active = sqlc.narg('is_active') + OR sqlc.narg('is_active') IS NULL + ); + +-- name: UpdateBank :one +UPDATE banks +SET slug = COALESCE(sqlc.narg(slug), slug), + swift = COALESCE(sqlc.narg(swift), swift), + name = COALESCE(sqlc.narg(name), name), + acct_length = COALESCE(sqlc.narg(acct_length), acct_length), + country_id = COALESCE(sqlc.narg(country_id), country_id), + is_mobilemoney = COALESCE(sqlc.narg(is_mobilemoney), is_mobilemoney), + is_active = COALESCE(sqlc.narg(is_active), is_active), + is_rtgs = COALESCE(sqlc.narg(is_rtgs), is_rtgs), + active = COALESCE(sqlc.narg(active), active), + is_24hrs = COALESCE(sqlc.narg(is_24hrs), is_24hrs), + updated_at = CURRENT_TIMESTAMP, + currency = COALESCE(sqlc.narg(currency), currency), + bank_logo = COALESCE(sqlc.narg(bank_logo), bank_logo) +WHERE id = $1 +RETURNING *; + +-- name: DeleteBank :exec +DELETE FROM banks +WHERE id = $1; diff --git a/db/query/issue_reporting.sql b/db/query/issue_reporting.sql new file mode 100644 index 0000000..31ea229 --- /dev/null +++ b/db/query/issue_reporting.sql @@ -0,0 +1,32 @@ +-- name: CreateReportedIssue :one +INSERT INTO reported_issues ( + customer_id, subject, description, issue_type, metadata +) VALUES ( + $1, $2, $3, $4, $5 +) +RETURNING *; + +-- name: ListReportedIssues :many +SELECT * FROM reported_issues +ORDER BY created_at DESC +LIMIT $1 OFFSET $2; + +-- name: ListReportedIssuesByCustomer :many +SELECT * FROM reported_issues +WHERE customer_id = $1 +ORDER BY created_at DESC +LIMIT $2 OFFSET $3; + +-- name: CountReportedIssues :one +SELECT COUNT(*) FROM reported_issues; + +-- name: CountReportedIssuesByCustomer :one +SELECT COUNT(*) FROM reported_issues WHERE customer_id = $1; + +-- name: UpdateReportedIssueStatus :exec +UPDATE reported_issues +SET status = $2, updated_at = NOW() +WHERE id = $1; + +-- name: DeleteReportedIssue :exec +DELETE FROM reported_issues WHERE id = $1; diff --git a/db/query/report.sql b/db/query/report.sql index 7689643..24677c1 100644 --- a/db/query/report.sql +++ b/db/query/report.sql @@ -1,34 +1,44 @@ -- name: GetTotalBetsMadeInRange :one SELECT COUNT(*) AS total_bets FROM bets -WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to') - AND ( - company_id = sqlc.narg('company_id') - OR sqlc.narg('company_id') IS NULL -); +WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to'); -- name: GetTotalCashMadeInRange :one SELECT COALESCE(SUM(amount), 0) AS total_cash_made FROM bets -WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to') - AND ( - company_id = sqlc.narg('company_id') - OR sqlc.narg('company_id') IS NULL -); +WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to'); -- name: GetTotalCashOutInRange :one SELECT COALESCE(SUM(amount), 0) AS total_cash_out FROM bets WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to') - AND cashed_out = true - AND ( - company_id = sqlc.narg('company_id') - OR sqlc.narg('company_id') IS NULL -); + AND cashed_out = true; -- name: GetTotalCashBacksInRange :one SELECT COALESCE(SUM(amount), 0) AS total_cash_backs FROM bets WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to') - AND status = 5 - AND ( - company_id = sqlc.narg('company_id') - OR sqlc.narg('company_id') IS NULL -); + AND status = 5; +-- name: GetCompanyWiseReport :many +SELECT + b.company_id, + c.name AS company_name, + COUNT(*) AS total_bets, + COALESCE(SUM(b.amount), 0) AS total_cash_made, + COALESCE(SUM(CASE WHEN b.cashed_out THEN b.amount ELSE 0 END), 0) AS total_cash_out, + COALESCE(SUM(CASE WHEN b.status = 5 THEN b.amount ELSE 0 END), 0) AS total_cash_backs +FROM bets b +JOIN companies c ON b.company_id = c.id +WHERE b.created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to') +GROUP BY b.company_id, c.name; +-- name: GetBranchWiseReport :many +SELECT + b.branch_id, + br.name AS branch_name, + br.company_id, + COUNT(*) AS total_bets, + COALESCE(SUM(b.amount), 0) AS total_cash_made, + COALESCE(SUM(CASE WHEN b.cashed_out THEN b.amount ELSE 0 END), 0) AS total_cash_out, + COALESCE(SUM(CASE WHEN b.status = 5 THEN b.amount ELSE 0 END), 0) AS total_cash_backs +FROM bets b +JOIN branches br ON b.branch_id = br.id +WHERE b.created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to') +GROUP BY b.branch_id, br.name, br.company_id; + diff --git a/db/query/virtual_games.sql b/db/query/virtual_games.sql index 799259d..68f2fca 100644 --- a/db/query/virtual_games.sql +++ b/db/query/virtual_games.sql @@ -4,28 +4,26 @@ INSERT INTO virtual_game_sessions ( ) VALUES ( $1, $2, $3, $4, $5, $6 ) RETURNING id, user_id, game_id, session_token, currency, status, created_at, updated_at, expires_at; - -- name: GetVirtualGameSessionByToken :one SELECT id, user_id, game_id, session_token, currency, status, created_at, updated_at, expires_at FROM virtual_game_sessions WHERE session_token = $1; - -- name: UpdateVirtualGameSessionStatus :exec UPDATE virtual_game_sessions SET status = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $1; - -- name: CreateVirtualGameTransaction :one INSERT INTO virtual_game_transactions ( - session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status + session_id, user_id, company_id, provider, wallet_id, transaction_type, amount, currency, external_transaction_id, status ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8 -) RETURNING id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at; - + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 +) RETURNING id, session_id, user_id, company_id, provider, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at; -- name: CreateVirtualGameHistory :one INSERT INTO virtual_game_histories ( session_id, user_id, + company_id, + provider, wallet_id, game_id, transaction_type, @@ -35,11 +33,13 @@ INSERT INTO virtual_game_histories ( reference_transaction_id, status ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 ) RETURNING id, session_id, user_id, + company_id, + provider, wallet_id, game_id, transaction_type, @@ -50,25 +50,39 @@ INSERT INTO virtual_game_histories ( status, created_at, updated_at; - - -- name: GetVirtualGameTransactionByExternalID :one SELECT id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at FROM virtual_game_transactions WHERE external_transaction_id = $1; - -- name: UpdateVirtualGameTransactionStatus :exec UPDATE virtual_game_transactions SET status = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $1; - -- name: GetVirtualGameSummaryInRange :many SELECT + c.name AS company_name, vg.name AS game_name, - COUNT(vgh.id) AS number_of_bets, - COALESCE(SUM(vgh.amount), 0) AS total_transaction_sum -FROM virtual_game_histories vgh -JOIN virtual_games vg ON vgh.game_id = vg.id -WHERE vgh.transaction_type = 'BET' - AND vgh.created_at BETWEEN $1 AND $2 -GROUP BY vg.name; + COUNT(vgt.id) AS number_of_bets, + COALESCE(SUM(vgt.amount), 0) AS total_transaction_sum +FROM virtual_game_transactions vgt +JOIN virtual_game_sessions vgs ON vgt.session_id = vgs.id +JOIN virtual_games vg ON vgs.game_id = vg.id +JOIN companies c ON vgt.company_id = c.id +WHERE vgt.transaction_type = 'BET' + AND vgt.created_at BETWEEN $1 AND $2 +GROUP BY c.name, vg.name; +-- name: AddFavoriteGame :exec +INSERT INTO favorite_games ( + user_id, + game_id, + created_at +) VALUES ($1, $2, NOW()) +ON CONFLICT (user_id, game_id) DO NOTHING; +-- name: RemoveFavoriteGame :exec +DELETE FROM favorite_games +WHERE user_id = $1 AND game_id = $2; +-- name: ListFavoriteGames :many +SELECT game_id +FROM favorite_games +WHERE user_id = $1; + diff --git a/db/query/wallet.sql b/db/query/wallet.sql index a5f8482..79028e5 100644 --- a/db/query/wallet.sql +++ b/db/query/wallet.sql @@ -55,4 +55,14 @@ WHERE id = $2; UPDATE wallets SET is_active = $1, updated_at = CURRENT_TIMESTAMP -WHERE id = $2; \ No newline at end of file +WHERE id = $2; +-- name: GetCompanyByWalletID :one +SELECT id, name, admin_id, wallet_id +FROM companies +WHERE wallet_id = $1 +LIMIT 1; +-- name: GetBranchByWalletID :one +SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at +FROM branches +WHERE wallet_id = $1 +LIMIT 1; \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 754c307..ec52242 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -304,6 +304,217 @@ const docTemplate = `{ } } }, + "/api/v1/banks": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Institutions - Banks" + ], + "summary": "List all banks", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Bank" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Institutions - Banks" + ], + "summary": "Create a new bank", + "parameters": [ + { + "description": "Bank Info", + "name": "bank", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.Bank" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/domain.Bank" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/banks/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Institutions - Banks" + ], + "summary": "Get a bank by ID", + "parameters": [ + { + "type": "integer", + "description": "Bank ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Bank" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Institutions - Banks" + ], + "summary": "Update a bank", + "parameters": [ + { + "type": "integer", + "description": "Bank ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Bank Info", + "name": "bank", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.Bank" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Bank" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Institutions - Banks" + ], + "summary": "Delete a bank", + "parameters": [ + { + "type": "integer", + "description": "Bank ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "Deleted successfully", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/chapa/banks": { "get": { "description": "Get list of banks supported by Chapa", @@ -634,6 +845,230 @@ const docTemplate = `{ } } }, + "/api/v1/issues": { + "get": { + "description": "Admin endpoint to list all reported issues with pagination", + "produces": [ + "application/json" + ], + "tags": [ + "Issues" + ], + "summary": "Get all reported issues", + "parameters": [ + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ReportedIssue" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "post": { + "description": "Allows a customer to report a new issue related to the betting platform", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Issues" + ], + "summary": "Report an issue", + "parameters": [ + { + "description": "Issue to report", + "name": "issue", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.ReportedIssue" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/domain.ReportedIssue" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/issues/customer/{customer_id}": { + "get": { + "description": "Returns all issues reported by a specific customer", + "produces": [ + "application/json" + ], + "tags": [ + "Issues" + ], + "summary": "Get reported issues by a customer", + "parameters": [ + { + "type": "integer", + "description": "Customer ID", + "name": "customer_id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ReportedIssue" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/issues/{issue_id}": { + "delete": { + "description": "Admin endpoint to delete a reported issue", + "tags": [ + "Issues" + ], + "summary": "Delete a reported issue", + "parameters": [ + { + "type": "integer", + "description": "Issue ID", + "name": "issue_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/issues/{issue_id}/status": { + "patch": { + "description": "Admin endpoint to update the status of a reported issue", + "consumes": [ + "application/json" + ], + "tags": [ + "Issues" + ], + "summary": "Update issue status", + "parameters": [ + { + "type": "integer", + "description": "Issue ID", + "name": "issue_id", + "in": "path", + "required": true + }, + { + "description": "New issue status (pending, in_progress, resolved, rejected)", + "name": "status", + "in": "body", + "required": true, + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/logs": { "get": { "description": "Fetches the 100 most recent application logs from MongoDB", @@ -841,9 +1276,36 @@ const docTemplate = `{ } } }, - "/api/v1/virtual-games/recommendations/{userID}": { + "/api/v1/virtual-game/favorites": { "get": { - "description": "Returns a list of recommended virtual games for a specific user", + "description": "Lists the games that the user marked as favorite", + "produces": [ + "application/json" + ], + "tags": [ + "VirtualGames - Favourites" + ], + "summary": "Get user's favorite games", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.GameRecommendation" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "post": { + "description": "Adds a game to the user's favorite games list", "consumes": [ "application/json" ], @@ -851,29 +1313,78 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Recommendations" + "VirtualGames - Favourites" ], - "summary": "Get virtual game recommendations", + "summary": "Add game to favorites", "parameters": [ { - "type": "string", - "description": "User ID", - "name": "userID", + "description": "Game ID to add", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.FavoriteGameRequest" + } + } + ], + "responses": { + "201": { + "description": "created", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/virtual-game/favorites/{gameID}": { + "delete": { + "description": "Removes a game from the user's favorites", + "produces": [ + "application/json" + ], + "tags": [ + "VirtualGames - Favourites" + ], + "summary": "Remove game from favorites", + "parameters": [ + { + "type": "integer", + "description": "Game ID to remove", + "name": "gameID", "in": "path", "required": true } ], "responses": { "200": { - "description": "Recommended games fetched successfully", + "description": "removed", "schema": { - "$ref": "#/definitions/domain.RecommendationSuccessfulResponse" + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" } }, "500": { - "description": "Failed to fetch recommendations", + "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/domain.RecommendationErrorResponse" + "$ref": "#/definitions/domain.ErrorResponse" } } } @@ -1084,269 +1595,6 @@ const docTemplate = `{ } } }, - "/bet": { - "get": { - "description": "Gets all the bets", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Gets all bets", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.BetRes" - } - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - }, - "post": { - "description": "Creates a bet", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Create a bet", - "parameters": [ - { - "description": "Creates bet", - "name": "createBet", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.CreateBetReq" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/domain.BetRes" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/bet/cashout/{id}": { - "get": { - "description": "Gets a single bet by cashout id", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Gets bet by cashout id", - "parameters": [ - { - "type": "string", - "description": "cashout ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/domain.BetRes" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/bet/{id}": { - "get": { - "description": "Gets a single bet by id", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Gets bet by id", - "parameters": [ - { - "type": "integer", - "description": "Bet ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/domain.BetRes" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - }, - "delete": { - "description": "Deletes bet by id", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Deletes bet by id", - "parameters": [ - { - "type": "integer", - "description": "Bet ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - }, - "patch": { - "description": "Updates the cashed out field", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Updates the cashed out field", - "parameters": [ - { - "type": "integer", - "description": "Bet ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Updates Cashed Out", - "name": "updateCashOut", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handlers.UpdateCashOutReq" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/branch": { "get": { "description": "Gets all branches", @@ -3083,52 +3331,6 @@ const docTemplate = `{ } } }, - "/random/bet": { - "post": { - "description": "Generate a random bet", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Generate a random bet", - "parameters": [ - { - "description": "Create Random bet", - "name": "createBet", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.RandomBetReq" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/domain.BetRes" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/referral/settings": { "get": { "security": [ @@ -3402,6 +3604,315 @@ const docTemplate = `{ } } }, + "/sport/bet": { + "get": { + "description": "Gets all the bets", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets all bets", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.BetRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "post": { + "description": "Creates a bet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Create a bet", + "parameters": [ + { + "description": "Creates bet", + "name": "createBet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CreateBetReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BetRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/sport/bet/cashout/{id}": { + "get": { + "description": "Gets a single bet by cashout id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets bet by cashout id", + "parameters": [ + { + "type": "string", + "description": "cashout ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BetRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/sport/bet/{id}": { + "get": { + "description": "Gets a single bet by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets bet by id", + "parameters": [ + { + "type": "integer", + "description": "Bet ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BetRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "delete": { + "description": "Deletes bet by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Deletes bet by id", + "parameters": [ + { + "type": "integer", + "description": "Bet ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "patch": { + "description": "Updates the cashed out field", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Updates the cashed out field", + "parameters": [ + { + "type": "integer", + "description": "Bet ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updates Cashed Out", + "name": "updateCashOut", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.UpdateCashOutReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/sport/random/bet": { + "post": { + "description": "Generate a random bet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Generate a random bet", + "parameters": [ + { + "description": "Create Random bet", + "name": "createBet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.RandomBetReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BetRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/supportedOperation": { "get": { "description": "Gets all supported operations", @@ -4528,19 +5039,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/domain.ErrorResponse" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/domain.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/domain.ErrorResponse" } } } @@ -4585,19 +5096,19 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/domain.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/domain.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/domain.ErrorResponse" } } } @@ -4786,6 +5297,59 @@ const docTemplate = `{ } } }, + "domain.Bank": { + "type": "object", + "properties": { + "acct_length": { + "type": "integer" + }, + "active": { + "type": "integer" + }, + "bank_logo": { + "description": "URL or base64", + "type": "string" + }, + "country_id": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_24hrs": { + "description": "nullable", + "type": "integer" + }, + "is_active": { + "type": "integer" + }, + "is_mobilemoney": { + "description": "nullable", + "type": "integer" + }, + "is_rtgs": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "swift": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "domain.BetOutcome": { "type": "object", "properties": { @@ -5253,6 +5817,14 @@ const docTemplate = `{ "STATUS_REMOVED" ] }, + "domain.FavoriteGameRequest": { + "type": "object", + "properties": { + "game_id": { + "type": "integer" + } + } + }, "domain.GameRecommendation": { "type": "object", "properties": { @@ -5444,11 +6016,13 @@ const docTemplate = `{ "domain.PaymentStatus": { "type": "string", "enum": [ + "success", "pending", "completed", "failed" ], "x-enum-varnames": [ + "PaymentStatusSuccessful", "PaymentStatusPending", "PaymentStatusCompleted", "PaymentStatusFailed" @@ -5543,28 +6117,6 @@ const docTemplate = `{ } } }, - "domain.RecommendationErrorResponse": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } - }, - "domain.RecommendationSuccessfulResponse": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "recommended_games": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.VirtualGame" - } - } - } - }, "domain.ReferralSettings": { "type": "object", "properties": { @@ -5617,6 +6169,39 @@ const docTemplate = `{ } } }, + "domain.ReportedIssue": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "customer_id": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "issue_type": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": true + }, + "status": { + "type": "string" + }, + "subject": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "domain.Response": { "type": "object", "properties": { @@ -5806,53 +6391,6 @@ const docTemplate = `{ } } }, - "domain.VirtualGame": { - "type": "object", - "properties": { - "category": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "is_active": { - "type": "boolean" - }, - "is_featured": { - "type": "boolean" - }, - "max_bet": { - "type": "number" - }, - "min_bet": { - "type": "number" - }, - "name": { - "type": "string" - }, - "popularity_score": { - "type": "integer" - }, - "provider": { - "type": "string" - }, - "rtp": { - "type": "number" - }, - "thumbnail_url": { - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "volatility": { - "type": "string" - } - } - }, "handlers.AdminRes": { "type": "object", "properties": { @@ -6670,44 +7208,38 @@ const docTemplate = `{ "type": "object", "properties": { "amount": { - "type": "number", - "example": 100 + "type": "number" }, "cashier_id": { - "type": "integer", - "example": 789 + "type": "integer" }, "created_at": { - "type": "string", - "example": "2025-04-08T12:00:00Z" + "type": "string" }, "id": { - "type": "integer", - "example": 1 + "type": "integer" }, "payment_method": { - "type": "string", - "example": "bank" + "type": "string" }, "receiver_wallet_id": { - "type": "integer", - "example": 1 + "type": "integer" + }, + "reference_number": { + "description": "← Add this", + "type": "string" }, "sender_wallet_id": { - "type": "integer", - "example": 1 + "type": "integer" }, "type": { - "type": "string", - "example": "transfer" + "type": "string" }, "updated_at": { - "type": "string", - "example": "2025-04-08T12:30:00Z" + "type": "string" }, "verified": { - "type": "boolean", - "example": true + "type": "boolean" } } }, diff --git a/docs/swagger.json b/docs/swagger.json index 0402648..52af909 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -296,6 +296,217 @@ } } }, + "/api/v1/banks": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Institutions - Banks" + ], + "summary": "List all banks", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Bank" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Institutions - Banks" + ], + "summary": "Create a new bank", + "parameters": [ + { + "description": "Bank Info", + "name": "bank", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.Bank" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/domain.Bank" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/banks/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Institutions - Banks" + ], + "summary": "Get a bank by ID", + "parameters": [ + { + "type": "integer", + "description": "Bank ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Bank" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Institutions - Banks" + ], + "summary": "Update a bank", + "parameters": [ + { + "type": "integer", + "description": "Bank ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Bank Info", + "name": "bank", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.Bank" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Bank" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Institutions - Banks" + ], + "summary": "Delete a bank", + "parameters": [ + { + "type": "integer", + "description": "Bank ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "Deleted successfully", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/chapa/banks": { "get": { "description": "Get list of banks supported by Chapa", @@ -626,6 +837,230 @@ } } }, + "/api/v1/issues": { + "get": { + "description": "Admin endpoint to list all reported issues with pagination", + "produces": [ + "application/json" + ], + "tags": [ + "Issues" + ], + "summary": "Get all reported issues", + "parameters": [ + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ReportedIssue" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "post": { + "description": "Allows a customer to report a new issue related to the betting platform", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Issues" + ], + "summary": "Report an issue", + "parameters": [ + { + "description": "Issue to report", + "name": "issue", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.ReportedIssue" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/domain.ReportedIssue" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/issues/customer/{customer_id}": { + "get": { + "description": "Returns all issues reported by a specific customer", + "produces": [ + "application/json" + ], + "tags": [ + "Issues" + ], + "summary": "Get reported issues by a customer", + "parameters": [ + { + "type": "integer", + "description": "Customer ID", + "name": "customer_id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ReportedIssue" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/issues/{issue_id}": { + "delete": { + "description": "Admin endpoint to delete a reported issue", + "tags": [ + "Issues" + ], + "summary": "Delete a reported issue", + "parameters": [ + { + "type": "integer", + "description": "Issue ID", + "name": "issue_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/issues/{issue_id}/status": { + "patch": { + "description": "Admin endpoint to update the status of a reported issue", + "consumes": [ + "application/json" + ], + "tags": [ + "Issues" + ], + "summary": "Update issue status", + "parameters": [ + { + "type": "integer", + "description": "Issue ID", + "name": "issue_id", + "in": "path", + "required": true + }, + { + "description": "New issue status (pending, in_progress, resolved, rejected)", + "name": "status", + "in": "body", + "required": true, + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/logs": { "get": { "description": "Fetches the 100 most recent application logs from MongoDB", @@ -833,9 +1268,36 @@ } } }, - "/api/v1/virtual-games/recommendations/{userID}": { + "/api/v1/virtual-game/favorites": { "get": { - "description": "Returns a list of recommended virtual games for a specific user", + "description": "Lists the games that the user marked as favorite", + "produces": [ + "application/json" + ], + "tags": [ + "VirtualGames - Favourites" + ], + "summary": "Get user's favorite games", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.GameRecommendation" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "post": { + "description": "Adds a game to the user's favorite games list", "consumes": [ "application/json" ], @@ -843,29 +1305,78 @@ "application/json" ], "tags": [ - "Recommendations" + "VirtualGames - Favourites" ], - "summary": "Get virtual game recommendations", + "summary": "Add game to favorites", "parameters": [ { - "type": "string", - "description": "User ID", - "name": "userID", + "description": "Game ID to add", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.FavoriteGameRequest" + } + } + ], + "responses": { + "201": { + "description": "created", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/virtual-game/favorites/{gameID}": { + "delete": { + "description": "Removes a game from the user's favorites", + "produces": [ + "application/json" + ], + "tags": [ + "VirtualGames - Favourites" + ], + "summary": "Remove game from favorites", + "parameters": [ + { + "type": "integer", + "description": "Game ID to remove", + "name": "gameID", "in": "path", "required": true } ], "responses": { "200": { - "description": "Recommended games fetched successfully", + "description": "removed", "schema": { - "$ref": "#/definitions/domain.RecommendationSuccessfulResponse" + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" } }, "500": { - "description": "Failed to fetch recommendations", + "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/domain.RecommendationErrorResponse" + "$ref": "#/definitions/domain.ErrorResponse" } } } @@ -1076,269 +1587,6 @@ } } }, - "/bet": { - "get": { - "description": "Gets all the bets", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Gets all bets", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.BetRes" - } - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - }, - "post": { - "description": "Creates a bet", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Create a bet", - "parameters": [ - { - "description": "Creates bet", - "name": "createBet", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.CreateBetReq" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/domain.BetRes" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/bet/cashout/{id}": { - "get": { - "description": "Gets a single bet by cashout id", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Gets bet by cashout id", - "parameters": [ - { - "type": "string", - "description": "cashout ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/domain.BetRes" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/bet/{id}": { - "get": { - "description": "Gets a single bet by id", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Gets bet by id", - "parameters": [ - { - "type": "integer", - "description": "Bet ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/domain.BetRes" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - }, - "delete": { - "description": "Deletes bet by id", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Deletes bet by id", - "parameters": [ - { - "type": "integer", - "description": "Bet ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - }, - "patch": { - "description": "Updates the cashed out field", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Updates the cashed out field", - "parameters": [ - { - "type": "integer", - "description": "Bet ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Updates Cashed Out", - "name": "updateCashOut", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handlers.UpdateCashOutReq" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/branch": { "get": { "description": "Gets all branches", @@ -3075,52 +3323,6 @@ } } }, - "/random/bet": { - "post": { - "description": "Generate a random bet", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "bet" - ], - "summary": "Generate a random bet", - "parameters": [ - { - "description": "Create Random bet", - "name": "createBet", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.RandomBetReq" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/domain.BetRes" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/referral/settings": { "get": { "security": [ @@ -3394,6 +3596,315 @@ } } }, + "/sport/bet": { + "get": { + "description": "Gets all the bets", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets all bets", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.BetRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "post": { + "description": "Creates a bet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Create a bet", + "parameters": [ + { + "description": "Creates bet", + "name": "createBet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CreateBetReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BetRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/sport/bet/cashout/{id}": { + "get": { + "description": "Gets a single bet by cashout id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets bet by cashout id", + "parameters": [ + { + "type": "string", + "description": "cashout ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BetRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/sport/bet/{id}": { + "get": { + "description": "Gets a single bet by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets bet by id", + "parameters": [ + { + "type": "integer", + "description": "Bet ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BetRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "delete": { + "description": "Deletes bet by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Deletes bet by id", + "parameters": [ + { + "type": "integer", + "description": "Bet ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "patch": { + "description": "Updates the cashed out field", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Updates the cashed out field", + "parameters": [ + { + "type": "integer", + "description": "Bet ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updates Cashed Out", + "name": "updateCashOut", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.UpdateCashOutReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/sport/random/bet": { + "post": { + "description": "Generate a random bet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Generate a random bet", + "parameters": [ + { + "description": "Create Random bet", + "name": "createBet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.RandomBetReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BetRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/supportedOperation": { "get": { "description": "Gets all supported operations", @@ -4520,19 +5031,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/domain.ErrorResponse" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/domain.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/domain.ErrorResponse" } } } @@ -4577,19 +5088,19 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/domain.ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/domain.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/domain.ErrorResponse" } } } @@ -4778,6 +5289,59 @@ } } }, + "domain.Bank": { + "type": "object", + "properties": { + "acct_length": { + "type": "integer" + }, + "active": { + "type": "integer" + }, + "bank_logo": { + "description": "URL or base64", + "type": "string" + }, + "country_id": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_24hrs": { + "description": "nullable", + "type": "integer" + }, + "is_active": { + "type": "integer" + }, + "is_mobilemoney": { + "description": "nullable", + "type": "integer" + }, + "is_rtgs": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "swift": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "domain.BetOutcome": { "type": "object", "properties": { @@ -5245,6 +5809,14 @@ "STATUS_REMOVED" ] }, + "domain.FavoriteGameRequest": { + "type": "object", + "properties": { + "game_id": { + "type": "integer" + } + } + }, "domain.GameRecommendation": { "type": "object", "properties": { @@ -5436,11 +6008,13 @@ "domain.PaymentStatus": { "type": "string", "enum": [ + "success", "pending", "completed", "failed" ], "x-enum-varnames": [ + "PaymentStatusSuccessful", "PaymentStatusPending", "PaymentStatusCompleted", "PaymentStatusFailed" @@ -5535,28 +6109,6 @@ } } }, - "domain.RecommendationErrorResponse": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } - }, - "domain.RecommendationSuccessfulResponse": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "recommended_games": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.VirtualGame" - } - } - } - }, "domain.ReferralSettings": { "type": "object", "properties": { @@ -5609,6 +6161,39 @@ } } }, + "domain.ReportedIssue": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "customer_id": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "issue_type": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": true + }, + "status": { + "type": "string" + }, + "subject": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "domain.Response": { "type": "object", "properties": { @@ -5798,53 +6383,6 @@ } } }, - "domain.VirtualGame": { - "type": "object", - "properties": { - "category": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "is_active": { - "type": "boolean" - }, - "is_featured": { - "type": "boolean" - }, - "max_bet": { - "type": "number" - }, - "min_bet": { - "type": "number" - }, - "name": { - "type": "string" - }, - "popularity_score": { - "type": "integer" - }, - "provider": { - "type": "string" - }, - "rtp": { - "type": "number" - }, - "thumbnail_url": { - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "volatility": { - "type": "string" - } - } - }, "handlers.AdminRes": { "type": "object", "properties": { @@ -6662,44 +7200,38 @@ "type": "object", "properties": { "amount": { - "type": "number", - "example": 100 + "type": "number" }, "cashier_id": { - "type": "integer", - "example": 789 + "type": "integer" }, "created_at": { - "type": "string", - "example": "2025-04-08T12:00:00Z" + "type": "string" }, "id": { - "type": "integer", - "example": 1 + "type": "integer" }, "payment_method": { - "type": "string", - "example": "bank" + "type": "string" }, "receiver_wallet_id": { - "type": "integer", - "example": 1 + "type": "integer" + }, + "reference_number": { + "description": "← Add this", + "type": "string" }, "sender_wallet_id": { - "type": "integer", - "example": 1 + "type": "integer" }, "type": { - "type": "string", - "example": "transfer" + "type": "string" }, "updated_at": { - "type": "string", - "example": "2025-04-08T12:30:00Z" + "type": "string" }, "verified": { - "type": "boolean", - "example": true + "type": "boolean" } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ffb24c6..86b5932 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -31,6 +31,42 @@ definitions: user_id: type: string type: object + domain.Bank: + properties: + acct_length: + type: integer + active: + type: integer + bank_logo: + description: URL or base64 + type: string + country_id: + type: integer + created_at: + type: string + currency: + type: string + id: + type: integer + is_24hrs: + description: nullable + type: integer + is_active: + type: integer + is_mobilemoney: + description: nullable + type: integer + is_rtgs: + type: integer + name: + type: string + slug: + type: string + swift: + type: string + updated_at: + type: string + type: object domain.BetOutcome: properties: away_team_name: @@ -355,6 +391,11 @@ definitions: - STATUS_SUSPENDED - STATUS_DECIDED_BY_FA - STATUS_REMOVED + domain.FavoriteGameRequest: + properties: + game_id: + type: integer + type: object domain.GameRecommendation: properties: bets: @@ -491,11 +532,13 @@ definitions: - BANK domain.PaymentStatus: enum: + - success - pending - completed - failed type: string x-enum-varnames: + - PaymentStatusSuccessful - PaymentStatusPending - PaymentStatusCompleted - PaymentStatusFailed @@ -559,20 +602,6 @@ definitions: items: {} type: array type: object - domain.RecommendationErrorResponse: - properties: - message: - type: string - type: object - domain.RecommendationSuccessfulResponse: - properties: - message: - type: string - recommended_games: - items: - $ref: '#/definitions/domain.VirtualGame' - type: array - type: object domain.ReferralSettings: properties: betReferralBonusPercentage: @@ -607,6 +636,28 @@ definitions: totalRewardEarned: type: number type: object + domain.ReportedIssue: + properties: + created_at: + type: string + customer_id: + type: integer + description: + type: string + id: + type: integer + issue_type: + type: string + metadata: + additionalProperties: true + type: object + status: + type: string + subject: + type: string + updated_at: + type: string + type: object domain.Response: properties: data: {} @@ -742,37 +793,6 @@ definitions: - $ref: '#/definitions/domain.EventStatus' description: Match Status for event type: object - domain.VirtualGame: - properties: - category: - type: string - created_at: - type: string - id: - type: integer - is_active: - type: boolean - is_featured: - type: boolean - max_bet: - type: number - min_bet: - type: number - name: - type: string - popularity_score: - type: integer - provider: - type: string - rtp: - type: number - thumbnail_url: - type: string - updated_at: - type: string - volatility: - type: string - type: object handlers.AdminRes: properties: created_at: @@ -1340,34 +1360,27 @@ definitions: handlers.TransferWalletRes: properties: amount: - example: 100 type: number cashier_id: - example: 789 type: integer created_at: - example: "2025-04-08T12:00:00Z" type: string id: - example: 1 type: integer payment_method: - example: bank type: string receiver_wallet_id: - example: 1 type: integer + reference_number: + description: ← Add this + type: string sender_wallet_id: - example: 1 type: integer type: - example: transfer type: string updated_at: - example: "2025-04-08T12:30:00Z" type: string verified: - example: true type: boolean type: object handlers.UpdateCashOutReq: @@ -1801,6 +1814,144 @@ paths: summary: Launch an Alea Play virtual game tags: - Alea Virtual Games + /api/v1/banks: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.Bank' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: List all banks + tags: + - Institutions - Banks + post: + consumes: + - application/json + parameters: + - description: Bank Info + in: body + name: bank + required: true + schema: + $ref: '#/definitions/domain.Bank' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/domain.Bank' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Create a new bank + tags: + - Institutions - Banks + /api/v1/banks/{id}: + delete: + parameters: + - description: Bank ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "204": + description: Deleted successfully + schema: + type: string + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Delete a bank + tags: + - Institutions - Banks + get: + parameters: + - description: Bank ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Bank' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get a bank by ID + tags: + - Institutions - Banks + put: + consumes: + - application/json + parameters: + - description: Bank ID + in: path + name: id + required: true + type: integer + - description: Bank Info + in: body + name: bank + required: true + schema: + $ref: '#/definitions/domain.Bank' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Bank' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Update a bank + tags: + - Institutions - Banks /api/v1/chapa/banks: get: consumes: @@ -2011,6 +2162,154 @@ paths: summary: Convert currency tags: - Multi-Currency + /api/v1/issues: + get: + description: Admin endpoint to list all reported issues with pagination + parameters: + - description: Limit + in: query + name: limit + type: integer + - description: Offset + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.ReportedIssue' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get all reported issues + tags: + - Issues + post: + consumes: + - application/json + description: Allows a customer to report a new issue related to the betting + platform + parameters: + - description: Issue to report + in: body + name: issue + required: true + schema: + $ref: '#/definitions/domain.ReportedIssue' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/domain.ReportedIssue' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Report an issue + tags: + - Issues + /api/v1/issues/{issue_id}: + delete: + description: Admin endpoint to delete a reported issue + parameters: + - description: Issue ID + in: path + name: issue_id + required: true + type: integer + responses: + "204": + description: No Content + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Delete a reported issue + tags: + - Issues + /api/v1/issues/{issue_id}/status: + patch: + consumes: + - application/json + description: Admin endpoint to update the status of a reported issue + parameters: + - description: Issue ID + in: path + name: issue_id + required: true + type: integer + - description: New issue status (pending, in_progress, resolved, rejected) + in: body + name: status + required: true + schema: + properties: + status: + type: string + type: object + responses: + "204": + description: No Content + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Update issue status + tags: + - Issues + /api/v1/issues/customer/{customer_id}: + get: + description: Returns all issues reported by a specific customer + parameters: + - description: Customer ID + in: path + name: customer_id + required: true + type: integer + - description: Limit + in: query + name: limit + type: integer + - description: Offset + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.ReportedIssue' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get reported issues by a customer + tags: + - Issues /api/v1/logs: get: description: Fetches the 100 most recent application logs from MongoDB @@ -2144,31 +2443,81 @@ paths: summary: Get dashboard report tags: - Reports - /api/v1/virtual-games/recommendations/{userID}: + /api/v1/virtual-game/favorites: get: - consumes: - - application/json - description: Returns a list of recommended virtual games for a specific user - parameters: - - description: User ID - in: path - name: userID - required: true - type: string + description: Lists the games that the user marked as favorite produces: - application/json responses: "200": - description: Recommended games fetched successfully + description: OK schema: - $ref: '#/definitions/domain.RecommendationSuccessfulResponse' + items: + $ref: '#/definitions/domain.GameRecommendation' + type: array "500": - description: Failed to fetch recommendations + description: Internal Server Error schema: - $ref: '#/definitions/domain.RecommendationErrorResponse' - summary: Get virtual game recommendations + $ref: '#/definitions/domain.ErrorResponse' + summary: Get user's favorite games tags: - - Recommendations + - VirtualGames - Favourites + post: + consumes: + - application/json + description: Adds a game to the user's favorite games list + parameters: + - description: Game ID to add + in: body + name: body + required: true + schema: + $ref: '#/definitions/domain.FavoriteGameRequest' + produces: + - application/json + responses: + "201": + description: created + schema: + type: string + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Add game to favorites + tags: + - VirtualGames - Favourites + /api/v1/virtual-game/favorites/{gameID}: + delete: + description: Removes a game from the user's favorites + parameters: + - description: Game ID to remove + in: path + name: gameID + required: true + type: integer + produces: + - application/json + responses: + "200": + description: removed + schema: + type: string + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Remove game from favorites + tags: + - VirtualGames - Favourites /api/v1/webhooks/alea: post: consumes: @@ -2301,180 +2650,6 @@ paths: summary: Refresh token tags: - auth - /bet: - get: - consumes: - - application/json - description: Gets all the bets - produces: - - application/json - responses: - "200": - description: OK - schema: - items: - $ref: '#/definitions/domain.BetRes' - type: array - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Gets all bets - tags: - - bet - post: - consumes: - - application/json - description: Creates a bet - parameters: - - description: Creates bet - in: body - name: createBet - required: true - schema: - $ref: '#/definitions/domain.CreateBetReq' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/domain.BetRes' - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Create a bet - tags: - - bet - /bet/{id}: - delete: - consumes: - - application/json - description: Deletes bet by id - parameters: - - description: Bet ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/response.APIResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Deletes bet by id - tags: - - bet - get: - consumes: - - application/json - description: Gets a single bet by id - parameters: - - description: Bet ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/domain.BetRes' - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Gets bet by id - tags: - - bet - patch: - consumes: - - application/json - description: Updates the cashed out field - parameters: - - description: Bet ID - in: path - name: id - required: true - type: integer - - description: Updates Cashed Out - in: body - name: updateCashOut - required: true - schema: - $ref: '#/definitions/handlers.UpdateCashOutReq' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/response.APIResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Updates the cashed out field - tags: - - bet - /bet/cashout/{id}: - get: - consumes: - - application/json - description: Gets a single bet by cashout id - parameters: - - description: cashout ID - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/domain.BetRes' - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Gets bet by cashout id - tags: - - bet /branch: get: consumes: @@ -3622,36 +3797,6 @@ paths: summary: Recommend virtual games tags: - Virtual Games - PopOK - /random/bet: - post: - consumes: - - application/json - description: Generate a random bet - parameters: - - description: Create Random bet - in: body - name: createBet - required: true - schema: - $ref: '#/definitions/domain.RandomBetReq' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/domain.BetRes' - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Generate a random bet - tags: - - bet /referral/settings: get: consumes: @@ -3828,6 +3973,210 @@ paths: summary: Gets all companies tags: - company + /sport/bet: + get: + consumes: + - application/json + description: Gets all the bets + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.BetRes' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets all bets + tags: + - bet + post: + consumes: + - application/json + description: Creates a bet + parameters: + - description: Creates bet + in: body + name: createBet + required: true + schema: + $ref: '#/definitions/domain.CreateBetReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.BetRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Create a bet + tags: + - bet + /sport/bet/{id}: + delete: + consumes: + - application/json + description: Deletes bet by id + parameters: + - description: Bet ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Deletes bet by id + tags: + - bet + get: + consumes: + - application/json + description: Gets a single bet by id + parameters: + - description: Bet ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.BetRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets bet by id + tags: + - bet + patch: + consumes: + - application/json + description: Updates the cashed out field + parameters: + - description: Bet ID + in: path + name: id + required: true + type: integer + - description: Updates Cashed Out + in: body + name: updateCashOut + required: true + schema: + $ref: '#/definitions/handlers.UpdateCashOutReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Updates the cashed out field + tags: + - bet + /sport/bet/cashout/{id}: + get: + consumes: + - application/json + description: Gets a single bet by cashout id + parameters: + - description: cashout ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.BetRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets bet by cashout id + tags: + - bet + /sport/random/bet: + post: + consumes: + - application/json + description: Generate a random bet + parameters: + - description: Create Random bet + in: body + name: createBet + required: true + schema: + $ref: '#/definitions/domain.RandomBetReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.BetRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Generate a random bet + tags: + - bet /supportedOperation: get: consumes: @@ -4564,15 +4913,15 @@ paths: "200": description: OK schema: - $ref: '#/definitions/response.APIResponse' + $ref: '#/definitions/domain.ErrorResponse' "400": description: Bad Request schema: - $ref: '#/definitions/response.APIResponse' + $ref: '#/definitions/domain.ErrorResponse' "500": description: Internal Server Error schema: - $ref: '#/definitions/response.APIResponse' + $ref: '#/definitions/domain.ErrorResponse' summary: Handle PopOK game callback tags: - Virtual Games - PopOK @@ -4598,15 +4947,15 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/response.APIResponse' + $ref: '#/definitions/domain.ErrorResponse' "401": description: Unauthorized schema: - $ref: '#/definitions/response.APIResponse' + $ref: '#/definitions/domain.ErrorResponse' "500": description: Internal Server Error schema: - $ref: '#/definitions/response.APIResponse' + $ref: '#/definitions/domain.ErrorResponse' security: - Bearer: [] summary: Launch a PopOK virtual game diff --git a/gen/db/institutions.sql.go b/gen/db/institutions.sql.go new file mode 100644 index 0000000..b182933 --- /dev/null +++ b/gen/db/institutions.sql.go @@ -0,0 +1,251 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: institutions.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const CreateBank = `-- name: CreateBank :one +INSERT INTO banks ( + slug, + swift, + name, + acct_length, + country_id, + is_mobilemoney, + is_active, + is_rtgs, + active, + is_24hrs, + created_at, + updated_at, + currency, + bank_logo +) +VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, $11, $12 +) +RETURNING id, slug, swift, name, acct_length, country_id, is_mobilemoney, is_active, is_rtgs, active, is_24hrs, created_at, updated_at, currency, bank_logo +` + +type CreateBankParams struct { + Slug string `json:"slug"` + Swift string `json:"swift"` + Name string `json:"name"` + AcctLength int32 `json:"acct_length"` + CountryID int32 `json:"country_id"` + IsMobilemoney pgtype.Int4 `json:"is_mobilemoney"` + IsActive int32 `json:"is_active"` + IsRtgs int32 `json:"is_rtgs"` + Active int32 `json:"active"` + Is24hrs pgtype.Int4 `json:"is_24hrs"` + Currency string `json:"currency"` + BankLogo pgtype.Text `json:"bank_logo"` +} + +func (q *Queries) CreateBank(ctx context.Context, arg CreateBankParams) (Bank, error) { + row := q.db.QueryRow(ctx, CreateBank, + arg.Slug, + arg.Swift, + arg.Name, + arg.AcctLength, + arg.CountryID, + arg.IsMobilemoney, + arg.IsActive, + arg.IsRtgs, + arg.Active, + arg.Is24hrs, + arg.Currency, + arg.BankLogo, + ) + var i Bank + err := row.Scan( + &i.ID, + &i.Slug, + &i.Swift, + &i.Name, + &i.AcctLength, + &i.CountryID, + &i.IsMobilemoney, + &i.IsActive, + &i.IsRtgs, + &i.Active, + &i.Is24hrs, + &i.CreatedAt, + &i.UpdatedAt, + &i.Currency, + &i.BankLogo, + ) + return i, err +} + +const DeleteBank = `-- name: DeleteBank :exec +DELETE FROM banks +WHERE id = $1 +` + +func (q *Queries) DeleteBank(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeleteBank, id) + return err +} + +const GetAllBanks = `-- name: GetAllBanks :many +SELECT id, slug, swift, name, acct_length, country_id, is_mobilemoney, is_active, is_rtgs, active, is_24hrs, created_at, updated_at, currency, bank_logo +FROM banks +WHERE ( + country_id = $1 + OR $1 IS NULL + ) + AND ( + is_active = $2 + OR $2 IS NULL + ) +` + +type GetAllBanksParams struct { + CountryID pgtype.Int4 `json:"country_id"` + IsActive pgtype.Int4 `json:"is_active"` +} + +func (q *Queries) GetAllBanks(ctx context.Context, arg GetAllBanksParams) ([]Bank, error) { + rows, err := q.db.Query(ctx, GetAllBanks, arg.CountryID, arg.IsActive) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Bank + for rows.Next() { + var i Bank + if err := rows.Scan( + &i.ID, + &i.Slug, + &i.Swift, + &i.Name, + &i.AcctLength, + &i.CountryID, + &i.IsMobilemoney, + &i.IsActive, + &i.IsRtgs, + &i.Active, + &i.Is24hrs, + &i.CreatedAt, + &i.UpdatedAt, + &i.Currency, + &i.BankLogo, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetBankByID = `-- name: GetBankByID :one +SELECT id, slug, swift, name, acct_length, country_id, is_mobilemoney, is_active, is_rtgs, active, is_24hrs, created_at, updated_at, currency, bank_logo +FROM banks +WHERE id = $1 +` + +func (q *Queries) GetBankByID(ctx context.Context, id int64) (Bank, error) { + row := q.db.QueryRow(ctx, GetBankByID, id) + var i Bank + err := row.Scan( + &i.ID, + &i.Slug, + &i.Swift, + &i.Name, + &i.AcctLength, + &i.CountryID, + &i.IsMobilemoney, + &i.IsActive, + &i.IsRtgs, + &i.Active, + &i.Is24hrs, + &i.CreatedAt, + &i.UpdatedAt, + &i.Currency, + &i.BankLogo, + ) + return i, err +} + +const UpdateBank = `-- name: UpdateBank :one +UPDATE banks +SET slug = COALESCE($2, slug), + swift = COALESCE($3, swift), + name = COALESCE($4, name), + acct_length = COALESCE($5, acct_length), + country_id = COALESCE($6, country_id), + is_mobilemoney = COALESCE($7, is_mobilemoney), + is_active = COALESCE($8, is_active), + is_rtgs = COALESCE($9, is_rtgs), + active = COALESCE($10, active), + is_24hrs = COALESCE($11, is_24hrs), + updated_at = CURRENT_TIMESTAMP, + currency = COALESCE($12, currency), + bank_logo = COALESCE($13, bank_logo) +WHERE id = $1 +RETURNING id, slug, swift, name, acct_length, country_id, is_mobilemoney, is_active, is_rtgs, active, is_24hrs, created_at, updated_at, currency, bank_logo +` + +type UpdateBankParams struct { + ID int64 `json:"id"` + Slug pgtype.Text `json:"slug"` + Swift pgtype.Text `json:"swift"` + Name pgtype.Text `json:"name"` + AcctLength pgtype.Int4 `json:"acct_length"` + CountryID pgtype.Int4 `json:"country_id"` + IsMobilemoney pgtype.Int4 `json:"is_mobilemoney"` + IsActive pgtype.Int4 `json:"is_active"` + IsRtgs pgtype.Int4 `json:"is_rtgs"` + Active pgtype.Int4 `json:"active"` + Is24hrs pgtype.Int4 `json:"is_24hrs"` + Currency pgtype.Text `json:"currency"` + BankLogo pgtype.Text `json:"bank_logo"` +} + +func (q *Queries) UpdateBank(ctx context.Context, arg UpdateBankParams) (Bank, error) { + row := q.db.QueryRow(ctx, UpdateBank, + arg.ID, + arg.Slug, + arg.Swift, + arg.Name, + arg.AcctLength, + arg.CountryID, + arg.IsMobilemoney, + arg.IsActive, + arg.IsRtgs, + arg.Active, + arg.Is24hrs, + arg.Currency, + arg.BankLogo, + ) + var i Bank + err := row.Scan( + &i.ID, + &i.Slug, + &i.Swift, + &i.Name, + &i.AcctLength, + &i.CountryID, + &i.IsMobilemoney, + &i.IsActive, + &i.IsRtgs, + &i.Active, + &i.Is24hrs, + &i.CreatedAt, + &i.UpdatedAt, + &i.Currency, + &i.BankLogo, + ) + return i, err +} diff --git a/gen/db/issue_reporting.sql.go b/gen/db/issue_reporting.sql.go new file mode 100644 index 0000000..c737b9e --- /dev/null +++ b/gen/db/issue_reporting.sql.go @@ -0,0 +1,181 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: issue_reporting.sql + +package dbgen + +import ( + "context" +) + +const CountReportedIssues = `-- name: CountReportedIssues :one +SELECT COUNT(*) FROM reported_issues +` + +func (q *Queries) CountReportedIssues(ctx context.Context) (int64, error) { + row := q.db.QueryRow(ctx, CountReportedIssues) + var count int64 + err := row.Scan(&count) + return count, err +} + +const CountReportedIssuesByCustomer = `-- name: CountReportedIssuesByCustomer :one +SELECT COUNT(*) FROM reported_issues WHERE customer_id = $1 +` + +func (q *Queries) CountReportedIssuesByCustomer(ctx context.Context, customerID int64) (int64, error) { + row := q.db.QueryRow(ctx, CountReportedIssuesByCustomer, customerID) + var count int64 + err := row.Scan(&count) + return count, err +} + +const CreateReportedIssue = `-- name: CreateReportedIssue :one +INSERT INTO reported_issues ( + customer_id, subject, description, issue_type, metadata +) VALUES ( + $1, $2, $3, $4, $5 +) +RETURNING id, customer_id, subject, description, issue_type, status, metadata, created_at, updated_at +` + +type CreateReportedIssueParams struct { + CustomerID int64 `json:"customer_id"` + Subject string `json:"subject"` + Description string `json:"description"` + IssueType string `json:"issue_type"` + Metadata []byte `json:"metadata"` +} + +func (q *Queries) CreateReportedIssue(ctx context.Context, arg CreateReportedIssueParams) (ReportedIssue, error) { + row := q.db.QueryRow(ctx, CreateReportedIssue, + arg.CustomerID, + arg.Subject, + arg.Description, + arg.IssueType, + arg.Metadata, + ) + var i ReportedIssue + err := row.Scan( + &i.ID, + &i.CustomerID, + &i.Subject, + &i.Description, + &i.IssueType, + &i.Status, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const DeleteReportedIssue = `-- name: DeleteReportedIssue :exec +DELETE FROM reported_issues WHERE id = $1 +` + +func (q *Queries) DeleteReportedIssue(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeleteReportedIssue, id) + return err +} + +const ListReportedIssues = `-- name: ListReportedIssues :many +SELECT id, customer_id, subject, description, issue_type, status, metadata, created_at, updated_at FROM reported_issues +ORDER BY created_at DESC +LIMIT $1 OFFSET $2 +` + +type ListReportedIssuesParams struct { + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) ListReportedIssues(ctx context.Context, arg ListReportedIssuesParams) ([]ReportedIssue, error) { + rows, err := q.db.Query(ctx, ListReportedIssues, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ReportedIssue + for rows.Next() { + var i ReportedIssue + if err := rows.Scan( + &i.ID, + &i.CustomerID, + &i.Subject, + &i.Description, + &i.IssueType, + &i.Status, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const ListReportedIssuesByCustomer = `-- name: ListReportedIssuesByCustomer :many +SELECT id, customer_id, subject, description, issue_type, status, metadata, created_at, updated_at FROM reported_issues +WHERE customer_id = $1 +ORDER BY created_at DESC +LIMIT $2 OFFSET $3 +` + +type ListReportedIssuesByCustomerParams struct { + CustomerID int64 `json:"customer_id"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) ListReportedIssuesByCustomer(ctx context.Context, arg ListReportedIssuesByCustomerParams) ([]ReportedIssue, error) { + rows, err := q.db.Query(ctx, ListReportedIssuesByCustomer, arg.CustomerID, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ReportedIssue + for rows.Next() { + var i ReportedIssue + if err := rows.Scan( + &i.ID, + &i.CustomerID, + &i.Subject, + &i.Description, + &i.IssueType, + &i.Status, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const UpdateReportedIssueStatus = `-- name: UpdateReportedIssueStatus :exec +UPDATE reported_issues +SET status = $2, updated_at = NOW() +WHERE id = $1 +` + +type UpdateReportedIssueStatusParams struct { + ID int64 `json:"id"` + Status string `json:"status"` +} + +func (q *Queries) UpdateReportedIssueStatus(ctx context.Context, arg UpdateReportedIssueStatusParams) error { + _, err := q.db.Exec(ctx, UpdateReportedIssueStatus, arg.ID, arg.Status) + return err +} diff --git a/gen/db/models.go b/gen/db/models.go index c10e203..3ba6c5e 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -55,6 +55,24 @@ func (ns NullReferralstatus) Value() (driver.Value, error) { return string(ns.Referralstatus), nil } +type Bank struct { + ID int64 `json:"id"` + Slug string `json:"slug"` + Swift string `json:"swift"` + Name string `json:"name"` + AcctLength int32 `json:"acct_length"` + CountryID int32 `json:"country_id"` + IsMobilemoney pgtype.Int4 `json:"is_mobilemoney"` + IsActive int32 `json:"is_active"` + IsRtgs int32 `json:"is_rtgs"` + Active int32 `json:"active"` + Is24hrs pgtype.Int4 `json:"is_24hrs"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + Currency string `json:"currency"` + BankLogo pgtype.Text `json:"bank_logo"` +} + type Bet struct { ID int64 `json:"id"` Amount int64 `json:"amount"` @@ -233,6 +251,13 @@ type ExchangeRate struct { CreatedAt pgtype.Timestamp `json:"created_at"` } +type FavoriteGame struct { + ID int64 `json:"id"` + UserID int64 `json:"user_id"` + GameID int64 `json:"game_id"` + CreatedAt pgtype.Timestamp `json:"created_at"` +} + type League struct { ID int64 `json:"id"` Name string `json:"name"` @@ -327,6 +352,18 @@ type RefreshToken struct { Revoked bool `json:"revoked"` } +type ReportedIssue struct { + ID int64 `json:"id"` + CustomerID int64 `json:"customer_id"` + Subject string `json:"subject"` + Description string `json:"description"` + IssueType string `json:"issue_type"` + Status string `json:"status"` + Metadata []byte `json:"metadata"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` +} + type Result struct { ID int64 `json:"id"` BetOutcomeID int64 `json:"bet_outcome_id"` @@ -476,6 +513,8 @@ type VirtualGameHistory struct { ID int64 `json:"id"` SessionID pgtype.Text `json:"session_id"` UserID int64 `json:"user_id"` + CompanyID pgtype.Int8 `json:"company_id"` + Provider pgtype.Text `json:"provider"` WalletID pgtype.Int8 `json:"wallet_id"` GameID pgtype.Int8 `json:"game_id"` TransactionType string `json:"transaction_type"` @@ -504,6 +543,9 @@ type VirtualGameTransaction struct { ID int64 `json:"id"` SessionID int64 `json:"session_id"` UserID int64 `json:"user_id"` + CompanyID pgtype.Int8 `json:"company_id"` + Provider pgtype.Text `json:"provider"` + GameID pgtype.Text `json:"game_id"` WalletID int64 `json:"wallet_id"` TransactionType string `json:"transaction_type"` Amount int64 `json:"amount"` diff --git a/gen/db/report.sql.go b/gen/db/report.sql.go index bcaab4d..7040673 100644 --- a/gen/db/report.sql.go +++ b/gen/db/report.sql.go @@ -11,24 +11,132 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const GetBranchWiseReport = `-- name: GetBranchWiseReport :many +SELECT + b.branch_id, + br.name AS branch_name, + br.company_id, + COUNT(*) AS total_bets, + COALESCE(SUM(b.amount), 0) AS total_cash_made, + COALESCE(SUM(CASE WHEN b.cashed_out THEN b.amount ELSE 0 END), 0) AS total_cash_out, + COALESCE(SUM(CASE WHEN b.status = 5 THEN b.amount ELSE 0 END), 0) AS total_cash_backs +FROM bets b +JOIN branches br ON b.branch_id = br.id +WHERE b.created_at BETWEEN $1 AND $2 +GROUP BY b.branch_id, br.name, br.company_id +` + +type GetBranchWiseReportParams struct { + From pgtype.Timestamp `json:"from"` + To pgtype.Timestamp `json:"to"` +} + +type GetBranchWiseReportRow struct { + BranchID pgtype.Int8 `json:"branch_id"` + BranchName string `json:"branch_name"` + CompanyID int64 `json:"company_id"` + TotalBets int64 `json:"total_bets"` + TotalCashMade interface{} `json:"total_cash_made"` + TotalCashOut interface{} `json:"total_cash_out"` + TotalCashBacks interface{} `json:"total_cash_backs"` +} + +func (q *Queries) GetBranchWiseReport(ctx context.Context, arg GetBranchWiseReportParams) ([]GetBranchWiseReportRow, error) { + rows, err := q.db.Query(ctx, GetBranchWiseReport, arg.From, arg.To) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetBranchWiseReportRow + for rows.Next() { + var i GetBranchWiseReportRow + if err := rows.Scan( + &i.BranchID, + &i.BranchName, + &i.CompanyID, + &i.TotalBets, + &i.TotalCashMade, + &i.TotalCashOut, + &i.TotalCashBacks, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetCompanyWiseReport = `-- name: GetCompanyWiseReport :many +SELECT + b.company_id, + c.name AS company_name, + COUNT(*) AS total_bets, + COALESCE(SUM(b.amount), 0) AS total_cash_made, + COALESCE(SUM(CASE WHEN b.cashed_out THEN b.amount ELSE 0 END), 0) AS total_cash_out, + COALESCE(SUM(CASE WHEN b.status = 5 THEN b.amount ELSE 0 END), 0) AS total_cash_backs +FROM bets b +JOIN companies c ON b.company_id = c.id +WHERE b.created_at BETWEEN $1 AND $2 +GROUP BY b.company_id, c.name +` + +type GetCompanyWiseReportParams struct { + From pgtype.Timestamp `json:"from"` + To pgtype.Timestamp `json:"to"` +} + +type GetCompanyWiseReportRow struct { + CompanyID pgtype.Int8 `json:"company_id"` + CompanyName string `json:"company_name"` + TotalBets int64 `json:"total_bets"` + TotalCashMade interface{} `json:"total_cash_made"` + TotalCashOut interface{} `json:"total_cash_out"` + TotalCashBacks interface{} `json:"total_cash_backs"` +} + +func (q *Queries) GetCompanyWiseReport(ctx context.Context, arg GetCompanyWiseReportParams) ([]GetCompanyWiseReportRow, error) { + rows, err := q.db.Query(ctx, GetCompanyWiseReport, arg.From, arg.To) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetCompanyWiseReportRow + for rows.Next() { + var i GetCompanyWiseReportRow + if err := rows.Scan( + &i.CompanyID, + &i.CompanyName, + &i.TotalBets, + &i.TotalCashMade, + &i.TotalCashOut, + &i.TotalCashBacks, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const GetTotalBetsMadeInRange = `-- name: GetTotalBetsMadeInRange :one SELECT COUNT(*) AS total_bets FROM bets WHERE created_at BETWEEN $1 AND $2 - AND ( - company_id = $3 - OR $3 IS NULL -) ` type GetTotalBetsMadeInRangeParams struct { - From pgtype.Timestamp `json:"from"` - To pgtype.Timestamp `json:"to"` - CompanyID pgtype.Int8 `json:"company_id"` + From pgtype.Timestamp `json:"from"` + To pgtype.Timestamp `json:"to"` } func (q *Queries) GetTotalBetsMadeInRange(ctx context.Context, arg GetTotalBetsMadeInRangeParams) (int64, error) { - row := q.db.QueryRow(ctx, GetTotalBetsMadeInRange, arg.From, arg.To, arg.CompanyID) + row := q.db.QueryRow(ctx, GetTotalBetsMadeInRange, arg.From, arg.To) var total_bets int64 err := row.Scan(&total_bets) return total_bets, err @@ -39,20 +147,15 @@ SELECT COALESCE(SUM(amount), 0) AS total_cash_backs FROM bets WHERE created_at BETWEEN $1 AND $2 AND status = 5 - AND ( - company_id = $3 - OR $3 IS NULL -) ` type GetTotalCashBacksInRangeParams struct { - From pgtype.Timestamp `json:"from"` - To pgtype.Timestamp `json:"to"` - CompanyID pgtype.Int8 `json:"company_id"` + From pgtype.Timestamp `json:"from"` + To pgtype.Timestamp `json:"to"` } func (q *Queries) GetTotalCashBacksInRange(ctx context.Context, arg GetTotalCashBacksInRangeParams) (interface{}, error) { - row := q.db.QueryRow(ctx, GetTotalCashBacksInRange, arg.From, arg.To, arg.CompanyID) + row := q.db.QueryRow(ctx, GetTotalCashBacksInRange, arg.From, arg.To) var total_cash_backs interface{} err := row.Scan(&total_cash_backs) return total_cash_backs, err @@ -62,20 +165,15 @@ const GetTotalCashMadeInRange = `-- name: GetTotalCashMadeInRange :one SELECT COALESCE(SUM(amount), 0) AS total_cash_made FROM bets WHERE created_at BETWEEN $1 AND $2 - AND ( - company_id = $3 - OR $3 IS NULL -) ` type GetTotalCashMadeInRangeParams struct { - From pgtype.Timestamp `json:"from"` - To pgtype.Timestamp `json:"to"` - CompanyID pgtype.Int8 `json:"company_id"` + From pgtype.Timestamp `json:"from"` + To pgtype.Timestamp `json:"to"` } func (q *Queries) GetTotalCashMadeInRange(ctx context.Context, arg GetTotalCashMadeInRangeParams) (interface{}, error) { - row := q.db.QueryRow(ctx, GetTotalCashMadeInRange, arg.From, arg.To, arg.CompanyID) + row := q.db.QueryRow(ctx, GetTotalCashMadeInRange, arg.From, arg.To) var total_cash_made interface{} err := row.Scan(&total_cash_made) return total_cash_made, err @@ -86,20 +184,15 @@ SELECT COALESCE(SUM(amount), 0) AS total_cash_out FROM bets WHERE created_at BETWEEN $1 AND $2 AND cashed_out = true - AND ( - company_id = $3 - OR $3 IS NULL -) ` type GetTotalCashOutInRangeParams struct { - From pgtype.Timestamp `json:"from"` - To pgtype.Timestamp `json:"to"` - CompanyID pgtype.Int8 `json:"company_id"` + From pgtype.Timestamp `json:"from"` + To pgtype.Timestamp `json:"to"` } func (q *Queries) GetTotalCashOutInRange(ctx context.Context, arg GetTotalCashOutInRangeParams) (interface{}, error) { - row := q.db.QueryRow(ctx, GetTotalCashOutInRange, arg.From, arg.To, arg.CompanyID) + row := q.db.QueryRow(ctx, GetTotalCashOutInRange, arg.From, arg.To) var total_cash_out interface{} err := row.Scan(&total_cash_out) return total_cash_out, err diff --git a/gen/db/virtual_games.sql.go b/gen/db/virtual_games.sql.go index a65275f..c05d582 100644 --- a/gen/db/virtual_games.sql.go +++ b/gen/db/virtual_games.sql.go @@ -11,10 +11,31 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const AddFavoriteGame = `-- name: AddFavoriteGame :exec +INSERT INTO favorite_games ( + user_id, + game_id, + created_at +) VALUES ($1, $2, NOW()) +ON CONFLICT (user_id, game_id) DO NOTHING +` + +type AddFavoriteGameParams struct { + UserID int64 `json:"user_id"` + GameID int64 `json:"game_id"` +} + +func (q *Queries) AddFavoriteGame(ctx context.Context, arg AddFavoriteGameParams) error { + _, err := q.db.Exec(ctx, AddFavoriteGame, arg.UserID, arg.GameID) + return err +} + const CreateVirtualGameHistory = `-- name: CreateVirtualGameHistory :one INSERT INTO virtual_game_histories ( session_id, user_id, + company_id, + provider, wallet_id, game_id, transaction_type, @@ -24,11 +45,13 @@ INSERT INTO virtual_game_histories ( reference_transaction_id, status ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 ) RETURNING id, session_id, user_id, + company_id, + provider, wallet_id, game_id, transaction_type, @@ -44,6 +67,8 @@ INSERT INTO virtual_game_histories ( type CreateVirtualGameHistoryParams struct { SessionID pgtype.Text `json:"session_id"` UserID int64 `json:"user_id"` + CompanyID pgtype.Int8 `json:"company_id"` + Provider pgtype.Text `json:"provider"` WalletID pgtype.Int8 `json:"wallet_id"` GameID pgtype.Int8 `json:"game_id"` TransactionType string `json:"transaction_type"` @@ -58,6 +83,8 @@ func (q *Queries) CreateVirtualGameHistory(ctx context.Context, arg CreateVirtua row := q.db.QueryRow(ctx, CreateVirtualGameHistory, arg.SessionID, arg.UserID, + arg.CompanyID, + arg.Provider, arg.WalletID, arg.GameID, arg.TransactionType, @@ -72,6 +99,8 @@ func (q *Queries) CreateVirtualGameHistory(ctx context.Context, arg CreateVirtua &i.ID, &i.SessionID, &i.UserID, + &i.CompanyID, + &i.Provider, &i.WalletID, &i.GameID, &i.TransactionType, @@ -129,27 +158,47 @@ func (q *Queries) CreateVirtualGameSession(ctx context.Context, arg CreateVirtua const CreateVirtualGameTransaction = `-- name: CreateVirtualGameTransaction :one INSERT INTO virtual_game_transactions ( - session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status + session_id, user_id, company_id, provider, wallet_id, transaction_type, amount, currency, external_transaction_id, status ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8 -) RETURNING id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 +) RETURNING id, session_id, user_id, company_id, provider, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at ` type CreateVirtualGameTransactionParams struct { - SessionID int64 `json:"session_id"` - UserID int64 `json:"user_id"` - WalletID int64 `json:"wallet_id"` - TransactionType string `json:"transaction_type"` - Amount int64 `json:"amount"` - Currency string `json:"currency"` - ExternalTransactionID string `json:"external_transaction_id"` - Status string `json:"status"` + SessionID int64 `json:"session_id"` + UserID int64 `json:"user_id"` + CompanyID pgtype.Int8 `json:"company_id"` + Provider pgtype.Text `json:"provider"` + WalletID int64 `json:"wallet_id"` + TransactionType string `json:"transaction_type"` + Amount int64 `json:"amount"` + Currency string `json:"currency"` + ExternalTransactionID string `json:"external_transaction_id"` + Status string `json:"status"` } -func (q *Queries) CreateVirtualGameTransaction(ctx context.Context, arg CreateVirtualGameTransactionParams) (VirtualGameTransaction, error) { +type CreateVirtualGameTransactionRow struct { + ID int64 `json:"id"` + SessionID int64 `json:"session_id"` + UserID int64 `json:"user_id"` + CompanyID pgtype.Int8 `json:"company_id"` + Provider pgtype.Text `json:"provider"` + WalletID int64 `json:"wallet_id"` + TransactionType string `json:"transaction_type"` + Amount int64 `json:"amount"` + Currency string `json:"currency"` + ExternalTransactionID string `json:"external_transaction_id"` + Status string `json:"status"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + +func (q *Queries) CreateVirtualGameTransaction(ctx context.Context, arg CreateVirtualGameTransactionParams) (CreateVirtualGameTransactionRow, error) { row := q.db.QueryRow(ctx, CreateVirtualGameTransaction, arg.SessionID, arg.UserID, + arg.CompanyID, + arg.Provider, arg.WalletID, arg.TransactionType, arg.Amount, @@ -157,11 +206,13 @@ func (q *Queries) CreateVirtualGameTransaction(ctx context.Context, arg CreateVi arg.ExternalTransactionID, arg.Status, ) - var i VirtualGameTransaction + var i CreateVirtualGameTransactionRow err := row.Scan( &i.ID, &i.SessionID, &i.UserID, + &i.CompanyID, + &i.Provider, &i.WalletID, &i.TransactionType, &i.Amount, @@ -199,22 +250,26 @@ func (q *Queries) GetVirtualGameSessionByToken(ctx context.Context, sessionToken const GetVirtualGameSummaryInRange = `-- name: GetVirtualGameSummaryInRange :many SELECT + c.name AS company_name, vg.name AS game_name, - COUNT(vgh.id) AS number_of_bets, - COALESCE(SUM(vgh.amount), 0) AS total_transaction_sum -FROM virtual_game_histories vgh -JOIN virtual_games vg ON vgh.game_id = vg.id -WHERE vgh.transaction_type = 'BET' - AND vgh.created_at BETWEEN $1 AND $2 -GROUP BY vg.name + COUNT(vgt.id) AS number_of_bets, + COALESCE(SUM(vgt.amount), 0) AS total_transaction_sum +FROM virtual_game_transactions vgt +JOIN virtual_game_sessions vgs ON vgt.session_id = vgs.id +JOIN virtual_games vg ON vgs.game_id = vg.id +JOIN companies c ON vgt.company_id = c.id +WHERE vgt.transaction_type = 'BET' + AND vgt.created_at BETWEEN $1 AND $2 +GROUP BY c.name, vg.name ` type GetVirtualGameSummaryInRangeParams struct { - CreatedAt pgtype.Timestamp `json:"created_at"` - CreatedAt_2 pgtype.Timestamp `json:"created_at_2"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + CreatedAt_2 pgtype.Timestamptz `json:"created_at_2"` } type GetVirtualGameSummaryInRangeRow struct { + CompanyName string `json:"company_name"` GameName string `json:"game_name"` NumberOfBets int64 `json:"number_of_bets"` TotalTransactionSum interface{} `json:"total_transaction_sum"` @@ -229,7 +284,12 @@ func (q *Queries) GetVirtualGameSummaryInRange(ctx context.Context, arg GetVirtu var items []GetVirtualGameSummaryInRangeRow for rows.Next() { var i GetVirtualGameSummaryInRangeRow - if err := rows.Scan(&i.GameName, &i.NumberOfBets, &i.TotalTransactionSum); err != nil { + if err := rows.Scan( + &i.CompanyName, + &i.GameName, + &i.NumberOfBets, + &i.TotalTransactionSum, + ); err != nil { return nil, err } items = append(items, i) @@ -246,9 +306,23 @@ FROM virtual_game_transactions WHERE external_transaction_id = $1 ` -func (q *Queries) GetVirtualGameTransactionByExternalID(ctx context.Context, externalTransactionID string) (VirtualGameTransaction, error) { +type GetVirtualGameTransactionByExternalIDRow struct { + ID int64 `json:"id"` + SessionID int64 `json:"session_id"` + UserID int64 `json:"user_id"` + WalletID int64 `json:"wallet_id"` + TransactionType string `json:"transaction_type"` + Amount int64 `json:"amount"` + Currency string `json:"currency"` + ExternalTransactionID string `json:"external_transaction_id"` + Status string `json:"status"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + +func (q *Queries) GetVirtualGameTransactionByExternalID(ctx context.Context, externalTransactionID string) (GetVirtualGameTransactionByExternalIDRow, error) { row := q.db.QueryRow(ctx, GetVirtualGameTransactionByExternalID, externalTransactionID) - var i VirtualGameTransaction + var i GetVirtualGameTransactionByExternalIDRow err := row.Scan( &i.ID, &i.SessionID, @@ -265,6 +339,47 @@ func (q *Queries) GetVirtualGameTransactionByExternalID(ctx context.Context, ext return i, err } +const ListFavoriteGames = `-- name: ListFavoriteGames :many +SELECT game_id +FROM favorite_games +WHERE user_id = $1 +` + +func (q *Queries) ListFavoriteGames(ctx context.Context, userID int64) ([]int64, error) { + rows, err := q.db.Query(ctx, ListFavoriteGames, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int64 + for rows.Next() { + var game_id int64 + if err := rows.Scan(&game_id); err != nil { + return nil, err + } + items = append(items, game_id) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const RemoveFavoriteGame = `-- name: RemoveFavoriteGame :exec +DELETE FROM favorite_games +WHERE user_id = $1 AND game_id = $2 +` + +type RemoveFavoriteGameParams struct { + UserID int64 `json:"user_id"` + GameID int64 `json:"game_id"` +} + +func (q *Queries) RemoveFavoriteGame(ctx context.Context, arg RemoveFavoriteGameParams) error { + _, err := q.db.Exec(ctx, RemoveFavoriteGame, arg.UserID, arg.GameID) + return err +} + const UpdateVirtualGameSessionStatus = `-- name: UpdateVirtualGameSessionStatus :exec UPDATE virtual_game_sessions SET status = $2, updated_at = CURRENT_TIMESTAMP diff --git a/gen/db/wallet.sql.go b/gen/db/wallet.sql.go index 172469c..3a49ebf 100644 --- a/gen/db/wallet.sql.go +++ b/gen/db/wallet.sql.go @@ -221,6 +221,50 @@ func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) { return items, nil } +const GetBranchByWalletID = `-- name: GetBranchByWalletID :one +SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at +FROM branches +WHERE wallet_id = $1 +LIMIT 1 +` + +func (q *Queries) GetBranchByWalletID(ctx context.Context, walletID int64) (Branch, error) { + row := q.db.QueryRow(ctx, GetBranchByWalletID, walletID) + var i Branch + err := row.Scan( + &i.ID, + &i.Name, + &i.Location, + &i.IsActive, + &i.WalletID, + &i.BranchManagerID, + &i.CompanyID, + &i.IsSelfOwned, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const GetCompanyByWalletID = `-- name: GetCompanyByWalletID :one +SELECT id, name, admin_id, wallet_id +FROM companies +WHERE wallet_id = $1 +LIMIT 1 +` + +func (q *Queries) GetCompanyByWalletID(ctx context.Context, walletID int64) (Company, error) { + row := q.db.QueryRow(ctx, GetCompanyByWalletID, walletID) + var i Company + err := row.Scan( + &i.ID, + &i.Name, + &i.AdminID, + &i.WalletID, + ) + return i, err +} + const GetCustomerWallet = `-- name: GetCustomerWallet :one SELECT id, customer_id, regular_id, regular_balance, static_id, static_balance, regular_is_active, static_is_active, regular_updated_at, static_updated_at, created_at, first_name, last_name, phone_number FROM customer_wallet_details diff --git a/internal/domain/chapa.go b/internal/domain/chapa.go index 2a3b236..57a090f 100644 --- a/internal/domain/chapa.go +++ b/internal/domain/chapa.go @@ -16,6 +16,7 @@ type PaymentStatus string type WithdrawalStatus string const ( + WithdrawalStatusSuccessful WithdrawalStatus = "success" WithdrawalStatusPending WithdrawalStatus = "pending" WithdrawalStatusProcessing WithdrawalStatus = "processing" WithdrawalStatusCompleted WithdrawalStatus = "completed" @@ -23,9 +24,10 @@ const ( ) const ( - PaymentStatusPending PaymentStatus = "pending" - PaymentStatusCompleted PaymentStatus = "completed" - PaymentStatusFailed PaymentStatus = "failed" + PaymentStatusSuccessful PaymentStatus = "success" + PaymentStatusPending PaymentStatus = "pending" + PaymentStatusCompleted PaymentStatus = "completed" + PaymentStatusFailed PaymentStatus = "failed" ) type ChapaDepositRequest struct { @@ -70,22 +72,23 @@ type ChapaVerificationResponse struct { TxRef string `json:"tx_ref"` } -type Bank struct { - ID int `json:"id"` - Slug string `json:"slug"` - Swift string `json:"swift"` - Name string `json:"name"` - AcctLength int `json:"acct_length"` - CountryID int `json:"country_id"` - IsMobileMoney int `json:"is_mobilemoney"` // nullable - IsActive int `json:"is_active"` - IsRTGS int `json:"is_rtgs"` - Active int `json:"active"` - Is24Hrs int `json:"is_24hrs"` // nullable - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Currency string `json:"currency"` -} +// type Bank struct { +// ID int `json:"id"` +// Slug string `json:"slug"` +// Swift string `json:"swift"` +// Name string `json:"name"` +// AcctLength int `json:"acct_length"` +// CountryID int `json:"country_id"` +// IsMobileMoney int `json:"is_mobilemoney"` // nullable +// IsActive int `json:"is_active"` +// IsRTGS int `json:"is_rtgs"` +// Active int `json:"active"` +// Is24Hrs int `json:"is_24hrs"` // nullable +// CreatedAt time.Time `json:"created_at"` +// UpdatedAt time.Time `json:"updated_at"` +// Currency string `json:"currency"` +// BankLogo string `json:"bank_logo"` // URL or base64 +// } type BankResponse struct { Message string `json:"message"` @@ -142,11 +145,9 @@ type ChapaWithdrawalRequest struct { // } type ChapaWithdrawalResponse struct { - Status string `json:"status"` Message string `json:"message"` - Data struct { - Reference string `json:"reference"` - } `json:"data"` + Status string `json:"status"` + Data string `json:"data"` // Accepts string instead of struct } type ChapaTransactionType struct { diff --git a/internal/domain/institutions.go b/internal/domain/institutions.go new file mode 100644 index 0000000..0e09b57 --- /dev/null +++ b/internal/domain/institutions.go @@ -0,0 +1,21 @@ +package domain + +import "time" + +type Bank struct { + ID int `json:"id"` + Slug string `json:"slug"` + Swift string `json:"swift"` + Name string `json:"name"` + AcctLength int `json:"acct_length"` + CountryID int `json:"country_id"` + IsMobileMoney int `json:"is_mobilemoney"` // nullable + IsActive int `json:"is_active"` + IsRTGS int `json:"is_rtgs"` + Active int `json:"active"` + Is24Hrs int `json:"is_24hrs"` // nullable + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Currency string `json:"currency"` + BankLogo string `json:"bank_logo"` // URL or base64 +} \ No newline at end of file diff --git a/internal/domain/issue_reporting.go b/internal/domain/issue_reporting.go new file mode 100644 index 0000000..1f55aee --- /dev/null +++ b/internal/domain/issue_reporting.go @@ -0,0 +1,15 @@ +package domain + +import "time" + +type ReportedIssue struct { + ID int64 `json:"id"` + CustomerID int64 `json:"customer_id"` + Subject string `json:"subject"` + Description string `json:"description"` + IssueType string `json:"issue_type"` + Status string `json:"status"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/internal/domain/notification.go b/internal/domain/notification.go index 9351d68..db054c1 100644 --- a/internal/domain/notification.go +++ b/internal/domain/notification.go @@ -97,15 +97,16 @@ func FromJSON(data []byte) (*Notification, error) { func ReceiverFromRole(role Role) NotificationRecieverSide { - if role == RoleAdmin { + switch role { + case RoleAdmin: return NotificationRecieverSideAdmin - } else if role == RoleCashier { + case RoleCashier: return NotificationRecieverSideCashier - } else if role == RoleBranchManager { + case RoleBranchManager: return NotificationRecieverSideBranchManager - } else if role == RoleCustomer { + case RoleCustomer: return NotificationRecieverSideCustomer - } else { + default: return "" } } diff --git a/internal/domain/report.go b/internal/domain/report.go index a6c5be0..77bf4bf 100644 --- a/internal/domain/report.go +++ b/internal/domain/report.go @@ -33,6 +33,8 @@ type ReportData struct { Deposits float64 TotalTickets int64 VirtualGameStats []VirtualGameStat + CompanyReports []CompanyReport + BranchReports []BranchReport } type VirtualGameStat struct { @@ -366,3 +368,41 @@ type CashierPerformance struct { LastActivity time.Time `json:"last_activity"` ActiveDays int `json:"active_days"` } + +type CompanyWalletBalance struct { + CompanyID int64 `json:"company_id"` + CompanyName string `json:"company_name"` + Balance float64 `json:"balance"` +} + +type BranchWalletBalance struct { + BranchID int64 `json:"branch_id"` + BranchName string `json:"branch_name"` + CompanyID int64 `json:"company_id"` + Balance float64 `json:"balance"` +} + +type LiveWalletMetrics struct { + Timestamp time.Time `json:"timestamp"` + CompanyBalances []CompanyWalletBalance `json:"company_balances"` + BranchBalances []BranchWalletBalance `json:"branch_balances"` +} + +type CompanyReport struct { + CompanyID int64 + CompanyName string + TotalBets int64 + TotalCashIn float64 + TotalCashOut float64 + TotalCashBacks float64 +} + +type BranchReport struct { + BranchID int64 + BranchName string + CompanyID int64 + TotalBets int64 + TotalCashIn float64 + TotalCashOut float64 + TotalCashBacks float64 +} diff --git a/internal/domain/virtual_game.go b/internal/domain/virtual_game.go index ff35ead..d2174cc 100644 --- a/internal/domain/virtual_game.go +++ b/internal/domain/virtual_game.go @@ -4,6 +4,30 @@ import ( "time" ) +type Provider string + +const ( + PROVIDER_POPOK Provider = "PopOk" + PROVIDER_ALEA_PLAY Provider = "AleaPlay" + PROVIDER_VELI_GAMES Provider = "VeliGames" +) + +type FavoriteGame struct { + ID int64 `json:"id"` + UserID int64 `json:"user_id"` + GameID int64 `json:"game_id"` + CreatedAt time.Time `json:"created_at"` +} + +type FavoriteGameRequest struct { + GameID int64 `json:"game_id"` +} + +type FavoriteGameResponse struct { + GameID int64 `json:"game_id"` + GameName string `json:"game_name"` +} + type VirtualGame struct { ID int64 `json:"id"` Name string `json:"name"` @@ -42,6 +66,8 @@ type VirtualGameHistory struct { ID int64 `json:"id"` SessionID string `json:"session_id,omitempty"` // Optional, if session tracking is used UserID int64 `json:"user_id"` + CompanyID int64 `json:"company_id"` + Provider string `json:"provider"` WalletID *int64 `json:"wallet_id,omitempty"` // Optional if wallet detail is needed GameID *int64 `json:"game_id,omitempty"` // Optional for game-level analysis TransactionType string `json:"transaction_type"` // BET, WIN, CANCEL, etc. @@ -58,6 +84,9 @@ type VirtualGameTransaction struct { ID int64 `json:"id"` SessionID int64 `json:"session_id"` UserID int64 `json:"user_id"` + CompanyID int64 `json:"company_id"` + Provider string `json:"provider"` + GameID string `json:"game_id"` WalletID int64 `json:"wallet_id"` TransactionType string `json:"transaction_type"` // BET, WIN, REFUND, CASHOUT, etc. Amount int64 `json:"amount"` // Always in cents @@ -159,6 +188,11 @@ type PopOKWinResponse struct { Balance float64 `json:"balance"` } +type PopOKGenerateTokenRequest struct { + GameID string `json:"newGameId"` + Token string `json:"token"` +} + type PopOKCancelRequest struct { ExternalToken string `json:"externalToken"` PlayerID string `json:"playerId"` @@ -172,6 +206,10 @@ type PopOKCancelResponse struct { Balance float64 `json:"balance"` } +type PopOKGenerateTokenResponse struct { + NewToken string `json:"newToken"` +} + type AleaPlayCallback struct { EventID string `json:"event_id"` TransactionID string `json:"transaction_id"` diff --git a/internal/repository/institutions.go b/internal/repository/institutions.go new file mode 100644 index 0000000..6cf72a4 --- /dev/null +++ b/internal/repository/institutions.go @@ -0,0 +1,139 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/jackc/pgx/v5/pgtype" +) + +type BankRepository interface { + CreateBank(ctx context.Context, bank *domain.Bank) error + GetBankByID(ctx context.Context, id int) (*domain.Bank, error) + GetAllBanks(ctx context.Context, countryID *int, isActive *int) ([]domain.Bank, error) + UpdateBank(ctx context.Context, bank *domain.Bank) error + DeleteBank(ctx context.Context, id int) error +} + +type BankRepo struct { + store *Store +} + +func NewBankRepository(store *Store) BankRepository { + return &BankRepo{store: store} +} + +func (r *BankRepo) CreateBank(ctx context.Context, bank *domain.Bank) error { + params := dbgen.CreateBankParams{ + Slug: bank.Slug, + Swift: bank.Swift, + Name: bank.Name, + AcctLength: int32(bank.AcctLength), + CountryID: int32(bank.CountryID), + IsMobilemoney: pgtype.Int4{Int32: int32(bank.IsMobileMoney), Valid: true}, + IsActive: int32(bank.IsActive), + IsRtgs: int32(bank.IsRTGS), + Active: int32(bank.Active), + Is24hrs: pgtype.Int4{Int32: int32(bank.Is24Hrs), Valid: true}, + Currency: bank.Currency, + BankLogo: pgtype.Text{String: bank.BankLogo, Valid: true}, + } + createdBank, err := r.store.queries.CreateBank(ctx, params) + if err != nil { + return err + } + // Update the ID and timestamps on the passed struct + bank.ID = int(createdBank.ID) + bank.CreatedAt = createdBank.CreatedAt.Time + bank.UpdatedAt = createdBank.UpdatedAt.Time + return nil +} + +func (r *BankRepo) GetBankByID(ctx context.Context, id int) (*domain.Bank, error) { + dbBank, err := r.store.queries.GetBankByID(ctx, int64(id)) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, err + } + return mapDBBankToDomain(&dbBank), nil +} + +func (r *BankRepo) GetAllBanks(ctx context.Context, countryID *int, isActive *int) ([]domain.Bank, error) { + params := dbgen.GetAllBanksParams{ + CountryID: pgtype.Int4{}, + IsActive: pgtype.Int4{}, + } + if countryID != nil { + params.CountryID = pgtype.Int4{Int32: int32(*countryID), Valid: true} + } + if isActive != nil { + params.IsActive = pgtype.Int4{Int32: int32(*isActive), Valid: true} + } + + dbBanks, err := r.store.queries.GetAllBanks(ctx, params) + if err != nil { + return nil, err + } + + banks := make([]domain.Bank, len(dbBanks)) + for i, b := range dbBanks { + banks[i] = *mapDBBankToDomain(&b) + } + return banks, nil +} + +func (r *BankRepo) UpdateBank(ctx context.Context, bank *domain.Bank) error { + params := dbgen.UpdateBankParams{ + ID: int64(bank.ID), + Slug: pgtype.Text{String: bank.Slug, Valid: true}, + Swift: pgtype.Text{String: bank.Swift, Valid: true}, + Name: pgtype.Text{String: bank.Name, Valid: true}, + AcctLength: pgtype.Int4{Int32: int32(bank.AcctLength), Valid: true}, + CountryID: pgtype.Int4{Int32: int32(bank.CountryID), Valid: true}, + IsMobilemoney: pgtype.Int4{Int32: int32(bank.IsMobileMoney), Valid: true}, + IsActive: pgtype.Int4{Int32: int32(bank.IsActive), Valid: true}, + IsRtgs: pgtype.Int4{Int32: int32(bank.IsRTGS), Valid: true}, + Active: pgtype.Int4{Int32: int32(bank.Active), Valid: true}, + Is24hrs: pgtype.Int4{Int32: int32(bank.Is24Hrs), Valid: true}, + Currency: pgtype.Text{String: bank.Currency, Valid: true}, + BankLogo: pgtype.Text{String: bank.BankLogo, Valid: true}, + } + updatedBank, err := r.store.queries.UpdateBank(ctx, params) + if err != nil { + return err + } + + // update timestamps in domain struct + bank.UpdatedAt = updatedBank.UpdatedAt.Time + return nil +} + +func (r *BankRepo) DeleteBank(ctx context.Context, id int) error { + return r.store.queries.DeleteBank(ctx, int64(id)) +} + +// Helper to map DB struct to domain +func mapDBBankToDomain(dbBank *dbgen.Bank) *domain.Bank { + return &domain.Bank{ + ID: int(dbBank.ID), + Slug: dbBank.Slug, + Swift: dbBank.Swift, + Name: dbBank.Name, + AcctLength: int(dbBank.AcctLength), + CountryID: int(dbBank.CountryID), + IsMobileMoney: int(dbBank.IsMobilemoney.Int32), + IsActive: int(dbBank.IsActive), + IsRTGS: int(dbBank.IsRtgs), + Active: int(dbBank.Active), + Is24Hrs: int(dbBank.Is24hrs.Int32), + CreatedAt: dbBank.CreatedAt.Time, + UpdatedAt: dbBank.UpdatedAt.Time, + Currency: dbBank.Currency, + BankLogo: dbBank.BankLogo.String, + } +} diff --git a/internal/repository/issue_reporting.go b/internal/repository/issue_reporting.go new file mode 100644 index 0000000..01687f3 --- /dev/null +++ b/internal/repository/issue_reporting.go @@ -0,0 +1,65 @@ +package repository + +import ( + "context" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" +) + +type ReportedIssueRepository interface { + CreateReportedIssue(ctx context.Context, arg dbgen.CreateReportedIssueParams) (dbgen.ReportedIssue, error) + ListReportedIssues(ctx context.Context, limit, offset int32) ([]dbgen.ReportedIssue, error) + ListReportedIssuesByCustomer(ctx context.Context, customerID int64, limit, offset int32) ([]dbgen.ReportedIssue, error) + CountReportedIssues(ctx context.Context) (int64, error) + CountReportedIssuesByCustomer(ctx context.Context, customerID int64) (int64, error) + UpdateReportedIssueStatus(ctx context.Context, id int64, status string) error + DeleteReportedIssue(ctx context.Context, id int64) error +} + +type ReportedIssueRepo struct { + store *Store +} + +func NewReportedIssueRepository(store *Store) ReportedIssueRepository { + return &ReportedIssueRepo{store: store} +} + +func (s *ReportedIssueRepo) CreateReportedIssue(ctx context.Context, arg dbgen.CreateReportedIssueParams) (dbgen.ReportedIssue, error) { + return s.store.queries.CreateReportedIssue(ctx, arg) +} + +func (s *ReportedIssueRepo) ListReportedIssues(ctx context.Context, limit, offset int32) ([]dbgen.ReportedIssue, error) { + params := dbgen.ListReportedIssuesParams{ + Limit: limit, + Offset: offset, + } + return s.store.queries.ListReportedIssues(ctx, params) +} + +func (s *ReportedIssueRepo) ListReportedIssuesByCustomer(ctx context.Context, customerID int64, limit, offset int32) ([]dbgen.ReportedIssue, error) { + params := dbgen.ListReportedIssuesByCustomerParams{ + CustomerID: customerID, + Limit: limit, + Offset: offset, + } + return s.store.queries.ListReportedIssuesByCustomer(ctx, params) +} + +func (s *ReportedIssueRepo) CountReportedIssues(ctx context.Context) (int64, error) { + return s.store.queries.CountReportedIssues(ctx) +} + +func (s *ReportedIssueRepo) CountReportedIssuesByCustomer(ctx context.Context, customerID int64) (int64, error) { + return s.store.queries.CountReportedIssuesByCustomer(ctx, customerID) +} + +func (s *ReportedIssueRepo) UpdateReportedIssueStatus(ctx context.Context, id int64, status string) error { + return s.store.queries.UpdateReportedIssueStatus(ctx, dbgen.UpdateReportedIssueStatusParams{ + ID: id, + Status: status, + }) +} + +func (s *ReportedIssueRepo) DeleteReportedIssue(ctx context.Context, id int64) error { + return s.store.queries.DeleteReportedIssue(ctx, id) +} diff --git a/internal/repository/notification.go b/internal/repository/notification.go index 21ace2b..c7fb7e9 100644 --- a/internal/repository/notification.go +++ b/internal/repository/notification.go @@ -317,6 +317,40 @@ func (s *Store) CountUnreadNotifications(ctx context.Context, userID int64) (int return count, nil } +func (s *Store) GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error) { + dbCompany, err := s.queries.GetCompanyByWalletID(ctx, walletID) + if err != nil { + return domain.Company{}, err + } + + return domain.Company{ + ID: dbCompany.ID, + Name: dbCompany.Name, + AdminID: dbCompany.AdminID, + WalletID: dbCompany.WalletID, + }, nil +} + +func (s *Store) GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error) { + dbBranch, err := s.queries.GetBranchByWalletID(ctx, walletID) + if err != nil { + return domain.Branch{}, err + } + + return domain.Branch{ + ID: dbBranch.ID, + Name: dbBranch.Name, + Location: dbBranch.Location, + IsSuspended: dbBranch.IsActive, + WalletID: dbBranch.WalletID, + BranchManagerID: dbBranch.BranchManagerID, + CompanyID: dbBranch.CompanyID, + IsSelfOwned: dbBranch.IsSelfOwned, + // Creat: dbBranch.CreatedAt.Time, + // UpdatedAt: dbBranch.UpdatedAt.Time, + }, nil +} + // func (s *Store) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) { // dbNotifications, err := s.queries.GetAllNotifications(ctx, dbgen.GetAllNotificationsParams{ // Limit: int32(limit), diff --git a/internal/repository/report.go b/internal/repository/report.go index ccbad5e..bff2ad0 100644 --- a/internal/repository/report.go +++ b/internal/repository/report.go @@ -15,13 +15,15 @@ type ReportRepository interface { SaveReport(report *domain.Report) error FindReportsByTimeFrame(timeFrame domain.TimeFrame, limit int) ([]*domain.Report, error) - GetTotalCashOutInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error) - GetTotalCashMadeInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error) - GetTotalCashBacksInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error) - GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time, companyID int64) (int64, error) + GetTotalCashOutInRange(ctx context.Context, from, to time.Time) (float64, error) + GetTotalCashMadeInRange(ctx context.Context, from, to time.Time) (float64, error) + GetTotalCashBacksInRange(ctx context.Context, from, to time.Time) (float64, error) + GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time) (int64, error) GetVirtualGameSummaryInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetVirtualGameSummaryInRangeRow, error) GetAllTicketsInRange(ctx context.Context, from, to time.Time) (dbgen.GetAllTicketsInRangeRow, error) GetWalletTransactionsInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetWalletTransactionsInRangeRow, error) + GetCompanyWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetCompanyWiseReportRow, error) + GetBranchWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetBranchWiseReportRow, error) } type ReportRepo struct { @@ -117,20 +119,18 @@ func (r *ReportRepo) FindReportsByTimeFrame(timeFrame domain.TimeFrame, limit in return reports, nil } -func (r *ReportRepo) GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time, companyID int64) (int64, error) { +func (r *ReportRepo) GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time) (int64, error) { params := dbgen.GetTotalBetsMadeInRangeParams{ - From: ToPgTimestamp(from), - To: ToPgTimestamp(to), - CompanyID: ToPgInt8(companyID), + From: ToPgTimestamp(from), + To: ToPgTimestamp(to), } return r.store.queries.GetTotalBetsMadeInRange(ctx, params) } -func (r *ReportRepo) GetTotalCashBacksInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error) { +func (r *ReportRepo) GetTotalCashBacksInRange(ctx context.Context, from, to time.Time) (float64, error) { params := dbgen.GetTotalCashBacksInRangeParams{ - From: ToPgTimestamp(from), - To: ToPgTimestamp(to), - CompanyID: ToPgInt8(companyID), + From: ToPgTimestamp(from), + To: ToPgTimestamp(to), } value, err := r.store.queries.GetTotalCashBacksInRange(ctx, params) if err != nil { @@ -139,11 +139,10 @@ func (r *ReportRepo) GetTotalCashBacksInRange(ctx context.Context, from, to time return parseFloat(value) } -func (r *ReportRepo) GetTotalCashMadeInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error) { +func (r *ReportRepo) GetTotalCashMadeInRange(ctx context.Context, from, to time.Time) (float64, error) { params := dbgen.GetTotalCashMadeInRangeParams{ - From: ToPgTimestamp(from), - To: ToPgTimestamp(to), - CompanyID: ToPgInt8(companyID), + From: ToPgTimestamp(from), + To: ToPgTimestamp(to), } value, err := r.store.queries.GetTotalCashMadeInRange(ctx, params) if err != nil { @@ -152,11 +151,10 @@ func (r *ReportRepo) GetTotalCashMadeInRange(ctx context.Context, from, to time. return parseFloat(value) } -func (r *ReportRepo) GetTotalCashOutInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error) { +func (r *ReportRepo) GetTotalCashOutInRange(ctx context.Context, from, to time.Time) (float64, error) { params := dbgen.GetTotalCashOutInRangeParams{ - From: ToPgTimestamp(from), - To: ToPgTimestamp(to), - CompanyID: ToPgInt8(companyID), + From: ToPgTimestamp(from), + To: ToPgTimestamp(to), } value, err := r.store.queries.GetTotalCashOutInRange(ctx, params) if err != nil { @@ -183,8 +181,8 @@ func (r *ReportRepo) GetAllTicketsInRange(ctx context.Context, from, to time.Tim func (r *ReportRepo) GetVirtualGameSummaryInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetVirtualGameSummaryInRangeRow, error) { params := dbgen.GetVirtualGameSummaryInRangeParams{ - CreatedAt: ToPgTimestamp(from), - CreatedAt_2: ToPgTimestamp(to), + CreatedAt: ToPgTimestamptz(from), + CreatedAt_2: ToPgTimestamptz(to), } return r.store.queries.GetVirtualGameSummaryInRange(ctx, params) } @@ -193,8 +191,8 @@ func ToPgTimestamp(t time.Time) pgtype.Timestamp { return pgtype.Timestamp{Time: t, Valid: true} } -func ToPgInt8(i int64) pgtype.Int8 { - return pgtype.Int8{Int64: i, Valid: true} +func ToPgTimestamptz(t time.Time) pgtype.Timestamptz { + return pgtype.Timestamptz{Time: t, Valid: true} } func parseFloat(value interface{}) (float64, error) { @@ -218,3 +216,19 @@ func parseFloat(value interface{}) (float64, error) { return 0, fmt.Errorf("unexpected type %T for value: %+v", v, v) } } + +func (r *ReportRepo) GetCompanyWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetCompanyWiseReportRow, error) { + params := dbgen.GetCompanyWiseReportParams{ + From: ToPgTimestamp(from), + To: ToPgTimestamp(to), + } + return r.store.queries.GetCompanyWiseReport(ctx, params) +} + +func (r *ReportRepo) GetBranchWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetBranchWiseReportRow, error) { + params := dbgen.GetBranchWiseReportParams{ + From: ToPgTimestamp(from), + To: ToPgTimestamp(to), + } + return r.store.queries.GetBranchWiseReport(ctx, params) +} diff --git a/internal/repository/virtual_game.go b/internal/repository/virtual_game.go index 25c5280..b4c8e06 100644 --- a/internal/repository/virtual_game.go +++ b/internal/repository/virtual_game.go @@ -19,6 +19,9 @@ type VirtualGameRepository interface { GetVirtualGameTransactionByExternalID(ctx context.Context, externalID string) (*domain.VirtualGameTransaction, error) UpdateVirtualGameTransactionStatus(ctx context.Context, id int64, status string) error // WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error + AddFavoriteGame(ctx context.Context, userID, gameID int64) error + RemoveFavoriteGame(ctx context.Context, userID, gameID int64) error + ListFavoriteGames(ctx context.Context, userID int64) ([]int64, error) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) GetUserGameHistory(ctx context.Context, userID int64) ([]domain.VirtualGameHistory, error) @@ -38,6 +41,26 @@ func NewVirtualGameRepository(store *Store) VirtualGameRepository { return &VirtualGameRepo{store: store} } +func (r *VirtualGameRepo) AddFavoriteGame(ctx context.Context, userID, gameID int64) error { + params := dbgen.AddFavoriteGameParams{ + UserID: userID, + GameID: gameID, + } + return r.store.queries.AddFavoriteGame(ctx, params) +} + +func (r *VirtualGameRepo) RemoveFavoriteGame(ctx context.Context, userID, gameID int64) error { + params := dbgen.RemoveFavoriteGameParams{ + UserID: userID, + GameID: gameID, + } + return r.store.queries.RemoveFavoriteGame(ctx, params) +} + +func (r *VirtualGameRepo) ListFavoriteGames(ctx context.Context, userID int64) ([]int64, error) { + return r.store.queries.ListFavoriteGames(ctx, userID) +} + func (r *VirtualGameRepo) CreateVirtualGameSession(ctx context.Context, session *domain.VirtualGameSession) error { params := dbgen.CreateVirtualGameSessionParams{ UserID: session.UserID, diff --git a/internal/repository/wallet.go b/internal/repository/wallet.go index f352049..4a6ae45 100644 --- a/internal/repository/wallet.go +++ b/internal/repository/wallet.go @@ -275,3 +275,4 @@ func (s *Store) GetTotalWallets(ctx context.Context, filter domain.ReportFilter) return total, nil } + diff --git a/internal/services/chapa/client.go b/internal/services/chapa/client.go index 748bc13..baac2fd 100644 --- a/internal/services/chapa/client.go +++ b/internal/services/chapa/client.go @@ -31,8 +31,8 @@ func NewClient(baseURL, secretKey string) *Client { func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error) { payload := map[string]interface{}{ - "amount": fmt.Sprintf("%.2f", float64(req.Amount)/100), - "currency": req.Currency, + "amount": fmt.Sprintf("%.2f", float64(req.Amount)/100), + "currency": req.Currency, // "email": req.Email, "first_name": req.FirstName, "last_name": req.LastName, @@ -175,6 +175,51 @@ func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain }, nil } +func (c *Client) ManualVerifyTransfer(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) { + url := fmt.Sprintf("%s/transfers/verify/%s", c.baseURL, txRef) + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+c.secretKey) + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var response struct { + Status string `json:"status"` + Amount float64 `json:"amount"` + Currency string `json:"currency"` + } + + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + var status domain.PaymentStatus + switch response.Status { + case "success": + status = domain.PaymentStatusCompleted + default: + status = domain.PaymentStatusFailed + } + + return &domain.ChapaVerificationResponse{ + Status: string(status), + Amount: response.Amount, + Currency: response.Currency, + }, nil +} + func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) { req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/banks", nil) if err != nil { @@ -223,10 +268,6 @@ func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) } func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawalRequest) (bool, error) { - // base, err := url.Parse(c.baseURL) - // if err != nil { - // return false, fmt.Errorf("invalid base URL: %w", err) - // } endpoint := c.baseURL + "/transfers" fmt.Printf("\n\nChapa withdrawal URL is %v\n\n", endpoint) @@ -240,7 +281,9 @@ func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawa return false, fmt.Errorf("failed to create request: %w", err) } - c.setHeaders(httpReq) + // Set headers here + httpReq.Header.Set("Authorization", "Bearer "+c.secretKey) + httpReq.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(httpReq) if err != nil { @@ -249,7 +292,8 @@ func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawa defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return false, fmt.Errorf("chapa api returned status: %d", resp.StatusCode) + body, _ := io.ReadAll(resp.Body) + return false, fmt.Errorf("chapa api returned status: %d - %s", resp.StatusCode, string(body)) } var response domain.ChapaWithdrawalResponse @@ -257,7 +301,7 @@ func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawa return false, fmt.Errorf("failed to decode response: %w", err) } - return response.Status == string(domain.WithdrawalStatusProcessing), nil + return response.Status == string(domain.WithdrawalStatusSuccessful), nil } func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain.ChapaVerificationResponse, error) { diff --git a/internal/services/chapa/service.go b/internal/services/chapa/service.go index 07656bc..96d5145 100644 --- a/internal/services/chapa/service.go +++ b/internal/services/chapa/service.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "strconv" + "strings" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -60,7 +61,9 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma var senderWallet domain.Wallet // Generate unique reference - reference := uuid.New().String() + // reference := uuid.New().String() + reference := fmt.Sprintf("chapa-deposit-%d-%s", userID, uuid.New().String()) + senderWallets, err := s.walletStore.GetWalletsByUser(ctx, userID) if err != nil { return "", fmt.Errorf("failed to get sender wallets: %w", err) @@ -92,8 +95,7 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma Verified: false, } - // Initialize payment with Chapa - response, err := s.chapaClient.InitializePayment(ctx, domain.ChapaDepositRequest{ + payload := domain.ChapaDepositRequest{ Amount: amount, Currency: "ETB", Email: user.Email, @@ -102,7 +104,12 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma TxRef: reference, CallbackURL: s.cfg.CHAPA_CALLBACK_URL, ReturnURL: s.cfg.CHAPA_RETURN_URL, - }) + } + + // Initialize payment with Chapa + response, err := s.chapaClient.InitializePayment(ctx, payload) + + fmt.Printf("\n\nChapa payload is: %+v\n\n", payload) if err != nil { // Update payment status to failed @@ -110,10 +117,14 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma return "", fmt.Errorf("failed to initialize payment: %w", err) } - if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil { + tempTransfer, err := s.transferStore.CreateTransfer(ctx, transfer) + + if err != nil { return "", fmt.Errorf("failed to save payment: %w", err) } + fmt.Printf("\n\nTemp transfer is: %v\n\n", tempTransfer) + return response.CheckoutURL, nil } @@ -185,12 +196,16 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma } success, err := s.chapaClient.InitiateTransfer(ctx, transferReq) - if err != nil || !success { - // Update withdrawal status to failed + if err != nil { _ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed)) return nil, fmt.Errorf("failed to initiate transfer: %w", err) } + if !success { + _ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed)) + return nil, errors.New("chapa rejected the transfer request") + } + // Update withdrawal status to processing if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusProcessing)); err != nil { return nil, fmt.Errorf("failed to update withdrawal status: %w", err) @@ -212,50 +227,68 @@ func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.Bank, error) return banks, nil } -func (s *Service) ManualVerifTransaction(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) { - // First check if we already have a verified record +func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) { + // Lookup transfer by reference transfer, err := s.transferStore.GetTransferByReference(ctx, txRef) - if err == nil && transfer.Verified { + if err != nil { + return nil, fmt.Errorf("transfer not found for reference %s: %w", txRef, err) + } + + if transfer.Verified { return &domain.ChapaVerificationResponse{ Status: string(domain.PaymentStatusCompleted), - Amount: float64(transfer.Amount) / 100, // Convert from cents/kobo + Amount: float64(transfer.Amount) / 100, Currency: "ETB", }, nil } - fmt.Printf("\n\nSender wallet ID is:%v\n\n", transfer.SenderWalletID.Value) - fmt.Printf("\n\nTransfer is:%v\n\n", transfer) - - // just making sure that the sender id is valid + // Validate sender wallet if !transfer.SenderWalletID.Valid { - return nil, fmt.Errorf("sender wallet id is invalid: %v \n", transfer.SenderWalletID) + return nil, fmt.Errorf("invalid sender wallet ID: %v", transfer.SenderWalletID) } - // If not verified or not found, verify with Chapa - verification, err := s.chapaClient.VerifyPayment(ctx, txRef) - if err != nil { - return nil, fmt.Errorf("failed to verify payment: %w", err) - } + var verification *domain.ChapaVerificationResponse - // Update our records if payment is successful - if verification.Status == domain.PaymentStatusCompleted { - err = s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true) + // Decide verification method based on type + switch strings.ToLower(string(transfer.Type)) { + case "deposit": + // Use Chapa Payment Verification + verification, err = s.chapaClient.ManualVerifyPayment(ctx, txRef) if err != nil { - return nil, fmt.Errorf("failed to update verification status: %w", err) + return nil, fmt.Errorf("failed to verify deposit with Chapa: %w", err) } - // Credit user's wallet - err = s.walletStore.UpdateBalance(ctx, transfer.SenderWalletID.Value, transfer.Amount) - if err != nil { - return nil, fmt.Errorf("failed to update wallet balance: %w", err) + if verification.Status == string(domain.PaymentStatusSuccessful) { + // Mark verified + if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil { + return nil, fmt.Errorf("failed to mark deposit transfer as verified: %w", err) + } + + // Credit wallet + if _, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value, transfer.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{}); err != nil { + return nil, fmt.Errorf("failed to credit wallet: %w", err) + } } + + case "withdraw": + // Use Chapa Transfer Verification + verification, err = s.chapaClient.ManualVerifyTransfer(ctx, txRef) + if err != nil { + return nil, fmt.Errorf("failed to verify withdrawal with Chapa: %w", err) + } + + if verification.Status == string(domain.PaymentStatusSuccessful) { + // Mark verified (withdraw doesn't affect balance) + if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil { + return nil, fmt.Errorf("failed to mark withdrawal transfer as verified: %w", err) + } + } + + default: + return nil, fmt.Errorf("unsupported transfer type: %s", transfer.Type) } - return &domain.ChapaVerificationResponse{ - Status: string(verification.Status), - Amount: float64(verification.Amount), - Currency: verification.Currency, - }, nil + return verification, nil } func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebHookTransfer) error { @@ -281,12 +314,13 @@ func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domai // verified = true // } - if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil { - return fmt.Errorf("failed to update payment status: %w", err) - } - // If payment is completed, credit user's wallet - if transfer.Status == string(domain.PaymentStatusCompleted) { + if transfer.Status == string(domain.PaymentStatusSuccessful) { + + if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil { + return fmt.Errorf("failed to update payment status: %w", err) + } + if _, err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID.Value, payment.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{ ReferenceNumber: domain.ValidString{ Value: transfer.Reference, @@ -322,12 +356,11 @@ func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domai // verified = true // } - if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil { - return fmt.Errorf("failed to update payment status: %w", err) - } - - // If payment is completed, credit user's wallet - if payment.Status == string(domain.PaymentStatusFailed) { + if payment.Status == string(domain.PaymentStatusSuccessful) { + if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil { + return fmt.Errorf("failed to update payment status: %w", err) + } // If payment is completed, credit user's walle + } else { if _, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value, transfer.Amount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil { return fmt.Errorf("failed to credit user wallet: %w", err) } diff --git a/internal/services/institutions/port.go b/internal/services/institutions/port.go new file mode 100644 index 0000000..b73a84a --- /dev/null +++ b/internal/services/institutions/port.go @@ -0,0 +1 @@ +package institutions diff --git a/internal/services/institutions/service.go b/internal/services/institutions/service.go new file mode 100644 index 0000000..9b54cd1 --- /dev/null +++ b/internal/services/institutions/service.go @@ -0,0 +1,44 @@ +package institutions + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" +) + +type Service struct { + repo repository.BankRepository +} + +func New(repo repository.BankRepository) *Service { + return &Service{repo: repo} +} + +func (s *Service) Create(ctx context.Context, bank *domain.Bank) error { + return s.repo.CreateBank(ctx, bank) +} + +func (s *Service) Update(ctx context.Context, bank *domain.Bank) error { + return s.repo.UpdateBank(ctx, bank) +} + +func (s *Service) GetByID(ctx context.Context, id int64) (*domain.Bank, error) { + return s.repo.GetBankByID(ctx, int(id)) +} + +func (s *Service) Delete(ctx context.Context, id int64) error { + return s.repo.DeleteBank(ctx, int(id)) +} + +func (s *Service) List(ctx context.Context) ([]*domain.Bank, error) { + banks, err := s.repo.GetAllBanks(ctx, nil, nil) + if err != nil { + return nil, err + } + result := make([]*domain.Bank, len(banks)) + for i := range banks { + result[i] = &banks[i] + } + return result, nil +} diff --git a/internal/services/issue_reporting/service.go b/internal/services/issue_reporting/service.go new file mode 100644 index 0000000..88aba2f --- /dev/null +++ b/internal/services/issue_reporting/service.go @@ -0,0 +1,83 @@ +package issuereporting + +import ( + "context" + "errors" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" +) + +type Service struct { + repo repository.ReportedIssueRepository +} + +func New(repo repository.ReportedIssueRepository) *Service { + return &Service{repo: repo} +} + +func (s *Service) CreateReportedIssue(ctx context.Context, issue domain.ReportedIssue) (domain.ReportedIssue, error) { + params := dbgen.CreateReportedIssueParams{ + // Map fields from domain.ReportedIssue to dbgen.CreateReportedIssueParams here. + // Example: + // Title: issue.Title, + // Description: issue.Description, + // CustomerID: issue.CustomerID, + // Status: issue.Status, + // Add other fields as necessary. + } + dbIssue, err := s.repo.CreateReportedIssue(ctx, params) + if err != nil { + return domain.ReportedIssue{}, err + } + // Map dbgen.ReportedIssue to domain.ReportedIssue + reportedIssue := domain.ReportedIssue{ + ID: dbIssue.ID, + Subject: dbIssue.Subject, + Description: dbIssue.Description, + CustomerID: dbIssue.CustomerID, + Status: dbIssue.Status, + CreatedAt: dbIssue.CreatedAt.Time, + UpdatedAt: dbIssue.UpdatedAt.Time, + // Add other fields as necessary + } + return reportedIssue, nil +} + +func (s *Service) GetIssuesForCustomer(ctx context.Context, customerID int64, limit, offset int) ([]domain.ReportedIssue, error) { + dbIssues, err := s.repo.ListReportedIssuesByCustomer(ctx, customerID, int32(limit), int32(offset)) + if err != nil { + return nil, err + } + reportedIssues := make([]domain.ReportedIssue, len(dbIssues)) + for i, dbIssue := range dbIssues { + reportedIssues[i] = domain.ReportedIssue{ + ID: dbIssue.ID, + Subject: dbIssue.Subject, + Description: dbIssue.Description, + CustomerID: dbIssue.CustomerID, + Status: dbIssue.Status, + CreatedAt: dbIssue.CreatedAt.Time, + UpdatedAt: dbIssue.UpdatedAt.Time, + // Add other fields as necessary + } + } + return reportedIssues, nil +} + +func (s *Service) GetAllIssues(ctx context.Context, limit, offset int) ([]dbgen.ReportedIssue, error) { + return s.repo.ListReportedIssues(ctx, int32(limit), int32(offset)) +} + +func (s *Service) UpdateIssueStatus(ctx context.Context, issueID int64, status string) error { + validStatuses := map[string]bool{"pending": true, "in_progress": true, "resolved": true, "rejected": true} + if !validStatuses[status] { + return errors.New("invalid status") + } + return s.repo.UpdateReportedIssueStatus(ctx, issueID, status) +} + +func (s *Service) DeleteIssue(ctx context.Context, issueID int64) error { + return s.repo.DeleteReportedIssue(ctx, issueID) +} diff --git a/internal/services/notfication/port.go b/internal/services/notfication/port.go index 2d03f80..d20f4bc 100644 --- a/internal/services/notfication/port.go +++ b/internal/services/notfication/port.go @@ -8,6 +8,8 @@ import ( ) type NotificationStore interface { + GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error) + GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error) SendNotification(ctx context.Context, notification *domain.Notification) error MarkAsRead(ctx context.Context, notificationID string, recipientID int64) error ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error) diff --git a/internal/services/notfication/service.go b/internal/services/notfication/service.go index 547bb59..f3ca2d6 100644 --- a/internal/services/notfication/service.go +++ b/internal/services/notfication/service.go @@ -12,6 +12,8 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" + + // "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/ws" afro "github.com/amanuelabay/afrosms-go" "github.com/gorilla/websocket" @@ -19,20 +21,21 @@ import ( ) type Service struct { - repo repository.NotificationRepository - Hub *ws.NotificationHub - connections sync.Map - notificationCh chan *domain.Notification - stopCh chan struct{} - config *config.Config - logger *slog.Logger - redisClient *redis.Client + repo repository.NotificationRepository + Hub *ws.NotificationHub + notificationStore NotificationStore + connections sync.Map + notificationCh chan *domain.Notification + stopCh chan struct{} + config *config.Config + logger *slog.Logger + redisClient *redis.Client } func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *config.Config) *Service { hub := ws.NewNotificationHub() rdb := redis.NewClient(&redis.Options{ - Addr: cfg.RedisAddr, // e.g., “redis:6379” + Addr: cfg.RedisAddr, // e.g., "redis:6379" }) svc := &Service{ @@ -264,7 +267,8 @@ func (s *Service) retryFailedNotifications() { go func(notification *domain.Notification) { for attempt := 0; attempt < 3; attempt++ { time.Sleep(time.Duration(attempt) * time.Second) - if notification.DeliveryChannel == domain.DeliveryChannelSMS { + switch notification.DeliveryChannel { + case domain.DeliveryChannelSMS: if err := s.SendSMS(ctx, notification.RecipientID, notification.Payload.Message); err == nil { notification.DeliveryStatus = domain.DeliveryStatusSent if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil { @@ -273,7 +277,7 @@ func (s *Service) retryFailedNotifications() { s.logger.Info("[NotificationSvc.RetryFailedNotifications] Successfully retried notification", "id", notification.ID) return } - } else if notification.DeliveryChannel == domain.DeliveryChannelEmail { + case domain.DeliveryChannelEmail: if err := s.SendEmail(ctx, notification.RecipientID, notification.Payload.Headline, notification.Payload.Message); err == nil { notification.DeliveryStatus = domain.DeliveryStatusSent if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil { @@ -303,52 +307,60 @@ func (s *Service) RunRedisSubscriber(ctx context.Context) { ch := pubsub.Channel() for msg := range ch { - var payload domain.LiveMetric - if err := json.Unmarshal([]byte(msg.Payload), &payload); err != nil { - s.logger.Error("[NotificationSvc.runRedisSubscriber] failed unmarshal metric", "error", err) + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(msg.Payload), &parsed); err != nil { + s.logger.Error("invalid Redis message format", "payload", msg.Payload, "error", err) continue } - // Broadcast via WebSocket Hub - s.Hub.Broadcast <- map[string]interface{}{ - "type": "LIVE_METRIC_UPDATE", + + eventType, _ := parsed["type"].(string) + payload := parsed["payload"] + recipientID, hasRecipient := parsed["recipient_id"] + recipientType, _ := parsed["recipient_type"].(string) + + message := map[string]interface{}{ + "type": eventType, "payload": payload, } + + if hasRecipient { + message["recipient_id"] = recipientID + message["recipient_type"] = recipientType + } + + s.Hub.Broadcast <- message } } -func (s *Service) UpdateLiveMetrics(ctx context.Context, updates domain.MetricUpdates) error { +func (s *Service) UpdateLiveWalletMetrics(ctx context.Context, companies []domain.GetCompany, branches []domain.BranchWallet) error { const key = "live_metrics" - val, err := s.redisClient.Get(ctx, key).Result() - var metric domain.LiveMetric - if err == redis.Nil { - metric = domain.LiveMetric{} - } else if err != nil { - return err - } else { - if err := json.Unmarshal([]byte(val), &metric); err != nil { - return err - } + companyBalances := make([]domain.CompanyWalletBalance, 0, len(companies)) + for _, c := range companies { + companyBalances = append(companyBalances, domain.CompanyWalletBalance{ + CompanyID: c.ID, + CompanyName: c.Name, + Balance: float64(c.WalletBalance.Float32()), + }) } - // Apply increments if provided - if updates.TotalCashSportsbookDelta != nil { - metric.TotalCashSportsbook += *updates.TotalCashSportsbookDelta - } - if updates.TotalCashSportGamesDelta != nil { - metric.TotalCashSportGames += *updates.TotalCashSportGamesDelta - } - if updates.TotalLiveTicketsDelta != nil { - metric.TotalLiveTickets += *updates.TotalLiveTicketsDelta - } - if updates.TotalUnsettledCashDelta != nil { - metric.TotalUnsettledCash += *updates.TotalUnsettledCashDelta - } - if updates.TotalGamesDelta != nil { - metric.TotalGames += *updates.TotalGamesDelta + branchBalances := make([]domain.BranchWalletBalance, 0, len(branches)) + for _, b := range branches { + branchBalances = append(branchBalances, domain.BranchWalletBalance{ + BranchID: b.ID, + BranchName: b.Name, + CompanyID: b.CompanyID, + Balance: float64(b.Balance.Float32()), + }) } - updatedData, err := json.Marshal(metric) + payload := domain.LiveWalletMetrics{ + Timestamp: time.Now(), + CompanyBalances: companyBalances, + BranchBalances: branchBalances, + } + + updatedData, err := json.Marshal(payload) if err != nil { return err } @@ -357,11 +369,9 @@ func (s *Service) UpdateLiveMetrics(ctx context.Context, updates domain.MetricUp return err } - if err := s.redisClient.Publish(ctx, "live_metrics", updatedData).Err(); err != nil { + if err := s.redisClient.Publish(ctx, key, updatedData).Err(); err != nil { return err } - - s.logger.Info("[NotificationSvc.UpdateLiveMetrics] Live metrics updated and broadcasted") return nil } @@ -383,3 +393,83 @@ func (s *Service) GetLiveMetrics(ctx context.Context) (domain.LiveMetric, error) return metric, nil } + +func (s *Service) UpdateLiveWalletMetricForWallet(ctx context.Context, wallet domain.Wallet) { + var ( + payload domain.LiveWalletMetrics + event map[string]interface{} + key = "live_metrics" + ) + + // Try company first + company, companyErr := s.notificationStore.GetCompanyByWalletID(ctx, wallet.ID) + if companyErr == nil { + payload = domain.LiveWalletMetrics{ + Timestamp: time.Now(), + CompanyBalances: []domain.CompanyWalletBalance{{ + CompanyID: company.ID, + CompanyName: company.Name, + Balance: float64(wallet.Balance), + }}, + BranchBalances: []domain.BranchWalletBalance{}, + } + + event = map[string]interface{}{ + "type": "LIVE_WALLET_METRICS_UPDATE", + "recipient_id": company.ID, + "recipient_type": "company", + "payload": payload, + } + } else { + // Try branch next + branch, branchErr := s.notificationStore.GetBranchByWalletID(ctx, wallet.ID) + if branchErr == nil { + payload = domain.LiveWalletMetrics{ + Timestamp: time.Now(), + CompanyBalances: []domain.CompanyWalletBalance{}, + BranchBalances: []domain.BranchWalletBalance{{ + BranchID: branch.ID, + BranchName: branch.Name, + CompanyID: branch.CompanyID, + Balance: float64(wallet.Balance), + }}, + } + + event = map[string]interface{}{ + "type": "LIVE_WALLET_METRICS_UPDATE", + "recipient_id": branch.ID, + "recipient_type": "branch", + "payload": payload, + } + } else { + // Neither company nor branch matched this wallet + s.logger.Warn("wallet not linked to any company or branch", "walletID", wallet.ID) + return + } + } + + // Save latest metric to Redis + if jsonBytes, err := json.Marshal(payload); err == nil { + s.redisClient.Set(ctx, key, jsonBytes, 0) + } else { + s.logger.Error("failed to marshal wallet metrics payload", "walletID", wallet.ID, "err", err) + } + + // Publish via Redis + if jsonEvent, err := json.Marshal(event); err == nil { + s.redisClient.Publish(ctx, key, jsonEvent) + } else { + s.logger.Error("failed to marshal event payload", "walletID", wallet.ID, "err", err) + } + + // Broadcast over WebSocket + s.Hub.Broadcast <- event +} + +func (s *Service) GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error) { + return s.notificationStore.GetCompanyByWalletID(ctx, walletID) +} + +func (s *Service) GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error) { + return s.notificationStore.GetBranchByWalletID(ctx, walletID) +} diff --git a/internal/services/report/service.go b/internal/services/report/service.go index 8a15335..3e047e3 100644 --- a/internal/services/report/service.go +++ b/internal/services/report/service.go @@ -476,6 +476,7 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error { defer writer.Flush() // Summary section + writer.Write([]string{"Sports Betting Reports (Periodic)"}) writer.Write([]string{"Period", "Total Bets", "Total Cash Made", "Total Cash Out", "Total Cash Backs", "Total Deposits", "Total Withdrawals", "Total Tickets"}) writer.Write([]string{ period, @@ -491,6 +492,7 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error { writer.Write([]string{}) // Empty line for spacing // Virtual Game Summary section + writer.Write([]string{"Virtual Game Reports (Periodic)"}) writer.Write([]string{"Game Name", "Number of Bets", "Total Transaction Sum"}) for _, row := range data.VirtualGameStats { writer.Write([]string{ @@ -500,18 +502,66 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error { }) } + writer.Write([]string{}) // Empty line + writer.Write([]string{"Company Reports (Periodic)"}) + writer.Write([]string{"Company ID", "Company Name", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"}) + for _, cr := range data.CompanyReports { + writer.Write([]string{ + fmt.Sprintf("%d", cr.CompanyID), + cr.CompanyName, + fmt.Sprintf("%d", cr.TotalBets), + fmt.Sprintf("%.2f", cr.TotalCashIn), + fmt.Sprintf("%.2f", cr.TotalCashOut), + fmt.Sprintf("%.2f", cr.TotalCashBacks), + }) + } + + writer.Write([]string{}) // Empty line + writer.Write([]string{"Branch Reports (Periodic)"}) + writer.Write([]string{"Branch ID", "Branch Name", "Company ID", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"}) + for _, br := range data.BranchReports { + writer.Write([]string{ + fmt.Sprintf("%d", br.BranchID), + br.BranchName, + fmt.Sprintf("%d", br.CompanyID), + fmt.Sprintf("%d", br.TotalBets), + fmt.Sprintf("%.2f", br.TotalCashIn), + fmt.Sprintf("%.2f", br.TotalCashOut), + fmt.Sprintf("%.2f", br.TotalCashBacks), + }) + } + + var totalBets int64 + var totalCashIn, totalCashOut, totalCashBacks float64 + for _, cr := range data.CompanyReports { + totalBets += cr.TotalBets + totalCashIn += cr.TotalCashIn + totalCashOut += cr.TotalCashOut + totalCashBacks += cr.TotalCashBacks + } + + writer.Write([]string{}) + writer.Write([]string{"Total Summary"}) + writer.Write([]string{"Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"}) + writer.Write([]string{ + fmt.Sprintf("%d", totalBets), + fmt.Sprintf("%.2f", totalCashIn), + fmt.Sprintf("%.2f", totalCashOut), + fmt.Sprintf("%.2f", totalCashBacks), + }) + return nil } func (s *Service) fetchReportData(ctx context.Context, period string) (domain.ReportData, error) { from, to := getTimeRange(period) - companyID := int64(0) + // companyID := int64(0) // Basic metrics - totalBets, _ := s.repo.GetTotalBetsMadeInRange(ctx, from, to, companyID) - cashIn, _ := s.repo.GetTotalCashMadeInRange(ctx, from, to, companyID) - cashOut, _ := s.repo.GetTotalCashOutInRange(ctx, from, to, companyID) - cashBacks, _ := s.repo.GetTotalCashBacksInRange(ctx, from, to, companyID) + totalBets, _ := s.repo.GetTotalBetsMadeInRange(ctx, from, to) + cashIn, _ := s.repo.GetTotalCashMadeInRange(ctx, from, to) + cashOut, _ := s.repo.GetTotalCashOutInRange(ctx, from, to) + cashBacks, _ := s.repo.GetTotalCashBacksInRange(ctx, from, to) // Wallet Transactions transactions, _ := s.repo.GetWalletTransactionsInRange(ctx, from, to) @@ -555,6 +605,113 @@ func (s *Service) fetchReportData(ctx context.Context, period string) (domain.Re }) } + companyRows, _ := s.repo.GetCompanyWiseReport(ctx, from, to) + var companyReports []domain.CompanyReport + for _, row := range companyRows { + var totalCashIn, totalCashOut, totalCashBacks float64 + switch v := row.TotalCashMade.(type) { + case string: + val, err := strconv.ParseFloat(v, 64) + if err == nil { + totalCashIn = val + } + case float64: + totalCashIn = v + case int: + totalCashIn = float64(v) + default: + totalCashIn = 0 + } + switch v := row.TotalCashOut.(type) { + case string: + val, err := strconv.ParseFloat(v, 64) + if err == nil { + totalCashOut = val + } + case float64: + totalCashOut = v + case int: + totalCashOut = float64(v) + default: + totalCashOut = 0 + } + switch v := row.TotalCashBacks.(type) { + case string: + val, err := strconv.ParseFloat(v, 64) + if err == nil { + totalCashBacks = val + } + case float64: + totalCashBacks = v + case int: + totalCashBacks = float64(v) + default: + totalCashBacks = 0 + } + companyReports = append(companyReports, domain.CompanyReport{ + CompanyID: row.CompanyID.Int64, + CompanyName: row.CompanyName, + TotalBets: row.TotalBets, + TotalCashIn: totalCashIn, + TotalCashOut: totalCashOut, + TotalCashBacks: totalCashBacks, + }) + } + + branchRows, _ := s.repo.GetBranchWiseReport(ctx, from, to) + var branchReports []domain.BranchReport + for _, row := range branchRows { + var totalCashIn, totalCashOut, totalCashBacks float64 + switch v := row.TotalCashMade.(type) { + case string: + val, err := strconv.ParseFloat(v, 64) + if err == nil { + totalCashIn = val + } + case float64: + totalCashIn = v + case int: + totalCashIn = float64(v) + default: + totalCashIn = 0 + } + switch v := row.TotalCashOut.(type) { + case string: + val, err := strconv.ParseFloat(v, 64) + if err == nil { + totalCashOut = val + } + case float64: + totalCashOut = v + case int: + totalCashOut = float64(v) + default: + totalCashOut = 0 + } + switch v := row.TotalCashBacks.(type) { + case string: + val, err := strconv.ParseFloat(v, 64) + if err == nil { + totalCashBacks = val + } + case float64: + totalCashBacks = v + case int: + totalCashBacks = float64(v) + default: + totalCashBacks = 0 + } + branchReports = append(branchReports, domain.BranchReport{ + BranchID: row.BranchID.Int64, + BranchName: row.BranchName, + CompanyID: row.CompanyID, + TotalBets: row.TotalBets, + TotalCashIn: totalCashIn, + TotalCashOut: totalCashOut, + TotalCashBacks: totalCashBacks, + }) + } + return domain.ReportData{ TotalBets: totalBets, TotalCashIn: cashIn, @@ -564,6 +721,8 @@ func (s *Service) fetchReportData(ctx context.Context, period string) (domain.Re Withdrawals: totalWithdrawals, TotalTickets: totalTickets.TotalTickets, VirtualGameStats: virtualGameStatsDomain, + CompanyReports: companyReports, + BranchReports: branchReports, }, nil } @@ -595,8 +754,6 @@ func getTimeRange(period string) (time.Time, time.Time) { } } - - // func (s *Service) GetCompanyPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.CompanyPerformance, error) { // // Get company bet activity // companyBets, err := s.betStore.GetCompanyBetActivity(ctx, filter) diff --git a/internal/services/ticket/service.go b/internal/services/ticket/service.go index d1f9ff1..6a91b7d 100644 --- a/internal/services/ticket/service.go +++ b/internal/services/ticket/service.go @@ -236,13 +236,13 @@ func (s *Service) CreateTicket(ctx context.Context, req domain.CreateTicketReq, return domain.Ticket{}, rows, err } - updates := domain.MetricUpdates{ - TotalLiveTicketsDelta: domain.PtrInt64(1), - } + // updates := domain.MetricUpdates{ + // TotalLiveTicketsDelta: domain.PtrInt64(1), + // } - if err := s.notificationSvc.UpdateLiveMetrics(ctx, updates); err != nil { - // handle error - } + // if err := s.notificationSvc.UpdateLiveMetrics(ctx, updates); err != nil { + // // handle error + // } return ticket, rows, nil } diff --git a/internal/services/virtualGame/port.go b/internal/services/virtualGame/port.go index 173598f..36f57de 100644 --- a/internal/services/virtualGame/port.go +++ b/internal/services/virtualGame/port.go @@ -13,8 +13,13 @@ type VirtualGameService interface { GetPlayerInfo(ctx context.Context, req *domain.PopOKPlayerInfoRequest) (*domain.PopOKPlayerInfoResponse, error) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequest) (*domain.PopOKCancelResponse, error) + ProcessTournamentWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error) + ProcessPromoWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) ListGames(ctx context.Context, currency string) ([]domain.PopOKGame, error) RecommendGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error) + AddFavoriteGame(ctx context.Context, userID, gameID int64) error + RemoveFavoriteGame(ctx context.Context, userID, gameID int64) error + ListFavoriteGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error) } diff --git a/internal/services/virtualGame/service.go b/internal/services/virtualGame/service.go index b795b33..e413993 100644 --- a/internal/services/virtualGame/service.go +++ b/internal/services/virtualGame/service.go @@ -52,6 +52,7 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI sessionId := fmt.Sprintf("%d-%s-%d", userID, gameID, time.Now().UnixNano()) token, err := jwtutil.CreatePopOKJwt( userID, + user.CompanyID, user.FirstName, currency, "en", @@ -69,6 +70,8 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI tx := &domain.VirtualGameHistory{ SessionID: sessionId, // Optional: populate if session tracking is implemented UserID: userID, + CompanyID: user.CompanyID.Value, + Provider: string(domain.PROVIDER_POPOK), GameID: toInt64Ptr(gameID), TransactionType: "LAUNCH", Amount: 0, @@ -211,8 +214,11 @@ func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) ( // Create transaction record tx := &domain.VirtualGameTransaction{ UserID: claims.UserID, + CompanyID: claims.CompanyID.Value, + Provider: string(domain.PROVIDER_POPOK), + GameID: req.GameID, TransactionType: "BET", - Amount: -amountCents, // Negative for bets + Amount: amountCents, // Negative for bets Currency: req.Currency, ExternalTransactionID: req.TransactionID, Status: "COMPLETED", @@ -279,6 +285,9 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) ( // 5. Create transaction record tx := &domain.VirtualGameTransaction{ UserID: claims.UserID, + CompanyID: claims.CompanyID.Value, + Provider: string(domain.PROVIDER_POPOK), + GameID: req.GameID, TransactionType: "WIN", Amount: amountCents, Currency: req.Currency, @@ -299,6 +308,167 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) ( }, nil } +func (s *service) ProcessTournamentWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error) { + // 1. Validate token and get user ID + claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey) + if err != nil { + s.logger.Error("Invalid token in tournament win request", "error", err) + return nil, fmt.Errorf("invalid token") + } + + // 2. Check for duplicate tournament win transaction + existingTx, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID) + if err != nil { + s.logger.Error("Failed to check existing tournament transaction", "error", err) + return nil, fmt.Errorf("transaction check failed") + } + if existingTx != nil && existingTx.TransactionType == "TOURNAMENT_WIN" { + s.logger.Warn("Duplicate tournament win", "transactionID", req.TransactionID) + wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) + balance := 0.0 + if len(wallets) > 0 { + balance = float64(wallets[0].Balance) / 100 + } + return &domain.PopOKWinResponse{ + TransactionID: req.TransactionID, + ExternalTrxID: fmt.Sprintf("%v", existingTx.ID), + Balance: balance, + }, nil + } + + // 3. Convert amount to cents + amountCents := int64(req.Amount * 100) + + // 4. Credit user wallet + if _, err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil { + s.logger.Error("Failed to credit wallet for tournament", "userID", claims.UserID, "error", err) + return nil, fmt.Errorf("wallet credit failed") + } + + // 5. Log tournament win transaction + tx := &domain.VirtualGameTransaction{ + UserID: claims.UserID, + TransactionType: "TOURNAMENT_WIN", + Amount: amountCents, + Currency: req.Currency, + ExternalTransactionID: req.TransactionID, + Status: "COMPLETED", + CreatedAt: time.Now(), + } + + if err := s.repo.CreateVirtualGameTransaction(ctx, tx); err != nil { + s.logger.Error("Failed to record tournament win transaction", "error", err) + return nil, fmt.Errorf("transaction recording failed") + } + + // 6. Fetch updated balance + wallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) + if err != nil { + return nil, fmt.Errorf("Failed to get wallet balance") + } + + return &domain.PopOKWinResponse{ + TransactionID: req.TransactionID, + ExternalTrxID: fmt.Sprintf("%v", tx.ID), + Balance: float64(wallets[0].Balance) / 100, + }, nil +} + +func (s *service) ProcessPromoWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error) { + claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey) + if err != nil { + s.logger.Error("Invalid token in promo win request", "error", err) + return nil, fmt.Errorf("invalid token") + } + + existingTx, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID) + if err != nil { + s.logger.Error("Failed to check existing promo transaction", "error", err) + return nil, fmt.Errorf("transaction check failed") + } + if existingTx != nil && existingTx.TransactionType == "PROMO_WIN" { + s.logger.Warn("Duplicate promo win", "transactionID", req.TransactionID) + wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) + balance := 0.0 + if len(wallets) > 0 { + balance = float64(wallets[0].Balance) / 100 + } + return &domain.PopOKWinResponse{ + TransactionID: req.TransactionID, + ExternalTrxID: fmt.Sprintf("%v", existingTx.ID), + Balance: balance, + }, nil + } + + amountCents := int64(req.Amount * 100) + + if _, err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil { + s.logger.Error("Failed to credit wallet for promo", "userID", claims.UserID, "error", err) + return nil, fmt.Errorf("wallet credit failed") + } + + tx := &domain.VirtualGameTransaction{ + UserID: claims.UserID, + TransactionType: "PROMO_WIN", + Amount: amountCents, + Currency: req.Currency, + ExternalTransactionID: req.TransactionID, + Status: "COMPLETED", + CreatedAt: time.Now(), + } + + if err := s.repo.CreateVirtualGameTransaction(ctx, tx); err != nil { + s.logger.Error("Failed to create promo win transaction", "error", err) + return nil, fmt.Errorf("transaction recording failed") + } + + wallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) + if err != nil { + return nil, fmt.Errorf("failed to read wallets") + } + + return &domain.PopOKWinResponse{ + TransactionID: req.TransactionID, + ExternalTrxID: fmt.Sprintf("%v", tx.ID), + Balance: float64(wallets[0].Balance) / 100, + }, nil +} + +// func (s *service) GenerateNewToken(ctx context.Context, req *domain.PopOKGenerateTokenRequest) (*domain.PopOKGenerateTokenResponse, error) { +// userID, err := strconv.ParseInt(req.PlayerID, 10, 64) +// if err != nil { +// s.logger.Error("Invalid player ID", "playerID", req.PlayerID, "error", err) +// return nil, fmt.Errorf("invalid player ID") +// } + +// user, err := s.store.GetUserByID(ctx, userID) +// if err != nil { +// s.logger.Error("Failed to find user for token refresh", "userID", userID, "error", err) +// return nil, fmt.Errorf("user not found") +// } + +// newSessionID := fmt.Sprintf("%d-%s-%d", userID, req.GameID, time.Now().UnixNano()) + +// token, err := jwtutil.CreatePopOKJwt( +// userID, +// user.FirstName, +// req.Currency, +// "en", +// req.Mode, +// newSessionID, +// s.config.PopOK.SecretKey, +// 24*time.Hour, +// ) +// if err != nil { +// s.logger.Error("Failed to generate new token", "userID", userID, "error", err) +// return nil, fmt.Errorf("token generation failed") +// } + +// return &domain.PopOKGenerateTokenResponse{ +// NewToken: token, +// }, nil +// } + func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequest) (*domain.PopOKCancelResponse, error) { // 1. Validate token and get user ID claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey) @@ -480,7 +650,7 @@ func (s *service) ListGames(ctx context.Context, currency string) ([]domain.PopO func (s *service) RecommendGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error) { // Fetch all available games - games, err := s.ListGames(ctx, "ETB") // currency can be dynamic + games, err := s.ListGames(ctx, "ETB") if err != nil || len(games) == 0 { return nil, fmt.Errorf("could not fetch games") } @@ -544,3 +714,48 @@ func toInt64Ptr(s string) *int64 { } return &id } + +func (s *service) AddFavoriteGame(ctx context.Context, userID, gameID int64) error { + return s.repo.AddFavoriteGame(ctx, userID, gameID) +} + +func (s *service) RemoveFavoriteGame(ctx context.Context, userID, gameID int64) error { + return s.repo.RemoveFavoriteGame(ctx, userID, gameID) +} + +func (s *service) ListFavoriteGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error) { + gameIDs, err := s.repo.ListFavoriteGames(ctx, userID) + if err != nil { + s.logger.Error("Failed to list favorite games", "userID", userID, "error", err) + return nil, err + } + + if len(gameIDs) == 0 { + return []domain.GameRecommendation{}, nil + } + + allGames, err := s.ListGames(ctx, "ETB") // You can use dynamic currency if needed + if err != nil { + return nil, err + } + + var favorites []domain.GameRecommendation + idMap := make(map[int64]bool) + for _, id := range gameIDs { + idMap[id] = true + } + + for _, g := range allGames { + if idMap[int64(g.ID)] { + favorites = append(favorites, domain.GameRecommendation{ + GameID: g.ID, + GameName: g.GameName, + Thumbnail: g.Thumbnail, + Bets: g.Bets, + Reason: "Marked as favorite", + }) + } + } + + return favorites, nil +} diff --git a/internal/services/wallet/port.go b/internal/services/wallet/port.go index a54433a..fb761cb 100644 --- a/internal/services/wallet/port.go +++ b/internal/services/wallet/port.go @@ -7,6 +7,8 @@ import ( ) type WalletStore interface { + // GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error) + // GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error) CreateWallet(ctx context.Context, wallet domain.CreateWallet) (domain.Wallet, error) CreateCustomerWallet(ctx context.Context, customerWallet domain.CreateCustomerWallet) (domain.CustomerWallet, error) GetWalletByID(ctx context.Context, id int64) (domain.Wallet, error) diff --git a/internal/services/wallet/service.go b/internal/services/wallet/service.go index 2d3b927..8186593 100644 --- a/internal/services/wallet/service.go +++ b/internal/services/wallet/service.go @@ -10,6 +10,7 @@ type Service struct { walletStore WalletStore transferStore TransferStore notificationStore notificationservice.NotificationStore + notificationSvc *notificationservice.Service logger *slog.Logger } diff --git a/internal/services/wallet/wallet.go b/internal/services/wallet/wallet.go index d7948d9..eab19f1 100644 --- a/internal/services/wallet/wallet.go +++ b/internal/services/wallet/wallet.go @@ -69,7 +69,18 @@ func (s *Service) GetAllBranchWallets(ctx context.Context) ([]domain.BranchWalle } func (s *Service) UpdateBalance(ctx context.Context, id int64, balance domain.Currency) error { - return s.walletStore.UpdateBalance(ctx, id, balance) + err := s.walletStore.UpdateBalance(ctx, id, balance) + if err != nil { + return err + } + + wallet, err := s.GetWalletByID(ctx, id) + if err != nil { + return err + } + + go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet) + return nil } func (s *Service) AddToWallet( @@ -84,6 +95,8 @@ func (s *Service) AddToWallet( return domain.Transfer{}, err } + go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet) + // Log the transfer here for reference newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ Amount: amount, @@ -121,6 +134,8 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain. return domain.Transfer{}, nil } + go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet) + // Log the transfer here for reference newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ Amount: amount, diff --git a/internal/web_server/app.go b/internal/web_server/app.go index c7bf992..61bc682 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -12,6 +12,8 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions" + issuereporting "github.com/SamuelTariku/FortuneBet-Backend/internal/services/issue_reporting" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation" @@ -37,6 +39,8 @@ import ( ) type App struct { + issueReportingSvc *issuereporting.Service + instSvc *institutions.Service currSvc *currency.Service fiber *fiber.App aleaVirtualGameService alea.AleaVirtualGameService @@ -70,6 +74,8 @@ type App struct { } func NewApp( + issueReportingSvc *issuereporting.Service, + instSvc *institutions.Service, currSvc *currency.Service, port int, validator *customvalidator.CustomValidator, settingSvc *settings.Service, @@ -113,6 +119,8 @@ func NewApp( })) s := &App{ + issueReportingSvc: issueReportingSvc, + instSvc: instSvc, currSvc: currSvc, fiber: app, port: port, diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index 30bdde0..77950e2 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -22,7 +22,7 @@ import ( // @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /bet [post] +// @Router /sport/bet [post] func (h *Handler) CreateBet(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) role := c.Locals("role").(domain.Role) @@ -82,7 +82,7 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error { // @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /random/bet [post] +// @Router /sport/random/bet [post] func (h *Handler) RandomBet(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) @@ -207,7 +207,7 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error { // @Success 200 {array} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /bet [get] +// @Router /sport/bet [get] func (h *Handler) GetAllBet(c *fiber.Ctx) error { role := c.Locals("role").(domain.Role) companyID := c.Locals("company_id").(domain.ValidInt64) @@ -305,7 +305,7 @@ func (h *Handler) GetAllBet(c *fiber.Ctx) error { // @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /bet/{id} [get] +// @Router /sport/bet/{id} [get] func (h *Handler) GetBetByID(c *fiber.Ctx) error { betID := c.Params("id") id, err := strconv.ParseInt(betID, 10, 64) @@ -351,7 +351,7 @@ func (h *Handler) GetBetByID(c *fiber.Ctx) error { // @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /bet/cashout/{id} [get] +// @Router /sport/bet/cashout/{id} [get] func (h *Handler) GetBetByCashoutID(c *fiber.Ctx) error { cashoutID := c.Params("id") @@ -392,7 +392,7 @@ type UpdateCashOutReq struct { // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /bet/{id} [patch] +// @Router /sport/bet/{id} [patch] func (h *Handler) UpdateCashOut(c *fiber.Ctx) error { type UpdateCashOutReq struct { CashedOut bool `json:"cashed_out" validate:"required" example:"true"` @@ -455,7 +455,7 @@ func (h *Handler) UpdateCashOut(c *fiber.Ctx) error { // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /bet/{id} [delete] +// @Router /sport/bet/{id} [delete] func (h *Handler) DeleteBet(c *fiber.Ctx) error { betID := c.Params("id") id, err := strconv.ParseInt(betID, 10, 64) diff --git a/internal/web_server/handlers/chapa.go b/internal/web_server/handlers/chapa.go index 751f78c..5e9ad56 100644 --- a/internal/web_server/handlers/chapa.go +++ b/internal/web_server/handlers/chapa.go @@ -32,7 +32,7 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error { var req domain.ChapaDepositRequestPayload if err := c.BodyParser(&req); err != nil { - fmt.Sprintln("We first first are here init Chapa payment") + // fmt.Println("We first first are here init Chapa payment") return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Error: err.Error(), Message: "Failed to parse request body", @@ -41,7 +41,7 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error { amount := domain.Currency(req.Amount * 100) - fmt.Sprintln("We are here init Chapa payment") + fmt.Println("We are here init Chapa payment") checkoutURL, err := h.chapaSvc.InitiateDeposit(c.Context(), userID, amount) if err != nil { @@ -79,7 +79,7 @@ func (h *Handler) WebhookCallback(c *fiber.Ctx) error { } switch chapaTransactionType.Type { - case h.Cfg.CHAPA_TRANSFER_TYPE: + case h.Cfg.CHAPA_PAYMENT_TYPE: chapaTransferVerificationRequest := new(domain.ChapaWebHookTransfer) if err := c.BodyParser(chapaTransferVerificationRequest); err != nil { @@ -100,7 +100,7 @@ func (h *Handler) WebhookCallback(c *fiber.Ctx) error { Data: chapaTransferVerificationRequest, Success: true, }) - case h.Cfg.CHAPA_PAYMENT_TYPE: + case h.Cfg.CHAPA_TRANSFER_TYPE: chapaPaymentVerificationRequest := new(domain.ChapaWebHookPayment) if err := c.BodyParser(chapaPaymentVerificationRequest); err != nil { return domain.UnProcessableEntityResponse(c) @@ -147,7 +147,7 @@ func (h *Handler) ManualVerifyTransaction(c *fiber.Ctx) error { }) } - verification, err := h.chapaSvc.ManualVerifTransaction(c.Context(), txRef) + verification, err := h.chapaSvc.ManuallyVerify(c.Context(), txRef) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to verify Chapa transaction", diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index 7708d8d..bb2a792 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -11,6 +11,8 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions" + issuereporting "github.com/SamuelTariku/FortuneBet-Backend/internal/services/issue_reporting" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" @@ -31,6 +33,8 @@ import ( ) type Handler struct { + issueReportingSvc *issuereporting.Service + instSvc *institutions.Service currSvc *currency.Service logger *slog.Logger settingSvc *settings.Service @@ -61,6 +65,8 @@ type Handler struct { } func New( + issueReportingSvc *issuereporting.Service, + instSvc *institutions.Service, currSvc *currency.Service, logger *slog.Logger, settingSvc *settings.Service, @@ -90,6 +96,8 @@ func New( mongoLoggerSvc *zap.Logger, ) *Handler { return &Handler{ + issueReportingSvc: issueReportingSvc, + instSvc: instSvc, currSvc: currSvc, logger: logger, settingSvc: settingSvc, diff --git a/internal/web_server/handlers/institutions.go b/internal/web_server/handlers/institutions.go new file mode 100644 index 0000000..bd723c0 --- /dev/null +++ b/internal/web_server/handlers/institutions.go @@ -0,0 +1,135 @@ +package handlers + +import ( + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/gofiber/fiber/v2" +) + +// @Summary Create a new bank +// @Tags Institutions - Banks +// @Accept json +// @Produce json +// @Param bank body domain.Bank true "Bank Info" +// @Success 201 {object} domain.Bank +// @Failure 400 {object} domain.ErrorResponse +// @Router /api/v1/banks [post] +func (h *Handler) CreateBank(c *fiber.Ctx) error { + var bank domain.Bank + if err := c.BodyParser(&bank); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid payload"}) + } + + err := h.instSvc.Create(c.Context(), &bank) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + return c.Status(fiber.StatusCreated).JSON(bank) +} + +// @Summary Get a bank by ID +// @Tags Institutions - Banks +// @Produce json +// @Param id path int true "Bank ID" +// @Success 200 {object} domain.Bank +// @Failure 400 {object} domain.ErrorResponse +// @Failure 404 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/banks/{id} [get] +func (h *Handler) GetBankByID(c *fiber.Ctx) error { + id, err := c.ParamsInt("id") + if err != nil || id <= 0 { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid bank ID"}) + } + + bank, err := h.instSvc.GetByID(c.Context(), int64(id)) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "bank not found"}) + } + + return c.JSON(bank) +} + +// @Summary Update a bank +// @Tags Institutions - Banks +// @Accept json +// @Produce json +// @Param id path int true "Bank ID" +// @Param bank body domain.Bank true "Bank Info" +// @Success 200 {object} domain.Bank +// @Failure 400 {object} domain.ErrorResponse +// @Failure 404 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/banks/{id} [put] +func (h *Handler) UpdateBank(c *fiber.Ctx) error { + id, err := c.ParamsInt("id") + if err != nil || id <= 0 { + // return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid bank ID"}) + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Failed to update bank", + Error: err.Error(), + }) + } + + var bank domain.Bank + if err := c.BodyParser(&bank); err != nil { + // return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid payload"}) + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Failed to update bank", + Error: err.Error(), + }) + } + bank.ID = id + + err = h.instSvc.Update(c.Context(), &bank) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to update bank", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Bank updated successfully", + StatusCode: fiber.StatusOK, + Success: true, + Data: bank, + }) + // return c.JSON(bank) +} + +// @Summary Delete a bank +// @Tags Institutions - Banks +// @Produce json +// @Param id path int true "Bank ID" +// @Success 204 {string} string "Deleted successfully" +// @Failure 400 {object} domain.ErrorResponse +// @Failure 404 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/banks/{id} [delete] +func (h *Handler) DeleteBank(c *fiber.Ctx) error { + id, err := c.ParamsInt("id") + if err != nil || id <= 0 { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid bank ID"}) + } + + err = h.instSvc.Delete(c.Context(), int64(id)) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + + return c.SendStatus(fiber.StatusNoContent) +} + +// @Summary List all banks +// @Tags Institutions - Banks +// @Produce json +// @Success 200 {array} domain.Bank +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/banks [get] +func (h *Handler) ListBanks(c *fiber.Ctx) error { + banks, err := h.instSvc.List(c.Context()) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(banks) +} diff --git a/internal/web_server/handlers/issue_reporting.go b/internal/web_server/handlers/issue_reporting.go new file mode 100644 index 0000000..d49c6f5 --- /dev/null +++ b/internal/web_server/handlers/issue_reporting.go @@ -0,0 +1,147 @@ +package handlers + +import ( + "strconv" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/gofiber/fiber/v2" +) + +// CreateIssue godoc +// @Summary Report an issue +// @Description Allows a customer to report a new issue related to the betting platform +// @Tags Issues +// @Accept json +// @Produce json +// @Param issue body domain.ReportedIssue true "Issue to report" +// @Success 201 {object} domain.ReportedIssue +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/issues [post] +func (h *Handler) CreateIssue(c *fiber.Ctx) error { + var req domain.ReportedIssue + if err := c.BodyParser(&req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + created, err := h.issueReportingSvc.CreateReportedIssue(c.Context(), req) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + return c.Status(fiber.StatusCreated).JSON(created) +} + +// GetCustomerIssues godoc +// @Summary Get reported issues by a customer +// @Description Returns all issues reported by a specific customer +// @Tags Issues +// @Produce json +// @Param customer_id path int true "Customer ID" +// @Param limit query int false "Limit" +// @Param offset query int false "Offset" +// @Success 200 {array} domain.ReportedIssue +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/issues/customer/{customer_id} [get] +func (h *Handler) GetCustomerIssues(c *fiber.Ctx) error { + customerID, err := strconv.ParseInt(c.Params("customer_id"), 10, 64) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid customer ID") + } + + limit, offset := getPaginationParams(c) + + issues, err := h.issueReportingSvc.GetIssuesForCustomer(c.Context(), customerID, limit, offset) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + return c.JSON(issues) +} + +// GetAllIssues godoc +// @Summary Get all reported issues +// @Description Admin endpoint to list all reported issues with pagination +// @Tags Issues +// @Produce json +// @Param limit query int false "Limit" +// @Param offset query int false "Offset" +// @Success 200 {array} domain.ReportedIssue +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/issues [get] +func (h *Handler) GetAllIssues(c *fiber.Ctx) error { + limit, offset := getPaginationParams(c) + + issues, err := h.issueReportingSvc.GetAllIssues(c.Context(), limit, offset) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + return c.JSON(issues) +} + +// UpdateIssueStatus godoc +// @Summary Update issue status +// @Description Admin endpoint to update the status of a reported issue +// @Tags Issues +// @Accept json +// @Param issue_id path int true "Issue ID" +// @Param status body object{status=string} true "New issue status (pending, in_progress, resolved, rejected)" +// @Success 204 +// @Failure 400 {object} domain.ErrorResponse +// @Router /api/v1/issues/{issue_id}/status [patch] +func (h *Handler) UpdateIssueStatus(c *fiber.Ctx) error { + issueID, err := strconv.ParseInt(c.Params("issue_id"), 10, 64) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid issue ID") + } + + var body struct { + Status string `json:"status"` + } + if err := c.BodyParser(&body); err != nil || body.Status == "" { + return fiber.NewError(fiber.StatusBadRequest, "Invalid status payload") + } + + if err := h.issueReportingSvc.UpdateIssueStatus(c.Context(), issueID, body.Status); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.SendStatus(fiber.StatusNoContent) +} + +// DeleteIssue godoc +// @Summary Delete a reported issue +// @Description Admin endpoint to delete a reported issue +// @Tags Issues +// @Param issue_id path int true "Issue ID" +// @Success 204 +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/issues/{issue_id} [delete] +func (h *Handler) DeleteIssue(c *fiber.Ctx) error { + issueID, err := strconv.ParseInt(c.Params("issue_id"), 10, 64) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid issue ID") + } + + if err := h.issueReportingSvc.DeleteIssue(c.Context(), issueID); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + return c.SendStatus(fiber.StatusNoContent) +} + +func getPaginationParams(c *fiber.Ctx) (limit, offset int) { + limit = 20 + offset = 0 + + if l, err := strconv.Atoi(c.Query("limit")); err == nil && l > 0 { + limit = l + } + if o, err := strconv.Atoi(c.Query("offset")); err == nil && o >= 0 { + offset = o + } + return +} diff --git a/internal/web_server/handlers/recommendation.go b/internal/web_server/handlers/recommendation.go index cdd8cdf..5d79f87 100644 --- a/internal/web_server/handlers/recommendation.go +++ b/internal/web_server/handlers/recommendation.go @@ -1,9 +1,5 @@ package handlers -import ( - "github.com/gofiber/fiber/v2" -) - // @Summary Get virtual game recommendations // @Description Returns a list of recommended virtual games for a specific user // @Tags Recommendations @@ -13,14 +9,15 @@ import ( // @Success 200 {object} domain.RecommendationSuccessfulResponse "Recommended games fetched successfully" // @Failure 500 {object} domain.RecommendationErrorResponse "Failed to fetch recommendations" // @Router /api/v1/virtual-games/recommendations/{userID} [get] -func (h *Handler) GetRecommendations(c *fiber.Ctx) error { - userID := c.Params("userID") // or from JWT - recommendations, err := h.recommendationSvc.GetRecommendations(c.Context(), userID) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch recommendations") - } - return c.JSON(fiber.Map{ - "message": "Recommended games fetched successfully", - "recommended_games": recommendations, - }) -} + +// func (h *Handler) GetRecommendations(c *fiber.Ctx) error { +// userID := c.Params("userID") // or from JWT +// recommendations, err := h.recommendationSvc.GetRecommendations(c.Context(), userID) +// if err != nil { +// return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch recommendations") +// } +// return c.JSON(fiber.Map{ +// "message": "Recommended games fetched successfully", +// "recommended_games": recommendations, +// }) +// } diff --git a/internal/web_server/handlers/transfer_handler.go b/internal/web_server/handlers/transfer_handler.go index 783022f..428ff5a 100644 --- a/internal/web_server/handlers/transfer_handler.go +++ b/internal/web_server/handlers/transfer_handler.go @@ -11,17 +11,19 @@ import ( ) type TransferWalletRes struct { - ID int64 `json:"id" example:"1"` - Amount float32 `json:"amount" example:"100.0"` - Verified bool `json:"verified" example:"true"` - Type string `json:"type" example:"transfer"` - PaymentMethod string `json:"payment_method" example:"bank"` - ReceiverWalletID *int64 `json:"receiver_wallet_id" example:"1"` - SenderWalletID *int64 `json:"sender_wallet_id" example:"1"` - CashierID *int64 `json:"cashier_id" example:"789"` - CreatedAt time.Time `json:"created_at" example:"2025-04-08T12:00:00Z"` - UpdatedAt time.Time `json:"updated_at" example:"2025-04-08T12:30:00Z"` + ID int64 `json:"id"` + Amount float32 `json:"amount"` + Verified bool `json:"verified"` + Type string `json:"type"` + PaymentMethod string `json:"payment_method"` + ReceiverWalletID *int64 `json:"receiver_wallet_id,omitempty"` + SenderWalletID *int64 `json:"sender_wallet_id,omitempty"` + CashierID *int64 `json:"cashier_id,omitempty"` + ReferenceNumber string `json:"reference_number"` // ← Add this + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } + type RefillRes struct { ID int64 `json:"id" example:"1"` Amount float32 `json:"amount" example:"100.0"` @@ -35,33 +37,34 @@ type RefillRes struct { UpdatedAt time.Time `json:"updated_at" example:"2025-04-08T12:30:00Z"` } -func convertTransfer(transfer domain.Transfer) TransferWalletRes { +func convertTransfer(t domain.Transfer) TransferWalletRes { + var receiverID *int64 + if t.ReceiverWalletID.Valid { + receiverID = &t.ReceiverWalletID.Value + } + + var senderID *int64 + if t.SenderWalletID.Valid { + senderID = &t.SenderWalletID.Value + } var cashierID *int64 - if transfer.CashierID.Valid { - cashierID = &transfer.CashierID.Value - } - var receiverID *int64 - if transfer.ReceiverWalletID.Valid { - receiverID = &transfer.ReceiverWalletID.Value - } - - var senderId *int64 - if transfer.SenderWalletID.Valid { - senderId = &transfer.SenderWalletID.Value + if t.CashierID.Valid { + cashierID = &t.CashierID.Value } return TransferWalletRes{ - ID: transfer.ID, - Amount: transfer.Amount.Float32(), - Verified: transfer.Verified, - Type: string(transfer.Type), - PaymentMethod: string(transfer.PaymentMethod), + ID: t.ID, + Amount: float32(t.Amount), + Verified: t.Verified, + Type: string(t.Type), + PaymentMethod: string(t.PaymentMethod), ReceiverWalletID: receiverID, - SenderWalletID: senderId, + SenderWalletID: senderID, CashierID: cashierID, - CreatedAt: transfer.CreatedAt, - UpdatedAt: transfer.UpdatedAt, + ReferenceNumber: t.ReferenceNumber, + CreatedAt: t.CreatedAt, + UpdatedAt: t.UpdatedAt, } } @@ -142,10 +145,11 @@ func (h *Handler) TransferToWallet(c *fiber.Ctx) error { var senderID int64 //TODO: check to make sure that the cashiers aren't transferring TO branch wallet - if role == domain.RoleCustomer { + switch role { + case domain.RoleCustomer: h.logger.Error("Unauthorized access", "userID", userID, "role", role) return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) - } else if role == domain.RoleBranchManager || role == domain.RoleAdmin || role == domain.RoleSuperAdmin { + case domain.RoleBranchManager, domain.RoleAdmin, domain.RoleSuperAdmin: company, err := h.companySvc.GetCompanyByID(c.Context(), companyID.Value) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ @@ -156,7 +160,7 @@ func (h *Handler) TransferToWallet(c *fiber.Ctx) error { } senderID = company.WalletID h.logger.Error("Will", "userID", userID, "role", role) - } else { + default: cashierBranch, err := h.branchSvc.GetBranchByCashier(c.Context(), userID) if err != nil { h.logger.Error("Failed to get branch", "user ID", userID, "error", err) diff --git a/internal/web_server/handlers/virtual_games_hadlers.go b/internal/web_server/handlers/virtual_games_hadlers.go index 3c48879..4b51f58 100644 --- a/internal/web_server/handlers/virtual_games_hadlers.go +++ b/internal/web_server/handlers/virtual_games_hadlers.go @@ -1,6 +1,8 @@ package handlers import ( + "strconv" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" @@ -25,9 +27,9 @@ type launchVirtualGameRes struct { // @Security Bearer // @Param launch body launchVirtualGameReq true "Game launch details" // @Success 200 {object} launchVirtualGameRes -// @Failure 400 {object} response.APIResponse -// @Failure 401 {object} response.APIResponse -// @Failure 500 {object} response.APIResponse +// @Failure 400 {object} domain.ErrorResponse +// @Failure 401 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse // @Router /virtual-game/launch [post] func (h *Handler) LaunchVirtualGame(c *fiber.Ctx) error { @@ -37,6 +39,12 @@ func (h *Handler) LaunchVirtualGame(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification") } + // companyID, ok := c.Locals("company_id").(int64) + // if !ok || companyID == 0 { + // h.logger.Error("Invalid company ID in context") + // return fiber.NewError(fiber.StatusUnauthorized, "Invalid company identification") + // } + var req launchVirtualGameReq if err := c.BodyParser(&req); err != nil { h.logger.Error("Failed to parse LaunchVirtualGame request", "error", err) @@ -64,9 +72,9 @@ func (h *Handler) LaunchVirtualGame(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param callback body domain.PopOKCallback true "Callback data" -// @Success 200 {object} response.APIResponse -// @Failure 400 {object} response.APIResponse -// @Failure 500 {object} response.APIResponse +// @Success 200 {object} domain.ErrorResponse +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse // @Router /virtual-game/callback [post] func (h *Handler) HandleVirtualGameCallback(c *fiber.Ctx) error { var callback domain.PopOKCallback @@ -199,3 +207,119 @@ func (h *Handler) RecommendGames(c *fiber.Ctx) error { return c.JSON(recommendations) } + +func (h *Handler) HandleTournamentWin(c *fiber.Ctx) error { + var req domain.PopOKWinRequest + + if err := c.BodyParser(&req); err != nil { + h.logger.Error("Invalid tournament win request body", "error", err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request body", + }) + } + + resp, err := h.virtualGameSvc.ProcessTournamentWin(c.Context(), &req) + if err != nil { + h.logger.Error("Failed to process tournament win", "error", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return c.JSON(resp) +} + +func (h *Handler) HandlePromoWin(c *fiber.Ctx) error { + var req domain.PopOKWinRequest + + if err := c.BodyParser(&req); err != nil { + h.logger.Error("Invalid promo win request body", "error", err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request body", + }) + } + + resp, err := h.virtualGameSvc.ProcessPromoWin(c.Context(), &req) + if err != nil { + h.logger.Error("Failed to process promo win", "error", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return c.JSON(resp) +} + +// AddFavoriteGame godoc +// @Summary Add game to favorites +// @Description Adds a game to the user's favorite games list +// @Tags VirtualGames - Favourites +// @Accept json +// @Produce json +// @Param body body domain.FavoriteGameRequest true "Game ID to add" +// @Success 201 {string} domain.Response "created" +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/virtual-game/favorites [post] +func (h *Handler) AddFavorite(c *fiber.Ctx) error { + userID := c.Locals("user_id").(int64) + + var req domain.FavoriteGameRequest + if err := c.BodyParser(&req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request") + } + + err := h.virtualGameSvc.AddFavoriteGame(c.Context(), userID, req.GameID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Could not add favorite", + Error: err.Error(), + }) + // return fiber.NewError(fiber.StatusInternalServerError, "Could not add favorite") + } + return c.Status(fiber.StatusCreated).JSON(domain.Response{ + Message: "Game added to favorites", + StatusCode: fiber.StatusCreated, + Success: true, + }) + // return c.SendStatus(fiber.StatusCreated) +} + +// RemoveFavoriteGame godoc +// @Summary Remove game from favorites +// @Description Removes a game from the user's favorites +// @Tags VirtualGames - Favourites +// @Produce json +// @Param gameID path int64 true "Game ID to remove" +// @Success 200 {string} domain.Response "removed" +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/virtual-game/favorites/{gameID} [delete] +func (h *Handler) RemoveFavorite(c *fiber.Ctx) error { + userID := c.Locals("user_id").(int64) + gameID, _ := strconv.ParseInt(c.Params("gameID"), 10, 64) + + err := h.virtualGameSvc.RemoveFavoriteGame(c.Context(), userID, gameID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Could not remove favorite") + } + return c.SendStatus(fiber.StatusOK) +} + +// ListFavoriteGames godoc +// @Summary Get user's favorite games +// @Description Lists the games that the user marked as favorite +// @Tags VirtualGames - Favourites +// @Produce json +// @Success 200 {array} domain.GameRecommendation +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/virtual-game/favorites [get] +func (h *Handler) ListFavorites(c *fiber.Ctx) error { + userID := c.Locals("user_id").(int64) + + games, err := h.virtualGameSvc.ListFavoriteGames(c.Context(), userID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Could not fetch favorites") + } + return c.Status(fiber.StatusOK).JSON(games) +} diff --git a/internal/web_server/jwt/jwt.go b/internal/web_server/jwt/jwt.go index 5271440..8a3f0b3 100644 --- a/internal/web_server/jwt/jwt.go +++ b/internal/web_server/jwt/jwt.go @@ -24,12 +24,13 @@ type UserClaim struct { type PopOKClaim struct { jwt.RegisteredClaims - UserID int64 `json:"user_id"` - Username string `json:"username"` - Currency string `json:"currency"` - Lang string `json:"lang"` - Mode string `json:"mode"` - SessionID string `json:"session_id"` + UserID int64 `json:"user_id"` + Username string `json:"username"` + Currency string `json:"currency"` + Lang string `json:"lang"` + Mode string `json:"mode"` + SessionID string `json:"session_id"` + CompanyID domain.ValidInt64 `json:"company_id"` } type JwtConfig struct { @@ -54,7 +55,7 @@ func CreateJwt(userId int64, Role domain.Role, CompanyID domain.ValidInt64, key return jwtToken, err } -func CreatePopOKJwt(userID int64, username, currency, lang, mode, sessionID, key string, expiry time.Duration) (string, error) { +func CreatePopOKJwt(userID int64, CompanyID domain.ValidInt64, username, currency, lang, mode, sessionID, key string, expiry time.Duration) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, PopOKClaim{ RegisteredClaims: jwt.RegisteredClaims{ Issuer: "fortune-bet", @@ -69,6 +70,7 @@ func CreatePopOKJwt(userID int64, username, currency, lang, mode, sessionID, key Lang: lang, Mode: mode, SessionID: sessionID, + CompanyID: CompanyID, }) return token.SignedString([]byte(key)) } diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index ee1c8dc..737d2e1 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -20,6 +20,8 @@ import ( func (a *App) initAppRoutes() { h := handlers.New( + a.issueReportingSvc, + a.instSvc, a.currSvc, a.logger, a.settingSvc, @@ -182,14 +184,14 @@ func (a *App) initAppRoutes() { a.fiber.Get("/ticket/:id", h.GetTicketByID) // Bet Routes - a.fiber.Post("/bet", a.authMiddleware, h.CreateBet) - a.fiber.Get("/bet", a.authMiddleware, h.GetAllBet) - a.fiber.Get("/bet/:id", h.GetBetByID) - a.fiber.Get("/bet/cashout/:id", a.authMiddleware, h.GetBetByCashoutID) - a.fiber.Patch("/bet/:id", a.authMiddleware, h.UpdateCashOut) - a.fiber.Delete("/bet/:id", a.authMiddleware, h.DeleteBet) + a.fiber.Post("/sport/bet", a.authMiddleware, h.CreateBet) + a.fiber.Get("/sport/bet", a.authMiddleware, h.GetAllBet) + a.fiber.Get("/sport/bet/:id", h.GetBetByID) + a.fiber.Get("/sport/bet/cashout/:id", a.authMiddleware, h.GetBetByCashoutID) + a.fiber.Patch("/sport/bet/:id", a.authMiddleware, h.UpdateCashOut) + a.fiber.Delete("/sport/bet/:id", a.authMiddleware, h.DeleteBet) - a.fiber.Post("/random/bet", a.authMiddleware, h.RandomBet) + a.fiber.Post("/sport/random/bet", a.authMiddleware, h.RandomBet) // Wallet a.fiber.Get("/wallet", h.GetAllWallets) @@ -277,9 +279,20 @@ func (a *App) initAppRoutes() { a.fiber.Post("/bet", h.HandleBet) a.fiber.Post("/win", h.HandleWin) a.fiber.Post("/cancel", h.HandleCancel) + a.fiber.Post("/promoWin ", h.HandlePromoWin) + a.fiber.Post("/tournamentWin ", h.HandleTournamentWin) a.fiber.Get("/popok/games", h.GetGameList) a.fiber.Get("/popok/games/recommend", a.authMiddleware, h.RecommendGames) + group.Post("/virtual-game/favorites", a.authMiddleware, h.AddFavorite) + group.Delete("/virtual-game/favorites/:gameID", a.authMiddleware, h.RemoveFavorite) + group.Get("/virtual-game/favorites", a.authMiddleware, h.ListFavorites) + //Issue Reporting Routes + group.Post("/issues", a.authMiddleware, a.OnlyAdminAndAbove, h.CreateIssue) + group.Get("/issues/customer/:customer_id", a.authMiddleware, a.OnlyAdminAndAbove, h.GetCustomerIssues) + group.Get("/issues", a.authMiddleware, a.OnlyAdminAndAbove, h.GetAllIssues) + group.Patch("/issues/:issue_id/status", a.authMiddleware, a.OnlyAdminAndAbove, h.UpdateIssueStatus) + group.Delete("/issues/:issue_id", a.authMiddleware, a.OnlyAdminAndAbove, h.DeleteIssue) } ///user/profile get