diff --git a/cmd/main.go b/cmd/main.go index 67eef77..00bc4a3 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -18,7 +18,6 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - "github.com/SamuelTariku/FortuneBet-Backend/internal/infrastructure" customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger" "github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger" @@ -36,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" @@ -43,12 +44,12 @@ import ( referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/report" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet/monitor" @@ -56,7 +57,6 @@ import ( httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" - "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/worker" ) // @title FortuneBet API @@ -87,7 +87,7 @@ func main() { logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel) - domain.MongoDBLogger, err = mongoLogger.InitLogger() + domain.MongoDBLogger, err = mongoLogger.InitLogger(cfg) if err != nil { log.Fatalf("Logger initialization failed: %v", err) } @@ -99,11 +99,11 @@ func main() { v := customvalidator.NewCustomValidator(validator.New()) // Initialize services + settingSvc := settings.NewService(store) authSvc := authentication.NewService(store, store, cfg.RefreshExpiry) userSvc := user.NewService(store, store, cfg) eventSvc := event.New(cfg.Bet365Token, store) oddsSvc := odds.New(store, cfg, logger) - ticketSvc := ticket.NewService(store) notificationRepo := repository.NewNotificationRepository(store) virtuaGamesRepo := repository.NewVirtualGameRepository(store) notificationSvc := notificationservice.New(notificationRepo, logger, cfg) @@ -121,8 +121,9 @@ func main() { branchSvc := branch.NewService(store) companySvc := company.NewService(store) leagueSvc := league.New(store) + ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger, *settingSvc, notificationSvc) betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, logger, domain.MongoDBLogger) - resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc) + resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc) referalRepo := repository.NewReferralRepository(store) vitualGameRepo := repository.NewVirtualGameRepository(store) recommendationRepo := repository.NewRecommendationRepository(store) @@ -130,7 +131,7 @@ func main() { referalSvc := referralservice.New(referalRepo, *walletSvc, store, cfg, logger) virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger) aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger) - veliService := veli.NewVeliPlayService(vitualGameRepo, *walletSvc, cfg, logger) + // veliService := veli.NewVeliPlayService(vitualGameRepo, *walletSvc, cfg, logger) recommendationSvc := recommendation.NewService(recommendationRepo) chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY) @@ -138,6 +139,7 @@ func main() { wallet.TransferStore(store), *walletSvc, user.UserStore(store), + cfg, chapaClient, ) @@ -162,15 +164,19 @@ func main() { logger, ) - // Initialize report worker with CSV exporter - csvExporter := infrastructure.CSVExporter{ - ExportPath: cfg.ReportExportPath, // Make sure to add this to your config - } + go httpserver.SetupReportCronJobs(context.Background(), reportSvc) - reportWorker := worker.NewReportWorker( - reportSvc, - csvExporter, - ) + 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 + // } + + // reportWorker := worker.NewReportWorker( + // reportSvc, + // csvExporter, + // ) // Start cron jobs for automated reporting @@ -196,13 +202,39 @@ func main() { httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc) httpserver.StartTicketCrons(*ticketSvc) - go httpserver.SetupReportCronJob(reportWorker) + + // 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, + settingSvc, authSvc, logger, jwtutil.JwtConfig{ @@ -225,10 +257,11 @@ func main() { referalSvc, virtualGameSvc, aleaService, - veliService, + // veliService, recommendationSvc, resultSvc, cfg, + domain.MongoDBLogger, ) logger.Info("Starting server", "port", cfg.Port) @@ -236,4 +269,5 @@ func main() { logger.Error("Failed to start server", "error", err) os.Exit(1) } + select {} } diff --git a/db.sql b/db.sql new file mode 100644 index 0000000..c149b92 --- /dev/null +++ b/db.sql @@ -0,0 +1,17 @@ +psql (16.8) +Type "help" for help. + +gh=# +gh=# +gh=# +gh=# +gh=# +gh=# +gh=# +gh=# +gh=# +gh=# +gh=# +gh=# +gh=#  +gh=# \ No newline at end of file diff --git a/db/migrations/000001_fortune.down.sql b/db/migrations/000001_fortune.down.sql index 2724f06..2332e39 100644 --- a/db/migrations/000001_fortune.down.sql +++ b/db/migrations/000001_fortune.down.sql @@ -76,4 +76,7 @@ DROP TABLE IF EXISTS refresh_tokens; DROP TABLE IF EXISTS otps; DROP TABLE IF EXISTS odds; DROP TABLE IF EXISTS events; -DROP TABLE IF EXISTS leagues; \ No newline at end of file +DROP TABLE IF EXISTS leagues; +DROP TABLE IF EXISTS teams; +DROP TABLE IF EXISTS settings; +-- DELETE FROM wallet_transfer; \ No newline at end of file diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 393e2cc..6ed5000 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -55,6 +55,7 @@ CREATE TABLE IF NOT EXISTS bets ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, is_shop_bet BOOLEAN NOT NULL, + outcomes_hash TEXT NOT NULL, UNIQUE(cashout_id), CHECK ( user_id IS NOT NULL @@ -111,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, @@ -138,8 +156,8 @@ CREATE TABLE IF NOT EXISTS wallet_transfer ( sender_wallet_id BIGINT, cashier_id BIGINT, verified BOOLEAN DEFAULT false, - reference_number VARCHAR(255), - status VARCHAR(255), + reference_number VARCHAR(255) NOT NULL, + status VARCHAR(255), payment_method VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP @@ -249,10 +267,12 @@ CREATE TABLE companies ( CREATE TABLE leagues ( id BIGINT PRIMARY KEY, name TEXT NOT NULL, + img TEXT, country_code TEXT, bet365_id INT, sport_id INT NOT NULL, - is_active BOOLEAN DEFAULT true + is_active BOOLEAN DEFAULT true, + is_featured BOOLEAN DEFAULT false ); CREATE TABLE teams ( id TEXT PRIMARY KEY, @@ -261,6 +281,12 @@ CREATE TABLE teams ( bet365_id INT, logo_url TEXT ); +CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); -- Views CREATE VIEW companies_details AS SELECT companies.*, @@ -273,12 +299,12 @@ FROM companies JOIN wallets ON wallets.id = companies.wallet_id JOIN users ON users.id = companies.admin_id; ; - CREATE VIEW branch_details AS SELECT branches.*, CONCAT(users.first_name, ' ', users.last_name) AS manager_name, users.phone_number AS manager_phone_number, - wallets.balance + wallets.balance, + wallets.is_active AS wallet_is_active FROM branches LEFT JOIN users ON branches.branch_manager_id = users.id LEFT JOin wallets ON wallets.id = branches.wallet_id; @@ -299,42 +325,60 @@ SELECT tickets.*, FROM tickets LEFT JOIN ticket_outcomes ON tickets.id = ticket_outcomes.ticket_id GROUP BY tickets.id; +CREATE VIEW customer_wallet_details AS +SELECT cw.id, + cw.customer_id, + rw.id AS regular_id, + rw.balance AS regular_balance, + sw.id AS static_id, + sw.balance AS static_balance, + rw.is_active as regular_is_active, + sw.is_active as static_is_active, + rw.updated_at as regular_updated_at, + sw.updated_at as static_updated_at, + cw.created_at, + users.first_name, + users.last_name, + users.phone_number +FROM customer_wallets cw + JOIN wallets rw ON cw.regular_wallet_id = rw.id + JOIN wallets sw ON cw.static_wallet_id = sw.id + JOIN users ON users.id = cw.customer_id; -- Foreign Keys - ALTER TABLE users - ADD CONSTRAINT unique_email UNIQUE (email), +ADD CONSTRAINT unique_email UNIQUE (email), ADD CONSTRAINT unique_phone_number UNIQUE (phone_number); ALTER TABLE refresh_tokens - ADD CONSTRAINT fk_refresh_tokens_users FOREIGN KEY (user_id) REFERENCES users(id); +ADD CONSTRAINT fk_refresh_tokens_users FOREIGN KEY (user_id) REFERENCES users(id); ALTER TABLE bets - ADD CONSTRAINT fk_bets_users FOREIGN KEY (user_id) REFERENCES users(id), +ADD CONSTRAINT fk_bets_users FOREIGN KEY (user_id) REFERENCES users(id), ADD CONSTRAINT fk_bets_branches FOREIGN KEY (branch_id) REFERENCES branches(id); ALTER TABLE wallets - ADD CONSTRAINT fk_wallets_users FOREIGN KEY (user_id) REFERENCES users(id), +ADD CONSTRAINT fk_wallets_users FOREIGN KEY (user_id) REFERENCES users(id), ADD COLUMN currency VARCHAR(3) NOT NULL DEFAULT 'ETB'; ALTER TABLE customer_wallets - ADD CONSTRAINT fk_customer_wallets_customers FOREIGN KEY (customer_id) REFERENCES users(id), +ADD CONSTRAINT fk_customer_wallets_customers FOREIGN KEY (customer_id) REFERENCES users(id), ADD CONSTRAINT fk_customer_wallets_regular_wallet FOREIGN KEY (regular_wallet_id) REFERENCES wallets(id), ADD CONSTRAINT fk_customer_wallets_static_wallet FOREIGN KEY (static_wallet_id) REFERENCES wallets(id); ALTER TABLE wallet_transfer - ADD CONSTRAINT fk_wallet_transfer_receiver_wallet FOREIGN KEY (receiver_wallet_id) REFERENCES wallets(id), +ADD CONSTRAINT fk_wallet_transfer_receiver_wallet FOREIGN KEY (receiver_wallet_id) REFERENCES wallets(id), ADD CONSTRAINT fk_wallet_transfer_sender_wallet FOREIGN KEY (sender_wallet_id) REFERENCES wallets(id), ADD CONSTRAINT fk_wallet_transfer_cashier FOREIGN KEY (cashier_id) REFERENCES users(id); ALTER TABLE transactions - ADD CONSTRAINT fk_transactions_branches FOREIGN KEY (branch_id) REFERENCES branches(id), +ADD CONSTRAINT fk_transactions_branches FOREIGN KEY (branch_id) REFERENCES branches(id), ADD CONSTRAINT fk_transactions_cashiers FOREIGN KEY (cashier_id) REFERENCES users(id), ADD CONSTRAINT fk_transactions_bets FOREIGN KEY (bet_id) REFERENCES bets(id); ALTER TABLE branches - ADD CONSTRAINT fk_branches_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id), +ADD CONSTRAINT fk_branches_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id), ADD CONSTRAINT fk_branches_manager FOREIGN KEY (branch_manager_id) REFERENCES users(id); ALTER TABLE branch_operations - ADD CONSTRAINT fk_branch_operations_operations FOREIGN KEY (operation_id) REFERENCES supported_operations(id) ON DELETE CASCADE, +ADD CONSTRAINT fk_branch_operations_operations FOREIGN KEY (operation_id) REFERENCES supported_operations(id) ON DELETE CASCADE, ADD CONSTRAINT fk_branch_operations_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE; ALTER TABLE branch_cashiers - ADD CONSTRAINT fk_branch_cashiers_users FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, +ADD CONSTRAINT fk_branch_cashiers_users FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, ADD CONSTRAINT fk_branch_cashiers_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE; ALTER TABLE companies - ADD CONSTRAINT fk_companies_admin FOREIGN KEY (admin_id) REFERENCES users(id), +ADD CONSTRAINT fk_companies_admin FOREIGN KEY (admin_id) REFERENCES users(id), ADD CONSTRAINT fk_companies_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE; ----------------------------------------------seed data------------------------------------------------------------- -------------------------------------- DO NOT USE IN PRODUCTION------------------------------------------------- diff --git a/db/migrations/000004_virtual_game_Sessios.up.sql b/db/migrations/000004_virtual_game_Sessios.up.sql index 09606ba..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, @@ -40,6 +43,41 @@ CREATE TABLE virtual_game_transactions ( updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); +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 + amount BIGINT NOT NULL, -- in cents or smallest currency unit + currency VARCHAR(10) NOT NULL, + external_transaction_id VARCHAR(100) NOT NULL, + reference_transaction_id VARCHAR(100), -- nullable, for cancel/refund + status VARCHAR(20) NOT NULL DEFAULT 'COMPLETED', -- transaction status + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + 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); +CREATE INDEX idx_virtual_game_game_id ON virtual_game_histories(game_id); +CREATE INDEX idx_virtual_game_external_transaction_id ON virtual_game_histories(external_transaction_id); + 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/000006_recommendation.up.sql b/db/migrations/000006_recommendation.up.sql index f7806c5..e0900fa 100644 --- a/db/migrations/000006_recommendation.up.sql +++ b/db/migrations/000006_recommendation.up.sql @@ -1,28 +1,28 @@ --- CREATE TABLE virtual_games ( --- id BIGSERIAL PRIMARY KEY, --- name VARCHAR(255) NOT NULL, --- provider VARCHAR(100) NOT NULL, --- category VARCHAR(100) NOT NULL, --- min_bet DECIMAL(15,2) NOT NULL, --- max_bet DECIMAL(15,2) NOT NULL, --- volatility VARCHAR(50) NOT NULL, --- rtp DECIMAL(5,2) NOT NULL, --- is_featured BOOLEAN DEFAULT false, --- popularity_score INTEGER DEFAULT 0, --- thumbnail_url TEXT, --- created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, --- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP --- ); +CREATE TABLE IF NOT EXISTS virtual_games ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + provider VARCHAR(100) NOT NULL, + category VARCHAR(100) NOT NULL, + min_bet DECIMAL(15,2) NOT NULL, + max_bet DECIMAL(15,2) NOT NULL, + volatility VARCHAR(50) NOT NULL, + rtp DECIMAL(5,2) NOT NULL, + is_featured BOOLEAN DEFAULT false, + popularity_score INTEGER DEFAULT 0, + thumbnail_url TEXT, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); CREATE TABLE user_game_interactions ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id), game_id BIGINT NOT NULL REFERENCES virtual_games(id), - interaction_type VARCHAR(50) NOT NULL, -- 'view', 'play', 'bet', 'favorite' - amount DECIMAL(15,2), + interaction_type VARCHAR(50) NOT NULL, + -- 'view', 'play', 'bet', 'favorite' + amount DECIMAL(15, 2), duration_seconds INTEGER, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); - CREATE INDEX idx_user_game_interactions_user ON user_game_interactions(user_id); CREATE INDEX idx_user_game_interactions_game ON user_game_interactions(game_id); \ No newline at end of file diff --git a/db/migrations/000007_setting_data.up.sql b/db/migrations/000007_setting_data.up.sql new file mode 100644 index 0000000..f4cdfd6 --- /dev/null +++ b/db/migrations/000007_setting_data.up.sql @@ -0,0 +1,17 @@ +-- Settings Initial Data +INSERT INTO settings (key, value) +VALUES ('max_number_of_outcomes', '30') ON CONFLICT (key) DO +UPDATE +SET value = EXCLUDED.value; +INSERT INTO settings (key, value) +VALUES ('bet_amount_limit', '100000') ON CONFLICT (key) DO +UPDATE +SET value = EXCLUDED.value; +INSERT INTO settings (key, value) +VALUES ('daily_ticket_limit', '50') ON CONFLICT (key) DO +UPDATE +SET value = EXCLUDED.value; +INSERT INTO settings (key, value) +VALUES ('total_winnings_limit', '1000000') ON CONFLICT (key) DO +UPDATE +SET value = EXCLUDED.value; \ No newline at end of file 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/bet.sql b/db/query/bet.sql index 335cf56..8989ffe 100644 --- a/db/query/bet.sql +++ b/db/query/bet.sql @@ -9,9 +9,10 @@ INSERT INTO bets ( user_id, is_shop_bet, cashout_id, - company_id + company_id, + outcomes_hash ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *; -- name: CreateBetOutcome :copyfrom INSERT INTO bet_outcomes ( @@ -48,16 +49,33 @@ VALUES ( SELECT * FROM bet_with_outcomes wHERE ( - branch_id = $1 - OR $1 IS NULL + branch_id = sqlc.narg('branch_id') + OR sqlc.narg('branch_id') IS NULL ) AND ( - company_id = $2 - OR $2 IS NULL + company_id = sqlc.narg('company_id') + OR sqlc.narg('company_id') IS NULL ) AND ( - user_id = $3 - OR $3 IS NULL + user_id = sqlc.narg('user_id') + OR sqlc.narg('user_id') IS NULL + ) + AND ( + is_shop_bet = sqlc.narg('is_shop_bet') + OR sqlc.narg('is_shop_bet') IS NULL + ) + AND ( + full_name ILIKE '%' || sqlc.narg('query') || '%' + OR phone_number ILIKE '%' || sqlc.narg('query') || '%' + OR sqlc.narg('query') IS NULL + ) + AND ( + created_at > sqlc.narg('created_before') + OR sqlc.narg('created_before') IS NULL + ) + AND ( + created_at < sqlc.narg('created_after') + OR sqlc.narg('created_after') IS NULL ); -- name: GetBetByID :one SELECT * @@ -78,11 +96,22 @@ WHERE user_id = $1; -- name: GetBetOutcomeByEventID :many SELECT * FROM bet_outcomes -WHERE event_id = $1; +WHERE (event_id = $1) + AND ( + status = sqlc.narg('filter_status') + OR sqlc.narg('filter_status') IS NULL + OR status = sqlc.narg('filter_status_2') + OR sqlc.narg('filter_status_2') IS NULL + ); -- name: GetBetOutcomeByBetID :many SELECT * FROM bet_outcomes WHERE bet_id = $1; +-- name: GetBetCount :one +SELECT COUNT(*) +FROM bets +where user_id = $1 + AND outcomes_hash = $2; -- name: UpdateCashOut :exec UPDATE bets SET cashed_out = $2, @@ -93,6 +122,16 @@ UPDATE bet_outcomes SET status = $1 WHERE id = $2 RETURNING *; +-- name: UpdateBetOutcomeStatusByBetID :one +UPDATE bet_outcomes +SET status = $1 +WHERE bet_id = $2 +RETURNING *; +-- name: UpdateBetOutcomeStatusForEvent :many +UPDATE bet_outcomes +SEt status = $1 +WHERE event_id = $2 +RETURNING *; -- name: UpdateStatus :exec UPDATE bets SET status = $1, diff --git a/db/query/branch.sql b/db/query/branch.sql index bb01b26..34f22eb 100644 --- a/db/query/branch.sql +++ b/db/query/branch.sql @@ -23,7 +23,32 @@ VALUES ($1, $2) RETURNING *; -- name: GetAllBranches :many SELECT * -FROM branch_details; +FROM branch_details +WHERE ( + company_id = sqlc.narg('company_id') + OR sqlc.narg('company_id') IS NULL + ) + AND ( + is_active = sqlc.narg('is_active') + OR sqlc.narg('is_active') IS NULL + ) + AND ( + branch_manager_id = sqlc.narg('branch_manager_id') + OR sqlc.narg('branch_manager_id') IS NULL + ) + AND ( + name ILIKE '%' || sqlc.narg('query') || '%' + OR location ILIKE '%' || sqlc.narg('query') || '%' + OR sqlc.narg('query') IS NULL + ) + AND ( + created_at > sqlc.narg('created_before') + OR sqlc.narg('created_before') IS NULL + ) + AND ( + created_at < sqlc.narg('created_after') + OR sqlc.narg('created_after') IS NULL + ); -- name: GetBranchByID :one SELECT * FROM branch_details @@ -61,7 +86,8 @@ SET name = COALESCE(sqlc.narg(name), name), location = COALESCE(sqlc.narg(location), location), branch_manager_id = COALESCE(sqlc.narg(branch_manager_id), branch_manager_id), company_id = COALESCE(sqlc.narg(company_id), company_id), - is_self_owned = COALESCE(sqlc.narg(is_self_owned), is_self_owned) + is_self_owned = COALESCE(sqlc.narg(is_self_owned), is_self_owned), + is_active = COALESCE(sqlc.narg(is_active), is_active) WHERE id = $1 RETURNING *; -- name: DeleteBranch :exec 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/leagues.sql b/db/query/leagues.sql index e8ee241..7aa7623 100644 --- a/db/query/leagues.sql +++ b/db/query/leagues.sql @@ -5,14 +5,16 @@ INSERT INTO leagues ( country_code, bet365_id, sport_id, - is_active + is_active, + is_featured ) -VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO +VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, country_code = EXCLUDED.country_code, bet365_id = EXCLUDED.bet365_id, is_active = EXCLUDED.is_active, + is_featured = EXCLUDED.is_featured, sport_id = EXCLUDED.sport_id; -- name: GetAllLeagues :many SELECT id, @@ -20,6 +22,7 @@ SELECT id, country_code, bet365_id, is_active, + is_featured, sport_id FROM leagues WHERE ( @@ -34,7 +37,21 @@ WHERE ( is_active = sqlc.narg('is_active') OR sqlc.narg('is_active') IS NULL ) + AND ( + is_featured = sqlc.narg('is_featured') + OR sqlc.narg('is_featured') IS NULL + ) LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); +-- name: GetFeaturedLeagues :many +SELECT id, + name, + country_code, + bet365_id, + is_active, + is_featured, + sport_id +FROM leagues +WHERE is_featured = true; -- name: CheckLeagueSupport :one SELECT EXISTS( SELECT 1 @@ -48,6 +65,7 @@ SET name = COALESCE(sqlc.narg('name'), name), country_code = COALESCE(sqlc.narg('country_code'), country_code), bet365_id = COALESCE(sqlc.narg('bet365_id'), bet365_id), is_active = COALESCE(sqlc.narg('is_active'), is_active), + is_featured = COALESCE(sqlc.narg('is_featured'), is_featured), sport_id = COALESCE(sqlc.narg('sport_id'), sport_id) WHERE id = $1; -- name: UpdateLeagueByBet365ID :exec @@ -56,6 +74,7 @@ SET name = COALESCE(sqlc.narg('name'), name), id = COALESCE(sqlc.narg('id'), id), country_code = COALESCE(sqlc.narg('country_code'), country_code), is_active = COALESCE(sqlc.narg('is_active'), is_active), + is_featured = COALESCE(sqlc.narg('is_featured'), is_featured), sport_id = COALESCE(sqlc.narg('sport_id'), sport_id) WHERE bet365_id = $1; -- name: SetLeagueActive :exec diff --git a/db/query/report.sql b/db/query/report.sql new file mode 100644 index 0000000..24677c1 --- /dev/null +++ b/db/query/report.sql @@ -0,0 +1,44 @@ +-- name: GetTotalBetsMadeInRange :one +SELECT COUNT(*) AS total_bets +FROM bets +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'); +-- 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; +-- 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; +-- 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/settings.sql b/db/query/settings.sql new file mode 100644 index 0000000..d0f4482 --- /dev/null +++ b/db/query/settings.sql @@ -0,0 +1,13 @@ +-- name: GetSettings :many +SELECT * +FROM settings; +-- name: GetSetting :one +SELECT * +FROM settings +WHERE key = $1; +-- name: SaveSetting :one +INSERT INTO settings (key, value, updated_at) +VALUES ($1, $2, CURRENT_TIMESTAMP) ON CONFLICT (key) DO +UPDATE +SET value = EXCLUDED.value +RETURNING *; \ No newline at end of file diff --git a/db/query/ticket.sql b/db/query/ticket.sql index d091f04..7648842 100644 --- a/db/query/ticket.sql +++ b/db/query/ticket.sql @@ -58,4 +58,8 @@ Delete from tickets where created_at < now() - interval '1 day'; -- name: DeleteTicketOutcome :exec Delete from ticket_outcomes -where ticket_id = $1; \ No newline at end of file +where ticket_id = $1; +-- name: GetAllTicketsInRange :one +SELECT COUNT(*) as total_tickets, COALESCE(SUM(amount), 0) as total_amount +FROM tickets +WHERE created_at BETWEEN $1 AND $2; \ No newline at end of file diff --git a/db/query/transfer.sql b/db/query/transfer.sql index ac8e7ed..b4cc137 100644 --- a/db/query/transfer.sql +++ b/db/query/transfer.sql @@ -38,4 +38,13 @@ WHERE id = $2; UPDATE wallet_transfer SET status = $1, updated_at = CURRENT_TIMESTAMP -WHERE id = $2; \ No newline at end of file +WHERE id = $2; + +-- name: GetWalletTransactionsInRange :many +SELECT type, COUNT(*) as count, SUM(amount) as total_amount +FROM wallet_transfer +WHERE created_at BETWEEN $1 AND $2 +GROUP BY type; + + + diff --git a/db/query/virtual_games.sql b/db/query/virtual_games.sql index e04a24e..68f2fca 100644 --- a/db/query/virtual_games.sql +++ b/db/query/virtual_games.sql @@ -4,30 +4,85 @@ 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, + amount, + currency, + external_transaction_id, + reference_transaction_id, + status +) VALUES ( + $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, + amount, + currency, + external_transaction_id, + reference_transaction_id, + 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(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..79028e5 100644 --- a/db/query/wallet.sql +++ b/db/query/wallet.sql @@ -26,20 +26,13 @@ WHERE id = $1; SELECT * FROM wallets WHERE user_id = $1; +-- name: GetAllCustomerWallet :many +SELECT * +FROM customer_wallet_details; -- name: GetCustomerWallet :one -SELECT cw.id, - cw.customer_id, - rw.id AS regular_id, - rw.balance AS regular_balance, - sw.id AS static_id, - sw.balance AS static_balance, - rw.updated_at as regular_updated_at, - sw.updated_at as static_updated_at, - cw.created_at -FROM customer_wallets cw - JOIN wallets rw ON cw.regular_wallet_id = rw.id - JOIN wallets sw ON cw.static_wallet_id = sw.id -WHERE cw.customer_id = $1; +SELECT * +FROM customer_wallet_details +WHERE customer_id = $1; -- name: GetAllBranchWallets :many SELECT wallets.id, wallets.balance, @@ -62,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/docker-compose.yml b/docker-compose.yml index bf5801f..39cb050 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,5 @@ +version: "3.8" + services: postgres: image: postgres:16-alpine @@ -54,6 +56,18 @@ services: networks: - app + redis: + image: redis:7-alpine + ports: + - "6379:6379" + networks: + - app + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + app: build: context: . @@ -64,14 +78,19 @@ services: environment: - DB_URL=postgresql://root:secret@postgres:5432/gh?sslmode=disable - MONGO_URI=mongodb://root:secret@mongo:27017 + - REDIS_ADDR=redis:6379 depends_on: migrate: condition: service_completed_successfully mongo: condition: service_healthy + redis: + condition: service_healthy networks: - app command: ["/app/bin/web"] + volumes: + - "C:/Users/User/Desktop:/host-desktop" test: build: diff --git a/docs/docs.go b/docs/docs.go index f8825e0..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,347 @@ 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", + "produces": [ + "application/json" + ], + "tags": [ + "Logs" + ], + "summary": "Retrieve latest application logs", + "responses": { + "200": { + "description": "List of application logs", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.LogEntry" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/report-files/download/{filename}": { + "get": { + "description": "Downloads a generated report CSV file from the server", + "produces": [ + "text/csv" + ], + "tags": [ + "Reports" + ], + "summary": "Download a CSV report file", + "parameters": [ + { + "type": "string", + "description": "Name of the report file to download (e.g., report_daily_2025-06-21.csv)", + "name": "filename", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "CSV file will be downloaded", + "schema": { + "type": "file" + } + }, + "400": { + "description": "Missing or invalid filename", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Report file not found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal server error while serving the file", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/report-files/list": { + "get": { + "description": "Returns a list of all generated report CSV files available for download", + "produces": [ + "application/json" + ], + "tags": [ + "Reports" + ], + "summary": "List available report CSV files", + "responses": { + "200": { + "description": "List of CSV report filenames", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + } + }, + "500": { + "description": "Failed to read report directory", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/reports/dashboard": { "get": { "security": [ @@ -724,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" ], @@ -734,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" } } } @@ -811,76 +1439,6 @@ const docTemplate = `{ } } }, - "/api/veli/launch/{game_id}": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Generates authenticated launch URL for Veli games", - "tags": [ - "Veli Games" - ], - "summary": "Launch a Veli game", - "parameters": [ - { - "type": "string", - "description": "Game ID (e.g., veli_aviator_v1)", - "name": "game_id", - "in": "path", - "required": true - }, - { - "type": "string", - "default": "USD", - "description": "Currency code", - "name": "currency", - "in": "query" - }, - { - "enum": [ - "real", - "demo" - ], - "type": "string", - "default": "real", - "description": "Game mode", - "name": "mode", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Returns launch URL", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "400": { - "description": "Invalid request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal server error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, "/auth/login": { "post": { "description": "Login customer", @@ -1037,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", @@ -2957,9 +3252,9 @@ const docTemplate = `{ } } }, - "/random/bet": { - "post": { - "description": "Generate a random bet", + "/popok/games": { + "get": { + "description": "Retrieves the list of available PopOK slot games", "consumes": [ "application/json" ], @@ -2967,37 +3262,70 @@ const docTemplate = `{ "application/json" ], "tags": [ - "bet" + "Virtual Games - PopOK" ], - "summary": "Generate a random bet", + "summary": "Get PopOK Games List", "parameters": [ { - "description": "Create Random bet", - "name": "createBet", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.RandomBetReq" - } + "type": "string", + "default": "USD", + "description": "Currency (e.g. USD, ETB)", + "name": "currency", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/domain.BetRes" + "type": "array", + "items": { + "$ref": "#/definitions/domain.PopOKGame" + } } }, - "400": { - "description": "Bad Request", + "502": { + "description": "Bad Gateway", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/popok/games/recommend": { + "get": { + "description": "Recommends games based on user history or randomly", + "produces": [ + "application/json" + ], + "tags": [ + "Virtual Games - PopOK" + ], + "summary": "Recommend virtual games", + "parameters": [ + { + "type": "integer", + "description": "User ID", + "name": "user_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.GameRecommendation" + } } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/domain.ErrorResponse" } } } @@ -3276,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", @@ -3377,7 +4014,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/handlers.TicketRes" + "$ref": "#/definitions/domain.TicketRes" } } }, @@ -3414,7 +4051,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handlers.CreateTicketReq" + "$ref": "#/definitions/domain.CreateTicketReq" } } ], @@ -3422,7 +4059,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.CreateTicketRes" + "$ref": "#/definitions/domain.CreateTicketRes" } }, "400": { @@ -3466,7 +4103,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.TicketRes" + "$ref": "#/definitions/domain.TicketRes" } }, "400": { @@ -3484,6 +4121,38 @@ const docTemplate = `{ } } }, + "/top-leagues": { + "get": { + "description": "Retrieve all top leagues", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve all top leagues", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.UpcomingEvent" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/transaction": { "get": { "description": "Gets all the transactions", @@ -4352,7 +5021,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "virtual-game" + "Virtual Games - PopOK" ], "summary": "Handle PopOK game callback", "parameters": [ @@ -4370,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" } } } @@ -4403,7 +5072,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "virtual-game" + "Virtual Games - PopOK" ], "summary": "Launch a PopOK virtual game", "parameters": [ @@ -4427,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" } } } @@ -4577,70 +5246,6 @@ const docTemplate = `{ } } } - }, - "/webhooks/veli": { - "post": { - "description": "Processes game round settlements from Veli", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Veli Games" - ], - "summary": "Veli Games webhook handler", - "parameters": [ - { - "description": "Callback payload", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.VeliCallback" - } - } - ], - "responses": { - "200": { - "description": "Callback processed", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "400": { - "description": "Invalid payload", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "403": { - "description": "Invalid signature", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Processing error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } } }, "definitions": { @@ -4692,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": { @@ -4956,6 +5614,52 @@ const docTemplate = `{ } } }, + "domain.CreateTicketOutcomeReq": { + "type": "object", + "properties": { + "event_id": { + "description": "TicketID int64 ` + "`" + `json:\"ticket_id\" example:\"1\"` + "`" + `", + "type": "integer", + "example": 1 + }, + "market_id": { + "type": "integer", + "example": 1 + }, + "odd_id": { + "type": "integer", + "example": 1 + } + } + }, + "domain.CreateTicketReq": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "example": 100 + }, + "outcomes": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.CreateTicketOutcomeReq" + } + } + } + }, + "domain.CreateTicketRes": { + "type": "object", + "properties": { + "created_number": { + "type": "integer", + "example": 3 + }, + "fast_code": { + "type": "integer", + "example": 1234 + } + } + }, "domain.DashboardSummary": { "type": "object", "properties": { @@ -5113,6 +5817,38 @@ const docTemplate = `{ "STATUS_REMOVED" ] }, + "domain.FavoriteGameRequest": { + "type": "object", + "properties": { + "game_id": { + "type": "integer" + } + } + }, + "domain.GameRecommendation": { + "type": "object", + "properties": { + "bets": { + "type": "array", + "items": { + "type": "number" + } + }, + "game_id": { + "type": "integer" + }, + "game_name": { + "type": "string" + }, + "reason": { + "description": "e.g., \"Based on your activity\", \"Popular\", \"Random pick\"", + "type": "string" + }, + "thumbnail": { + "type": "string" + } + } + }, "domain.League": { "type": "object", "properties": { @@ -5132,6 +5868,10 @@ const docTemplate = `{ "type": "boolean", "example": false }, + "is_featured": { + "type": "boolean", + "example": false + }, "name": { "type": "string", "example": "BPL" @@ -5142,6 +5882,36 @@ const docTemplate = `{ } } }, + "domain.LogEntry": { + "type": "object", + "properties": { + "caller": { + "type": "string" + }, + "env": { + "type": "string" + }, + "fields": { + "type": "object", + "additionalProperties": true + }, + "level": { + "type": "string" + }, + "message": { + "type": "string" + }, + "service": { + "type": "string" + }, + "stacktrace": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, "domain.Odd": { "type": "object", "properties": { @@ -5193,6 +5963,17 @@ const docTemplate = `{ } } }, + "domain.OtpProvider": { + "type": "string", + "enum": [ + "twilio", + "aformessage" + ], + "x-enum-varnames": [ + "TwilioSms", + "AfroMessage" + ] + }, "domain.OutcomeStatus": { "type": "integer", "enum": [ @@ -5235,11 +6016,13 @@ const docTemplate = `{ "domain.PaymentStatus": { "type": "string", "enum": [ + "success", "pending", "completed", "failed" ], "x-enum-varnames": [ + "PaymentStatusSuccessful", "PaymentStatusPending", "PaymentStatusCompleted", "PaymentStatusFailed" @@ -5273,6 +6056,29 @@ const docTemplate = `{ } } }, + "domain.PopOKGame": { + "type": "object", + "properties": { + "bets": { + "type": "array", + "items": { + "type": "number" + } + }, + "gameName": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "status": { + "type": "integer" + }, + "thumbnail": { + "type": "string" + } + } + }, "domain.RandomBetReq": { "type": "object", "required": [ @@ -5311,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": { @@ -5385,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": { @@ -5482,6 +6299,29 @@ const docTemplate = `{ } } }, + "domain.TicketRes": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "example": 100 + }, + "id": { + "type": "integer", + "example": 1 + }, + "outcomes": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.TicketOutcome" + } + }, + "total_odds": { + "type": "number", + "example": 4.22 + } + } + }, "domain.UpcomingEvent": { "type": "object", "properties": { @@ -5551,98 +6391,6 @@ const docTemplate = `{ } } }, - "domain.VeliCallback": { - "type": "object", - "properties": { - "amount": { - "description": "Transaction amount", - "type": "number" - }, - "currency": { - "description": "e.g., \"USD\"", - "type": "string" - }, - "event_type": { - "description": "\"bet_placed\", \"game_result\", etc.", - "type": "string" - }, - "game_id": { - "description": "e.g., \"veli_aviator_v1\"", - "type": "string" - }, - "multiplier": { - "description": "For games with multipliers (Aviator/Plinko)", - "type": "number" - }, - "round_id": { - "description": "Unique round identifier (replaces transaction_id)", - "type": "string" - }, - "session_id": { - "description": "Matches VirtualGameSession.SessionToken", - "type": "string" - }, - "signature": { - "description": "HMAC-SHA256", - "type": "string" - }, - "timestamp": { - "description": "Unix timestamp", - "type": "integer" - }, - "user_id": { - "description": "Veli's user identifier", - "type": "string" - } - } - }, - "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": { @@ -5994,52 +6742,6 @@ const docTemplate = `{ } } }, - "handlers.CreateTicketOutcomeReq": { - "type": "object", - "properties": { - "event_id": { - "description": "TicketID int64 ` + "`" + `json:\"ticket_id\" example:\"1\"` + "`" + `", - "type": "integer", - "example": 1 - }, - "market_id": { - "type": "integer", - "example": 1 - }, - "odd_id": { - "type": "integer", - "example": 1 - } - } - }, - "handlers.CreateTicketReq": { - "type": "object", - "properties": { - "amount": { - "type": "number", - "example": 100 - }, - "outcomes": { - "type": "array", - "items": { - "$ref": "#/definitions/handlers.CreateTicketOutcomeReq" - } - } - } - }, - "handlers.CreateTicketRes": { - "type": "object", - "properties": { - "created_number": { - "type": "integer", - "example": 3 - }, - "fast_code": { - "type": "integer", - "example": 1234 - } - } - }, "handlers.CreateTransactionReq": { "type": "object", "properties": { @@ -6249,6 +6951,9 @@ const docTemplate = `{ }, "handlers.RegisterCodeReq": { "type": "object", + "required": [ + "provider" + ], "properties": { "email": { "type": "string", @@ -6257,11 +6962,22 @@ const docTemplate = `{ "phone_number": { "type": "string", "example": "1234567890" + }, + "provider": { + "allOf": [ + { + "$ref": "#/definitions/domain.OtpProvider" + } + ], + "example": "twilio" } } }, "handlers.RegisterUserReq": { "type": "object", + "required": [ + "provider" + ], "properties": { "email": { "type": "string", @@ -6287,6 +7003,14 @@ const docTemplate = `{ "type": "string", "example": "1234567890" }, + "provider": { + "allOf": [ + { + "$ref": "#/definitions/domain.OtpProvider" + } + ], + "example": "twilio" + }, "referal_code": { "type": "string", "example": "ABC123" @@ -6295,6 +7019,9 @@ const docTemplate = `{ }, "handlers.ResetCodeReq": { "type": "object", + "required": [ + "provider" + ], "properties": { "email": { "type": "string", @@ -6303,6 +7030,14 @@ const docTemplate = `{ "phone_number": { "type": "string", "example": "1234567890" + }, + "provider": { + "allOf": [ + { + "$ref": "#/definitions/domain.OtpProvider" + } + ], + "example": "twilio" } } }, @@ -6371,29 +7106,6 @@ const docTemplate = `{ } } }, - "handlers.TicketRes": { - "type": "object", - "properties": { - "amount": { - "type": "number", - "example": 100 - }, - "id": { - "type": "integer", - "example": 1 - }, - "outcomes": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.TicketOutcome" - } - }, - "total_odds": { - "type": "number", - "example": 4.22 - } - } - }, "handlers.TransactionRes": { "type": "object", "properties": { @@ -6496,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 1bb4270..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,347 @@ } } }, + "/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", + "produces": [ + "application/json" + ], + "tags": [ + "Logs" + ], + "summary": "Retrieve latest application logs", + "responses": { + "200": { + "description": "List of application logs", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.LogEntry" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/report-files/download/{filename}": { + "get": { + "description": "Downloads a generated report CSV file from the server", + "produces": [ + "text/csv" + ], + "tags": [ + "Reports" + ], + "summary": "Download a CSV report file", + "parameters": [ + { + "type": "string", + "description": "Name of the report file to download (e.g., report_daily_2025-06-21.csv)", + "name": "filename", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "CSV file will be downloaded", + "schema": { + "type": "file" + } + }, + "400": { + "description": "Missing or invalid filename", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Report file not found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal server error while serving the file", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/report-files/list": { + "get": { + "description": "Returns a list of all generated report CSV files available for download", + "produces": [ + "application/json" + ], + "tags": [ + "Reports" + ], + "summary": "List available report CSV files", + "responses": { + "200": { + "description": "List of CSV report filenames", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + } + }, + "500": { + "description": "Failed to read report directory", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/reports/dashboard": { "get": { "security": [ @@ -716,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" ], @@ -726,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" } } } @@ -803,76 +1431,6 @@ } } }, - "/api/veli/launch/{game_id}": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Generates authenticated launch URL for Veli games", - "tags": [ - "Veli Games" - ], - "summary": "Launch a Veli game", - "parameters": [ - { - "type": "string", - "description": "Game ID (e.g., veli_aviator_v1)", - "name": "game_id", - "in": "path", - "required": true - }, - { - "type": "string", - "default": "USD", - "description": "Currency code", - "name": "currency", - "in": "query" - }, - { - "enum": [ - "real", - "demo" - ], - "type": "string", - "default": "real", - "description": "Game mode", - "name": "mode", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Returns launch URL", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "400": { - "description": "Invalid request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal server error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, "/auth/login": { "post": { "description": "Login customer", @@ -1029,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", @@ -2949,9 +3244,9 @@ } } }, - "/random/bet": { - "post": { - "description": "Generate a random bet", + "/popok/games": { + "get": { + "description": "Retrieves the list of available PopOK slot games", "consumes": [ "application/json" ], @@ -2959,37 +3254,70 @@ "application/json" ], "tags": [ - "bet" + "Virtual Games - PopOK" ], - "summary": "Generate a random bet", + "summary": "Get PopOK Games List", "parameters": [ { - "description": "Create Random bet", - "name": "createBet", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.RandomBetReq" - } + "type": "string", + "default": "USD", + "description": "Currency (e.g. USD, ETB)", + "name": "currency", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/domain.BetRes" + "type": "array", + "items": { + "$ref": "#/definitions/domain.PopOKGame" + } } }, - "400": { - "description": "Bad Request", + "502": { + "description": "Bad Gateway", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/popok/games/recommend": { + "get": { + "description": "Recommends games based on user history or randomly", + "produces": [ + "application/json" + ], + "tags": [ + "Virtual Games - PopOK" + ], + "summary": "Recommend virtual games", + "parameters": [ + { + "type": "integer", + "description": "User ID", + "name": "user_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.GameRecommendation" + } } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/domain.ErrorResponse" } } } @@ -3268,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", @@ -3369,7 +4006,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/handlers.TicketRes" + "$ref": "#/definitions/domain.TicketRes" } } }, @@ -3406,7 +4043,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handlers.CreateTicketReq" + "$ref": "#/definitions/domain.CreateTicketReq" } } ], @@ -3414,7 +4051,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.CreateTicketRes" + "$ref": "#/definitions/domain.CreateTicketRes" } }, "400": { @@ -3458,7 +4095,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.TicketRes" + "$ref": "#/definitions/domain.TicketRes" } }, "400": { @@ -3476,6 +4113,38 @@ } } }, + "/top-leagues": { + "get": { + "description": "Retrieve all top leagues", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve all top leagues", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.UpcomingEvent" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/transaction": { "get": { "description": "Gets all the transactions", @@ -4344,7 +5013,7 @@ "application/json" ], "tags": [ - "virtual-game" + "Virtual Games - PopOK" ], "summary": "Handle PopOK game callback", "parameters": [ @@ -4362,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" } } } @@ -4395,7 +5064,7 @@ "application/json" ], "tags": [ - "virtual-game" + "Virtual Games - PopOK" ], "summary": "Launch a PopOK virtual game", "parameters": [ @@ -4419,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" } } } @@ -4569,70 +5238,6 @@ } } } - }, - "/webhooks/veli": { - "post": { - "description": "Processes game round settlements from Veli", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Veli Games" - ], - "summary": "Veli Games webhook handler", - "parameters": [ - { - "description": "Callback payload", - "name": "payload", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.VeliCallback" - } - } - ], - "responses": { - "200": { - "description": "Callback processed", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "400": { - "description": "Invalid payload", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "403": { - "description": "Invalid signature", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Processing error", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } } }, "definitions": { @@ -4684,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": { @@ -4948,6 +5606,52 @@ } } }, + "domain.CreateTicketOutcomeReq": { + "type": "object", + "properties": { + "event_id": { + "description": "TicketID int64 `json:\"ticket_id\" example:\"1\"`", + "type": "integer", + "example": 1 + }, + "market_id": { + "type": "integer", + "example": 1 + }, + "odd_id": { + "type": "integer", + "example": 1 + } + } + }, + "domain.CreateTicketReq": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "example": 100 + }, + "outcomes": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.CreateTicketOutcomeReq" + } + } + } + }, + "domain.CreateTicketRes": { + "type": "object", + "properties": { + "created_number": { + "type": "integer", + "example": 3 + }, + "fast_code": { + "type": "integer", + "example": 1234 + } + } + }, "domain.DashboardSummary": { "type": "object", "properties": { @@ -5105,6 +5809,38 @@ "STATUS_REMOVED" ] }, + "domain.FavoriteGameRequest": { + "type": "object", + "properties": { + "game_id": { + "type": "integer" + } + } + }, + "domain.GameRecommendation": { + "type": "object", + "properties": { + "bets": { + "type": "array", + "items": { + "type": "number" + } + }, + "game_id": { + "type": "integer" + }, + "game_name": { + "type": "string" + }, + "reason": { + "description": "e.g., \"Based on your activity\", \"Popular\", \"Random pick\"", + "type": "string" + }, + "thumbnail": { + "type": "string" + } + } + }, "domain.League": { "type": "object", "properties": { @@ -5124,6 +5860,10 @@ "type": "boolean", "example": false }, + "is_featured": { + "type": "boolean", + "example": false + }, "name": { "type": "string", "example": "BPL" @@ -5134,6 +5874,36 @@ } } }, + "domain.LogEntry": { + "type": "object", + "properties": { + "caller": { + "type": "string" + }, + "env": { + "type": "string" + }, + "fields": { + "type": "object", + "additionalProperties": true + }, + "level": { + "type": "string" + }, + "message": { + "type": "string" + }, + "service": { + "type": "string" + }, + "stacktrace": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, "domain.Odd": { "type": "object", "properties": { @@ -5185,6 +5955,17 @@ } } }, + "domain.OtpProvider": { + "type": "string", + "enum": [ + "twilio", + "aformessage" + ], + "x-enum-varnames": [ + "TwilioSms", + "AfroMessage" + ] + }, "domain.OutcomeStatus": { "type": "integer", "enum": [ @@ -5227,11 +6008,13 @@ "domain.PaymentStatus": { "type": "string", "enum": [ + "success", "pending", "completed", "failed" ], "x-enum-varnames": [ + "PaymentStatusSuccessful", "PaymentStatusPending", "PaymentStatusCompleted", "PaymentStatusFailed" @@ -5265,6 +6048,29 @@ } } }, + "domain.PopOKGame": { + "type": "object", + "properties": { + "bets": { + "type": "array", + "items": { + "type": "number" + } + }, + "gameName": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "status": { + "type": "integer" + }, + "thumbnail": { + "type": "string" + } + } + }, "domain.RandomBetReq": { "type": "object", "required": [ @@ -5303,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": { @@ -5377,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": { @@ -5474,6 +6291,29 @@ } } }, + "domain.TicketRes": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "example": 100 + }, + "id": { + "type": "integer", + "example": 1 + }, + "outcomes": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.TicketOutcome" + } + }, + "total_odds": { + "type": "number", + "example": 4.22 + } + } + }, "domain.UpcomingEvent": { "type": "object", "properties": { @@ -5543,98 +6383,6 @@ } } }, - "domain.VeliCallback": { - "type": "object", - "properties": { - "amount": { - "description": "Transaction amount", - "type": "number" - }, - "currency": { - "description": "e.g., \"USD\"", - "type": "string" - }, - "event_type": { - "description": "\"bet_placed\", \"game_result\", etc.", - "type": "string" - }, - "game_id": { - "description": "e.g., \"veli_aviator_v1\"", - "type": "string" - }, - "multiplier": { - "description": "For games with multipliers (Aviator/Plinko)", - "type": "number" - }, - "round_id": { - "description": "Unique round identifier (replaces transaction_id)", - "type": "string" - }, - "session_id": { - "description": "Matches VirtualGameSession.SessionToken", - "type": "string" - }, - "signature": { - "description": "HMAC-SHA256", - "type": "string" - }, - "timestamp": { - "description": "Unix timestamp", - "type": "integer" - }, - "user_id": { - "description": "Veli's user identifier", - "type": "string" - } - } - }, - "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": { @@ -5986,52 +6734,6 @@ } } }, - "handlers.CreateTicketOutcomeReq": { - "type": "object", - "properties": { - "event_id": { - "description": "TicketID int64 `json:\"ticket_id\" example:\"1\"`", - "type": "integer", - "example": 1 - }, - "market_id": { - "type": "integer", - "example": 1 - }, - "odd_id": { - "type": "integer", - "example": 1 - } - } - }, - "handlers.CreateTicketReq": { - "type": "object", - "properties": { - "amount": { - "type": "number", - "example": 100 - }, - "outcomes": { - "type": "array", - "items": { - "$ref": "#/definitions/handlers.CreateTicketOutcomeReq" - } - } - } - }, - "handlers.CreateTicketRes": { - "type": "object", - "properties": { - "created_number": { - "type": "integer", - "example": 3 - }, - "fast_code": { - "type": "integer", - "example": 1234 - } - } - }, "handlers.CreateTransactionReq": { "type": "object", "properties": { @@ -6241,6 +6943,9 @@ }, "handlers.RegisterCodeReq": { "type": "object", + "required": [ + "provider" + ], "properties": { "email": { "type": "string", @@ -6249,11 +6954,22 @@ "phone_number": { "type": "string", "example": "1234567890" + }, + "provider": { + "allOf": [ + { + "$ref": "#/definitions/domain.OtpProvider" + } + ], + "example": "twilio" } } }, "handlers.RegisterUserReq": { "type": "object", + "required": [ + "provider" + ], "properties": { "email": { "type": "string", @@ -6279,6 +6995,14 @@ "type": "string", "example": "1234567890" }, + "provider": { + "allOf": [ + { + "$ref": "#/definitions/domain.OtpProvider" + } + ], + "example": "twilio" + }, "referal_code": { "type": "string", "example": "ABC123" @@ -6287,6 +7011,9 @@ }, "handlers.ResetCodeReq": { "type": "object", + "required": [ + "provider" + ], "properties": { "email": { "type": "string", @@ -6295,6 +7022,14 @@ "phone_number": { "type": "string", "example": "1234567890" + }, + "provider": { + "allOf": [ + { + "$ref": "#/definitions/domain.OtpProvider" + } + ], + "example": "twilio" } } }, @@ -6363,29 +7098,6 @@ } } }, - "handlers.TicketRes": { - "type": "object", - "properties": { - "amount": { - "type": "number", - "example": 100 - }, - "id": { - "type": "integer", - "example": 1 - }, - "outcomes": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.TicketOutcome" - } - }, - "total_odds": { - "type": "number", - "example": 4.22 - } - } - }, "handlers.TransactionRes": { "type": "object", "properties": { @@ -6488,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 df02b3c..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: @@ -211,6 +247,38 @@ definitions: - $ref: '#/definitions/domain.OutcomeStatus' example: 1 type: object + domain.CreateTicketOutcomeReq: + properties: + event_id: + description: TicketID int64 `json:"ticket_id" example:"1"` + example: 1 + type: integer + market_id: + example: 1 + type: integer + odd_id: + example: 1 + type: integer + type: object + domain.CreateTicketReq: + properties: + amount: + example: 100 + type: number + outcomes: + items: + $ref: '#/definitions/domain.CreateTicketOutcomeReq' + type: array + type: object + domain.CreateTicketRes: + properties: + created_number: + example: 3 + type: integer + fast_code: + example: 1234 + type: integer + type: object domain.DashboardSummary: properties: active_admins: @@ -323,6 +391,27 @@ definitions: - STATUS_SUSPENDED - STATUS_DECIDED_BY_FA - STATUS_REMOVED + domain.FavoriteGameRequest: + properties: + game_id: + type: integer + type: object + domain.GameRecommendation: + properties: + bets: + items: + type: number + type: array + game_id: + type: integer + game_name: + type: string + reason: + description: e.g., "Based on your activity", "Popular", "Random pick" + type: string + thumbnail: + type: string + type: object domain.League: properties: bet365_id: @@ -337,6 +426,9 @@ definitions: is_active: example: false type: boolean + is_featured: + example: false + type: boolean name: example: BPL type: string @@ -344,6 +436,26 @@ definitions: example: 1 type: integer type: object + domain.LogEntry: + properties: + caller: + type: string + env: + type: string + fields: + additionalProperties: true + type: object + level: + type: string + message: + type: string + service: + type: string + stacktrace: + type: string + timestamp: + type: string + type: object domain.Odd: properties: category: @@ -378,6 +490,14 @@ definitions: source: type: string type: object + domain.OtpProvider: + enum: + - twilio + - aformessage + type: string + x-enum-varnames: + - TwilioSms + - AfroMessage domain.OutcomeStatus: enum: - 0 @@ -412,11 +532,13 @@ definitions: - BANK domain.PaymentStatus: enum: + - success - pending - completed - failed type: string x-enum-varnames: + - PaymentStatusSuccessful - PaymentStatusPending - PaymentStatusCompleted - PaymentStatusFailed @@ -439,6 +561,21 @@ definitions: description: BET, WIN, REFUND, JACKPOT_WIN type: string type: object + domain.PopOKGame: + properties: + bets: + items: + type: number + type: array + gameName: + type: string + id: + type: integer + status: + type: integer + thumbnail: + type: string + type: object domain.RandomBetReq: properties: branch_id: @@ -465,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: @@ -513,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: {} @@ -583,6 +728,22 @@ definitions: example: 1 type: integer type: object + domain.TicketRes: + properties: + amount: + example: 100 + type: number + id: + example: 1 + type: integer + outcomes: + items: + $ref: '#/definitions/domain.TicketOutcome' + type: array + total_odds: + example: 4.22 + type: number + type: object domain.UpcomingEvent: properties: away_kit_image: @@ -632,70 +793,6 @@ definitions: - $ref: '#/definitions/domain.EventStatus' description: Match Status for event type: object - domain.VeliCallback: - properties: - amount: - description: Transaction amount - type: number - currency: - description: e.g., "USD" - type: string - event_type: - description: '"bet_placed", "game_result", etc.' - type: string - game_id: - description: e.g., "veli_aviator_v1" - type: string - multiplier: - description: For games with multipliers (Aviator/Plinko) - type: number - round_id: - description: Unique round identifier (replaces transaction_id) - type: string - session_id: - description: Matches VirtualGameSession.SessionToken - type: string - signature: - description: HMAC-SHA256 - type: string - timestamp: - description: Unix timestamp - type: integer - user_id: - description: Veli's user identifier - type: string - 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: @@ -946,38 +1043,6 @@ definitions: example: SportsBook type: string type: object - handlers.CreateTicketOutcomeReq: - properties: - event_id: - description: TicketID int64 `json:"ticket_id" example:"1"` - example: 1 - type: integer - market_id: - example: 1 - type: integer - odd_id: - example: 1 - type: integer - type: object - handlers.CreateTicketReq: - properties: - amount: - example: 100 - type: number - outcomes: - items: - $ref: '#/definitions/handlers.CreateTicketOutcomeReq' - type: array - type: object - handlers.CreateTicketRes: - properties: - created_number: - example: 3 - type: integer - fast_code: - example: 1234 - type: integer - type: object handlers.CreateTransactionReq: properties: account_name: @@ -1126,6 +1191,12 @@ definitions: phone_number: example: "1234567890" type: string + provider: + allOf: + - $ref: '#/definitions/domain.OtpProvider' + example: twilio + required: + - provider type: object handlers.RegisterUserReq: properties: @@ -1147,9 +1218,15 @@ definitions: phone_number: example: "1234567890" type: string + provider: + allOf: + - $ref: '#/definitions/domain.OtpProvider' + example: twilio referal_code: example: ABC123 type: string + required: + - provider type: object handlers.ResetCodeReq: properties: @@ -1159,6 +1236,12 @@ definitions: phone_number: example: "1234567890" type: string + provider: + allOf: + - $ref: '#/definitions/domain.OtpProvider' + example: twilio + required: + - provider type: object handlers.ResetPasswordReq: properties: @@ -1205,22 +1288,6 @@ definitions: example: SportsBook type: string type: object - handlers.TicketRes: - properties: - amount: - example: 100 - type: number - id: - example: 1 - type: integer - outcomes: - items: - $ref: '#/definitions/domain.TicketOutcome' - type: array - total_odds: - example: 4.22 - type: number - type: object handlers.TransactionRes: properties: account_name: @@ -1293,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: @@ -1754,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: @@ -1964,6 +2162,229 @@ 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 + produces: + - application/json + responses: + "200": + description: List of application logs + schema: + items: + $ref: '#/definitions/domain.LogEntry' + type: array + "500": + description: Internal server error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Retrieve latest application logs + tags: + - Logs + /api/v1/report-files/download/{filename}: + get: + description: Downloads a generated report CSV file from the server + parameters: + - description: Name of the report file to download (e.g., report_daily_2025-06-21.csv) + in: path + name: filename + required: true + type: string + produces: + - text/csv + responses: + "200": + description: CSV file will be downloaded + schema: + type: file + "400": + description: Missing or invalid filename + schema: + $ref: '#/definitions/domain.ErrorResponse' + "404": + description: Report file not found + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal server error while serving the file + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Download a CSV report file + tags: + - Reports + /api/v1/report-files/list: + get: + description: Returns a list of all generated report CSV files available for + download + produces: + - application/json + responses: + "200": + description: List of CSV report filenames + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + items: + type: string + type: array + type: object + "500": + description: Failed to read report directory + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: List available report CSV files + tags: + - Reports /api/v1/reports/dashboard: get: consumes: @@ -2022,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: @@ -2077,52 +2548,6 @@ paths: summary: Process Alea Play game callback tags: - Alea Virtual Games - /api/veli/launch/{game_id}: - get: - description: Generates authenticated launch URL for Veli games - parameters: - - description: Game ID (e.g., veli_aviator_v1) - in: path - name: game_id - required: true - type: string - - default: USD - description: Currency code - in: query - name: currency - type: string - - default: real - description: Game mode - enum: - - real - - demo - in: query - name: mode - type: string - responses: - "200": - description: Returns launch URL - schema: - additionalProperties: - type: string - type: object - "400": - description: Invalid request - schema: - additionalProperties: - type: string - type: object - "500": - description: Internal server error - schema: - additionalProperties: - type: string - type: object - security: - - BearerAuth: [] - summary: Launch a Veli game - tags: - - Veli Games /auth/login: post: consumes: @@ -2225,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: @@ -3494,36 +3745,58 @@ paths: summary: Create a operation tags: - branch - /random/bet: - post: + /popok/games: + get: consumes: - application/json - description: Generate a random bet + description: Retrieves the list of available PopOK slot games parameters: - - description: Create Random bet - in: body - name: createBet - required: true - schema: - $ref: '#/definitions/domain.RandomBetReq' + - default: USD + description: Currency (e.g. USD, ETB) + in: query + name: currency + type: string produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/domain.BetRes' - "400": - description: Bad Request + items: + $ref: '#/definitions/domain.PopOKGame' + type: array + "502": + description: Bad Gateway schema: - $ref: '#/definitions/response.APIResponse' + $ref: '#/definitions/domain.ErrorResponse' + summary: Get PopOK Games List + tags: + - Virtual Games - PopOK + /popok/games/recommend: + get: + description: Recommends games based on user history or randomly + parameters: + - description: User ID + in: query + name: user_id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.GameRecommendation' + type: array "500": description: Internal Server Error schema: - $ref: '#/definitions/response.APIResponse' - summary: Generate a random bet + $ref: '#/definitions/domain.ErrorResponse' + summary: Recommend virtual games tags: - - bet + - Virtual Games - PopOK /referral/settings: get: consumes: @@ -3700,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: @@ -3766,7 +4243,7 @@ paths: description: OK schema: items: - $ref: '#/definitions/handlers.TicketRes' + $ref: '#/definitions/domain.TicketRes' type: array "400": description: Bad Request @@ -3789,14 +4266,14 @@ paths: name: createTicket required: true schema: - $ref: '#/definitions/handlers.CreateTicketReq' + $ref: '#/definitions/domain.CreateTicketReq' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/handlers.CreateTicketRes' + $ref: '#/definitions/domain.CreateTicketRes' "400": description: Bad Request schema: @@ -3825,7 +4302,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/handlers.TicketRes' + $ref: '#/definitions/domain.TicketRes' "400": description: Bad Request schema: @@ -3837,6 +4314,27 @@ paths: summary: Get ticket by ID tags: - ticket + /top-leagues: + get: + consumes: + - application/json + description: Retrieve all top leagues + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.UpcomingEvent' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Retrieve all top leagues + tags: + - prematch /transaction: get: consumes: @@ -4415,18 +4913,18 @@ 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-game + - Virtual Games - PopOK /virtual-game/launch: post: consumes: @@ -4449,20 +4947,20 @@ 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 tags: - - virtual-game + - Virtual Games - PopOK /wallet: get: consumes: @@ -4551,48 +5049,6 @@ paths: summary: Activate and Deactivate Wallet tags: - wallet - /webhooks/veli: - post: - consumes: - - application/json - description: Processes game round settlements from Veli - parameters: - - description: Callback payload - in: body - name: payload - required: true - schema: - $ref: '#/definitions/domain.VeliCallback' - produces: - - application/json - responses: - "200": - description: Callback processed - schema: - additionalProperties: - type: string - type: object - "400": - description: Invalid payload - schema: - additionalProperties: - type: string - type: object - "403": - description: Invalid signature - schema: - additionalProperties: - type: string - type: object - "500": - description: Processing error - schema: - additionalProperties: - type: string - type: object - summary: Veli Games webhook handler - tags: - - Veli Games securityDefinitions: Bearer: in: header diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index e4cde1d..c5da84e 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -22,23 +22,25 @@ INSERT INTO bets ( user_id, is_shop_bet, cashout_id, - company_id + company_id, + outcomes_hash ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) -RETURNING id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) +RETURNING id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash ` type CreateBetParams struct { - Amount int64 `json:"amount"` - TotalOdds float32 `json:"total_odds"` - Status int32 `json:"status"` - FullName string `json:"full_name"` - PhoneNumber string `json:"phone_number"` - BranchID pgtype.Int8 `json:"branch_id"` - UserID pgtype.Int8 `json:"user_id"` - IsShopBet bool `json:"is_shop_bet"` - CashoutID string `json:"cashout_id"` - CompanyID pgtype.Int8 `json:"company_id"` + Amount int64 `json:"amount"` + TotalOdds float32 `json:"total_odds"` + Status int32 `json:"status"` + FullName string `json:"full_name"` + PhoneNumber string `json:"phone_number"` + BranchID pgtype.Int8 `json:"branch_id"` + UserID pgtype.Int8 `json:"user_id"` + IsShopBet bool `json:"is_shop_bet"` + CashoutID string `json:"cashout_id"` + CompanyID pgtype.Int8 `json:"company_id"` + OutcomesHash string `json:"outcomes_hash"` } func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, error) { @@ -53,6 +55,7 @@ func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, erro arg.IsShopBet, arg.CashoutID, arg.CompanyID, + arg.OutcomesHash, ) var i Bet err := row.Scan( @@ -70,6 +73,7 @@ func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, erro &i.CreatedAt, &i.UpdatedAt, &i.IsShopBet, + &i.OutcomesHash, ) return i, err } @@ -111,7 +115,7 @@ func (q *Queries) DeleteBetOutcome(ctx context.Context, betID int64) error { } const GetAllBets = `-- name: GetAllBets :many -SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes +SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, outcomes FROM bet_with_outcomes wHERE ( branch_id = $1 @@ -125,16 +129,45 @@ wHERE ( user_id = $3 OR $3 IS NULL ) + AND ( + is_shop_bet = $4 + OR $4 IS NULL + ) + AND ( + full_name ILIKE '%' || $5 || '%' + OR phone_number ILIKE '%' || $5 || '%' + OR $5 IS NULL + ) + AND ( + created_at > $6 + OR $6 IS NULL + ) + AND ( + created_at < $7 + OR $7 IS NULL + ) ` type GetAllBetsParams struct { - BranchID pgtype.Int8 `json:"branch_id"` - CompanyID pgtype.Int8 `json:"company_id"` - UserID pgtype.Int8 `json:"user_id"` + BranchID pgtype.Int8 `json:"branch_id"` + CompanyID pgtype.Int8 `json:"company_id"` + UserID pgtype.Int8 `json:"user_id"` + IsShopBet pgtype.Bool `json:"is_shop_bet"` + Query pgtype.Text `json:"query"` + CreatedBefore pgtype.Timestamp `json:"created_before"` + CreatedAfter pgtype.Timestamp `json:"created_after"` } func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWithOutcome, error) { - rows, err := q.db.Query(ctx, GetAllBets, arg.BranchID, arg.CompanyID, arg.UserID) + rows, err := q.db.Query(ctx, GetAllBets, + arg.BranchID, + arg.CompanyID, + arg.UserID, + arg.IsShopBet, + arg.Query, + arg.CreatedBefore, + arg.CreatedAfter, + ) if err != nil { return nil, err } @@ -157,6 +190,7 @@ func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWi &i.CreatedAt, &i.UpdatedAt, &i.IsShopBet, + &i.OutcomesHash, &i.Outcomes, ); err != nil { return nil, err @@ -170,7 +204,7 @@ func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWi } const GetBetByBranchID = `-- name: GetBetByBranchID :many -SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes +SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, outcomes FROM bet_with_outcomes WHERE branch_id = $1 ` @@ -199,6 +233,7 @@ func (q *Queries) GetBetByBranchID(ctx context.Context, branchID pgtype.Int8) ([ &i.CreatedAt, &i.UpdatedAt, &i.IsShopBet, + &i.OutcomesHash, &i.Outcomes, ); err != nil { return nil, err @@ -212,7 +247,7 @@ func (q *Queries) GetBetByBranchID(ctx context.Context, branchID pgtype.Int8) ([ } const GetBetByCashoutID = `-- name: GetBetByCashoutID :one -SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes +SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, outcomes FROM bet_with_outcomes WHERE cashout_id = $1 ` @@ -235,13 +270,14 @@ func (q *Queries) GetBetByCashoutID(ctx context.Context, cashoutID string) (BetW &i.CreatedAt, &i.UpdatedAt, &i.IsShopBet, + &i.OutcomesHash, &i.Outcomes, ) return i, err } const GetBetByID = `-- name: GetBetByID :one -SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes +SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, outcomes FROM bet_with_outcomes WHERE id = $1 ` @@ -264,13 +300,14 @@ func (q *Queries) GetBetByID(ctx context.Context, id int64) (BetWithOutcome, err &i.CreatedAt, &i.UpdatedAt, &i.IsShopBet, + &i.OutcomesHash, &i.Outcomes, ) return i, err } const GetBetByUserID = `-- name: GetBetByUserID :many -SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes +SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, outcomes FROM bet_with_outcomes WHERE user_id = $1 ` @@ -299,6 +336,7 @@ func (q *Queries) GetBetByUserID(ctx context.Context, userID pgtype.Int8) ([]Bet &i.CreatedAt, &i.UpdatedAt, &i.IsShopBet, + &i.OutcomesHash, &i.Outcomes, ); err != nil { return nil, err @@ -311,6 +349,25 @@ func (q *Queries) GetBetByUserID(ctx context.Context, userID pgtype.Int8) ([]Bet return items, nil } +const GetBetCount = `-- name: GetBetCount :one +SELECT COUNT(*) +FROM bets +where user_id = $1 + AND outcomes_hash = $2 +` + +type GetBetCountParams struct { + UserID pgtype.Int8 `json:"user_id"` + OutcomesHash string `json:"outcomes_hash"` +} + +func (q *Queries) GetBetCount(ctx context.Context, arg GetBetCountParams) (int64, error) { + row := q.db.QueryRow(ctx, GetBetCount, arg.UserID, arg.OutcomesHash) + var count int64 + err := row.Scan(&count) + return count, err +} + const GetBetOutcomeByBetID = `-- name: GetBetOutcomeByBetID :many SELECT id, bet_id, sport_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, status, expires FROM bet_outcomes @@ -356,11 +413,23 @@ func (q *Queries) GetBetOutcomeByBetID(ctx context.Context, betID int64) ([]BetO const GetBetOutcomeByEventID = `-- name: GetBetOutcomeByEventID :many SELECT id, bet_id, sport_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, status, expires FROM bet_outcomes -WHERE event_id = $1 +WHERE (event_id = $1) + AND ( + status = $2 + OR $2 IS NULL + OR status = $3 + OR $3 IS NULL + ) ` -func (q *Queries) GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]BetOutcome, error) { - rows, err := q.db.Query(ctx, GetBetOutcomeByEventID, eventID) +type GetBetOutcomeByEventIDParams struct { + EventID int64 `json:"event_id"` + FilterStatus pgtype.Int4 `json:"filter_status"` + FilterStatus2 pgtype.Int4 `json:"filter_status_2"` +} + +func (q *Queries) GetBetOutcomeByEventID(ctx context.Context, arg GetBetOutcomeByEventIDParams) ([]BetOutcome, error) { + rows, err := q.db.Query(ctx, GetBetOutcomeByEventID, arg.EventID, arg.FilterStatus, arg.FilterStatus2) if err != nil { return nil, err } @@ -430,6 +499,89 @@ func (q *Queries) UpdateBetOutcomeStatus(ctx context.Context, arg UpdateBetOutco return i, err } +const UpdateBetOutcomeStatusByBetID = `-- name: UpdateBetOutcomeStatusByBetID :one +UPDATE bet_outcomes +SET status = $1 +WHERE bet_id = $2 +RETURNING id, bet_id, sport_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, status, expires +` + +type UpdateBetOutcomeStatusByBetIDParams struct { + Status int32 `json:"status"` + BetID int64 `json:"bet_id"` +} + +func (q *Queries) UpdateBetOutcomeStatusByBetID(ctx context.Context, arg UpdateBetOutcomeStatusByBetIDParams) (BetOutcome, error) { + row := q.db.QueryRow(ctx, UpdateBetOutcomeStatusByBetID, arg.Status, arg.BetID) + var i BetOutcome + err := row.Scan( + &i.ID, + &i.BetID, + &i.SportID, + &i.EventID, + &i.OddID, + &i.HomeTeamName, + &i.AwayTeamName, + &i.MarketID, + &i.MarketName, + &i.Odd, + &i.OddName, + &i.OddHeader, + &i.OddHandicap, + &i.Status, + &i.Expires, + ) + return i, err +} + +const UpdateBetOutcomeStatusForEvent = `-- name: UpdateBetOutcomeStatusForEvent :many +UPDATE bet_outcomes +SEt status = $1 +WHERE event_id = $2 +RETURNING id, bet_id, sport_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, status, expires +` + +type UpdateBetOutcomeStatusForEventParams struct { + Status int32 `json:"status"` + EventID int64 `json:"event_id"` +} + +func (q *Queries) UpdateBetOutcomeStatusForEvent(ctx context.Context, arg UpdateBetOutcomeStatusForEventParams) ([]BetOutcome, error) { + rows, err := q.db.Query(ctx, UpdateBetOutcomeStatusForEvent, arg.Status, arg.EventID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []BetOutcome + for rows.Next() { + var i BetOutcome + if err := rows.Scan( + &i.ID, + &i.BetID, + &i.SportID, + &i.EventID, + &i.OddID, + &i.HomeTeamName, + &i.AwayTeamName, + &i.MarketID, + &i.MarketName, + &i.Odd, + &i.OddName, + &i.OddHeader, + &i.OddHandicap, + &i.Status, + &i.Expires, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const UpdateCashOut = `-- name: UpdateCashOut :exec UPDATE bets SET cashed_out = $2, diff --git a/gen/db/branch.sql.go b/gen/db/branch.sql.go index 92e7f80..71e5257 100644 --- a/gen/db/branch.sql.go +++ b/gen/db/branch.sql.go @@ -155,12 +155,53 @@ func (q *Queries) DeleteBranchOperation(ctx context.Context, arg DeleteBranchOpe } const GetAllBranches = `-- name: GetAllBranches :many -SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance +SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active FROM branch_details +WHERE ( + company_id = $1 + OR $1 IS NULL + ) + AND ( + is_active = $2 + OR $2 IS NULL + ) + AND ( + branch_manager_id = $3 + OR $3 IS NULL + ) + AND ( + name ILIKE '%' || $4 || '%' + OR location ILIKE '%' || $4 || '%' + OR $4 IS NULL + ) + AND ( + created_at > $5 + OR $5 IS NULL + ) + AND ( + created_at < $6 + OR $6 IS NULL + ) ` -func (q *Queries) GetAllBranches(ctx context.Context) ([]BranchDetail, error) { - rows, err := q.db.Query(ctx, GetAllBranches) +type GetAllBranchesParams struct { + CompanyID pgtype.Int8 `json:"company_id"` + IsActive pgtype.Bool `json:"is_active"` + BranchManagerID pgtype.Int8 `json:"branch_manager_id"` + Query pgtype.Text `json:"query"` + CreatedBefore pgtype.Timestamp `json:"created_before"` + CreatedAfter pgtype.Timestamp `json:"created_after"` +} + +func (q *Queries) GetAllBranches(ctx context.Context, arg GetAllBranchesParams) ([]BranchDetail, error) { + rows, err := q.db.Query(ctx, GetAllBranches, + arg.CompanyID, + arg.IsActive, + arg.BranchManagerID, + arg.Query, + arg.CreatedBefore, + arg.CreatedAfter, + ) if err != nil { return nil, err } @@ -182,6 +223,7 @@ func (q *Queries) GetAllBranches(ctx context.Context) ([]BranchDetail, error) { &i.ManagerName, &i.ManagerPhoneNumber, &i.Balance, + &i.WalletIsActive, ); err != nil { return nil, err } @@ -244,7 +286,7 @@ func (q *Queries) GetBranchByCashier(ctx context.Context, userID int64) (Branch, } const GetBranchByCompanyID = `-- name: GetBranchByCompanyID :many -SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance +SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active FROM branch_details WHERE company_id = $1 ` @@ -272,6 +314,7 @@ func (q *Queries) GetBranchByCompanyID(ctx context.Context, companyID int64) ([] &i.ManagerName, &i.ManagerPhoneNumber, &i.Balance, + &i.WalletIsActive, ); err != nil { return nil, err } @@ -284,7 +327,7 @@ func (q *Queries) GetBranchByCompanyID(ctx context.Context, companyID int64) ([] } const GetBranchByID = `-- name: GetBranchByID :one -SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance +SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active FROM branch_details WHERE id = $1 ` @@ -306,12 +349,13 @@ func (q *Queries) GetBranchByID(ctx context.Context, id int64) (BranchDetail, er &i.ManagerName, &i.ManagerPhoneNumber, &i.Balance, + &i.WalletIsActive, ) return i, err } const GetBranchByManagerID = `-- name: GetBranchByManagerID :many -SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance +SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active FROM branch_details WHERE branch_manager_id = $1 ` @@ -339,6 +383,7 @@ func (q *Queries) GetBranchByManagerID(ctx context.Context, branchManagerID int6 &i.ManagerName, &i.ManagerPhoneNumber, &i.Balance, + &i.WalletIsActive, ); err != nil { return nil, err } @@ -398,7 +443,7 @@ func (q *Queries) GetBranchOperations(ctx context.Context, branchID int64) ([]Ge } const SearchBranchByName = `-- name: SearchBranchByName :many -SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance +SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active FROM branch_details WHERE name ILIKE '%' || $1 || '%' ` @@ -426,6 +471,7 @@ func (q *Queries) SearchBranchByName(ctx context.Context, dollar_1 pgtype.Text) &i.ManagerName, &i.ManagerPhoneNumber, &i.Balance, + &i.WalletIsActive, ); err != nil { return nil, err } @@ -443,7 +489,8 @@ SET name = COALESCE($2, name), location = COALESCE($3, location), branch_manager_id = COALESCE($4, branch_manager_id), company_id = COALESCE($5, company_id), - is_self_owned = COALESCE($6, is_self_owned) + is_self_owned = COALESCE($6, is_self_owned), + is_active = COALESCE($7, is_active) WHERE id = $1 RETURNING id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at ` @@ -455,6 +502,7 @@ type UpdateBranchParams struct { BranchManagerID pgtype.Int8 `json:"branch_manager_id"` CompanyID pgtype.Int8 `json:"company_id"` IsSelfOwned pgtype.Bool `json:"is_self_owned"` + IsActive pgtype.Bool `json:"is_active"` } func (q *Queries) UpdateBranch(ctx context.Context, arg UpdateBranchParams) (Branch, error) { @@ -465,6 +513,7 @@ func (q *Queries) UpdateBranch(ctx context.Context, arg UpdateBranchParams) (Bra arg.BranchManagerID, arg.CompanyID, arg.IsSelfOwned, + arg.IsActive, ) var i Branch err := row.Scan( 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/leagues.sql.go b/gen/db/leagues.sql.go index 9db2644..fa5da4c 100644 --- a/gen/db/leagues.sql.go +++ b/gen/db/leagues.sql.go @@ -33,6 +33,7 @@ SELECT id, country_code, bet365_id, is_active, + is_featured, sport_id FROM leagues WHERE ( @@ -47,13 +48,18 @@ WHERE ( is_active = $3 OR $3 IS NULL ) -LIMIT $5 OFFSET $4 + AND ( + is_featured = $4 + OR $4 IS NULL + ) +LIMIT $6 OFFSET $5 ` type GetAllLeaguesParams struct { CountryCode pgtype.Text `json:"country_code"` SportID pgtype.Int4 `json:"sport_id"` IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` Offset pgtype.Int4 `json:"offset"` Limit pgtype.Int4 `json:"limit"` } @@ -64,6 +70,7 @@ type GetAllLeaguesRow struct { CountryCode pgtype.Text `json:"country_code"` Bet365ID pgtype.Int4 `json:"bet365_id"` IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` SportID int32 `json:"sport_id"` } @@ -72,6 +79,7 @@ func (q *Queries) GetAllLeagues(ctx context.Context, arg GetAllLeaguesParams) ([ arg.CountryCode, arg.SportID, arg.IsActive, + arg.IsFeatured, arg.Offset, arg.Limit, ) @@ -88,6 +96,57 @@ func (q *Queries) GetAllLeagues(ctx context.Context, arg GetAllLeaguesParams) ([ &i.CountryCode, &i.Bet365ID, &i.IsActive, + &i.IsFeatured, + &i.SportID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetFeaturedLeagues = `-- name: GetFeaturedLeagues :many +SELECT id, + name, + country_code, + bet365_id, + is_active, + is_featured, + sport_id +FROM leagues +WHERE is_featured = true +` + +type GetFeaturedLeaguesRow struct { + ID int64 `json:"id"` + Name string `json:"name"` + CountryCode pgtype.Text `json:"country_code"` + Bet365ID pgtype.Int4 `json:"bet365_id"` + IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` + SportID int32 `json:"sport_id"` +} + +func (q *Queries) GetFeaturedLeagues(ctx context.Context) ([]GetFeaturedLeaguesRow, error) { + rows, err := q.db.Query(ctx, GetFeaturedLeagues) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetFeaturedLeaguesRow + for rows.Next() { + var i GetFeaturedLeaguesRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.CountryCode, + &i.Bet365ID, + &i.IsActive, + &i.IsFeatured, &i.SportID, ); err != nil { return nil, err @@ -107,14 +166,16 @@ INSERT INTO leagues ( country_code, bet365_id, sport_id, - is_active + is_active, + is_featured ) -VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO +VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, country_code = EXCLUDED.country_code, bet365_id = EXCLUDED.bet365_id, is_active = EXCLUDED.is_active, + is_featured = EXCLUDED.is_featured, sport_id = EXCLUDED.sport_id ` @@ -125,6 +186,7 @@ type InsertLeagueParams struct { Bet365ID pgtype.Int4 `json:"bet365_id"` SportID int32 `json:"sport_id"` IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` } func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) error { @@ -135,6 +197,7 @@ func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) erro arg.Bet365ID, arg.SportID, arg.IsActive, + arg.IsFeatured, ) return err } @@ -161,7 +224,8 @@ SET name = COALESCE($2, name), country_code = COALESCE($3, country_code), bet365_id = COALESCE($4, bet365_id), is_active = COALESCE($5, is_active), - sport_id = COALESCE($6, sport_id) + is_featured = COALESCE($6, is_featured), + sport_id = COALESCE($7, sport_id) WHERE id = $1 ` @@ -171,6 +235,7 @@ type UpdateLeagueParams struct { CountryCode pgtype.Text `json:"country_code"` Bet365ID pgtype.Int4 `json:"bet365_id"` IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` SportID pgtype.Int4 `json:"sport_id"` } @@ -181,6 +246,7 @@ func (q *Queries) UpdateLeague(ctx context.Context, arg UpdateLeagueParams) erro arg.CountryCode, arg.Bet365ID, arg.IsActive, + arg.IsFeatured, arg.SportID, ) return err @@ -192,7 +258,8 @@ SET name = COALESCE($2, name), id = COALESCE($3, id), country_code = COALESCE($4, country_code), is_active = COALESCE($5, is_active), - sport_id = COALESCE($6, sport_id) + is_featured = COALESCE($6, is_featured), + sport_id = COALESCE($7, sport_id) WHERE bet365_id = $1 ` @@ -202,6 +269,7 @@ type UpdateLeagueByBet365IDParams struct { ID pgtype.Int8 `json:"id"` CountryCode pgtype.Text `json:"country_code"` IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` SportID pgtype.Int4 `json:"sport_id"` } @@ -212,6 +280,7 @@ func (q *Queries) UpdateLeagueByBet365ID(ctx context.Context, arg UpdateLeagueBy arg.ID, arg.CountryCode, arg.IsActive, + arg.IsFeatured, arg.SportID, ) return err diff --git a/gen/db/models.go b/gen/db/models.go index 1da22f3..3ba6c5e 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -55,21 +55,40 @@ 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"` - TotalOdds float32 `json:"total_odds"` - Status int32 `json:"status"` - FullName string `json:"full_name"` - PhoneNumber string `json:"phone_number"` - CompanyID pgtype.Int8 `json:"company_id"` - BranchID pgtype.Int8 `json:"branch_id"` - UserID pgtype.Int8 `json:"user_id"` - CashedOut bool `json:"cashed_out"` - CashoutID string `json:"cashout_id"` - CreatedAt pgtype.Timestamp `json:"created_at"` - UpdatedAt pgtype.Timestamp `json:"updated_at"` - IsShopBet bool `json:"is_shop_bet"` + ID int64 `json:"id"` + Amount int64 `json:"amount"` + TotalOdds float32 `json:"total_odds"` + Status int32 `json:"status"` + FullName string `json:"full_name"` + PhoneNumber string `json:"phone_number"` + CompanyID pgtype.Int8 `json:"company_id"` + BranchID pgtype.Int8 `json:"branch_id"` + UserID pgtype.Int8 `json:"user_id"` + CashedOut bool `json:"cashed_out"` + CashoutID string `json:"cashout_id"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` + IsShopBet bool `json:"is_shop_bet"` + OutcomesHash string `json:"outcomes_hash"` } type BetOutcome struct { @@ -91,21 +110,22 @@ type BetOutcome struct { } type BetWithOutcome struct { - ID int64 `json:"id"` - Amount int64 `json:"amount"` - TotalOdds float32 `json:"total_odds"` - Status int32 `json:"status"` - FullName string `json:"full_name"` - PhoneNumber string `json:"phone_number"` - CompanyID pgtype.Int8 `json:"company_id"` - BranchID pgtype.Int8 `json:"branch_id"` - UserID pgtype.Int8 `json:"user_id"` - CashedOut bool `json:"cashed_out"` - CashoutID string `json:"cashout_id"` - CreatedAt pgtype.Timestamp `json:"created_at"` - UpdatedAt pgtype.Timestamp `json:"updated_at"` - IsShopBet bool `json:"is_shop_bet"` - Outcomes []BetOutcome `json:"outcomes"` + ID int64 `json:"id"` + Amount int64 `json:"amount"` + TotalOdds float32 `json:"total_odds"` + Status int32 `json:"status"` + FullName string `json:"full_name"` + PhoneNumber string `json:"phone_number"` + CompanyID pgtype.Int8 `json:"company_id"` + BranchID pgtype.Int8 `json:"branch_id"` + UserID pgtype.Int8 `json:"user_id"` + CashedOut bool `json:"cashed_out"` + CashoutID string `json:"cashout_id"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` + IsShopBet bool `json:"is_shop_bet"` + OutcomesHash string `json:"outcomes_hash"` + Outcomes []BetOutcome `json:"outcomes"` } type Branch struct { @@ -141,6 +161,7 @@ type BranchDetail struct { ManagerName interface{} `json:"manager_name"` ManagerPhoneNumber pgtype.Text `json:"manager_phone_number"` Balance pgtype.Int8 `json:"balance"` + WalletIsActive pgtype.Bool `json:"wallet_is_active"` } type BranchOperation struct { @@ -179,6 +200,23 @@ type CustomerWallet struct { UpdatedAt pgtype.Timestamp `json:"updated_at"` } +type CustomerWalletDetail struct { + ID int64 `json:"id"` + CustomerID int64 `json:"customer_id"` + RegularID int64 `json:"regular_id"` + RegularBalance int64 `json:"regular_balance"` + StaticID int64 `json:"static_id"` + StaticBalance int64 `json:"static_balance"` + RegularIsActive bool `json:"regular_is_active"` + StaticIsActive bool `json:"static_is_active"` + RegularUpdatedAt pgtype.Timestamp `json:"regular_updated_at"` + StaticUpdatedAt pgtype.Timestamp `json:"static_updated_at"` + CreatedAt pgtype.Timestamp `json:"created_at"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + PhoneNumber pgtype.Text `json:"phone_number"` +} + type Event struct { ID string `json:"id"` SportID pgtype.Int4 `json:"sport_id"` @@ -204,13 +242,31 @@ type Event struct { Source pgtype.Text `json:"source"` } +type ExchangeRate struct { + ID int32 `json:"id"` + FromCurrency string `json:"from_currency"` + ToCurrency string `json:"to_currency"` + Rate pgtype.Numeric `json:"rate"` + ValidUntil pgtype.Timestamp `json:"valid_until"` + 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"` + Img pgtype.Text `json:"img"` CountryCode pgtype.Text `json:"country_code"` Bet365ID pgtype.Int4 `json:"bet365_id"` SportID int32 `json:"sport_id"` IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` } type Notification struct { @@ -296,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"` @@ -311,6 +379,13 @@ type Result struct { UpdatedAt pgtype.Timestamp `json:"updated_at"` } +type Setting struct { + Key string `json:"key"` + Value string `json:"value"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` +} + type SupportedOperation struct { ID int64 `json:"id"` Name string `json:"name"` @@ -434,6 +509,24 @@ type VirtualGame struct { UpdatedAt pgtype.Timestamptz `json:"updated_at"` } +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"` + Amount int64 `json:"amount"` + Currency string `json:"currency"` + ExternalTransactionID string `json:"external_transaction_id"` + ReferenceTransactionID pgtype.Text `json:"reference_transaction_id"` + Status string `json:"status"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` +} + type VirtualGameSession struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` @@ -450,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"` @@ -470,6 +566,7 @@ type Wallet struct { IsActive bool `json:"is_active"` CreatedAt pgtype.Timestamp `json:"created_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"` + Currency string `json:"currency"` BonusBalance pgtype.Numeric `json:"bonus_balance"` CashBalance pgtype.Numeric `json:"cash_balance"` } @@ -488,7 +585,7 @@ type WalletTransfer struct { SenderWalletID pgtype.Int8 `json:"sender_wallet_id"` CashierID pgtype.Int8 `json:"cashier_id"` Verified pgtype.Bool `json:"verified"` - ReferenceNumber pgtype.Text `json:"reference_number"` + ReferenceNumber string `json:"reference_number"` Status pgtype.Text `json:"status"` PaymentMethod pgtype.Text `json:"payment_method"` CreatedAt pgtype.Timestamp `json:"created_at"` diff --git a/gen/db/report.sql.go b/gen/db/report.sql.go new file mode 100644 index 0000000..7040673 --- /dev/null +++ b/gen/db/report.sql.go @@ -0,0 +1,199 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: report.sql + +package dbgen + +import ( + "context" + + "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 +` + +type GetTotalBetsMadeInRangeParams struct { + 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) + var total_bets int64 + err := row.Scan(&total_bets) + return total_bets, err +} + +const GetTotalCashBacksInRange = `-- name: GetTotalCashBacksInRange :one +SELECT COALESCE(SUM(amount), 0) AS total_cash_backs +FROM bets +WHERE created_at BETWEEN $1 AND $2 + AND status = 5 +` + +type GetTotalCashBacksInRangeParams struct { + 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) + var total_cash_backs interface{} + err := row.Scan(&total_cash_backs) + return total_cash_backs, err +} + +const GetTotalCashMadeInRange = `-- name: GetTotalCashMadeInRange :one +SELECT COALESCE(SUM(amount), 0) AS total_cash_made +FROM bets +WHERE created_at BETWEEN $1 AND $2 +` + +type GetTotalCashMadeInRangeParams struct { + 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) + var total_cash_made interface{} + err := row.Scan(&total_cash_made) + return total_cash_made, err +} + +const GetTotalCashOutInRange = `-- name: GetTotalCashOutInRange :one +SELECT COALESCE(SUM(amount), 0) AS total_cash_out +FROM bets +WHERE created_at BETWEEN $1 AND $2 + AND cashed_out = true +` + +type GetTotalCashOutInRangeParams struct { + 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) + var total_cash_out interface{} + err := row.Scan(&total_cash_out) + return total_cash_out, err +} diff --git a/gen/db/settings.sql.go b/gen/db/settings.sql.go new file mode 100644 index 0000000..d842661 --- /dev/null +++ b/gen/db/settings.sql.go @@ -0,0 +1,83 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: settings.sql + +package dbgen + +import ( + "context" +) + +const GetSetting = `-- name: GetSetting :one +SELECT key, value, created_at, updated_at +FROM settings +WHERE key = $1 +` + +func (q *Queries) GetSetting(ctx context.Context, key string) (Setting, error) { + row := q.db.QueryRow(ctx, GetSetting, key) + var i Setting + err := row.Scan( + &i.Key, + &i.Value, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const GetSettings = `-- name: GetSettings :many +SELECT key, value, created_at, updated_at +FROM settings +` + +func (q *Queries) GetSettings(ctx context.Context) ([]Setting, error) { + rows, err := q.db.Query(ctx, GetSettings) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Setting + for rows.Next() { + var i Setting + if err := rows.Scan( + &i.Key, + &i.Value, + &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 SaveSetting = `-- name: SaveSetting :one +INSERT INTO settings (key, value, updated_at) +VALUES ($1, $2, CURRENT_TIMESTAMP) ON CONFLICT (key) DO +UPDATE +SET value = EXCLUDED.value +RETURNING key, value, created_at, updated_at +` + +type SaveSettingParams struct { + Key string `json:"key"` + Value string `json:"value"` +} + +func (q *Queries) SaveSetting(ctx context.Context, arg SaveSettingParams) (Setting, error) { + row := q.db.QueryRow(ctx, SaveSetting, arg.Key, arg.Value) + var i Setting + err := row.Scan( + &i.Key, + &i.Value, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} diff --git a/gen/db/ticket.sql.go b/gen/db/ticket.sql.go index 4140384..c72c1ea 100644 --- a/gen/db/ticket.sql.go +++ b/gen/db/ticket.sql.go @@ -128,6 +128,29 @@ func (q *Queries) GetAllTickets(ctx context.Context) ([]TicketWithOutcome, error return items, nil } +const GetAllTicketsInRange = `-- name: GetAllTicketsInRange :one +SELECT COUNT(*) as total_tickets, COALESCE(SUM(amount), 0) as total_amount +FROM tickets +WHERE created_at BETWEEN $1 AND $2 +` + +type GetAllTicketsInRangeParams struct { + CreatedAt pgtype.Timestamp `json:"created_at"` + CreatedAt_2 pgtype.Timestamp `json:"created_at_2"` +} + +type GetAllTicketsInRangeRow struct { + TotalTickets int64 `json:"total_tickets"` + TotalAmount interface{} `json:"total_amount"` +} + +func (q *Queries) GetAllTicketsInRange(ctx context.Context, arg GetAllTicketsInRangeParams) (GetAllTicketsInRangeRow, error) { + row := q.db.QueryRow(ctx, GetAllTicketsInRange, arg.CreatedAt, arg.CreatedAt_2) + var i GetAllTicketsInRangeRow + err := row.Scan(&i.TotalTickets, &i.TotalAmount) + return i, err +} + const GetTicketByID = `-- name: GetTicketByID :one SELECT id, amount, total_odds, ip, created_at, updated_at, outcomes FROM ticket_with_outcomes diff --git a/gen/db/transfer.sql.go b/gen/db/transfer.sql.go index 18b6243..5055d84 100644 --- a/gen/db/transfer.sql.go +++ b/gen/db/transfer.sql.go @@ -34,7 +34,7 @@ type CreateTransferParams struct { SenderWalletID pgtype.Int8 `json:"sender_wallet_id"` CashierID pgtype.Int8 `json:"cashier_id"` Verified pgtype.Bool `json:"verified"` - ReferenceNumber pgtype.Text `json:"reference_number"` + ReferenceNumber string `json:"reference_number"` Status pgtype.Text `json:"status"` PaymentMethod pgtype.Text `json:"payment_method"` } @@ -139,7 +139,7 @@ FROM wallet_transfer WHERE reference_number = $1 ` -func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber pgtype.Text) (WalletTransfer, error) { +func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber string) (WalletTransfer, error) { row := q.db.QueryRow(ctx, GetTransferByReference, referenceNumber) var i WalletTransfer err := row.Scan( @@ -199,6 +199,44 @@ func (q *Queries) GetTransfersByWallet(ctx context.Context, receiverWalletID pgt return items, nil } +const GetWalletTransactionsInRange = `-- name: GetWalletTransactionsInRange :many +SELECT type, COUNT(*) as count, SUM(amount) as total_amount +FROM wallet_transfer +WHERE created_at BETWEEN $1 AND $2 +GROUP BY type +` + +type GetWalletTransactionsInRangeParams struct { + CreatedAt pgtype.Timestamp `json:"created_at"` + CreatedAt_2 pgtype.Timestamp `json:"created_at_2"` +} + +type GetWalletTransactionsInRangeRow struct { + Type pgtype.Text `json:"type"` + Count int64 `json:"count"` + TotalAmount int64 `json:"total_amount"` +} + +func (q *Queries) GetWalletTransactionsInRange(ctx context.Context, arg GetWalletTransactionsInRangeParams) ([]GetWalletTransactionsInRangeRow, error) { + rows, err := q.db.Query(ctx, GetWalletTransactionsInRange, arg.CreatedAt, arg.CreatedAt_2) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetWalletTransactionsInRangeRow + for rows.Next() { + var i GetWalletTransactionsInRangeRow + if err := rows.Scan(&i.Type, &i.Count, &i.TotalAmount); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const UpdateTransferStatus = `-- name: UpdateTransferStatus :exec UPDATE wallet_transfer SET status = $1, diff --git a/gen/db/virtual_games.sql.go b/gen/db/virtual_games.sql.go index 16034ee..c05d582 100644 --- a/gen/db/virtual_games.sql.go +++ b/gen/db/virtual_games.sql.go @@ -11,6 +11,110 @@ 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, + amount, + currency, + external_transaction_id, + reference_transaction_id, + status +) VALUES ( + $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, + amount, + currency, + external_transaction_id, + reference_transaction_id, + status, + created_at, + updated_at +` + +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"` + Amount int64 `json:"amount"` + Currency string `json:"currency"` + ExternalTransactionID string `json:"external_transaction_id"` + ReferenceTransactionID pgtype.Text `json:"reference_transaction_id"` + Status string `json:"status"` +} + +func (q *Queries) CreateVirtualGameHistory(ctx context.Context, arg CreateVirtualGameHistoryParams) (VirtualGameHistory, error) { + row := q.db.QueryRow(ctx, CreateVirtualGameHistory, + arg.SessionID, + arg.UserID, + arg.CompanyID, + arg.Provider, + arg.WalletID, + arg.GameID, + arg.TransactionType, + arg.Amount, + arg.Currency, + arg.ExternalTransactionID, + arg.ReferenceTransactionID, + arg.Status, + ) + var i VirtualGameHistory + err := row.Scan( + &i.ID, + &i.SessionID, + &i.UserID, + &i.CompanyID, + &i.Provider, + &i.WalletID, + &i.GameID, + &i.TransactionType, + &i.Amount, + &i.Currency, + &i.ExternalTransactionID, + &i.ReferenceTransactionID, + &i.Status, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + const CreateVirtualGameSession = `-- name: CreateVirtualGameSession :one INSERT INTO virtual_game_sessions ( user_id, game_id, session_token, currency, status, expires_at @@ -54,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, @@ -82,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, @@ -122,15 +248,81 @@ func (q *Queries) GetVirtualGameSessionByToken(ctx context.Context, sessionToken return i, err } +const GetVirtualGameSummaryInRange = `-- name: GetVirtualGameSummaryInRange :many +SELECT + c.name AS company_name, + vg.name AS game_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.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"` +} + +func (q *Queries) GetVirtualGameSummaryInRange(ctx context.Context, arg GetVirtualGameSummaryInRangeParams) ([]GetVirtualGameSummaryInRangeRow, error) { + rows, err := q.db.Query(ctx, GetVirtualGameSummaryInRange, arg.CreatedAt, arg.CreatedAt_2) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetVirtualGameSummaryInRangeRow + for rows.Next() { + var i GetVirtualGameSummaryInRangeRow + if err := rows.Scan( + &i.CompanyName, + &i.GameName, + &i.NumberOfBets, + &i.TotalTransactionSum, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const GetVirtualGameTransactionByExternalID = `-- 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 ` -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, @@ -147,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 e46ea0b..3a49ebf 100644 --- a/gen/db/wallet.sql.go +++ b/gen/db/wallet.sql.go @@ -49,7 +49,7 @@ INSERT INTO wallets ( user_id ) VALUES ($1, $2, $3, $4) -RETURNING id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, bonus_balance, cash_balance +RETURNING id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, currency, bonus_balance, cash_balance ` type CreateWalletParams struct { @@ -77,6 +77,7 @@ func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (Wal &i.IsActive, &i.CreatedAt, &i.UpdatedAt, + &i.Currency, &i.BonusBalance, &i.CashBalance, ) @@ -142,8 +143,48 @@ func (q *Queries) GetAllBranchWallets(ctx context.Context) ([]GetAllBranchWallet return items, nil } +const GetAllCustomerWallet = `-- name: GetAllCustomerWallet :many +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 +` + +func (q *Queries) GetAllCustomerWallet(ctx context.Context) ([]CustomerWalletDetail, error) { + rows, err := q.db.Query(ctx, GetAllCustomerWallet) + if err != nil { + return nil, err + } + defer rows.Close() + var items []CustomerWalletDetail + for rows.Next() { + var i CustomerWalletDetail + if err := rows.Scan( + &i.ID, + &i.CustomerID, + &i.RegularID, + &i.RegularBalance, + &i.StaticID, + &i.StaticBalance, + &i.RegularIsActive, + &i.StaticIsActive, + &i.RegularUpdatedAt, + &i.StaticUpdatedAt, + &i.CreatedAt, + &i.FirstName, + &i.LastName, + &i.PhoneNumber, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const GetAllWallets = `-- name: GetAllWallets :many -SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, bonus_balance, cash_balance +SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, currency, bonus_balance, cash_balance FROM wallets ` @@ -166,6 +207,7 @@ func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) { &i.IsActive, &i.CreatedAt, &i.UpdatedAt, + &i.Currency, &i.BonusBalance, &i.CashBalance, ); err != nil { @@ -179,37 +221,59 @@ func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) { return items, nil } -const GetCustomerWallet = `-- name: GetCustomerWallet :one -SELECT cw.id, - cw.customer_id, - rw.id AS regular_id, - rw.balance AS regular_balance, - sw.id AS static_id, - sw.balance AS static_balance, - rw.updated_at as regular_updated_at, - sw.updated_at as static_updated_at, - cw.created_at -FROM customer_wallets cw - JOIN wallets rw ON cw.regular_wallet_id = rw.id - JOIN wallets sw ON cw.static_wallet_id = sw.id -WHERE cw.customer_id = $1 +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 ` -type GetCustomerWalletRow struct { - ID int64 `json:"id"` - CustomerID int64 `json:"customer_id"` - RegularID int64 `json:"regular_id"` - RegularBalance int64 `json:"regular_balance"` - StaticID int64 `json:"static_id"` - StaticBalance int64 `json:"static_balance"` - RegularUpdatedAt pgtype.Timestamp `json:"regular_updated_at"` - StaticUpdatedAt pgtype.Timestamp `json:"static_updated_at"` - CreatedAt pgtype.Timestamp `json:"created_at"` +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 } -func (q *Queries) GetCustomerWallet(ctx context.Context, customerID int64) (GetCustomerWalletRow, error) { +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 +WHERE customer_id = $1 +` + +func (q *Queries) GetCustomerWallet(ctx context.Context, customerID int64) (CustomerWalletDetail, error) { row := q.db.QueryRow(ctx, GetCustomerWallet, customerID) - var i GetCustomerWalletRow + var i CustomerWalletDetail err := row.Scan( &i.ID, &i.CustomerID, @@ -217,15 +281,20 @@ func (q *Queries) GetCustomerWallet(ctx context.Context, customerID int64) (GetC &i.RegularBalance, &i.StaticID, &i.StaticBalance, + &i.RegularIsActive, + &i.StaticIsActive, &i.RegularUpdatedAt, &i.StaticUpdatedAt, &i.CreatedAt, + &i.FirstName, + &i.LastName, + &i.PhoneNumber, ) return i, err } const GetWalletByID = `-- name: GetWalletByID :one -SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, bonus_balance, cash_balance +SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, currency, bonus_balance, cash_balance FROM wallets WHERE id = $1 ` @@ -243,6 +312,7 @@ func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) { &i.IsActive, &i.CreatedAt, &i.UpdatedAt, + &i.Currency, &i.BonusBalance, &i.CashBalance, ) @@ -250,7 +320,7 @@ func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) { } const GetWalletByUserID = `-- name: GetWalletByUserID :many -SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, bonus_balance, cash_balance +SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, currency, bonus_balance, cash_balance FROM wallets WHERE user_id = $1 ` @@ -274,6 +344,7 @@ func (q *Queries) GetWalletByUserID(ctx context.Context, userID int64) ([]Wallet &i.IsActive, &i.CreatedAt, &i.UpdatedAt, + &i.Currency, &i.BonusBalance, &i.CashBalance, ); err != nil { diff --git a/go.mod b/go.mod index cfc550d..7fe0d0c 100644 --- a/go.mod +++ b/go.mod @@ -77,4 +77,16 @@ require ( go.uber.org/multierr v1.10.0 // indirect ) -require go.uber.org/atomic v1.9.0 // indirect +require ( + github.com/go-resty/resty/v2 v2.16.5 + github.com/twilio/twilio-go v1.26.3 +) + +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/redis/go-redis/v9 v9.10.0 // indirect + go.uber.org/atomic v1.9.0 // indirect +) diff --git a/go.sum b/go.sum index 6859366..514814e 100644 --- a/go.sum +++ b/go.sum @@ -9,11 +9,14 @@ github.com/amanuelabay/afrosms-go v1.0.6/go.mod h1:5mzzZtWSCDdvQsA0OyYf5CtbdGpl9 github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= @@ -22,6 +25,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -49,11 +54,15 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= +github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/gofiber/fiber/v2 v2.32.0/go.mod h1:CMy5ZLiXkn6qwthrl03YMyW1NLfj0rhxz2LKl4t7ZTY= github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -94,6 +103,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275 h1:IZycmTpoUtQK3PD60UYBwjaCUHUP7cML494ao9/O8+Q= +github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275/go.mod h1:zt6UU74K6Z6oMOYJbJzYpYucqdcQwSMPBEdSvGiaUMw= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -114,8 +125,12 @@ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6 github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs= +github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/resend/resend-go/v2 v2.20.0 h1:MrIrgV0aHhwRgmcRPw33Nexn6aGJvCvG2XwfFpAMBGM= github.com/resend/resend-go/v2 v2.20.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -150,6 +165,8 @@ github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9J github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= +github.com/twilio/twilio-go v1.26.3 h1:K2mYBzbhPVyWF+Jq5Sw53edBFvkgWo4sKTvgaO7461I= +github.com/twilio/twilio-go v1.26.3/go.mod h1:FpgNWMoD8CFnmukpKq9RNpUSGXC0BwnbeKZj2YHlIkw= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= @@ -170,6 +187,7 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= @@ -198,6 +216,7 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -213,8 +232,10 @@ golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -233,9 +254,12 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= diff --git a/internal/config/config.go b/internal/config/config.go index 802302e..b469617 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -13,25 +13,28 @@ import ( ) var ( - ErrInvalidDbUrl = errors.New("db url is invalid") - ErrInvalidPort = errors.New("port number is invalid") - ErrRefreshExpiry = errors.New("refresh token expiry is invalid") - ErrAccessExpiry = errors.New("access token expiry is invalid") - ErrInvalidJwtKey = errors.New("jwt key is invalid") - ErrLogLevel = errors.New("log level not set") - ErrInvalidLevel = errors.New("invalid log level") - ErrInvalidEnv = errors.New("env not set or invalid") - ErrInvalidSMSAPIKey = errors.New("SMS API key is invalid") - ErrMissingBetToken = errors.New("missing BET365_TOKEN in .env") - ErrInvalidPopOKClientID = errors.New("PopOK client ID is invalid") - ErrInvalidPopOKSecretKey = errors.New("PopOK secret key is invalid") - ErrInvalidPopOKBaseURL = errors.New("PopOK base URL is invalid") - ErrInvalidPopOKCallbackURL = errors.New("PopOK callback URL is invalid") - ErrInvalidVeliAPIURL = errors.New("Veli API URL is invalid") - ErrInvalidVeliOperatorKey = errors.New("Veli operator key is invalid") - ErrInvalidVeliSecretKey = errors.New("Veli secret key is invalid") - ErrMissingResendApiKey = errors.New("missing Resend Api key") - ErrMissingResendSenderEmail = errors.New("missing Resend sender name") + ErrInvalidDbUrl = errors.New("db url is invalid") + ErrInvalidPort = errors.New("port number is invalid") + ErrRefreshExpiry = errors.New("refresh token expiry is invalid") + ErrAccessExpiry = errors.New("access token expiry is invalid") + ErrInvalidJwtKey = errors.New("jwt key is invalid") + ErrLogLevel = errors.New("log level not set") + ErrInvalidLevel = errors.New("invalid log level") + ErrInvalidEnv = errors.New("env not set or invalid") + ErrInvalidSMSAPIKey = errors.New("SMS API key is invalid") + ErrMissingBetToken = errors.New("missing BET365_TOKEN in .env") + ErrInvalidPopOKClientID = errors.New("PopOK client ID is invalid") + ErrInvalidPopOKSecretKey = errors.New("PopOK secret key is invalid") + ErrInvalidPopOKBaseURL = errors.New("PopOK base URL is invalid") + ErrInvalidPopOKCallbackURL = errors.New("PopOK callback URL is invalid") + ErrInvalidVeliAPIURL = errors.New("Veli API URL is invalid") + ErrInvalidVeliOperatorKey = errors.New("Veli operator key is invalid") + ErrInvalidVeliSecretKey = errors.New("Veli secret key is invalid") + ErrMissingResendApiKey = errors.New("missing Resend Api key") + ErrMissingResendSenderEmail = errors.New("missing Resend sender name") + ErrMissingTwilioAccountSid = errors.New("missing twilio account sid") + ErrMissingTwilioAuthToken = errors.New("missing twilio auth token") + ErrMissingTwilioSenderPhoneNumber = errors.New("missing twilio sender phone number") ) type AleaPlayConfig struct { @@ -44,15 +47,14 @@ type AleaPlayConfig struct { SessionTimeout int `mapstructure:"session_timeout"` // In hours } -type VeliGamesConfig struct { - Enabled bool `mapstructure:"enabled"` - APIURL string `mapstructure:"api_url"` - OperatorKey string `mapstructure:"operator_key"` - SecretKey string `mapstructure:"secret_key"` - DefaultCurrency string `mapstructure:"default_currency"` - GameIDs struct { - Aviator string `mapstructure:"aviator"` - } `mapstructure:"game_ids"` +type VeliConfig struct { + APIKey string `mapstructure:"VELI_API_KEY"` + BaseURL string `mapstructure:"VELI_BASE_URL"` + SecretKey string `mapstructure:"VELI_SECRET_KEY"` + OperatorID string `mapstructure:"VELI_OPERATOR_ID"` + Currency string `mapstructure:"VELI_DEFAULT_CURRENCY"` + WebhookURL string `mapstructure:"VELI_WEBHOOK_URL"` + Enabled bool `mapstructure:"Enabled"` } type Config struct { @@ -60,6 +62,7 @@ type Config struct { FIXER_BASE_URL string BASE_CURRENCY domain.IntCurrency Port int + Service string DbUrl string RefreshExpiry int AccessExpiry int @@ -81,10 +84,14 @@ type Config struct { CHAPA_RETURN_URL string Bet365Token string PopOK domain.PopOKConfig - AleaPlay AleaPlayConfig `mapstructure:"alea_play"` - VeliGames VeliGamesConfig `mapstructure:"veli_games"` + AleaPlay AleaPlayConfig `mapstructure:"alea_play"` + VeliGames VeliConfig `mapstructure:"veli_games"` ResendApiKey string ResendSenderEmail string + TwilioAccountSid string + TwilioAuthToken string + TwilioSenderPhoneNumber string + RedisAddr string } func NewConfig() (*Config, error) { @@ -109,6 +116,8 @@ func (c *Config) loadEnv() error { c.ReportExportPath = os.Getenv("REPORT_EXPORT_PATH") + c.RedisAddr = os.Getenv("REDIS_ADDR") + c.CHAPA_TRANSFER_TYPE = os.Getenv("CHAPA_TRANSFER_TYPE") c.CHAPA_PAYMENT_TYPE = os.Getenv("CHAPA_PAYMENT_TYPE") @@ -236,26 +245,26 @@ func (c *Config) loadEnv() error { if apiURL == "" { apiURL = "https://api.velitech.games" // Default production URL } - c.VeliGames.APIURL = apiURL + c.VeliGames.BaseURL = apiURL operatorKey := os.Getenv("VELI_OPERATOR_KEY") if operatorKey == "" && c.VeliGames.Enabled { return ErrInvalidVeliOperatorKey } - c.VeliGames.OperatorKey = operatorKey + // c.VeliGames.OperatorKey = operatorKey secretKey := os.Getenv("VELI_SECRET_KEY") if secretKey == "" && c.VeliGames.Enabled { return ErrInvalidVeliSecretKey } c.VeliGames.SecretKey = secretKey - c.VeliGames.GameIDs.Aviator = os.Getenv("VELI_GAME_ID_AVIATOR") + // c.VeliGames.GameIDs.Aviator = os.Getenv("VELI_GAME_ID_AVIATOR") defaultCurrency := os.Getenv("VELI_DEFAULT_CURRENCY") if defaultCurrency == "" { defaultCurrency = "USD" // Default currency } - c.VeliGames.DefaultCurrency = defaultCurrency + // c.VeliGames.DefaultCurrency = defaultCurrency c.LogLevel = lvl @@ -324,6 +333,24 @@ func (c *Config) loadEnv() error { } c.ResendSenderEmail = resendSenderEmail + twilioAccountSid := os.Getenv("TWILIO_ACCOUNT_SID") + if twilioAccountSid == "" { + return ErrMissingTwilioAccountSid + } + c.TwilioAccountSid = twilioAccountSid + + twilioAuthToken := os.Getenv("TWILIO_AUTH_TOKEN") + if twilioAuthToken == "" { + return ErrMissingTwilioAuthToken + } + c.TwilioAuthToken = twilioAuthToken + + twilioSenderPhoneNumber := os.Getenv("TWILIO_SENDER_PHONE_NUMBER") + if twilioSenderPhoneNumber == "" { + return ErrMissingTwilioSenderPhoneNumber + } + c.TwilioSenderPhoneNumber = twilioSenderPhoneNumber + return nil } diff --git a/internal/domain/bet.go b/internal/domain/bet.go index cbd904e..5571fcb 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -57,9 +57,13 @@ type Bet struct { } type BetFilter struct { - BranchID ValidInt64 // Can Be Nullable - CompanyID ValidInt64 // Can Be Nullable - UserID ValidInt64 // Can Be Nullable + BranchID ValidInt64 // Can Be Nullable + CompanyID ValidInt64 // Can Be Nullable + UserID ValidInt64 // Can Be Nullable + IsShopBet ValidBool + Query ValidString + CreatedBefore ValidTime + CreatedAfter ValidTime } type GetBet struct { @@ -80,16 +84,17 @@ type GetBet struct { } type CreateBet struct { - Amount Currency - TotalOdds float32 - Status OutcomeStatus - FullName string - PhoneNumber string - CompanyID ValidInt64 // Can Be Nullable - BranchID ValidInt64 // Can Be Nullable - UserID ValidInt64 // Can Be Nullable - IsShopBet bool - CashoutID string + Amount Currency + TotalOdds float32 + Status OutcomeStatus + FullName string + PhoneNumber string + CompanyID ValidInt64 // Can Be Nullable + BranchID ValidInt64 // Can Be Nullable + UserID ValidInt64 // Can Be Nullable + IsShopBet bool + CashoutID string + OutcomesHash string } type CreateBetOutcomeReq struct { @@ -173,4 +178,3 @@ func ConvertBet(bet GetBet) BetRes { CreatedAt: bet.CreatedAt, } } - diff --git a/internal/domain/branch.go b/internal/domain/branch.go index 43d2cc0..6f1be95 100644 --- a/internal/domain/branch.go +++ b/internal/domain/branch.go @@ -7,10 +7,19 @@ type Branch struct { WalletID int64 BranchManagerID int64 CompanyID int64 - IsSuspended bool + IsActive bool IsSelfOwned bool } +type BranchFilter struct { + CompanyID ValidInt64 + IsActive ValidBool + BranchManagerID ValidInt64 + Query ValidString + CreatedBefore ValidTime + CreatedAfter ValidTime +} + type BranchDetail struct { ID int64 Name string @@ -19,10 +28,11 @@ type BranchDetail struct { Balance Currency BranchManagerID int64 CompanyID int64 - IsSuspended bool + IsActive bool IsSelfOwned bool ManagerName string ManagerPhoneNumber string + WalletIsActive bool } type SupportedOperation struct { @@ -53,6 +63,7 @@ type UpdateBranch struct { BranchManagerID *int64 CompanyID *int64 IsSelfOwned *bool + IsActive *bool } type CreateSupportedOperation struct { 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/common.go b/internal/domain/common.go index a6a408f..54433ab 100644 --- a/internal/domain/common.go +++ b/internal/domain/common.go @@ -78,3 +78,6 @@ func CalculateWinnings(amount Currency, totalOdds float32) Currency { return ToCurrency(possibleWin - incomeTax) } + +func PtrFloat64(v float64) *float64 { return &v } +func PtrInt64(v int64) *int64 { return &v } diff --git a/internal/domain/institutions.go b/internal/domain/institutions.go 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/league.go b/internal/domain/league.go index 67787a5..c4a2d12 100644 --- a/internal/domain/league.go +++ b/internal/domain/league.go @@ -7,6 +7,7 @@ type League struct { Bet365ID int32 `json:"bet365_id" example:"1121"` IsActive bool `json:"is_active" example:"false"` SportID int32 `json:"sport_id" example:"1"` + IsFeatured bool `json:"is_featured" example:"false"` } type UpdateLeague struct { @@ -15,6 +16,7 @@ type UpdateLeague struct { CountryCode ValidString `json:"cc" example:"uk"` Bet365ID ValidInt32 `json:"bet365_id" example:"1121"` IsActive ValidBool `json:"is_active" example:"false"` + IsFeatured ValidBool `json:"is_featured" example:"false"` SportID ValidInt32 `json:"sport_id" example:"1"` } @@ -22,6 +24,69 @@ type LeagueFilter struct { CountryCode ValidString SportID ValidInt32 IsActive ValidBool + IsFeatured ValidBool Limit ValidInt64 Offset ValidInt64 } + +// These leagues are automatically featured when the league is created +var FeaturedLeagues = []int64{ + // Football + 10044469, // Ethiopian Premier League + 10041282, //Premier League + 10083364, //La Liga + 10041095, //German Bundesliga + 10041100, //Ligue 1 + 10041809, //UEFA Champions League + 10041957, //UEFA Europa League + 10079560, //UEFA Conference League + 10050282, //UEFA Nations League + 10044685, //FIFA Club World Cup + 10050346, //UEFA Super Cup + 10081269, //CONCACAF Champions Cup + 10070189, //CONCACAF Gold Cup + 10076185, //UEFA Regions Cup + + 10067913, //Europe - World Cup Qualifying + 10040162, //Asia - World Cup Qualifying + 10067624, //South America - World Cup Qualifying + 10073057, //North & Central America - World Cup Qualifying + + 10037075, //International Match + 10077480, //Women’s International + 10037109, //Europe Friendlies + 10068837, //Euro U21 + + 10041315, //Italian Serie A + 10036538, //Spain Segunda + 10047168, // US MLS + + 10043156, //England FA Cup + 10042103, //France Cup + 10041088, //Premier League 2 + 10084250, //Turkiye Super League + 10041187, //Kenya Super League + 10041391, //Netherlands Eredivisie + + // Basketball + 10041830, //NBA + 10049984, //WNBA + 10037165, //German Bundesliga + 10036608, //Italian Lega 1 + 10040795, //EuroLeague + 10041534, //Basketball Africa League + + // Ice Hockey + 10037477, //NHL + 10037447, //AHL + 10069385, //IIHF World Championship + + // AMERICAN FOOTBALL + 10037219, //NFL + + // BASEBALL + 10037485, // MLB + + // VOLLEYBALL + 10069666, //FIVB Nations League +} diff --git a/internal/domain/notification.go b/internal/domain/notification.go index bcad707..db054c1 100644 --- a/internal/domain/notification.go +++ b/internal/domain/notification.go @@ -14,19 +14,22 @@ type NotificationDeliveryStatus string type DeliveryChannel string const ( - NotificationTypeCashOutSuccess NotificationType = "cash_out_success" - NotificationTypeDepositSuccess NotificationType = "deposit_success" - NotificationTypeBetPlaced NotificationType = "bet_placed" - NotificationTypeDailyReport NotificationType = "daily_report" - NotificationTypeHighLossOnBet NotificationType = "high_loss_on_bet" - NotificationTypeBetOverload NotificationType = "bet_overload" - NotificationTypeSignUpWelcome NotificationType = "signup_welcome" - NotificationTypeOTPSent NotificationType = "otp_sent" - NOTIFICATION_TYPE_WALLET NotificationType = "wallet_threshold" - NOTIFICATION_TYPE_TRANSFER NotificationType = "transfer_failed" - NOTIFICATION_TYPE_ADMIN_ALERT NotificationType = "admin_alert" - NOTIFICATION_RECEIVER_ADMIN NotificationRecieverSide = "admin" + NotificationTypeCashOutSuccess NotificationType = "cash_out_success" + NotificationTypeDepositSuccess NotificationType = "deposit_success" + NotificationTypeWithdrawSuccess NotificationType = "withdraw_success" + NotificationTypeBetPlaced NotificationType = "bet_placed" + NotificationTypeDailyReport NotificationType = "daily_report" + NotificationTypeHighLossOnBet NotificationType = "high_loss_on_bet" + NotificationTypeBetOverload NotificationType = "bet_overload" + NotificationTypeSignUpWelcome NotificationType = "signup_welcome" + NotificationTypeOTPSent NotificationType = "otp_sent" + NOTIFICATION_TYPE_WALLET NotificationType = "wallet_threshold" + NOTIFICATION_TYPE_TRANSFER_FAIL NotificationType = "transfer_failed" + NOTIFICATION_TYPE_TRANSFER_SUCCESS NotificationType = "transfer_success" + NOTIFICATION_TYPE_ADMIN_ALERT NotificationType = "admin_alert" + NOTIFICATION_TYPE_BET_RESULT NotificationType = "bet_result" + NOTIFICATION_RECEIVER_ADMIN NotificationRecieverSide = "admin" NotificationRecieverSideAdmin NotificationRecieverSide = "admin" NotificationRecieverSideCustomer NotificationRecieverSide = "customer" NotificationRecieverSideCashier NotificationRecieverSide = "cashier" @@ -57,9 +60,9 @@ const ( ) type NotificationPayload struct { - Headline string `json:"headline"` - Message string `json:"message"` - Tags []string `json:"tags"` + Headline string `json:"headline"` + Message string `json:"message"` + Tags []string `json:"tags"` } type Notification struct { @@ -91,3 +94,19 @@ func FromJSON(data []byte) (*Notification, error) { } return &n, nil } + +func ReceiverFromRole(role Role) NotificationRecieverSide { + + switch role { + case RoleAdmin: + return NotificationRecieverSideAdmin + case RoleCashier: + return NotificationRecieverSideCashier + case RoleBranchManager: + return NotificationRecieverSideBranchManager + case RoleCustomer: + return NotificationRecieverSideCustomer + default: + return "" + } +} diff --git a/internal/domain/otp.go b/internal/domain/otp.go index a6904e4..23c8640 100644 --- a/internal/domain/otp.go +++ b/internal/domain/otp.go @@ -26,6 +26,13 @@ const ( OtpMediumSms OtpMedium = "sms" ) +type OtpProvider string + +const ( + TwilioSms OtpProvider = "twilio" + AfroMessage OtpProvider = "aformessage" +) + type Otp struct { ID int64 SentTo string diff --git a/internal/domain/report.go b/internal/domain/report.go index 938633a..77bf4bf 100644 --- a/internal/domain/report.go +++ b/internal/domain/report.go @@ -10,6 +10,39 @@ const ( Monthly TimeFrame = "monthly" ) +type ReportFrequency string + +const ( + ReportDaily ReportFrequency = "daily" + ReportWeekly ReportFrequency = "weekly" + ReportMonthly ReportFrequency = "monthly" +) + +type ReportRequest struct { + Frequency ReportFrequency + StartDate time.Time + EndDate time.Time +} + +type ReportData struct { + TotalBets int64 + TotalCashIn float64 + TotalCashOut float64 + CashBacks float64 + Withdrawals float64 + Deposits float64 + TotalTickets int64 + VirtualGameStats []VirtualGameStat + CompanyReports []CompanyReport + BranchReports []BranchReport +} + +type VirtualGameStat struct { + GameName string + NumBets int64 + TotalTransaction float64 +} + type Report struct { ID string TimeFrame TimeFrame @@ -22,6 +55,22 @@ type Report struct { GeneratedAt time.Time } +type LiveMetric struct { + TotalCashSportsbook float64 + TotalCashSportGames float64 + TotalLiveTickets int64 + TotalUnsettledCash float64 + TotalGames int64 +} + +type MetricUpdates struct { + TotalCashSportsbookDelta *float64 + TotalCashSportGamesDelta *float64 + TotalLiveTicketsDelta *int64 + TotalUnsettledCashDelta *float64 + TotalGamesDelta *int64 +} + type DashboardSummary struct { TotalStakes Currency `json:"total_stakes"` TotalBets int64 `json:"total_bets"` @@ -319,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/settings.go b/internal/domain/settings.go new file mode 100644 index 0000000..c0c8368 --- /dev/null +++ b/internal/domain/settings.go @@ -0,0 +1,22 @@ +package domain + +import "time" + +type Setting struct { + Key string + Value string + UpdatedAt time.Time +} + +type SettingRes struct { + Key string `json:"key"` + Value string `json:"value"` + UpdatedAt string `json:"updated_at"` +} + +type SettingList struct { + MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"` + BetAmountLimit Currency `json:"bet_amount_limit"` + DailyTicketPerIP int64 `json:"daily_ticket_limit"` + TotalWinningLimit Currency `json:"total_winning_limit"` +} diff --git a/internal/domain/ticket.go b/internal/domain/ticket.go index e85638f..63c4d29 100644 --- a/internal/domain/ticket.go +++ b/internal/domain/ticket.go @@ -53,3 +53,31 @@ type CreateTicket struct { TotalOdds float32 IP string } + +type CreateTicketOutcomeReq struct { + // TicketID int64 `json:"ticket_id" example:"1"` + EventID int64 `json:"event_id" example:"1"` + OddID int64 `json:"odd_id" example:"1"` + MarketID int64 `json:"market_id" example:"1"` + // HomeTeamName string `json:"home_team_name" example:"Manchester"` + // AwayTeamName string `json:"away_team_name" example:"Liverpool"` + // MarketName string `json:"market_name" example:"Fulltime Result"` + // Odd float32 `json:"odd" example:"1.5"` + // OddName string `json:"odd_name" example:"1"` + // Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` +} + +type CreateTicketReq struct { + Outcomes []CreateTicketOutcomeReq `json:"outcomes"` + Amount float32 `json:"amount" example:"100.0"` +} +type CreateTicketRes struct { + FastCode int64 `json:"fast_code" example:"1234"` + CreatedNumber int64 `json:"created_number" example:"3"` +} +type TicketRes struct { + ID int64 `json:"id" example:"1"` + Outcomes []TicketOutcome `json:"outcomes"` + Amount float32 `json:"amount" example:"100.0"` + TotalOdds float32 `json:"total_odds" example:"4.22"` +} diff --git a/internal/domain/transfer.go b/internal/domain/transfer.go index 6e366a2..a518aec 100644 --- a/internal/domain/transfer.go +++ b/internal/domain/transfer.go @@ -12,7 +12,10 @@ const ( type PaymentMethod string +// Info on why the wallet was modified +// If its internal system modification then, its always direct const ( + TRANSFER_DIRECT PaymentMethod = "direct" TRANSFER_CASH PaymentMethod = "cash" TRANSFER_BANK PaymentMethod = "bank" TRANSFER_CHAPA PaymentMethod = "chapa" @@ -22,31 +25,36 @@ const ( TRANSFER_OTHER PaymentMethod = "other" ) -// There is always a receiving wallet id -// There is a sender wallet id only if wallet transfer type +// Info for the payment providers +type PaymentDetails struct { + ReferenceNumber ValidString + BankNumber ValidString +} + +// A Transfer is logged for every modification of ALL wallets and wallet types type Transfer struct { - ID int64 - Amount Currency - Verified bool - Type TransferType - PaymentMethod PaymentMethod - ReceiverWalletID int64 - SenderWalletID int64 - ReferenceNumber string - Status string - CashierID ValidInt64 - CreatedAt time.Time - UpdatedAt time.Time + ID int64 `json:"id"` + Amount Currency `json:"amount"` + Verified bool `json:"verified"` + Type TransferType `json:"type"` + PaymentMethod PaymentMethod `json:"payment_method"` + ReceiverWalletID ValidInt64 `json:"receiver_wallet_id"` + SenderWalletID ValidInt64 `json:"sender_wallet_id"` + ReferenceNumber string `json:"reference_number"` // <-- needed + Status string `json:"status"` + CashierID ValidInt64 `json:"cashier_id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type CreateTransfer struct { - Amount Currency - Verified bool - ReferenceNumber string - Status string - ReceiverWalletID int64 - SenderWalletID int64 - CashierID ValidInt64 - Type TransferType - PaymentMethod PaymentMethod + Amount Currency `json:"amount"` + Verified bool `json:"verified"` + Type TransferType `json:"type"` + PaymentMethod PaymentMethod `json:"payment_method"` + ReceiverWalletID ValidInt64 `json:"receiver_wallet_id"` + SenderWalletID ValidInt64 `json:"sender_wallet_id"` + ReferenceNumber string `json:"reference_number"` // <-- needed + Status string `json:"status"` + CashierID ValidInt64 `json:"cashier_id"` } diff --git a/internal/domain/veli_games.go b/internal/domain/veli_games.go new file mode 100644 index 0000000..3652c32 --- /dev/null +++ b/internal/domain/veli_games.go @@ -0,0 +1,36 @@ +package domain + +import "time" + +type Game struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + ReleaseDate string `json:"release_date"` + Developer string `json:"developer"` + Publisher string `json:"publisher"` + Genres []string `json:"genres"` + Platforms []string `json:"platforms"` + Price float64 `json:"price"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type GameListResponse struct { + Data []Game `json:"data"` + Total int `json:"total"` + Page int `json:"page"` + PerPage int `json:"per_page"` + TotalPages int `json:"total_pages"` +} + +type GameCreateRequest struct { + Name string `json:"name" validate:"required"` + Description string `json:"description" validate:"required"` + ReleaseDate string `json:"release_date" validate:"required"` + Developer string `json:"developer" validate:"required"` + Publisher string `json:"publisher" validate:"required"` + Genres []string `json:"genres" validate:"required"` + Platforms []string `json:"platforms" validate:"required"` + Price float64 `json:"price" validate:"required"` +} diff --git a/internal/domain/virtual_game.go b/internal/domain/virtual_game.go index 0c5af92..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"` @@ -38,10 +62,31 @@ type VirtualGameSession struct { GameMode string `json:"game_mode"` // real, demo, tournament } +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. + Amount int64 `json:"amount"` // Stored in minor units (e.g. cents) + Currency string `json:"currency"` // e.g., ETB, USD + ExternalTransactionID string `json:"external_transaction_id"` // Provider transaction ID + ReferenceTransactionID string `json:"reference_transaction_id,omitempty"` // For CANCELs pointing to BETs + Status string `json:"status"` // COMPLETED, CANCELLED, FAILED, etc. + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + 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 @@ -143,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"` @@ -156,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"` @@ -191,3 +245,27 @@ type GameSpecificData struct { RiskLevel string `json:"risk_level,omitempty"` // For Mines BucketIndex int `json:"bucket_index,omitempty"` // For Plinko } + +type PopOKGame struct { + ID int `json:"id"` + GameName string `json:"gameName"` + Bets []float64 `json:"bets"` + Thumbnail string `json:"thumbnail"` + Status int `json:"status"` +} + +type PopOKGameListResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data struct { + Slots []PopOKGame `json:"slots"` + } `json:"data"` +} + +type GameRecommendation struct { + GameID int `json:"game_id"` + GameName string `json:"game_name"` + Thumbnail string `json:"thumbnail"` + Bets []float64 `json:"bets"` + Reason string `json:"reason"` // e.g., "Based on your activity", "Popular", "Random pick" +} diff --git a/internal/domain/wallet.go b/internal/domain/wallet.go index 5a90078..7fe8f73 100644 --- a/internal/domain/wallet.go +++ b/internal/domain/wallet.go @@ -15,6 +15,13 @@ type Wallet struct { CreatedAt time.Time } +type WalletFilter struct { + IsActive ValidBool + Query ValidString + CreatedBefore ValidTime + CreatedAfter ValidTime +} + type CustomerWallet struct { ID int64 RegularID int64 @@ -28,9 +35,14 @@ type GetCustomerWallet struct { StaticID int64 StaticBalance Currency CustomerID int64 + RegularIsActive bool + StaticIsActive bool RegularUpdatedAt time.Time StaticUpdatedAt time.Time CreatedAt time.Time + FirstName string + LastName string + PhoneNumber string } type BranchWallet struct { @@ -58,3 +70,11 @@ type CreateCustomerWallet struct { RegularWalletID int64 StaticWalletID int64 } + +type WalletType string + +const ( + CustomerWalletType WalletType = "customer_wallet" + BranchWalletType WalletType = "branch_wallet" + CompanyWalletType WalletType = "company_wallet" +) diff --git a/internal/logger/mongoLogger/init.go b/internal/logger/mongoLogger/init.go index 77ef645..6784c04 100644 --- a/internal/logger/mongoLogger/init.go +++ b/internal/logger/mongoLogger/init.go @@ -4,16 +4,18 @@ import ( "fmt" "os" + "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) -func InitLogger() (*zap.Logger, error) { +func InitLogger(cfg *config.Config) (*zap.Logger, error) { mongoCore, err := NewMongoCore( - "mongodb://root:secret@localhost:27017/?authSource=admin", + os.Getenv("MONGODB_URL"), "logdb", "applogs", zapcore.InfoLevel, + cfg, ) if err != nil { return nil, fmt.Errorf("failed to create MongoDB core: %w", err) diff --git a/internal/logger/mongoLogger/logger.go b/internal/logger/mongoLogger/logger.go index b3bec21..55197df 100644 --- a/internal/logger/mongoLogger/logger.go +++ b/internal/logger/mongoLogger/logger.go @@ -7,6 +7,7 @@ import ( "maps" + "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" @@ -17,9 +18,10 @@ type MongoCore struct { collection *mongo.Collection level zapcore.Level fields []zapcore.Field + cfg *config.Config } -func NewMongoCore(uri, dbName, collectionName string, level zapcore.Level) (zapcore.Core, error) { +func NewMongoCore(uri, dbName, collectionName string, level zapcore.Level, cfg *config.Config) (zapcore.Core, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() @@ -36,6 +38,7 @@ func NewMongoCore(uri, dbName, collectionName string, level zapcore.Level) (zapc return &MongoCore{ collection: coll, level: level, + cfg: cfg, }, nil } @@ -73,8 +76,8 @@ func (mc *MongoCore) Write(entry zapcore.Entry, fields []zapcore.Field) error { "fields": logMap, "caller": entry.Caller.String(), "stacktrace": entry.Stack, - "service": "fortunebet-backend", - "env": "dev", + "service": mc.cfg.Service, + "env": mc.cfg.Env, } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) diff --git a/internal/repository/bet.go b/internal/repository/bet.go index 560eb62..448b764 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -209,6 +209,22 @@ func (s *Store) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]doma Int64: filter.UserID.Value, Valid: filter.UserID.Valid, }, + IsShopBet: pgtype.Bool{ + Bool: filter.IsShopBet.Value, + Valid: filter.IsShopBet.Valid, + }, + Query: pgtype.Text{ + String: filter.Query.Value, + Valid: filter.Query.Valid, + }, + CreatedBefore: pgtype.Timestamp{ + Time: filter.CreatedBefore.Value, + Valid: filter.CreatedBefore.Valid, + }, + CreatedAfter: pgtype.Timestamp{ + Time: filter.CreatedAfter.Value, + Valid: filter.CreatedAfter.Valid, + }, }) if err != nil { domain.MongoDBLogger.Error("failed to get all bets", @@ -265,6 +281,19 @@ func (s *Store) GetBetByUserID(ctx context.Context, UserID int64) ([]domain.GetB return result, nil } +func (s *Store) GetBetCount(ctx context.Context, UserID int64, outcomesHash string) (int64, error) { + count, err := s.queries.GetBetCount(ctx, dbgen.GetBetCountParams{ + UserID: pgtype.Int8{Int64: UserID}, + OutcomesHash: outcomesHash, + }) + + if err != nil { + return 0, err + } + + return count, nil +} + func (s *Store) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error { err := s.queries.UpdateCashOut(ctx, dbgen.UpdateCashOutParams{ ID: id, @@ -295,8 +324,19 @@ func (s *Store) UpdateStatus(ctx context.Context, id int64, status domain.Outcom return err } -func (s *Store) GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]domain.BetOutcome, error) { - outcomes, err := s.queries.GetBetOutcomeByEventID(ctx, eventID) +func (s *Store) GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error) { + + outcomes, err := s.queries.GetBetOutcomeByEventID(ctx, dbgen.GetBetOutcomeByEventIDParams{ + EventID: eventID, + FilterStatus: pgtype.Int4{ + Int32: int32(domain.OUTCOME_STATUS_PENDING), + Valid: is_filtered, + }, + FilterStatus2: pgtype.Int4{ + Int32: int32(domain.OUTCOME_STATUS_ERROR), + Valid: is_filtered, + }, + }) if err != nil { domain.MongoDBLogger.Error("failed to get bet outcomes by event ID", zap.Int64("event_id", eventID), @@ -347,8 +387,44 @@ func (s *Store) UpdateBetOutcomeStatus(ctx context.Context, id int64, status dom return res, nil } -func (s *Store) DeleteBet(ctx context.Context, id int64) error { - return s.queries.DeleteBet(ctx, id) +func (s *Store) UpdateBetOutcomeStatusByBetID(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) { + update, err := s.queries.UpdateBetOutcomeStatusByBetID(ctx, dbgen.UpdateBetOutcomeStatusByBetIDParams{ + Status: int32(status), + BetID: id, + }) + if err != nil { + domain.MongoDBLogger.Error("failed to update bet outcome status", + zap.Int64("id", id), + zap.Int32("status", int32(status)), + zap.Error(err), + ) + return domain.BetOutcome{}, err + } + + res := convertDBBetOutcomes(update) + return res, nil +} + +func (s *Store) UpdateBetOutcomeStatusForEvent(ctx context.Context, eventID int64, status domain.OutcomeStatus) ([]domain.BetOutcome, error) { + outcomes, err := s.queries.UpdateBetOutcomeStatusForEvent(ctx, dbgen.UpdateBetOutcomeStatusForEventParams{ + EventID: eventID, + Status: int32(status), + }) + + if err != nil { + domain.MongoDBLogger.Error("failed to update bet outcome status for event", + zap.Int64("eventID", eventID), + zap.Int32("status", int32(status)), + zap.Error(err), + ) + return nil, err + } + + var result []domain.BetOutcome = make([]domain.BetOutcome, 0, len(outcomes)) + for _, outcome := range outcomes { + result = append(result, convertDBBetOutcomes(outcome)) + } + return result, nil } // GetBetSummary returns aggregated bet statistics diff --git a/internal/repository/branch.go b/internal/repository/branch.go index 51f460f..e9491bb 100644 --- a/internal/repository/branch.go +++ b/internal/repository/branch.go @@ -32,6 +32,8 @@ func convertDBBranchDetail(dbBranch dbgen.BranchDetail) domain.BranchDetail { ManagerName: dbBranch.ManagerName.(string), ManagerPhoneNumber: dbBranch.ManagerPhoneNumber.String, Balance: domain.Currency(dbBranch.Balance.Int64), + IsActive: dbBranch.IsActive, + WalletIsActive: dbBranch.WalletIsActive.Bool, } } @@ -83,6 +85,12 @@ func convertUpdateBranch(updateBranch domain.UpdateBranch) dbgen.UpdateBranchPar Valid: true, } } + if updateBranch.IsActive != nil { + newUpdateBranch.IsActive = pgtype.Bool{ + Bool: *updateBranch.IsActive, + Valid: true, + } + } return newUpdateBranch } @@ -128,8 +136,29 @@ func (s *Store) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]do return branches, nil } -func (s *Store) GetAllBranches(ctx context.Context) ([]domain.BranchDetail, error) { - dbBranches, err := s.queries.GetAllBranches(ctx) +func (s *Store) GetAllBranches(ctx context.Context, filter domain.BranchFilter) ([]domain.BranchDetail, error) { + dbBranches, err := s.queries.GetAllBranches(ctx, dbgen.GetAllBranchesParams{ + CompanyID: pgtype.Int8{ + Int64: filter.CompanyID.Value, + Valid: filter.CompanyID.Valid, + }, + BranchManagerID: pgtype.Int8{ + Int64: filter.BranchManagerID.Value, + Valid: filter.BranchManagerID.Valid, + }, + Query: pgtype.Text{ + String: filter.Query.Value, + Valid: filter.Query.Valid, + }, + CreatedBefore: pgtype.Timestamp{ + Time: filter.CreatedBefore.Value, + Valid: filter.CreatedBefore.Valid, + }, + CreatedAfter: pgtype.Timestamp{ + Time: filter.CreatedAfter.Value, + Valid: filter.CreatedAfter.Valid, + }, + }) if err != nil { return nil, err } 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/league.go b/internal/repository/league.go index 67a1ba0..4cb9bb6 100644 --- a/internal/repository/league.go +++ b/internal/repository/league.go @@ -15,6 +15,7 @@ func (s *Store) SaveLeague(ctx context.Context, l domain.League) error { CountryCode: pgtype.Text{String: l.CountryCode, Valid: true}, Bet365ID: pgtype.Int4{Int32: l.Bet365ID, Valid: true}, IsActive: pgtype.Bool{Bool: l.IsActive, Valid: true}, + IsFeatured: pgtype.Bool{Bool: l.IsFeatured, Valid: true}, SportID: l.SportID, }) } @@ -33,6 +34,10 @@ func (s *Store) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ( Bool: filter.IsActive.Value, Valid: filter.IsActive.Valid, }, + IsFeatured: pgtype.Bool{ + Bool: filter.IsFeatured.Value, + Valid: filter.IsFeatured.Valid, + }, Limit: pgtype.Int4{ Int32: int32(filter.Limit.Value), Valid: filter.Limit.Valid, @@ -54,12 +59,35 @@ func (s *Store) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ( CountryCode: league.CountryCode.String, Bet365ID: league.Bet365ID.Int32, IsActive: league.IsActive.Bool, + IsFeatured: league.IsFeatured.Bool, SportID: league.SportID, } } return leagues, nil } +func (s *Store) GetFeaturedLeagues(ctx context.Context) ([]domain.League, error) { + l, err := s.queries.GetFeaturedLeagues(ctx) + + if err != nil { + return nil, err + } + + leagues := make([]domain.League, len(l)) + for i, league := range l { + leagues[i] = domain.League{ + ID: league.ID, + Name: league.Name, + CountryCode: league.CountryCode.String, + Bet365ID: league.Bet365ID.Int32, + IsActive: league.IsActive.Bool, + + SportID: league.SportID, + } + } + return leagues, nil +} + func (s *Store) CheckLeagueSupport(ctx context.Context, leagueID int64) (bool, error) { return s.queries.CheckLeagueSupport(ctx, leagueID) } @@ -93,6 +121,10 @@ func (s *Store) UpdateLeague(ctx context.Context, league domain.UpdateLeague) er Bool: league.IsActive.Value, Valid: league.IsActive.Valid, }, + IsFeatured: pgtype.Bool{ + Bool: league.IsFeatured.Value, + Valid: league.IsActive.Valid, + }, SportID: pgtype.Int4{ Int32: league.SportID.Value, Valid: league.SportID.Valid, diff --git a/internal/repository/notification.go b/internal/repository/notification.go index 21ace2b..c279377 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, + IsActive: 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 f7b2693..bff2ad0 100644 --- a/internal/repository/report.go +++ b/internal/repository/report.go @@ -2,15 +2,28 @@ package repository import ( "context" + "fmt" "time" + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/jackc/pgx/v5/pgtype" ) type ReportRepository interface { GenerateReport(timeFrame domain.TimeFrame, start, end time.Time) (*domain.Report, error) SaveReport(report *domain.Report) error FindReportsByTimeFrame(timeFrame domain.TimeFrame, limit int) ([]*domain.Report, 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 { @@ -105,3 +118,117 @@ func (r *ReportRepo) FindReportsByTimeFrame(timeFrame domain.TimeFrame, limit in return reports, nil } + +func (r *ReportRepo) GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time) (int64, error) { + params := dbgen.GetTotalBetsMadeInRangeParams{ + From: ToPgTimestamp(from), + To: ToPgTimestamp(to), + } + return r.store.queries.GetTotalBetsMadeInRange(ctx, params) +} + +func (r *ReportRepo) GetTotalCashBacksInRange(ctx context.Context, from, to time.Time) (float64, error) { + params := dbgen.GetTotalCashBacksInRangeParams{ + From: ToPgTimestamp(from), + To: ToPgTimestamp(to), + } + value, err := r.store.queries.GetTotalCashBacksInRange(ctx, params) + if err != nil { + return 0, err + } + return parseFloat(value) +} + +func (r *ReportRepo) GetTotalCashMadeInRange(ctx context.Context, from, to time.Time) (float64, error) { + params := dbgen.GetTotalCashMadeInRangeParams{ + From: ToPgTimestamp(from), + To: ToPgTimestamp(to), + } + value, err := r.store.queries.GetTotalCashMadeInRange(ctx, params) + if err != nil { + return 0, err + } + return parseFloat(value) +} + +func (r *ReportRepo) GetTotalCashOutInRange(ctx context.Context, from, to time.Time) (float64, error) { + params := dbgen.GetTotalCashOutInRangeParams{ + From: ToPgTimestamp(from), + To: ToPgTimestamp(to), + } + value, err := r.store.queries.GetTotalCashOutInRange(ctx, params) + if err != nil { + return 0, err + } + return parseFloat(value) +} + +func (r *ReportRepo) GetWalletTransactionsInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetWalletTransactionsInRangeRow, error) { + params := dbgen.GetWalletTransactionsInRangeParams{ + CreatedAt: ToPgTimestamp(from), + CreatedAt_2: ToPgTimestamp(to), + } + return r.store.queries.GetWalletTransactionsInRange(ctx, params) +} + +func (r *ReportRepo) GetAllTicketsInRange(ctx context.Context, from, to time.Time) (dbgen.GetAllTicketsInRangeRow, error) { + params := dbgen.GetAllTicketsInRangeParams{ + CreatedAt: ToPgTimestamp(from), + CreatedAt_2: ToPgTimestamp(to), + } + return r.store.queries.GetAllTicketsInRange(ctx, params) +} + +func (r *ReportRepo) GetVirtualGameSummaryInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetVirtualGameSummaryInRangeRow, error) { + params := dbgen.GetVirtualGameSummaryInRangeParams{ + CreatedAt: ToPgTimestamptz(from), + CreatedAt_2: ToPgTimestamptz(to), + } + return r.store.queries.GetVirtualGameSummaryInRange(ctx, params) +} + +func ToPgTimestamp(t time.Time) pgtype.Timestamp { + return pgtype.Timestamp{Time: t, Valid: true} +} + +func ToPgTimestamptz(t time.Time) pgtype.Timestamptz { + return pgtype.Timestamptz{Time: t, Valid: true} +} + +func parseFloat(value interface{}) (float64, error) { + switch v := value.(type) { + case float64: + return v, nil + case int64: + return float64(v), nil + case pgtype.Numeric: + if !v.Valid { + return 0, nil + } + f, err := v.Float64Value() + if err != nil { + return 0, fmt.Errorf("failed to convert pgtype.Numeric to float64: %w", err) + } + return f.Float64, nil + case nil: + return 0, nil + default: + 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/settings.go b/internal/repository/settings.go new file mode 100644 index 0000000..7cf0d29 --- /dev/null +++ b/internal/repository/settings.go @@ -0,0 +1,127 @@ +package repository + +import ( + "context" + "fmt" + "strconv" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "go.uber.org/zap" +) + +type DBSettingList struct { + MaxNumberOfOutcomes domain.ValidInt64 + BetAmountLimit domain.ValidInt64 + DailyTicketPerIP domain.ValidInt64 + TotalWinningLimit domain.ValidInt64 +} + +func GetDBSettingList(settings []dbgen.Setting) (domain.SettingList, error) { + var dbSettingList DBSettingList + var int64SettingsMap = map[string]*domain.ValidInt64{ + "max_number_of_outcomes": &dbSettingList.MaxNumberOfOutcomes, + "bet_amount_limit": &dbSettingList.BetAmountLimit, + "daily_ticket_limit": &dbSettingList.DailyTicketPerIP, + "total_winnings_limit": &dbSettingList.TotalWinningLimit, + } + + for _, setting := range settings { + is_setting_unknown := true + for key, dbSetting := range int64SettingsMap { + if setting.Key == key { + value, err := strconv.ParseInt(setting.Value, 10, 64) + if err != nil { + return domain.SettingList{}, err + } + *dbSetting = domain.ValidInt64{ + Value: value, + Valid: true, + } + is_setting_unknown = false + } + } + + if is_setting_unknown { + domain.MongoDBLogger.Warn("unknown setting found on database", zap.String("setting", setting.Key)) + } + } + + for key, dbSetting := range int64SettingsMap { + if !dbSetting.Valid { + fmt.Printf("setting value not found on database: %v \n", key) + domain.MongoDBLogger.Warn("setting value not found on database", zap.String("setting", key)) + } + } + + return domain.SettingList{ + MaxNumberOfOutcomes: dbSettingList.MaxNumberOfOutcomes.Value, + BetAmountLimit: domain.Currency(dbSettingList.BetAmountLimit.Value), + DailyTicketPerIP: dbSettingList.DailyTicketPerIP.Value, + TotalWinningLimit: domain.Currency(dbSettingList.TotalWinningLimit.Value), + }, nil +} +func (s *Store) GetSettingList(ctx context.Context) (domain.SettingList, error) { + settings, err := s.queries.GetSettings(ctx) + if err != nil { + domain.MongoDBLogger.Error("failed to get all settings", zap.Error(err)) + } + + return GetDBSettingList(settings) +} + +func (s *Store) GetSettings(ctx context.Context) ([]domain.Setting, error) { + settings, err := s.queries.GetSettings(ctx) + + if err != nil { + domain.MongoDBLogger.Error("failed to get all settings", zap.Error(err)) + } + + var result []domain.Setting = make([]domain.Setting, 0, len(settings)) + for _, setting := range settings { + result = append(result, domain.Setting{ + Key: setting.Key, + Value: setting.Value, + UpdatedAt: setting.UpdatedAt.Time, + }) + } + + return result, nil +} + +func (s *Store) GetSetting(ctx context.Context, key string) (domain.Setting, error) { + dbSetting, err := s.queries.GetSetting(ctx, key) + + if err != nil { + domain.MongoDBLogger.Error("failed to get all settings", zap.Error(err)) + } + + result := domain.Setting{ + Key: dbSetting.Key, + Value: dbSetting.Value, + UpdatedAt: dbSetting.UpdatedAt.Time, + } + + return result, nil +} + +func (s *Store) SaveSetting(ctx context.Context, key, value string) (domain.Setting, error) { + dbSetting, err := s.queries.SaveSetting(ctx, dbgen.SaveSettingParams{ + Key: key, + Value: value, + }) + + if err != nil { + domain.MongoDBLogger.Error("failed to update setting", zap.String("key", key), zap.String("value", value), zap.Error(err)) + + return domain.Setting{}, err + } + + setting := domain.Setting{ + Key: dbSetting.Key, + Value: dbSetting.Value, + } + + return setting, err + +} diff --git a/internal/repository/transfer.go b/internal/repository/transfer.go index 75c66ea..c432f9d 100644 --- a/internal/repository/transfer.go +++ b/internal/repository/transfer.go @@ -10,17 +10,27 @@ import ( func convertDBTransfer(transfer dbgen.WalletTransfer) domain.Transfer { return domain.Transfer{ - ID: transfer.ID, - Amount: domain.Currency(transfer.Amount.Int64), - Type: domain.TransferType(transfer.Type.String), - Verified: transfer.Verified.Bool, - ReceiverWalletID: transfer.ReceiverWalletID.Int64, - SenderWalletID: transfer.SenderWalletID.Int64, + ID: transfer.ID, + Amount: domain.Currency(transfer.Amount.Int64), + Type: domain.TransferType(transfer.Type.String), + Verified: transfer.Verified.Bool, + ReceiverWalletID: domain.ValidInt64{ + Value: transfer.ReceiverWalletID.Int64, + Valid: transfer.ReceiverWalletID.Valid, + }, + SenderWalletID: domain.ValidInt64{ + Value: transfer.SenderWalletID.Int64, + Valid: transfer.SenderWalletID.Valid, + }, CashierID: domain.ValidInt64{ Value: transfer.CashierID.Int64, Valid: transfer.CashierID.Valid, }, - PaymentMethod: domain.PaymentMethod(transfer.PaymentMethod.String), + PaymentMethod: domain.PaymentMethod(transfer.PaymentMethod.String), + ReferenceNumber: transfer.ReferenceNumber, + Status: transfer.Status.String, + CreatedAt: transfer.CreatedAt.Time, + UpdatedAt: transfer.UpdatedAt.Time, } } @@ -29,17 +39,19 @@ func convertCreateTransfer(transfer domain.CreateTransfer) dbgen.CreateTransferP Amount: pgtype.Int8{Int64: int64(transfer.Amount), Valid: true}, Type: pgtype.Text{String: string(transfer.Type), Valid: true}, ReceiverWalletID: pgtype.Int8{ - Int64: transfer.ReceiverWalletID, - Valid: true, + Int64: transfer.ReceiverWalletID.Value, + Valid: transfer.ReceiverWalletID.Valid, }, SenderWalletID: pgtype.Int8{ - Int64: transfer.SenderWalletID, - Valid: true, + Int64: transfer.SenderWalletID.Value, + Valid: transfer.SenderWalletID.Valid, }, CashierID: pgtype.Int8{ Int64: transfer.CashierID.Value, Valid: transfer.CashierID.Valid, }, + ReferenceNumber: string(transfer.ReferenceNumber), + PaymentMethod: pgtype.Text{String: string(transfer.PaymentMethod), Valid: true}, } } @@ -64,6 +76,7 @@ func (s *Store) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error) } return result, nil } + func (s *Store) GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.Transfer, error) { transfers, err := s.queries.GetTransfersByWallet(ctx, pgtype.Int8{Int64: walletID, Valid: true}) if err != nil { @@ -79,7 +92,7 @@ func (s *Store) GetTransfersByWallet(ctx context.Context, walletID int64) ([]dom } func (s *Store) GetTransferByReference(ctx context.Context, reference string) (domain.Transfer, error) { - transfer, err := s.queries.GetTransferByReference(ctx, pgtype.Text{String: reference, Valid: true}) + transfer, err := s.queries.GetTransferByReference(ctx, reference) if err != nil { return domain.Transfer{}, nil } diff --git a/internal/repository/virtual_game.go b/internal/repository/virtual_game.go index 3b5277b..b4c8e06 100644 --- a/internal/repository/virtual_game.go +++ b/internal/repository/virtual_game.go @@ -19,8 +19,13 @@ 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) + CreateVirtualGameHistory(ctx context.Context, his *domain.VirtualGameHistory) error } type VirtualGameRepo struct { @@ -36,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, @@ -92,6 +117,21 @@ func (r *VirtualGameRepo) CreateVirtualGameTransaction(ctx context.Context, tx * return err } +func (r *VirtualGameRepo) CreateVirtualGameHistory(ctx context.Context, his *domain.VirtualGameHistory) error { + params := dbgen.CreateVirtualGameHistoryParams{ + SessionID: pgtype.Text{String: his.SessionID, Valid: true}, + UserID: his.UserID, + // WalletID: pgtype.Int8{Int64: *his.WalletID, Valid: true}, + TransactionType: his.TransactionType, + Amount: his.Amount, + Currency: his.Currency, + ExternalTransactionID: his.ExternalTransactionID, + Status: his.Status, + } + _, err := r.store.queries.CreateVirtualGameHistory(ctx, params) + return err +} + func (r *VirtualGameRepo) GetVirtualGameTransactionByExternalID(ctx context.Context, externalID string) (*domain.VirtualGameTransaction, error) { dbTx, err := r.store.queries.GetVirtualGameTransactionByExternalID(ctx, externalID) if err != nil { @@ -153,6 +193,24 @@ func (r *VirtualGameRepo) GetGameCounts(ctx context.Context, filter domain.Repor return total, active, inactive, nil } +func (r *VirtualGameRepo) GetUserGameHistory(ctx context.Context, userID int64) ([]domain.VirtualGameHistory, error) { + query := `SELECT game_id FROM virtual_game_histories WHERE user_id = $1 AND transaction_type = 'BET' ORDER BY created_at DESC LIMIT 100` + rows, err := r.store.conn.Query(ctx, query, userID) + if err != nil { + return nil, err + } + defer rows.Close() + + var history []domain.VirtualGameHistory + for rows.Next() { + var tx domain.VirtualGameHistory + if err := rows.Scan(&tx.GameID); err == nil { + history = append(history, tx) + } + } + return history, nil +} + // func (r *VirtualGameRepo) WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error { // _, tx, err := r.store.BeginTx(ctx) // if err != nil { diff --git a/internal/repository/wallet.go b/internal/repository/wallet.go index 3271b54..4a6ae45 100644 --- a/internal/repository/wallet.go +++ b/internal/repository/wallet.go @@ -47,7 +47,7 @@ func convertCreateCustomerWallet(customerWallet domain.CreateCustomerWallet) dbg } } -func convertDBGetCustomerWallet(customerWallet dbgen.GetCustomerWalletRow) domain.GetCustomerWallet { +func convertDBGetCustomerWallet(customerWallet dbgen.CustomerWalletDetail) domain.GetCustomerWallet { return domain.GetCustomerWallet{ ID: customerWallet.ID, RegularID: customerWallet.RegularID, @@ -55,9 +55,14 @@ func convertDBGetCustomerWallet(customerWallet dbgen.GetCustomerWalletRow) domai StaticID: customerWallet.StaticID, StaticBalance: domain.Currency(customerWallet.StaticBalance), CustomerID: customerWallet.CustomerID, + RegularIsActive: customerWallet.RegularIsActive, + StaticIsActive: customerWallet.StaticIsActive, RegularUpdatedAt: customerWallet.RegularUpdatedAt.Time, StaticUpdatedAt: customerWallet.StaticUpdatedAt.Time, CreatedAt: customerWallet.CreatedAt.Time, + FirstName: customerWallet.FirstName, + LastName: customerWallet.LastName, + PhoneNumber: customerWallet.PhoneNumber.String, } } @@ -115,6 +120,19 @@ func (s *Store) GetWalletsByUser(ctx context.Context, userID int64) ([]domain.Wa return result, nil } +func (s *Store) GetAllCustomerWallets(ctx context.Context) ([]domain.GetCustomerWallet, error) { + customerWallets, err := s.queries.GetAllCustomerWallet(ctx) + if err != nil { + return nil, err + } + + var result []domain.GetCustomerWallet = make([]domain.GetCustomerWallet, 0, len(customerWallets)) + for _, wallet := range customerWallets { + result = append(result, convertDBGetCustomerWallet(wallet)) + } + return result, nil +} + func (s *Store) GetCustomerWallet(ctx context.Context, customerID int64) (domain.GetCustomerWallet, error) { customerWallet, err := s.queries.GetCustomerWallet(ctx, customerID) @@ -257,3 +275,4 @@ func (s *Store) GetTotalWallets(ctx context.Context, filter domain.ReportFilter) return total, nil } + diff --git a/internal/services/bet/port.go b/internal/services/bet/port.go index a249e43..8756667 100644 --- a/internal/services/bet/port.go +++ b/internal/services/bet/port.go @@ -15,12 +15,14 @@ type BetStore interface { GetAllBets(ctx context.Context, filter domain.BetFilter) ([]domain.GetBet, error) GetBetByBranchID(ctx context.Context, BranchID int64) ([]domain.GetBet, error) GetBetByUserID(ctx context.Context, UserID int64) ([]domain.GetBet, error) - GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]domain.BetOutcome, error) + GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error) GetBetOutcomeByBetID(ctx context.Context, betID int64) ([]domain.BetOutcome, error) + GetBetCount(ctx context.Context, userID int64, outcomesHash string) (int64, error) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) - DeleteBet(ctx context.Context, id int64) error + UpdateBetOutcomeStatusByBetID(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) + UpdateBetOutcomeStatusForEvent(ctx context.Context, eventID int64, status domain.OutcomeStatus) ([]domain.BetOutcome, error) GetBetSummary(ctx context.Context, filter domain.ReportFilter) ( totalStakes domain.Currency, diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index 59d0bc0..fb52bc0 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -3,13 +3,17 @@ package bet import ( "context" "crypto/rand" + "crypto/sha256" + "encoding/hex" "encoding/json" "errors" "fmt" "log/slog" "math/big" random "math/rand" + "sort" "strconv" + "strings" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -25,6 +29,12 @@ var ( ErrGenerateRandomOutcome = errors.New("Failed to generate any random outcome for events") ErrOutcomesNotCompleted = errors.New("Some bet outcomes are still pending") ErrEventHasBeenRemoved = errors.New("Event has been removed") + + ErrEventHasNotEnded = errors.New("Event has not ended yet") + ErrRawOddInvalid = errors.New("Prematch Raw Odd is Invalid") + ErrBranchIDRequired = errors.New("Branch ID required for this role") + ErrOutcomeLimit = errors.New("Too many outcomes on a single bet") + ErrTotalBalanceNotEnough = errors.New("Total Wallet balance is insufficient to create bet") ) type Service struct { @@ -37,7 +47,15 @@ type Service struct { mongoLogger *zap.Logger } -func NewService(betStore BetStore, eventSvc event.Service, prematchSvc odds.ServiceImpl, walletSvc wallet.Service, branchSvc branch.Service, logger *slog.Logger, mongoLogger *zap.Logger) *Service { +func NewService( + betStore BetStore, + eventSvc event.Service, + prematchSvc odds.ServiceImpl, + walletSvc wallet.Service, + branchSvc branch.Service, + logger *slog.Logger, + mongoLogger *zap.Logger, +) *Service { return &Service{ betStore: betStore, eventSvc: eventSvc, @@ -49,13 +67,6 @@ func NewService(betStore BetStore, eventSvc event.Service, prematchSvc odds.Serv } } -var ( - ErrEventHasNotEnded = errors.New("Event has not ended yet") - ErrRawOddInvalid = errors.New("Prematch Raw Odd is Invalid") - ErrBranchIDRequired = errors.New("Branch ID required for this role") - ErrOutcomeLimit = errors.New("Too many outcomes on a single bet") -) - func (s *Service) GenerateCashoutID() (string, error) { const chars = "abcdefghijklmnopqrstuvwxyz0123456789" const length int = 13 @@ -196,6 +207,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID var totalOdds float32 = 1 for _, outcomeReq := range req.Outcomes { + fmt.Println("reqq: ", outcomeReq) newOutcome, err := s.GenerateBetOutcome(ctx, outcomeReq.EventID, outcomeReq.MarketID, outcomeReq.OddID) if err != nil { s.mongoLogger.Error("failed to generate outcome", @@ -211,6 +223,23 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID outcomes = append(outcomes, newOutcome) } + outcomesHash, err := generateOutcomeHash(outcomes) + if err != nil { + s.mongoLogger.Error("failed to generate outcome hash", + zap.Int64("user_id", userID), + zap.Error(err), + ) + return domain.CreateBetRes{}, err + } + + count, err := s.GetBetCount(ctx, userID, outcomesHash) + if err != nil { + return domain.CreateBetRes{}, err + } + if count == 2 { + return domain.CreateBetRes{}, fmt.Errorf("bet already pleaced twice") + } + cashoutID, err := s.GenerateCashoutID() if err != nil { s.mongoLogger.Error("failed to generate cashout ID", @@ -221,12 +250,13 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID } newBet := domain.CreateBet{ - Amount: domain.ToCurrency(req.Amount), - TotalOdds: totalOdds, - Status: req.Status, - FullName: req.FullName, - PhoneNumber: req.PhoneNumber, - CashoutID: cashoutID, + Amount: domain.ToCurrency(req.Amount), + TotalOdds: totalOdds, + Status: req.Status, + FullName: req.FullName, + PhoneNumber: req.PhoneNumber, + CashoutID: cashoutID, + OutcomesHash: outcomesHash, } switch role { @@ -241,7 +271,11 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID } deductedAmount := req.Amount / 10 - err = s.walletSvc.DeductFromWallet(ctx, branch.WalletID, domain.ToCurrency(deductedAmount)) + _, err = s.walletSvc.DeductFromWallet(ctx, + branch.WalletID, domain.ToCurrency(deductedAmount), domain.BranchWalletType, domain.ValidInt64{ + Value: userID, + Valid: true, + }, domain.TRANSFER_DIRECT) if err != nil { s.mongoLogger.Error("failed to deduct from wallet", zap.Int64("wallet_id", branch.WalletID), @@ -274,7 +308,10 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID } deductedAmount := req.Amount / 10 - err = s.walletSvc.DeductFromWallet(ctx, branch.WalletID, domain.ToCurrency(deductedAmount)) + _, err = s.walletSvc.DeductFromWallet(ctx, branch.WalletID, domain.ToCurrency(deductedAmount), domain.BranchWalletType, domain.ValidInt64{ + Value: userID, + Valid: true, + }, domain.TRANSFER_DIRECT) if err != nil { s.mongoLogger.Error("wallet deduction failed", zap.Int64("wallet_id", branch.WalletID), @@ -290,7 +327,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID newBet.IsShopBet = true case domain.RoleCustomer: - wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID) + wallets, err := s.walletSvc.GetCustomerWallet(ctx, userID) if err != nil { s.mongoLogger.Error("failed to get customer wallets", zap.Int64("user_id", userID), @@ -298,16 +335,52 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID ) return domain.CreateBetRes{}, err } + if req.Amount < wallets.RegularBalance.Float32() { + _, err = s.walletSvc.DeductFromWallet(ctx, wallets.RegularID, + domain.ToCurrency(req.Amount), domain.CustomerWalletType, domain.ValidInt64{}, domain.TRANSFER_DIRECT) + if err != nil { + s.mongoLogger.Error("wallet deduction failed for customer regular wallet", + zap.Int64("customer_id", wallets.CustomerID), + zap.Int64("customer_wallet_id", wallets.ID), + zap.Int64("regular wallet_id", wallets.RegularID), + zap.Float32("amount", req.Amount), + zap.Error(err), + ) + return domain.CreateBetRes{}, err + } + } else { + combinedBalance := wallets.RegularBalance + wallets.StaticBalance + if req.Amount > combinedBalance.Float32() { + return domain.CreateBetRes{}, ErrTotalBalanceNotEnough + } + // Empty the regular balance + _, err = s.walletSvc.DeductFromWallet(ctx, wallets.RegularID, + wallets.RegularBalance, domain.CustomerWalletType, domain.ValidInt64{}, domain.TRANSFER_DIRECT) + if err != nil { + s.mongoLogger.Error("wallet deduction failed for customer regular wallet", + zap.Int64("customer_id", wallets.CustomerID), + zap.Int64("customer_wallet_id", wallets.ID), + zap.Int64("regular wallet_id", wallets.RegularID), + zap.Float32("amount", req.Amount), + zap.Error(err), + ) + return domain.CreateBetRes{}, err + } + // Empty remaining from static balance + remainingAmount := wallets.RegularBalance - domain.Currency(req.Amount) + _, err = s.walletSvc.DeductFromWallet(ctx, wallets.StaticID, + remainingAmount, domain.CustomerWalletType, domain.ValidInt64{}, domain.TRANSFER_DIRECT) + if err != nil { + s.mongoLogger.Error("wallet deduction failed for customer static wallet", + zap.Int64("customer_id", wallets.CustomerID), + zap.Int64("customer_wallet_id", wallets.ID), + zap.Int64("static wallet_id", wallets.StaticID), + zap.Float32("amount", req.Amount), + zap.Error(err), + ) + return domain.CreateBetRes{}, err + } - userWallet := wallets[0] - err = s.walletSvc.DeductFromWallet(ctx, userWallet.ID, domain.ToCurrency(req.Amount)) - if err != nil { - s.mongoLogger.Error("wallet deduction failed for customer", - zap.Int64("wallet_id", userWallet.ID), - zap.Float32("amount", req.Amount), - zap.Error(err), - ) - return domain.CreateBetRes{}, err } newBet.UserID = domain.ValidInt64{Value: userID, Valid: true} @@ -321,6 +394,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID return domain.CreateBetRes{}, fmt.Errorf("Unknown Role Type") } + fmt.Println("Bet is: ", newBet) bet, err := s.CreateBet(ctx, newBet) if err != nil { s.mongoLogger.Error("failed to create bet", @@ -636,6 +710,10 @@ func (s *Service) GetBetByUserID(ctx context.Context, UserID int64) ([]domain.Ge return s.betStore.GetBetByUserID(ctx, UserID) } +func (s *Service) GetBetCount(ctx context.Context, UserID int64, outcomesHash string) (int64, error) { + return s.betStore.GetBetCount(ctx, UserID, outcomesHash) +} + func (s *Service) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error { return s.betStore.UpdateCashOut(ctx, id, cashedOut) } @@ -676,7 +754,8 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc amount = bet.Amount } - err = s.walletSvc.AddToWallet(ctx, customerWallet.RegularID, amount) + _, err = s.walletSvc.AddToWallet(ctx, customerWallet.RegularID, amount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}) + if err != nil { s.mongoLogger.Error("failed to add winnings to wallet", zap.Int64("wallet_id", customerWallet.RegularID), @@ -782,6 +861,55 @@ func (s *Service) UpdateBetOutcomeStatus(ctx context.Context, id int64, status d } -func (s *Service) DeleteBet(ctx context.Context, id int64) error { - return s.betStore.DeleteBet(ctx, id) +func (s *Service) UpdateBetOutcomeStatusForEvent(ctx context.Context, eventID int64, status domain.OutcomeStatus) ([]domain.BetOutcome, error) { + outcomes, err := s.betStore.UpdateBetOutcomeStatusForEvent(ctx, eventID, status) + if err != nil { + s.mongoLogger.Error("failed to update bet outcome status", + zap.Int64("eventID", eventID), + zap.Error(err), + ) + return nil, err + } + + return outcomes, nil +} + +func (s *Service) SetBetToRemoved(ctx context.Context, id int64) error { + _, err := s.betStore.UpdateBetOutcomeStatusByBetID(ctx, id, domain.OUTCOME_STATUS_VOID) + if err != nil { + s.mongoLogger.Error("failed to update bet outcome to void", zap.Int64("id", id), + zap.Error(err), + ) + return err + } + + err = s.betStore.UpdateStatus(ctx, id, domain.OUTCOME_STATUS_VOID) + if err != nil { + s.mongoLogger.Error("failed to update bet to void", zap.Int64("id", id), + zap.Error(err), + ) + return err + } + return nil +} + +func generateOutcomeHash(outcomes []domain.CreateBetOutcome) (string, error) { + // should always be in the same order for producing the same hash + sort.Slice(outcomes, func(i, j int) bool { + if outcomes[i].EventID != outcomes[j].EventID { + return outcomes[i].EventID < outcomes[j].EventID + } + if outcomes[i].MarketID != outcomes[j].MarketID { + return outcomes[i].MarketID < outcomes[j].MarketID + } + return outcomes[i].OddID < outcomes[j].OddID + }) + + var sb strings.Builder + for _, o := range outcomes { + sb.WriteString(fmt.Sprintf("%d-%d-%d;", o.EventID, o.MarketID, o.OddID)) + } + + sum := sha256.Sum256([]byte(sb.String())) + return hex.EncodeToString(sum[:]), nil } diff --git a/internal/services/branch/port.go b/internal/services/branch/port.go index a128d59..3f242c8 100644 --- a/internal/services/branch/port.go +++ b/internal/services/branch/port.go @@ -11,7 +11,7 @@ type BranchStore interface { GetBranchByID(ctx context.Context, id int64) (domain.BranchDetail, error) GetBranchByManagerID(ctx context.Context, branchManagerID int64) ([]domain.BranchDetail, error) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]domain.BranchDetail, error) - GetAllBranches(ctx context.Context) ([]domain.BranchDetail, error) + GetAllBranches(ctx context.Context, filter domain.BranchFilter) ([]domain.BranchDetail, error) SearchBranchByName(ctx context.Context, name string) ([]domain.BranchDetail, error) UpdateBranch(ctx context.Context, branch domain.UpdateBranch) (domain.Branch, error) DeleteBranch(ctx context.Context, id int64) error diff --git a/internal/services/branch/service.go b/internal/services/branch/service.go index eb75170..eccb764 100644 --- a/internal/services/branch/service.go +++ b/internal/services/branch/service.go @@ -1,4 +1,4 @@ -package branch + package branch import ( "context" @@ -42,8 +42,8 @@ func (s *Service) GetBranchByCompanyID(ctx context.Context, companyID int64) ([] func (s *Service) GetBranchOperations(ctx context.Context, branchID int64) ([]domain.BranchOperation, error) { return s.branchStore.GetBranchOperations(ctx, branchID) } -func (s *Service) GetAllBranches(ctx context.Context) ([]domain.BranchDetail, error) { - return s.branchStore.GetAllBranches(ctx) +func (s *Service) GetAllBranches(ctx context.Context, filter domain.BranchFilter) ([]domain.BranchDetail, error) { + return s.branchStore.GetAllBranches(ctx, filter) } func (s *Service) GetBranchByCashier(ctx context.Context, userID int64) (domain.Branch, error) { diff --git a/internal/services/chapa/client.go b/internal/services/chapa/client.go index 94e1573..baac2fd 100644 --- a/internal/services/chapa/client.go +++ b/internal/services/chapa/client.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" "net/url" "time" @@ -30,9 +31,9 @@ func NewClient(baseURL, secretKey string) *Client { func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error) { payload := map[string]interface{}{ - "amount": req.Amount, - "currency": req.Currency, - "email": req.Email, + "amount": fmt.Sprintf("%.2f", float64(req.Amount)/100), + "currency": req.Currency, + // "email": req.Email, "first_name": req.FirstName, "last_name": req.LastName, "tx_ref": req.TxRef, @@ -40,6 +41,8 @@ func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositR "return_url": req.ReturnURL, } + fmt.Printf("\n\nChapa Payload: %+v\n\n", payload) + payloadBytes, err := json.Marshal(payload) if err != nil { return domain.ChapaDepositResponse{}, fmt.Errorf("failed to marshal payload: %w", err) @@ -50,6 +53,8 @@ func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositR return domain.ChapaDepositResponse{}, fmt.Errorf("failed to create request: %w", err) } + fmt.Printf("\n\nBase URL is: %+v\n\n", c.baseURL) + httpReq.Header.Set("Authorization", "Bearer "+c.secretKey) httpReq.Header.Set("Content-Type", "application/json") @@ -59,6 +64,11 @@ func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositR } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) // <-- Add this + return domain.ChapaDepositResponse{}, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body)) // <-- Log it + } + if resp.StatusCode != http.StatusOK { return domain.ChapaDepositResponse{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode) } @@ -77,7 +87,7 @@ func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositR return domain.ChapaDepositResponse{ CheckoutURL: response.Data.CheckoutURL, - // Reference: req.TxRef, + Reference: req.TxRef, }, nil } @@ -165,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 { @@ -213,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) @@ -230,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 { @@ -239,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 @@ -247,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 31a537f..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" @@ -31,6 +32,7 @@ func NewService( transferStore wallet.TransferStore, walletStore wallet.Service, userStore user.UserStore, + cfg *config.Config, chapaClient *Client, ) *Service { @@ -38,6 +40,7 @@ func NewService( transferStore: transferStore, walletStore: walletStore, userStore: userStore, + cfg: cfg, chapaClient: chapaClient, } } @@ -58,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) @@ -83,25 +88,28 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma PaymentMethod: domain.TRANSFER_CHAPA, ReferenceNumber: reference, // ReceiverWalletID: 1, - SenderWalletID: senderWallet.ID, - Verified: false, + SenderWalletID: domain.ValidInt64{ + Value: senderWallet.ID, + Valid: true, + }, + Verified: false, } - if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil { - return "", fmt.Errorf("failed to save payment: %w", err) - } - - // Initialize payment with Chapa - response, err := s.chapaClient.InitializePayment(ctx, domain.ChapaDepositRequest{ + payload := domain.ChapaDepositRequest{ Amount: amount, Currency: "ETB", Email: user.Email, FirstName: user.FirstName, LastName: user.LastName, TxRef: reference, - CallbackURL: "https://fortunebet.com/api/v1/payments/callback", - ReturnURL: "https://fortunebet.com/api/v1/payment-success", - }) + 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 @@ -109,8 +117,17 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma return "", fmt.Errorf("failed to initialize payment: %w", err) } + 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 } + func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req domain.ChapaWithdrawalRequest) (*domain.Transfer, error) { // Parse and validate amount amount, err := strconv.ParseInt(req.Amount, 10, 64) @@ -150,14 +167,16 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma reference := uuid.New().String() createTransfer := domain.CreateTransfer{ - Amount: domain.Currency(amount), - Type: domain.WITHDRAW, - ReceiverWalletID: 1, - SenderWalletID: withdrawWallet.ID, - Status: string(domain.PaymentStatusPending), - Verified: false, - ReferenceNumber: reference, - PaymentMethod: domain.TRANSFER_CHAPA, + Amount: domain.Currency(amount), + Type: domain.WITHDRAW, + SenderWalletID: domain.ValidInt64{ + Value: withdrawWallet.ID, + Valid: true, + }, + Status: string(domain.PaymentStatusPending), + Verified: false, + ReferenceNumber: reference, + PaymentMethod: domain.TRANSFER_CHAPA, } transfer, err := s.transferStore.CreateTransfer(ctx, createTransfer) @@ -177,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) @@ -204,42 +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 } - // 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) + // Validate sender wallet + if !transfer.SenderWalletID.Valid { + return nil, fmt.Errorf("invalid sender wallet ID: %v", transfer.SenderWalletID) } - // Update our records if payment is successful - if verification.Status == domain.PaymentStatusCompleted { - err = s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true) + var verification *domain.ChapaVerificationResponse + + // 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, 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 { @@ -265,13 +314,18 @@ 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 err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID, payment.Amount); err != nil { + 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, + }, + }); err != nil { return fmt.Errorf("failed to credit user wallet: %w", err) } } @@ -302,13 +356,12 @@ 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 err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID, transfer.Amount); err != nil { + 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/event/service.go b/internal/services/event/service.go index 0ad44a5..592241c 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -7,6 +7,7 @@ import ( "io" "log" "net/http" + "slices" "strconv" "sync" "time" @@ -202,8 +203,10 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { } func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, source string) { - sportIDs := []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91} + // sportIDs := []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91} + sportIDs := []int{1} // TODO: Add the league skipping again when we have dynamic leagues + // b, err := os.OpenFile("logs/skipped_leagues.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) // if err != nil { // log.Printf("❌ Failed to open leagues file %v", err) @@ -214,7 +217,6 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour var page int = 0 var limit int = 200 var count int = 0 - log.Printf("Sport ID %d", sportID) for page <= totalPages { page = page + 1 url := fmt.Sprintf(url, sportID, s.token, page) @@ -252,11 +254,13 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour // doesn't make sense to save and check back to back, but for now it can be here // no this its fine to keep it here // but change the league id to bet365 id later + //Automatically feature the league if its in the list err = s.store.SaveLeague(ctx, domain.League{ - ID: leagueID, - Name: ev.League.Name, - IsActive: true, - SportID: convertInt32(ev.SportID), + ID: leagueID, + Name: ev.League.Name, + IsActive: true, + IsFeatured: slices.Contains(domain.FeaturedLeagues, leagueID), + SportID: convertInt32(ev.SportID), }) if err != nil { 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/league/port.go b/internal/services/league/port.go index cfcf8b5..1f49632 100644 --- a/internal/services/league/port.go +++ b/internal/services/league/port.go @@ -9,6 +9,7 @@ import ( type Service interface { SaveLeague(ctx context.Context, l domain.League) error GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.League, error) + GetFeaturedLeagues(ctx context.Context) ([]domain.League, error) SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error UpdateLeague(ctx context.Context, league domain.UpdateLeague) error } diff --git a/internal/services/league/service.go b/internal/services/league/service.go index e23bf22..275dfb5 100644 --- a/internal/services/league/service.go +++ b/internal/services/league/service.go @@ -25,6 +25,10 @@ func (s *service) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) return s.store.GetAllLeagues(ctx, filter) } +func (s *service) GetFeaturedLeagues(ctx context.Context) ([]domain.League, error) { + return s.store.GetFeaturedLeagues(ctx) +} + func (s *service) SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error { return s.store.SetLeagueActive(ctx, leagueId, isActive) } 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 2e92e19..f3ca2d6 100644 --- a/internal/services/notfication/service.go +++ b/internal/services/notfication/service.go @@ -2,6 +2,7 @@ package notificationservice import ( "context" + "encoding/json" "errors" "log/slog" "sync" @@ -11,23 +12,32 @@ 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" + "github.com/redis/go-redis/v9" ) 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 + 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" + }) + svc := &Service{ repo: repo, Hub: hub, @@ -36,11 +46,13 @@ func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *confi notificationCh: make(chan *domain.Notification, 1000), stopCh: make(chan struct{}), config: cfg, + redisClient: rdb, } go hub.Run() go svc.startWorker() go svc.startRetryWorker() + go svc.RunRedisSubscriber(context.Background()) return svc } @@ -255,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 { @@ -264,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 { @@ -287,3 +300,176 @@ func (s *Service) CountUnreadNotifications(ctx context.Context, recipient_id int // func (s *Service) GetNotificationCounts(ctx context.Context, filter domain.ReportFilter) (total, read, unread int64, err error){ // return s.repo.Get(ctx, filter) // } + +func (s *Service) RunRedisSubscriber(ctx context.Context) { + pubsub := s.redisClient.Subscribe(ctx, "live_metrics") + defer pubsub.Close() + + ch := pubsub.Channel() + for msg := range ch { + 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 + } + + 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) UpdateLiveWalletMetrics(ctx context.Context, companies []domain.GetCompany, branches []domain.BranchWallet) error { + const key = "live_metrics" + + 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()), + }) + } + + 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()), + }) + } + + payload := domain.LiveWalletMetrics{ + Timestamp: time.Now(), + CompanyBalances: companyBalances, + BranchBalances: branchBalances, + } + + updatedData, err := json.Marshal(payload) + if err != nil { + return err + } + + if err := s.redisClient.Set(ctx, key, updatedData, 0).Err(); err != nil { + return err + } + + if err := s.redisClient.Publish(ctx, key, updatedData).Err(); err != nil { + return err + } + return nil +} + +func (s *Service) GetLiveMetrics(ctx context.Context) (domain.LiveMetric, error) { + const key = "live_metrics" + var metric domain.LiveMetric + + val, err := s.redisClient.Get(ctx, key).Result() + if err == redis.Nil { + // Key does not exist yet, return zero-valued struct + return domain.LiveMetric{}, nil + } else if err != nil { + return domain.LiveMetric{}, err + } + + if err := json.Unmarshal([]byte(val), &metric); err != nil { + return domain.LiveMetric{}, err + } + + 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/odds/service.go b/internal/services/odds/service.go index b3010d0..cdfda1e 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -1,4 +1,4 @@ -package odds + package odds import ( "context" diff --git a/internal/services/referal/service.go b/internal/services/referal/service.go index 4c1b5b8..bbb0d43 100644 --- a/internal/services/referal/service.go +++ b/internal/services/referal/service.go @@ -124,7 +124,7 @@ func (s *Service) ProcessReferral(ctx context.Context, referredPhone, referralCo walletID := wallets[0].ID currentBonus := float64(wallets[0].Balance) - err = s.walletSvc.AddToWallet(ctx, walletID, domain.Currency(int64((currentBonus+referral.RewardAmount)*100))) + _, err = s.walletSvc.AddToWallet(ctx, walletID, domain.ToCurrency(float32(currentBonus+referral.RewardAmount)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}) if err != nil { s.logger.Error("Failed to add referral reward to wallet", "walletID", walletID, "referrerID", referrerID, "error", err) return err @@ -162,7 +162,7 @@ func (s *Service) ProcessDepositBonus(ctx context.Context, userPhone string, amo walletID := wallets[0].ID bonus := amount * (settings.CashbackPercentage / 100) currentBonus := float64(wallets[0].Balance) - err = s.walletSvc.AddToWallet(ctx, walletID, domain.Currency(int64((currentBonus+bonus)*100))) + _, err = s.walletSvc.AddToWallet(ctx, walletID, domain.ToCurrency(float32(currentBonus+bonus)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}) if err != nil { s.logger.Error("Failed to add deposit bonus to wallet", "walletID", walletID, "userID", userID, "bonus", bonus, "error", err) return err @@ -216,7 +216,7 @@ func (s *Service) ProcessBetReferral(ctx context.Context, userPhone string, betA walletID := wallets[0].ID currentBalance := float64(wallets[0].Balance) - err = s.walletSvc.AddToWallet(ctx, walletID, domain.Currency(int64((currentBalance+bonus)*100))) + _, err = s.walletSvc.AddToWallet(ctx, walletID, domain.ToCurrency(float32(currentBalance+bonus)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}) if err != nil { s.logger.Error("Failed to add bet referral bonus to wallet", "walletID", walletID, "referrerID", referrerID, "bonus", bonus, "error", err) return err diff --git a/internal/services/report/service.go b/internal/services/report/service.go index eaa5c60..3e047e3 100644 --- a/internal/services/report/service.go +++ b/internal/services/report/service.go @@ -2,9 +2,14 @@ package report import ( "context" + "encoding/csv" "errors" + "fmt" "log/slog" + "os" "sort" + "strconv" + "strings" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -454,32 +459,299 @@ func (s *Service) GetSportPerformance(ctx context.Context, filter domain.ReportF return performances, nil } -func (s *Service) GenerateReport(timeFrame domain.TimeFrame) (*domain.Report, error) { - now := time.Now() - var start, end time.Time - - switch timeFrame { - case domain.Daily: - start = now.AddDate(0, 0, -1) - end = now - case domain.Weekly: - start = now.AddDate(0, 0, -7) - end = now - case domain.Monthly: - start = now.AddDate(0, -1, 0) - end = now - } - - report, err := s.repo.GenerateReport(timeFrame, start, end) +func (s *Service) GenerateReport(ctx context.Context, period string) error { + data, err := s.fetchReportData(ctx, period) if err != nil { - return nil, err + return fmt.Errorf("fetch data: %w", err) } - if err := s.repo.SaveReport(report); err != nil { - return nil, err + filePath := fmt.Sprintf("/host-desktop/report_%s_%s.csv", period, time.Now().Format("2006-01-02_15-04")) + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("create file: %w", err) + } + defer file.Close() + + writer := csv.NewWriter(file) + 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, + fmt.Sprintf("%d", data.TotalBets), + fmt.Sprintf("%.2f", data.TotalCashIn), + fmt.Sprintf("%.2f", data.TotalCashOut), + fmt.Sprintf("%.2f", data.CashBacks), + fmt.Sprintf("%.2f", data.Deposits), + fmt.Sprintf("%.2f", data.Withdrawals), + fmt.Sprintf("%d", data.TotalTickets), + }) + + 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{ + row.GameName, + fmt.Sprintf("%d", row.NumBets), + fmt.Sprintf("%.2f", row.TotalTransaction), + }) } - return report, nil + 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) + + // Basic metrics + 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) + var totalDeposits, totalWithdrawals float64 + for _, tx := range transactions { + switch strings.ToLower(tx.Type.String) { + case "deposit": + totalDeposits += float64(tx.TotalAmount) + case "withdraw": + totalWithdrawals += float64(tx.TotalAmount) + } + } + + // Ticket Count + totalTickets, _ := s.repo.GetAllTicketsInRange(ctx, from, to) + + // Virtual Game Summary + virtualGameStats, _ := s.repo.GetVirtualGameSummaryInRange(ctx, from, to) + + // Convert []dbgen.GetVirtualGameSummaryInRangeRow to []domain.VirtualGameStat + var virtualGameStatsDomain []domain.VirtualGameStat + for _, row := range virtualGameStats { + var totalTransaction float64 + switch v := row.TotalTransactionSum.(type) { + case string: + val, err := strconv.ParseFloat(v, 64) + if err == nil { + totalTransaction = val + } + case float64: + totalTransaction = v + case int: + totalTransaction = float64(v) + default: + totalTransaction = 0 + } + virtualGameStatsDomain = append(virtualGameStatsDomain, domain.VirtualGameStat{ + GameName: row.GameName, + NumBets: row.NumberOfBets, + TotalTransaction: totalTransaction, + }) + } + + 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, + TotalCashOut: cashOut, + CashBacks: cashBacks, + Deposits: totalDeposits, + Withdrawals: totalWithdrawals, + TotalTickets: totalTickets.TotalTickets, + VirtualGameStats: virtualGameStatsDomain, + CompanyReports: companyReports, + BranchReports: branchReports, + }, nil +} + +func getTimeRange(period string) (time.Time, time.Time) { + now := time.Now() + switch strings.ToLower(period) { + case "daily": + start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + end := start.Add(5 * time.Minute) + return start, end + case "weekly": + weekday := int(now.Weekday()) + if weekday == 0 { + weekday = 7 + } + start := now.AddDate(0, 0, -weekday+1) + start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, now.Location()) + end := start.AddDate(0, 0, 7) + return start, end + case "monthly": + start := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) + end := start.AddDate(0, 1, 0) + return start, end + default: + // Default to daily + start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + end := start.Add(24 * time.Hour) + return start, end + } } // func (s *Service) GetCompanyPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.CompanyPerformance, error) { diff --git a/internal/services/result/service.go b/internal/services/result/service.go index 189a0e3..3ceef62 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "log" "log/slog" "net/http" "strconv" @@ -17,30 +16,33 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "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" ) type Service struct { - repo *repository.Store - config *config.Config - logger *slog.Logger - client *http.Client - betSvc bet.Service - oddSvc odds.ServiceImpl - eventSvc event.Service - leagueSvc league.Service + repo *repository.Store + config *config.Config + logger *slog.Logger + client *http.Client + betSvc bet.Service + oddSvc odds.ServiceImpl + eventSvc event.Service + leagueSvc league.Service + notificationSvc *notificationservice.Service } -func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, betSvc bet.Service, oddSvc odds.ServiceImpl, eventSvc event.Service, leagueSvc league.Service) *Service { +func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, betSvc bet.Service, oddSvc odds.ServiceImpl, eventSvc event.Service, leagueSvc league.Service, notificationSvc *notificationservice.Service) *Service { return &Service{ - repo: repo, - config: cfg, - logger: logger, - client: &http.Client{Timeout: 10 * time.Second}, - betSvc: betSvc, - oddSvc: oddSvc, - eventSvc: eventSvc, - leagueSvc: leagueSvc, + repo: repo, + config: cfg, + logger: logger, + client: &http.Client{Timeout: 10 * time.Second}, + betSvc: betSvc, + oddSvc: oddSvc, + eventSvc: eventSvc, + leagueSvc: leagueSvc, + notificationSvc: notificationSvc, } } @@ -48,6 +50,127 @@ var ( ErrEventIsNotActive = fmt.Errorf("event has been cancelled or postponed") ) +func (s *Service) UpdateResultForOutcomes(ctx context.Context, eventID int64, resultRes json.RawMessage, sportID int64) error { + // TODO: Optimize this since there could be many outcomes with the same event_id and market_id that could be updated the same time + outcomes, err := s.repo.GetBetOutcomeByEventID(ctx, eventID, true) + if err != nil { + s.logger.Error("Failed to get pending bet outcomes", "error", err) + return fmt.Errorf("failed to get pending bet outcomes for event %d: %w", eventID, err) + } + + if len(outcomes) == 0 { + s.logger.Info("No bets have been placed on event", "event", eventID) + } + // if len(outcomes) == 0 { + // fmt.Printf("🕛 No bets have been placed on event %v (%d/%d) \n", eventID, i+1, len(events)) + // } else { + // fmt.Printf("✅ %d bets have been placed on event %v (%d/%d) \n", len(outcomes), event.ID, i+1, len(events)) + // } + + for _, outcome := range outcomes { + if outcome.Expires.After(time.Now()) { + s.logger.Warn("Outcome is not expired yet", "event_id", outcome.EventID, "outcome_id", outcome.ID) + return fmt.Errorf("Outcome has not expired yet") + } + + parseResult, err := s.parseResult(ctx, resultRes, outcome, sportID) + + if err != nil { + s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err) + return err + } + outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, parseResult.Status) + if err != nil { + s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err) + return err + } + if outcome.Status == domain.OUTCOME_STATUS_ERROR || outcome.Status == domain.OUTCOME_STATUS_PENDING { + s.logger.Error("Outcome is pending or error", "event_id", outcome.EventID, "outcome_id", outcome.ID) + return fmt.Errorf("Error while updating outcome") + } + + status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID) + if err != nil { + if err != bet.ErrOutcomesNotCompleted { + s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err) + } + return err + } + s.logger.Info("Updating bet status", "bet_id", outcome.BetID, "status", status.String()) + err = s.betSvc.UpdateStatus(ctx, outcome.BetID, status) + if err != nil { + s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err) + return err + } + } + return nil + +} + +func (s *Service) RefundAllOutcomes(ctx context.Context, eventID int64) error { + outcomes, err := s.repo.GetBetOutcomeByEventID(ctx, eventID, false) + + if err != nil { + s.logger.Error("Failed to get pending bet outcomes", "error", err) + return fmt.Errorf("failed to get pending bet outcomes for event %d: %w", eventID, err) + } + + if len(outcomes) == 0 { + s.logger.Info("No bets have been placed on event", "event", eventID) + } + + outcomes, err = s.betSvc.UpdateBetOutcomeStatusForEvent(ctx, eventID, domain.OUTCOME_STATUS_VOID) + + if err != nil { + s.logger.Error("Failed to update all outcomes for event") + } + + // Get all the unique bet_ids and how many outcomes have this bet_id + betIDSet := make(map[int64]int64) + for _, outcome := range outcomes { + betIDSet[outcome.BetID] += 1 + } + + for betID := range betIDSet { + status, err := s.betSvc.CheckBetOutcomeForBet(ctx, betID) + if err != nil { + if err != bet.ErrOutcomesNotCompleted { + s.logger.Error("Failed to check bet outcome for bet", "event_id", eventID, "error", err) + } + return err + } + err = s.betSvc.UpdateStatus(ctx, betID, status) + if err != nil { + s.logger.Error("Failed to update bet status", "event id", eventID, "error", err) + continue + } + } + return nil + // for _, outcome := range outcomes { + // outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, domain.OUTCOME_STATUS_VOID) + // if err != nil { + // s.logger.Error("Failed to refund all outcome status", "bet_outcome_id", outcome.ID, "error", err) + // return err + // } + + // // Check if all the bet outcomes have been set to refund for + // status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID) + // if err != nil { + // if err != bet.ErrOutcomesNotCompleted { + // s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err) + // } + // return err + // } + // err = s.betSvc.UpdateStatus(ctx, outcome.BetID, domain.OUTCOME_STATUS_VOID) + + // if err != nil { + // s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err) + // return err + // } + // } + +} + func (s *Service) FetchAndProcessResults(ctx context.Context) error { // TODO: Optimize this because there could be many bet outcomes for the same odd // Take market id and match result as param and update all the bet outcomes at the same time @@ -58,29 +181,15 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { } fmt.Printf("⚠️ Expired Events: %d \n", len(events)) removed := 0 - errs := make([]error, 0, len(events)) - for i, event := range events { + + for _, event := range events { eventID, err := strconv.ParseInt(event.ID, 10, 64) if err != nil { - s.logger.Error("Failed to parse event id") - errs = append(errs, fmt.Errorf("failed to parse event id %s: %w", event.ID, err)) - continue - } - outcomes, err := s.repo.GetBetOutcomeByEventID(ctx, eventID) - if err != nil { - s.logger.Error("Failed to get pending bet outcomes", "error", err) - errs = append(errs, fmt.Errorf("failed to get pending bet outcomes for event %d: %w", eventID, err)) + s.logger.Error("Failed to parse", "eventID", event.ID, "err", err) continue } - if len(outcomes) == 0 { - fmt.Printf("🕛 No bets have been placed on event %v (%d/%d) \n", event.ID, i+1, len(events)) - } else { - fmt.Printf("✅ %d bets have been placed on event %v (%d/%d) \n", len(outcomes), event.ID, i+1, len(events)) - } - - isDeleted := true result, err := s.fetchResult(ctx, eventID) if err != nil { if err == ErrEventIsNotActive { @@ -108,78 +217,41 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { } // TODO: Figure out what to do with the events that have been cancelled or postponed, etc... - if timeStatusParsed != int64(domain.TIME_STATUS_ENDED) { - s.logger.Warn("Event is not ended yet", "event_id", eventID, "time_status", commonResp.TimeStatus) - fmt.Printf("⚠️ Event %v is not ended yet (%d/%d) \n", event.ID, i+1, len(events)) - isDeleted = false + // if timeStatusParsed != int64(domain.TIME_STATUS_ENDED) { + // s.logger.Warn("Event is not ended yet", "event_id", eventID, "time_status", commonResp.TimeStatus) + // fmt.Printf("⚠️ Event %v is not ended yet (%d/%d) \n", event.ID, i+1, len(events)) + // isDeleted = false + // continue + // } + + // notification := &domain.Notification{ + // RecipientID: recipientID, + // Type: domain.NOTIFICATION_TYPE_WALLET, + // Level: domain.NotificationLevelWarning, + // Reciever: domain.NotificationRecieverSideAdmin, + // DeliveryChannel: domain.DeliveryChannelInApp, + // Payload: domain.NotificationPayload{ + // Headline: "Wallet Threshold Alert", + // Message: message, + // }, + // Priority: 2, // Medium priority + // } + + switch timeStatusParsed { + case int64(domain.TIME_STATUS_NOT_STARTED), int64(domain.TIME_STATUS_IN_PLAY): continue - } - for j, outcome := range outcomes { - fmt.Printf("⚙️ Processing 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n", - outcome.MarketName, - event.HomeTeam+" "+event.AwayTeam, event.ID, - j+1, len(outcomes)) + case int64(domain.TIME_STATUS_TO_BE_FIXED): + s.logger.Warn("Event needs to be rescheduled or corrected", "event_id", eventID) + // Admin users will be able to review the events - if outcome.Expires.After(time.Now()) { - isDeleted = false - s.logger.Warn("Outcome is not expired yet", "event_id", event.ID, "outcome_id", outcome.ID) - continue - } - - parseResult, err := s.parseResult(ctx, result.Results[0], outcome, sportID) + case int64(domain.TIME_STATUS_ENDED), int64(domain.TIME_STATUS_WALKOVER), int64(domain.TIME_STATUS_DECIDED_BY_FA): + err = s.UpdateResultForOutcomes(ctx, eventID, result.Results[0], sportID) if err != nil { - isDeleted = false - s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err) - errs = append(errs, fmt.Errorf("failed to parse result for event %d: %w", outcome.EventID, err)) - continue - } - outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, parseResult.Status) - if err != nil { - isDeleted = false - s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err) - continue - } - if outcome.Status == domain.OUTCOME_STATUS_ERROR || outcome.Status == domain.OUTCOME_STATUS_PENDING { - fmt.Printf("❌ Error while updating 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n", - outcome.MarketName, - event.HomeTeam+" "+event.AwayTeam, event.ID, - j+1, len(outcomes)) - - s.logger.Error("Outcome is pending or error", "event_id", outcome.EventID, "outcome_id", outcome.ID) - isDeleted = false - continue + s.logger.Error("Error while updating result for event", "event_id", eventID) } - fmt.Printf("✅ Successfully updated 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n", - outcome.MarketName, - event.HomeTeam+" "+event.AwayTeam, event.ID, - j+1, len(outcomes)) - - status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID) - if err != nil { - if err != bet.ErrOutcomesNotCompleted { - s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err) - } - isDeleted = false - continue - } - fmt.Printf("🧾 Updating bet %v - event %v (%d/%d) to %v\n", outcome.BetID, event.ID, j+1, len(outcomes), status.String()) - err = s.betSvc.UpdateStatus(ctx, outcome.BetID, status) - if err != nil { - s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err) - isDeleted = false - continue - } - fmt.Printf("✅ Successfully updated 🎫 Bet %v - event %v(%v) (%d/%d) \n", - outcome.BetID, - event.HomeTeam+" "+event.AwayTeam, event.ID, - j+1, len(outcomes)) - - } - if isDeleted { - removed += 1 - fmt.Printf("⚠️ Removing Event %v \n", event.ID) + s.logger.Info("Removing Event", "eventID", event.ID) err = s.repo.DeleteEvent(ctx, event.ID) if err != nil { s.logger.Error("Failed to remove event", "event_id", event.ID, "error", err) @@ -190,17 +262,91 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { s.logger.Error("Failed to remove odds for event", "event_id", event.ID, "error", err) return err } + removed += 1 + case int64(domain.TIME_STATUS_ABANDONED), int64(domain.TIME_STATUS_CANCELLED), int64(domain.TIME_STATUS_REMOVED): + s.logger.Info("Event abandoned/cancelled/removed", "event_id", eventID, "status", timeStatusParsed) + err = s.RefundAllOutcomes(ctx, eventID) + + s.logger.Info("Removing Event", "eventID", event.ID) + err = s.repo.DeleteEvent(ctx, event.ID) + if err != nil { + s.logger.Error("Failed to remove event", "event_id", event.ID, "error", err) + return err + } + err = s.repo.DeleteOddsForEvent(ctx, event.ID) + if err != nil { + s.logger.Error("Failed to remove odds for event", "event_id", event.ID, "error", err) + return err + } + removed += 1 } + // for j, outcome := range outcomes { + + // fmt.Printf("⚙️ Processing 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n", + // outcome.MarketName, + // event.HomeTeam+" "+event.AwayTeam, event.ID, + // j+1, len(outcomes)) + + // if outcome.Expires.After(time.Now()) { + // isDeleted = false + // s.logger.Warn("Outcome is not expired yet", "event_id", event.ID, "outcome_id", outcome.ID) + // continue + // } + + // parseResult, err := s.parseResult(ctx, result.Results[0], outcome, sportID) + // if err != nil { + // isDeleted = false + // s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err) + // errs = append(errs, fmt.Errorf("failed to parse result for event %d: %w", outcome.EventID, err)) + // continue + // } + + // outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, parseResult.Status) + // if err != nil { + // isDeleted = false + // s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err) + // continue + // } + // if outcome.Status == domain.OUTCOME_STATUS_ERROR || outcome.Status == domain.OUTCOME_STATUS_PENDING { + // fmt.Printf("❌ Error while updating 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n", + // outcome.MarketName, + // event.HomeTeam+" "+event.AwayTeam, event.ID, + // j+1, len(outcomes)) + + // s.logger.Error("Outcome is pending or error", "event_id", outcome.EventID, "outcome_id", outcome.ID) + // isDeleted = false + // continue + // } + + // fmt.Printf("✅ Successfully updated 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n", + // outcome.MarketName, + // event.HomeTeam+" "+event.AwayTeam, event.ID, + // j+1, len(outcomes)) + + // status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID) + // if err != nil { + // if err != bet.ErrOutcomesNotCompleted { + // s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err) + // } + // isDeleted = false + // continue + // } + + // fmt.Printf("🧾 Updating bet %v - event %v (%d/%d) to %v\n", outcome.BetID, event.ID, j+1, len(outcomes), status.String()) + // err = s.betSvc.UpdateStatus(ctx, outcome.BetID, status) + // if err != nil { + // s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err) + // isDeleted = false + // continue + // } + // fmt.Printf("✅ Successfully updated 🎫 Bet %v - event %v(%v) (%d/%d) \n", + // outcome.BetID, + // event.HomeTeam+" "+event.AwayTeam, event.ID, + // j+1, len(outcomes)) + // } } - fmt.Printf("🗑️ Removed Events: %d \n", removed) - if len(errs) > 0 { - s.logger.Error("Errors occurred while processing results", "errors", errs) - for _, err := range errs { - fmt.Println("Error:", err) - } - return fmt.Errorf("errors occurred while processing results: %v", errs) - } + s.logger.Info("Total Number of Removed Events", "count", removed) s.logger.Info("Successfully processed results", "removed_events", removed, "total_events", len(events)) return nil } @@ -211,10 +357,9 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error s.logger.Error("Failed to fetch events") return 0, err } - fmt.Printf("⚠️ Expired Events: %d \n", len(events)) updated := 0 - for i, event := range events { - fmt.Printf("⚙️ Processing event %v (%d/%d) \n", event.ID, i+1, len(events)) + for _, event := range events { + // fmt.Printf("⚙️ Processing event %v (%d/%d) \n", event.ID, i+1, len(events)) eventID, err := strconv.ParseInt(event.ID, 10, 64) if err != nil { s.logger.Error("Failed to parse event id") @@ -232,7 +377,6 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error } if result.Success != 1 || len(result.Results) == 0 { s.logger.Error("Invalid API response", "event_id", eventID) - fmt.Printf("⚠️ Invalid API response for event %v \n", result) continue } @@ -282,12 +426,13 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error continue } updated++ - fmt.Printf("✅ Successfully updated event %v to %v (%d/%d) \n", event.ID, eventStatus, i+1, len(events)) - + // fmt.Printf("✅ Successfully updated event %v to %v (%d/%d) \n", event.ID, eventStatus, i+1, len(events)) + s.logger.Info("Updated Event Status", "event ID", event.ID, "status", eventStatus) // Update the league because the league country code is only found on the result response leagueID, err := strconv.ParseInt(commonResp.League.ID, 10, 64) if err != nil { - log.Printf("❌ Invalid league id, leagueID %v", commonResp.League.ID) + // log.Printf("❌ Invalid league id, leagueID %v", commonResp.League.ID) + s.logger.Error("Invalid League ID", "leagueID", commonResp.League.ID) continue } @@ -304,12 +449,11 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error }) if err != nil { - log.Printf("❌ Error Updating League %v", commonResp.League.Name) - log.Printf("err:%v", err) + s.logger.Error("Error Updating League", "League Name", commonResp.League.Name, "err", err) continue } - fmt.Printf("✅ Updated League %v with country code %v \n", leagueID, commonResp.League.CC) - + // fmt.Printf("✅ Updated League %v with country code %v \n", leagueID, commonResp.League.CC) + s.logger.Info("Updated League with country code", "leagueID", leagueID, "code", commonResp.League.CC) } if updated == 0 { diff --git a/internal/services/settings/port.go b/internal/services/settings/port.go new file mode 100644 index 0000000..ce86f06 --- /dev/null +++ b/internal/services/settings/port.go @@ -0,0 +1,14 @@ +package settings + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type SettingStore interface { + GetSettingList(ctx context.Context) (domain.SettingList, error) + GetSettings(ctx context.Context) ([]domain.Setting, error) + GetSetting(ctx context.Context, key string) (domain.Setting, error) + SaveSetting(ctx context.Context, key, value string) (domain.Setting, error) +} diff --git a/internal/services/settings/service.go b/internal/services/settings/service.go new file mode 100644 index 0000000..66591a1 --- /dev/null +++ b/internal/services/settings/service.go @@ -0,0 +1,32 @@ +package settings + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type Service struct { + settingStore SettingStore +} + +func NewService(settingStore SettingStore) *Service { + return &Service{ + settingStore: settingStore, + } +} + +func (s *Service) GetSettingList(ctx context.Context) (domain.SettingList, error) { + return s.settingStore.GetSettingList(ctx) +} + +func (s *Service) GetSettings(ctx context.Context) ([]domain.Setting, error) { + return s.settingStore.GetSettings(ctx) +} + +func (s *Service) GetSetting(ctx context.Context, key string) (domain.Setting, error) { + return s.settingStore.GetSetting(ctx, key) +} +func (s *Service) SaveSetting(ctx context.Context, key, value string) (domain.Setting, error) { + return s.settingStore.SaveSetting(ctx, key, value) +} diff --git a/internal/services/ticket/service.go b/internal/services/ticket/service.go index 67c8a5a..6a91b7d 100644 --- a/internal/services/ticket/service.go +++ b/internal/services/ticket/service.go @@ -2,24 +2,255 @@ package ticket import ( "context" + "encoding/json" + "errors" + "strconv" + "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings" + "go.uber.org/zap" +) + +var ( + // ErrGenerateRandomOutcome = errors.New("Failed to generate any random outcome for events") + // ErrOutcomesNotCompleted = errors.New("Some bet outcomes are still pending") + ErrTicketHasExpired = errors.New("Ticket has expired") + ErrNoEventsAvailable = errors.New("Not enough events available with the given filters") + ErrEventHasBeenRemoved = errors.New("Event has been removed") + ErrTooManyOutcomesForTicket = errors.New("Too many odds/outcomes for a single ticket") + ErrTicketAmountTooHigh = errors.New("Cannot create a ticket with an amount above limit") + ErrTicketLimitForSingleUser = errors.New("Number of Ticket Limit reached") + ErrTicketWinningTooHigh = errors.New("Total Winnings over set limit") + + ErrRawOddInvalid = errors.New("Prematch Raw Odd is Invalid") ) type Service struct { - ticketStore TicketStore + ticketStore TicketStore + eventSvc event.Service + prematchSvc odds.ServiceImpl + mongoLogger *zap.Logger + settingSvc settings.Service + notificationSvc *notificationservice.Service } -func NewService(ticketStore TicketStore) *Service { +func NewService( + ticketStore TicketStore, + eventSvc event.Service, + prematchSvc odds.ServiceImpl, + mongoLogger *zap.Logger, + settingSvc settings.Service, + notificationSvc *notificationservice.Service, +) *Service { return &Service{ - ticketStore: ticketStore, + ticketStore: ticketStore, + eventSvc: eventSvc, + prematchSvc: prematchSvc, + mongoLogger: mongoLogger, + settingSvc: settingSvc, + notificationSvc: notificationSvc, } } -func (s *Service) CreateTicket(ctx context.Context, ticket domain.CreateTicket) (domain.Ticket, error) { - return s.ticketStore.CreateTicket(ctx, ticket) +func (s *Service) GenerateTicketOutcome(ctx context.Context, settings domain.SettingList, eventID int64, marketID int64, oddID int64) (domain.CreateTicketOutcome, error) { + eventIDStr := strconv.FormatInt(eventID, 10) + marketIDStr := strconv.FormatInt(marketID, 10) + oddIDStr := strconv.FormatInt(oddID, 10) + event, err := s.eventSvc.GetUpcomingEventByID(ctx, eventIDStr) + if err != nil { + s.mongoLogger.Error("failed to fetch upcoming event by ID", + zap.Int64("event_id", eventID), + zap.Error(err), + ) + return domain.CreateTicketOutcome{}, ErrEventHasBeenRemoved + } + + // Checking to make sure the event hasn't already started + currentTime := time.Now() + if event.StartTime.Before(currentTime) { + s.mongoLogger.Error("event has already started", + zap.Int64("event_id", eventID), + zap.Time("event_start_time", event.StartTime), + zap.Time("current_time", currentTime), + ) + return domain.CreateTicketOutcome{}, ErrTicketHasExpired + } + + odds, err := s.prematchSvc.GetRawOddsByMarketID(ctx, marketIDStr, eventIDStr) + + if err != nil { + s.mongoLogger.Error("failed to get raw odds by market ID", + zap.Int64("event_id", eventID), + zap.Int64("market_id", marketID), + zap.Error(err), + ) + // return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid market id", err, nil) + + } + type rawOddType struct { + ID string + Name string + Odds string + Header string + Handicap string + } + var selectedOdd rawOddType + var isOddFound bool = false + for _, raw := range odds.RawOdds { + var rawOdd rawOddType + rawBytes, err := json.Marshal(raw) + err = json.Unmarshal(rawBytes, &rawOdd) + if err != nil { + s.mongoLogger.Error("failed to unmarshal raw ods", + zap.Int64("event_id", eventID), + zap.String("rawOddID", rawOdd.ID), + zap.Error(err), + ) + continue + } + + if rawOdd.ID == oddIDStr { + selectedOdd = rawOdd + isOddFound = true + } + } + + if !isOddFound { + // return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid odd id", nil, nil) + s.mongoLogger.Error("Invalid Odd ID", + zap.Int64("event_id", eventID), + zap.String("oddIDStr", oddIDStr), + ) + return domain.CreateTicketOutcome{}, ErrRawOddInvalid + } + + parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32) + if err != nil { + s.mongoLogger.Error("failed to parse selected odd value", + zap.String("odd", selectedOdd.Odds), + zap.Int64("odd_id", oddID), + zap.Error(err), + ) + return domain.CreateTicketOutcome{}, err + } + + newOutcome := domain.CreateTicketOutcome{ + EventID: eventID, + OddID: oddID, + MarketID: marketID, + HomeTeamName: event.HomeTeam, + AwayTeamName: event.AwayTeam, + MarketName: odds.MarketName, + Odd: float32(parsedOdd), + OddName: selectedOdd.Name, + OddHeader: selectedOdd.Header, + OddHandicap: selectedOdd.Handicap, + Expires: event.StartTime, + } + + // outcomes = append(outcomes, ) + + return newOutcome, nil + } +func (s *Service) CreateTicket(ctx context.Context, req domain.CreateTicketReq, clientIP string) (domain.Ticket, int64, error) { + settingsList, err := s.settingSvc.GetSettingList(ctx) + + // Check to see if the number of outcomes is above a set limit + if len(req.Outcomes) > int(settingsList.MaxNumberOfOutcomes) { + return domain.Ticket{}, 0, ErrTooManyOutcomesForTicket + + } + + // Check to see if the amount is above a set limit + if req.Amount > settingsList.BetAmountLimit.Float32() { + return domain.Ticket{}, 0, ErrTicketAmountTooHigh + } + + count, err := s.CountTicketByIP(ctx, clientIP) + + if err != nil { + s.mongoLogger.Error("failed to count number of ticket using ip", + zap.Error(err), + ) + return domain.Ticket{}, 0, err + } + + // Check to see how many tickets a single anonymous user has created + if count > settingsList.DailyTicketPerIP { + + return domain.Ticket{}, 0, ErrTicketLimitForSingleUser + } + + var outcomes []domain.CreateTicketOutcome = make([]domain.CreateTicketOutcome, 0, len(req.Outcomes)) + var totalOdds float32 = 1 + for _, outcomeReq := range req.Outcomes { + newOutcome, err := s.GenerateTicketOutcome(ctx, settingsList, outcomeReq.EventID, outcomeReq.MarketID, outcomeReq.OddID) + if err != nil { + s.mongoLogger.Error("failed to generate outcome", + zap.Int64("event_id", outcomeReq.EventID), + zap.Int64("market_id", outcomeReq.MarketID), + zap.Int64("odd_id", outcomeReq.OddID), + zap.Error(err), + ) + return domain.Ticket{}, 0, err + } + totalOdds *= float32(newOutcome.Odd) + outcomes = append(outcomes, newOutcome) + } + totalWinnings := req.Amount * totalOdds + + // Check to see if the total winning amount is over a set limit + if totalWinnings > settingsList.TotalWinningLimit.Float32() { + s.mongoLogger.Error("Total Winnings over limit", + zap.Float32("Total Odds", totalOdds), + zap.Float32("amount", req.Amount), + zap.Float32("limit", settingsList.TotalWinningLimit.Float32())) + return domain.Ticket{}, 0, ErrTicketWinningTooHigh + } + + ticket, err := s.ticketStore.CreateTicket(ctx, domain.CreateTicket{ + Amount: domain.ToCurrency(req.Amount), + TotalOdds: totalOdds, + IP: clientIP, + }) + if err != nil { + s.mongoLogger.Error("Error Creating Ticket", zap.Float32("Total Odds", totalOdds), zap.Float32("amount", req.Amount)) + return domain.Ticket{}, 0, err + } + + // Add the ticket id now that it has fetched from the database + for index := range outcomes { + outcomes[index].TicketID = ticket.ID + } + + rows, err := s.CreateTicketOutcome(ctx, outcomes) + + if err != nil { + s.mongoLogger.Error("Error Creating Ticket Outcomes", zap.Any("outcomes", outcomes)) + return domain.Ticket{}, rows, err + } + + // updates := domain.MetricUpdates{ + // TotalLiveTicketsDelta: domain.PtrInt64(1), + // } + + // if err := s.notificationSvc.UpdateLiveMetrics(ctx, updates); err != nil { + // // handle error + // } + + return ticket, rows, nil +} + +// func (s *Service) CreateTicket(ctx context.Context, ticket domain.CreateTicket) (domain.Ticket, error) { +// return s.ticketStore.CreateTicket(ctx, ticket) +// } + func (s *Service) CreateTicketOutcome(ctx context.Context, outcomes []domain.CreateTicketOutcome) (int64, error) { return s.ticketStore.CreateTicketOutcome(ctx, outcomes) } diff --git a/internal/services/user/common.go b/internal/services/user/common.go index fd4f9aa..6cab064 100644 --- a/internal/services/user/common.go +++ b/internal/services/user/common.go @@ -10,18 +10,29 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers" afro "github.com/amanuelabay/afrosms-go" "github.com/resend/resend-go/v2" + "github.com/twilio/twilio-go" + twilioApi "github.com/twilio/twilio-go/rest/api/v2010" "golang.org/x/crypto/bcrypt" ) -func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium) error { +func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium, provider domain.OtpProvider) error { otpCode := helpers.GenerateOTP() message := fmt.Sprintf("Welcome to Fortune bets, your OTP is %s please don't share with anyone.", otpCode) switch medium { case domain.OtpMediumSms: - if err := s.SendSMSOTP(ctx, sentTo, message); err != nil { - return err + switch provider { + case "twilio": + if err := s.SendTwilioSMSOTP(ctx, sentTo, message, provider); err != nil { + return err + } + case "afromessage": + if err := s.SendAfroMessageSMSOTP(ctx, sentTo, message, provider); err != nil { + return err + } + default: + return fmt.Errorf("invalid sms provider: %s", provider) } case domain.OtpMediumEmail: if err := s.SendEmailOTP(ctx, sentTo, message); err != nil { @@ -51,7 +62,7 @@ func hashPassword(plaintextPassword string) ([]byte, error) { return hash, nil } -func (s *Service) SendSMSOTP(ctx context.Context, receiverPhone, message string) error { +func (s *Service) SendAfroMessageSMSOTP(ctx context.Context, receiverPhone, message string, provider domain.OtpProvider) error { apiKey := s.config.AFRO_SMS_API_KEY senderName := s.config.AFRO_SMS_SENDER_NAME hostURL := s.config.ADRO_SMS_HOST_URL @@ -79,6 +90,29 @@ func (s *Service) SendSMSOTP(ctx context.Context, receiverPhone, message string) } } +func (s *Service) SendTwilioSMSOTP(ctx context.Context, receiverPhone, message string, provider domain.OtpProvider) error { + accountSid := s.config.TwilioAccountSid + authToken := s.config.TwilioAuthToken + senderPhone := s.config.TwilioSenderPhoneNumber + + client := twilio.NewRestClientWithParams(twilio.ClientParams{ + Username: accountSid, + Password: authToken, + }) + + params := &twilioApi.CreateMessageParams{} + params.SetTo(receiverPhone) + params.SetFrom(senderPhone) + params.SetBody(message) + + _, err := client.Api.CreateMessage(params) + if err != nil { + return fmt.Errorf("Error sending SMS message: %s" + err.Error()) + } + + return nil +} + func (s *Service) SendEmailOTP(ctx context.Context, receiverEmail, message string) error { apiKey := s.config.ResendApiKey client := resend.NewClient(apiKey) diff --git a/internal/services/user/register.go b/internal/services/user/register.go index 3169b7b..c7e0d83 100644 --- a/internal/services/user/register.go +++ b/internal/services/user/register.go @@ -10,7 +10,7 @@ import ( func (s *Service) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) { // email,phone,error return s.userStore.CheckPhoneEmailExist(ctx, phoneNum, email) } -func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium, sentTo string) error { +func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.OtpProvider) error { var err error // check if user exists switch medium { @@ -26,7 +26,7 @@ func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium, } // send otp based on the medium - return s.SendOtp(ctx, sentTo, domain.OtpRegister, medium) + return s.SendOtp(ctx, sentTo, domain.OtpRegister, medium, provider) } func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterUserReq) (domain.User, error) { // normal // get otp diff --git a/internal/services/user/reset.go b/internal/services/user/reset.go index c6d3f47..7c4e5d5 100644 --- a/internal/services/user/reset.go +++ b/internal/services/user/reset.go @@ -8,7 +8,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) -func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, sentTo string) error { +func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.OtpProvider) error { var err error // check if user exists @@ -23,7 +23,7 @@ func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, se return err } - return s.SendOtp(ctx, sentTo, domain.OtpReset, medium) + return s.SendOtp(ctx, sentTo, domain.OtpReset, medium, provider) } @@ -57,7 +57,7 @@ func (s *Service) ResetPassword(ctx context.Context, resetReq domain.ResetPasswo return err } // reset pass and mark otp as used - + err = s.userStore.UpdatePassword(ctx, sentTo, hashedPassword, otp.ID) if err != nil { return err diff --git a/internal/services/virtualGame/Alea/service.go b/internal/services/virtualGame/Alea/service.go index aadd179..f3f9a9f 100644 --- a/internal/services/virtualGame/Alea/service.go +++ b/internal/services/virtualGame/Alea/service.go @@ -128,7 +128,7 @@ func (s *AleaPlayService) processTransaction(ctx context.Context, tx *domain.Vir } tx.WalletID = wallets[0].ID - if err := s.walletSvc.AddToWallet(ctx, tx.WalletID, domain.Currency(tx.Amount)); err != nil { + if _, err := s.walletSvc.AddToWallet(ctx, tx.WalletID, domain.Currency(tx.Amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil { return fmt.Errorf("wallet update failed: %w", err) } diff --git a/internal/services/virtualGame/port.go b/internal/services/virtualGame/port.go index 6a80458..36f57de 100644 --- a/internal/services/virtualGame/port.go +++ b/internal/services/virtualGame/port.go @@ -13,6 +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 b1e28d0..e413993 100644 --- a/internal/services/virtualGame/service.go +++ b/internal/services/virtualGame/service.go @@ -1,6 +1,7 @@ package virtualgameservice import ( + "bytes" "context" "crypto/hmac" "crypto/sha256" @@ -8,7 +9,12 @@ import ( "encoding/json" "errors" "fmt" + "io" "log/slog" + "math/rand/v2" + "net/http" + "sort" + "strconv" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" @@ -43,14 +49,15 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI return "", err } - sessionToken := fmt.Sprintf("%d-%s-%d", userID, gameID, time.Now().UnixNano()) + sessionId := fmt.Sprintf("%d-%s-%d", userID, gameID, time.Now().UnixNano()) token, err := jwtutil.CreatePopOKJwt( userID, - user.PhoneNumber, + user.CompanyID, + user.FirstName, currency, "en", mode, - sessionToken, + sessionId, s.config.PopOK.SecretKey, 24*time.Hour, ) @@ -59,19 +66,33 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI return "", err } + // Record game launch as a transaction (for history and recommendation purposes) + 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, + Currency: currency, + ExternalTransactionID: sessionId, + Status: "COMPLETED", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + if err := s.repo.CreateVirtualGameHistory(ctx, tx); err != nil { + s.logger.Error("Failed to record game launch transaction", "error", err) + // Do not fail game launch on logging error — just log and continue + } + params := fmt.Sprintf( "partnerId=%s&gameId=%s&gameMode=%s&lang=en&platform=%s&externalToken=%s", s.config.PopOK.ClientID, gameID, mode, s.config.PopOK.Platform, token, ) - // params = fmt.Sprintf( - // "partnerId=%s&gameId=%sgameMode=%s&lang=en&platform=%s", - // "1", "1", "fun", "111", - // ) - - // signature := s.generateSignature(params) return fmt.Sprintf("%s?%s", s.config.PopOK.BaseURL, params), nil - // return fmt.Sprintf("%s?%s", s.config.PopOK.BaseURL, params), nil } func (s *service) HandleCallback(ctx context.Context, callback *domain.PopOKCallback) error { @@ -117,7 +138,7 @@ func (s *service) HandleCallback(ctx context.Context, callback *domain.PopOKCall return errors.New("unknown transaction type") } - err = s.walletSvc.AddToWallet(ctx, walletID, domain.Currency(amount)) + _, err = s.walletSvc.AddToWallet(ctx, walletID, domain.Currency(amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}) if err != nil { s.logger.Error("Failed to update wallet", "walletID", walletID, "userID", session.UserID, "amount", amount, "error", err) return err @@ -148,6 +169,8 @@ func (s *service) HandleCallback(ctx context.Context, callback *domain.PopOKCall func (s *service) GetPlayerInfo(ctx context.Context, req *domain.PopOKPlayerInfoRequest) (*domain.PopOKPlayerInfoResponse, error) { claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey) + fmt.Printf("\n\nClaims: %+v\n\n", claims) + fmt.Printf("\n\nExternal token: %+v\n\n", req.ExternalToken) if err != nil { s.logger.Error("Failed to parse JWT", "error", err) return nil, fmt.Errorf("invalid token") @@ -184,15 +207,18 @@ func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) ( return &domain.PopOKBetResponse{}, fmt.Errorf("Failed to read user wallets") } - if err := s.walletSvc.DeductFromWallet(ctx, claims.UserID, domain.Currency(amountCents)); err != nil { + if _, err := s.walletSvc.DeductFromWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.CustomerWalletType, domain.ValidInt64{}, domain.TRANSFER_DIRECT); err != nil { return nil, fmt.Errorf("insufficient balance") } // 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", @@ -219,6 +245,8 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) ( return nil, fmt.Errorf("invalid token") } + fmt.Printf("\n\nClaims: %+v\n\n", claims) + // 2. Check for duplicate transaction (idempotency) existingTx, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID) if err != nil { @@ -245,7 +273,7 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) ( // 4. Credit to wallet - if err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents)); err != nil { + 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", "userID", claims.UserID, "error", err) return nil, fmt.Errorf("wallet credit failed") } @@ -257,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, @@ -277,14 +308,175 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) ( }, nil } -func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequest) (*domain.PopOKCancelResponse, error) { +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 cancel request", "error", err) + 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) + // if err != nil { + // s.logger.Error("Invalid token in cancel request", "error", err) + // return nil, fmt.Errorf("invalid token") + // } + // 2. Find the original bet transaction originalBet, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID) if err != nil { @@ -316,7 +508,7 @@ func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequ // 5. Refund the bet amount (absolute value since bet amount is negative) refundAmount := -originalBet.Amount - if err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(refundAmount)); err != nil { + if _, err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(refundAmount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil { s.logger.Error("Failed to refund bet", "userID", claims.UserID, "error", err) return nil, fmt.Errorf("refund failed") } @@ -399,3 +591,171 @@ func (s *service) verifySignature(callback *domain.PopOKCallback) bool { func (s *service) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) { return s.repo.GetGameCounts(ctx, filter) } + +func (s *service) ListGames(ctx context.Context, currency string) ([]domain.PopOKGame, error) { + now := time.Now().Format("02-01-2006 15:04:05") // dd-mm-yyyy hh:mm:ss + + // Calculate hash: sha256(privateKey + time) + rawHash := s.config.PopOK.SecretKey + now + hash := fmt.Sprintf("%x", sha256.Sum256([]byte(rawHash))) + + // Construct request payload + payload := map[string]interface{}{ + "action": "gameList", + "platform": s.config.PopOK.Platform, + "partnerId": s.config.PopOK.ClientID, + "currency": currency, + "time": now, + "hash": hash, + } + + bodyBytes, err := json.Marshal(payload) + if err != nil { + s.logger.Error("Failed to marshal game list request", "error", err) + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, "POST", s.config.PopOK.BaseURL+"/serviceApi.php", bytes.NewReader(bodyBytes)) + if err != nil { + s.logger.Error("Failed to create game list request", "error", err) + return nil, err + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + if err != nil { + s.logger.Error("Failed to send game list request", "error", err) + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + b, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("PopOK game list failed with status %d: %s", resp.StatusCode, string(b)) + } + + var gameList domain.PopOKGameListResponse + if err := json.NewDecoder(resp.Body).Decode(&gameList); err != nil { + s.logger.Error("Failed to decode game list response", "error", err) + return nil, err + } + + if gameList.Code != 0 { + return nil, fmt.Errorf("PopOK error: %s", gameList.Message) + } + + return gameList.Data.Slots, nil +} + +func (s *service) RecommendGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error) { + // Fetch all available games + games, err := s.ListGames(ctx, "ETB") + if err != nil || len(games) == 0 { + return nil, fmt.Errorf("could not fetch games") + } + + // Check if user has existing interaction + history, err := s.repo.GetUserGameHistory(ctx, userID) + if err != nil { + s.logger.Warn("No previous game history", "userID", userID) + } + + recommendations := []domain.GameRecommendation{} + + if len(history) > 0 { + // Score games based on interaction frequency + gameScores := map[int64]int{} + for _, h := range history { + if h.GameID != nil { + gameScores[*h.GameID]++ + } + } + + // Sort by score descending + sort.SliceStable(games, func(i, j int) bool { + return gameScores[int64(games[i].ID)] > gameScores[int64(games[j].ID)] + }) + + // Pick top 3 + for _, g := range games[:min(3, len(games))] { + recommendations = append(recommendations, domain.GameRecommendation{ + GameID: g.ID, + GameName: g.GameName, + Thumbnail: g.Thumbnail, + Bets: g.Bets, + Reason: "Based on your activity", + }) + } + } else { + // Pick 3 random games for new users + rand.Shuffle(len(games), func(i, j int) { + games[i], games[j] = games[j], games[i] + }) + + for _, g := range games[:min(3, len(games))] { + recommendations = append(recommendations, domain.GameRecommendation{ + GameID: g.ID, + GameName: g.GameName, + Thumbnail: g.Thumbnail, + Bets: g.Bets, + Reason: "Random pick", + }) + } + } + + return recommendations, nil +} + +func toInt64Ptr(s string) *int64 { + id, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return nil + } + 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/virtualGame/veli/client.go b/internal/services/virtualGame/veli/client.go new file mode 100644 index 0000000..756670d --- /dev/null +++ b/internal/services/virtualGame/veli/client.go @@ -0,0 +1,65 @@ +package veli + +import ( + "context" + "fmt" + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/config" + "github.com/go-resty/resty/v2" +) + +type VeliClient struct { + client *resty.Client + config *config.Config +} + +func NewVeliClient(cfg *config.Config) *VeliClient { + client := resty.New(). + SetBaseURL(cfg.VeliGames.APIKey). + SetHeader("Accept", "application/json"). + SetHeader("X-API-Key", cfg.VeliGames.APIKey). + SetTimeout(30 * time.Second) + + return &VeliClient{ + client: client, + config: cfg, + } +} + +func (vc *VeliClient) Get(ctx context.Context, endpoint string, result interface{}) error { + resp, err := vc.client.R(). + SetContext(ctx). + SetResult(result). + Get(endpoint) + + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + + if resp.IsError() { + return fmt.Errorf("API error: %s", resp.Status()) + } + + return nil +} + +func (vc *VeliClient) Post(ctx context.Context, endpoint string, body interface{}, result interface{}) error { + resp, err := vc.client.R(). + SetContext(ctx). + SetBody(body). + SetResult(result). + Post(endpoint) + + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + + if resp.IsError() { + return fmt.Errorf("API error: %s", resp.Status()) + } + + return nil +} + +// Add other HTTP methods as needed (Put, Delete, etc.) diff --git a/internal/services/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go index fc9097a..e6cc57f 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -1,158 +1,162 @@ package veli -import ( - "context" - "crypto/hmac" - "crypto/sha256" - "encoding/hex" - "errors" - "fmt" - "log/slog" - "net/url" - "time" +// import ( +// "context" +// "fmt" +// "log/slog" +// "time" - "github.com/SamuelTariku/FortuneBet-Backend/internal/config" - "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" -) +// "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +// "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" +// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" +// ) -type VeliPlayService struct { - repo repository.VirtualGameRepository - walletSvc wallet.Service - config *config.VeliGamesConfig - logger *slog.Logger -} +// type Service struct { +// client *VeliClient +// gameRepo repository.VeliGameRepository +// playerRepo repository.VeliPlayerRepository +// txRepo repository.VeliTransactionRepository +// walletSvc wallet.Service +// logger domain.Logger +// } -func NewVeliPlayService( - repo repository.VirtualGameRepository, - walletSvc wallet.Service, - cfg *config.Config, - logger *slog.Logger, -) *VeliPlayService { - return &VeliPlayService{ - repo: repo, - walletSvc: walletSvc, - config: &cfg.VeliGames, - logger: logger, - } -} +// func NewService( +// client *VeliClient, +// gameRepo repository.VeliGameRepository, +// playerRepo repository.VeliPlayerRepository, +// txRepo repository.VeliTransactionRepository, +// walletSvc wallet.Service, +// logger *slog.Logger, +// ) *Service { +// return &Service{ +// client: client, +// gameRepo: gameRepo, +// playerRepo: playerRepo, +// txRepo: txRepo, +// walletSvc: walletSvc, +// logger: logger, +// } +// } -func (s *VeliPlayService) GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error) { - session := &domain.VirtualGameSession{ - UserID: userID, - GameID: gameID, - SessionToken: generateSessionToken(userID), - Currency: currency, - Status: "ACTIVE", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - ExpiresAt: time.Now().Add(24 * time.Hour), - } +// func (s *Service) SyncGames(ctx context.Context) error { +// games, err := s.client.GetGameList(ctx) +// if err != nil { +// return fmt.Errorf("failed to get game list: %w", err) +// } - if err := s.repo.CreateVirtualGameSession(ctx, session); err != nil { - return "", fmt.Errorf("failed to create game session: %w", err) - } +// for _, game := range games { +// existing, err := s.gameRepo.GetGameByID(ctx, game.ID) +// if err != nil && err != domain.ErrGameNotFound { +// return fmt.Errorf("failed to check existing game: %w", err) +// } - // Veli-specific parameters - params := url.Values{ - "operator_key": []string{s.config.OperatorKey}, // Different from Alea's operator_id - "user_id": []string{fmt.Sprintf("%d", userID)}, - "game_id": []string{gameID}, - "currency": []string{currency}, - "mode": []string{mode}, - "timestamp": []string{fmt.Sprintf("%d", time.Now().Unix())}, - } +// if existing == nil { +// // New game - create +// if err := s.gameRepo.CreateGame(ctx, game); err != nil { +// s.logger.Error("failed to create game", "game_id", game.ID, "error", err) +// continue +// } +// } else { +// // Existing game - update +// if err := s.gameRepo.UpdateGame(ctx, game); err != nil { +// s.logger.Error("failed to update game", "game_id", game.ID, "error", err) +// continue +// } +// } +// } - signature := s.generateSignature(params.Encode()) - params.Add("signature", signature) +// return nil +// } - return fmt.Sprintf("%s/launch?%s", s.config.APIURL, params.Encode()), nil -} +// func (s *Service) LaunchGame(ctx context.Context, playerID, gameID string) (string, error) { +// // Verify player exists +// player, err := s.playerRepo.GetPlayer(ctx, playerID) +// if err != nil { +// return "", fmt.Errorf("failed to get player: %w", err) +// } -func (s *VeliPlayService) HandleCallback(ctx context.Context, callback *domain.VeliCallback) error { - if !s.verifyCallbackSignature(callback) { - return errors.New("invalid callback signature") - } +// // Verify game exists +// game, err := s.gameRepo.GetGameByID(ctx, gameID) +// if err != nil { +// return "", fmt.Errorf("failed to get game: %w", err) +// } - // Veli uses round_id instead of transaction_id for idempotency - existing, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, callback.RoundID) - if err != nil || existing != nil { - s.logger.Warn("duplicate round detected", "round_id", callback.RoundID) - return nil - } +// // Call Veli API +// gameURL, err := s.client.LaunchGame(ctx, playerID, gameID) +// if err != nil { +// return "", fmt.Errorf("failed to launch game: %w", err) +// } - session, err := s.repo.GetVirtualGameSessionByToken(ctx, callback.SessionID) - if err != nil { - return fmt.Errorf("failed to get game session: %w", err) - } +// // Create game session record +// session := domain.GameSession{ +// SessionID: fmt.Sprintf("%s-%s-%d", playerID, gameID, time.Now().Unix()), +// PlayerID: playerID, +// GameID: gameID, +// LaunchTime: time.Now(), +// Status: "active", +// } - // Convert amount based on event type (BET, WIN, etc.) - amount := convertAmount(callback.Amount, callback.EventType) +// if err := s.gameRepo.CreateGameSession(ctx, session); err != nil { +// s.logger.Error("failed to create game session", "error", err) +// } - tx := &domain.VirtualGameTransaction{ - SessionID: session.ID, - UserID: session.UserID, - TransactionType: callback.EventType, // e.g., "bet_placed", "game_result" - Amount: amount, - Currency: callback.Currency, - ExternalTransactionID: callback.RoundID, // Veli uses round_id as the unique identifier - Status: "COMPLETED", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - GameSpecificData: domain.GameSpecificData{ - Multiplier: callback.Multiplier, // Used for Aviator/Plinko - }, - } +// return gameURL, nil +// } - if err := s.processTransaction(ctx, tx, session.UserID); err != nil { - return fmt.Errorf("failed to process transaction: %w", err) - } +// func (s *Service) PlaceBet(ctx context.Context, playerID, gameID string, amount float64) (*domain.VeliTransaction, error) { +// // 1. Verify player balance +// balance, err := s.walletRepo.GetBalance(ctx, playerID) +// if err != nil { +// return nil, fmt.Errorf("failed to get balance: %w", err) +// } - return nil -} +// if balance < amount { +// return nil, domain.ErrInsufficientBalance +// } -func (s *VeliPlayService) generateSignature(data string) string { - h := hmac.New(sha256.New, []byte(s.config.SecretKey)) - h.Write([]byte(data)) - return hex.EncodeToString(h.Sum(nil)) -} +// // 2. Create transaction record +// tx := domain.VeliTransaction{ +// TransactionID: generateTransactionID(), +// PlayerID: playerID, +// GameID: gameID, +// Amount: amount, +// Type: "bet", +// Status: "pending", +// CreatedAt: time.Now(), +// } -func (s *VeliPlayService) verifyCallbackSignature(cb *domain.VeliCallback) bool { - signData := fmt.Sprintf("%s%s%s%.2f%s%d", - cb.RoundID, // Veli uses round_id instead of transaction_id - cb.SessionID, - cb.EventType, - cb.Amount, - cb.Currency, - cb.Timestamp, - ) - expectedSig := s.generateSignature(signData) - return expectedSig == cb.Signature -} +// if err := s.txRepo.CreateTransaction(ctx, tx); err != nil { +// return nil, fmt.Errorf("failed to create transaction: %w", err) +// } -func convertAmount(amount float64, eventType string) int64 { - cents := int64(amount * 100) - if eventType == "bet_placed" { - return -cents // Debit for bets - } - return cents // Credit for wins/results -} +// // 3. Call Veli API +// if err := s.client.PlaceBet(ctx, tx.TransactionID, playerID, gameID, amount); err != nil { +// // Update transaction status +// tx.Status = "failed" +// _ = s.txRepo.UpdateTransaction(ctx, tx) +// return nil, fmt.Errorf("failed to place bet: %w", err) +// } -func generateSessionToken(userID int64) string { - return fmt.Sprintf("veli-%d-%d", userID, time.Now().UnixNano()) -} +// // 4. Deduct from wallet +// if err := s.walletRepo.DeductBalance(ctx, playerID, amount); err != nil { +// // Attempt to rollback +// _ = s.client.RollbackBet(ctx, tx.TransactionID) +// tx.Status = "failed" +// _ = s.txRepo.UpdateTransaction(ctx, tx) +// return nil, fmt.Errorf("failed to deduct balance: %w", err) +// } -func (s *VeliPlayService) processTransaction(ctx context.Context, tx *domain.VirtualGameTransaction, userID int64) error { - wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID) - if err != nil || len(wallets) == 0 { - return errors.New("no wallet available for user") - } - tx.WalletID = wallets[0].ID +// // 5. Update transaction status +// tx.Status = "completed" +// if err := s.txRepo.UpdateTransaction(ctx, tx); err != nil { +// s.logger.Error("failed to update transaction status", "error", err) +// } - if err := s.walletSvc.AddToWallet(ctx, tx.WalletID, domain.Currency(tx.Amount)); err != nil { - return fmt.Errorf("wallet update failed: %w", err) - } +// return &tx, nil +// } - return s.repo.CreateVirtualGameTransaction(ctx, tx) -} +// // Implement SettleBet, RollbackBet, GetBalance, etc. following similar patterns + +// func generateTransactionID() string { +// return fmt.Sprintf("tx-%d", time.Now().UnixNano()) +// } diff --git a/internal/services/wallet/monitor/service.go b/internal/services/wallet/monitor/service.go index ea96534..4f48115 100644 --- a/internal/services/wallet/monitor/service.go +++ b/internal/services/wallet/monitor/service.go @@ -91,12 +91,11 @@ func (s *Service) checkWalletThresholds() { // Initialize initial deposit if not set s.mu.Lock() - if _, exists := s.initialDeposits[company.ID]; !exists { + initialDeposit, exists := s.initialDeposits[company.ID] + if !exists || wallet.Balance > initialDeposit { s.initialDeposits[company.ID] = wallet.Balance - s.mu.Unlock() - continue + initialDeposit = wallet.Balance // update local variable } - initialDeposit := s.initialDeposits[company.ID] s.mu.Unlock() if initialDeposit == 0 { diff --git a/internal/services/wallet/port.go b/internal/services/wallet/port.go index 145f38f..fb761cb 100644 --- a/internal/services/wallet/port.go +++ b/internal/services/wallet/port.go @@ -7,11 +7,14 @@ 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) GetAllWallets(ctx context.Context) ([]domain.Wallet, error) GetWalletsByUser(ctx context.Context, id int64) ([]domain.Wallet, error) + GetAllCustomerWallets(ctx context.Context) ([]domain.GetCustomerWallet, error) GetCustomerWallet(ctx context.Context, customerID int64) (domain.GetCustomerWallet, error) GetAllBranchWallets(ctx context.Context) ([]domain.BranchWallet, error) UpdateBalance(ctx context.Context, id int64, balance domain.Currency) 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/transfer.go b/internal/services/wallet/transfer.go index b4d5c67..b9e269e 100644 --- a/internal/services/wallet/transfer.go +++ b/internal/services/wallet/transfer.go @@ -14,85 +14,7 @@ var ( ) func (s *Service) CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) { - senderWallet, err := s.walletStore.GetWalletByID(ctx, transfer.SenderWalletID) - receiverWallet, err := s.walletStore.GetWalletByID(ctx, transfer.ReceiverWalletID) - if err != nil { - return domain.Transfer{}, fmt.Errorf("failed to get sender wallet: %w", err) - } - - // Check if wallet has sufficient balance - if senderWallet.Balance < transfer.Amount || senderWallet.Balance == 0 { - // Send notification to customer - customerNotification := &domain.Notification{ - RecipientID: receiverWallet.UserID, - Type: domain.NOTIFICATION_TYPE_TRANSFER, - Level: domain.NotificationLevelError, - Reciever: domain.NotificationRecieverSideCustomer, - DeliveryChannel: domain.DeliveryChannelInApp, - Payload: domain.NotificationPayload{ - Headline: "Service Temporarily Unavailable", - Message: "Our payment system is currently under maintenance. Please try again later.", - }, - Priority: 2, - Metadata: []byte(fmt.Sprintf(`{ - "transfer_amount": %d, - "current_balance": %d, - "wallet_id": %d, - "notification_type": "customer_facing" - }`, transfer.Amount, senderWallet.Balance, transfer.SenderWalletID)), - } - - // Send notification to admin team - adminNotification := &domain.Notification{ - RecipientID: senderWallet.UserID, - Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT, - Level: domain.NotificationLevelError, - Reciever: domain.NotificationRecieverSideAdmin, - DeliveryChannel: domain.DeliveryChannelEmail, // Or any preferred admin channel - Payload: domain.NotificationPayload{ - Headline: "CREDIT WARNING: System Running Out of Funds", - Message: fmt.Sprintf( - "Wallet ID %d has insufficient balance for transfer. Current balance: %.2f, Attempted transfer: %.2f", - transfer.SenderWalletID, - float64(senderWallet.Balance)/100, - float64(transfer.Amount)/100, - ), - }, - Priority: 1, // High priority for admin alerts - Metadata: fmt.Appendf(nil, `{ - "wallet_id": %d, - "balance": %d, - "required_amount": %d, - "notification_type": "admin_alert" - }`, transfer.SenderWalletID, senderWallet.Balance, transfer.Amount), - } - - // Send both notifications - if err := s.notificationStore.SendNotification(ctx, customerNotification); err != nil { - s.logger.Error("failed to send customer notification", - "user_id", "", - "error", err) - } - - // Get admin recipients and send to all - adminRecipients, err := s.notificationStore.ListRecipientIDs(ctx, domain.NotificationRecieverSideAdmin) - if err != nil { - s.logger.Error("failed to get admin recipients", "error", err) - } else { - for _, adminID := range adminRecipients { - adminNotification.RecipientID = adminID - if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil { - s.logger.Error("failed to send admin notification", - "admin_id", adminID, - "error", err) - } - } - } - - return domain.Transfer{}, ErrInsufficientBalance - } - - // Proceed with transfer if balance is sufficient + // This is just a transfer log when return s.transferStore.CreateTransfer(ctx, transfer) } @@ -120,43 +42,10 @@ func (s *Service) UpdateTransferStatus(ctx context.Context, id int64, status str return s.transferStore.UpdateTransferStatus(ctx, id, status) } -func (s *Service) RefillWallet(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) { - receiverWallet, err := s.GetWalletByID(ctx, transfer.ReceiverWalletID) - if err != nil { - return domain.Transfer{}, err - } - // Add to receiver - senderWallet, err := s.GetWalletByID(ctx, transfer.SenderWalletID) - if err != nil { - return domain.Transfer{}, err - } else if senderWallet.Balance < transfer.Amount { - return domain.Transfer{}, ErrInsufficientBalance - } - - err = s.walletStore.UpdateBalance(ctx, receiverWallet.ID, receiverWallet.Balance+transfer.Amount) - if err != nil { - return domain.Transfer{}, err - } - - // Log the transfer so that if there is a mistake, it can be reverted - newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ - CashierID: transfer.CashierID, - ReceiverWalletID: receiverWallet.ID, - Amount: transfer.Amount, - Type: domain.DEPOSIT, - PaymentMethod: transfer.PaymentMethod, - Verified: true, - }) - if err != nil { - return domain.Transfer{}, err - } - - return newTransfer, nil - -} - -func (s *Service) TransferToWallet(ctx context.Context, senderID int64, receiverID int64, amount domain.Currency, paymentMethod domain.PaymentMethod, cashierID domain.ValidInt64) (domain.Transfer, error) { +func (s *Service) TransferToWallet(ctx context.Context, senderID int64, receiverID int64, + amount domain.Currency, paymentMethod domain.PaymentMethod, + cashierID domain.ValidInt64) (domain.Transfer, error) { senderWallet, err := s.GetWalletByID(ctx, senderID) if err != nil { @@ -195,14 +84,20 @@ func (s *Service) TransferToWallet(ctx context.Context, senderID int64, receiver } // Log the transfer so that if there is a mistake, it can be reverted - transfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ - SenderWalletID: senderID, - CashierID: cashierID, - ReceiverWalletID: receiverID, - Amount: amount, - Type: domain.WALLET, - PaymentMethod: paymentMethod, - Verified: true, + transfer, err := s.CreateTransfer(ctx, domain.CreateTransfer{ + SenderWalletID: domain.ValidInt64{ + Value: senderID, + Valid: true, + }, + ReceiverWalletID: domain.ValidInt64{ + Value: receiverID, + Valid: true, + }, + CashierID: cashierID, + Amount: amount, + Type: domain.WALLET, + PaymentMethod: paymentMethod, + Verified: true, }) if err != nil { return domain.Transfer{}, err @@ -210,3 +105,66 @@ func (s *Service) TransferToWallet(ctx context.Context, senderID int64, receiver return transfer, nil } + +func (s *Service) SendTransferNotification(ctx context.Context, senderWallet domain.Wallet, receiverWallet domain.Wallet, + senderRole domain.Role, receiverRole domain.Role, amount domain.Currency) error { + // Send notification to sender (this could be any role) that money was transferred + senderWalletReceiverSide := domain.ReceiverFromRole(senderRole) + + senderNotify := &domain.Notification{ + RecipientID: senderWallet.UserID, + Type: domain.NOTIFICATION_TYPE_TRANSFER_SUCCESS, + Level: domain.NotificationLevelSuccess, + Reciever: senderWalletReceiverSide, + DeliveryChannel: domain.DeliveryChannelInApp, + Payload: domain.NotificationPayload{ + Headline: "Wallet has been deducted", + Message: fmt.Sprintf(`%s %d has been transferred from your wallet`,senderWallet.Currency, amount), + }, + Priority: 2, + Metadata: []byte(fmt.Sprintf(`{ + "transfer_amount": %d, + "current_balance": %d, + "wallet_id": %d, + "notification_type": "customer_facing" + }`, amount, senderWallet.Balance, senderWallet.ID)), + } + + // Sender notifications + if err := s.notificationStore.SendNotification(ctx, senderNotify); err != nil { + s.logger.Error("failed to send sender notification", + "user_id", "", + "error", err) + return err + } + + receiverWalletReceiverSide := domain.ReceiverFromRole(receiverRole) + + receiverNotify := &domain.Notification{ + RecipientID: receiverWallet.UserID, + Type: domain.NOTIFICATION_TYPE_TRANSFER_SUCCESS, + Level: domain.NotificationLevelSuccess, + Reciever: receiverWalletReceiverSide, + DeliveryChannel: domain.DeliveryChannelInApp, + Payload: domain.NotificationPayload{ + Headline: "Wallet has been credited", + Message: fmt.Sprintf(`%s %d has been transferred to your wallet`,receiverWallet.Currency, amount), + }, + Priority: 2, + Metadata: []byte(fmt.Sprintf(`{ + "transfer_amount": %d, + "current_balance": %d, + "wallet_id": %d, + "notification_type": "customer_facing" + }`, amount, receiverWallet.Balance, receiverWallet.ID)), + } + // Sender notifications + if err := s.notificationStore.SendNotification(ctx, receiverNotify); err != nil { + s.logger.Error("failed to send sender notification", + "user_id", "", + "error", err) + return err + } + + return nil +} diff --git a/internal/services/wallet/wallet.go b/internal/services/wallet/wallet.go index cf9cd4c..eab19f1 100644 --- a/internal/services/wallet/wallet.go +++ b/internal/services/wallet/wallet.go @@ -3,6 +3,7 @@ package wallet import ( "context" "errors" + "fmt" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) @@ -56,6 +57,9 @@ func (s *Service) GetWalletsByUser(ctx context.Context, id int64) ([]domain.Wall return s.walletStore.GetWalletsByUser(ctx, id) } +func (s *Service) GetAllCustomerWallet(ctx context.Context) ([]domain.GetCustomerWallet, error) { + return s.walletStore.GetAllCustomerWallets(ctx) +} func (s *Service) GetCustomerWallet(ctx context.Context, customerID int64) (domain.GetCustomerWallet, error) { return s.walletStore.GetCustomerWallet(ctx, customerID) } @@ -65,31 +69,170 @@ 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 + } -func (s *Service) AddToWallet(ctx context.Context, id int64, amount domain.Currency) error { wallet, err := s.GetWalletByID(ctx, id) if err != nil { return err } - return s.walletStore.UpdateBalance(ctx, id, wallet.Balance+amount) + go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet) + return nil } -func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.Currency) error { +func (s *Service) AddToWallet( + 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 err + return domain.Transfer{}, err + } + + err = s.walletStore.UpdateBalance(ctx, id, wallet.Balance+amount) + if err != nil { + 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, + Verified: true, + ReceiverWalletID: domain.ValidInt64{ + Value: wallet.ID, + Valid: true, + }, + CashierID: cashierID, + PaymentMethod: paymentMethod, + Type: domain.DEPOSIT, + ReferenceNumber: paymentDetails.ReferenceNumber.Value, + }) + + return newTransfer, err +} + +func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.Currency, walletType domain.WalletType, cashierID domain.ValidInt64, paymentMethod domain.PaymentMethod) (domain.Transfer, error) { + wallet, err := s.GetWalletByID(ctx, id) + if err != nil { + return domain.Transfer{}, err } if wallet.Balance < amount { - return ErrBalanceInsufficient + // Send Wallet low to admin + if walletType == domain.CompanyWalletType || walletType == domain.BranchWalletType { + s.SendAdminWalletLowNotification(ctx, wallet, amount) + } + return domain.Transfer{}, ErrBalanceInsufficient } - return s.walletStore.UpdateBalance(ctx, id, wallet.Balance-amount) + err = s.walletStore.UpdateBalance(ctx, id, wallet.Balance-amount) + + if err != nil { + 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, + Verified: true, + SenderWalletID: domain.ValidInt64{ + Value: wallet.ID, + Valid: true, + }, + CashierID: cashierID, + PaymentMethod: paymentMethod, + Type: domain.WITHDRAW, + ReferenceNumber: "", + }) + + return newTransfer, err } +// Directly Refilling wallet without +// func (s *Service) RefillWallet(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) { +// receiverWallet, err := s.GetWalletByID(ctx, transfer.ReceiverWalletID) +// if err != nil { +// return domain.Transfer{}, err +// } + +// // Add to receiver +// senderWallet, err := s.GetWalletByID(ctx, transfer.SenderWalletID) +// if err != nil { +// return domain.Transfer{}, err +// } else if senderWallet.Balance < transfer.Amount { +// return domain.Transfer{}, ErrInsufficientBalance +// } + +// err = s.walletStore.UpdateBalance(ctx, receiverWallet.ID, receiverWallet.Balance+transfer.Amount) +// if err != nil { +// return domain.Transfer{}, err +// } + +// // Log the transfer so that if there is a mistake, it can be reverted +// newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ +// CashierID: transfer.CashierID, +// ReceiverWalletID: transfer.ReceiverWalletID, +// Amount: transfer.Amount, +// Type: domain.DEPOSIT, +// PaymentMethod: transfer.PaymentMethod, +// Verified: true, +// }) +// if err != nil { +// return domain.Transfer{}, err +// } + +// return newTransfer, nil +// } + func (s *Service) UpdateWalletActive(ctx context.Context, id int64, isActive bool) error { return s.walletStore.UpdateWalletActive(ctx, id, isActive) } + +func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWallet domain.Wallet, amount domain.Currency) error { + // Send notification to admin team + adminNotification := &domain.Notification{ + RecipientID: adminWallet.UserID, + Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT, + Level: domain.NotificationLevelError, + Reciever: domain.NotificationRecieverSideAdmin, + DeliveryChannel: domain.DeliveryChannelEmail, // Or any preferred admin channel + Payload: domain.NotificationPayload{ + Headline: "CREDIT WARNING: System Running Out of Funds", + Message: fmt.Sprintf( + "Wallet ID %d has insufficient balance for transfer. Current balance: %.2f, Attempted transfer: %.2f", + adminWallet.ID, + adminWallet.Balance.Float32(), + amount.Float32(), + ), + }, + Priority: 1, // High priority for admin alerts + Metadata: fmt.Appendf(nil, `{ + "wallet_id": %d, + "balance": %d, + "required_amount": %d, + "notification_type": "admin_alert" + }`, adminWallet.ID, adminWallet.Balance, amount), + } + + // Get admin recipients and send to all + adminRecipients, err := s.notificationStore.ListRecipientIDs(ctx, domain.NotificationRecieverSideAdmin) + if err != nil { + s.logger.Error("failed to get admin recipients", "error", err) + return err + } else { + for _, adminID := range adminRecipients { + adminNotification.RecipientID = adminID + if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil { + s.logger.Error("failed to send admin notification", + "admin_id", adminID, + "error", err) + } + } + } + return nil +} diff --git a/internal/web_server/app.go b/internal/web_server/app.go index d9ef3a2..61bc682 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -12,12 +12,15 @@ 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" referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/report" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" @@ -27,6 +30,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" + "go.uber.org/zap" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" "github.com/bytedance/sonic" @@ -35,6 +39,8 @@ import ( ) type App struct { + issueReportingSvc *issuereporting.Service + instSvc *institutions.Service currSvc *currency.Service fiber *fiber.App aleaVirtualGameService alea.AleaVirtualGameService @@ -45,6 +51,7 @@ type App struct { NotidicationStore *notificationservice.Service referralSvc referralservice.ReferralStore port int + settingSvc *settings.Service authSvc *authentication.Service userSvc *user.Service betSvc *bet.Service @@ -63,11 +70,15 @@ type App struct { eventSvc event.Service leagueSvc league.Service resultSvc *result.Service + mongoLoggerSvc *zap.Logger } func NewApp( + issueReportingSvc *issuereporting.Service, + instSvc *institutions.Service, currSvc *currency.Service, port int, validator *customvalidator.CustomValidator, + settingSvc *settings.Service, authSvc *authentication.Service, logger *slog.Logger, JwtConfig jwtutil.JwtConfig, @@ -87,10 +98,11 @@ func NewApp( referralSvc referralservice.ReferralStore, virtualGameSvc virtualgameservice.VirtualGameService, aleaVirtualGameService alea.AleaVirtualGameService, - veliVirtualGameService veli.VeliVirtualGameService, + // veliVirtualGameService veli.VeliVirtualGameService, recommendationSvc recommendation.RecommendationService, resultSvc *result.Service, cfg *config.Config, + mongoLoggerSvc *zap.Logger, ) *App { app := fiber.New(fiber.Config{ CaseSensitive: true, @@ -107,9 +119,13 @@ func NewApp( })) s := &App{ - currSvc: currSvc, - fiber: app, - port: port, + issueReportingSvc: issueReportingSvc, + instSvc: instSvc, + currSvc: currSvc, + fiber: app, + port: port, + + settingSvc: settingSvc, authSvc: authSvc, validator: validator, logger: logger, @@ -131,10 +147,11 @@ func NewApp( leagueSvc: leagueSvc, virtualGameSvc: virtualGameSvc, aleaVirtualGameService: aleaVirtualGameService, - veliVirtualGameService: veliVirtualGameService, - recommendationSvc: recommendationSvc, - resultSvc: resultSvc, - cfg: cfg, + // veliVirtualGameService: veliVirtualGameService, + recommendationSvc: recommendationSvc, + resultSvc: resultSvc, + cfg: cfg, + mongoLoggerSvc: mongoLoggerSvc, } s.initAppRoutes() diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 5c8a104..58e9e24 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -8,37 +8,14 @@ import ( // "time" - "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/report" resultsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" - "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/worker" - "github.com/go-co-op/gocron" "github.com/robfig/cron/v3" ) -func SetupReportCronJob(reportWorker *worker.ReportWorker) { - s := gocron.NewScheduler(time.UTC) - - // Daily at midnight - _, _ = s.Every(1).Day().At("00:00").Do(func() { - _ = reportWorker.GenerateAndExport(domain.Daily) - }) - - // Weekly on Sunday at 00:05 - _, _ = s.Every(1).Week().Sunday().At("00:05").Do(func() { - _ = reportWorker.GenerateAndExport(domain.Weekly) - }) - - // Monthly on 1st at 00:10 - _, _ = s.Every(1).Month(1).At("00:10").Do(func() { - _ = reportWorker.GenerateAndExport(domain.Monthly) - }) - - s.StartAsync() -} - func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.ServiceImpl, resultService *resultsvc.Service) { c := cron.New(cron.WithSeconds()) @@ -55,7 +32,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S }, }, { - spec: "0 0 * * * *", // Every 15 minutes + spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events) task: func() { if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { log.Printf("FetchNonLiveOdds error: %v", err) @@ -128,3 +105,46 @@ func StartTicketCrons(ticketService ticket.Service) { c.Start() log.Println("Cron jobs started for ticket service") } + +func SetupReportCronJobs(ctx context.Context, reportService *report.Service) { + c := cron.New(cron.WithSeconds()) // use WithSeconds for tighter intervals during testing + + schedule := []struct { + spec string + period string + }{ + { + spec: "*/300 * * * * *", // Every 5 minutes (300 seconds) + period: "5min", + }, + { + spec: "0 0 0 * * *", // Daily at midnight + period: "daily", + }, + { + spec: "0 0 1 * * 0", // Weekly: Sunday at 1 AM + period: "weekly", + }, + { + spec: "0 0 2 1 * *", // Monthly: 1st day of month at 2 AM + period: "monthly", + }, + } + + for _, job := range schedule { + period := job.period + if _, err := c.AddFunc(job.spec, func() { + log.Printf("Running %s report at %s", period, time.Now().Format(time.RFC3339)) + if err := reportService.GenerateReport(ctx, period); err != nil { + log.Printf("Error generating %s report: %v", period, err) + } else { + log.Printf("Successfully generated %s report", period) + } + }); err != nil { + log.Fatalf("Failed to schedule %s report cron job: %v", period, err) + } + } + + c.Start() + log.Println("Cron jobs started for report generation service") +} diff --git a/internal/web_server/handlers/admin.go b/internal/web_server/handlers/admin.go index 795a61f..8e282f1 100644 --- a/internal/web_server/handlers/admin.go +++ b/internal/web_server/handlers/admin.go @@ -1,7 +1,6 @@ package handlers import ( - "log/slog" "strconv" "time" @@ -10,6 +9,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" + "go.uber.org/zap" ) type CreateAdminReq struct { @@ -38,15 +38,24 @@ func (h *Handler) CreateAdmin(c *fiber.Ctx) error { var req CreateAdminReq if err := c.BodyParser(&req); err != nil { - h.logger.Error("RegisterUser failed", "error", err) + h.mongoLoggerSvc.Error("failed to parse CreateAdmin request", + zap.Int64("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } + valErrs, ok := h.validator.Validate(c, req) if !ok { + h.mongoLoggerSvc.Error("validation failed for CreateAdmin request", + zap.Int64("status_code", fiber.StatusBadRequest), + zap.Any("validation_errors", valErrs), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } - // Admins can be created without company ids and can be assigned later if req.CompanyID == nil { companyID = domain.ValidInt64{ Value: 0, @@ -55,7 +64,12 @@ func (h *Handler) CreateAdmin(c *fiber.Ctx) error { } else { _, err := h.companySvc.GetCompanyByID(c.Context(), *req.CompanyID) if err != nil { - h.logger.Error("CreateAdmin company id is invalid", "error", err) + h.mongoLoggerSvc.Error("invalid company ID for CreateAdmin", + zap.Int64("status_code", fiber.StatusInternalServerError), + zap.Int64("company_id", *req.CompanyID), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusInternalServerError, "Company ID is invalid", nil, nil) } companyID = domain.ValidInt64{ @@ -74,10 +88,14 @@ func (h *Handler) CreateAdmin(c *fiber.Ctx) error { CompanyID: companyID, } - h.logger.Info("CreateAdmin", slog.Bool("company id", req.CompanyID == nil)) newUser, err := h.userSvc.CreateUser(c.Context(), user, true) if err != nil { - h.logger.Error("CreateAdmin failed", "error", err) + h.mongoLoggerSvc.Error("failed to create admin user", + zap.Int64("status_code", fiber.StatusInternalServerError), + zap.Any("request", req), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to create admin", nil, nil) } @@ -87,11 +105,23 @@ func (h *Handler) CreateAdmin(c *fiber.Ctx) error { AdminID: &newUser.ID, }) if err != nil { - h.logger.Error("CreateAdmin failed to update company", "error", err) + h.mongoLoggerSvc.Error("failed to update company with new admin", + zap.Int64("status_code", fiber.StatusInternalServerError), + zap.Int64("company_id", *req.CompanyID), + zap.Int64("admin_id", newUser.ID), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update company", nil, nil) } } + h.mongoLoggerSvc.Info("admin created successfully", + zap.Int64("admin_id", newUser.ID), + zap.String("email", newUser.Email), + zap.Time("timestamp", time.Now()), + ) + return response.WriteJSON(c, fiber.StatusOK, "Admin created successfully", nil, nil) } @@ -125,7 +155,6 @@ type AdminRes struct { // @Failure 500 {object} response.APIResponse // @Router /admin [get] func (h *Handler) GetAllAdmins(c *fiber.Ctx) error { - filter := user.Filter{ Role: string(domain.RoleAdmin), CompanyID: domain.ValidInt64{ @@ -141,27 +170,45 @@ func (h *Handler) GetAllAdmins(c *fiber.Ctx) error { Valid: true, }, } + valErrs, ok := h.validator.Validate(c, filter) if !ok { + h.mongoLoggerSvc.Error("invalid filter values in GetAllAdmins request", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Any("validation_errors", valErrs), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } + admins, total, err := h.userSvc.GetAllUsers(c.Context(), filter) if err != nil { - h.logger.Error("GetAllAdmins failed", "error", err) + h.mongoLoggerSvc.Error("failed to get admins from user service", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Any("filter", filter), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get Admins", nil, nil) } - var result []AdminRes = make([]AdminRes, len(admins)) + result := make([]AdminRes, len(admins)) for index, admin := range admins { lastLogin, err := h.authSvc.GetLastLogin(c.Context(), admin.ID) if err != nil { if err == authentication.ErrRefreshTokenNotFound { lastLogin = &admin.CreatedAt } else { - h.logger.Error("Failed to get user last login", "userID", admin.ID, "error", err) + h.mongoLoggerSvc.Error("failed to get last login for admin", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Int64("admin_id", admin.ID), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login") } } + result[index] = AdminRes{ ID: admin.ID, FirstName: admin.FirstName, @@ -179,6 +226,13 @@ func (h *Handler) GetAllAdmins(c *fiber.Ctx) error { } } + h.mongoLoggerSvc.Info("admins retrieved successfully", + zap.Int("status_code", fiber.StatusOK), + zap.Int("count", len(result)), + zap.Int("page", filter.Page.Value+1), + zap.Time("timestamp", time.Now()), + ) + return response.WritePaginatedJSON(c, fiber.StatusOK, "Admins retrieved successfully", result, nil, filter.Page.Value, int(total)) } @@ -195,41 +249,40 @@ func (h *Handler) GetAllAdmins(c *fiber.Ctx) error { // @Failure 500 {object} response.APIResponse // @Router /admin/{id} [get] func (h *Handler) GetAdminByID(c *fiber.Ctx) error { - // branchId := int64(12) //c.Locals("branch_id").(int64) - // filter := user.Filter{ - // Role: string(domain.RoleUser), - // BranchId: user.ValidBranchId{ - // Value: branchId, - // Valid: true, - // }, - // Page: c.QueryInt("page", 1), - // PageSize: c.QueryInt("page_size", 10), - // } - // valErrs, ok := validator.Validate(c, filter) - // if !ok { - // return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - // } - userIDstr := c.Params("id") userID, err := strconv.ParseInt(userIDstr, 10, 64) if err != nil { - h.logger.Error("failed to fetch user using UserID", "error", err) + h.mongoLoggerSvc.Error("invalid admin ID param", + zap.Int("status_code", fiber.StatusBadRequest), + zap.String("param", userIDstr), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid admin ID", nil, nil) } user, err := h.userSvc.GetUserByID(c.Context(), userID) if err != nil { - h.logger.Error("Get User By ID failed", "error", err) + h.mongoLoggerSvc.Error("failed to fetch admin by ID", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Int64("admin_id", userID), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get admin", nil, nil) } lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID) - if err != nil { - if err != authentication.ErrRefreshTokenNotFound { - h.logger.Error("Failed to get user last login", "userID", user.ID, "error", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login") - } - + if err != nil && err != authentication.ErrRefreshTokenNotFound { + h.mongoLoggerSvc.Error("failed to get admin last login", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Int64("admin_id", user.ID), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login") + } + if err == authentication.ErrRefreshTokenNotFound { lastLogin = &user.CreatedAt } @@ -249,7 +302,13 @@ func (h *Handler) GetAdminByID(c *fiber.Ctx) error { LastLogin: *lastLogin, } - return response.WriteJSON(c, fiber.StatusOK, "User retrieved successfully", res, nil) + h.mongoLoggerSvc.Info("admin retrieved successfully", + zap.Int("status_code", fiber.StatusOK), + zap.Int64("admin_id", user.ID), + zap.Time("timestamp", time.Now()), + ) + + return response.WriteJSON(c, fiber.StatusOK, "Admin retrieved successfully", res, nil) } type updateAdminReq struct { @@ -274,21 +333,36 @@ type updateAdminReq struct { func (h *Handler) UpdateAdmin(c *fiber.Ctx) error { var req updateAdminReq if err := c.BodyParser(&req); err != nil { - h.logger.Error("UpdateAdmin failed", "error", err) + h.mongoLoggerSvc.Error("UpdateAdmin failed - invalid request body", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil) } valErrs, ok := h.validator.Validate(c, req) - if !ok { + h.mongoLoggerSvc.Error("UpdateAdmin failed - validation errors", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Any("validation_errors", valErrs), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } + AdminIDStr := c.Params("id") AdminID, err := strconv.ParseInt(AdminIDStr, 10, 64) if err != nil { - h.logger.Error("UpdateAdmin failed", "error", err) + h.mongoLoggerSvc.Error("UpdateAdmin failed - invalid Admin ID param", + zap.Int("status_code", fiber.StatusBadRequest), + zap.String("admin_id_param", AdminIDStr), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid Admin ID", nil, nil) } + var companyID domain.ValidInt64 if req.CompanyID != nil { companyID = domain.ValidInt64{ @@ -296,6 +370,7 @@ func (h *Handler) UpdateAdmin(c *fiber.Ctx) error { Valid: true, } } + err = h.userSvc.UpdateUser(c.Context(), domain.UpdateUserReq{ UserId: AdminID, FirstName: domain.ValidString{ @@ -311,23 +386,38 @@ func (h *Handler) UpdateAdmin(c *fiber.Ctx) error { Valid: true, }, CompanyID: companyID, - }, - ) + }) if err != nil { - h.logger.Error("UpdateAdmin failed", "error", err) + h.mongoLoggerSvc.Error("UpdateAdmin failed - user service error", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Int64("admin_id", AdminID), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update admin", nil, nil) } + if req.CompanyID != nil { _, err := h.companySvc.UpdateCompany(c.Context(), domain.UpdateCompany{ ID: *req.CompanyID, AdminID: &AdminID, }) if err != nil { - h.logger.Error("CreateAdmin failed to update company", "error", err) + h.mongoLoggerSvc.Error("UpdateAdmin failed to update company", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Int64("admin_id", AdminID), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update company", nil, nil) } } - return response.WriteJSON(c, fiber.StatusOK, "Managers updated successfully", nil, nil) + h.mongoLoggerSvc.Info("UpdateAdmin succeeded", + zap.Int("status_code", fiber.StatusOK), + zap.Int64("admin_id", AdminID), + zap.Time("timestamp", time.Now()), + ) + return response.WriteJSON(c, fiber.StatusOK, "Managers updated successfully", nil, nil) } diff --git a/internal/web_server/handlers/auth_handler.go b/internal/web_server/handlers/auth_handler.go index 1b3cc97..4368266 100644 --- a/internal/web_server/handlers/auth_handler.go +++ b/internal/web_server/handlers/auth_handler.go @@ -2,11 +2,13 @@ package handlers import ( "errors" + "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" + "go.uber.org/zap" ) // loginCustomerReq represents the request body for the LoginCustomer endpoint. @@ -36,34 +38,58 @@ type loginCustomerRes struct { // @Failure 500 {object} response.APIResponse // @Router /auth/login [post] func (h *Handler) LoginCustomer(c *fiber.Ctx) error { - var req loginCustomerReq if err := c.BodyParser(&req); err != nil { - h.logger.Error("Failed to parse LoginCustomer request", "error", err) + h.mongoLoggerSvc.Error("Failed to parse LoginCustomer request", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } - if valErrs, ok := h.validator.Validate(c, req); !ok { - return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + if _, ok := h.validator.Validate(c, req); !ok { + h.mongoLoggerSvc.Error("LoginCustomer validation failed", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Any("request", req), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid Request") } successRes, err := h.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password) if err != nil { - h.logger.Info("Login attempt failed", "email", req.Email, "phone", req.PhoneNumber, "error", err) + h.mongoLoggerSvc.Info("Login attempt failed", + zap.Int("status_code", fiber.StatusUnauthorized), + zap.String("email", req.Email), + zap.String("phone", req.PhoneNumber), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + switch { case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound): return fiber.NewError(fiber.StatusUnauthorized, "Invalid credentials") case errors.Is(err, authentication.ErrUserSuspended): return fiber.NewError(fiber.StatusUnauthorized, "User login has been locked") default: - h.logger.Error("Login failed", "error", err) + h.mongoLoggerSvc.Error("Login failed", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return fiber.NewError(fiber.StatusInternalServerError, "Internal server error") } } accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, successRes.CompanyID, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry) if err != nil { - h.logger.Error("Failed to create access token", "userID", successRes.UserId, "error", err) + h.mongoLoggerSvc.Error("Failed to create access token", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Int64("user_id", successRes.UserId), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token") } @@ -72,6 +98,14 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error { RefreshToken: successRes.RfToken, Role: string(successRes.Role), } + + h.mongoLoggerSvc.Info("Login successful", + zap.Int("status_code", fiber.StatusOK), + zap.Int64("user_id", successRes.UserId), + zap.String("role", string(successRes.Role)), + zap.Time("timestamp", time.Now()), + ) + return response.WriteJSON(c, fiber.StatusOK, "Login successful", res, nil) } @@ -102,34 +136,65 @@ func (h *Handler) RefreshToken(c *fiber.Ctx) error { var req refreshToken if err := c.BodyParser(&req); err != nil { - h.logger.Error("Failed to parse RefreshToken request", "error", err) + h.mongoLoggerSvc.Error("Failed to parse RefreshToken request", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } if valErrs, ok := h.validator.Validate(c, req); !ok { + h.mongoLoggerSvc.Error("RefreshToken validation failed", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Any("validation_errors", valErrs), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } refreshToken, err := h.authSvc.RefreshToken(c.Context(), req.RefreshToken) if err != nil { - h.logger.Info("Refresh token attempt failed", "refreshToken", req.RefreshToken, "error", err) + h.mongoLoggerSvc.Info("Refresh token attempt failed", + zap.Int("status_code", fiber.StatusUnauthorized), + zap.String("refresh_token", req.RefreshToken), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) switch { case errors.Is(err, authentication.ErrExpiredToken): return fiber.NewError(fiber.StatusUnauthorized, "The refresh token has expired") case errors.Is(err, authentication.ErrRefreshTokenNotFound): return fiber.NewError(fiber.StatusUnauthorized, "Refresh token not found") default: - h.logger.Error("Refresh token failed", "error", err) + h.mongoLoggerSvc.Error("Refresh token failed", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return fiber.NewError(fiber.StatusInternalServerError, "Internal server error") } } user, err := h.userSvc.GetUserByID(c.Context(), refreshToken.UserID) + if err != nil { + h.mongoLoggerSvc.Error("Failed to get user by ID during refresh", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Int64("user_id", refreshToken.UserID), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user information") + } - // Assuming the refreshed token includes userID and role info; adjust if needed accessToken, err := jwtutil.CreateJwt(user.ID, user.Role, user.CompanyID, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry) if err != nil { - h.logger.Error("Failed to create new access token", "error", err) + h.mongoLoggerSvc.Error("Failed to create new access token", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Int64("user_id", user.ID), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token") } @@ -138,6 +203,14 @@ func (h *Handler) RefreshToken(c *fiber.Ctx) error { RefreshToken: req.RefreshToken, Role: string(user.Role), } + + h.mongoLoggerSvc.Info("Refresh token successful", + zap.Int("status_code", fiber.StatusOK), + zap.Int64("user_id", user.ID), + zap.String("role", string(user.Role)), + zap.Time("timestamp", time.Now()), + ) + return response.WriteJSON(c, fiber.StatusOK, "Refresh successful", res, nil) } @@ -158,30 +231,52 @@ type logoutReq struct { // @Failure 500 {object} response.APIResponse // @Router /auth/logout [post] func (h *Handler) LogOutCustomer(c *fiber.Ctx) error { - var req logoutReq if err := c.BodyParser(&req); err != nil { - h.logger.Error("Failed to parse LogOutCustomer request", "error", err) + h.mongoLoggerSvc.Error("Failed to parse LogOutCustomer request", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } if valErrs, ok := h.validator.Validate(c, req); !ok { + h.mongoLoggerSvc.Error("LogOutCustomer validation failed", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Any("validation_errors", valErrs), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } err := h.authSvc.Logout(c.Context(), req.RefreshToken) if err != nil { - h.logger.Info("Logout attempt failed", "refreshToken", req.RefreshToken, "error", err) + h.mongoLoggerSvc.Info("Logout attempt failed", + zap.Int("status_code", fiber.StatusUnauthorized), + zap.String("refresh_token", req.RefreshToken), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) switch { case errors.Is(err, authentication.ErrExpiredToken): return fiber.NewError(fiber.StatusUnauthorized, "The refresh token has expired") case errors.Is(err, authentication.ErrRefreshTokenNotFound): return fiber.NewError(fiber.StatusUnauthorized, "Refresh token not found") default: - h.logger.Error("Logout failed", "error", err) + h.mongoLoggerSvc.Error("Logout failed", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return fiber.NewError(fiber.StatusInternalServerError, "Internal server error") } } + h.mongoLoggerSvc.Info("Logout successful", + zap.Int("status_code", fiber.StatusOK), + zap.Time("timestamp", time.Now()), + ) + return response.WriteJSON(c, fiber.StatusOK, "Logout successful", nil, nil) } diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index a7a0706..77950e2 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -1,7 +1,6 @@ package handlers import ( - "fmt" "strconv" "time" @@ -10,6 +9,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" + "go.uber.org/zap" ) // CreateBet godoc @@ -22,38 +22,54 @@ 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 { - fmt.Printf("Calling leagues") - // Get user_id from middleware userID := c.Locals("user_id").(int64) role := c.Locals("role").(domain.Role) var req domain.CreateBetReq if err := c.BodyParser(&req); err != nil { - h.logger.Error("Failed to parse CreateBet request", "error", err) + h.mongoLoggerSvc.Error("Failed to parse CreateBet request", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } valErrs, ok := h.validator.Validate(c, req) if !ok { + h.mongoLoggerSvc.Error("CreateBet validation failed", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Any("validation_errors", valErrs), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } - res, err := h.betSvc. - PlaceBet(c.Context(), req, userID, role) - + res, err := h.betSvc.PlaceBet(c.Context(), req, userID, role) if err != nil { - h.logger.Error("PlaceBet failed", "error", err) + h.mongoLoggerSvc.Error("PlaceBet failed", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + switch err { case bet.ErrEventHasBeenRemoved, bet.ErrEventHasNotEnded, bet.ErrRawOddInvalid, wallet.ErrBalanceInsufficient: return fiber.NewError(fiber.StatusBadRequest, err.Error()) } + return fiber.NewError(fiber.StatusInternalServerError, "Unable to create bet") } - return response.WriteJSON(c, fiber.StatusOK, "Bet Created", res, nil) + h.mongoLoggerSvc.Info("Bet created successfully", + zap.Int("status_code", fiber.StatusOK), + zap.Int64("user_id", userID), + zap.Time("timestamp", time.Now()), + ) + return response.WriteJSON(c, fiber.StatusOK, "Bet Created", res, nil) } // RandomBet godoc @@ -66,22 +82,27 @@ 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 { - - // Get user_id from middleware userID := c.Locals("user_id").(int64) - // role := c.Locals("role").(domain.Role) leagueIDQuery, err := strconv.Atoi(c.Query("league_id")) if err != nil { - h.logger.Error("invalid league id", "error", err) + h.mongoLoggerSvc.Error("invalid league id", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, nil) } sportIDQuery, err := strconv.Atoi(c.Query("sport_id")) if err != nil { - h.logger.Error("invalid sport id", "error", err) + h.mongoLoggerSvc.Error("invalid sport id", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusBadRequest, "invalid sport id", nil, nil) } @@ -101,7 +122,11 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error { if firstStartTimeQuery != "" { firstStartTimeParsed, err := time.Parse(time.RFC3339, firstStartTimeQuery) if err != nil { - h.logger.Error("invalid start_time format", "error", err) + h.mongoLoggerSvc.Error("invalid start_time format", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil) } firstStartTime = domain.ValidTime{ @@ -109,11 +134,16 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error { Valid: true, } } + var lastStartTime domain.ValidTime if lastStartTimeQuery != "" { lastStartTimeParsed, err := time.Parse(time.RFC3339, lastStartTimeQuery) if err != nil { - h.logger.Error("invalid start_time format", "error", err) + h.mongoLoggerSvc.Error("invalid start_time format", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil) } lastStartTime = domain.ValidTime{ @@ -124,21 +154,33 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error { var req domain.RandomBetReq if err := c.BodyParser(&req); err != nil { - h.logger.Error("Failed to parse RandomBet request", "error", err) + h.mongoLoggerSvc.Error("Failed to parse RandomBet request", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } valErrs, ok := h.validator.Validate(c, req) if !ok { + h.mongoLoggerSvc.Error("RandomBet validation failed", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Any("validation_errors", valErrs), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } var res domain.CreateBetRes for i := 0; i < int(req.NumberOfBets); i++ { res, err = h.betSvc.PlaceRandomBet(c.Context(), userID, req.BranchID, leagueID, sportID, firstStartTime, lastStartTime) - if err != nil { - h.logger.Error("Random Bet failed", "error", err) + h.mongoLoggerSvc.Error("Random Bet failed", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) switch err { case bet.ErrNoEventsAvailable: return fiber.NewError(fiber.StatusBadRequest, "No events found") @@ -146,8 +188,14 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Unable to create random bet") } } - return response.WriteJSON(c, fiber.StatusOK, "Bet Created", res, nil) + h.mongoLoggerSvc.Info("Random bet(s) created successfully", + zap.Int("status_code", fiber.StatusOK), + zap.Int64("user_id", userID), + zap.Time("timestamp", time.Now()), + ) + + return response.WriteJSON(c, fiber.StatusOK, "Bet Created", res, nil) } // GetAllBet godoc @@ -159,17 +207,78 @@ 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) branchID := c.Locals("branch_id").(domain.ValidInt64) + var isShopBet domain.ValidBool + isShopBetQuery := c.Query("is_shop") + if isShopBetQuery != "" && role == domain.RoleSuperAdmin { + isShopBetParse, err := strconv.ParseBool(isShopBetQuery) + if err != nil { + h.mongoLoggerSvc.Error("Failed to parse is_shop_bet", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Failed to parse is_shop_bet") + } + isShopBet = domain.ValidBool{ + Value: isShopBetParse, + Valid: true, + } + } + + searchQuery := c.Query("query") + searchString := domain.ValidString{ + Value: searchQuery, + Valid: searchQuery != "", + } + + createdBeforeQuery := c.Query("created_before") + var createdBefore domain.ValidTime + if createdBeforeQuery != "" { + createdBeforeParsed, err := time.Parse(time.RFC3339, createdBeforeQuery) + if err != nil { + h.logger.Error("invalid start_time format", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil) + } + createdBefore = domain.ValidTime{ + Value: createdBeforeParsed, + Valid: true, + } + } + + createdAfterQuery := c.Query("created_after") + var createdAfter domain.ValidTime + if createdAfterQuery != "" { + createdAfterParsed, err := time.Parse(time.RFC3339, createdAfterQuery) + if err != nil { + h.logger.Error("invalid start_time format", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil) + } + createdAfter = domain.ValidTime{ + Value: createdAfterParsed, + Valid: true, + } + } + bets, err := h.betSvc.GetAllBets(c.Context(), domain.BetFilter{ - BranchID: branchID, - CompanyID: companyID, + BranchID: branchID, + CompanyID: companyID, + IsShopBet: isShopBet, + Query: searchString, + CreatedBefore: createdBefore, + CreatedAfter: createdAfter, }) if err != nil { - h.logger.Error("Failed to get bets", "error", err) + h.mongoLoggerSvc.Error("Failed to get bets", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve bets") } @@ -178,6 +287,11 @@ func (h *Handler) GetAllBet(c *fiber.Ctx) error { res[i] = domain.ConvertBet(bet) } + h.mongoLoggerSvc.Info("All bets retrieved successfully", + zap.Int("status_code", fiber.StatusOK), + zap.Time("timestamp", time.Now()), + ) + return response.WriteJSON(c, fiber.StatusOK, "All bets retrieved successfully", res, nil) } @@ -191,26 +305,40 @@ 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) if err != nil { - h.logger.Error("Invalid bet ID", "betID", betID, "error", err) + h.mongoLoggerSvc.Error("Invalid bet ID", + zap.String("betID", betID), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return fiber.NewError(fiber.StatusBadRequest, "Invalid bet ID") } bet, err := h.betSvc.GetBetByID(c.Context(), id) if err != nil { - // TODO: handle all the errors types - h.logger.Error("Failed to get bet by ID", "betID", id, "error", err) + h.mongoLoggerSvc.Error("Failed to get bet by ID", + zap.Int64("betID", id), + zap.Int("status_code", fiber.StatusNotFound), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return fiber.NewError(fiber.StatusNotFound, "Failed to retrieve bet") } res := domain.ConvertBet(bet) - return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil) + h.mongoLoggerSvc.Info("Bet retrieved successfully", + zap.Int64("betID", id), + zap.Int("status_code", fiber.StatusOK), + zap.Time("timestamp", time.Now()), + ) + return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil) } // GetBetByCashoutID godoc @@ -223,26 +351,30 @@ 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") - // id, err := strconv.ParseInt(cashoutID, 10, 64) - - // if err != nil { - // logger.Error("Invalid cashout ID", "cashoutID", cashoutID, "error", err) - // return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid cashout ID", err, nil) - // } bet, err := h.betSvc.GetBetByCashoutID(c.Context(), cashoutID) if err != nil { - h.logger.Error("Failed to get bet by ID", "cashoutID", cashoutID, "error", err) + h.mongoLoggerSvc.Error("Failed to get bet by cashout ID", + zap.String("cashoutID", cashoutID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve bet", err, nil) } res := domain.ConvertBet(bet) - return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil) + h.mongoLoggerSvc.Info("Bet retrieved successfully by cashout ID", + zap.String("cashoutID", cashoutID), + zap.Int("status_code", fiber.StatusOK), + zap.Time("timestamp", time.Now()), + ) + return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil) } type UpdateCashOutReq struct { @@ -260,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"` @@ -269,13 +401,23 @@ func (h *Handler) UpdateCashOut(c *fiber.Ctx) error { betID := c.Params("id") id, err := strconv.ParseInt(betID, 10, 64) if err != nil { - h.logger.Error("Invalid bet ID", "betID", betID, "error", err) + h.mongoLoggerSvc.Error("Invalid bet ID", + zap.String("betID", betID), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return fiber.NewError(fiber.StatusBadRequest, "Invalid bet ID") } var req UpdateCashOutReq if err := c.BodyParser(&req); err != nil { - h.logger.Error("Failed to parse UpdateCashOut request", "error", err) + h.mongoLoggerSvc.Error("Failed to parse UpdateCashOut request", + zap.Int64("betID", id), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request body", err, nil) } @@ -285,10 +427,21 @@ func (h *Handler) UpdateCashOut(c *fiber.Ctx) error { err = h.betSvc.UpdateCashOut(c.Context(), id, req.CashedOut) if err != nil { - h.logger.Error("Failed to update cash out bet", "betID", id, "error", err) + h.mongoLoggerSvc.Error("Failed to update cash out bet", + zap.Int64("betID", id), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update cash out bet") } + h.mongoLoggerSvc.Info("Bet updated successfully", + zap.Int64("betID", id), + zap.Int("status_code", fiber.StatusOK), + zap.Time("timestamp", time.Now()), + ) + return response.WriteJSON(c, fiber.StatusOK, "Bet updated successfully", nil, nil) } @@ -302,20 +455,36 @@ 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) if err != nil { - h.logger.Error("Invalid bet ID", "betID", betID, "error", err) + h.mongoLoggerSvc.Error("Invalid bet ID", + zap.String("betID", betID), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return fiber.NewError(fiber.StatusBadRequest, "Invalid bet ID") } - err = h.betSvc.DeleteBet(c.Context(), id) + err = h.betSvc.SetBetToRemoved(c.Context(), id) if err != nil { - h.logger.Error("Failed to delete bet by ID", "betID", id, "error", err) + h.mongoLoggerSvc.Error("Failed to delete bet by ID", + zap.Int64("betID", id), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete bet") } + h.mongoLoggerSvc.Info("Bet removed successfully", + zap.Int64("betID", id), + zap.Int("status_code", fiber.StatusOK), + zap.Time("timestamp", time.Now()), + ) + return response.WriteJSON(c, fiber.StatusOK, "Bet removed successfully", nil, nil) } diff --git a/internal/web_server/handlers/branch_handler.go b/internal/web_server/handlers/branch_handler.go index 6f869a1..d00297e 100644 --- a/internal/web_server/handlers/branch_handler.go +++ b/internal/web_server/handlers/branch_handler.go @@ -2,6 +2,8 @@ package handlers import ( "strconv" + "strings" + "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" @@ -55,6 +57,7 @@ type BranchRes struct { BranchManagerID int64 `json:"branch_manager_id" example:"1"` CompanyID int64 `json:"company_id" example:"1"` IsSelfOwned bool `json:"is_self_owned" example:"false"` + IsActive bool `json:"is_active" example:"false"` } type BranchDetailRes struct { @@ -68,6 +71,8 @@ type BranchDetailRes struct { ManagerName string `json:"manager_name" example:"John Smith"` ManagerPhoneNumber string `json:"manager_phone_number" example:"0911111111"` Balance float32 `json:"balance" example:"100.5"` + IsActive bool `json:"is_active" example:"false"` + WalletIsActive bool `json:"is_wallet_active" example:"false"` } func convertBranch(branch domain.Branch) BranchRes { @@ -79,6 +84,7 @@ func convertBranch(branch domain.Branch) BranchRes { BranchManagerID: branch.BranchManagerID, CompanyID: branch.CompanyID, IsSelfOwned: branch.IsSelfOwned, + IsActive: branch.IsActive, } } @@ -94,6 +100,8 @@ func convertBranchDetail(branch domain.BranchDetail) BranchDetailRes { ManagerName: branch.ManagerName, ManagerPhoneNumber: branch.ManagerPhoneNumber, Balance: branch.Balance.Float32(), + IsActive: branch.IsActive, + WalletIsActive: branch.WalletIsActive, } } @@ -381,8 +389,73 @@ func (h *Handler) GetBranchByCompanyID(c *fiber.Ctx) error { // @Failure 500 {object} response.APIResponse // @Router /branch [get] func (h *Handler) GetAllBranches(c *fiber.Ctx) error { - // TODO: Limit the get all branches to only the companies for branch manager and cashiers - branches, err := h.branchSvc.GetAllBranches(c.Context()) + companyID := c.Locals("company_id").(domain.ValidInt64) + isActiveParam := c.Params("is_active") + isActiveValid := isActiveParam != "" + isActive, err := strconv.ParseBool(isActiveParam) + + if isActiveValid && err != nil { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid is_active param", err, nil) + } + + branchManagerQuery := c.Query("branch_manager_id") + var branchManagerID domain.ValidInt64 + if branchManagerQuery != "" { + parseManagerID, err := strconv.ParseInt(branchManagerQuery, 10, 64) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Failed to parse branch_manager_id") + } + branchManagerID = domain.ValidInt64{ + Value: parseManagerID, + Valid: true, + } + } + searchQuery := c.Query("query") + searchString := domain.ValidString{ + Value: searchQuery, + Valid: searchQuery != "", + } + + createdBeforeQuery := c.Query("created_before") + var createdBefore domain.ValidTime + if createdBeforeQuery != "" { + createdBeforeParsed, err := time.Parse(time.RFC3339, createdBeforeQuery) + if err != nil { + h.logger.Error("invalid start_time format", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil) + } + createdBefore = domain.ValidTime{ + Value: createdBeforeParsed, + Valid: true, + } + } + + createdAfterQuery := c.Query("created_after") + var createdAfter domain.ValidTime + if createdAfterQuery != "" { + createdAfterParsed, err := time.Parse(time.RFC3339, createdAfterQuery) + if err != nil { + h.logger.Error("invalid start_time format", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil) + } + createdAfter = domain.ValidTime{ + Value: createdAfterParsed, + Valid: true, + } + } + + branches, err := h.branchSvc.GetAllBranches(c.Context(), + domain.BranchFilter{ + CompanyID: companyID, + IsActive: domain.ValidBool{ + Value: isActive, + Valid: isActiveValid, + }, + BranchManagerID: branchManagerID, + Query: searchString, + CreatedBefore: createdBefore, + CreatedAfter: createdAfter, + }) if err != nil { h.logger.Error("Failed to get branches", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get branches", err, nil) @@ -682,6 +755,42 @@ func (h *Handler) UpdateBranch(c *fiber.Ctx) error { } +func (h *Handler) UpdateBranchStatus(c *fiber.Ctx) error { + branchID := c.Params("id") + id, err := strconv.ParseInt(branchID, 10, 64) + if err != nil { + h.logger.Error("Invalid branch ID", "branchID", branchID, "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid branch ID", err, nil) + } + + var isActive bool + path := strings.Split(strings.Trim(c.Path(), "/"), "/") + + if path[len(path)-1] == "set-active" { + isActive = true + } else if path[len(path)-1] == "set-inactive" { + isActive = false + } else { + h.logger.Error("Invalid branch status", "status", isActive, "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid branch status", err, nil) + } + + branch, err := h.branchSvc.UpdateBranch(c.Context(), domain.UpdateBranch{ + ID: id, + IsActive: &isActive, + }) + + if err != nil { + h.logger.Error("Failed to update branch", "branchID", id, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update branch", err, nil) + } + + res := convertBranch(branch) + + return response.WriteJSON(c, fiber.StatusOK, "Branch Updated", res, nil) + +} + // DeleteBranch godoc // @Summary Delete the branch // @Description Delete the branch diff --git a/internal/web_server/handlers/chapa.go b/internal/web_server/handlers/chapa.go index 7d84ddf..5e9ad56 100644 --- a/internal/web_server/handlers/chapa.go +++ b/internal/web_server/handlers/chapa.go @@ -24,33 +24,38 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error { userID, ok := c.Locals("user_id").(int64) if !ok { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ - Error: "invalid user ID", + Error: "invalid user ID", + Message: "User ID is required to initiate a deposit", }) } 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(), + Error: err.Error(), + Message: "Failed to parse request body", }) } 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 { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Error: err.Error(), - Message: checkoutURL, + Message: "Failed to initiate Chapa deposit", }) } - return c.Status(fiber.StatusOK).JSON(domain.ChapaDepositResponse{ - CheckoutURL: checkoutURL, + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Chapa deposit process initiated successfully", + Data: checkoutURL, + StatusCode: 200, + Success: true, }) } @@ -74,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 { @@ -95,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) @@ -142,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/company_handler.go b/internal/web_server/handlers/company_handler.go index 2555cdd..e4d01c2 100644 --- a/internal/web_server/handlers/company_handler.go +++ b/internal/web_server/handlers/company_handler.go @@ -184,7 +184,34 @@ func (h *Handler) GetCompanyByID(c *fiber.Ctx) error { res := convertGetCompany(company) return response.WriteJSON(c, fiber.StatusOK, "Company retrieved successfully", res, nil) +} +// GetCompanyForAdmin godoc +// @Summary Gets company by id +// @Description Gets a single company by id +// @Tags company +// @Accept json +// @Produce json +// @Success 200 {object} CompanyRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /admin-company [get] +func (h *Handler) GetCompanyForAdmin(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + + if !companyID.Valid { + return response.WriteJSON(c, fiber.StatusInternalServerError, "Invalid company ID", nil, nil) + } + company, err := h.companySvc.GetCompanyByID(c.Context(), companyID.Value) + + if err != nil { + h.logger.Error("Failed to get company by ID", "companyID", companyID.Value, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to company branch", err, nil) + } + + res := convertGetCompany(company) + + return response.WriteJSON(c, fiber.StatusOK, "Company retrieved successfully", res, nil) } // GetAllCompanies godoc diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index a5e40a0..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" @@ -18,20 +20,24 @@ import ( referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/report" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" + "go.uber.org/zap" ) type Handler struct { + issueReportingSvc *issuereporting.Service + instSvc *institutions.Service currSvc *currency.Service logger *slog.Logger + settingSvc *settings.Service notificationSvc *notificationservice.Service userSvc *user.Service referralSvc referralservice.ReferralStore @@ -48,18 +54,22 @@ type Handler struct { leagueSvc league.Service virtualGameSvc virtualgameservice.VirtualGameService aleaVirtualGameSvc alea.AleaVirtualGameService - veliVirtualGameSvc veli.VeliVirtualGameService - recommendationSvc recommendation.RecommendationService - authSvc *authentication.Service - resultSvc result.Service - jwtConfig jwtutil.JwtConfig - validator *customvalidator.CustomValidator - Cfg *config.Config + // veliVirtualGameSvc veli.VeliVirtualGameService + recommendationSvc recommendation.RecommendationService + authSvc *authentication.Service + resultSvc result.Service + jwtConfig jwtutil.JwtConfig + validator *customvalidator.CustomValidator + Cfg *config.Config + mongoLoggerSvc *zap.Logger } func New( + issueReportingSvc *issuereporting.Service, + instSvc *institutions.Service, currSvc *currency.Service, logger *slog.Logger, + settingSvc *settings.Service, notificationSvc *notificationservice.Service, validator *customvalidator.CustomValidator, reportSvc report.ReportStore, @@ -68,7 +78,7 @@ func New( referralSvc referralservice.ReferralStore, virtualGameSvc virtualgameservice.VirtualGameService, aleaVirtualGameSvc alea.AleaVirtualGameService, - veliVirtualGameSvc veli.VeliVirtualGameService, + // veliVirtualGameSvc veli.VeliVirtualGameService, recommendationSvc recommendation.RecommendationService, userSvc *user.Service, transactionSvc *transaction.Service, @@ -83,10 +93,14 @@ func New( leagueSvc league.Service, resultSvc result.Service, cfg *config.Config, + mongoLoggerSvc *zap.Logger, ) *Handler { return &Handler{ + issueReportingSvc: issueReportingSvc, + instSvc: instSvc, currSvc: currSvc, logger: logger, + settingSvc: settingSvc, notificationSvc: notificationSvc, reportSvc: reportSvc, chapaSvc: chapaSvc, @@ -104,11 +118,12 @@ func New( leagueSvc: leagueSvc, virtualGameSvc: virtualGameSvc, aleaVirtualGameSvc: aleaVirtualGameSvc, - veliVirtualGameSvc: veliVirtualGameSvc, - recommendationSvc: recommendationSvc, - authSvc: authSvc, - resultSvc: resultSvc, - jwtConfig: jwtConfig, - Cfg: cfg, + // veliVirtualGameSvc: veliVirtualGameSvc, + recommendationSvc: recommendationSvc, + authSvc: authSvc, + resultSvc: resultSvc, + jwtConfig: jwtConfig, + Cfg: cfg, + mongoLoggerSvc: mongoLoggerSvc, } } 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/leagues.go b/internal/web_server/handlers/leagues.go index 9bd3299..000c557 100644 --- a/internal/web_server/handlers/leagues.go +++ b/internal/web_server/handlers/leagues.go @@ -106,3 +106,32 @@ func (h *Handler) SetLeagueActive(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil) } + +func (h *Handler) SetLeagueAsFeatured(c *fiber.Ctx) error { + fmt.Printf("Set Active Leagues") + leagueIdStr := c.Params("id") + if leagueIdStr == "" { + response.WriteJSON(c, fiber.StatusBadRequest, "Missing league id", nil, nil) + } + leagueId, err := strconv.Atoi(leagueIdStr) + if err != nil { + response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, nil) + } + + var req SetLeagueActiveReq + if err := c.BodyParser(&req); err != nil { + h.logger.Error("SetLeagueReq failed", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Failed to parse request", err, nil) + } + + valErrs, ok := h.validator.Validate(c, req) + if !ok { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + } + + if err := h.leagueSvc.SetLeagueActive(c.Context(), int64(leagueId), req.IsActive); err != nil { + response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update league", err, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil) +} diff --git a/internal/web_server/handlers/manager.go b/internal/web_server/handlers/manager.go index 948ca05..02c1496 100644 --- a/internal/web_server/handlers/manager.go +++ b/internal/web_server/handlers/manager.go @@ -111,7 +111,8 @@ type ManagersRes struct { func (h *Handler) GetAllManagers(c *fiber.Ctx) error { role := c.Locals("role").(domain.Role) companyId := c.Locals("company_id").(domain.ValidInt64) - + + // Checking to make sure that admin user has a company id in the token if role != domain.RoleSuperAdmin && !companyId.Valid { return fiber.NewError(fiber.StatusInternalServerError, "Cannot get company ID") } @@ -182,32 +183,38 @@ func (h *Handler) GetAllManagers(c *fiber.Ctx) error { // @Failure 500 {object} response.APIResponse // @Router /managers/{id} [get] func (h *Handler) GetManagerByID(c *fiber.Ctx) error { - // branchId := int64(12) //c.Locals("branch_id").(int64) - // filter := user.Filter{ - // Role: string(domain.RoleUser), - // BranchId: user.ValidBranchId{ - // Value: branchId, - // Valid: true, - // }, - // Page: c.QueryInt("page", 1), - // PageSize: c.QueryInt("page_size", 10), - // } - // valErrs, ok := validator.Validate(c, filter) - // if !ok { - // return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - // } + role := c.Locals("role").(domain.Role) + companyId := c.Locals("company_id").(domain.ValidInt64) + requestUserID := c.Locals("user_id").(int64) + + // Only Super Admin / Admin / Branch Manager can view this route + if role != domain.RoleSuperAdmin && role != domain.RoleAdmin && role != domain.RoleBranchManager { + return fiber.NewError(fiber.StatusUnauthorized, "Role Unauthorized") + } + + if role != domain.RoleSuperAdmin && !companyId.Valid { + return fiber.NewError(fiber.StatusInternalServerError, "Cannot get company ID") + } userIDstr := c.Params("id") userID, err := strconv.ParseInt(userIDstr, 10, 64) if err != nil { - h.logger.Error("failed to fetch user using UserID", "error", err) - return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid managers ID", nil, nil) + return fiber.NewError(fiber.StatusBadRequest, "Invalid managers ID") } user, err := h.userSvc.GetUserByID(c.Context(), userID) if err != nil { - h.logger.Error("Get User By ID failed", "error", err) - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get managers", nil, nil) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get managers") + } + + // A Branch Manager can only fetch his own branch info + if role == domain.RoleBranchManager && user.ID != requestUserID { + return fiber.NewError(fiber.StatusBadRequest, "User Access Not Allowed") + } + + // Check that only admin from company can view this route + if role != domain.RoleSuperAdmin && user.CompanyID.Value != companyId.Value { + return fiber.NewError(fiber.StatusBadRequest, "Only company user can view manager info") } lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID) @@ -259,7 +266,9 @@ type updateManagerReq struct { // @Failure 500 {object} response.APIResponse // @Router /managers/{id} [put] func (h *Handler) UpdateManagers(c *fiber.Ctx) error { + var req updateManagerReq + if err := c.BodyParser(&req); err != nil { h.logger.Error("UpdateManagers failed", "error", err) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil) diff --git a/internal/web_server/handlers/mongoLogger.go b/internal/web_server/handlers/mongoLogger.go index f31d780..1a139f8 100644 --- a/internal/web_server/handlers/mongoLogger.go +++ b/internal/web_server/handlers/mongoLogger.go @@ -2,6 +2,7 @@ package handlers import ( "context" + "os" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/gofiber/fiber/v2" @@ -10,9 +11,17 @@ import ( "go.mongodb.org/mongo-driver/mongo/options" ) +// GetLogsHandler godoc +// @Summary Retrieve latest application logs +// @Description Fetches the 100 most recent application logs from MongoDB +// @Tags Logs +// @Produce json +// @Success 200 {array} domain.LogEntry "List of application logs" +// @Failure 500 {object} domain.ErrorResponse "Internal server error" +// @Router /api/v1/logs [get] func GetLogsHandler(appCtx context.Context) fiber.Handler { return func(c *fiber.Ctx) error { - client, err := mongo.Connect(appCtx, options.Client().ApplyURI("mongodb://root:secret@localhost:27017/?authSource=admin")) + client, err := mongo.Connect(appCtx, options.Client().ApplyURI(os.Getenv("MONGODB_URL"))) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "MongoDB connection failed: "+err.Error()) } diff --git a/internal/web_server/handlers/prematch.go b/internal/web_server/handlers/prematch.go index 7117d1d..fbdf594 100644 --- a/internal/web_server/handlers/prematch.go +++ b/internal/web_server/handlers/prematch.go @@ -56,8 +56,8 @@ func (h *Handler) GetRawOddsByMarketID(c *fiber.Ctx) error { rawOdds, err := h.prematchSvc.GetRawOddsByMarketID(c.Context(), marketID, upcomingID) if err != nil { - fmt.Printf("Failed to fetch raw odds: %v market_id:%v upcomingID:%v\n", err, marketID, upcomingID) - h.logger.Error("failed to fetch raw odds", "error", err) + // fmt.Printf("Failed to fetch raw odds: %v market_id:%v upcomingID:%v\n", err, marketID, upcomingID) + h.logger.Error("Failed to get raw odds by market ID", "marketID", marketID, "upcomingID", upcomingID, "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve raw odds", err, nil) } @@ -172,6 +172,65 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { } +type TopLeaguesRes struct { + Leagues []TopLeague `json:"leagues"` +} + +type TopLeague struct { + LeagueID int64 `json:"league_id"` + LeagueName string `json:"league_name"` + LeagueCC string `json:"league_cc"` + LeagueSportID int32 `json:"league_sport_id"` + Events []domain.UpcomingEvent `json:"events"` + // Total int64 `json:"total"` +} + +// @Summary Retrieve all top leagues +// @Description Retrieve all top leagues +// @Tags prematch +// @Accept json +// @Produce json +// @Success 200 {array} TopLeague +// @Failure 500 {object} response.APIResponse +// @Router /top-leagues [get] +func (h *Handler) GetTopLeagues(c *fiber.Ctx) error { + + leagues, err := h.leagueSvc.GetFeaturedLeagues(c.Context()) + + if err != nil { + h.logger.Error("Error while fetching top leagues", "err", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get featured leagues", nil, nil) + } + + var topLeague []TopLeague = make([]TopLeague, 0, len(leagues)) + for _, league := range leagues { + events, _, err := h.eventSvc.GetPaginatedUpcomingEvents( + c.Context(), domain.EventFilter{ + LeagueID: domain.ValidInt32{ + Value: int32(league.ID), + Valid: true, + }, + }) + if err != nil { + fmt.Printf("Error while fetching events for top league %v \n", league.ID) + h.logger.Error("Error while fetching events for top league", "League ID", league.ID) + } + topLeague = append(topLeague, TopLeague{ + LeagueID: league.ID, + LeagueName: league.Name, + LeagueCC: league.CountryCode, + LeagueSportID: league.SportID, + Events: events, + }) + } + + res := TopLeaguesRes{ + Leagues: topLeague, + } + return response.WriteJSON(c, fiber.StatusOK, "All top leagues events retrieved successfully", res, nil) + +} + // @Summary Retrieve an upcoming by ID // @Description Retrieve an upcoming event by ID // @Tags prematch 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/report.go b/internal/web_server/handlers/report.go index c597763..ed396a2 100644 --- a/internal/web_server/handlers/report.go +++ b/internal/web_server/handlers/report.go @@ -3,7 +3,9 @@ package handlers import ( "context" "fmt" + "os" "strconv" + "strings" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -121,3 +123,81 @@ func parseReportFilter(c *fiber.Ctx) (domain.ReportFilter, error) { return filter, err } + +// DownloadReportFile godoc +// @Summary Download a CSV report file +// @Description Downloads a generated report CSV file from the server +// @Tags Reports +// @Param filename path string true "Name of the report file to download (e.g., report_daily_2025-06-21.csv)" +// @Produce text/csv +// @Success 200 {file} file "CSV file will be downloaded" +// @Failure 400 {object} domain.ErrorResponse "Missing or invalid filename" +// @Failure 404 {object} domain.ErrorResponse "Report file not found" +// @Failure 500 {object} domain.ErrorResponse "Internal server error while serving the file" +// @Router /api/v1/report-files/download/{filename} [get] +func (h *Handler) DownloadReportFile(c *fiber.Ctx) error { + filename := c.Params("filename") + if filename == "" { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Missing filename parameter", + Error: "filename is required", + }) + } + + filePath := fmt.Sprintf("/host-desktop/%s", filename) + + // Check if file exists + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ + Message: "Report file not found", + Error: "no such file", + }) + } + + // Set download headers and return file + c.Set("Content-Type", "text/csv") + c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) + + if err := c.SendFile(filePath); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to serve file", + Error: err.Error(), + }) + } + + return nil +} + +// ListReportFiles godoc +// @Summary List available report CSV files +// @Description Returns a list of all generated report CSV files available for download +// @Tags Reports +// @Produce json +// @Success 200 {object} domain.Response{data=[]string} "List of CSV report filenames" +// @Failure 500 {object} domain.ErrorResponse "Failed to read report directory" +// @Router /api/v1/report-files/list [get] +func (h *Handler) ListReportFiles(c *fiber.Ctx) error { + reportDir := "/host-desktop" + + files, err := os.ReadDir(reportDir) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to read report directory", + Error: err.Error(), + }) + } + + var reportFiles []string + for _, file := range files { + if !file.IsDir() && strings.HasSuffix(file.Name(), ".csv") { + reportFiles = append(reportFiles, file.Name()) + } + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + StatusCode: 200, + Message: "Report files retrieved successfully", + Data: reportFiles, + Success: true, + }) +} diff --git a/internal/web_server/handlers/ticket_handler.go b/internal/web_server/handlers/ticket_handler.go index 9706d2a..17e6276 100644 --- a/internal/web_server/handlers/ticket_handler.go +++ b/internal/web_server/handlers/ticket_handler.go @@ -1,56 +1,27 @@ package handlers import ( - "encoding/json" "strconv" - "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" ) -type CreateTicketOutcomeReq struct { - // TicketID int64 `json:"ticket_id" example:"1"` - EventID int64 `json:"event_id" example:"1"` - OddID int64 `json:"odd_id" example:"1"` - MarketID int64 `json:"market_id" example:"1"` - // HomeTeamName string `json:"home_team_name" example:"Manchester"` - // AwayTeamName string `json:"away_team_name" example:"Liverpool"` - // MarketName string `json:"market_name" example:"Fulltime Result"` - // Odd float32 `json:"odd" example:"1.5"` - // OddName string `json:"odd_name" example:"1"` - // Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` -} - -type CreateTicketReq struct { - Outcomes []CreateTicketOutcomeReq `json:"outcomes"` - Amount float32 `json:"amount" example:"100.0"` -} -type CreateTicketRes struct { - FastCode int64 `json:"fast_code" example:"1234"` - CreatedNumber int64 `json:"created_number" example:"3"` -} -type TicketRes struct { - ID int64 `json:"id" example:"1"` - Outcomes []domain.TicketOutcome `json:"outcomes"` - Amount float32 `json:"amount" example:"100.0"` - TotalOdds float32 `json:"total_odds" example:"4.22"` -} - // CreateTicket godoc // @Summary Create a temporary ticket // @Description Creates a temporary ticket // @Tags ticket // @Accept json // @Produce json -// @Param createTicket body CreateTicketReq true "Creates ticket" -// @Success 200 {object} CreateTicketRes +// @Param createTicket body domain.CreateTicketReq true "Creates ticket" +// @Success 200 {object} domain.CreateTicketRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /ticket [post] func (h *Handler) CreateTicket(c *fiber.Ctx) error { - var req CreateTicketReq + var req domain.CreateTicketReq if err := c.BodyParser(&req); err != nil { h.logger.Error("Failed to parse CreateTicket request", "error", err) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") @@ -60,122 +31,20 @@ func (h *Handler) CreateTicket(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } - // TODO Validate Outcomes Here and make sure they didn't expire - // Validation for creating tickets - if len(req.Outcomes) > 30 { - return response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, nil) - } - - if req.Amount > 100000 { - return response.WriteJSON(c, fiber.StatusBadRequest, "Cannot create a ticket with an amount above 100,000 birr", nil, nil) - } - - clientIP := c.IP() - count, err := h.ticketSvc.CountTicketByIP(c.Context(), clientIP) - if err != nil { - return response.WriteJSON(c, fiber.StatusInternalServerError, "Error fetching user info", nil, nil) - } - - if count > 50 { - return response.WriteJSON(c, fiber.StatusBadRequest, "Ticket Limit reached", nil, nil) - } - var outcomes []domain.CreateTicketOutcome = make([]domain.CreateTicketOutcome, 0, len(req.Outcomes)) - var totalOdds float32 = 1 - for _, outcome := range req.Outcomes { - eventIDStr := strconv.FormatInt(outcome.EventID, 10) - marketIDStr := strconv.FormatInt(outcome.MarketID, 10) - oddIDStr := strconv.FormatInt(outcome.OddID, 10) - event, err := h.eventSvc.GetUpcomingEventByID(c.Context(), eventIDStr) - if err != nil { - return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid event id", err, nil) - } - - // Checking to make sure the event hasn't already started - currentTime := time.Now() - if event.StartTime.Before(currentTime) { - return response.WriteJSON(c, fiber.StatusBadRequest, "The event has already expired", nil, nil) - } - - odds, err := h.prematchSvc.GetRawOddsByMarketID(c.Context(), marketIDStr, eventIDStr) - - if err != nil { - return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid market id", err, nil) - } - type rawOddType struct { - ID string - Name string - Odds string - Header string - Handicap string - } - var selectedOdd rawOddType - var isOddFound bool = false - for _, raw := range odds.RawOdds { - var rawOdd rawOddType - rawBytes, err := json.Marshal(raw) - err = json.Unmarshal(rawBytes, &rawOdd) - if err != nil { - h.logger.Error("Failed to unmarshal raw odd:", "error", err) - continue - } - if rawOdd.ID == oddIDStr { - selectedOdd = rawOdd - isOddFound = true - } - } - - if !isOddFound { - return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid odd id", nil, nil) - } - - parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32) - totalOdds = totalOdds * float32(parsedOdd) - outcomes = append(outcomes, domain.CreateTicketOutcome{ - EventID: outcome.EventID, - OddID: outcome.OddID, - MarketID: outcome.MarketID, - HomeTeamName: event.HomeTeam, - AwayTeamName: event.AwayTeam, - MarketName: odds.MarketName, - Odd: float32(parsedOdd), - OddName: selectedOdd.Name, - OddHeader: selectedOdd.Header, - OddHandicap: selectedOdd.Handicap, - Expires: event.StartTime, - }) - - } - totalWinnings := req.Amount * totalOdds - if totalWinnings > 1000000 { - return response.WriteJSON(c, fiber.StatusBadRequest, "Cannot create a ticket with 1,000,000 winnings", nil, nil) - } - ticket, err := h.ticketSvc.CreateTicket(c.Context(), domain.CreateTicket{ - Amount: domain.ToCurrency(req.Amount), - TotalOdds: totalOdds, - IP: clientIP, - }) - if err != nil { - h.logger.Error("CreateTicketReq failed", "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "Internal server error", - }) - } - - // Add the ticket id now that it has fetched from the database - for index := range outcomes { - outcomes[index].TicketID = ticket.ID - } - - rows, err := h.ticketSvc.CreateTicketOutcome(c.Context(), outcomes) + newTicket, rows, err := h.ticketSvc.CreateTicket(c.Context(), req, c.IP()) if err != nil { - h.logger.Error("CreateTicketReq failed to create outcomes", "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "Internal server error", - }) + switch err { + case ticket.ErrEventHasBeenRemoved, ticket.ErrTicketHasExpired, + ticket.ErrRawOddInvalid, ticket.ErrTooManyOutcomesForTicket, + ticket.ErrTicketAmountTooHigh, ticket.ErrTicketLimitForSingleUser, + ticket.ErrTicketWinningTooHigh: + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } - res := CreateTicketRes{ - FastCode: ticket.ID, + res := domain.CreateTicketRes{ + FastCode: newTicket.ID, CreatedNumber: rows, } return response.WriteJSON(c, fiber.StatusOK, "Ticket Created", res, nil) @@ -189,7 +58,7 @@ func (h *Handler) CreateTicket(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param id path int true "Ticket ID" -// @Success 200 {object} TicketRes +// @Success 200 {object} domain.TicketRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /ticket/{id} [get] @@ -207,7 +76,7 @@ func (h *Handler) GetTicketByID(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusNotFound, "Failed to retrieve ticket") } - res := TicketRes{ + res := domain.TicketRes{ ID: ticket.ID, Outcomes: ticket.Outcomes, Amount: ticket.Amount.Float32(), @@ -222,7 +91,7 @@ func (h *Handler) GetTicketByID(c *fiber.Ctx) error { // @Tags ticket // @Accept json // @Produce json -// @Success 200 {array} TicketRes +// @Success 200 {array} domain.TicketRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /ticket [get] @@ -234,9 +103,9 @@ func (h *Handler) GetAllTickets(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve tickets") } - res := make([]TicketRes, len(tickets)) + res := make([]domain.TicketRes, len(tickets)) for i, ticket := range tickets { - res[i] = TicketRes{ + res[i] = domain.TicketRes{ ID: ticket.ID, Outcomes: ticket.Outcomes, Amount: ticket.Amount.Float32(), diff --git a/internal/web_server/handlers/transfer_handler.go b/internal/web_server/handlers/transfer_handler.go index b272a39..428ff5a 100644 --- a/internal/web_server/handlers/transfer_handler.go +++ b/internal/web_server/handlers/transfer_handler.go @@ -1,6 +1,7 @@ package handlers import ( + "fmt" "strconv" "time" @@ -10,50 +11,60 @@ 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"` 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"` + 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"` } -func convertTransfer(transfer domain.Transfer) TransferWalletRes { - var senderWalletID *int64 - senderWalletID = &transfer.SenderWalletID +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 + 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), - ReceiverWalletID: transfer.ReceiverWalletID, - SenderWalletID: senderWalletID, + ID: t.ID, + Amount: float32(t.Amount), + Verified: t.Verified, + Type: string(t.Type), + PaymentMethod: string(t.PaymentMethod), + ReceiverWalletID: receiverID, + SenderWalletID: senderID, CashierID: cashierID, - CreatedAt: transfer.CreatedAt, - UpdatedAt: transfer.UpdatedAt, + ReferenceNumber: t.ReferenceNumber, + CreatedAt: t.CreatedAt, + UpdatedAt: t.UpdatedAt, } } @@ -126,23 +137,30 @@ func (h *Handler) TransferToWallet(c *fiber.Ctx) error { } // Get sender ID from the cashier userID := c.Locals("user_id").(int64) - role := string(c.Locals("role").(domain.Role)) - companyID := c.Locals("company_id").(int64) + role := c.Locals("role").(domain.Role) + companyID := c.Locals("company_id").(domain.ValidInt64) + + fmt.Printf("\n\nCompant ID: %v\n\n", companyID.Value) var senderID int64 //TODO: check to make sure that the cashiers aren't transferring TO branch wallet - if role == string(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 == string(domain.RoleBranchManager) || role == string(domain.RoleAdmin) || role == string(domain.RoleSuperAdmin) { - company, err := h.companySvc.GetCompanyByID(c.Context(), companyID) + case domain.RoleBranchManager, domain.RoleAdmin, domain.RoleSuperAdmin: + company, err := h.companySvc.GetCompanyByID(c.Context(), companyID.Value) if err != nil { - return response.WriteJSON(c, fiber.StatusInternalServerError, "Error fetching company", err, nil) + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to fetch company", + Error: err.Error(), + }) + // return response.WriteJSON(c, fiber.StatusInternalServerError, "Error fetching company", err, nil) } 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) @@ -190,6 +208,7 @@ func (h *Handler) RefillWallet(c *fiber.Ctx) error { receiverIDString := c.Params("id") + userID := c.Locals("user_id").(int64) receiverID, err := strconv.ParseInt(receiverIDString, 10, 64) if err != nil { @@ -197,13 +216,6 @@ func (h *Handler) RefillWallet(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid wallet ID", err, nil) } // Get sender ID from the cashier - userID := c.Locals("user_id").(int64) - role := string(c.Locals("role").(domain.Role)) - - if role != string(domain.RoleSuperAdmin) { - h.logger.Error("Unauthorized access", "userID", userID, "role", role) - return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) - } var req CreateRefillReq @@ -217,16 +229,11 @@ func (h *Handler) RefillWallet(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } - transfer, err := h.walletSvc.RefillWallet(c.Context(), domain.CreateTransfer{ - Amount: domain.ToCurrency(req.Amount), - PaymentMethod: domain.TRANSFER_BANK, - ReceiverWalletID: receiverID, - CashierID: domain.ValidInt64{ + transfer, err := h.walletSvc.AddToWallet( + c.Context(), receiverID, domain.ToCurrency(req.Amount), domain.ValidInt64{ Value: userID, Valid: true, - }, - Type: domain.TransferType("deposit"), - }) + }, domain.TRANSFER_BANK, domain.PaymentDetails{}) if !ok { return response.WriteJSON(c, fiber.StatusInternalServerError, "Creating Transfer Failed", err, nil) diff --git a/internal/web_server/handlers/user.go b/internal/web_server/handlers/user.go index 522551c..a088698 100644 --- a/internal/web_server/handlers/user.go +++ b/internal/web_server/handlers/user.go @@ -98,7 +98,7 @@ func (h *Handler) SendRegisterCode(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided") } - if err := h.userSvc.SendRegisterCode(c.Context(), medium, sentTo); err != nil { + if err := h.userSvc.SendRegisterCode(c.Context(), medium, sentTo, "twilio"); err != nil { h.logger.Error("Failed to send register code", "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to send register code") } @@ -174,11 +174,7 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Unknown Error") } - newWallet, err := h.walletSvc.CreateWallet(c.Context(), domain.CreateWallet{ - UserID: newUser.ID, - IsWithdraw: true, - IsBettable: true, - }) + newWallet, err := h.walletSvc.CreateCustomerWallet(c.Context(), newUser.ID) if err != nil { h.logger.Error("Failed to create wallet for user", "userID", newUser.ID, "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to create user wallet") @@ -192,7 +188,8 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error { } // TODO: Remove later - err = h.walletSvc.AddToWallet(c.Context(), newWallet.ID, domain.ToCurrency(100.0)) + _, err = h.walletSvc.AddToWallet( + c.Context(), newWallet.RegularID, domain.ToCurrency(100.0), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}) if err != nil { h.logger.Error("Failed to update wallet for user", "userID", newUser.ID, "error", err) @@ -205,6 +202,7 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error { type ResetCodeReq struct { Email string `json:"email" example:"john.doe@example.com"` PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"` + // Provider domain.OtpProvider `json:"provider" validate:"required" example:"twilio"` } // SendResetCode godoc @@ -242,7 +240,7 @@ func (h *Handler) SendResetCode(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided") } - if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo); err != nil { + if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo, "twilio"); err != nil { h.logger.Error("Failed to send reset code", "error", err) fmt.Println(err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to send reset code") @@ -413,6 +411,7 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error { return nil } companyID := c.Locals("company_id").(domain.ValidInt64) + users, err := h.userSvc.SearchUserByNameOrPhone(c.Context(), req.SearchString, req.Role, companyID) if err != nil { h.logger.Error("SearchUserByNameOrPhone failed", "error", err) diff --git a/internal/web_server/handlers/veli_games.go b/internal/web_server/handlers/veli_games.go index 0a32aec..d096ac9 100644 --- a/internal/web_server/handlers/veli_games.go +++ b/internal/web_server/handlers/veli_games.go @@ -1,75 +1,122 @@ package handlers -import ( - "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - "github.com/gofiber/fiber/v2" -) +// import ( +// "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +// "github.com/gofiber/fiber/v2" +// ) -// LaunchVeliGame godoc -// @Summary Launch a Veli game -// @Description Generates authenticated launch URL for Veli games -// @Tags Veli Games -// @Security BearerAuth -// @Param game_id path string true "Game ID (e.g., veli_aviator_v1)" -// @Param currency query string false "Currency code" default(USD) -// @Param mode query string false "Game mode" Enums(real, demo) default(real) -// @Success 200 {object} map[string]string "Returns launch URL" -// @Failure 400 {object} map[string]string "Invalid request" -// @Failure 500 {object} map[string]string "Internal server error" -// @Router /api/veli/launch/{game_id} [get] -func (h *Handler) LaunchVeliGame(c *fiber.Ctx) error { - userID := c.Locals("userID").(int64) - gameID := c.Params("game_id") - currency := c.Query("currency", "USD") - mode := c.Query("mode", "real") +// // @Summary Get Veli games list +// // @Description Get list of available Veli games +// // @Tags Virtual Games - Veli Games +// // @Produce json +// // @Success 200 {array} domain.VeliGame +// // @Failure 500 {object} domain.ErrorResponse +// // @Router /veli/games [get] +// func (h *Handler) GetGames(c *fiber.Ctx) error { +// games, err := h.service.GetGames(c.Context()) +// if err != nil { +// return domain.UnExpectedErrorResponse(c) +// } - launchURL, err := h.veliVirtualGameSvc.GenerateGameLaunchURL(c.Context(), userID, gameID, currency, mode) - if err != nil { - h.logger.Error("failed to generate Veli launch URL", - "error", err, - "userID", userID, - "gameID", gameID) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "failed to launch game", - }) - } +// return c.Status(fiber.StatusOK).JSON(games) +// } - return c.JSON(fiber.Map{ - "launch_url": launchURL, - }) -} +// // @Summary Launch Veli game +// // @Description Get URL to launch a Veli game +// // @Tags Virtual Games - Veli Games +// // @Accept json +// // @Produce json +// // @Param request body LaunchGameRequest true "Launch game request" +// // @Success 200 {object} LaunchGameResponse +// // @Failure 400 {object} domain.ErrorResponse +// // @Failure 500 {object} domain.ErrorResponse +// // @Router /veli/games/launch [post] +// func (h *Handler) LaunchGame(c *fiber.Ctx) error { +// var req struct { +// PlayerID string `json:"player_id" validate:"required"` +// GameID string `json:"game_id" validate:"required"` +// } -// HandleVeliCallback godoc -// @Summary Veli Games webhook handler -// @Description Processes game round settlements from Veli -// @Tags Veli Games -// @Accept json -// @Produce json -// @Param payload body domain.VeliCallback true "Callback payload" -// @Success 200 {object} map[string]string "Callback processed" -// @Failure 400 {object} map[string]string "Invalid payload" -// @Failure 403 {object} map[string]string "Invalid signature" -// @Failure 500 {object} map[string]string "Processing error" -// @Router /webhooks/veli [post] -func (h *Handler) HandleVeliCallback(c *fiber.Ctx) error { - var cb domain.VeliCallback - if err := c.BodyParser(&cb); err != nil { - h.logger.Error("invalid Veli callback format", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "invalid payload format", - }) - } +// if err := c.BodyParser(&req); err != nil { +// return domain.BadRequestResponse(c) +// } - if err := h.veliVirtualGameSvc.HandleCallback(c.Context(), &cb); err != nil { - h.logger.Error("failed to process Veli callback", - "roundID", cb.RoundID, - "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "failed to process callback", - }) - } +// gameURL, err := h.service.LaunchGame(c.Context(), req.PlayerID, req.GameID) +// if err != nil { +// return domain.UnExpectedErrorResponse(c) +// } - return c.JSON(fiber.Map{ - "status": "processed", - }) -} +// return c.Status(fiber.StatusOK).JSON(fiber.Map{ +// "url": gameURL, +// }) +// } + +// // @Summary Place bet +// // @Description Place a bet on a Veli game +// // @Tags Virtual Games - Veli Games +// // @Accept json +// // @Produce json +// // @Param request body PlaceBetRequest true "Place bet request" +// // @Success 200 {object} domain.VeliTransaction +// // @Failure 400 {object} domain.ErrorResponse +// // @Failure 500 {object} domain.ErrorResponse +// // @Router /veli/bets [post] +// func (h *Handler) PlaceBet(c *fiber.Ctx) error { +// var req struct { +// PlayerID string `json:"player_id" validate:"required"` +// GameID string `json:"game_id" validate:"required"` +// Amount float64 `json:"amount" validate:"required,gt=0"` +// } + +// if err := c.BodyParser(&req); err != nil { +// return domain.BadRequestResponse(c) +// } + +// tx, err := h.service.PlaceBet(c.Context(), req.PlayerID, req.GameID, req.Amount) +// if err != nil { +// if err == domain.ErrInsufficientBalance { +// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ +// Message: "Insufficient balance", +// }) +// } +// return domain.UnExpectedErrorResponse(c) +// } + +// return c.Status(fiber.StatusOK).JSON(tx) +// } + +// // @Summary Bet settlement webhook +// // @Description Handle bet settlement from Veli +// // @Tags Virtual Games - Veli Games +// // @Accept json +// // @Produce json +// // @Param request body SettlementRequest true "Settlement request" +// // @Success 200 {object} domain.Response +// // @Failure 400 {object} domain.ErrorResponse +// // @Failure 500 {object} domain.ErrorResponse +// // @Router /veli/webhooks/settlement [post] +// func (h *Handler) HandleSettlement(c *fiber.Ctx) error { +// var req struct { +// TransactionID string `json:"transaction_id" validate:"required"` +// PlayerID string `json:"player_id" validate:"required"` +// Amount float64 `json:"amount" validate:"required"` +// IsWin bool `json:"is_win"` +// } + +// if err := c.BodyParser(&req); err != nil { +// return domain.BadRequestResponse(c) +// } + +// // Verify signature +// if !h.service.VerifyWebhookSignature(c.Request().Body(), c.Get("X-Signature")) { +// return domain.UnauthorizedResponse(c) +// } + +// // Process settlement +// tx, err := h.service.SettleBet(c.Context(), req.TransactionID, req.PlayerID, req.Amount, req.IsWin) +// if err != nil { +// return domain.UnExpectedErrorResponse(c) +// } + +// return c.Status(fiber.StatusOK).JSON(tx) +// } diff --git a/internal/web_server/handlers/virtual_games_hadlers.go b/internal/web_server/handlers/virtual_games_hadlers.go index b47e55f..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" @@ -19,15 +21,15 @@ type launchVirtualGameRes struct { // LaunchVirtualGame godoc // @Summary Launch a PopOK virtual game // @Description Generates a URL to launch a PopOK game -// @Tags virtual-game +// @Tags Virtual Games - PopOK // @Accept json // @Produce json // @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) @@ -60,13 +68,13 @@ func (h *Handler) LaunchVirtualGame(c *fiber.Ctx) error { // HandleVirtualGameCallback godoc // @Summary Handle PopOK game callback // @Description Processes callbacks from PopOK for game events -// @Tags virtual-game +// @Tags Virtual Games - PopOK // @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 @@ -94,7 +102,7 @@ func (h *Handler) HandlePlayerInfo(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } - return response.WriteJSON(c, fiber.StatusOK, "Player info retrieved", resp, nil) + return c.Status(fiber.StatusOK).JSON(resp) } func (h *Handler) HandleBet(c *fiber.Ctx) error { @@ -103,16 +111,16 @@ func (h *Handler) HandleBet(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid bet request") } - resp, err := h.virtualGameSvc.ProcessBet(c.Context(), &req) - if err != nil { - code := fiber.StatusInternalServerError - if err.Error() == "invalid token" { - code = fiber.StatusUnauthorized - } else if err.Error() == "insufficient balance" { - code = fiber.StatusBadRequest - } - return fiber.NewError(code, err.Error()) - } + resp, _ := h.virtualGameSvc.ProcessBet(c.Context(), &req) + // if err != nil { + // code := fiber.StatusInternalServerError + // // if err.Error() == "invalid token" { + // // code = fiber.StatusUnauthorized + // // } else if err.Error() == "insufficient balance" { + // // code = fiber.StatusBadRequest + // // } + // return fiber.NewError(code, err.Error()) + // } return response.WriteJSON(c, fiber.StatusOK, "Bet processed", resp, nil) } @@ -123,14 +131,14 @@ func (h *Handler) HandleWin(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid win request") } - resp, err := h.virtualGameSvc.ProcessWin(c.Context(), &req) - if err != nil { - code := fiber.StatusInternalServerError - if err.Error() == "invalid token" { - code = fiber.StatusUnauthorized - } - return fiber.NewError(code, err.Error()) - } + resp, _ := h.virtualGameSvc.ProcessWin(c.Context(), &req) + // if err != nil { + // code := fiber.StatusInternalServerError + // if err.Error() == "invalid token" { + // code = fiber.StatusUnauthorized + // } + // return fiber.NewError(code, err.Error()) + // } return response.WriteJSON(c, fiber.StatusOK, "Win processed", resp, nil) } @@ -141,17 +149,177 @@ func (h *Handler) HandleCancel(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid cancel request") } - resp, err := h.virtualGameSvc.ProcessCancel(c.Context(), &req) - if err != nil { - code := fiber.StatusInternalServerError - switch err.Error() { - case "invalid token": - code = fiber.StatusUnauthorized - case "original bet not found", "invalid original transaction": - code = fiber.StatusBadRequest - } - return fiber.NewError(code, err.Error()) - } + resp, _ := h.virtualGameSvc.ProcessCancel(c.Context(), &req) + // if err != nil { + // code := fiber.StatusInternalServerError + // switch err.Error() { + // case "invalid token": + // code = fiber.StatusUnauthorized + // case "original bet not found", "invalid original transaction": + // code = fiber.StatusBadRequest + // } + // return fiber.NewError(code, err.Error()) + // } return response.WriteJSON(c, fiber.StatusOK, "Cancel processed", resp, nil) } + +// GetGameList godoc +// @Summary Get PopOK Games List +// @Description Retrieves the list of available PopOK slot games +// @Tags Virtual Games - PopOK +// @Accept json +// @Produce json +// @Param currency query string false "Currency (e.g. USD, ETB)" default(USD) +// @Success 200 {array} domain.PopOKGame +// @Failure 502 {object} domain.ErrorResponse +// @Router /popok/games [get] +func (h *Handler) GetGameList(c *fiber.Ctx) error { + currency := c.Query("currency", "ETB") // fallback default + + games, err := h.virtualGameSvc.ListGames(c.Context(), currency) + if err != nil { + return fiber.NewError(fiber.StatusBadGateway, "failed to fetch games") + } + return c.JSON(games) +} + +// RecommendGames godoc +// @Summary Recommend virtual games +// @Description Recommends games based on user history or randomly +// @Tags Virtual Games - PopOK +// @Produce json +// @Param user_id query int true "User ID" +// @Success 200 {array} domain.GameRecommendation +// @Failure 500 {object} domain.ErrorResponse +// @Router /popok/games/recommend [get] +func (h *Handler) RecommendGames(c *fiber.Ctx) error { + userIDVal := c.Locals("user_id") + userID, ok := userIDVal.(int64) + if !ok || userID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "invalid user ID") + } + + recommendations, err := h.virtualGameSvc.RecommendGames(c.Context(), userID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "failed to recommend games") + } + + 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/handlers/wallet_handler.go b/internal/web_server/handlers/wallet_handler.go index 443e7ce..853a4af 100644 --- a/internal/web_server/handlers/wallet_handler.go +++ b/internal/web_server/handlers/wallet_handler.go @@ -9,9 +9,6 @@ import ( "github.com/gofiber/fiber/v2" ) -type UpdateWalletActiveReq struct { - IsActive bool `json:"is_active" validate:"required" example:"true"` -} type WalletRes struct { ID int64 `json:"id" example:"1"` Balance float32 `json:"amount" example:"100.0"` @@ -45,9 +42,14 @@ type CustomerWalletRes struct { StaticID int64 `json:"static_id" example:"1"` StaticBalance float32 `json:"static_balance" example:"100.0"` CustomerID int64 `json:"customer_id" example:"1"` + RegularIsActive bool `json:"regular_is_active" example:"true"` + StaticIsActive bool `json:"static_is_active" example:"true"` RegularUpdatedAt time.Time `json:"regular_updated_at"` StaticUpdatedAt time.Time `json:"static_updated_at"` CreatedAt time.Time `json:"created_at"` + FirstName string `json:"first_name" example:"John"` + LastName string `json:"last_name" example:"Smith"` + PhoneNumber string `json:"phone_number" example:"0911111111"` } func ConvertCustomerWallet(wallet domain.GetCustomerWallet) CustomerWalletRes { @@ -58,9 +60,14 @@ func ConvertCustomerWallet(wallet domain.GetCustomerWallet) CustomerWalletRes { StaticID: wallet.StaticID, StaticBalance: wallet.StaticBalance.Float32(), CustomerID: wallet.CustomerID, + RegularIsActive: wallet.RegularIsActive, + StaticIsActive: wallet.StaticIsActive, RegularUpdatedAt: wallet.RegularUpdatedAt, StaticUpdatedAt: wallet.StaticUpdatedAt, CreatedAt: wallet.CreatedAt, + FirstName: wallet.FirstName, + LastName: wallet.LastName, + PhoneNumber: wallet.PhoneNumber, } } @@ -173,7 +180,38 @@ func (h *Handler) GetAllBranchWallets(c *fiber.Ctx) error { } return response.WriteJSON(c, fiber.StatusOK, "All Wallets retrieved", res, nil) +} +// GetAllCustomerWallets godoc +// @Summary Get all customer wallets +// @Description Retrieve all customer wallets +// @Tags wallet +// @Accept json +// @Produce json +// @Success 200 {array} CustomerWalletRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /customerWallet [get] +func (h *Handler) GetAllCustomerWallets(c *fiber.Ctx) error { + + wallets, err := h.walletSvc.GetAllCustomerWallet(c.Context()) + + if err != nil { + h.logger.Error("Failed to get wallets", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve wallets", err, nil) + } + + var res []CustomerWalletRes = make([]CustomerWalletRes, 0, len(wallets)) + + for _, wallet := range wallets { + res = append(res, ConvertCustomerWallet(wallet)) + } + + return response.WriteJSON(c, fiber.StatusOK, "All Wallets retrieved", res, nil) +} + +type UpdateWalletActiveReq struct { + IsActive bool `json:"is_active" validate:"required" example:"true"` } // UpdateWalletActive godoc @@ -255,13 +293,13 @@ func (h *Handler) GetCustomerWallet(c *fiber.Ctx) error { // h.logger.Info("Fetching customer wallet", "userID", userID) - wallet, err := h.walletSvc.GetWalletsByUser(c.Context(), userID) + wallet, err := h.walletSvc.GetCustomerWallet(c.Context(), userID) if err != nil { h.logger.Error("Failed to get customer wallet", "userID", userID, "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve wallet") } - res := convertWallet(wallet[0]) + res := ConvertCustomerWallet(wallet) return response.WriteJSON(c, fiber.StatusOK, "Wallet retrieved successfully", res, nil) } diff --git a/internal/web_server/jwt/jwt.go b/internal/web_server/jwt/jwt.go index 2617873..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,27 +55,24 @@ 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", - IssuedAt: jwt.NewNumericDate(time.Now()), Audience: jwt.ClaimStrings{"popokgaming.com"}, + IssuedAt: jwt.NewNumericDate(time.Now()), NotBefore: jwt.NewNumericDate(time.Now()), ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiry)), }, UserID: userID, - Username: username, + Username: username, // ✅ Must be a valid string Currency: currency, Lang: lang, Mode: mode, SessionID: sessionID, + CompanyID: CompanyID, }) - jwtToken, err := token.SignedString([]byte(key)) - if err != nil { - return "", err - } - return jwtToken, nil + return token.SignedString([]byte(key)) } func ParseJwt(jwtToken string, key string) (*UserClaim, error) { diff --git a/internal/web_server/middleware.go b/internal/web_server/middleware.go index 3a6303d..47dabc3 100644 --- a/internal/web_server/middleware.go +++ b/internal/web_server/middleware.go @@ -87,6 +87,22 @@ func (a *App) CompanyOnly(c *fiber.Ctx) error { return c.Next() } +func (a *App) OnlyAdminAndAbove(c *fiber.Ctx) error { + userRole := c.Locals("role").(domain.Role) + if userRole != domain.RoleSuperAdmin && userRole != domain.RoleAdmin { + return fiber.NewError(fiber.StatusUnauthorized, "Invalid access token") + } + return c.Next() +} + +func (a *App) OnlyBranchManagerAndAbove(c *fiber.Ctx) error { + userRole := c.Locals("role").(domain.Role) + if userRole != domain.RoleSuperAdmin && userRole != domain.RoleAdmin && userRole != domain.RoleBranchManager { + return fiber.NewError(fiber.StatusUnauthorized, "Invalid access token") + } + return c.Next() +} + func (a *App) WebsocketAuthMiddleware(c *fiber.Ctx) error { tokenStr := c.Query("token") if tokenStr == "" { diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 8f10c44..bfd971e 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -20,8 +20,11 @@ import ( func (a *App) initAppRoutes() { h := handlers.New( + a.issueReportingSvc, + a.instSvc, a.currSvc, a.logger, + a.settingSvc, a.NotidicationStore, a.validator, a.reportSvc, @@ -30,7 +33,7 @@ func (a *App) initAppRoutes() { a.referralSvc, a.virtualGameSvc, a.aleaVirtualGameService, - a.veliVirtualGameService, + // a.veliVirtualGameService, a.recommendationSvc, a.userSvc, a.transactionSvc, @@ -45,6 +48,7 @@ func (a *App) initAppRoutes() { a.leagueSvc, *a.resultSvc, a.cfg, + a.mongoLoggerSvc, ) group := a.fiber.Group("/api/v1") @@ -52,7 +56,7 @@ func (a *App) initAppRoutes() { a.fiber.Get("/", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{ "message": "Welcome to the FortuneBet API", - "version": "1.0dev6", + "version": "1.0dev7", }) }) @@ -130,6 +134,7 @@ func (a *App) initAppRoutes() { a.fiber.Get("/events", h.GetAllUpcomingEvents) a.fiber.Get("/events/:id", h.GetUpcomingEventByID) a.fiber.Delete("/events/:id", a.authMiddleware, a.SuperAdminOnly, h.SetEventStatusToRemoved) + a.fiber.Get("/top-leagues", h.GetTopLeagues) // Leagues a.fiber.Get("/leagues", h.GetAllLeagues) @@ -146,6 +151,8 @@ func (a *App) initAppRoutes() { a.fiber.Get("/branch/:id", a.authMiddleware, h.GetBranchByID) a.fiber.Get("/branch/:id/bets", a.authMiddleware, h.GetBetByBranchID) a.fiber.Put("/branch/:id", a.authMiddleware, h.UpdateBranch) + a.fiber.Put("/branch/:id/set-active", a.authMiddleware, h.UpdateBranchStatus) + a.fiber.Put("/branch/:id/set-inactive", a.authMiddleware, h.UpdateBranchStatus) a.fiber.Delete("/branch/:id", a.authMiddleware, h.DeleteBranch) a.fiber.Get("/search/branch", a.authMiddleware, h.SearchBranch) // /branch/search @@ -169,6 +176,7 @@ func (a *App) initAppRoutes() { a.fiber.Delete("/company/:id", a.authMiddleware, a.SuperAdminOnly, h.DeleteCompany) a.fiber.Get("/company/:id/branch", a.authMiddleware, h.GetBranchByCompanyID) a.fiber.Get("/search/company", a.authMiddleware, h.SearchCompany) + a.fiber.Get("/admin-company", a.authMiddleware, h.GetCompanyForAdmin) // Ticket Routes a.fiber.Post("/ticket", h.CreateTicket) @@ -176,20 +184,21 @@ 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) a.fiber.Get("/wallet/:id", h.GetWalletByID) a.fiber.Put("/wallet/:id", h.UpdateWalletActive) a.fiber.Get("/branchWallet", a.authMiddleware, h.GetAllBranchWallets) + a.fiber.Get("/customerWallet", a.authMiddleware, h.GetAllCustomerWallets) a.fiber.Get("/cashierWallet", a.authMiddleware, h.GetWalletForCashier) // Transfer @@ -211,6 +220,8 @@ func (a *App) initAppRoutes() { //Report Routes group.Get("/reports/dashboard", h.GetDashboardReport) + group.Get("/report-files/download/:filename", a.authMiddleware, a.SuperAdminOnly, h.DownloadReportFile) + group.Get("/report-files/list", a.authMiddleware, a.SuperAdminOnly, h.ListReportFiles) //Wallet Monitor Service // group.Get("/debug/wallet-monitor/status", func(c *fiber.Ctx) error { @@ -237,15 +248,15 @@ func (a *App) initAppRoutes() { group.Post("/webhooks/alea-play", a.authMiddleware, h.HandleAleaCallback) //Veli Virtual Game Routes - group.Get("/veli-games/launch", h.LaunchVeliGame) - group.Post("/webhooks/veli-games", h.HandleVeliCallback) + // group.Get("/veli-games/launch", h.LaunchVeliGame) + // group.Post("/webhooks/veli-games", h.HandleVeliCallback) //mongoDB logs ctx := context.Background() group.Get("/logs", a.authMiddleware, a.SuperAdminOnly, handlers.GetLogsHandler(ctx)) // Recommendation Routes - group.Get("/virtual-games/recommendations/:userID", h.GetRecommendations) + // group.Get("/virtual-games/recommendations/:userID", h.GetRecommendations) // Transactions /transactions a.fiber.Post("/transaction", a.authMiddleware, h.CreateTransaction) @@ -268,7 +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 diff --git a/internal/web_server/worker/report.go b/internal/web_server/worker/report.go deleted file mode 100644 index ab6fc6c..0000000 --- a/internal/web_server/worker/report.go +++ /dev/null @@ -1,29 +0,0 @@ -// worker/report_worker.go -package worker - -import ( - "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - "github.com/SamuelTariku/FortuneBet-Backend/internal/infrastructure" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/report" -) - -type ReportWorker struct { - reportService *report.Service - exporter infrastructure.CSVExporter -} - -func NewReportWorker(service *report.Service, exporter infrastructure.CSVExporter) *ReportWorker { - return &ReportWorker{ - reportService: service, - exporter: exporter, - } -} - -func (w *ReportWorker) GenerateAndExport(timeFrame domain.TimeFrame) error { - report, err := w.reportService.GenerateReport(timeFrame) - if err != nil { - return err - } - - return w.exporter.Export(report) -}