From 25230e3fcf84f63f2197e085d5097f754b0f0a1d Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Tue, 24 Jun 2025 17:41:04 +0300 Subject: [PATCH] fetch company and branch by wallet ID methods --- cmd/main.go | 19 ++ .../000004_virtual_game_Sessios.up.sql | 16 ++ db/query/report.sql | 50 +++-- db/query/virtual_games.sql | 52 +++-- db/query/wallet.sql | 12 +- docs/docs.go | 136 ++++++++++++- docs/swagger.json | 136 ++++++++++++- docs/swagger.yaml | 93 ++++++++- gen/db/models.go | 12 ++ gen/db/report.sql.go | 157 ++++++++++++--- gen/db/virtual_games.sql.go | 167 +++++++++++++--- gen/db/wallet.sql.go | 44 +++++ internal/domain/report.go | 40 ++++ internal/domain/virtual_game.go | 29 +++ internal/repository/notification.go | 34 ++++ internal/repository/report.go | 62 +++--- internal/repository/virtual_game.go | 23 +++ internal/repository/wallet.go | 1 + internal/services/notfication/port.go | 2 + internal/services/notfication/service.go | 184 +++++++++++++----- internal/services/report/service.go | 171 +++++++++++++++- internal/services/ticket/service.go | 12 +- internal/services/virtualGame/port.go | 3 + internal/services/virtualGame/service.go | 58 +++++- internal/services/wallet/port.go | 2 + internal/services/wallet/service.go | 1 + internal/services/wallet/wallet.go | 19 +- .../handlers/virtual_games_hadlers.go | 94 ++++++++- internal/web_server/jwt/jwt.go | 16 +- internal/web_server/routes.go | 4 +- 30 files changed, 1431 insertions(+), 218 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 9d04181..3cf89fa 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -199,6 +199,25 @@ 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") + } + // go httpserver.SetupReportCronJob(reportWorker) // Initialize and start HTTP server 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/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 e825653..3d6169b 100644 --- a/db/query/wallet.sql +++ b/db/query/wallet.sql @@ -62,4 +62,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 1edc2a8..a4a551c 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1052,6 +1052,122 @@ const docTemplate = `{ } } }, + "/api/v1/virtual-game/favorites": { + "post": { + "description": "Adds a game to the user's favorite games list", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VirtualGames - Favourites" + ], + "summary": "Add game to favorites", + "parameters": [ + { + "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-games/favorites": { + "get": { + "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" + } + } + } + } + }, + "/api/v1/virtual-games/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": "removed", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/webhooks/alea": { "post": { "description": "Handles webhook callbacks from Alea Play virtual games for bet settlement", @@ -4701,19 +4817,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" } } } @@ -4758,19 +4874,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" } } } @@ -5479,6 +5595,14 @@ const docTemplate = `{ "STATUS_REMOVED" ] }, + "domain.FavoriteGameRequest": { + "type": "object", + "properties": { + "game_id": { + "type": "integer" + } + } + }, "domain.GameRecommendation": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 6260c8a..6be7103 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1044,6 +1044,122 @@ } } }, + "/api/v1/virtual-game/favorites": { + "post": { + "description": "Adds a game to the user's favorite games list", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VirtualGames - Favourites" + ], + "summary": "Add game to favorites", + "parameters": [ + { + "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-games/favorites": { + "get": { + "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" + } + } + } + } + }, + "/api/v1/virtual-games/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": "removed", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/webhooks/alea": { "post": { "description": "Handles webhook callbacks from Alea Play virtual games for bet settlement", @@ -4693,19 +4809,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" } } } @@ -4750,19 +4866,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" } } } @@ -5471,6 +5587,14 @@ "STATUS_REMOVED" ] }, + "domain.FavoriteGameRequest": { + "type": "object", + "properties": { + "game_id": { + "type": "integer" + } + } + }, "domain.GameRecommendation": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 63f93a8..09d1e07 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -391,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: @@ -2268,6 +2273,82 @@ paths: summary: Get dashboard report tags: - Reports + /api/v1/virtual-game/favorites: + 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-games/favorites: + get: + description: Lists the games that the user marked as favorite + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.GameRecommendation' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get user's favorite games + tags: + - VirtualGames - Favourites + /api/v1/virtual-games/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: @@ -4663,15 +4744,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 @@ -4697,15 +4778,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/models.go b/gen/db/models.go index e052db4..75e1d83 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -233,6 +233,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"` @@ -476,6 +483,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 +513,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 c0c3d3c..212ee99 100644 --- a/gen/db/wallet.sql.go +++ b/gen/db/wallet.sql.go @@ -181,6 +181,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 cw.id, cw.customer_id, 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 cc96a46..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 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 3271b54..7007be9 100644 --- a/internal/repository/wallet.go +++ b/internal/repository/wallet.go @@ -257,3 +257,4 @@ func (s *Store) GetTotalWallets(ctx context.Context, filter domain.ReportFilter) return total, nil } + 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 0067e36..00b3fab 100644 --- a/internal/services/ticket/service.go +++ b/internal/services/ticket/service.go @@ -226,13 +226,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 26d7816..36f57de 100644 --- a/internal/services/virtualGame/port.go +++ b/internal/services/virtualGame/port.go @@ -19,4 +19,7 @@ type VirtualGameService interface { 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 128364d..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, @@ -641,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") } @@ -705,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 145f38f..9ba436c 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 27de5e4..911febf 100644 --- a/internal/services/wallet/wallet.go +++ b/internal/services/wallet/wallet.go @@ -66,11 +66,22 @@ 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( - ctx context.Context, id int64, amount domain.Currency, cashierID domain.ValidInt64, paymentMethod domain.PaymentMethod, paymentDetails domain. PaymentDetails) (domain.Transfer, error) { + ctx context.Context, id int64, amount domain.Currency, cashierID domain.ValidInt64, paymentMethod domain.PaymentMethod, paymentDetails domain.PaymentDetails) (domain.Transfer, error) { wallet, err := s.GetWalletByID(ctx, id) if err != nil { return domain.Transfer{}, err @@ -81,6 +92,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, @@ -118,6 +131,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/handlers/virtual_games_hadlers.go b/internal/web_server/handlers/virtual_games_hadlers.go index 255d3a6..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 @@ -241,3 +249,77 @@ func (h *Handler) HandlePromoWin(c *fiber.Ctx) 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 0523271..c6a603c 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -279,7 +279,9 @@ func (a *App) initAppRoutes() { 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) } ///user/profile get