diff --git a/cmd/main.go b/cmd/main.go index 3660fa5..0969e35 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -46,6 +46,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/messenger" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/raffle" "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" @@ -114,13 +115,11 @@ func main() { authSvc := authentication.NewService(store, store, cfg.RefreshExpiry) userSvc := user.NewService(store, store, messengerSvc, cfg) - eventSvc := event.New(cfg.Bet365Token, store, domain.MongoDBLogger) + eventSvc := event.New(cfg.Bet365Token, store, *settingSvc, domain.MongoDBLogger, cfg) oddsSvc := odds.New(store, cfg, eventSvc, logger, domain.MongoDBLogger) notificationRepo := repository.NewNotificationRepository(store) virtuaGamesRepo := repository.NewVirtualGameRepository(store) notificationSvc := notificationservice.New(notificationRepo, domain.MongoDBLogger, logger, cfg, messengerSvc, userSvc) - - var notificatioStore notificationservice.NotificationStore // var userStore user.UserStore // Initialize producer @@ -132,7 +131,6 @@ func main() { wallet.WalletStore(store), wallet.TransferStore(store), wallet.DirectDepositStore(store), - notificatioStore, notificationSvc, userSvc, domain.MongoDBLogger, @@ -145,17 +143,18 @@ func main() { leagueSvc := league.New(store) ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger, *settingSvc, notificationSvc) betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, *companySvc, *settingSvc, *userSvc, notificationSvc, logger, domain.MongoDBLogger) - resultSvc := result.NewService(store, cfg, logger, domain.MongoDBLogger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc, *userSvc) - bonusSvc := bonus.NewService(store) + resultSvc := result.NewService(store, cfg, logger, domain.MongoDBLogger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc, messengerSvc, *userSvc) + bonusSvc := bonus.NewService(store, walletSvc, settingSvc, notificationSvc, domain.MongoDBLogger) referalRepo := repository.NewReferralRepository(store) vitualGameRepo := repository.NewVirtualGameRepository(store) recommendationRepo := repository.NewRecommendationRepository(store) - referalSvc := referralservice.New(referalRepo, *walletSvc, store, cfg, logger) + referalSvc := referralservice.New(referalRepo, *walletSvc, *settingSvc, cfg, logger, domain.MongoDBLogger) + raffleSvc := raffle.NewService(store) virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger) aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger) veliCLient := veli.NewClient(cfg, walletSvc) - veliVirtualGameService := veli.New(virtualGameSvc, vitualGameRepo, veliCLient, walletSvc, wallet.TransferStore(store), cfg) + veliVirtualGameService := veli.New(virtualGameSvc, vitualGameRepo, veliCLient, walletSvc, wallet.TransferStore(store), domain.MongoDBLogger, cfg) atlasClient := atlas.NewClient(cfg, walletSvc) atlasVirtualGameService := atlas.New(virtualGameSvc, vitualGameRepo, atlasClient, walletSvc, wallet.TransferStore(store), cfg) recommendationSvc := recommendation.NewService(recommendationRepo) @@ -285,6 +284,7 @@ func main() { eventSvc, leagueSvc, referalSvc, + raffleSvc, bonusSvc, virtualGameSvc, aleaService, diff --git a/db/data/001_initial_seed_data.sql b/db/data/001_initial_seed_data.sql new file mode 100644 index 0000000..95a21d8 --- /dev/null +++ b/db/data/001_initial_seed_data.sql @@ -0,0 +1,379 @@ +CREATE EXTENSION IF NOT EXISTS pgcrypto; +-- Locations Initial Data +INSERT INTO branch_locations (key, value) +VALUES ('addis_ababa', 'Addis Ababa'), + ('dire_dawa', 'Dire Dawa'), + ('mekelle', 'Mekelle'), + ('adama', 'Adama'), + ('awassa', 'Awassa'), + ('bahir_dar', 'Bahir Dar'), + ('gonder', 'Gonder'), + ('dessie', 'Dessie'), + ('jimma', 'Jimma'), + ('jijiga', 'Jijiga'), + ('shashamane', 'Shashamane'), + ('bishoftu', 'Bishoftu'), + ('sodo', 'Sodo'), + ('arba_minch', 'Arba Minch'), + ('hosaena', 'Hosaena'), + ('harar', 'Harar'), + ('dilla', 'Dilla'), + ('nekemte', 'Nekemte'), + ('debre_birhan', 'Debre Birhan'), + ('asella', 'Asella'), + ('debre_markos', 'Debre Markos'), + ('kombolcha', 'Kombolcha'), + ('debre_tabor', 'Debre Tabor'), + ('adigrat', 'Adigrat'), + ('areka', 'Areka'), + ('weldiya', 'Weldiya'), + ('sebeta', 'Sebeta'), + ('burayu', 'Burayu'), + ('shire', 'Shire'), + ('ambo', 'Ambo'), + ('arsi_negele', 'Arsi Negele'), + ('aksum', 'Aksum'), + ('gambela', 'Gambela'), + ('bale_robe', 'Bale Robe'), + ('butajira', 'Butajira'), + ('batu', 'Batu'), + ('boditi', 'Boditi'), + ('adwa', 'Adwa'), + ('yirgalem', 'Yirgalem'), + ('waliso', 'Waliso'), + ('welkite', 'Welkite'), + ('gode', 'Gode'), + ('meki', 'Meki'), + ('negele_borana', 'Negele Borana'), + ('alaba_kulito', 'Alaba Kulito'), + ('alamata,', 'Alamata,'), + ('chiro', 'Chiro'), + ('tepi', 'Tepi'), + ('durame', 'Durame'), + ('goba', 'Goba'), + ('assosa', 'Assosa'), + ('gimbi', 'Gimbi'), + ('wukro', 'Wukro'), + ('haramaya', 'Haramaya'), + ('mizan_teferi', 'Mizan Teferi'), + ('sawla', 'Sawla'), + ('mojo', 'Mojo'), + ('dembi_dolo', 'Dembi Dolo'), + ('aleta_wendo', 'Aleta Wendo'), + ('metu', 'Metu'), + ('mota', 'Mota'), + ('fiche', 'Fiche'), + ('finote_selam', 'Finote Selam'), + ('bule_hora_town', 'Bule Hora Town'), + ('bonga', 'Bonga'), + ('kobo', 'Kobo'), + ('jinka', 'Jinka'), + ('dangila', 'Dangila'), + ('degehabur', 'Degehabur'), + ('bedessa', 'Bedessa'), + ('agaro', 'Agaro') ON CONFLICT (key) DO +UPDATE +SET value = EXCLUDED.value; +-- Settings Initial Data +INSERT INTO global_settings (key, value) +VALUES ('sms_provider', 'afro_message'), + ('max_number_of_outcomes', '30'), + ('bet_amount_limit', '10000000'), + ('daily_ticket_limit', '50'), + ('total_winnings_limit', '1000000'), + ('amount_for_bet_referral', '1000000'), + ('cashback_amount_cap', '1000'), + ('default_winning_limit', '5000000'), + ('referral_reward_amount', '10000'), + ('cashback_percentage', '0.2'), + ('default_max_referrals', '15'), + ('minimum_bet_amount', '100'), + ('bet_duplicate_limit', '5'), + ('send_email_on_bet_finish', 'true'), + ('send_sms_on_bet_finish', 'false'), + ('welcome_bonus_active', 'false'), + ('welcome_bonus_multiplier', '1.5'), + ('welcome_bonus_cap', '100000'), + ('welcome_bonus_count', '3'), + ('welcome_bonus_expiry', '10') ON CONFLICT (key) DO NOTHING; +-- Users +INSERT INTO users ( + id, + first_name, + last_name, + email, + phone_number, + password, + role, + email_verified, + phone_verified, + created_at, + updated_at, + suspended, + company_id + ) +VALUES ( + 1, + 'John', + 'Doe', + 'john.doe@example.com', + NULL, + crypt('password@123', gen_salt('bf'))::bytea, + 'customer', + TRUE, + FALSE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + FALSE, + 1 + ), + ( + 2, + 'Test', + 'Admin', + 'test.admin@gmail.com', + '0988554466', + crypt('password@123', gen_salt('bf'))::bytea, + 'admin', + TRUE, + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + FALSE, + 1 + ), + ( + 3, + 'Samuel', + 'Tariku', + 'cybersamt@gmail.com', + '0911111111', + crypt('password@123', gen_salt('bf'))::bytea, + 'super_admin', + TRUE, + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + FALSE, + NULL + ), + ( + 4, + 'Kirubel', + 'Kibru', + 'kirubel.jkl679@gmail.com', + '0911554486', + crypt('password@123', gen_salt('bf'))::bytea, + 'super_admin', + TRUE, + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + FALSE, + NULL + ) ON CONFLICT (id) DO +UPDATE +SET first_name = EXCLUDED.first_name, + last_name = EXCLUDED.last_name, + email = EXCLUDED.email, + phone_number = EXCLUDED.phone_number, + password = EXCLUDED.password, + role = EXCLUDED.role, + email_verified = EXCLUDED.email_verified, + phone_verified = EXCLUDED.phone_verified, + created_at = EXCLUDED.created_at, + updated_at = EXCLUDED.updated_at, + suspended = EXCLUDED.suspended, + company_id = EXCLUDED.company_id; +-- Supported Operations +INSERT INTO supported_operations (id, name, description) +VALUES (1, 'SportBook', 'Sportbook operations'), + (2, 'Virtual', 'Virtual operations') ON CONFLICT (id) DO +UPDATE +SET name = EXCLUDED.name, + description = EXCLUDED.description; +-- Wallets +INSERT INTO wallets ( + id, + balance, + is_withdraw, + is_bettable, + is_transferable, + user_id, + type, + currency, + is_active, + created_at, + updated_at + ) +VALUES ( + 1, + 10000, + TRUE, + TRUE, + TRUE, + 1, + 'regular_wallet', + 'ETB', + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ), + ( + 2, + 5000, + FALSE, + TRUE, + TRUE, + 1, + 'static_wallet', + 'ETB', + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ), + ( + 3, + 100000000, + TRUE, + TRUE, + TRUE, + 2, + 'company_wallet', + 'ETB', + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ), + ( + 4, + 50000000, + TRUE, + TRUE, + TRUE, + 2, + 'branch_wallet', + 'ETB', + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ) ON CONFLICT (id) DO +UPDATE +SET balance = EXCLUDED.balance, + is_withdraw = EXCLUDED.is_withdraw, + is_bettable = EXCLUDED.is_bettable, + is_transferable = EXCLUDED.is_transferable, + user_id = EXCLUDED.user_id, + type = EXCLUDED.type, + currency = EXCLUDED.currency, + is_active = EXCLUDED.is_active, + created_at = EXCLUDED.created_at, + updated_at = EXCLUDED.updated_at; +-- Customer Wallets +INSERT INTO customer_wallets ( + id, + customer_id, + regular_wallet_id, + static_wallet_id + ) +VALUES (1, 1, 1, 2) ON CONFLICT (id) DO +UPDATE +SET customer_id = EXCLUDED.customer_id, + regular_wallet_id = EXCLUDED.regular_wallet_id, + static_wallet_id = EXCLUDED.static_wallet_id; +-- Company +INSERT INTO companies ( + id, + name, + slug, + admin_id, + wallet_id, + deducted_percentage, + is_active, + created_at, + updated_at + ) +VALUES ( + 1, + 'FortuneBets', + 'fortunebets', + 2, + 3, + 0.10, + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ) ON CONFLICT (id) DO +UPDATE +SET name = EXCLUDED.name, + slug = EXCLUDED.slug, + admin_id = EXCLUDED.admin_id, + wallet_id = EXCLUDED.wallet_id, + deducted_percentage = EXCLUDED.deducted_percentage, + is_active = EXCLUDED.is_active, + created_at = EXCLUDED.created_at, + updated_at = EXCLUDED.updated_at; +-- Branch +INSERT INTO branches ( + id, + name, + location, + wallet_id, + branch_manager_id, + company_id, + is_self_owned, + profit_percent, + is_active, + created_at, + updated_at + ) +VALUES ( + 1, + 'Test Branch', + 'addis_ababa', + 4, + 2, + 1, + TRUE, + 0.10, + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ) ON CONFLICT (id) DO +UPDATE +SET name = EXCLUDED.name, + location = EXCLUDED.location, + wallet_id = EXCLUDED.wallet_id, + branch_manager_id = EXCLUDED.branch_manager_id, + company_id = EXCLUDED.company_id, + is_self_owned = EXCLUDED.is_self_owned, + profit_percent = EXCLUDED.profit_percent, + is_active = EXCLUDED.is_active, + created_at = EXCLUDED.created_at, + updated_at = EXCLUDED.updated_at; +-- Bonus +INSERT INTO user_bonuses ( + id, + name, + description, + type, + user_id, + reward_amount, + expires_at + ) +VALUES ( + 1, + 'Welcome Bonus', + 'Awarded for deposit number (1 / 3)', + 'welcome_bonus', + 1, + 1000, + now() + INTERVAL '1 day' + ) ON CONFLICT (id) DO +UPDATE +SET name = EXCLUDED.name, + description = EXCLUDED.description, + type = EXCLUDED.type, + user_id = EXCLUDED.user_id, + reward_amount = EXCLUDED.reward_amount, + expires_at = EXCLUDED.expires_at; \ No newline at end of file diff --git a/db/data/002_veli_user.sql b/db/data/002_veli_user.sql new file mode 100644 index 0000000..1dfe96a --- /dev/null +++ b/db/data/002_veli_user.sql @@ -0,0 +1,148 @@ +-- Users +INSERT INTO users ( + id, + first_name, + last_name, + email, + phone_number, + password, + role, + email_verified, + phone_verified, + created_at, + updated_at, + suspended, + company_id + ) +VALUES ( + 5, + 'Test', + 'Veli', + 'test.veli@example.com', + NULL, + crypt('password@123', gen_salt('bf'))::bytea, + 'customer', + TRUE, + FALSE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + FALSE, + 1 + ), + ( + 6, + 'Kirubel', + 'Kibru', + 'modernkibru@gmail.com', + NULL, + crypt('password@123', gen_salt('bf'))::bytea, + 'customer', + TRUE, + FALSE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + FALSE, + 1 + ) ON CONFLICT (id) DO +UPDATE +SET first_name = EXCLUDED.first_name, + last_name = EXCLUDED.last_name, + email = EXCLUDED.email, + phone_number = EXCLUDED.phone_number, + password = EXCLUDED.password, + role = EXCLUDED.role, + email_verified = EXCLUDED.email_verified, + phone_verified = EXCLUDED.phone_verified, + created_at = EXCLUDED.created_at, + updated_at = EXCLUDED.updated_at, + suspended = EXCLUDED.suspended, + company_id = EXCLUDED.company_id; +INSERT INTO wallets ( + id, + balance, + is_withdraw, + is_bettable, + is_transferable, + user_id, + type, + currency, + is_active, + created_at, + updated_at + ) +VALUES ( + 5, + 10000, + TRUE, + TRUE, + TRUE, + 1, + 'regular_wallet', + 'ETB', + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ), + ( + 6, + 5000, + FALSE, + TRUE, + TRUE, + 1, + 'static_wallet', + 'ETB', + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ), + ( + 7, + 1000000, + TRUE, + TRUE, + TRUE, + 1, + 'regular_wallet', + 'ETB', + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ), + ( + 8, + 5000, + FALSE, + TRUE, + TRUE, + 1, + 'static_wallet', + 'ETB', + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ) ON CONFLICT (id) DO +UPDATE +SET balance = EXCLUDED.balance, + is_withdraw = EXCLUDED.is_withdraw, + is_bettable = EXCLUDED.is_bettable, + is_transferable = EXCLUDED.is_transferable, + user_id = EXCLUDED.user_id, + type = EXCLUDED.type, + currency = EXCLUDED.currency, + is_active = EXCLUDED.is_active, + created_at = EXCLUDED.created_at, + updated_at = EXCLUDED.updated_at; +-- Customer Wallets +INSERT INTO customer_wallets ( + id, + customer_id, + regular_wallet_id, + static_wallet_id + ) +VALUES (2, 5, 5, 6), + (3, 6, 7, 8) ON CONFLICT (id) DO +UPDATE +SET customer_id = EXCLUDED.customer_id, + regular_wallet_id = EXCLUDED.regular_wallet_id, + static_wallet_id = EXCLUDED.static_wallet_id; \ No newline at end of file diff --git a/db/data/003_fix_autoincrement_desync.sql b/db/data/003_fix_autoincrement_desync.sql new file mode 100644 index 0000000..835e10e --- /dev/null +++ b/db/data/003_fix_autoincrement_desync.sql @@ -0,0 +1,31 @@ +-- For each table with an id sequence +SELECT setval( + pg_get_serial_sequence('users', 'id'), + COALESCE(MAX(id), 1) + ) +FROM users; +SELECT setval( + pg_get_serial_sequence('wallets', 'id'), + COALESCE(MAX(id), 1) + ) +FROM wallets; +SELECT setval( + pg_get_serial_sequence('customer_wallets', 'id'), + COALESCE(MAX(id), 1) + ) +FROM customer_wallets; +SELECT setval( + pg_get_serial_sequence('companies', 'id'), + COALESCE(MAX(id), 1) + ) +FROM companies; +SELECT setval( + pg_get_serial_sequence('branches', 'id'), + COALESCE(MAX(id), 1) + ) +FROM branches; +SELECT setval( + pg_get_serial_sequence('supported_operations', 'id'), + COALESCE(MAX(id), 1) + ) +FROM supported_operations; \ No newline at end of file diff --git a/db/data/seed_data.sql b/db/data/seed_data.sql deleted file mode 100644 index 860d7e5..0000000 --- a/db/data/seed_data.sql +++ /dev/null @@ -1,222 +0,0 @@ -BEGIN; -CREATE EXTENSION IF NOT EXISTS pgcrypto; --- Users -INSERT INTO users ( - id, - first_name, - last_name, - email, - phone_number, - password, - role, - email_verified, - phone_verified, - created_at, - updated_at, - suspended, - company_id - ) -VALUES ( - 1, - 'John', - 'Doe', - 'john.doe@example.com', - NULL, - crypt('password@123', gen_salt('bf'))::bytea, - 'customer', - TRUE, - FALSE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP, - FALSE, - NULL - ), - ( - 2, - 'Test', - 'Admin', - 'test.admin@gmail.com', - '0988554466', - crypt('password123', gen_salt('bf'))::bytea, - 'admin', - TRUE, - TRUE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP, - FALSE, - 1 - ), - ( - 3, - 'Samuel', - 'Tariku', - 'cybersamt@gmail.com', - '0911111111', - crypt('password@123', gen_salt('bf'))::bytea, - 'super_admin', - TRUE, - TRUE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP, - FALSE, - NULL - ), - ( - 4, - 'Kirubel', - 'Kibru', - 'kirubel.jkl679@gmail.com', - '0911554486', - crypt('password@123', gen_salt('bf'))::bytea, - 'super_admin', - TRUE, - TRUE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP, - FALSE, - NULL - ), - ( - 5, - 'Test', - 'Veli', - 'test.veli@example.com', - NULL, - crypt('password@123', gen_salt('bf'))::bytea, - 'customer', - TRUE, - FALSE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP, - FALSE, - NULL - ); --- Supported Operations -INSERT INTO supported_operations (id, name, description) -VALUES (1, 'SportBook', 'Sportbook operations'), - (2, 'Virtual', 'Virtual operations'); --- Wallets -INSERT INTO wallets ( - id, - balance, - is_withdraw, - is_bettable, - is_transferable, - user_id, - type, - currency, - is_active, - created_at, - updated_at - ) -VALUES ( - 1, - 10000, - TRUE, - TRUE, - TRUE, - 1, - 'regular', - 'ETB', - TRUE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP - ), - ( - 2, - 5000, - FALSE, - TRUE, - TRUE, - 1, - 'static', - 'ETB', - TRUE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP - ), - ( - 3, - 20000, - TRUE, - TRUE, - TRUE, - 2, - 'company_main', - 'ETB', - TRUE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP - ), - ( - 4, - 15000, - TRUE, - TRUE, - TRUE, - 2, - 'branch_main', - 'ETB', - TRUE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP - ); --- Customer Wallets -INSERT INTO customer_wallets ( - id, - customer_id, - regular_wallet_id, - static_wallet_id - ) -VALUES (1, 1, 1, 2); --- Company -INSERT INTO companies ( - id, - name, - slug, - admin_id, - wallet_id, - deducted_percentage, - is_active, - created_at, - updated_at - ) -VALUES ( - 1, - 'FortuneBets', - 'fortunebets', - 2, - 3, - 0.10, - TRUE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP - ); --- Branch -INSERT INTO branches ( - id, - name, - location, - wallet_id, - branch_manager_id, - company_id, - is_self_owned, - profit_percent, - is_active, - created_at, - updated_at - ) -VALUES ( - 1, - 'Test Branch', - 'addis_ababa', - 4, - 2, - 1, - TRUE, - 0.10, - TRUE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP - ); -COMMIT; \ No newline at end of file diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 710fbd7..c6c550d 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -18,42 +18,42 @@ CREATE TABLE IF NOT EXISTS users ( email IS NOT NULL OR phone_number IS NOT NULL ), - UNIQUE(email, company_id), + UNIQUE (email, company_id), UNIQUE (phone_number, company_id) ); - CREATE TABLE IF NOT EXISTS virtual_game_providers ( id BIGSERIAL PRIMARY KEY, - provider_id VARCHAR(100) UNIQUE NOT NULL, -- providerId from Veli Games - provider_name VARCHAR(255) NOT NULL, -- providerName - logo_dark TEXT, -- logoForDark (URL) - logo_light TEXT, -- logoForLight (URL) - enabled BOOLEAN NOT NULL DEFAULT TRUE, -- allow enabling/disabling providers + provider_id VARCHAR(100) UNIQUE NOT NULL, + -- providerId from Veli Games + provider_name VARCHAR(255) NOT NULL, + -- providerName + logo_dark TEXT, + -- logoForDark (URL) + logo_light TEXT, + -- logoForLight (URL) + enabled BOOLEAN NOT NULL DEFAULT TRUE, + -- allow enabling/disabling providers created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ ); - CREATE TABLE IF NOT EXISTS virtual_games ( id BIGSERIAL PRIMARY KEY, - game_id VARCHAR(150) NOT NULL, + game_id VARCHAR(150) NOT NULL, provider_id VARCHAR(100) NOT NULL REFERENCES virtual_game_providers(provider_id) ON DELETE CASCADE, - name VARCHAR(255) NOT NULL, - category VARCHAR(100), - device_type VARCHAR(100), - volatility VARCHAR(50), - rtp NUMERIC(5,2), + name VARCHAR(255) NOT NULL, + category VARCHAR(100), + device_type VARCHAR(100), + volatility VARCHAR(50), + rtp NUMERIC(5, 2), has_demo BOOLEAN DEFAULT FALSE, has_free_bets BOOLEAN DEFAULT FALSE, - bets NUMERIC[] DEFAULT '{}', - thumbnail TEXT, - status INT, + bets NUMERIC [] DEFAULT '{}', + thumbnail TEXT, + status INT, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ ); - -CREATE UNIQUE INDEX IF NOT EXISTS ux_virtual_games_provider_game - ON virtual_games (provider_id, game_id); - +CREATE UNIQUE INDEX IF NOT EXISTS ux_virtual_games_provider_game ON virtual_games (provider_id, game_id); CREATE TABLE IF NOT EXISTS wallets ( id BIGSERIAL PRIMARY KEY, balance BIGINT NOT NULL DEFAULT 0, @@ -62,7 +62,14 @@ CREATE TABLE IF NOT EXISTS wallets ( is_bettable BOOLEAN NOT NULL, is_transferable BOOLEAN NOT NULL, user_id BIGINT NOT NULL, - type VARCHAR(255) NOT NULL, + type TEXT NOT NULL CHECK ( + type IN ( + 'regular_wallet', + 'static_wallet', + 'branch_wallet', + 'company_wallet' + ) + ), is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP @@ -118,7 +125,7 @@ CREATE TABLE exchange_rates ( to_currency VARCHAR(3) NOT NULL, rate DECIMAL(19, 6) NOT NULL, valid_until TIMESTAMP NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_at TIMESTAMP NOT NULL DEFAULT NOW (), UNIQUE (from_currency, to_currency) ); CREATE TABLE IF NOT EXISTS bet_outcomes ( @@ -186,7 +193,8 @@ CREATE TABLE IF NOT EXISTS wallets ( type VARCHAR(255) NOT NULL, is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT balance_positve CHECK (balance >= 0) ); CREATE TABLE IF NOT EXISTS customer_wallets ( id BIGSERIAL PRIMARY KEY, @@ -242,9 +250,9 @@ CREATE TABLE IF NOT EXISTS shop_bets ( cashed_out BOOLEAN DEFAULT FALSE NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(shop_transaction_id), - UNIQUE(bet_id), - UNIQUE(cashout_id) + UNIQUE (shop_transaction_id), + UNIQUE (bet_id), + UNIQUE (cashout_id) ); CREATE TABLE IF NOT EXISTS shop_deposits ( id BIGSERIAL PRIMARY KEY, @@ -254,7 +262,7 @@ CREATE TABLE IF NOT EXISTS shop_deposits ( branch_wallet_id BIGINT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(shop_transaction_id) + UNIQUE (shop_transaction_id) ); CREATE TABLE IF NOT EXISTS branches ( id BIGSERIAL PRIMARY KEY, @@ -268,7 +276,7 @@ CREATE TABLE IF NOT EXISTS branches ( is_self_owned BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(wallet_id), + UNIQUE (wallet_id), CONSTRAINT profit_percentage_check CHECK ( profit_percent >= 0 AND profit_percent < 1 @@ -285,12 +293,9 @@ CREATE TABLE IF NOT EXISTS branch_cashiers ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, branch_id BIGINT NOT NULL, - UNIQUE(user_id, branch_id) -); -CREATE TABLE IF NOT EXISTS branch_locations ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL + UNIQUE (user_id, branch_id) ); +CREATE TABLE IF NOT EXISTS branch_locations (key TEXT PRIMARY KEY, value TEXT NOT NULL); CREATE TABLE events ( id TEXT PRIMARY KEY, sport_id INT NOT NULL, @@ -311,21 +316,15 @@ CREATE TABLE events ( match_period INT, is_live BOOLEAN NOT NULL DEFAULT false, status TEXT NOT NULL, - fetched_at TIMESTAMP DEFAULT now(), + fetched_at TIMESTAMP DEFAULT now (), source TEXT NOT NULL DEFAULT 'b365api' CHECK ( - source IN ( - 'b365api', - 'bfair', - '1xbet', - 'bwin', - 'enetpulse' - ) + source IN ('b365api', 'bfair', '1xbet', 'bwin', 'enetpulse') ), default_is_active BOOLEAN NOT NULL DEFAULT true, default_is_featured BOOLEAN NOT NULL DEFAULT false, - default_winning_upper_limit INT NOT NULL, + default_winning_upper_limit BIGINT NOT NULL, is_monitored BOOLEAN NOT NULL DEFAULT FALSE, - UNIQUE(id, source) + UNIQUE (id, source) ); CREATE TABLE event_history ( id BIGSERIAL PRIMARY KEY, @@ -341,7 +340,7 @@ CREATE TABLE company_event_settings ( is_featured BOOLEAN, winning_upper_limit INT, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(company_id, event_id) + UNIQUE (company_id, event_id) ); CREATE TABLE odds_market ( id BIGSERIAL PRIMARY KEY, @@ -352,13 +351,13 @@ CREATE TABLE odds_market ( market_id TEXT NOT NULL, raw_odds JSONB NOT NULL, default_is_active BOOLEAN NOT NULL DEFAULT true, - fetched_at TIMESTAMP DEFAULT now(), + fetched_at TIMESTAMP DEFAULT now (), expires_at TIMESTAMP NOT NULL, UNIQUE (event_id, market_id) ); CREATE TABLE odd_history ( id BIGSERIAL PRIMARY KEY, - odds_market_id BIGINT NOT NULL REFERENCES odds_market(id), + odds_market_id BIGINT NOT NULL REFERENCES odds_market (id), raw_odd_id BIGINT NOT NULL, market_id TEXT NOT NULL, event_id TEXT NOT NULL, @@ -380,7 +379,7 @@ CREATE TABLE company_odd_settings ( is_active BOOLEAN, custom_raw_odds JSONB, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(company_id, odds_market_id) + UNIQUE (company_id, odds_market_id) ); CREATE TABLE result_log ( id BIGSERIAL PRIMARY KEY, @@ -430,7 +429,7 @@ CREATE TABLE company_league_settings ( is_active BOOLEAN, is_featured BOOLEAN, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(league_id, company_id) + UNIQUE (league_id, company_id) ); CREATE TABLE teams ( id BIGSERIAL PRIMARY KEY, @@ -447,24 +446,32 @@ CREATE TABLE IF NOT EXISTS global_settings ( ); -- Tenant/Company-specific overrides CREATE TABLE IF NOT EXISTS company_settings ( - company_id BIGINT NOT NULL REFERENCES companies(id) ON DELETE CASCADE, + company_id BIGINT NOT NULL REFERENCES companies (id) ON DELETE CASCADE, key TEXT NOT NULL, value TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (company_id, key) ); -CREATE TABLE bonus ( - multiplier REAL NOT NULL, +CREATE TABLE user_bonuses ( id BIGSERIAL PRIMARY KEY, - balance_cap BIGINT NOT NULL DEFAULT 0 + name TEXT NOT NULL, + description TEXT NOT NULL, + type TEXT NOT NULL, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + reward_amount BIGINT NOT NULL, + is_claimed BOOLEAN NOT NULL DEFAULT false, + expires_at TIMESTAMP NOT NULL, + claimed_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE flags ( id BIGSERIAL PRIMARY KEY, - bet_id BIGINT REFERENCES bets(id) ON DELETE CASCADE, - odds_market_id BIGINT REFERENCES odds_market(id), + bet_id BIGINT REFERENCES bets (id) ON DELETE CASCADE, + odds_market_id BIGINT REFERENCES odds_market (id), reason TEXT, - flagged_at TIMESTAMP DEFAULT NOW(), + flagged_at TIMESTAMP DEFAULT NOW (), resolved BOOLEAN DEFAULT FALSE, -- either bet or odd is flagged (not at the same time) CHECK ( @@ -480,21 +487,56 @@ CREATE TABLE flags ( ); CREATE TABLE direct_deposits ( id BIGSERIAL PRIMARY KEY, - customer_id BIGINT NOT NULL REFERENCES users(id), - wallet_id BIGINT NOT NULL REFERENCES wallets(id), + customer_id BIGINT NOT NULL REFERENCES users (id), + wallet_id BIGINT NOT NULL REFERENCES wallets (id), amount NUMERIC(15, 2) NOT NULL, bank_reference TEXT NOT NULL, sender_account TEXT NOT NULL, status TEXT NOT NULL CHECK (status IN ('pending', 'completed', 'rejected')), - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - verified_by BIGINT REFERENCES users(id), + created_at TIMESTAMP NOT NULL DEFAULT NOW (), + verified_by BIGINT REFERENCES users (id), verification_notes TEXT, verified_at TIMESTAMP ); -CREATE INDEX idx_direct_deposits_status ON direct_deposits(status); -CREATE INDEX idx_direct_deposits_customer ON direct_deposits(customer_id); -CREATE INDEX idx_direct_deposits_reference ON direct_deposits(bank_reference); --- Views +CREATE INDEX idx_direct_deposits_status ON direct_deposits (status); +CREATE INDEX idx_direct_deposits_customer ON direct_deposits (customer_id); +CREATE INDEX idx_direct_deposits_reference ON direct_deposits (bank_reference); +CREATE TABLE IF NOT EXISTS raffles ( + id SERIAL PRIMARY KEY, + company_id INT NOT NULL, + name VARCHAR(255) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + expires_at TIMESTAMP NOT NULL, + type VARCHAR(50) NOT NULL CHECK (type IN ('virtual', 'sport')), + status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'completed')) +); +CREATE TABLE IF NOT EXISTS raffle_tickets ( + id SERIAL PRIMARY KEY, + raffle_id INT NOT NULL REFERENCES raffles(id) ON DELETE CASCADE, + user_id INT NOT NULL, + is_active BOOL DEFAULT true +); +CREATE TABLE IF NOT EXISTS raffle_winners ( + id SERIAL PRIMARY KEY, + raffle_id INT NOT NULL REFERENCES raffles(id) ON DELETE CASCADE, + user_id INT NOT NULL, + rank INT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); +CREATE TABLE IF NOT EXISTS raffle_sport_filters ( + id SERIAL PRIMARY KEY, + raffle_id INT NOT NULL REFERENCES raffles(id) ON DELETE CASCADE, + sport_id BIGINT NOT NULL, + league_id BIGINT NOT NULL, + CONSTRAINT unique_raffle_sport_league UNIQUE (raffle_id, sport_id, league_id) +); +CREATE TABLE IF NOT EXISTS raffle_game_filters ( + id SERIAL PRIMARY KEY, + raffle_id INT NOT NULL REFERENCES raffles(id) ON DELETE CASCADE, + game_id VARCHAR(150) NOT NULL, + CONSTRAINT unique_raffle_game UNIQUE (raffle_id, game_id) +); +------ Views CREATE VIEW companies_details AS SELECT companies.*, wallets.balance, @@ -508,7 +550,7 @@ FROM companies ; CREATE VIEW branch_details AS SELECT branches.*, - CONCAT(users.first_name, ' ', users.last_name) AS manager_name, + CONCAT (users.first_name, ' ', users.last_name) AS manager_name, users.phone_number AS manager_phone_number, wallets.balance, wallets.is_active AS wallet_is_active @@ -522,9 +564,9 @@ CREATE TABLE IF NOT EXISTS supported_operations ( ); CREATE VIEW bet_with_outcomes AS SELECT bets.*, - CONCAT(users.first_name, ' ', users.last_name) AS full_name, + CONCAT (users.first_name, ' ', users.last_name) AS full_name, users.phone_number, - JSON_AGG(bet_outcomes.*) AS outcomes + JSON_AGG (bet_outcomes.*) AS outcomes FROM bets LEFT JOIN bet_outcomes ON bets.id = bet_outcomes.bet_id LEFT JOIN users ON bets.user_id = users.id @@ -534,7 +576,7 @@ GROUP BY bets.id, users.phone_number; CREATE VIEW ticket_with_outcomes AS SELECT tickets.*, - JSON_AGG(ticket_outcomes.*) AS outcomes + JSON_AGG (ticket_outcomes.*) AS outcomes FROM tickets LEFT JOIN ticket_outcomes ON tickets.id = ticket_outcomes.ticket_id GROUP BY tickets.id; @@ -588,7 +630,7 @@ SELECT sb.*, st.verified AS transaction_verified, bets.status, bets.total_odds, - JSON_AGG(bet_outcomes.*) AS outcomes + JSON_AGG (bet_outcomes.*) AS outcomes FROM shop_bets AS sb JOIN shop_transactions st ON st.id = sb.shop_transaction_id JOIN bets ON bets.id = sb.bet_id @@ -619,7 +661,7 @@ SELECT l.*, COALESCE(cls.is_featured, l.default_is_featured) AS is_featured, cls.updated_at FROM leagues l - JOIN company_league_settings cls ON l.id = cls.league_id; + LEFT JOIN company_league_settings cls ON l.id = cls.league_id; CREATE VIEW event_with_settings AS SELECT e.*, ces.company_id, @@ -632,7 +674,7 @@ SELECT e.*, ces.updated_at, l.country_code as league_cc FROM events e - JOIN company_event_settings ces ON e.id = ces.event_id + LEFT JOIN company_event_settings ces ON e.id = ces.event_id JOIN leagues l ON l.id = e.league_id; CREATE VIEW event_with_country AS SELECT events.*, @@ -654,7 +696,7 @@ SELECT o.id, COALESCE(cos.custom_raw_odds, o.raw_odds) AS raw_odds, cos.updated_at FROM odds_market o - JOIN company_odd_settings cos ON o.id = cos.odds_market_id; + LEFT JOIN company_odd_settings cos ON o.id = cos.odds_market_id; CREATE VIEW odds_market_with_event AS SELECT o.*, e.is_monitored, @@ -665,47 +707,47 @@ FROM odds_market o JOIN events e ON o.event_id = e.id; -- Foreign Keys 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); 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); ALTER TABLE customer_wallets -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); +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_sender_wallet FOREIGN KEY (sender_wallet_id) REFERENCES wallets(id), - ADD CONSTRAINT fk_wallet_transfer_cashier FOREIGN KEY (cashier_id) REFERENCES users(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 shop_transactions -ADD CONSTRAINT fk_shop_transactions_branches FOREIGN KEY (branch_id) REFERENCES branches(id), - ADD CONSTRAINT fk_shop_transactions_users FOREIGN KEY (user_id) REFERENCES users(id); +ADD CONSTRAINT fk_shop_transactions_branches FOREIGN KEY (branch_id) REFERENCES branches (id), + ADD CONSTRAINT fk_shop_transactions_users FOREIGN KEY (user_id) REFERENCES users (id); ALTER TABLE shop_bets -ADD CONSTRAINT fk_shop_bet_transactions FOREIGN KEY (shop_transaction_id) REFERENCES shop_transactions(id), - ADD CONSTRAINT fk_shop_bet_bets FOREIGN KEY (bet_id) REFERENCES bets(id); +ADD CONSTRAINT fk_shop_bet_transactions FOREIGN KEY (shop_transaction_id) REFERENCES shop_transactions (id), + ADD CONSTRAINT fk_shop_bet_bets FOREIGN KEY (bet_id) REFERENCES bets (id); ALTER TABLE shop_deposits -ADD CONSTRAINT fk_shop_deposit_transactions FOREIGN KEY (shop_transaction_id) REFERENCES shop_transactions(id), - ADD CONSTRAINT fk_shop_deposit_customers FOREIGN KEY (customer_id) REFERENCES users(id); +ADD CONSTRAINT fk_shop_deposit_transactions FOREIGN KEY (shop_transaction_id) REFERENCES shop_transactions (id), + ADD CONSTRAINT fk_shop_deposit_customers FOREIGN KEY (customer_id) REFERENCES users (id); ALTER TABLE branches -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), - ADD CONSTRAINT fk_branches_location FOREIGN KEY (location) REFERENCES branch_locations(key); +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), + ADD CONSTRAINT fk_branches_location FOREIGN KEY (location) REFERENCES branch_locations (key); 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_branches FOREIGN KEY (branch_id) REFERENCES branches(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_branches FOREIGN KEY (branch_id) REFERENCES branches(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_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE; +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; ALTER TABLE company_league_settings -ADD CONSTRAINT fk_league_settings_company FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, - ADD CONSTRAINT fk_league_settings_league FOREIGN KEY (league_id) REFERENCES leagues(id) ON DELETE CASCADE; +ADD CONSTRAINT fk_league_settings_company FOREIGN KEY (company_id) REFERENCES companies (id) ON DELETE CASCADE, + ADD CONSTRAINT fk_league_settings_league FOREIGN KEY (league_id) REFERENCES leagues (id) ON DELETE CASCADE; ALTER TABLE company_event_settings -ADD CONSTRAINT fk_event_settings_company FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, - ADD CONSTRAINT fk_event_settings_event FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE; +ADD CONSTRAINT fk_event_settings_company FOREIGN KEY (company_id) REFERENCES companies (id) ON DELETE CASCADE, + ADD CONSTRAINT fk_event_settings_event FOREIGN KEY (event_id) REFERENCES events (id) ON DELETE CASCADE; ALTER TABLE company_odd_settings -ADD CONSTRAINT fk_odds_settings_company FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, - ADD CONSTRAINT fk_odds_settings_odds_market FOREIGN KEY (odds_market_id) REFERENCES odds_market(id) ON DELETE CASCADE; \ No newline at end of file +ADD CONSTRAINT fk_odds_settings_company FOREIGN KEY (company_id) REFERENCES companies (id) ON DELETE CASCADE, + ADD CONSTRAINT fk_odds_settings_odds_market FOREIGN KEY (odds_market_id) REFERENCES odds_market (id) ON DELETE CASCADE; diff --git a/db/migrations/000002_notification.up.sql b/db/migrations/000002_notification.up.sql index 1845f48..8fd9ad8 100644 --- a/db/migrations/000002_notification.up.sql +++ b/db/migrations/000002_notification.up.sql @@ -18,7 +18,8 @@ CREATE TABLE IF NOT EXISTS notifications ( 'admin_alert', 'bet_result', 'transfer_rejected', - 'approval_required' + 'approval_required', + 'bonus_awarded' ) ), level TEXT NOT NULL CHECK (level IN ('info', 'error', 'warning', 'success')), diff --git a/db/migrations/000003_referal.up.sql b/db/migrations/000003_referal.up.sql index 521e3e3..badf443 100644 --- a/db/migrations/000003_referal.up.sql +++ b/db/migrations/000003_referal.up.sql @@ -1,46 +1,37 @@ -CREATE TYPE ReferralStatus AS ENUM ('PENDING', 'COMPLETED', 'EXPIRED', 'CANCELLED'); -CREATE TABLE IF NOT EXISTS referral_settings ( - id BIGSERIAL PRIMARY KEY, - referral_reward_amount DECIMAL(15, 2) NOT NULL DEFAULT 0.00, - cashback_percentage DECIMAL(5, 2) NOT NULL DEFAULT 0.00, - bet_referral_bonus_percentage NUMERIC DEFAULT 5.0, - max_referrals INTEGER NOT NULL DEFAULT 0, - expires_after_days INTEGER NOT NULL DEFAULT 30, - updated_by VARCHAR(255) NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - version INTEGER NOT NULL DEFAULT 0, - CONSTRAINT referral_reward_amount_positive CHECK (referral_reward_amount >= 0), - CONSTRAINT cashback_percentage_range CHECK ( - cashback_percentage >= 0 - AND cashback_percentage <= 100 - ) -); -CREATE TABLE IF NOT EXISTS referrals ( +-- CREATE TYPE ReferralStatus AS ENUM ('PENDING', 'COMPLETED', 'EXPIRED', 'CANCELLED'); +-- CREATE TABLE IF NOT EXISTS referral_settings ( +-- id BIGSERIAL PRIMARY KEY, +-- referral_reward_amount DECIMAL(15, 2) NOT NULL DEFAULT 0.00, +-- cashback_percentage DECIMAL(5, 2) NOT NULL DEFAULT 0.00, +-- bet_referral_bonus_percentage NUMERIC DEFAULT 5.0, +-- max_referrals INTEGER NOT NULL DEFAULT 0, +-- expires_after_days INTEGER NOT NULL DEFAULT 30, +-- updated_by VARCHAR(255) NOT NULL, +-- created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, +-- updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, +-- version INTEGER NOT NULL DEFAULT 0, +-- CONSTRAINT referral_reward_amount_positive CHECK (referral_reward_amount >= 0), +-- CONSTRAINT cashback_percentage_range CHECK ( +-- cashback_percentage >= 0 +-- AND cashback_percentage <= 100 +-- ) +-- ); +CREATE TABLE IF NOT EXISTS referral_codes ( id BIGSERIAL PRIMARY KEY, referral_code VARCHAR(10) NOT NULL UNIQUE, - referrer_id VARCHAR(255) NOT NULL, - referred_id VARCHAR(255) UNIQUE, - status ReferralStatus NOT NULL DEFAULT 'PENDING', - reward_amount DECIMAL(15, 2) NOT NULL DEFAULT 0.00, - cashback_amount DECIMAL(15, 2) NOT NULL DEFAULT 0.00, + referrer_id BIGINT NOT NULL UNIQUE REFERENCES users (id), + company_id BIGINT NOT NULL REFERENCES companies (id), + is_active BOOLEAN NOT NULL DEFAULT true, + number_of_referrals BIGINT NOT NULL, + reward_amount BIGINT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - expires_at TIMESTAMPTZ NOT NULL, - -- FOREIGN KEY (referrer_id) REFERENCES users (id), - -- FOREIGN KEY (referred_id) REFERENCES users (id), - CONSTRAINT reward_amount_positive CHECK (reward_amount >= 0), - CONSTRAINT cashback_amount_positive CHECK (cashback_amount >= 0) + CONSTRAINT reward_amount_positive CHECK (reward_amount >= 0) ); -CREATE INDEX idx_referrals_referral_code ON referrals (referral_code); -CREATE INDEX idx_referrals_referrer_id ON referrals (referrer_id); -CREATE INDEX idx_referrals_status ON referrals (status); -ALTER TABLE users -ADD COLUMN IF NOT EXISTS referral_code VARCHAR(10) UNIQUE, - ADD COLUMN IF NOT EXISTS referred_by VARCHAR(10); --- Modify wallet table to track bonus money separately -ALTER TABLE wallets -ADD COLUMN IF NOT EXISTS bonus_balance DECIMAL(15, 2) NOT NULL DEFAULT 0.00, - ADD COLUMN IF NOT EXISTS cash_balance DECIMAL(15, 2) NOT NULL DEFAULT 0.00, - ADD CONSTRAINT bonus_balance_positive CHECK (bonus_balance >= 0), - ADD CONSTRAINT cash_balance_positive CHECK (cash_balance >= 0); \ No newline at end of file +CREATE INDEX idx_referrals_referrer_id ON referral_codes (referrer_id); +CREATE TABLE IF NOT EXISTS user_referrals ( + id BIGSERIAL PRIMARY KEY, + referred_id BIGINT UNIQUE NOT NULL REFERENCES users (id), + referral_code_id BIGINT NOT NULL REFERENCES referral_codes (id), + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/db/migrations/000007_setting_data.up.sql b/db/migrations/000007_setting_data.up.sql index a381c02..9ae381a 100644 --- a/db/migrations/000007_setting_data.up.sql +++ b/db/migrations/000007_setting_data.up.sql @@ -1,11 +1,11 @@ --- Settings Initial Data -INSERT INTO global_settings (key, value) -VALUES ('sms_provider', 'afro_message'), - ('max_number_of_outcomes', '30'), - ('bet_amount_limit', '10000000'), - ('daily_ticket_limit', '50'), - ('total_winnings_limit', '1000000'), - ('amount_for_bet_referral', '1000000'), - ('cashback_amount_cap', '1000') ON CONFLICT (key) DO -UPDATE -SET value = EXCLUDED.value; \ No newline at end of file +-- -- Settings Initial Data +-- INSERT INTO global_settings (key, value) +-- VALUES ('sms_provider', 'afro_message'), +-- ('max_number_of_outcomes', '30'), +-- ('bet_amount_limit', '10000000'), +-- ('daily_ticket_limit', '50'), +-- ('total_winnings_limit', '1000000'), +-- ('amount_for_bet_referral', '1000000'), +-- ('cashback_amount_cap', '1000') ON CONFLICT (key) DO +-- UPDATE +-- SET value = EXCLUDED.value; \ No newline at end of file diff --git a/db/migrations/000009_location_data.up.sql b/db/migrations/000009_location_data.up.sql index 156831d..1f88a7f 100644 --- a/db/migrations/000009_location_data.up.sql +++ b/db/migrations/000009_location_data.up.sql @@ -1,75 +1,75 @@ --- Locations Initial Data -INSERT INTO branch_locations (key, value) -VALUES ('addis_ababa', 'Addis Ababa'), - ('dire_dawa', 'Dire Dawa'), - ('mekelle', 'Mekelle'), - ('adama', 'Adama'), - ('awassa', 'Awassa'), - ('bahir_dar', 'Bahir Dar'), - ('gonder', 'Gonder'), - ('dessie', 'Dessie'), - ('jimma', 'Jimma'), - ('jijiga', 'Jijiga'), - ('shashamane', 'Shashamane'), - ('bishoftu', 'Bishoftu'), - ('sodo', 'Sodo'), - ('arba_minch', 'Arba Minch'), - ('hosaena', 'Hosaena'), - ('harar', 'Harar'), - ('dilla', 'Dilla'), - ('nekemte', 'Nekemte'), - ('debre_birhan', 'Debre Birhan'), - ('asella', 'Asella'), - ('debre_markos', 'Debre Markos'), - ('kombolcha', 'Kombolcha'), - ('debre_tabor', 'Debre Tabor'), - ('adigrat', 'Adigrat'), - ('areka', 'Areka'), - ('weldiya', 'Weldiya'), - ('sebeta', 'Sebeta'), - ('burayu', 'Burayu'), - ('shire', 'Shire'), - ('ambo', 'Ambo'), - ('arsi_negele', 'Arsi Negele'), - ('aksum', 'Aksum'), - ('gambela', 'Gambela'), - ('bale_robe', 'Bale Robe'), - ('butajira', 'Butajira'), - ('batu', 'Batu'), - ('boditi', 'Boditi'), - ('adwa', 'Adwa'), - ('yirgalem', 'Yirgalem'), - ('waliso', 'Waliso'), - ('welkite', 'Welkite'), - ('gode', 'Gode'), - ('meki', 'Meki'), - ('negele_borana', 'Negele Borana'), - ('alaba_kulito', 'Alaba Kulito'), - ('alamata,', 'Alamata,'), - ('chiro', 'Chiro'), - ('tepi', 'Tepi'), - ('durame', 'Durame'), - ('goba', 'Goba'), - ('assosa', 'Assosa'), - ('gimbi', 'Gimbi'), - ('wukro', 'Wukro'), - ('haramaya', 'Haramaya'), - ('mizan_teferi', 'Mizan Teferi'), - ('sawla', 'Sawla'), - ('mojo', 'Mojo'), - ('dembi_dolo', 'Dembi Dolo'), - ('aleta_wendo', 'Aleta Wendo'), - ('metu', 'Metu'), - ('mota', 'Mota'), - ('fiche', 'Fiche'), - ('finote_selam', 'Finote Selam'), - ('bule_hora_town', 'Bule Hora Town'), - ('bonga', 'Bonga'), - ('kobo', 'Kobo'), - ('jinka', 'Jinka'), - ('dangila', 'Dangila'), - ('degehabur', 'Degehabur'), - ('bedessa', 'Bedessa'), - ('agaro', 'Agaro') ON CONFLICT (key) DO -UPDATE -SET value = EXCLUDED.value; \ No newline at end of file +-- -- Locations Initial Data +-- INSERT INTO branch_locations (key, value) +-- VALUES ('addis_ababa', 'Addis Ababa'), +-- ('dire_dawa', 'Dire Dawa'), +-- ('mekelle', 'Mekelle'), +-- ('adama', 'Adama'), +-- ('awassa', 'Awassa'), +-- ('bahir_dar', 'Bahir Dar'), +-- ('gonder', 'Gonder'), +-- ('dessie', 'Dessie'), +-- ('jimma', 'Jimma'), +-- ('jijiga', 'Jijiga'), +-- ('shashamane', 'Shashamane'), +-- ('bishoftu', 'Bishoftu'), +-- ('sodo', 'Sodo'), +-- ('arba_minch', 'Arba Minch'), +-- ('hosaena', 'Hosaena'), +-- ('harar', 'Harar'), +-- ('dilla', 'Dilla'), +-- ('nekemte', 'Nekemte'), +-- ('debre_birhan', 'Debre Birhan'), +-- ('asella', 'Asella'), +-- ('debre_markos', 'Debre Markos'), +-- ('kombolcha', 'Kombolcha'), +-- ('debre_tabor', 'Debre Tabor'), +-- ('adigrat', 'Adigrat'), +-- ('areka', 'Areka'), +-- ('weldiya', 'Weldiya'), +-- ('sebeta', 'Sebeta'), +-- ('burayu', 'Burayu'), +-- ('shire', 'Shire'), +-- ('ambo', 'Ambo'), +-- ('arsi_negele', 'Arsi Negele'), +-- ('aksum', 'Aksum'), +-- ('gambela', 'Gambela'), +-- ('bale_robe', 'Bale Robe'), +-- ('butajira', 'Butajira'), +-- ('batu', 'Batu'), +-- ('boditi', 'Boditi'), +-- ('adwa', 'Adwa'), +-- ('yirgalem', 'Yirgalem'), +-- ('waliso', 'Waliso'), +-- ('welkite', 'Welkite'), +-- ('gode', 'Gode'), +-- ('meki', 'Meki'), +-- ('negele_borana', 'Negele Borana'), +-- ('alaba_kulito', 'Alaba Kulito'), +-- ('alamata,', 'Alamata,'), +-- ('chiro', 'Chiro'), +-- ('tepi', 'Tepi'), +-- ('durame', 'Durame'), +-- ('goba', 'Goba'), +-- ('assosa', 'Assosa'), +-- ('gimbi', 'Gimbi'), +-- ('wukro', 'Wukro'), +-- ('haramaya', 'Haramaya'), +-- ('mizan_teferi', 'Mizan Teferi'), +-- ('sawla', 'Sawla'), +-- ('mojo', 'Mojo'), +-- ('dembi_dolo', 'Dembi Dolo'), +-- ('aleta_wendo', 'Aleta Wendo'), +-- ('metu', 'Metu'), +-- ('mota', 'Mota'), +-- ('fiche', 'Fiche'), +-- ('finote_selam', 'Finote Selam'), +-- ('bule_hora_town', 'Bule Hora Town'), +-- ('bonga', 'Bonga'), +-- ('kobo', 'Kobo'), +-- ('jinka', 'Jinka'), +-- ('dangila', 'Dangila'), +-- ('degehabur', 'Degehabur'), +-- ('bedessa', 'Bedessa'), +-- ('agaro', 'Agaro') ON CONFLICT (key) DO +-- UPDATE +-- SET value = EXCLUDED.value; \ No newline at end of file diff --git a/db/migrations/000010_seed_data.up.sql b/db/migrations/000010_seed_data.up.sql index 6534e76..b42fb9a 100644 --- a/db/migrations/000010_seed_data.up.sql +++ b/db/migrations/000010_seed_data.up.sql @@ -1,218 +1,220 @@ -CREATE EXTENSION IF NOT EXISTS pgcrypto; --- Users -INSERT INTO users ( - id, - first_name, - last_name, - email, - phone_number, - password, - role, - email_verified, - phone_verified, - created_at, - updated_at, - suspended, - company_id - ) -VALUES ( - 1, - 'John', - 'Doe', - 'john.doe@example.com', - NULL, - crypt('password123', gen_salt('bf'))::bytea, - 'customer', - TRUE, - FALSE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP, - FALSE, - NULL - ), - ( - 2, - 'Test', - 'Admin', - 'test.admin@gmail.com', - '0988554466', - crypt('password123', gen_salt('bf'))::bytea, - 'admin', - TRUE, - TRUE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP, - FALSE, - 1 - ), - ( - 3, - 'Samuel', - 'Tariku', - 'cybersamt@gmail.com', - '0911111111', - crypt('password@123', gen_salt('bf'))::bytea, - 'super_admin', - TRUE, - TRUE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP, - FALSE, - NULL - ), - ( - 4, - 'Kirubel', - 'Kibru', - 'kirubel.jkl679@gmail.com', - '0911554486', - crypt('password@123', gen_salt('bf'))::bytea, - 'super_admin', - TRUE, - TRUE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP, - FALSE, - NULL - ), - ( - 5, - 'Test', - 'Veli', - 'test.veli@example.com', - NULL, - crypt('password@123', gen_salt('bf'))::bytea, - 'customer', - TRUE, - FALSE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP, - FALSE, - NULL - ); --- Supported Operations -INSERT INTO supported_operations (id, name, description) -VALUES (1, 'SportBook', 'Sportbook operations'), - (2, 'Virtual', 'Virtual operations'); --- Wallets -INSERT INTO wallets ( - id, - balance, - is_withdraw, - is_bettable, - is_transferable, - user_id, - type, - currency, - is_active, - created_at, - updated_at - ) -VALUES ( - 1, - 10000, - TRUE, - TRUE, - TRUE, - 1, - 'regular', - 'ETB', - TRUE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP - ), - ( - 2, - 5000, - FALSE, - TRUE, - TRUE, - 1, - 'static', - 'ETB', - TRUE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP - ), - ( - 3, - 20000, - TRUE, - TRUE, - TRUE, - 2, - 'company_main', - 'ETB', - TRUE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP - ), - ( - 4, - 15000, - TRUE, - TRUE, - TRUE, - 2, - 'branch_main', - 'ETB', - TRUE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP - ); --- Customer Wallets -INSERT INTO customer_wallets ( - id, - customer_id, - regular_wallet_id, - static_wallet_id - ) -VALUES (1, 1, 1, 2); --- Company -INSERT INTO companies ( - id, - name, - admin_id, - wallet_id, - deducted_percentage, - is_active, - created_at, - updated_at - ) -VALUES ( - 1, - 'Test Company', - 2, - 3, - 0.10, - TRUE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP - ); --- Branch -INSERT INTO branches ( - id, - name, - location, - wallet_id, - branch_manager_id, - company_id, - is_self_owned, - profit_percent, - is_active, - created_at, - updated_at - ) -VALUES ( - 1, - 'Test Branch', - 'addis_ababa', - 4, - 2, - 1, - TRUE, - 0.10, - TRUE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP - ); \ No newline at end of file +-- CREATE EXTENSION IF NOT EXISTS pgcrypto; +-- -- Users +-- INSERT INTO users ( +-- id, +-- first_name, +-- last_name, +-- email, +-- phone_number, +-- password, +-- role, +-- email_verified, +-- phone_verified, +-- created_at, +-- updated_at, +-- suspended, +-- company_id +-- ) +-- VALUES ( +-- 1, +-- 'John', +-- 'Doe', +-- 'john.doe@example.com', +-- NULL, +-- crypt('password@123', gen_salt('bf'))::bytea, +-- 'customer', +-- TRUE, +-- FALSE, +-- CURRENT_TIMESTAMP, +-- CURRENT_TIMESTAMP, +-- FALSE, +-- 1 +-- ), +-- ( +-- 2, +-- 'Test', +-- 'Admin', +-- 'test.admin@gmail.com', +-- '0988554466', +-- crypt('password@123', gen_salt('bf'))::bytea, +-- 'admin', +-- TRUE, +-- TRUE, +-- CURRENT_TIMESTAMP, +-- CURRENT_TIMESTAMP, +-- FALSE, +-- 1 +-- ), +-- ( +-- 3, +-- 'Samuel', +-- 'Tariku', +-- 'cybersamt@gmail.com', +-- '0911111111', +-- crypt('password@123', gen_salt('bf'))::bytea, +-- 'super_admin', +-- TRUE, +-- TRUE, +-- CURRENT_TIMESTAMP, +-- CURRENT_TIMESTAMP, +-- FALSE, +-- NULL +-- ), +-- ( +-- 4, +-- 'Kirubel', +-- 'Kibru', +-- 'kirubel.jkl679@gmail.com', +-- '0911554486', +-- crypt('password@123', gen_salt('bf'))::bytea, +-- 'super_admin', +-- TRUE, +-- TRUE, +-- CURRENT_TIMESTAMP, +-- CURRENT_TIMESTAMP, +-- FALSE, +-- NULL +-- ), +-- ( +-- 5, +-- 'Test', +-- 'Veli', +-- 'test.veli@example.com', +-- NULL, +-- crypt('password@123', gen_salt('bf'))::bytea, +-- 'customer', +-- TRUE, +-- FALSE, +-- CURRENT_TIMESTAMP, +-- CURRENT_TIMESTAMP, +-- FALSE, +-- 1 +-- ); +-- -- Supported Operations +-- INSERT INTO supported_operations (id, name, description) +-- VALUES (1, 'SportBook', 'Sportbook operations'), +-- (2, 'Virtual', 'Virtual operations'); +-- -- Wallets +-- INSERT INTO wallets ( +-- id, +-- balance, +-- is_withdraw, +-- is_bettable, +-- is_transferable, +-- user_id, +-- type, +-- currency, +-- is_active, +-- created_at, +-- updated_at +-- ) +-- VALUES ( +-- 1, +-- 10000, +-- TRUE, +-- TRUE, +-- TRUE, +-- 1, +-- 'regular_wallet', +-- 'ETB', +-- TRUE, +-- CURRENT_TIMESTAMP, +-- CURRENT_TIMESTAMP +-- ), +-- ( +-- 2, +-- 5000, +-- FALSE, +-- TRUE, +-- TRUE, +-- 1, +-- 'static_wallet', +-- 'ETB', +-- TRUE, +-- CURRENT_TIMESTAMP, +-- CURRENT_TIMESTAMP +-- ), +-- ( +-- 3, +-- 20000, +-- TRUE, +-- TRUE, +-- TRUE, +-- 2, +-- 'company_wallet', +-- 'ETB', +-- TRUE, +-- CURRENT_TIMESTAMP, +-- CURRENT_TIMESTAMP +-- ), +-- ( +-- 4, +-- 15000, +-- TRUE, +-- TRUE, +-- TRUE, +-- 2, +-- 'branch_wallet', +-- 'ETB', +-- TRUE, +-- CURRENT_TIMESTAMP, +-- CURRENT_TIMESTAMP +-- ); +-- -- Customer Wallets +-- INSERT INTO customer_wallets ( +-- id, +-- customer_id, +-- regular_wallet_id, +-- static_wallet_id +-- ) +-- VALUES (1, 1, 1, 2); +-- -- Company +-- INSERT INTO companies ( +-- id, +-- name, +-- slug, +-- admin_id, +-- wallet_id, +-- deducted_percentage, +-- is_active, +-- created_at, +-- updated_at +-- ) +-- VALUES ( +-- 1, +-- 'FortuneBets', +-- 'fortunebets', +-- 2, +-- 3, +-- 0.10, +-- TRUE, +-- CURRENT_TIMESTAMP, +-- CURRENT_TIMESTAMP +-- ); +-- -- Branch +-- INSERT INTO branches ( +-- id, +-- name, +-- location, +-- wallet_id, +-- branch_manager_id, +-- company_id, +-- is_self_owned, +-- profit_percent, +-- is_active, +-- created_at, +-- updated_at +-- ) +-- VALUES ( +-- 1, +-- 'Test Branch', +-- 'addis_ababa', +-- 4, +-- 2, +-- 1, +-- TRUE, +-- 0.10, +-- TRUE, +-- CURRENT_TIMESTAMP, +-- CURRENT_TIMESTAMP +-- ); \ No newline at end of file diff --git a/db/query/bonus.sql b/db/query/bonus.sql index 82b3113..216528a 100644 --- a/db/query/bonus.sql +++ b/db/query/bonus.sql @@ -1,17 +1,61 @@ --- name: CreateBonusMultiplier :exec -INSERT INTO bonus (multiplier, balance_cap) -VALUES ($1, $2); +-- name: CreateUserBonus :one +INSERT INTO user_bonuses ( + name, + description, + type, + user_id, + reward_amount, + expires_at + ) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING *; +-- name: GetAllUserBonuses :many +SELECT * +FROM user_bonuses +WHERE ( + user_id = sqlc.narg('user_id') + OR sqlc.narg('user_id') IS NULL + ) +LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); +-- name: GetUserBonusByID :one +SELECT * +FROM user_bonuses +WHERE id = $1; --- name: GetBonusMultiplier :many -SELECT id, multiplier -FROM bonus; - --- name: GetBonusBalanceCap :many -SELECT id, balance_cap -FROM bonus; - --- name: UpdateBonusMultiplier :exec -UPDATE bonus -SET multiplier = $1, - balance_cap = $2 -WHERE id = $3; \ No newline at end of file +-- name: GetBonusCount :one +SELECT COUNT(*) +FROM user_bonuses +WHERE ( + user_id = sqlc.narg('user_id') + OR sqlc.narg('user_id') IS NULL + ); +-- name: GetBonusStats :one +SELECT COUNT(*) AS total_bonuses, + COALESCE(SUM(reward_amount), 0)::bigint AS total_reward_earned, + COUNT( + CASE + WHEN is_claimed = true THEN 1 + END + ) AS claimed_bonuses, + COUNT( + CASE + WHEN expires_at < now() THEN 1 + END + ) AS expired_bonuses +FROM user_bonuses + JOIN users ON users.id = user_bonuses.user_id +WHERE ( + company_id = sqlc.narg('company_id') + OR sqlc.narg('company_id') IS NULL + ) + AND ( + user_id = sqlc.narg('user_id') + OR sqlc.narg('user_id') IS NULL + ); +-- name: UpdateUserBonus :exec +UPDATE user_bonuses +SET is_claimed = $2 +WHERE id = $1; +-- name: DeleteUserBonus :exec +DELETE FROM user_bonuses +WHERE id = $1; \ No newline at end of file diff --git a/db/query/events.sql b/db/query/events.sql index 1a61445..c5ec974 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -14,7 +14,8 @@ INSERT INTO events ( start_time, is_live, status, - source + source, + default_winning_upper_limit ) VALUES ( $1, @@ -31,7 +32,8 @@ VALUES ( $12, $13, $14, - $15 + $15, + $16 ) ON CONFLICT (id) DO UPDATE SET sport_id = EXCLUDED.sport_id, @@ -44,7 +46,6 @@ SET sport_id = EXCLUDED.sport_id, away_kit_image = EXCLUDED.away_kit_image, league_id = EXCLUDED.league_id, league_name = EXCLUDED.league_name, - league_cc = EXCLUDED.league_cc, start_time = EXCLUDED.start_time, score = EXCLUDED.score, match_minute = EXCLUDED.match_minute, @@ -53,8 +54,9 @@ SET sport_id = EXCLUDED.sport_id, match_period = EXCLUDED.match_period, is_live = EXCLUDED.is_live, source = EXCLUDED.source, + default_winning_upper_limit = EXCLUDED.default_winning_upper_limit, fetched_at = now(); --- name: InsertEventSettings :exec +-- name: SaveEventSettings :exec INSERT INTO company_event_settings ( company_id, event_id, @@ -152,16 +154,18 @@ ORDER BY start_time ASC LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- name: GetTotalCompanyEvents :one SELECT COUNT(*) -FROM event_with_settings -WHERE company_id = $1 - AND is_live = false +FROM events e + LEFT JOIN company_event_settings ces ON e.id = ces.event_id + AND ces.company_id = $1 + JOIN leagues l ON l.id = e.league_id +WHERE is_live = false AND status = 'upcoming' AND ( league_id = sqlc.narg('league_id') OR sqlc.narg('league_id') IS NULL ) AND ( - sport_id = sqlc.narg('sport_id') + e.sport_id = sqlc.narg('sport_id') OR sqlc.narg('sport_id') IS NULL ) AND ( @@ -178,14 +182,35 @@ WHERE company_id = $1 OR sqlc.narg('first_start_time') IS NULL ) AND ( - league_cc = sqlc.narg('country_code') + l.country_code = sqlc.narg('country_code') OR sqlc.narg('country_code') IS NULL + ) + AND ( + ces.is_featured = sqlc.narg('is_featured') + OR e.default_is_featured = sqlc.narg('is_featured') + OR sqlc.narg('is_featured') IS NULL + ) + AND ( + ces.is_active = sqlc.narg('is_active') + OR e.default_is_active = sqlc.narg('is_active') + OR sqlc.narg('is_active') IS NULL ); -- name: GetEventsWithSettings :many -SELECT * -FROM event_with_settings -WHERE company_id = $1 - AND start_time > now() +SELECT e.*, + ces.company_id, + COALESCE(ces.is_active, e.default_is_active) AS is_active, + COALESCE(ces.is_featured, e.default_is_featured) AS is_featured, + COALESCE( + ces.winning_upper_limit, + e.default_winning_upper_limit + ) AS winning_upper_limit, + ces.updated_at, + l.country_code as league_cc +FROM events e + LEFT JOIN company_event_settings ces ON e.id = ces.event_id + AND ces.company_id = $1 + JOIN leagues l ON l.id = e.league_id +WHERE start_time > now() AND is_live = false AND status = 'upcoming' AND ( @@ -193,7 +218,7 @@ WHERE company_id = $1 OR sqlc.narg('league_id') IS NULL ) AND ( - sport_id = sqlc.narg('sport_id') + e.sport_id = sqlc.narg('sport_id') OR sqlc.narg('sport_id') IS NULL ) AND ( @@ -210,9 +235,19 @@ WHERE company_id = $1 OR sqlc.narg('first_start_time') IS NULL ) AND ( - league_cc = sqlc.narg('country_code') + l.country_code = sqlc.narg('country_code') OR sqlc.narg('country_code') IS NULL ) + AND ( + ces.is_featured = sqlc.narg('is_featured') + OR e.default_is_featured = sqlc.narg('is_featured') + OR sqlc.narg('is_featured') IS NULL + ) + AND ( + ces.is_active = sqlc.narg('is_active') + OR e.default_is_active = sqlc.narg('is_active') + OR sqlc.narg('is_active') IS NULL + ) ORDER BY start_time ASC LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- name: GetUpcomingByID :one @@ -223,13 +258,27 @@ WHERE id = $1 AND status = 'upcoming' LIMIT 1; -- name: GetEventWithSettingByID :one -SELECT * -FROM event_with_settings -WHERE id = $1 - AND company_id = $2 +SELECT e.*, + ces.company_id, + COALESCE(ces.is_active, e.default_is_active) AS is_active, + COALESCE(ces.is_featured, e.default_is_featured) AS is_featured, + COALESCE( + ces.winning_upper_limit, + e.default_winning_upper_limit + ) AS winning_upper_limit, + ces.updated_at, + l.country_code as league_cc +FROM events e + LEFT JOIN company_event_settings ces ON e.id = ces.event_id + AND ces.company_id = $2 + JOIN leagues l ON l.id = e.league_id +WHERE e.id = $1 AND is_live = false AND status = 'upcoming' LIMIT 1; +-- name: GetSportAndLeagueIDs :one +SELECT sport_id, league_id FROM events +WHERE id = $1; -- name: UpdateMatchResult :exec UPDATE events SET score = $1, @@ -243,19 +292,6 @@ WHERE id = $1; UPDATE events SET is_monitored = $1 WHERE id = $2; --- name: UpdateEventSettings :exec -UPDATE company_event_settings -SET is_active = COALESCE(sqlc.narg('is_active'), is_active), - is_featured = COALESCE( - sqlc.narg('is_featured'), - is_featured - ), - winning_upper_limit = COALESCE( - sqlc.narg('winning_upper_limit'), - winning_upper_limit - ) -WHERE event_id = $1 - AND company_id = $2; -- name: DeleteEvent :exec DELETE FROM events -WHERE id = $1; \ No newline at end of file +WHERE id = $1; diff --git a/db/query/leagues.sql b/db/query/leagues.sql index fbbc562..476c3e8 100644 --- a/db/query/leagues.sql +++ b/db/query/leagues.sql @@ -36,13 +36,18 @@ WHERE ( sport_id = sqlc.narg('sport_id') OR sqlc.narg('sport_id') IS NULL ) + AND ( + name ILIKE '%' || sqlc.narg('query') || '%' + OR sqlc.narg('query') IS NULL + ) ORDER BY name ASC LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); --- name: GetAllLeaguesWithSettings :many -SELECT * -FROM league_with_settings -WHERE (company_id = $1) - AND ( +-- name: GetTotalLeaguesWithSettings :one +SELECT COUNT(*) +FROM leagues l + LEFT JOIN company_league_settings cls ON l.id = cls.league_id + AND company_id = $1 +WHERE ( country_code = sqlc.narg('country_code') OR sqlc.narg('country_code') IS NULL ) @@ -52,12 +57,49 @@ WHERE (company_id = $1) ) AND ( is_active = sqlc.narg('is_active') + OR default_is_active = sqlc.narg('is_active') OR sqlc.narg('is_active') IS NULL ) AND ( is_featured = sqlc.narg('is_featured') + OR default_is_featured = sqlc.narg('is_featured') OR sqlc.narg('is_featured') IS NULL ) + AND ( + name ILIKE '%' || sqlc.narg('query') || '%' + OR sqlc.narg('query') IS NULL + ); +-- name: GetAllLeaguesWithSettings :many +SELECT l.*, + cls.company_id, + COALESCE(cls.is_active, l.default_is_active) AS is_active, + COALESCE(cls.is_featured, l.default_is_featured) AS is_featured, + cls.updated_at +FROM leagues l + LEFT JOIN company_league_settings cls ON l.id = cls.league_id + AND company_id = $1 +WHERE ( + country_code = sqlc.narg('country_code') + OR sqlc.narg('country_code') IS NULL + ) + AND ( + sport_id = sqlc.narg('sport_id') + OR sqlc.narg('sport_id') IS NULL + ) + AND ( + is_active = sqlc.narg('is_active') + OR default_is_active = sqlc.narg('is_active') + OR sqlc.narg('is_active') IS NULL + ) + AND ( + is_featured = sqlc.narg('is_featured') + OR default_is_featured = sqlc.narg('is_featured') + OR sqlc.narg('is_featured') IS NULL + ) + AND ( + name ILIKE '%' || sqlc.narg('query') || '%' + OR sqlc.narg('query') IS NULL + ) ORDER BY is_featured DESC, name ASC LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); diff --git a/db/query/odds.sql b/db/query/odds.sql index 8c231b1..dc467c6 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -26,7 +26,7 @@ SET market_type = EXCLUDED.market_type, raw_odds = EXCLUDED.raw_odds, fetched_at = EXCLUDED.fetched_at, expires_at = EXCLUDED.expires_at; --- name: InsertOddSettings :exec +-- name: SaveOddSettings :exec INSERT INTO company_odd_settings ( company_id, odds_market_id, @@ -42,21 +42,69 @@ SELECT * FROM odds_market_with_event LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- name: GetAllOddsWithSettings :many -SELECT * -FROM odds_market_with_settings -WHERE company_id = $1 +SELECT o.id, + o.event_id, + o.market_type, + o.market_name, + o.market_category, + o.market_id, + o.default_is_active, + o.fetched_at, + o.expires_at, + cos.company_id, + COALESCE(cos.is_active, o.default_is_active) AS is_active, + COALESCE(cos.custom_raw_odds, o.raw_odds) AS raw_odds, + cos.updated_at +FROM odds_market o + LEFT JOIN company_odd_settings cos ON o.id = cos.odds_market_id + AND company_id = $1 LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); +-- name: GetOddByID :one +SELECT * +FROM odds_market_with_event +WHERE id = $1; -- name: GetOddsByMarketID :one SELECT * FROM odds_market_with_event WHERE market_id = $1 AND event_id = $2; -- name: GetOddsWithSettingsByMarketID :one -SELECT * -FROM odds_market_with_settings +SELECT o.id, + o.event_id, + o.market_type, + o.market_name, + o.market_category, + o.market_id, + o.default_is_active, + o.fetched_at, + o.expires_at, + cos.company_id, + COALESCE(cos.is_active, o.default_is_active) AS is_active, + COALESCE(cos.custom_raw_odds, o.raw_odds) AS raw_odds, + cos.updated_at +FROM odds_market o + LEFT JOIN company_odd_settings cos ON o.id = cos.odds_market_id + AND company_id = $3 WHERE market_id = $1 - AND event_id = $2 - AND company_id = $3; + AND event_id = $2; +-- name: GetOddsWithSettingsByID :one +SELECT o.id, + o.event_id, + o.market_type, + o.market_name, + o.market_category, + o.market_id, + o.default_is_active, + o.fetched_at, + o.expires_at, + cos.company_id, + COALESCE(cos.is_active, o.default_is_active) AS is_active, + COALESCE(cos.custom_raw_odds, o.raw_odds) AS raw_odds, + cos.updated_at +FROM odds_market o + LEFT JOIN company_odd_settings cos ON o.id = cos.odds_market_id + AND company_id = $2 +WHERE o.id = $1; -- name: GetOddsByEventID :many SELECT * FROM odds_market_with_event @@ -75,10 +123,23 @@ WHERE event_id = $1 ) LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- name: GetOddsWithSettingsByEventID :many -SELECT * -FROM odds_market_with_settings -WHERE event_id = $1 +SELECT o.id, + o.event_id, + o.market_type, + o.market_name, + o.market_category, + o.market_id, + o.default_is_active, + o.fetched_at, + o.expires_at, + cos.company_id, + COALESCE(cos.is_active, o.default_is_active) AS is_active, + COALESCE(cos.custom_raw_odds, o.raw_odds) AS raw_odds, + cos.updated_at +FROM odds_market o + LEFT JOIN company_odd_settings cos ON o.id = cos.odds_market_id AND company_id = $2 +WHERE event_id = $1 LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- name: DeleteOddsForEvent :exec DELETE FROM odds_market diff --git a/db/query/raffle.sql b/db/query/raffle.sql new file mode 100644 index 0000000..55f302c --- /dev/null +++ b/db/query/raffle.sql @@ -0,0 +1,73 @@ +-- name: CreateRaffle :one +INSERT INTO raffles (company_id, name, expires_at, type) +VALUES ($1, $2, $3, $4) +RETURNING *; + +-- name: GetRafflesOfCompany :many +SELECT * FROM raffles WHERE company_id = $1; + +-- name: DeleteRaffle :one +DELETE FROM raffles +WHERE id = $1 +RETURNING *; + +-- name: UpdateRaffleTicketStatus :exec +UPDATE raffle_tickets +SET is_active = $1 +WHERE id = $2; + +-- name: CreateRaffleTicket :one +INSERT INTO raffle_tickets (raffle_id, user_id) +VALUES ($1, $2) +RETURNING *; + +-- name: GetUserRaffleTickets :many +SELECT + rt.id AS ticket_id, + rt.user_id, + r.name, + r.type, + r.expires_at, + r.status +FROM raffle_tickets rt +JOIN raffles r ON rt.raffle_id = r.id +WHERE rt.user_id = $1; + +-- name: GetRaffleStanding :many +SELECT + u.id AS user_id, + rt.raffle_id, + u.first_name, + u.last_name, + u.phone_number, + u.email, + COUNT(*) AS ticket_count +FROM raffle_tickets rt +JOIN users u ON rt.user_id = u.id +WHERE rt.is_active = true + AND rt.raffle_id = $1 +GROUP BY u.id, rt.raffle_id, u.first_name, u.last_name, u.phone_number, u.email +ORDER BY ticket_count DESC +LIMIT $2; + +-- name: CreateRaffleWinner :one +INSERT INTO raffle_winners (raffle_id, user_id, rank) +VALUES ($1, $2, $3) +RETURNING *; + +-- name: SetRaffleComplete :exec +UPDATE raffles +SET status = 'completed' +WHERE id = $1; + +-- name: AddSportRaffleFilter :one +INSERT INTO raffle_sport_filters (raffle_id, sport_id, league_id) +VALUES ($1, $2, $3) +RETURNING *; + +-- name: CheckValidSportRaffleFilter :one +SELECT COUNT(*) > 0 AS exists +FROM raffle_sport_filters +WHERE raffle_id = $1 + AND sport_id = $2 + AND league_id = $3; diff --git a/db/query/referal.sql b/db/query/referal.sql index 206606e..3dbe00e 100644 --- a/db/query/referal.sql +++ b/db/query/referal.sql @@ -1,77 +1,51 @@ --- name: CreateReferral :one -INSERT INTO referrals ( - referral_code, - referrer_id, - status, - reward_amount, - expires_at -) VALUES ( - $1, $2, $3, $4, $5 -) RETURNING *; - --- name: GetReferralByCode :one -SELECT * FROM referrals -WHERE referral_code = $1; - --- name: UpdateReferral :one -UPDATE referrals -SET - referred_id = $2, - status = $3, - updated_at = CURRENT_TIMESTAMP -WHERE id = $1 +-- name: CreateReferralCode :one +INSERT INTO referral_codes ( + referral_code, + referrer_id, + company_id, + number_of_referrals, + reward_amount + ) +VALUES ($1, $2, $3, $4, $5) RETURNING *; - --- name: UpdateReferralCode :exec -UPDATE users -SET - referral_code = $2, - updated_at = CURRENT_TIMESTAMP -WHERE id = $1; - --- name: GetReferralStats :one -SELECT - COUNT(*) as total_referrals, - COUNT(CASE WHEN status = 'COMPLETED' THEN 1 END) as completed_referrals, - COALESCE(SUM(reward_amount), 0) as total_reward_earned, - COALESCE(SUM(CASE WHEN status = 'PENDING' THEN reward_amount END), 0) as pending_rewards -FROM referrals +-- name: CreateUserReferral :one +INSERT INTO user_referrals (referred_id, referral_code_id) +VALUES ($1, $2) +RETURNING *; +-- name: GetReferralCodeByUser :many +SELECt * +FROM referral_codes WHERE referrer_id = $1; - --- name: GetReferralSettings :one -SELECT * FROM referral_settings -LIMIT 1; - --- name: UpdateReferralSettings :one -UPDATE referral_settings -SET - referral_reward_amount = $2, - cashback_percentage = $3, - bet_referral_bonus_percentage= $4, - max_referrals = $5, - expires_after_days = $6, - updated_by = $7, - updated_at = CURRENT_TIMESTAMP -WHERE id = $1 -RETURNING *; - --- name: CreateReferralSettings :one -INSERT INTO referral_settings ( - referral_reward_amount, - cashback_percentage, - max_referrals, - bet_referral_bonus_percentage, - expires_after_days, - updated_by -) VALUES ( - $1, $2, $3, $4, $5, $6 -) RETURNING *; - --- name: GetReferralByReferredID :one -SELECT * FROM referrals WHERE referred_id = $1 LIMIT 1; - --- name: GetActiveReferralByReferrerID :one -SELECT * FROM referrals WHERE referrer_id = $1 AND status = 'PENDING' LIMIT 1; - --- name: GetReferralCountByID :one -SELECT count(*) FROM referrals WHERE referrer_id = $1; \ No newline at end of file +-- name: GetReferralCode :one +SELECT * +FROM referral_codes +WHERE referral_code = $1; +-- name: UpdateReferralCode :exec +UPDATE referral_codes +SET is_active = $2, + referral_code = $3, + number_of_referrals = $4, + reward_amount = $5, + updated_at = CURRENT_TIMESTAMP +WHERE id = $1; +-- name: GetReferralStats :one +SELECT COUNT(*) AS total_referrals, + COALESCE(SUM(reward_amount), 0)::bigint AS total_reward_earned +FROM user_referrals + JOIN referral_codes ON referral_codes.id = referral_code_id +WHERE referrer_id = $1 + AND company_id = $2; +-- name: GetUserReferral :one +SELECT * +FROM user_referrals +WHERE referred_id = $1; +-- name: GetUserReferralsByCode :many +SELECT user_referrals.* +FROM user_referrals + JOIN referral_codes ON referral_codes.id = referral_code_id +WHERE referral_code = $1; +-- name: GetUserReferralsCount :one +SELECT COUNT(*) +FROM user_referrals + JOIN referral_codes ON referral_codes.id = referral_code_id +WHERE referrer_id = $1; \ No newline at end of file diff --git a/db/query/settings.sql b/db/query/settings.sql index cda4e87..e51c1ff 100644 --- a/db/query/settings.sql +++ b/db/query/settings.sql @@ -27,7 +27,9 @@ SELECT * FROM company_settings WHERE key = $1; -- name: GetOverrideSettings :many -SELECT gs.*, +SELECT gs.key, + gs.created_at, + gs.updated_at, COALESCE(cs.value, gs.value) AS value FROM global_settings gs LEFT JOIN company_settings cs ON cs.key = gs.key diff --git a/db/query/transfer.sql b/db/query/transfer.sql index dc4c156..0229d0f 100644 --- a/db/query/transfer.sql +++ b/db/query/transfer.sql @@ -30,6 +30,19 @@ WHERE id = $1; SELECT * FROM wallet_transfer_details WHERE reference_number = $1; +-- name: GetTransferStats :one +SELECT COUNT(*) AS total_transfers, COUNT(*) FILTER ( + WHERE type = 'deposit' + ) AS total_deposits, + COUNT(*) FILTER ( + WHERE type = 'withdraw' + ) AS total_withdraw, + COUNT(*) FILTER ( + WHERE type = 'wallet' + ) AS total_wallet_to_wallet +FROM wallet_transfer +WHERE sender_wallet_id = $1 + OR receiver_wallet_id = $1; -- name: UpdateTransferVerification :exec UPDATE wallet_transfer SET verified = $1, diff --git a/db/query/user.sql b/db/query/user.sql index 1b408cf..e749355 100644 --- a/db/query/user.sql +++ b/db/query/user.sql @@ -193,7 +193,7 @@ SET password = $1, WHERE ( email = $2 OR phone_number = $3 - AND company_id = $4 + AND company_id = $5 ); -- name: GetAdminByCompanyID :one SELECT users.* diff --git a/db/query/virtual_games.sql b/db/query/virtual_games.sql index 1e81b98..46e5061 100644 --- a/db/query/virtual_games.sql +++ b/db/query/virtual_games.sql @@ -1,77 +1,157 @@ -- name: CreateVirtualGameProvider :one INSERT INTO virtual_game_providers ( - provider_id, provider_name, logo_dark, logo_light, enabled -) VALUES ( - $1, $2, $3, $4, $5 -) RETURNING id, provider_id, provider_name, logo_dark, logo_light, enabled, created_at, updated_at; - + provider_id, + provider_name, + logo_dark, + logo_light, + enabled + ) +VALUES ($1, $2, $3, $4, $5) +RETURNING id, + provider_id, + provider_name, + logo_dark, + logo_light, + enabled, + created_at, + updated_at; -- name: DeleteVirtualGameProvider :exec DELETE FROM virtual_game_providers WHERE provider_id = $1; - -- name: DeleteAllVirtualGameProviders :exec DELETE FROM virtual_game_providers; - -- name: GetVirtualGameProviderByID :one -SELECT id, provider_id, provider_name, logo_dark, logo_light, enabled, created_at, updated_at +SELECT id, + provider_id, + provider_name, + logo_dark, + logo_light, + enabled, + created_at, + updated_at FROM virtual_game_providers WHERE provider_id = $1; - -- name: ListVirtualGameProviders :many -SELECT id, provider_id, provider_name, logo_dark, logo_light, enabled, created_at, updated_at +SELECT id, + provider_id, + provider_name, + logo_dark, + logo_light, + enabled, + created_at, + updated_at FROM virtual_game_providers ORDER BY created_at DESC LIMIT $1 OFFSET $2; - -- name: CountVirtualGameProviders :one SELECT COUNT(*) AS total FROM virtual_game_providers; - -- name: UpdateVirtualGameProviderEnabled :one UPDATE virtual_game_providers SET enabled = $2, updated_at = CURRENT_TIMESTAMP WHERE provider_id = $1 -RETURNING id, provider_id, provider_name, logo_dark, logo_light, enabled, created_at, updated_at; - +RETURNING id, + provider_id, + provider_name, + logo_dark, + logo_light, + enabled, + created_at, + updated_at; -- name: CreateVirtualGameSession :one INSERT INTO virtual_game_sessions ( - user_id, game_id, session_token, currency, status, expires_at -) VALUES ( - $1, $2, $3, $4, $5, $6 -) RETURNING id, user_id, game_id, session_token, currency, status, created_at, updated_at, expires_at; + user_id, + game_id, + session_token, + currency, + status, + expires_at + ) +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 +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 +SET status = $2, + updated_at = CURRENT_TIMESTAMP WHERE id = $1; -- name: CreateVirtualGameTransaction :one INSERT INTO virtual_game_transactions ( - 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, $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, + transaction_type, + amount, + currency, + external_transaction_id, + status + ) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) +RETURNING id, 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, + 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, @@ -87,60 +167,78 @@ INSERT INTO virtual_game_histories ( 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 +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 +SET status = $2, + updated_at = CURRENT_TIMESTAMP WHERE id = $1; -- name: GetVirtualGameSummaryInRange :many -SELECT - c.name AS company_name, +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 + 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; + 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; +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; +WHERE user_id = $1 + AND game_id = $2; -- name: ListFavoriteGames :many SELECT game_id FROM favorite_games WHERE user_id = $1; - -- name: CreateVirtualGame :one INSERT INTO virtual_games ( - game_id, - provider_id, - name, - category, - device_type, - volatility, - rtp, - has_demo, - has_free_bets, - bets, - thumbnail, - status -) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 -) -RETURNING - id, + game_id, + provider_id, + name, + category, + device_type, + volatility, + rtp, + has_demo, + has_free_bets, + bets, + thumbnail, + status + ) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12 + ) +RETURNING id, game_id, provider_id, name, @@ -155,10 +253,8 @@ RETURNING status, created_at, updated_at; - -- name: GetAllVirtualGames :many -SELECT - vg.id, +SELECT vg.id, vg.game_id, vg.provider_id, vp.provider_name, @@ -175,14 +271,20 @@ SELECT vg.created_at, vg.updated_at FROM virtual_games vg -JOIN virtual_game_providers vp ON vg.provider_id = vp.provider_id -WHERE - ($1::text IS NULL OR vg.category = $1) -- category filter (optional) - AND ($2::text IS NULL OR vg.name ILIKE '%' || $2 || '%') -- search by name (optional) + JOIN virtual_game_providers vp ON vg.provider_id = vp.provider_id +WHERE ( + vg.category = sqlc.narg('category') + OR sqlc.narg('category') IS NULL + ) + AND ( + name ILIKE '%' || sqlc.narg('name') || '%' + OR sqlc.narg('name') IS NULL + ) + AND ( + vg.provider_id = sqlc.narg('provider_id') + OR sqlc.narg('provider_id') IS NULL + ) ORDER BY vg.created_at DESC -LIMIT $3 OFFSET $4; - +LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- name: DeleteAllVirtualGames :exec -DELETE FROM virtual_games; - - +DELETE FROM virtual_games; \ No newline at end of file diff --git a/gen/db/auth.sql.go b/gen/db/auth.sql.go index 1817514..8dd2280 100644 --- a/gen/db/auth.sql.go +++ b/gen/db/auth.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: auth.sql package dbgen @@ -76,7 +76,7 @@ func (q *Queries) GetRefreshTokenByUserID(ctx context.Context, userID int64) (Re } const GetUserByEmailPhone = `-- name: GetUserByEmailPhone :one -SELECT id, first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at, company_id, suspended_at, suspended, referral_code, referred_by +SELECT id, first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at, company_id, suspended_at, suspended FROM users WHERE ( email = $1 @@ -112,8 +112,6 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho &i.CompanyID, &i.SuspendedAt, &i.Suspended, - &i.ReferralCode, - &i.ReferredBy, ) return i, err } diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index ff64087..573c4c2 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: bet.sql package dbgen diff --git a/gen/db/bet_stat.sql.go b/gen/db/bet_stat.sql.go index 275ef07..9a7b494 100644 --- a/gen/db/bet_stat.sql.go +++ b/gen/db/bet_stat.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: bet_stat.sql package dbgen diff --git a/gen/db/bonus.sql.go b/gen/db/bonus.sql.go index 12677b8..1a5d8e9 100644 --- a/gen/db/bonus.sql.go +++ b/gen/db/bonus.sql.go @@ -1,49 +1,112 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: bonus.sql package dbgen import ( "context" + + "github.com/jackc/pgx/v5/pgtype" ) -const CreateBonusMultiplier = `-- name: CreateBonusMultiplier :exec -INSERT INTO bonus (multiplier, balance_cap) -VALUES ($1, $2) +const CreateUserBonus = `-- name: CreateUserBonus :one +INSERT INTO user_bonuses ( + name, + description, + type, + user_id, + reward_amount, + expires_at + ) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING id, name, description, type, user_id, reward_amount, is_claimed, expires_at, claimed_at, created_at, updated_at ` -type CreateBonusMultiplierParams struct { - Multiplier float32 `json:"multiplier"` - BalanceCap int64 `json:"balance_cap"` +type CreateUserBonusParams struct { + Name string `json:"name"` + Description string `json:"description"` + Type string `json:"type"` + UserID int64 `json:"user_id"` + RewardAmount int64 `json:"reward_amount"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` } -func (q *Queries) CreateBonusMultiplier(ctx context.Context, arg CreateBonusMultiplierParams) error { - _, err := q.db.Exec(ctx, CreateBonusMultiplier, arg.Multiplier, arg.BalanceCap) +func (q *Queries) CreateUserBonus(ctx context.Context, arg CreateUserBonusParams) (UserBonuse, error) { + row := q.db.QueryRow(ctx, CreateUserBonus, + arg.Name, + arg.Description, + arg.Type, + arg.UserID, + arg.RewardAmount, + arg.ExpiresAt, + ) + var i UserBonuse + err := row.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.Type, + &i.UserID, + &i.RewardAmount, + &i.IsClaimed, + &i.ExpiresAt, + &i.ClaimedAt, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const DeleteUserBonus = `-- name: DeleteUserBonus :exec +DELETE FROM user_bonuses +WHERE id = $1 +` + +func (q *Queries) DeleteUserBonus(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeleteUserBonus, id) return err } -const GetBonusBalanceCap = `-- name: GetBonusBalanceCap :many -SELECT id, balance_cap -FROM bonus +const GetAllUserBonuses = `-- name: GetAllUserBonuses :many +SELECT id, name, description, type, user_id, reward_amount, is_claimed, expires_at, claimed_at, created_at, updated_at +FROM user_bonuses +WHERE ( + user_id = $1 + OR $1 IS NULL + ) +LIMIT $3 OFFSET $2 ` -type GetBonusBalanceCapRow struct { - ID int64 `json:"id"` - BalanceCap int64 `json:"balance_cap"` +type GetAllUserBonusesParams struct { + UserID pgtype.Int8 `json:"user_id"` + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` } -func (q *Queries) GetBonusBalanceCap(ctx context.Context) ([]GetBonusBalanceCapRow, error) { - rows, err := q.db.Query(ctx, GetBonusBalanceCap) +func (q *Queries) GetAllUserBonuses(ctx context.Context, arg GetAllUserBonusesParams) ([]UserBonuse, error) { + rows, err := q.db.Query(ctx, GetAllUserBonuses, arg.UserID, arg.Offset, arg.Limit) if err != nil { return nil, err } defer rows.Close() - var items []GetBonusBalanceCapRow + var items []UserBonuse for rows.Next() { - var i GetBonusBalanceCapRow - if err := rows.Scan(&i.ID, &i.BalanceCap); err != nil { + var i UserBonuse + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.Type, + &i.UserID, + &i.RewardAmount, + &i.IsClaimed, + &i.ExpiresAt, + &i.ClaimedAt, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { return nil, err } items = append(items, i) @@ -54,50 +117,108 @@ func (q *Queries) GetBonusBalanceCap(ctx context.Context) ([]GetBonusBalanceCapR return items, nil } -const GetBonusMultiplier = `-- name: GetBonusMultiplier :many -SELECT id, multiplier -FROM bonus +const GetBonusCount = `-- name: GetBonusCount :one +SELECT COUNT(*) +FROM user_bonuses +WHERE ( + user_id = $1 + OR $1 IS NULL + ) ` -type GetBonusMultiplierRow struct { - ID int64 `json:"id"` - Multiplier float32 `json:"multiplier"` +func (q *Queries) GetBonusCount(ctx context.Context, userID pgtype.Int8) (int64, error) { + row := q.db.QueryRow(ctx, GetBonusCount, userID) + var count int64 + err := row.Scan(&count) + return count, err } -func (q *Queries) GetBonusMultiplier(ctx context.Context) ([]GetBonusMultiplierRow, error) { - rows, err := q.db.Query(ctx, GetBonusMultiplier) - if err != nil { - return nil, err - } - defer rows.Close() - var items []GetBonusMultiplierRow - for rows.Next() { - var i GetBonusMultiplierRow - if err := rows.Scan(&i.ID, &i.Multiplier); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const UpdateBonusMultiplier = `-- name: UpdateBonusMultiplier :exec -UPDATE bonus -SET multiplier = $1, - balance_cap = $2 -WHERE id = $3 +const GetBonusStats = `-- name: GetBonusStats :one +SELECT COUNT(*) AS total_bonuses, + COALESCE(SUM(reward_amount), 0)::bigint AS total_reward_earned, + COUNT( + CASE + WHEN is_claimed = true THEN 1 + END + ) AS claimed_bonuses, + COUNT( + CASE + WHEN expires_at < now() THEN 1 + END + ) AS expired_bonuses +FROM user_bonuses + JOIN users ON users.id = user_bonuses.user_id +WHERE ( + company_id = $1 + OR $1 IS NULL + ) + AND ( + user_id = $2 + OR $2 IS NULL + ) ` -type UpdateBonusMultiplierParams struct { - Multiplier float32 `json:"multiplier"` - BalanceCap int64 `json:"balance_cap"` - ID int64 `json:"id"` +type GetBonusStatsParams struct { + CompanyID pgtype.Int8 `json:"company_id"` + UserID pgtype.Int8 `json:"user_id"` } -func (q *Queries) UpdateBonusMultiplier(ctx context.Context, arg UpdateBonusMultiplierParams) error { - _, err := q.db.Exec(ctx, UpdateBonusMultiplier, arg.Multiplier, arg.BalanceCap, arg.ID) +type GetBonusStatsRow struct { + TotalBonuses int64 `json:"total_bonuses"` + TotalRewardEarned int64 `json:"total_reward_earned"` + ClaimedBonuses int64 `json:"claimed_bonuses"` + ExpiredBonuses int64 `json:"expired_bonuses"` +} + +func (q *Queries) GetBonusStats(ctx context.Context, arg GetBonusStatsParams) (GetBonusStatsRow, error) { + row := q.db.QueryRow(ctx, GetBonusStats, arg.CompanyID, arg.UserID) + var i GetBonusStatsRow + err := row.Scan( + &i.TotalBonuses, + &i.TotalRewardEarned, + &i.ClaimedBonuses, + &i.ExpiredBonuses, + ) + return i, err +} + +const GetUserBonusByID = `-- name: GetUserBonusByID :one +SELECT id, name, description, type, user_id, reward_amount, is_claimed, expires_at, claimed_at, created_at, updated_at +FROM user_bonuses +WHERE id = $1 +` + +func (q *Queries) GetUserBonusByID(ctx context.Context, id int64) (UserBonuse, error) { + row := q.db.QueryRow(ctx, GetUserBonusByID, id) + var i UserBonuse + err := row.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.Type, + &i.UserID, + &i.RewardAmount, + &i.IsClaimed, + &i.ExpiresAt, + &i.ClaimedAt, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const UpdateUserBonus = `-- name: UpdateUserBonus :exec +UPDATE user_bonuses +SET is_claimed = $2 +WHERE id = $1 +` + +type UpdateUserBonusParams struct { + ID int64 `json:"id"` + IsClaimed bool `json:"is_claimed"` +} + +func (q *Queries) UpdateUserBonus(ctx context.Context, arg UpdateUserBonusParams) error { + _, err := q.db.Exec(ctx, UpdateUserBonus, arg.ID, arg.IsClaimed) return err } diff --git a/gen/db/branch.sql.go b/gen/db/branch.sql.go index a9a57b8..89d2959 100644 --- a/gen/db/branch.sql.go +++ b/gen/db/branch.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: branch.sql package dbgen diff --git a/gen/db/cashier.sql.go b/gen/db/cashier.sql.go index c15f497..55e69d2 100644 --- a/gen/db/cashier.sql.go +++ b/gen/db/cashier.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: cashier.sql package dbgen @@ -12,7 +12,7 @@ import ( ) const GetAllCashiers = `-- name: GetAllCashiers :many -SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended, users.referral_code, users.referred_by, +SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended, branch_id, branches.name AS branch_name, branches.wallet_id AS branch_wallet, @@ -57,8 +57,6 @@ type GetAllCashiersRow struct { CompanyID pgtype.Int8 `json:"company_id"` SuspendedAt pgtype.Timestamptz `json:"suspended_at"` Suspended bool `json:"suspended"` - ReferralCode pgtype.Text `json:"referral_code"` - ReferredBy pgtype.Text `json:"referred_by"` BranchID int64 `json:"branch_id"` BranchName string `json:"branch_name"` BranchWallet int64 `json:"branch_wallet"` @@ -89,8 +87,6 @@ func (q *Queries) GetAllCashiers(ctx context.Context, arg GetAllCashiersParams) &i.CompanyID, &i.SuspendedAt, &i.Suspended, - &i.ReferralCode, - &i.ReferredBy, &i.BranchID, &i.BranchName, &i.BranchWallet, @@ -107,7 +103,7 @@ func (q *Queries) GetAllCashiers(ctx context.Context, arg GetAllCashiersParams) } const GetCashierByID = `-- name: GetCashierByID :one -SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended, users.referral_code, users.referred_by, +SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended, branch_id, branches.name AS branch_name, branches.wallet_id AS branch_wallet, @@ -133,8 +129,6 @@ type GetCashierByIDRow struct { CompanyID pgtype.Int8 `json:"company_id"` SuspendedAt pgtype.Timestamptz `json:"suspended_at"` Suspended bool `json:"suspended"` - ReferralCode pgtype.Text `json:"referral_code"` - ReferredBy pgtype.Text `json:"referred_by"` BranchID int64 `json:"branch_id"` BranchName string `json:"branch_name"` BranchWallet int64 `json:"branch_wallet"` @@ -159,8 +153,6 @@ func (q *Queries) GetCashierByID(ctx context.Context, id int64) (GetCashierByIDR &i.CompanyID, &i.SuspendedAt, &i.Suspended, - &i.ReferralCode, - &i.ReferredBy, &i.BranchID, &i.BranchName, &i.BranchWallet, @@ -170,7 +162,7 @@ func (q *Queries) GetCashierByID(ctx context.Context, id int64) (GetCashierByIDR } const GetCashiersByBranch = `-- name: GetCashiersByBranch :many -SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended, users.referral_code, users.referred_by +SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended FROM branch_cashiers JOIN users ON branch_cashiers.user_id = users.id JOIN branches ON branches.id = branch_id @@ -201,8 +193,6 @@ func (q *Queries) GetCashiersByBranch(ctx context.Context, branchID int64) ([]Us &i.CompanyID, &i.SuspendedAt, &i.Suspended, - &i.ReferralCode, - &i.ReferredBy, ); err != nil { return nil, err } diff --git a/gen/db/company.sql.go b/gen/db/company.sql.go index 506eaca..18bc509 100644 --- a/gen/db/company.sql.go +++ b/gen/db/company.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: company.sql package dbgen diff --git a/gen/db/copyfrom.go b/gen/db/copyfrom.go index 1212253..f7a4793 100644 --- a/gen/db/copyfrom.go +++ b/gen/db/copyfrom.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: copyfrom.go package dbgen diff --git a/gen/db/db.go b/gen/db/db.go index 84de07c..8134784 100644 --- a/gen/db/db.go +++ b/gen/db/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 package dbgen diff --git a/gen/db/direct_deposit.sql.go b/gen/db/direct_deposit.sql.go index be02750..ff5a3b2 100644 --- a/gen/db/direct_deposit.sql.go +++ b/gen/db/direct_deposit.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: direct_deposit.sql package dbgen diff --git a/gen/db/disabled_odds.sql.go b/gen/db/disabled_odds.sql.go index 85dcd2e..917acce 100644 --- a/gen/db/disabled_odds.sql.go +++ b/gen/db/disabled_odds.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: disabled_odds.sql package dbgen diff --git a/gen/db/event_history.sql.go b/gen/db/event_history.sql.go index ab29359..64762c3 100644 --- a/gen/db/event_history.sql.go +++ b/gen/db/event_history.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: event_history.sql package dbgen diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index 313b240..9c9afe7 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: events.sql package dbgen @@ -78,10 +78,21 @@ func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]EventWithCountry, } const GetEventWithSettingByID = `-- name: GetEventWithSettingByID :one -SELECT id, sport_id, match_name, home_team, away_team, home_team_id, away_team_id, home_kit_image, away_kit_image, league_id, league_name, start_time, score, match_minute, timer_status, added_time, match_period, is_live, status, fetched_at, source, default_is_active, default_is_featured, default_winning_upper_limit, is_monitored, company_id, is_active, is_featured, winning_upper_limit, updated_at, league_cc -FROM event_with_settings -WHERE id = $1 - AND company_id = $2 +SELECT e.id, e.sport_id, e.match_name, e.home_team, e.away_team, e.home_team_id, e.away_team_id, e.home_kit_image, e.away_kit_image, e.league_id, e.league_name, e.start_time, e.score, e.match_minute, e.timer_status, e.added_time, e.match_period, e.is_live, e.status, e.fetched_at, e.source, e.default_is_active, e.default_is_featured, e.default_winning_upper_limit, e.is_monitored, + ces.company_id, + COALESCE(ces.is_active, e.default_is_active) AS is_active, + COALESCE(ces.is_featured, e.default_is_featured) AS is_featured, + COALESCE( + ces.winning_upper_limit, + e.default_winning_upper_limit + ) AS winning_upper_limit, + ces.updated_at, + l.country_code as league_cc +FROM events e + LEFT JOIN company_event_settings ces ON e.id = ces.event_id + AND ces.company_id = $2 + JOIN leagues l ON l.id = e.league_id +WHERE e.id = $1 AND is_live = false AND status = 'upcoming' LIMIT 1 @@ -92,9 +103,43 @@ type GetEventWithSettingByIDParams struct { CompanyID int64 `json:"company_id"` } -func (q *Queries) GetEventWithSettingByID(ctx context.Context, arg GetEventWithSettingByIDParams) (EventWithSetting, error) { +type GetEventWithSettingByIDRow struct { + ID string `json:"id"` + SportID int32 `json:"sport_id"` + MatchName string `json:"match_name"` + HomeTeam string `json:"home_team"` + AwayTeam string `json:"away_team"` + HomeTeamID int64 `json:"home_team_id"` + AwayTeamID int64 `json:"away_team_id"` + HomeKitImage string `json:"home_kit_image"` + AwayKitImage string `json:"away_kit_image"` + LeagueID int64 `json:"league_id"` + LeagueName string `json:"league_name"` + StartTime pgtype.Timestamp `json:"start_time"` + Score pgtype.Text `json:"score"` + MatchMinute pgtype.Int4 `json:"match_minute"` + TimerStatus pgtype.Text `json:"timer_status"` + AddedTime pgtype.Int4 `json:"added_time"` + MatchPeriod pgtype.Int4 `json:"match_period"` + IsLive bool `json:"is_live"` + Status string `json:"status"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + Source string `json:"source"` + DefaultIsActive bool `json:"default_is_active"` + DefaultIsFeatured bool `json:"default_is_featured"` + DefaultWinningUpperLimit int64 `json:"default_winning_upper_limit"` + IsMonitored bool `json:"is_monitored"` + CompanyID pgtype.Int8 `json:"company_id"` + IsActive bool `json:"is_active"` + IsFeatured bool `json:"is_featured"` + WinningUpperLimit int32 `json:"winning_upper_limit"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` + LeagueCc pgtype.Text `json:"league_cc"` +} + +func (q *Queries) GetEventWithSettingByID(ctx context.Context, arg GetEventWithSettingByIDParams) (GetEventWithSettingByIDRow, error) { row := q.db.QueryRow(ctx, GetEventWithSettingByID, arg.ID, arg.CompanyID) - var i EventWithSetting + var i GetEventWithSettingByIDRow err := row.Scan( &i.ID, &i.SportID, @@ -132,10 +177,21 @@ func (q *Queries) GetEventWithSettingByID(ctx context.Context, arg GetEventWithS } const GetEventsWithSettings = `-- name: GetEventsWithSettings :many -SELECT id, sport_id, match_name, home_team, away_team, home_team_id, away_team_id, home_kit_image, away_kit_image, league_id, league_name, start_time, score, match_minute, timer_status, added_time, match_period, is_live, status, fetched_at, source, default_is_active, default_is_featured, default_winning_upper_limit, is_monitored, company_id, is_active, is_featured, winning_upper_limit, updated_at, league_cc -FROM event_with_settings -WHERE company_id = $1 - AND start_time > now() +SELECT e.id, e.sport_id, e.match_name, e.home_team, e.away_team, e.home_team_id, e.away_team_id, e.home_kit_image, e.away_kit_image, e.league_id, e.league_name, e.start_time, e.score, e.match_minute, e.timer_status, e.added_time, e.match_period, e.is_live, e.status, e.fetched_at, e.source, e.default_is_active, e.default_is_featured, e.default_winning_upper_limit, e.is_monitored, + ces.company_id, + COALESCE(ces.is_active, e.default_is_active) AS is_active, + COALESCE(ces.is_featured, e.default_is_featured) AS is_featured, + COALESCE( + ces.winning_upper_limit, + e.default_winning_upper_limit + ) AS winning_upper_limit, + ces.updated_at, + l.country_code as league_cc +FROM events e + LEFT JOIN company_event_settings ces ON e.id = ces.event_id + AND ces.company_id = $1 + JOIN leagues l ON l.id = e.league_id +WHERE start_time > now() AND is_live = false AND status = 'upcoming' AND ( @@ -143,7 +199,7 @@ WHERE company_id = $1 OR $2 IS NULL ) AND ( - sport_id = $3 + e.sport_id = $3 OR $3 IS NULL ) AND ( @@ -160,11 +216,21 @@ WHERE company_id = $1 OR $6 IS NULL ) AND ( - league_cc = $7 + l.country_code = $7 OR $7 IS NULL ) + AND ( + ces.is_featured = $8 + OR e.default_is_featured = $8 + OR $8 IS NULL + ) + AND ( + ces.is_active = $9 + OR e.default_is_active = $9 + OR $9 IS NULL + ) ORDER BY start_time ASC -LIMIT $9 OFFSET $8 +LIMIT $11 OFFSET $10 ` type GetEventsWithSettingsParams struct { @@ -175,11 +241,47 @@ type GetEventsWithSettingsParams struct { LastStartTime pgtype.Timestamp `json:"last_start_time"` FirstStartTime pgtype.Timestamp `json:"first_start_time"` CountryCode pgtype.Text `json:"country_code"` + IsFeatured pgtype.Bool `json:"is_featured"` + IsActive pgtype.Bool `json:"is_active"` Offset pgtype.Int4 `json:"offset"` Limit pgtype.Int4 `json:"limit"` } -func (q *Queries) GetEventsWithSettings(ctx context.Context, arg GetEventsWithSettingsParams) ([]EventWithSetting, error) { +type GetEventsWithSettingsRow struct { + ID string `json:"id"` + SportID int32 `json:"sport_id"` + MatchName string `json:"match_name"` + HomeTeam string `json:"home_team"` + AwayTeam string `json:"away_team"` + HomeTeamID int64 `json:"home_team_id"` + AwayTeamID int64 `json:"away_team_id"` + HomeKitImage string `json:"home_kit_image"` + AwayKitImage string `json:"away_kit_image"` + LeagueID int64 `json:"league_id"` + LeagueName string `json:"league_name"` + StartTime pgtype.Timestamp `json:"start_time"` + Score pgtype.Text `json:"score"` + MatchMinute pgtype.Int4 `json:"match_minute"` + TimerStatus pgtype.Text `json:"timer_status"` + AddedTime pgtype.Int4 `json:"added_time"` + MatchPeriod pgtype.Int4 `json:"match_period"` + IsLive bool `json:"is_live"` + Status string `json:"status"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + Source string `json:"source"` + DefaultIsActive bool `json:"default_is_active"` + DefaultIsFeatured bool `json:"default_is_featured"` + DefaultWinningUpperLimit int64 `json:"default_winning_upper_limit"` + IsMonitored bool `json:"is_monitored"` + CompanyID pgtype.Int8 `json:"company_id"` + IsActive bool `json:"is_active"` + IsFeatured bool `json:"is_featured"` + WinningUpperLimit int32 `json:"winning_upper_limit"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` + LeagueCc pgtype.Text `json:"league_cc"` +} + +func (q *Queries) GetEventsWithSettings(ctx context.Context, arg GetEventsWithSettingsParams) ([]GetEventsWithSettingsRow, error) { rows, err := q.db.Query(ctx, GetEventsWithSettings, arg.CompanyID, arg.LeagueID, @@ -188,6 +290,8 @@ func (q *Queries) GetEventsWithSettings(ctx context.Context, arg GetEventsWithSe arg.LastStartTime, arg.FirstStartTime, arg.CountryCode, + arg.IsFeatured, + arg.IsActive, arg.Offset, arg.Limit, ) @@ -195,9 +299,9 @@ func (q *Queries) GetEventsWithSettings(ctx context.Context, arg GetEventsWithSe return nil, err } defer rows.Close() - var items []EventWithSetting + var items []GetEventsWithSettingsRow for rows.Next() { - var i EventWithSetting + var i GetEventsWithSettingsRow if err := rows.Scan( &i.ID, &i.SportID, @@ -401,18 +505,37 @@ func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginat return items, nil } +const GetSportAndLeagueIDs = `-- name: GetSportAndLeagueIDs :one +SELECT sport_id, league_id FROM events +WHERE id = $1 +` + +type GetSportAndLeagueIDsRow struct { + SportID int32 `json:"sport_id"` + LeagueID int64 `json:"league_id"` +} + +func (q *Queries) GetSportAndLeagueIDs(ctx context.Context, id string) (GetSportAndLeagueIDsRow, error) { + row := q.db.QueryRow(ctx, GetSportAndLeagueIDs, id) + var i GetSportAndLeagueIDsRow + err := row.Scan(&i.SportID, &i.LeagueID) + return i, err +} + const GetTotalCompanyEvents = `-- name: GetTotalCompanyEvents :one SELECT COUNT(*) -FROM event_with_settings -WHERE company_id = $1 - AND is_live = false +FROM events e + LEFT JOIN company_event_settings ces ON e.id = ces.event_id + AND ces.company_id = $1 + JOIN leagues l ON l.id = e.league_id +WHERE is_live = false AND status = 'upcoming' AND ( league_id = $2 OR $2 IS NULL ) AND ( - sport_id = $3 + e.sport_id = $3 OR $3 IS NULL ) AND ( @@ -429,9 +552,19 @@ WHERE company_id = $1 OR $6 IS NULL ) AND ( - league_cc = $7 + l.country_code = $7 OR $7 IS NULL ) + AND ( + ces.is_featured = $8 + OR e.default_is_featured = $8 + OR $8 IS NULL + ) + AND ( + ces.is_active = $9 + OR e.default_is_active = $9 + OR $9 IS NULL + ) ` type GetTotalCompanyEventsParams struct { @@ -442,6 +575,8 @@ type GetTotalCompanyEventsParams struct { LastStartTime pgtype.Timestamp `json:"last_start_time"` FirstStartTime pgtype.Timestamp `json:"first_start_time"` CountryCode pgtype.Text `json:"country_code"` + IsFeatured pgtype.Bool `json:"is_featured"` + IsActive pgtype.Bool `json:"is_active"` } func (q *Queries) GetTotalCompanyEvents(ctx context.Context, arg GetTotalCompanyEventsParams) (int64, error) { @@ -453,6 +588,8 @@ func (q *Queries) GetTotalCompanyEvents(ctx context.Context, arg GetTotalCompany arg.LastStartTime, arg.FirstStartTime, arg.CountryCode, + arg.IsFeatured, + arg.IsActive, ) var count int64 err := row.Scan(&count) @@ -573,7 +710,8 @@ INSERT INTO events ( start_time, is_live, status, - source + source, + default_winning_upper_limit ) VALUES ( $1, @@ -590,7 +728,8 @@ VALUES ( $12, $13, $14, - $15 + $15, + $16 ) ON CONFLICT (id) DO UPDATE SET sport_id = EXCLUDED.sport_id, @@ -603,7 +742,6 @@ SET sport_id = EXCLUDED.sport_id, away_kit_image = EXCLUDED.away_kit_image, league_id = EXCLUDED.league_id, league_name = EXCLUDED.league_name, - league_cc = EXCLUDED.league_cc, start_time = EXCLUDED.start_time, score = EXCLUDED.score, match_minute = EXCLUDED.match_minute, @@ -612,25 +750,27 @@ SET sport_id = EXCLUDED.sport_id, match_period = EXCLUDED.match_period, is_live = EXCLUDED.is_live, source = EXCLUDED.source, + default_winning_upper_limit = EXCLUDED.default_winning_upper_limit, fetched_at = now() ` type InsertEventParams struct { - ID string `json:"id"` - SportID int32 `json:"sport_id"` - MatchName string `json:"match_name"` - HomeTeam string `json:"home_team"` - AwayTeam string `json:"away_team"` - HomeTeamID int64 `json:"home_team_id"` - AwayTeamID int64 `json:"away_team_id"` - HomeKitImage string `json:"home_kit_image"` - AwayKitImage string `json:"away_kit_image"` - LeagueID int64 `json:"league_id"` - LeagueName string `json:"league_name"` - StartTime pgtype.Timestamp `json:"start_time"` - IsLive bool `json:"is_live"` - Status string `json:"status"` - Source string `json:"source"` + ID string `json:"id"` + SportID int32 `json:"sport_id"` + MatchName string `json:"match_name"` + HomeTeam string `json:"home_team"` + AwayTeam string `json:"away_team"` + HomeTeamID int64 `json:"home_team_id"` + AwayTeamID int64 `json:"away_team_id"` + HomeKitImage string `json:"home_kit_image"` + AwayKitImage string `json:"away_kit_image"` + LeagueID int64 `json:"league_id"` + LeagueName string `json:"league_name"` + StartTime pgtype.Timestamp `json:"start_time"` + IsLive bool `json:"is_live"` + Status string `json:"status"` + Source string `json:"source"` + DefaultWinningUpperLimit int64 `json:"default_winning_upper_limit"` } func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error { @@ -650,40 +790,7 @@ func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error arg.IsLive, arg.Status, arg.Source, - ) - return err -} - -const InsertEventSettings = `-- name: InsertEventSettings :exec -INSERT INTO company_event_settings ( - company_id, - event_id, - is_active, - is_featured, - winning_upper_limit - ) -VALUES ($1, $2, $3, $4, $5) ON CONFLICT(company_id, event_id) DO -UPDATE -SET is_active = EXCLUDED.is_active, - is_featured = EXCLUDED.is_featured, - winning_upper_limit = EXCLUDED.winning_upper_limit -` - -type InsertEventSettingsParams struct { - CompanyID int64 `json:"company_id"` - EventID string `json:"event_id"` - IsActive pgtype.Bool `json:"is_active"` - IsFeatured pgtype.Bool `json:"is_featured"` - WinningUpperLimit pgtype.Int4 `json:"winning_upper_limit"` -} - -func (q *Queries) InsertEventSettings(ctx context.Context, arg InsertEventSettingsParams) error { - _, err := q.db.Exec(ctx, InsertEventSettings, - arg.CompanyID, - arg.EventID, - arg.IsActive, - arg.IsFeatured, - arg.WinningUpperLimit, + arg.DefaultWinningUpperLimit, ) return err } @@ -727,6 +834,40 @@ func (q *Queries) ListLiveEvents(ctx context.Context) ([]string, error) { return items, nil } +const SaveEventSettings = `-- name: SaveEventSettings :exec +INSERT INTO company_event_settings ( + company_id, + event_id, + is_active, + is_featured, + winning_upper_limit + ) +VALUES ($1, $2, $3, $4, $5) ON CONFLICT(company_id, event_id) DO +UPDATE +SET is_active = EXCLUDED.is_active, + is_featured = EXCLUDED.is_featured, + winning_upper_limit = EXCLUDED.winning_upper_limit +` + +type SaveEventSettingsParams struct { + CompanyID int64 `json:"company_id"` + EventID string `json:"event_id"` + IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` + WinningUpperLimit pgtype.Int4 `json:"winning_upper_limit"` +} + +func (q *Queries) SaveEventSettings(ctx context.Context, arg SaveEventSettingsParams) error { + _, err := q.db.Exec(ctx, SaveEventSettings, + arg.CompanyID, + arg.EventID, + arg.IsActive, + arg.IsFeatured, + arg.WinningUpperLimit, + ) + return err +} + const UpdateEventMonitored = `-- name: UpdateEventMonitored :exec UPDATE events SET is_monitored = $1 @@ -743,40 +884,6 @@ func (q *Queries) UpdateEventMonitored(ctx context.Context, arg UpdateEventMonit return err } -const UpdateEventSettings = `-- name: UpdateEventSettings :exec -UPDATE company_event_settings -SET is_active = COALESCE($3, is_active), - is_featured = COALESCE( - $4, - is_featured - ), - winning_upper_limit = COALESCE( - $5, - winning_upper_limit - ) -WHERE event_id = $1 - AND company_id = $2 -` - -type UpdateEventSettingsParams struct { - EventID string `json:"event_id"` - CompanyID int64 `json:"company_id"` - IsActive pgtype.Bool `json:"is_active"` - IsFeatured pgtype.Bool `json:"is_featured"` - WinningUpperLimit pgtype.Int4 `json:"winning_upper_limit"` -} - -func (q *Queries) UpdateEventSettings(ctx context.Context, arg UpdateEventSettingsParams) error { - _, err := q.db.Exec(ctx, UpdateEventSettings, - arg.EventID, - arg.CompanyID, - arg.IsActive, - arg.IsFeatured, - arg.WinningUpperLimit, - ) - return err -} - const UpdateMatchResult = `-- name: UpdateMatchResult :exec UPDATE events SET score = $1, diff --git a/gen/db/events_stat.sql.go b/gen/db/events_stat.sql.go index 677fa2a..615e2fa 100644 --- a/gen/db/events_stat.sql.go +++ b/gen/db/events_stat.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: events_stat.sql package dbgen diff --git a/gen/db/flags.sql.go b/gen/db/flags.sql.go index 653543f..4b82cac 100644 --- a/gen/db/flags.sql.go +++ b/gen/db/flags.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: flags.sql package dbgen diff --git a/gen/db/institutions.sql.go b/gen/db/institutions.sql.go index 324ac3e..61ca108 100644 --- a/gen/db/institutions.sql.go +++ b/gen/db/institutions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: institutions.sql package dbgen diff --git a/gen/db/issue_reporting.sql.go b/gen/db/issue_reporting.sql.go index 7fcb4af..e35fba1 100644 --- a/gen/db/issue_reporting.sql.go +++ b/gen/db/issue_reporting.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: issue_reporting.sql package dbgen diff --git a/gen/db/leagues.sql.go b/gen/db/leagues.sql.go index 5d49d4d..0aaad2c 100644 --- a/gen/db/leagues.sql.go +++ b/gen/db/leagues.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: leagues.sql package dbgen @@ -44,13 +44,18 @@ WHERE ( sport_id = $2 OR $2 IS NULL ) + AND ( + name ILIKE '%' || $3 || '%' + OR $3 IS NULL + ) ORDER BY name ASC -LIMIT $4 OFFSET $3 +LIMIT $5 OFFSET $4 ` type GetAllLeaguesParams struct { CountryCode pgtype.Text `json:"country_code"` SportID pgtype.Int4 `json:"sport_id"` + Query pgtype.Text `json:"query"` Offset pgtype.Int4 `json:"offset"` Limit pgtype.Int4 `json:"limit"` } @@ -59,6 +64,7 @@ func (q *Queries) GetAllLeagues(ctx context.Context, arg GetAllLeaguesParams) ([ rows, err := q.db.Query(ctx, GetAllLeagues, arg.CountryCode, arg.SportID, + arg.Query, arg.Offset, arg.Limit, ) @@ -90,10 +96,15 @@ func (q *Queries) GetAllLeagues(ctx context.Context, arg GetAllLeaguesParams) ([ } const GetAllLeaguesWithSettings = `-- name: GetAllLeaguesWithSettings :many -SELECT id, name, img_url, country_code, bet365_id, sport_id, default_is_active, default_is_featured, company_id, is_active, is_featured, updated_at -FROM league_with_settings -WHERE (company_id = $1) - AND ( +SELECT l.id, l.name, l.img_url, l.country_code, l.bet365_id, l.sport_id, l.default_is_active, l.default_is_featured, + cls.company_id, + COALESCE(cls.is_active, l.default_is_active) AS is_active, + COALESCE(cls.is_featured, l.default_is_featured) AS is_featured, + cls.updated_at +FROM leagues l + LEFT JOIN company_league_settings cls ON l.id = cls.league_id + AND company_id = $1 +WHERE ( country_code = $2 OR $2 IS NULL ) @@ -103,15 +114,21 @@ WHERE (company_id = $1) ) AND ( is_active = $4 + OR default_is_active = $4 OR $4 IS NULL ) AND ( is_featured = $5 + OR default_is_featured = $5 OR $5 IS NULL ) + AND ( + name ILIKE '%' || $6 || '%' + OR $6 IS NULL + ) ORDER BY is_featured DESC, name ASC -LIMIT $7 OFFSET $6 +LIMIT $8 OFFSET $7 ` type GetAllLeaguesWithSettingsParams struct { @@ -120,17 +137,34 @@ type GetAllLeaguesWithSettingsParams struct { SportID pgtype.Int4 `json:"sport_id"` IsActive pgtype.Bool `json:"is_active"` IsFeatured pgtype.Bool `json:"is_featured"` + Query pgtype.Text `json:"query"` Offset pgtype.Int4 `json:"offset"` Limit pgtype.Int4 `json:"limit"` } -func (q *Queries) GetAllLeaguesWithSettings(ctx context.Context, arg GetAllLeaguesWithSettingsParams) ([]LeagueWithSetting, error) { +type GetAllLeaguesWithSettingsRow struct { + ID int64 `json:"id"` + Name string `json:"name"` + ImgUrl pgtype.Text `json:"img_url"` + CountryCode pgtype.Text `json:"country_code"` + Bet365ID pgtype.Int4 `json:"bet365_id"` + SportID int32 `json:"sport_id"` + DefaultIsActive bool `json:"default_is_active"` + DefaultIsFeatured bool `json:"default_is_featured"` + CompanyID pgtype.Int8 `json:"company_id"` + IsActive bool `json:"is_active"` + IsFeatured bool `json:"is_featured"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` +} + +func (q *Queries) GetAllLeaguesWithSettings(ctx context.Context, arg GetAllLeaguesWithSettingsParams) ([]GetAllLeaguesWithSettingsRow, error) { rows, err := q.db.Query(ctx, GetAllLeaguesWithSettings, arg.CompanyID, arg.CountryCode, arg.SportID, arg.IsActive, arg.IsFeatured, + arg.Query, arg.Offset, arg.Limit, ) @@ -138,9 +172,9 @@ func (q *Queries) GetAllLeaguesWithSettings(ctx context.Context, arg GetAllLeagu return nil, err } defer rows.Close() - var items []LeagueWithSetting + var items []GetAllLeaguesWithSettingsRow for rows.Next() { - var i LeagueWithSetting + var i GetAllLeaguesWithSettingsRow if err := rows.Scan( &i.ID, &i.Name, @@ -165,6 +199,58 @@ func (q *Queries) GetAllLeaguesWithSettings(ctx context.Context, arg GetAllLeagu return items, nil } +const GetTotalLeaguesWithSettings = `-- name: GetTotalLeaguesWithSettings :one +SELECT COUNT(*) +FROM leagues l + LEFT JOIN company_league_settings cls ON l.id = cls.league_id + AND company_id = $1 +WHERE ( + country_code = $2 + OR $2 IS NULL + ) + AND ( + sport_id = $3 + OR $3 IS NULL + ) + AND ( + is_active = $4 + OR default_is_active = $4 + OR $4 IS NULL + ) + AND ( + is_featured = $5 + OR default_is_featured = $5 + OR $5 IS NULL + ) + AND ( + name ILIKE '%' || $6 || '%' + OR $6 IS NULL + ) +` + +type GetTotalLeaguesWithSettingsParams struct { + CompanyID int64 `json:"company_id"` + CountryCode pgtype.Text `json:"country_code"` + SportID pgtype.Int4 `json:"sport_id"` + IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` + Query pgtype.Text `json:"query"` +} + +func (q *Queries) GetTotalLeaguesWithSettings(ctx context.Context, arg GetTotalLeaguesWithSettingsParams) (int64, error) { + row := q.db.QueryRow(ctx, GetTotalLeaguesWithSettings, + arg.CompanyID, + arg.CountryCode, + arg.SportID, + arg.IsActive, + arg.IsFeatured, + arg.Query, + ) + var count int64 + err := row.Scan(&count) + return count, err +} + const InsertLeague = `-- name: InsertLeague :exec INSERT INTO leagues ( id, diff --git a/gen/db/location.sql.go b/gen/db/location.sql.go index 008aa61..254c73a 100644 --- a/gen/db/location.sql.go +++ b/gen/db/location.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: location.sql package dbgen diff --git a/gen/db/models.go b/gen/db/models.go index 68ebcd4..1e27632 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -1,60 +1,13 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 package dbgen import ( - "database/sql/driver" - "fmt" - "github.com/jackc/pgx/v5/pgtype" ) -type Referralstatus string - -const ( - ReferralstatusPENDING Referralstatus = "PENDING" - ReferralstatusCOMPLETED Referralstatus = "COMPLETED" - ReferralstatusEXPIRED Referralstatus = "EXPIRED" - ReferralstatusCANCELLED Referralstatus = "CANCELLED" -) - -func (e *Referralstatus) Scan(src interface{}) error { - switch s := src.(type) { - case []byte: - *e = Referralstatus(s) - case string: - *e = Referralstatus(s) - default: - return fmt.Errorf("unsupported scan type for Referralstatus: %T", src) - } - return nil -} - -type NullReferralstatus struct { - Referralstatus Referralstatus `json:"referralstatus"` - Valid bool `json:"valid"` // Valid is true if Referralstatus is not NULL -} - -// Scan implements the Scanner interface. -func (ns *NullReferralstatus) Scan(value interface{}) error { - if value == nil { - ns.Referralstatus, ns.Valid = "", false - return nil - } - ns.Valid = true - return ns.Referralstatus.Scan(value) -} - -// Value implements the driver Valuer interface. -func (ns NullReferralstatus) Value() (driver.Value, error) { - if !ns.Valid { - return nil, nil - } - return string(ns.Referralstatus), nil -} - type Bank struct { ID int64 `json:"id"` Slug string `json:"slug"` @@ -126,12 +79,6 @@ type BetWithOutcome struct { Outcomes []BetOutcome `json:"outcomes"` } -type Bonu struct { - Multiplier float32 `json:"multiplier"` - ID int64 `json:"id"` - BalanceCap int64 `json:"balance_cap"` -} - type Branch struct { ID int64 `json:"id"` Name string `json:"name"` @@ -321,7 +268,7 @@ type Event struct { Source string `json:"source"` DefaultIsActive bool `json:"default_is_active"` DefaultIsFeatured bool `json:"default_is_featured"` - DefaultWinningUpperLimit int32 `json:"default_winning_upper_limit"` + DefaultWinningUpperLimit int64 `json:"default_winning_upper_limit"` IsMonitored bool `json:"is_monitored"` } @@ -356,7 +303,7 @@ type EventWithCountry struct { Source string `json:"source"` DefaultIsActive bool `json:"default_is_active"` DefaultIsFeatured bool `json:"default_is_featured"` - DefaultWinningUpperLimit int32 `json:"default_winning_upper_limit"` + DefaultWinningUpperLimit int64 `json:"default_winning_upper_limit"` IsMonitored bool `json:"is_monitored"` LeagueCc pgtype.Text `json:"league_cc"` } @@ -385,9 +332,9 @@ type EventWithSetting struct { Source string `json:"source"` DefaultIsActive bool `json:"default_is_active"` DefaultIsFeatured bool `json:"default_is_featured"` - DefaultWinningUpperLimit int32 `json:"default_winning_upper_limit"` + DefaultWinningUpperLimit int64 `json:"default_winning_upper_limit"` IsMonitored bool `json:"is_monitored"` - CompanyID int64 `json:"company_id"` + CompanyID pgtype.Int8 `json:"company_id"` IsActive bool `json:"is_active"` IsFeatured bool `json:"is_featured"` WinningUpperLimit int32 `json:"winning_upper_limit"` @@ -447,7 +394,7 @@ type LeagueWithSetting struct { SportID int32 `json:"sport_id"` DefaultIsActive bool `json:"default_is_active"` DefaultIsFeatured bool `json:"default_is_featured"` - CompanyID int64 `json:"company_id"` + CompanyID pgtype.Int8 `json:"company_id"` IsActive bool `json:"is_active"` IsFeatured bool `json:"is_featured"` UpdatedAt pgtype.Timestamp `json:"updated_at"` @@ -520,7 +467,7 @@ type OddsMarketWithSetting struct { DefaultIsActive bool `json:"default_is_active"` FetchedAt pgtype.Timestamp `json:"fetched_at"` ExpiresAt pgtype.Timestamp `json:"expires_at"` - CompanyID int64 `json:"company_id"` + CompanyID pgtype.Int8 `json:"company_id"` IsActive bool `json:"is_active"` RawOdds []byte `json:"raw_odds"` UpdatedAt pgtype.Timestamp `json:"updated_at"` @@ -538,30 +485,54 @@ type Otp struct { ExpiresAt pgtype.Timestamptz `json:"expires_at"` } -type Referral struct { - ID int64 `json:"id"` - ReferralCode string `json:"referral_code"` - ReferrerID string `json:"referrer_id"` - ReferredID pgtype.Text `json:"referred_id"` - Status Referralstatus `json:"status"` - RewardAmount pgtype.Numeric `json:"reward_amount"` - CashbackAmount pgtype.Numeric `json:"cashback_amount"` - CreatedAt pgtype.Timestamptz `json:"created_at"` - UpdatedAt pgtype.Timestamptz `json:"updated_at"` - ExpiresAt pgtype.Timestamptz `json:"expires_at"` +type Raffle struct { + ID int32 `json:"id"` + CompanyID int32 `json:"company_id"` + Name string `json:"name"` + CreatedAt pgtype.Timestamp `json:"created_at"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + Type string `json:"type"` + Status string `json:"status"` } -type ReferralSetting struct { - ID int64 `json:"id"` - ReferralRewardAmount pgtype.Numeric `json:"referral_reward_amount"` - CashbackPercentage pgtype.Numeric `json:"cashback_percentage"` - BetReferralBonusPercentage pgtype.Numeric `json:"bet_referral_bonus_percentage"` - MaxReferrals int32 `json:"max_referrals"` - ExpiresAfterDays int32 `json:"expires_after_days"` - UpdatedBy string `json:"updated_by"` - CreatedAt pgtype.Timestamptz `json:"created_at"` - UpdatedAt pgtype.Timestamptz `json:"updated_at"` - Version int32 `json:"version"` +type RaffleGameFilter struct { + ID int32 `json:"id"` + RaffleID int32 `json:"raffle_id"` + GameID string `json:"game_id"` +} + +type RaffleSportFilter struct { + ID int32 `json:"id"` + RaffleID int32 `json:"raffle_id"` + SportID int64 `json:"sport_id"` + LeagueID int64 `json:"league_id"` +} + +type RaffleTicket struct { + ID int32 `json:"id"` + RaffleID int32 `json:"raffle_id"` + UserID int32 `json:"user_id"` + IsActive pgtype.Bool `json:"is_active"` +} + +type RaffleWinner struct { + ID int32 `json:"id"` + RaffleID int32 `json:"raffle_id"` + UserID int32 `json:"user_id"` + Rank int32 `json:"rank"` + CreatedAt pgtype.Timestamp `json:"created_at"` +} + +type ReferralCode struct { + ID int64 `json:"id"` + ReferralCode string `json:"referral_code"` + ReferrerID int64 `json:"referrer_id"` + CompanyID int64 `json:"company_id"` + IsActive bool `json:"is_active"` + NumberOfReferrals int64 `json:"number_of_referrals"` + RewardAmount int64 `json:"reward_amount"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` } type RefreshToken struct { @@ -794,8 +765,20 @@ type User struct { CompanyID pgtype.Int8 `json:"company_id"` SuspendedAt pgtype.Timestamptz `json:"suspended_at"` Suspended bool `json:"suspended"` - ReferralCode pgtype.Text `json:"referral_code"` - ReferredBy pgtype.Text `json:"referred_by"` +} + +type UserBonuse struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Type string `json:"type"` + UserID int64 `json:"user_id"` + RewardAmount int64 `json:"reward_amount"` + IsClaimed bool `json:"is_claimed"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + ClaimedAt pgtype.Timestamp `json:"claimed_at"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` } type UserGameInteraction struct { @@ -808,6 +791,13 @@ type UserGameInteraction struct { CreatedAt pgtype.Timestamptz `json:"created_at"` } +type UserReferral struct { + ID int64 `json:"id"` + ReferredID int64 `json:"referred_id"` + ReferralCodeID int64 `json:"referral_code_id"` + CreatedAt pgtype.Timestamptz `json:"created_at"` +} + type VirtualGame struct { ID int64 `json:"id"` GameID string `json:"game_id"` @@ -896,8 +886,6 @@ type Wallet struct { IsActive bool `json:"is_active"` CreatedAt pgtype.Timestamp `json:"created_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"` - BonusBalance pgtype.Numeric `json:"bonus_balance"` - CashBalance pgtype.Numeric `json:"cash_balance"` } type WalletThresholdNotification struct { diff --git a/gen/db/monitor.sql.go b/gen/db/monitor.sql.go index a9a7ecb..b5f248f 100644 --- a/gen/db/monitor.sql.go +++ b/gen/db/monitor.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: monitor.sql package dbgen diff --git a/gen/db/notification.sql.go b/gen/db/notification.sql.go index ba9882b..9ce7e42 100644 --- a/gen/db/notification.sql.go +++ b/gen/db/notification.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: notification.sql package dbgen diff --git a/gen/db/odd_history.sql.go b/gen/db/odd_history.sql.go index 0a0333d..dd69a51 100644 --- a/gen/db/odd_history.sql.go +++ b/gen/db/odd_history.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: odd_history.sql package dbgen diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index 33fcde8..d194d14 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: odds.sql package dbgen @@ -68,9 +68,22 @@ func (q *Queries) GetAllOdds(ctx context.Context, arg GetAllOddsParams) ([]OddsM } const GetAllOddsWithSettings = `-- name: GetAllOddsWithSettings :many -SELECT id, event_id, market_type, market_name, market_category, market_id, default_is_active, fetched_at, expires_at, company_id, is_active, raw_odds, updated_at -FROM odds_market_with_settings -WHERE company_id = $1 +SELECT o.id, + o.event_id, + o.market_type, + o.market_name, + o.market_category, + o.market_id, + o.default_is_active, + o.fetched_at, + o.expires_at, + cos.company_id, + COALESCE(cos.is_active, o.default_is_active) AS is_active, + COALESCE(cos.custom_raw_odds, o.raw_odds) AS raw_odds, + cos.updated_at +FROM odds_market o + LEFT JOIN company_odd_settings cos ON o.id = cos.odds_market_id + AND company_id = $1 LIMIT $3 OFFSET $2 ` @@ -80,15 +93,31 @@ type GetAllOddsWithSettingsParams struct { Limit pgtype.Int4 `json:"limit"` } -func (q *Queries) GetAllOddsWithSettings(ctx context.Context, arg GetAllOddsWithSettingsParams) ([]OddsMarketWithSetting, error) { +type GetAllOddsWithSettingsRow struct { + ID int64 `json:"id"` + EventID string `json:"event_id"` + MarketType string `json:"market_type"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID string `json:"market_id"` + DefaultIsActive bool `json:"default_is_active"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + CompanyID pgtype.Int8 `json:"company_id"` + IsActive bool `json:"is_active"` + RawOdds []byte `json:"raw_odds"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` +} + +func (q *Queries) GetAllOddsWithSettings(ctx context.Context, arg GetAllOddsWithSettingsParams) ([]GetAllOddsWithSettingsRow, error) { rows, err := q.db.Query(ctx, GetAllOddsWithSettings, arg.CompanyID, arg.Offset, arg.Limit) if err != nil { return nil, err } defer rows.Close() - var items []OddsMarketWithSetting + var items []GetAllOddsWithSettingsRow for rows.Next() { - var i OddsMarketWithSetting + var i GetAllOddsWithSettingsRow if err := rows.Scan( &i.ID, &i.EventID, @@ -114,6 +143,34 @@ func (q *Queries) GetAllOddsWithSettings(ctx context.Context, arg GetAllOddsWith return items, nil } +const GetOddByID = `-- name: GetOddByID :one +SELECT id, event_id, market_type, market_name, market_category, market_id, raw_odds, default_is_active, fetched_at, expires_at, is_monitored, is_live, status, source +FROM odds_market_with_event +WHERE id = $1 +` + +func (q *Queries) GetOddByID(ctx context.Context, id int64) (OddsMarketWithEvent, error) { + row := q.db.QueryRow(ctx, GetOddByID, id) + var i OddsMarketWithEvent + err := row.Scan( + &i.ID, + &i.EventID, + &i.MarketType, + &i.MarketName, + &i.MarketCategory, + &i.MarketID, + &i.RawOdds, + &i.DefaultIsActive, + &i.FetchedAt, + &i.ExpiresAt, + &i.IsMonitored, + &i.IsLive, + &i.Status, + &i.Source, + ) + return i, err +} + const GetOddsByEventID = `-- name: GetOddsByEventID :many SELECT id, event_id, market_type, market_name, market_category, market_id, raw_odds, default_is_active, fetched_at, expires_at, is_monitored, is_live, status, source FROM odds_market_with_event @@ -219,10 +276,23 @@ func (q *Queries) GetOddsByMarketID(ctx context.Context, arg GetOddsByMarketIDPa } const GetOddsWithSettingsByEventID = `-- name: GetOddsWithSettingsByEventID :many -SELECT id, event_id, market_type, market_name, market_category, market_id, default_is_active, fetched_at, expires_at, company_id, is_active, raw_odds, updated_at -FROM odds_market_with_settings -WHERE event_id = $1 +SELECT o.id, + o.event_id, + o.market_type, + o.market_name, + o.market_category, + o.market_id, + o.default_is_active, + o.fetched_at, + o.expires_at, + cos.company_id, + COALESCE(cos.is_active, o.default_is_active) AS is_active, + COALESCE(cos.custom_raw_odds, o.raw_odds) AS raw_odds, + cos.updated_at +FROM odds_market o + LEFT JOIN company_odd_settings cos ON o.id = cos.odds_market_id AND company_id = $2 +WHERE event_id = $1 LIMIT $4 OFFSET $3 ` @@ -233,7 +303,23 @@ type GetOddsWithSettingsByEventIDParams struct { Limit pgtype.Int4 `json:"limit"` } -func (q *Queries) GetOddsWithSettingsByEventID(ctx context.Context, arg GetOddsWithSettingsByEventIDParams) ([]OddsMarketWithSetting, error) { +type GetOddsWithSettingsByEventIDRow struct { + ID int64 `json:"id"` + EventID string `json:"event_id"` + MarketType string `json:"market_type"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID string `json:"market_id"` + DefaultIsActive bool `json:"default_is_active"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + CompanyID pgtype.Int8 `json:"company_id"` + IsActive bool `json:"is_active"` + RawOdds []byte `json:"raw_odds"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` +} + +func (q *Queries) GetOddsWithSettingsByEventID(ctx context.Context, arg GetOddsWithSettingsByEventIDParams) ([]GetOddsWithSettingsByEventIDRow, error) { rows, err := q.db.Query(ctx, GetOddsWithSettingsByEventID, arg.EventID, arg.CompanyID, @@ -244,9 +330,9 @@ func (q *Queries) GetOddsWithSettingsByEventID(ctx context.Context, arg GetOddsW return nil, err } defer rows.Close() - var items []OddsMarketWithSetting + var items []GetOddsWithSettingsByEventIDRow for rows.Next() { - var i OddsMarketWithSetting + var i GetOddsWithSettingsByEventIDRow if err := rows.Scan( &i.ID, &i.EventID, @@ -272,23 +358,50 @@ func (q *Queries) GetOddsWithSettingsByEventID(ctx context.Context, arg GetOddsW return items, nil } -const GetOddsWithSettingsByMarketID = `-- name: GetOddsWithSettingsByMarketID :one -SELECT id, event_id, market_type, market_name, market_category, market_id, default_is_active, fetched_at, expires_at, company_id, is_active, raw_odds, updated_at -FROM odds_market_with_settings -WHERE market_id = $1 - AND event_id = $2 - AND company_id = $3 +const GetOddsWithSettingsByID = `-- name: GetOddsWithSettingsByID :one +SELECT o.id, + o.event_id, + o.market_type, + o.market_name, + o.market_category, + o.market_id, + o.default_is_active, + o.fetched_at, + o.expires_at, + cos.company_id, + COALESCE(cos.is_active, o.default_is_active) AS is_active, + COALESCE(cos.custom_raw_odds, o.raw_odds) AS raw_odds, + cos.updated_at +FROM odds_market o + LEFT JOIN company_odd_settings cos ON o.id = cos.odds_market_id + AND company_id = $2 +WHERE o.id = $1 ` -type GetOddsWithSettingsByMarketIDParams struct { - MarketID string `json:"market_id"` - EventID string `json:"event_id"` - CompanyID int64 `json:"company_id"` +type GetOddsWithSettingsByIDParams struct { + ID int64 `json:"id"` + CompanyID int64 `json:"company_id"` } -func (q *Queries) GetOddsWithSettingsByMarketID(ctx context.Context, arg GetOddsWithSettingsByMarketIDParams) (OddsMarketWithSetting, error) { - row := q.db.QueryRow(ctx, GetOddsWithSettingsByMarketID, arg.MarketID, arg.EventID, arg.CompanyID) - var i OddsMarketWithSetting +type GetOddsWithSettingsByIDRow struct { + ID int64 `json:"id"` + EventID string `json:"event_id"` + MarketType string `json:"market_type"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID string `json:"market_id"` + DefaultIsActive bool `json:"default_is_active"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + CompanyID pgtype.Int8 `json:"company_id"` + IsActive bool `json:"is_active"` + RawOdds []byte `json:"raw_odds"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` +} + +func (q *Queries) GetOddsWithSettingsByID(ctx context.Context, arg GetOddsWithSettingsByIDParams) (GetOddsWithSettingsByIDRow, error) { + row := q.db.QueryRow(ctx, GetOddsWithSettingsByID, arg.ID, arg.CompanyID) + var i GetOddsWithSettingsByIDRow err := row.Scan( &i.ID, &i.EventID, @@ -307,34 +420,68 @@ func (q *Queries) GetOddsWithSettingsByMarketID(ctx context.Context, arg GetOdds return i, err } -const InsertOddSettings = `-- name: InsertOddSettings :exec -INSERT INTO company_odd_settings ( - company_id, - odds_market_id, - is_active, - custom_raw_odds - ) -VALUES ($1, $2, $3, $4) ON CONFLICT (company_id, odds_market_id) DO -UPDATE -SET is_active = EXCLUDED.is_active, - custom_raw_odds = EXCLUDED.custom_raw_odds +const GetOddsWithSettingsByMarketID = `-- name: GetOddsWithSettingsByMarketID :one +SELECT o.id, + o.event_id, + o.market_type, + o.market_name, + o.market_category, + o.market_id, + o.default_is_active, + o.fetched_at, + o.expires_at, + cos.company_id, + COALESCE(cos.is_active, o.default_is_active) AS is_active, + COALESCE(cos.custom_raw_odds, o.raw_odds) AS raw_odds, + cos.updated_at +FROM odds_market o + LEFT JOIN company_odd_settings cos ON o.id = cos.odds_market_id + AND company_id = $3 +WHERE market_id = $1 + AND event_id = $2 ` -type InsertOddSettingsParams struct { - CompanyID int64 `json:"company_id"` - OddsMarketID int64 `json:"odds_market_id"` - IsActive pgtype.Bool `json:"is_active"` - CustomRawOdds []byte `json:"custom_raw_odds"` +type GetOddsWithSettingsByMarketIDParams struct { + MarketID string `json:"market_id"` + EventID string `json:"event_id"` + CompanyID int64 `json:"company_id"` } -func (q *Queries) InsertOddSettings(ctx context.Context, arg InsertOddSettingsParams) error { - _, err := q.db.Exec(ctx, InsertOddSettings, - arg.CompanyID, - arg.OddsMarketID, - arg.IsActive, - arg.CustomRawOdds, +type GetOddsWithSettingsByMarketIDRow struct { + ID int64 `json:"id"` + EventID string `json:"event_id"` + MarketType string `json:"market_type"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID string `json:"market_id"` + DefaultIsActive bool `json:"default_is_active"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + CompanyID pgtype.Int8 `json:"company_id"` + IsActive bool `json:"is_active"` + RawOdds []byte `json:"raw_odds"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` +} + +func (q *Queries) GetOddsWithSettingsByMarketID(ctx context.Context, arg GetOddsWithSettingsByMarketIDParams) (GetOddsWithSettingsByMarketIDRow, error) { + row := q.db.QueryRow(ctx, GetOddsWithSettingsByMarketID, arg.MarketID, arg.EventID, arg.CompanyID) + var i GetOddsWithSettingsByMarketIDRow + err := row.Scan( + &i.ID, + &i.EventID, + &i.MarketType, + &i.MarketName, + &i.MarketCategory, + &i.MarketID, + &i.DefaultIsActive, + &i.FetchedAt, + &i.ExpiresAt, + &i.CompanyID, + &i.IsActive, + &i.RawOdds, + &i.UpdatedAt, ) - return err + return i, err } const InsertOddsMarket = `-- name: InsertOddsMarket :exec @@ -391,3 +538,33 @@ func (q *Queries) InsertOddsMarket(ctx context.Context, arg InsertOddsMarketPara ) return err } + +const SaveOddSettings = `-- name: SaveOddSettings :exec +INSERT INTO company_odd_settings ( + company_id, + odds_market_id, + is_active, + custom_raw_odds + ) +VALUES ($1, $2, $3, $4) ON CONFLICT (company_id, odds_market_id) DO +UPDATE +SET is_active = EXCLUDED.is_active, + custom_raw_odds = EXCLUDED.custom_raw_odds +` + +type SaveOddSettingsParams struct { + CompanyID int64 `json:"company_id"` + OddsMarketID int64 `json:"odds_market_id"` + IsActive pgtype.Bool `json:"is_active"` + CustomRawOdds []byte `json:"custom_raw_odds"` +} + +func (q *Queries) SaveOddSettings(ctx context.Context, arg SaveOddSettingsParams) error { + _, err := q.db.Exec(ctx, SaveOddSettings, + arg.CompanyID, + arg.OddsMarketID, + arg.IsActive, + arg.CustomRawOdds, + ) + return err +} diff --git a/gen/db/otp.sql.go b/gen/db/otp.sql.go index 7dba175..c96aaaa 100644 --- a/gen/db/otp.sql.go +++ b/gen/db/otp.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: otp.sql package dbgen diff --git a/gen/db/raffle.sql.go b/gen/db/raffle.sql.go new file mode 100644 index 0000000..a4888f9 --- /dev/null +++ b/gen/db/raffle.sql.go @@ -0,0 +1,328 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: raffle.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const AddSportRaffleFilter = `-- name: AddSportRaffleFilter :one +INSERT INTO raffle_sport_filters (raffle_id, sport_id, league_id) +VALUES ($1, $2, $3) +RETURNING id, raffle_id, sport_id, league_id +` + +type AddSportRaffleFilterParams struct { + RaffleID int32 `json:"raffle_id"` + SportID int64 `json:"sport_id"` + LeagueID int64 `json:"league_id"` +} + +func (q *Queries) AddSportRaffleFilter(ctx context.Context, arg AddSportRaffleFilterParams) (RaffleSportFilter, error) { + row := q.db.QueryRow(ctx, AddSportRaffleFilter, arg.RaffleID, arg.SportID, arg.LeagueID) + var i RaffleSportFilter + err := row.Scan( + &i.ID, + &i.RaffleID, + &i.SportID, + &i.LeagueID, + ) + return i, err +} + +const CheckValidSportRaffleFilter = `-- name: CheckValidSportRaffleFilter :one +SELECT COUNT(*) > 0 AS exists +FROM raffle_sport_filters +WHERE raffle_id = $1 + AND sport_id = $2 + AND league_id = $3 +` + +type CheckValidSportRaffleFilterParams struct { + RaffleID int32 `json:"raffle_id"` + SportID int64 `json:"sport_id"` + LeagueID int64 `json:"league_id"` +} + +func (q *Queries) CheckValidSportRaffleFilter(ctx context.Context, arg CheckValidSportRaffleFilterParams) (bool, error) { + row := q.db.QueryRow(ctx, CheckValidSportRaffleFilter, arg.RaffleID, arg.SportID, arg.LeagueID) + var exists bool + err := row.Scan(&exists) + return exists, err +} + +const CreateRaffle = `-- name: CreateRaffle :one +INSERT INTO raffles (company_id, name, expires_at, type) +VALUES ($1, $2, $3, $4) +RETURNING id, company_id, name, created_at, expires_at, type, status +` + +type CreateRaffleParams struct { + CompanyID int32 `json:"company_id"` + Name string `json:"name"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + Type string `json:"type"` +} + +func (q *Queries) CreateRaffle(ctx context.Context, arg CreateRaffleParams) (Raffle, error) { + row := q.db.QueryRow(ctx, CreateRaffle, + arg.CompanyID, + arg.Name, + arg.ExpiresAt, + arg.Type, + ) + var i Raffle + err := row.Scan( + &i.ID, + &i.CompanyID, + &i.Name, + &i.CreatedAt, + &i.ExpiresAt, + &i.Type, + &i.Status, + ) + return i, err +} + +const CreateRaffleTicket = `-- name: CreateRaffleTicket :one +INSERT INTO raffle_tickets (raffle_id, user_id) +VALUES ($1, $2) +RETURNING id, raffle_id, user_id, is_active +` + +type CreateRaffleTicketParams struct { + RaffleID int32 `json:"raffle_id"` + UserID int32 `json:"user_id"` +} + +func (q *Queries) CreateRaffleTicket(ctx context.Context, arg CreateRaffleTicketParams) (RaffleTicket, error) { + row := q.db.QueryRow(ctx, CreateRaffleTicket, arg.RaffleID, arg.UserID) + var i RaffleTicket + err := row.Scan( + &i.ID, + &i.RaffleID, + &i.UserID, + &i.IsActive, + ) + return i, err +} + +const CreateRaffleWinner = `-- name: CreateRaffleWinner :one +INSERT INTO raffle_winners (raffle_id, user_id, rank) +VALUES ($1, $2, $3) +RETURNING id, raffle_id, user_id, rank, created_at +` + +type CreateRaffleWinnerParams struct { + RaffleID int32 `json:"raffle_id"` + UserID int32 `json:"user_id"` + Rank int32 `json:"rank"` +} + +func (q *Queries) CreateRaffleWinner(ctx context.Context, arg CreateRaffleWinnerParams) (RaffleWinner, error) { + row := q.db.QueryRow(ctx, CreateRaffleWinner, arg.RaffleID, arg.UserID, arg.Rank) + var i RaffleWinner + err := row.Scan( + &i.ID, + &i.RaffleID, + &i.UserID, + &i.Rank, + &i.CreatedAt, + ) + return i, err +} + +const DeleteRaffle = `-- name: DeleteRaffle :one +DELETE FROM raffles +WHERE id = $1 +RETURNING id, company_id, name, created_at, expires_at, type, status +` + +func (q *Queries) DeleteRaffle(ctx context.Context, id int32) (Raffle, error) { + row := q.db.QueryRow(ctx, DeleteRaffle, id) + var i Raffle + err := row.Scan( + &i.ID, + &i.CompanyID, + &i.Name, + &i.CreatedAt, + &i.ExpiresAt, + &i.Type, + &i.Status, + ) + return i, err +} + +const GetRaffleStanding = `-- name: GetRaffleStanding :many +SELECT + u.id AS user_id, + rt.raffle_id, + u.first_name, + u.last_name, + u.phone_number, + u.email, + COUNT(*) AS ticket_count +FROM raffle_tickets rt +JOIN users u ON rt.user_id = u.id +WHERE rt.is_active = true + AND rt.raffle_id = $1 +GROUP BY u.id, rt.raffle_id, u.first_name, u.last_name, u.phone_number, u.email +ORDER BY ticket_count DESC +LIMIT $2 +` + +type GetRaffleStandingParams struct { + RaffleID int32 `json:"raffle_id"` + Limit int32 `json:"limit"` +} + +type GetRaffleStandingRow struct { + UserID int64 `json:"user_id"` + RaffleID int32 `json:"raffle_id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + PhoneNumber pgtype.Text `json:"phone_number"` + Email pgtype.Text `json:"email"` + TicketCount int64 `json:"ticket_count"` +} + +func (q *Queries) GetRaffleStanding(ctx context.Context, arg GetRaffleStandingParams) ([]GetRaffleStandingRow, error) { + rows, err := q.db.Query(ctx, GetRaffleStanding, arg.RaffleID, arg.Limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetRaffleStandingRow + for rows.Next() { + var i GetRaffleStandingRow + if err := rows.Scan( + &i.UserID, + &i.RaffleID, + &i.FirstName, + &i.LastName, + &i.PhoneNumber, + &i.Email, + &i.TicketCount, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetRafflesOfCompany = `-- name: GetRafflesOfCompany :many +SELECT id, company_id, name, created_at, expires_at, type, status FROM raffles WHERE company_id = $1 +` + +func (q *Queries) GetRafflesOfCompany(ctx context.Context, companyID int32) ([]Raffle, error) { + rows, err := q.db.Query(ctx, GetRafflesOfCompany, companyID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Raffle + for rows.Next() { + var i Raffle + if err := rows.Scan( + &i.ID, + &i.CompanyID, + &i.Name, + &i.CreatedAt, + &i.ExpiresAt, + &i.Type, + &i.Status, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetUserRaffleTickets = `-- name: GetUserRaffleTickets :many +SELECT + rt.id AS ticket_id, + rt.user_id, + r.name, + r.type, + r.expires_at, + r.status +FROM raffle_tickets rt +JOIN raffles r ON rt.raffle_id = r.id +WHERE rt.user_id = $1 +` + +type GetUserRaffleTicketsRow struct { + TicketID int32 `json:"ticket_id"` + UserID int32 `json:"user_id"` + Name string `json:"name"` + Type string `json:"type"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + Status string `json:"status"` +} + +func (q *Queries) GetUserRaffleTickets(ctx context.Context, userID int32) ([]GetUserRaffleTicketsRow, error) { + rows, err := q.db.Query(ctx, GetUserRaffleTickets, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetUserRaffleTicketsRow + for rows.Next() { + var i GetUserRaffleTicketsRow + if err := rows.Scan( + &i.TicketID, + &i.UserID, + &i.Name, + &i.Type, + &i.ExpiresAt, + &i.Status, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const SetRaffleComplete = `-- name: SetRaffleComplete :exec +UPDATE raffles +SET status = 'completed' +WHERE id = $1 +` + +func (q *Queries) SetRaffleComplete(ctx context.Context, id int32) error { + _, err := q.db.Exec(ctx, SetRaffleComplete, id) + return err +} + +const UpdateRaffleTicketStatus = `-- name: UpdateRaffleTicketStatus :exec +UPDATE raffle_tickets +SET is_active = $1 +WHERE id = $2 +` + +type UpdateRaffleTicketStatusParams struct { + IsActive pgtype.Bool `json:"is_active"` + ID int32 `json:"id"` +} + +func (q *Queries) UpdateRaffleTicketStatus(ctx context.Context, arg UpdateRaffleTicketStatusParams) error { + _, err := q.db.Exec(ctx, UpdateRaffleTicketStatus, arg.IsActive, arg.ID) + return err +} diff --git a/gen/db/referal.sql.go b/gen/db/referal.sql.go index b5ceeed..99d8bb2 100644 --- a/gen/db/referal.sql.go +++ b/gen/db/referal.sql.go @@ -1,335 +1,254 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: referal.sql package dbgen import ( "context" - - "github.com/jackc/pgx/v5/pgtype" ) -const CreateReferral = `-- name: CreateReferral :one -INSERT INTO referrals ( - referral_code, - referrer_id, - status, - reward_amount, - expires_at -) VALUES ( - $1, $2, $3, $4, $5 -) RETURNING id, referral_code, referrer_id, referred_id, status, reward_amount, cashback_amount, created_at, updated_at, expires_at +const CreateReferralCode = `-- name: CreateReferralCode :one +INSERT INTO referral_codes ( + referral_code, + referrer_id, + company_id, + number_of_referrals, + reward_amount + ) +VALUES ($1, $2, $3, $4, $5) +RETURNING id, referral_code, referrer_id, company_id, is_active, number_of_referrals, reward_amount, created_at, updated_at ` -type CreateReferralParams struct { - ReferralCode string `json:"referral_code"` - ReferrerID string `json:"referrer_id"` - Status Referralstatus `json:"status"` - RewardAmount pgtype.Numeric `json:"reward_amount"` - ExpiresAt pgtype.Timestamptz `json:"expires_at"` +type CreateReferralCodeParams struct { + ReferralCode string `json:"referral_code"` + ReferrerID int64 `json:"referrer_id"` + CompanyID int64 `json:"company_id"` + NumberOfReferrals int64 `json:"number_of_referrals"` + RewardAmount int64 `json:"reward_amount"` } -func (q *Queries) CreateReferral(ctx context.Context, arg CreateReferralParams) (Referral, error) { - row := q.db.QueryRow(ctx, CreateReferral, +func (q *Queries) CreateReferralCode(ctx context.Context, arg CreateReferralCodeParams) (ReferralCode, error) { + row := q.db.QueryRow(ctx, CreateReferralCode, arg.ReferralCode, arg.ReferrerID, - arg.Status, + arg.CompanyID, + arg.NumberOfReferrals, arg.RewardAmount, - arg.ExpiresAt, ) - var i Referral + var i ReferralCode err := row.Scan( &i.ID, &i.ReferralCode, &i.ReferrerID, - &i.ReferredID, - &i.Status, + &i.CompanyID, + &i.IsActive, + &i.NumberOfReferrals, &i.RewardAmount, - &i.CashbackAmount, &i.CreatedAt, &i.UpdatedAt, - &i.ExpiresAt, ) return i, err } -const CreateReferralSettings = `-- name: CreateReferralSettings :one -INSERT INTO referral_settings ( - referral_reward_amount, - cashback_percentage, - max_referrals, - bet_referral_bonus_percentage, - expires_after_days, - updated_by -) VALUES ( - $1, $2, $3, $4, $5, $6 -) RETURNING id, referral_reward_amount, cashback_percentage, bet_referral_bonus_percentage, max_referrals, expires_after_days, updated_by, created_at, updated_at, version +const CreateUserReferral = `-- name: CreateUserReferral :one +INSERT INTO user_referrals (referred_id, referral_code_id) +VALUES ($1, $2) +RETURNING id, referred_id, referral_code_id, created_at ` -type CreateReferralSettingsParams struct { - ReferralRewardAmount pgtype.Numeric `json:"referral_reward_amount"` - CashbackPercentage pgtype.Numeric `json:"cashback_percentage"` - MaxReferrals int32 `json:"max_referrals"` - BetReferralBonusPercentage pgtype.Numeric `json:"bet_referral_bonus_percentage"` - ExpiresAfterDays int32 `json:"expires_after_days"` - UpdatedBy string `json:"updated_by"` +type CreateUserReferralParams struct { + ReferredID int64 `json:"referred_id"` + ReferralCodeID int64 `json:"referral_code_id"` } -func (q *Queries) CreateReferralSettings(ctx context.Context, arg CreateReferralSettingsParams) (ReferralSetting, error) { - row := q.db.QueryRow(ctx, CreateReferralSettings, - arg.ReferralRewardAmount, - arg.CashbackPercentage, - arg.MaxReferrals, - arg.BetReferralBonusPercentage, - arg.ExpiresAfterDays, - arg.UpdatedBy, - ) - var i ReferralSetting +func (q *Queries) CreateUserReferral(ctx context.Context, arg CreateUserReferralParams) (UserReferral, error) { + row := q.db.QueryRow(ctx, CreateUserReferral, arg.ReferredID, arg.ReferralCodeID) + var i UserReferral err := row.Scan( &i.ID, - &i.ReferralRewardAmount, - &i.CashbackPercentage, - &i.BetReferralBonusPercentage, - &i.MaxReferrals, - &i.ExpiresAfterDays, - &i.UpdatedBy, - &i.CreatedAt, - &i.UpdatedAt, - &i.Version, - ) - return i, err -} - -const GetActiveReferralByReferrerID = `-- name: GetActiveReferralByReferrerID :one -SELECT id, referral_code, referrer_id, referred_id, status, reward_amount, cashback_amount, created_at, updated_at, expires_at FROM referrals WHERE referrer_id = $1 AND status = 'PENDING' LIMIT 1 -` - -func (q *Queries) GetActiveReferralByReferrerID(ctx context.Context, referrerID string) (Referral, error) { - row := q.db.QueryRow(ctx, GetActiveReferralByReferrerID, referrerID) - var i Referral - err := row.Scan( - &i.ID, - &i.ReferralCode, - &i.ReferrerID, &i.ReferredID, - &i.Status, - &i.RewardAmount, - &i.CashbackAmount, + &i.ReferralCodeID, &i.CreatedAt, - &i.UpdatedAt, - &i.ExpiresAt, ) return i, err } -const GetReferralByCode = `-- name: GetReferralByCode :one -SELECT id, referral_code, referrer_id, referred_id, status, reward_amount, cashback_amount, created_at, updated_at, expires_at FROM referrals +const GetReferralCode = `-- name: GetReferralCode :one +SELECT id, referral_code, referrer_id, company_id, is_active, number_of_referrals, reward_amount, created_at, updated_at +FROM referral_codes WHERE referral_code = $1 ` -func (q *Queries) GetReferralByCode(ctx context.Context, referralCode string) (Referral, error) { - row := q.db.QueryRow(ctx, GetReferralByCode, referralCode) - var i Referral +func (q *Queries) GetReferralCode(ctx context.Context, referralCode string) (ReferralCode, error) { + row := q.db.QueryRow(ctx, GetReferralCode, referralCode) + var i ReferralCode err := row.Scan( &i.ID, &i.ReferralCode, &i.ReferrerID, - &i.ReferredID, - &i.Status, + &i.CompanyID, + &i.IsActive, + &i.NumberOfReferrals, &i.RewardAmount, - &i.CashbackAmount, &i.CreatedAt, &i.UpdatedAt, - &i.ExpiresAt, ) return i, err } -const GetReferralByReferredID = `-- name: GetReferralByReferredID :one -SELECT id, referral_code, referrer_id, referred_id, status, reward_amount, cashback_amount, created_at, updated_at, expires_at FROM referrals WHERE referred_id = $1 LIMIT 1 +const GetReferralCodeByUser = `-- name: GetReferralCodeByUser :many +SELECt id, referral_code, referrer_id, company_id, is_active, number_of_referrals, reward_amount, created_at, updated_at +FROM referral_codes +WHERE referrer_id = $1 ` -func (q *Queries) GetReferralByReferredID(ctx context.Context, referredID pgtype.Text) (Referral, error) { - row := q.db.QueryRow(ctx, GetReferralByReferredID, referredID) - var i Referral +func (q *Queries) GetReferralCodeByUser(ctx context.Context, referrerID int64) ([]ReferralCode, error) { + rows, err := q.db.Query(ctx, GetReferralCodeByUser, referrerID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ReferralCode + for rows.Next() { + var i ReferralCode + if err := rows.Scan( + &i.ID, + &i.ReferralCode, + &i.ReferrerID, + &i.CompanyID, + &i.IsActive, + &i.NumberOfReferrals, + &i.RewardAmount, + &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 GetReferralStats = `-- name: GetReferralStats :one +SELECT COUNT(*) AS total_referrals, + COALESCE(SUM(reward_amount), 0)::bigint AS total_reward_earned +FROM user_referrals + JOIN referral_codes ON referral_codes.id = referral_code_id +WHERE referrer_id = $1 + AND company_id = $2 +` + +type GetReferralStatsParams struct { + ReferrerID int64 `json:"referrer_id"` + CompanyID int64 `json:"company_id"` +} + +type GetReferralStatsRow struct { + TotalReferrals int64 `json:"total_referrals"` + TotalRewardEarned int64 `json:"total_reward_earned"` +} + +func (q *Queries) GetReferralStats(ctx context.Context, arg GetReferralStatsParams) (GetReferralStatsRow, error) { + row := q.db.QueryRow(ctx, GetReferralStats, arg.ReferrerID, arg.CompanyID) + var i GetReferralStatsRow + err := row.Scan(&i.TotalReferrals, &i.TotalRewardEarned) + return i, err +} + +const GetUserReferral = `-- name: GetUserReferral :one +SELECT id, referred_id, referral_code_id, created_at +FROM user_referrals +WHERE referred_id = $1 +` + +func (q *Queries) GetUserReferral(ctx context.Context, referredID int64) (UserReferral, error) { + row := q.db.QueryRow(ctx, GetUserReferral, referredID) + var i UserReferral err := row.Scan( &i.ID, - &i.ReferralCode, - &i.ReferrerID, &i.ReferredID, - &i.Status, - &i.RewardAmount, - &i.CashbackAmount, + &i.ReferralCodeID, &i.CreatedAt, - &i.UpdatedAt, - &i.ExpiresAt, ) return i, err } -const GetReferralCountByID = `-- name: GetReferralCountByID :one -SELECT count(*) FROM referrals WHERE referrer_id = $1 +const GetUserReferralsByCode = `-- name: GetUserReferralsByCode :many +SELECT user_referrals.id, user_referrals.referred_id, user_referrals.referral_code_id, user_referrals.created_at +FROM user_referrals + JOIN referral_codes ON referral_codes.id = referral_code_id +WHERE referral_code = $1 ` -func (q *Queries) GetReferralCountByID(ctx context.Context, referrerID string) (int64, error) { - row := q.db.QueryRow(ctx, GetReferralCountByID, referrerID) +func (q *Queries) GetUserReferralsByCode(ctx context.Context, referralCode string) ([]UserReferral, error) { + rows, err := q.db.Query(ctx, GetUserReferralsByCode, referralCode) + if err != nil { + return nil, err + } + defer rows.Close() + var items []UserReferral + for rows.Next() { + var i UserReferral + if err := rows.Scan( + &i.ID, + &i.ReferredID, + &i.ReferralCodeID, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetUserReferralsCount = `-- name: GetUserReferralsCount :one +SELECT COUNT(*) +FROM user_referrals + JOIN referral_codes ON referral_codes.id = referral_code_id +WHERE referrer_id = $1 +` + +func (q *Queries) GetUserReferralsCount(ctx context.Context, referrerID int64) (int64, error) { + row := q.db.QueryRow(ctx, GetUserReferralsCount, referrerID) var count int64 err := row.Scan(&count) return count, err } -const GetReferralSettings = `-- name: GetReferralSettings :one -SELECT id, referral_reward_amount, cashback_percentage, bet_referral_bonus_percentage, max_referrals, expires_after_days, updated_by, created_at, updated_at, version FROM referral_settings -LIMIT 1 -` - -func (q *Queries) GetReferralSettings(ctx context.Context) (ReferralSetting, error) { - row := q.db.QueryRow(ctx, GetReferralSettings) - var i ReferralSetting - err := row.Scan( - &i.ID, - &i.ReferralRewardAmount, - &i.CashbackPercentage, - &i.BetReferralBonusPercentage, - &i.MaxReferrals, - &i.ExpiresAfterDays, - &i.UpdatedBy, - &i.CreatedAt, - &i.UpdatedAt, - &i.Version, - ) - return i, err -} - -const GetReferralStats = `-- name: GetReferralStats :one -SELECT - COUNT(*) as total_referrals, - COUNT(CASE WHEN status = 'COMPLETED' THEN 1 END) as completed_referrals, - COALESCE(SUM(reward_amount), 0) as total_reward_earned, - COALESCE(SUM(CASE WHEN status = 'PENDING' THEN reward_amount END), 0) as pending_rewards -FROM referrals -WHERE referrer_id = $1 -` - -type GetReferralStatsRow struct { - TotalReferrals int64 `json:"total_referrals"` - CompletedReferrals int64 `json:"completed_referrals"` - TotalRewardEarned interface{} `json:"total_reward_earned"` - PendingRewards interface{} `json:"pending_rewards"` -} - -func (q *Queries) GetReferralStats(ctx context.Context, referrerID string) (GetReferralStatsRow, error) { - row := q.db.QueryRow(ctx, GetReferralStats, referrerID) - var i GetReferralStatsRow - err := row.Scan( - &i.TotalReferrals, - &i.CompletedReferrals, - &i.TotalRewardEarned, - &i.PendingRewards, - ) - return i, err -} - -const UpdateReferral = `-- name: UpdateReferral :one -UPDATE referrals -SET - referred_id = $2, - status = $3, - updated_at = CURRENT_TIMESTAMP -WHERE id = $1 -RETURNING id, referral_code, referrer_id, referred_id, status, reward_amount, cashback_amount, created_at, updated_at, expires_at -` - -type UpdateReferralParams struct { - ID int64 `json:"id"` - ReferredID pgtype.Text `json:"referred_id"` - Status Referralstatus `json:"status"` -} - -func (q *Queries) UpdateReferral(ctx context.Context, arg UpdateReferralParams) (Referral, error) { - row := q.db.QueryRow(ctx, UpdateReferral, arg.ID, arg.ReferredID, arg.Status) - var i Referral - err := row.Scan( - &i.ID, - &i.ReferralCode, - &i.ReferrerID, - &i.ReferredID, - &i.Status, - &i.RewardAmount, - &i.CashbackAmount, - &i.CreatedAt, - &i.UpdatedAt, - &i.ExpiresAt, - ) - return i, err -} - const UpdateReferralCode = `-- name: UpdateReferralCode :exec -UPDATE users -SET - referral_code = $2, - updated_at = CURRENT_TIMESTAMP +UPDATE referral_codes +SET is_active = $2, + referral_code = $3, + number_of_referrals = $4, + reward_amount = $5, + updated_at = CURRENT_TIMESTAMP WHERE id = $1 ` type UpdateReferralCodeParams struct { - ID int64 `json:"id"` - ReferralCode pgtype.Text `json:"referral_code"` + ID int64 `json:"id"` + IsActive bool `json:"is_active"` + ReferralCode string `json:"referral_code"` + NumberOfReferrals int64 `json:"number_of_referrals"` + RewardAmount int64 `json:"reward_amount"` } func (q *Queries) UpdateReferralCode(ctx context.Context, arg UpdateReferralCodeParams) error { - _, err := q.db.Exec(ctx, UpdateReferralCode, arg.ID, arg.ReferralCode) + _, err := q.db.Exec(ctx, UpdateReferralCode, + arg.ID, + arg.IsActive, + arg.ReferralCode, + arg.NumberOfReferrals, + arg.RewardAmount, + ) return err } - -const UpdateReferralSettings = `-- name: UpdateReferralSettings :one -UPDATE referral_settings -SET - referral_reward_amount = $2, - cashback_percentage = $3, - bet_referral_bonus_percentage= $4, - max_referrals = $5, - expires_after_days = $6, - updated_by = $7, - updated_at = CURRENT_TIMESTAMP -WHERE id = $1 -RETURNING id, referral_reward_amount, cashback_percentage, bet_referral_bonus_percentage, max_referrals, expires_after_days, updated_by, created_at, updated_at, version -` - -type UpdateReferralSettingsParams struct { - ID int64 `json:"id"` - ReferralRewardAmount pgtype.Numeric `json:"referral_reward_amount"` - CashbackPercentage pgtype.Numeric `json:"cashback_percentage"` - BetReferralBonusPercentage pgtype.Numeric `json:"bet_referral_bonus_percentage"` - MaxReferrals int32 `json:"max_referrals"` - ExpiresAfterDays int32 `json:"expires_after_days"` - UpdatedBy string `json:"updated_by"` -} - -func (q *Queries) UpdateReferralSettings(ctx context.Context, arg UpdateReferralSettingsParams) (ReferralSetting, error) { - row := q.db.QueryRow(ctx, UpdateReferralSettings, - arg.ID, - arg.ReferralRewardAmount, - arg.CashbackPercentage, - arg.BetReferralBonusPercentage, - arg.MaxReferrals, - arg.ExpiresAfterDays, - arg.UpdatedBy, - ) - var i ReferralSetting - err := row.Scan( - &i.ID, - &i.ReferralRewardAmount, - &i.CashbackPercentage, - &i.BetReferralBonusPercentage, - &i.MaxReferrals, - &i.ExpiresAfterDays, - &i.UpdatedBy, - &i.CreatedAt, - &i.UpdatedAt, - &i.Version, - ) - return i, err -} diff --git a/gen/db/report.sql.go b/gen/db/report.sql.go index 1a1ccde..d6193c1 100644 --- a/gen/db/report.sql.go +++ b/gen/db/report.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: report.sql package dbgen diff --git a/gen/db/result.sql.go b/gen/db/result.sql.go index bff7b1e..899561b 100644 --- a/gen/db/result.sql.go +++ b/gen/db/result.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: result.sql package dbgen diff --git a/gen/db/result_log.sql.go b/gen/db/result_log.sql.go index 468795e..3f11e16 100644 --- a/gen/db/result_log.sql.go +++ b/gen/db/result_log.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: result_log.sql package dbgen diff --git a/gen/db/settings.sql.go b/gen/db/settings.sql.go index f67fecc..76eb504 100644 --- a/gen/db/settings.sql.go +++ b/gen/db/settings.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: settings.sql package dbgen @@ -181,7 +181,9 @@ func (q *Queries) GetGlobalSettings(ctx context.Context) ([]GlobalSetting, error } const GetOverrideSettings = `-- name: GetOverrideSettings :many -SELECT gs.key, gs.value, gs.created_at, gs.updated_at, +SELECT gs.key, + gs.created_at, + gs.updated_at, COALESCE(cs.value, gs.value) AS value FROM global_settings gs LEFT JOIN company_settings cs ON cs.key = gs.key @@ -190,10 +192,9 @@ FROM global_settings gs type GetOverrideSettingsRow struct { Key string `json:"key"` - Value string `json:"value"` CreatedAt pgtype.Timestamp `json:"created_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"` - Value_2 string `json:"value_2"` + Value string `json:"value"` } func (q *Queries) GetOverrideSettings(ctx context.Context, companyID int64) ([]GetOverrideSettingsRow, error) { @@ -207,10 +208,9 @@ func (q *Queries) GetOverrideSettings(ctx context.Context, companyID int64) ([]G var i GetOverrideSettingsRow if err := rows.Scan( &i.Key, - &i.Value, &i.CreatedAt, &i.UpdatedAt, - &i.Value_2, + &i.Value, ); err != nil { return nil, err } diff --git a/gen/db/shop_transactions.sql.go b/gen/db/shop_transactions.sql.go index bcd884e..7664dbb 100644 --- a/gen/db/shop_transactions.sql.go +++ b/gen/db/shop_transactions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: shop_transactions.sql package dbgen diff --git a/gen/db/ticket.sql.go b/gen/db/ticket.sql.go index bc9bb5f..45603ba 100644 --- a/gen/db/ticket.sql.go +++ b/gen/db/ticket.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: ticket.sql package dbgen diff --git a/gen/db/transfer.sql.go b/gen/db/transfer.sql.go index 35e38d4..fe25cbe 100644 --- a/gen/db/transfer.sql.go +++ b/gen/db/transfer.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: transfer.sql package dbgen @@ -182,6 +182,40 @@ func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber st return i, err } +const GetTransferStats = `-- name: GetTransferStats :one +SELECT COUNT(*) AS total_transfers, COUNT(*) FILTER ( + WHERE type = 'deposit' + ) AS total_deposits, + COUNT(*) FILTER ( + WHERE type = 'withdraw' + ) AS total_withdraw, + COUNT(*) FILTER ( + WHERE type = 'wallet' + ) AS total_wallet_to_wallet +FROM wallet_transfer +WHERE sender_wallet_id = $1 + OR receiver_wallet_id = $1 +` + +type GetTransferStatsRow struct { + TotalTransfers int64 `json:"total_transfers"` + TotalDeposits int64 `json:"total_deposits"` + TotalWithdraw int64 `json:"total_withdraw"` + TotalWalletToWallet int64 `json:"total_wallet_to_wallet"` +} + +func (q *Queries) GetTransferStats(ctx context.Context, senderWalletID pgtype.Int8) (GetTransferStatsRow, error) { + row := q.db.QueryRow(ctx, GetTransferStats, senderWalletID) + var i GetTransferStatsRow + err := row.Scan( + &i.TotalTransfers, + &i.TotalDeposits, + &i.TotalWithdraw, + &i.TotalWalletToWallet, + ) + return i, err +} + const GetTransfersByWallet = `-- name: GetTransfersByWallet :many SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number FROM wallet_transfer_details diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index 0d4c33b..9f9cd95 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: user.sql package dbgen @@ -163,7 +163,7 @@ func (q *Queries) DeleteUser(ctx context.Context, id int64) error { } const GetAdminByCompanyID = `-- name: GetAdminByCompanyID :one -SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended, users.referral_code, users.referred_by +SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended FROM companies JOIN users ON companies.admin_id = users.id where companies.id = $1 @@ -187,8 +187,6 @@ func (q *Queries) GetAdminByCompanyID(ctx context.Context, id int64) (User, erro &i.CompanyID, &i.SuspendedAt, &i.Suspended, - &i.ReferralCode, - &i.ReferredBy, ) return i, err } @@ -388,7 +386,7 @@ func (q *Queries) GetUserByEmail(ctx context.Context, arg GetUserByEmailParams) } const GetUserByID = `-- name: GetUserByID :one -SELECT id, first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at, company_id, suspended_at, suspended, referral_code, referred_by +SELECT id, first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at, company_id, suspended_at, suspended FROM users WHERE id = $1 ` @@ -411,8 +409,6 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) { &i.CompanyID, &i.SuspendedAt, &i.Suspended, - &i.ReferralCode, - &i.ReferredBy, ) return i, err } @@ -587,7 +583,7 @@ SET password = $1, WHERE ( email = $2 OR phone_number = $3 - AND company_id = $4 + AND company_id = $5 ) ` @@ -596,6 +592,7 @@ type UpdatePasswordParams struct { Email pgtype.Text `json:"email"` PhoneNumber pgtype.Text `json:"phone_number"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` + CompanyID pgtype.Int8 `json:"company_id"` } func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error { @@ -604,6 +601,7 @@ func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) arg.Email, arg.PhoneNumber, arg.UpdatedAt, + arg.CompanyID, ) return err } diff --git a/gen/db/virtual_games.sql.go b/gen/db/virtual_games.sql.go index 33697d7..b98f602 100644 --- a/gen/db/virtual_games.sql.go +++ b/gen/db/virtual_games.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: virtual_games.sql package dbgen @@ -12,12 +12,8 @@ import ( ) 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 +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 { @@ -44,23 +40,34 @@ func (q *Queries) CountVirtualGameProviders(ctx context.Context) (int64, error) const CreateVirtualGame = `-- name: CreateVirtualGame :one INSERT INTO virtual_games ( - game_id, - provider_id, - name, - category, - device_type, - volatility, - rtp, - has_demo, - has_free_bets, - bets, - thumbnail, - status -) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 -) -RETURNING - id, + game_id, + provider_id, + name, + category, + device_type, + volatility, + rtp, + has_demo, + has_free_bets, + bets, + thumbnail, + status + ) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12 + ) +RETURNING id, game_id, provider_id, name, @@ -130,22 +137,34 @@ func (q *Queries) CreateVirtualGame(ctx context.Context, arg CreateVirtualGamePa 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 + ) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12 + ) +RETURNING id, session_id, user_id, company_id, @@ -215,10 +234,21 @@ func (q *Queries) CreateVirtualGameHistory(ctx context.Context, arg CreateVirtua const CreateVirtualGameProvider = `-- name: CreateVirtualGameProvider :one INSERT INTO virtual_game_providers ( - provider_id, provider_name, logo_dark, logo_light, enabled -) VALUES ( - $1, $2, $3, $4, $5 -) RETURNING id, provider_id, provider_name, logo_dark, logo_light, enabled, created_at, updated_at + provider_id, + provider_name, + logo_dark, + logo_light, + enabled + ) +VALUES ($1, $2, $3, $4, $5) +RETURNING id, + provider_id, + provider_name, + logo_dark, + logo_light, + enabled, + created_at, + updated_at ` type CreateVirtualGameProviderParams struct { @@ -253,10 +283,23 @@ func (q *Queries) CreateVirtualGameProvider(ctx context.Context, arg CreateVirtu const CreateVirtualGameSession = `-- name: CreateVirtualGameSession :one INSERT INTO virtual_game_sessions ( - user_id, game_id, session_token, currency, status, expires_at -) VALUES ( - $1, $2, $3, $4, $5, $6 -) RETURNING id, user_id, game_id, session_token, currency, status, created_at, updated_at, expires_at + user_id, + game_id, + session_token, + currency, + status, + expires_at + ) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING id, + user_id, + game_id, + session_token, + currency, + status, + created_at, + updated_at, + expires_at ` type CreateVirtualGameSessionParams struct { @@ -294,10 +337,31 @@ func (q *Queries) CreateVirtualGameSession(ctx context.Context, arg CreateVirtua const CreateVirtualGameTransaction = `-- name: CreateVirtualGameTransaction :one INSERT INTO virtual_game_transactions ( - 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, $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 + 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, $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 { @@ -390,8 +454,7 @@ func (q *Queries) DeleteVirtualGameProvider(ctx context.Context, providerID stri } const GetAllVirtualGames = `-- name: GetAllVirtualGames :many -SELECT - vg.id, +SELECT vg.id, vg.game_id, vg.provider_id, vp.provider_name, @@ -408,19 +471,29 @@ SELECT vg.created_at, vg.updated_at FROM virtual_games vg -JOIN virtual_game_providers vp ON vg.provider_id = vp.provider_id -WHERE - ($1::text IS NULL OR vg.category = $1) -- category filter (optional) - AND ($2::text IS NULL OR vg.name ILIKE '%' || $2 || '%') -- search by name (optional) + JOIN virtual_game_providers vp ON vg.provider_id = vp.provider_id +WHERE ( + vg.category = $1 + OR $1 IS NULL + ) + AND ( + name ILIKE '%' || $2 || '%' + OR $2 IS NULL + ) + AND ( + vg.provider_id = $3 + OR $3 IS NULL + ) ORDER BY vg.created_at DESC -LIMIT $3 OFFSET $4 +LIMIT $5 OFFSET $4 ` type GetAllVirtualGamesParams struct { - Column1 string `json:"column_1"` - Column2 string `json:"column_2"` - Limit int32 `json:"limit"` - Offset int32 `json:"offset"` + Category pgtype.Text `json:"category"` + Name pgtype.Text `json:"name"` + ProviderID pgtype.Text `json:"provider_id"` + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` } type GetAllVirtualGamesRow struct { @@ -444,10 +517,11 @@ type GetAllVirtualGamesRow struct { func (q *Queries) GetAllVirtualGames(ctx context.Context, arg GetAllVirtualGamesParams) ([]GetAllVirtualGamesRow, error) { rows, err := q.db.Query(ctx, GetAllVirtualGames, - arg.Column1, - arg.Column2, - arg.Limit, + arg.Category, + arg.Name, + arg.ProviderID, arg.Offset, + arg.Limit, ) if err != nil { return nil, err @@ -485,7 +559,14 @@ func (q *Queries) GetAllVirtualGames(ctx context.Context, arg GetAllVirtualGames } const GetVirtualGameProviderByID = `-- name: GetVirtualGameProviderByID :one -SELECT id, provider_id, provider_name, logo_dark, logo_light, enabled, created_at, updated_at +SELECT id, + provider_id, + provider_name, + logo_dark, + logo_light, + enabled, + created_at, + updated_at FROM virtual_game_providers WHERE provider_id = $1 ` @@ -507,7 +588,15 @@ func (q *Queries) GetVirtualGameProviderByID(ctx context.Context, providerID str } const GetVirtualGameSessionByToken = `-- name: GetVirtualGameSessionByToken :one -SELECT id, user_id, game_id, session_token, currency, status, created_at, updated_at, expires_at +SELECT id, + user_id, + game_id, + session_token, + currency, + status, + created_at, + updated_at, + expires_at FROM virtual_game_sessions WHERE session_token = $1 ` @@ -530,18 +619,18 @@ func (q *Queries) GetVirtualGameSessionByToken(ctx context.Context, sessionToken } const GetVirtualGameSummaryInRange = `-- name: GetVirtualGameSummaryInRange :many -SELECT - c.name AS company_name, +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 + 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 + AND vgt.created_at BETWEEN $1 AND $2 +GROUP BY c.name, + vg.name ` type GetVirtualGameSummaryInRangeParams struct { @@ -582,7 +671,17 @@ func (q *Queries) GetVirtualGameSummaryInRange(ctx context.Context, arg GetVirtu } 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 +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 ` @@ -647,7 +746,14 @@ func (q *Queries) ListFavoriteGames(ctx context.Context, userID int64) ([]int64, } const ListVirtualGameProviders = `-- name: ListVirtualGameProviders :many -SELECT id, provider_id, provider_name, logo_dark, logo_light, enabled, created_at, updated_at +SELECT id, + provider_id, + provider_name, + logo_dark, + logo_light, + enabled, + created_at, + updated_at FROM virtual_game_providers ORDER BY created_at DESC LIMIT $1 OFFSET $2 @@ -689,7 +795,8 @@ func (q *Queries) ListVirtualGameProviders(ctx context.Context, arg ListVirtualG const RemoveFavoriteGame = `-- name: RemoveFavoriteGame :exec DELETE FROM favorite_games -WHERE user_id = $1 AND game_id = $2 +WHERE user_id = $1 + AND game_id = $2 ` type RemoveFavoriteGameParams struct { @@ -707,7 +814,14 @@ UPDATE virtual_game_providers SET enabled = $2, updated_at = CURRENT_TIMESTAMP WHERE provider_id = $1 -RETURNING id, provider_id, provider_name, logo_dark, logo_light, enabled, created_at, updated_at +RETURNING id, + provider_id, + provider_name, + logo_dark, + logo_light, + enabled, + created_at, + updated_at ` type UpdateVirtualGameProviderEnabledParams struct { @@ -733,7 +847,8 @@ func (q *Queries) UpdateVirtualGameProviderEnabled(ctx context.Context, arg Upda const UpdateVirtualGameSessionStatus = `-- name: UpdateVirtualGameSessionStatus :exec UPDATE virtual_game_sessions -SET status = $2, updated_at = CURRENT_TIMESTAMP +SET status = $2, + updated_at = CURRENT_TIMESTAMP WHERE id = $1 ` @@ -749,7 +864,8 @@ func (q *Queries) UpdateVirtualGameSessionStatus(ctx context.Context, arg Update const UpdateVirtualGameTransactionStatus = `-- name: UpdateVirtualGameTransactionStatus :exec UPDATE virtual_game_transactions -SET status = $2, updated_at = CURRENT_TIMESTAMP +SET status = $2, + updated_at = CURRENT_TIMESTAMP WHERE id = $1 ` diff --git a/gen/db/wallet.sql.go b/gen/db/wallet.sql.go index 7e3eb7f..ccb2d37 100644 --- a/gen/db/wallet.sql.go +++ b/gen/db/wallet.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: wallet.sql package dbgen @@ -50,7 +50,7 @@ INSERT INTO wallets ( type ) VALUES ($1, $2, $3, $4, $5) -RETURNING id, balance, currency, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at, bonus_balance, cash_balance +RETURNING id, balance, currency, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at ` type CreateWalletParams struct { @@ -82,8 +82,6 @@ func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (Wal &i.IsActive, &i.CreatedAt, &i.UpdatedAt, - &i.BonusBalance, - &i.CashBalance, ) return i, err } @@ -188,7 +186,7 @@ func (q *Queries) GetAllCustomerWallet(ctx context.Context) ([]CustomerWalletDet } const GetAllWallets = `-- name: GetAllWallets :many -SELECT id, balance, currency, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at, bonus_balance, cash_balance +SELECT id, balance, currency, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at FROM wallets ` @@ -213,8 +211,6 @@ func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) { &i.IsActive, &i.CreatedAt, &i.UpdatedAt, - &i.BonusBalance, - &i.CashBalance, ); err != nil { return nil, err } @@ -319,7 +315,7 @@ func (q *Queries) GetCustomerWallet(ctx context.Context, customerID int64) (Cust } const GetWalletByID = `-- name: GetWalletByID :one -SELECT id, balance, currency, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at, bonus_balance, cash_balance +SELECT id, balance, currency, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at FROM wallets WHERE id = $1 ` @@ -339,14 +335,12 @@ func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) { &i.IsActive, &i.CreatedAt, &i.UpdatedAt, - &i.BonusBalance, - &i.CashBalance, ) return i, err } const GetWalletByUserID = `-- name: GetWalletByUserID :many -SELECT id, balance, currency, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at, bonus_balance, cash_balance +SELECT id, balance, currency, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at FROM wallets WHERE user_id = $1 ` @@ -372,8 +366,6 @@ func (q *Queries) GetWalletByUserID(ctx context.Context, userID int64) ([]Wallet &i.IsActive, &i.CreatedAt, &i.UpdatedAt, - &i.BonusBalance, - &i.CashBalance, ); err != nil { return nil, err } diff --git a/internal/domain/auth.go b/internal/domain/auth.go index 513ff8e..374fe91 100644 --- a/internal/domain/auth.go +++ b/internal/domain/auth.go @@ -10,3 +10,10 @@ type RefreshToken struct { CreatedAt time.Time Revoked bool } + +// I used this because i was getting an error with the ValidInt64 +// when it was being unmarshaled by the jwt +type NullJwtInt64 struct { + Value int64 + Valid bool +} diff --git a/internal/domain/bet.go b/internal/domain/bet.go index 6ee3734..d59b68f 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -235,6 +235,7 @@ func ConvertDBBetWithOutcomes(bet dbgen.BetWithOutcome) GetBet { return GetBet{ ID: bet.ID, + CompanyID: bet.CompanyID, Amount: Currency(bet.Amount), TotalOdds: bet.TotalOdds, Status: OutcomeStatus(bet.Status), diff --git a/internal/domain/bonus.go b/internal/domain/bonus.go new file mode 100644 index 0000000..f436381 --- /dev/null +++ b/internal/domain/bonus.go @@ -0,0 +1,172 @@ +package domain + +import ( + "time" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/jackc/pgx/v5/pgtype" +) + +type BonusType string + +var ( + WelcomeBonus BonusType = "welcome_bonus" + DepositBonus BonusType = "deposit_bonus" +) + +type UserBonus struct { + ID int64 + Name string + Description string + UserID int64 + Type BonusType + RewardAmount Currency + IsClaimed bool + ExpiresAt time.Time + CreatedAt time.Time + UpdatedAt time.Time +} + +type UserBonusRes struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + UserID int64 `json:"user_id"` + Type BonusType `json:"type"` + RewardAmount float32 `json:"reward_amount"` + IsClaimed bool `json:"is_claimed"` + ExpiresAt time.Time `json:"expires_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func ConvertToBonusRes(bonus UserBonus) UserBonusRes { + return UserBonusRes{ + ID: bonus.ID, + Name: bonus.Name, + Description: bonus.Description, + Type: bonus.Type, + UserID: bonus.UserID, + RewardAmount: bonus.RewardAmount.Float32(), + IsClaimed: bonus.IsClaimed, + ExpiresAt: bonus.ExpiresAt, + CreatedAt: bonus.CreatedAt, + UpdatedAt: bonus.UpdatedAt, + } +} + +func ConvertToBonusResList(bonuses []UserBonus) []UserBonusRes { + result := make([]UserBonusRes, len(bonuses)) + + for i, bonus := range bonuses { + result[i] = ConvertToBonusRes(bonus) + } + + return result +} + +type CreateBonus struct { + Name string + Description string + Type BonusType + UserID int64 + RewardAmount Currency + ExpiresAt time.Time +} + +// type CreateBonusReq struct { +// Name string `json:"name"` +// Description string `json:"description"` +// Type BonusType `json:"type"` +// UserID int64 `json:"user_id"` +// RewardAmount float32 `json:"reward_amount"` +// ExpiresAt time.Time `json:"expires_at"` +// } + +// func ConvertCreateBonusReq(bonus CreateBonusReq, companyID int64) CreateBonus { +// return CreateBonus{ +// Name: bonus.Name, +// Description: bonus.Description, +// Type: bonus.Type, +// UserID: bonus.UserID, +// RewardAmount: ToCurrency(bonus.RewardAmount), +// ExpiresAt: bonus.ExpiresAt, +// } +// } + +func ConvertCreateBonus(bonus CreateBonus) dbgen.CreateUserBonusParams { + return dbgen.CreateUserBonusParams{ + Name: bonus.Name, + Description: bonus.Description, + Type: string(bonus.Type), + UserID: bonus.UserID, + RewardAmount: int64(bonus.RewardAmount), + ExpiresAt: pgtype.Timestamp{ + Time: bonus.ExpiresAt, + Valid: true, + }, + } +} + +func ConvertDBBonus(bonus dbgen.UserBonuse) UserBonus { + return UserBonus{ + ID: bonus.ID, + Name: bonus.Name, + Description: bonus.Description, + Type: BonusType(bonus.Type), + UserID: bonus.UserID, + + RewardAmount: Currency(bonus.RewardAmount), + IsClaimed: bonus.IsClaimed, + ExpiresAt: bonus.ExpiresAt.Time, + CreatedAt: bonus.CreatedAt.Time, + UpdatedAt: bonus.UpdatedAt.Time, + } +} + +func ConvertDBBonuses(bonuses []dbgen.UserBonuse) []UserBonus { + result := make([]UserBonus, len(bonuses)) + for i, bonus := range bonuses { + result[i] = ConvertDBBonus(bonus) + } + return result +} + +type BonusFilter struct { + UserID ValidInt64 + CompanyID ValidInt64 + Limit ValidInt + Offset ValidInt +} + +type BonusStats struct { + TotalBonus int64 + TotalRewardAmount Currency + ClaimedBonuses int64 + ExpiredBonuses int64 +} + +type BonusStatsRes struct { + TotalBonus int64 `json:"total_bonus"` + TotalRewardAmount float32 `json:"total_reward_amount"` + ClaimedBonuses int64 `json:"claimed_bonuses"` + ExpiredBonuses int64 `json:"expired_bonuses"` +} + +func ConvertToBonusStatsRes(bonus BonusStats) BonusStatsRes { + return BonusStatsRes{ + TotalBonus: bonus.TotalBonus, + TotalRewardAmount: bonus.TotalRewardAmount.Float32(), + ClaimedBonuses: bonus.ClaimedBonuses, + ExpiredBonuses: bonus.ExpiredBonuses, + } +} + +func ConvertDBBonusStats(stats dbgen.GetBonusStatsRow) BonusStats { + return BonusStats{ + TotalBonus: stats.TotalBonuses, + TotalRewardAmount: Currency(stats.TotalRewardEarned), + ClaimedBonuses: stats.ClaimedBonuses, + ExpiredBonuses: stats.ExpiredBonuses, + } +} diff --git a/internal/domain/event.go b/internal/domain/event.go index 0906918..152f1de 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -69,7 +69,7 @@ type BaseEvent struct { IsMonitored bool DefaultIsFeatured bool DefaultIsActive bool - DefaultWinningUpperLimit int32 + DefaultWinningUpperLimit int64 Score ValidString MatchMinute ValidInt TimerStatus ValidString @@ -97,92 +97,99 @@ type BaseEventRes struct { IsMonitored bool `json:"is_monitored"` DefaultIsFeatured bool `json:"default_is_featured"` DefaultIsActive bool `json:"default_is_active"` - DefaultWinningUpperLimit int32 `json:"default_winning_upper_limit"` + DefaultWinningUpperLimit int64 `json:"default_winning_upper_limit"` + Score string `json:"score"` + MatchMinute int `json:"match_minute"` + TimerStatus string `json:"timer_status"` + AddedTime int `json:"added_time"` + MatchPeriod int `json:"match_period"` + IsLive bool `json:"is_live"` + FetchedAt time.Time `json:"fetched_at"` +} +type EventWithSettings struct { + ID string + SportID int32 + MatchName string + HomeTeam string + AwayTeam string + HomeTeamID int64 + AwayTeamID int64 + HomeTeamImage string + AwayTeamImage string + LeagueID int64 + LeagueName string + LeagueCC ValidString + StartTime time.Time + Source EventSource + Status EventStatus + IsMonitored bool + IsFeatured bool + IsActive bool + WinningUpperLimit int32 + DefaultIsFeatured bool + DefaultIsActive bool + DefaultWinningUpperLimit int64 + Score ValidString + MatchMinute ValidInt + TimerStatus ValidString + AddedTime ValidInt + MatchPeriod ValidInt + IsLive bool + UpdatedAt time.Time + FetchedAt time.Time +} + +type CreateEvent struct { + ID string + SportID int32 + MatchName string + HomeTeam string + AwayTeam string + HomeTeamID int64 + AwayTeamID int64 + HomeTeamImage string + AwayTeamImage string + LeagueID int64 + LeagueName string + StartTime time.Time + IsLive bool + Status EventStatus + Source EventSource + DefaultWinningUpperLimit int64 +} + +type EventWithSettingsRes struct { + ID string `json:"id"` + SportID int32 `json:"sport_id"` + MatchName string `json:"match_name"` + HomeTeam string `json:"home_team"` + AwayTeam string `json:"away_team"` + HomeTeamID int64 `json:"home_team_id"` + AwayTeamID int64 `json:"away_team_id"` + HomeTeamImage string `json:"home_team_image"` + AwayTeamImage string `json:"away_team_image"` + LeagueID int64 `json:"league_id"` + LeagueName string `json:"league_name"` + LeagueCC string `json:"league_cc"` + StartTime time.Time `json:"start_time"` + Source EventSource `json:"source"` + Status EventStatus `json:"status"` + IsMonitored bool `json:"is_monitored"` + IsFeatured bool `json:"is_featured"` + IsActive bool `json:"is_active"` + WinningUpperLimit int32 `json:"winning_upper_limit"` + DefaultIsFeatured bool `json:"default_is_featured"` + DefaultIsActive bool `json:"default_is_active"` + DefaultWinningUpperLimit int64 `json:"default_winning_upper_limit"` Score string `json:"score,omitempty"` MatchMinute int `json:"match_minute,omitempty"` TimerStatus string `json:"timer_status,omitempty"` AddedTime int `json:"added_time,omitempty"` MatchPeriod int `json:"match_period,omitempty"` IsLive bool `json:"is_live"` + UpdatedAt time.Time `json:"updated_at"` FetchedAt time.Time `json:"fetched_at"` } -type EventWithSettings struct { - ID string - SportID int32 - MatchName string - HomeTeam string - AwayTeam string - HomeTeamID int64 - AwayTeamID int64 - HomeTeamImage string - AwayTeamImage string - LeagueID int64 - LeagueName string - LeagueCC ValidString - StartTime time.Time - Source EventSource - Status EventStatus - IsFeatured bool - IsMonitored bool - IsActive bool - WinningUpperLimit int32 - Score ValidString - MatchMinute ValidInt - TimerStatus ValidString - AddedTime ValidInt - MatchPeriod ValidInt - IsLive bool - UpdatedAt time.Time - FetchedAt time.Time -} - -type CreateEvent struct { - ID string - SportID int32 - MatchName string - HomeTeam string - AwayTeam string - HomeTeamID int64 - AwayTeamID int64 - HomeTeamImage string - AwayTeamImage string - LeagueID int64 - LeagueName string - StartTime time.Time - IsLive bool - Status EventStatus - Source EventSource -} - -type EventWithSettingsRes struct { - ID string `json:"id"` - SportID int32 `json:"sport_id"` - MatchName string `json:"match_name"` - HomeTeam string `json:"home_team"` - AwayTeam string `json:"away_team"` - HomeTeamID int64 `json:"home_team_id"` - AwayTeamID int64 `json:"away_team_id"` - HomeTeamImage string `json:"home_team_image"` - AwayTeamImage string `json:"away_team_image"` - LeagueID int64 `json:"league_id"` - LeagueName string `json:"league_name"` - LeagueCC string `json:"league_cc"` - StartTime time.Time `json:"start_time"` - Source EventSource `json:"source"` - Status EventStatus `json:"status"` - IsFeatured bool `json:"is_featured"` - IsMonitored bool `json:"is_monitored"` - IsActive bool `json:"is_active"` - WinningUpperLimit int32 `json:"winning_upper_limit"` - Score string `json:"score,omitempty"` - MatchMinute int `json:"match_minute,omitempty"` - TimerStatus string `json:"timer_status,omitempty"` - AddedTime int `json:"added_time,omitempty"` - MatchPeriod int `json:"match_period,omitempty"` - IsLive bool `json:"is_live"` - UpdatedAt time.Time `json:"updated_at"` - FetchedAt time.Time `json:"fetched_at"` -} type EventSettings struct { CompanyID int64 @@ -212,6 +219,7 @@ type EventFilter struct { Offset ValidInt32 MatchStatus ValidString // e.g., "upcoming", "in_play", "ended" Featured ValidBool + Active ValidBool } func ConvertDBEvent(event dbgen.EventWithCountry) BaseEvent { @@ -273,26 +281,27 @@ func ConvertDBEvents(events []dbgen.EventWithCountry) []BaseEvent { func ConvertCreateEvent(e CreateEvent) dbgen.InsertEventParams { return dbgen.InsertEventParams{ - ID: e.ID, - SportID: e.SportID, - MatchName: e.MatchName, - HomeTeam: e.HomeTeam, - AwayTeam: e.AwayTeam, - HomeTeamID: e.HomeTeamID, - AwayTeamID: e.AwayTeamID, - HomeKitImage: e.HomeTeamImage, - AwayKitImage: e.AwayTeamImage, - LeagueID: e.LeagueID, - LeagueName: e.LeagueName, - StartTime: pgtype.Timestamp{Time: e.StartTime, Valid: true}, - IsLive: e.IsLive, - Status: string(e.Status), - Source: string(e.Source), + ID: e.ID, + SportID: e.SportID, + MatchName: e.MatchName, + HomeTeam: e.HomeTeam, + AwayTeam: e.AwayTeam, + HomeTeamID: e.HomeTeamID, + AwayTeamID: e.AwayTeamID, + HomeKitImage: e.HomeTeamImage, + AwayKitImage: e.AwayTeamImage, + LeagueID: e.LeagueID, + LeagueName: e.LeagueName, + StartTime: pgtype.Timestamp{Time: e.StartTime, Valid: true}, + IsLive: e.IsLive, + Status: string(e.Status), + Source: string(e.Source), + DefaultWinningUpperLimit: e.DefaultWinningUpperLimit, } } -func ConvertCreateEventSettings(eventSettings CreateEventSettings) dbgen.InsertEventSettingsParams { - return dbgen.InsertEventSettingsParams{ +func ConvertCreateEventSettings(eventSettings CreateEventSettings) dbgen.SaveEventSettingsParams { + return dbgen.SaveEventSettingsParams{ CompanyID: eventSettings.CompanyID, EventID: eventSettings.EventID, IsActive: eventSettings.IsActive.ToPG(), @@ -318,13 +327,15 @@ func ConvertDBEventWithSetting(event dbgen.EventWithSetting) EventWithSettings { Value: event.LeagueCc.String, Valid: event.LeagueCc.Valid, }, - StartTime: event.StartTime.Time.UTC(), - Source: EventSource(event.Source), - Status: EventStatus(event.Status), - IsFeatured: event.IsFeatured, - IsMonitored: event.IsMonitored, - IsActive: event.IsActive, - WinningUpperLimit: event.WinningUpperLimit, + StartTime: event.StartTime.Time.UTC(), + Source: EventSource(event.Source), + Status: EventStatus(event.Status), + IsFeatured: event.IsFeatured, + IsMonitored: event.IsMonitored, + IsActive: event.IsActive, + DefaultIsFeatured: event.DefaultIsFeatured, + DefaultIsActive: event.DefaultIsActive, + DefaultWinningUpperLimit: event.DefaultWinningUpperLimit, Score: ValidString{ Value: event.Score.String, Valid: event.Score.Valid, @@ -359,8 +370,8 @@ func ConvertDBEventWithSettings(events []dbgen.EventWithSetting) []EventWithSett return result } -func ConvertUpdateEventSettings(event CreateEventSettings) dbgen.UpdateEventSettingsParams { - return dbgen.UpdateEventSettingsParams{ +func ConvertUpdateEventSettings(event CreateEventSettings) dbgen.SaveEventSettingsParams { + return dbgen.SaveEventSettingsParams{ EventID: event.EventID, CompanyID: event.CompanyID, IsActive: event.IsActive.ToPG(), @@ -409,32 +420,35 @@ func ConvertEventResList(events []BaseEvent) []BaseEventRes { func ConvertEventWitSettingRes(event EventWithSettings) EventWithSettingsRes { return EventWithSettingsRes{ - ID: event.ID, - SportID: event.SportID, - MatchName: event.MatchName, - HomeTeam: event.HomeTeam, - AwayTeam: event.AwayTeam, - HomeTeamID: event.HomeTeamID, - AwayTeamID: event.AwayTeamID, - HomeTeamImage: event.HomeTeamImage, - AwayTeamImage: event.AwayTeamImage, - LeagueID: event.LeagueID, - LeagueName: event.LeagueName, - LeagueCC: event.LeagueCC.Value, - StartTime: event.StartTime.UTC(), - Source: EventSource(event.Source), - Status: EventStatus(event.Status), - IsFeatured: event.IsFeatured, - IsMonitored: event.IsMonitored, - IsActive: event.IsActive, - WinningUpperLimit: event.WinningUpperLimit, - Score: event.Score.Value, - MatchMinute: event.MatchMinute.Value, - TimerStatus: event.TimerStatus.Value, - AddedTime: event.AddedTime.Value, - MatchPeriod: event.MatchPeriod.Value, - IsLive: event.IsLive, - FetchedAt: event.FetchedAt.UTC(), + ID: event.ID, + SportID: event.SportID, + MatchName: event.MatchName, + HomeTeam: event.HomeTeam, + AwayTeam: event.AwayTeam, + HomeTeamID: event.HomeTeamID, + AwayTeamID: event.AwayTeamID, + HomeTeamImage: event.HomeTeamImage, + AwayTeamImage: event.AwayTeamImage, + LeagueID: event.LeagueID, + LeagueName: event.LeagueName, + LeagueCC: event.LeagueCC.Value, + StartTime: event.StartTime.UTC(), + Source: EventSource(event.Source), + Status: EventStatus(event.Status), + IsFeatured: event.IsFeatured, + IsMonitored: event.IsMonitored, + IsActive: event.IsActive, + DefaultIsFeatured: event.DefaultIsFeatured, + DefaultIsActive: event.DefaultIsActive, + DefaultWinningUpperLimit: event.DefaultWinningUpperLimit, + WinningUpperLimit: event.WinningUpperLimit, + Score: event.Score.Value, + MatchMinute: event.MatchMinute.Value, + TimerStatus: event.TimerStatus.Value, + AddedTime: event.AddedTime.Value, + MatchPeriod: event.MatchPeriod.Value, + IsLive: event.IsLive, + FetchedAt: event.FetchedAt.UTC(), } } diff --git a/internal/domain/jsontypes.go b/internal/domain/jsontypes.go new file mode 100644 index 0000000..3e52375 --- /dev/null +++ b/internal/domain/jsontypes.go @@ -0,0 +1,27 @@ +package domain + +import ( + "encoding/json" + "fmt" +) + +// Custom type for fields that can be string or int +type StringOrNumber string + +func (s *StringOrNumber) UnmarshalJSON(data []byte) error { + // Try as string + var str string + if err := json.Unmarshal(data, &str); err == nil { + *s = StringOrNumber(str) + return nil + } + + // Try as number + var num json.Number + if err := json.Unmarshal(data, &num); err == nil { + *s = StringOrNumber(num.String()) + return nil + } + + return fmt.Errorf("StringOrNumber: cannot unmarshal %s", string(data)) +} diff --git a/internal/domain/league.go b/internal/domain/league.go index a44ae70..6743b6e 100644 --- a/internal/domain/league.go +++ b/internal/domain/league.go @@ -8,24 +8,30 @@ import ( // The ID and the Bet365 IDs we have here are both gotten from the betapi type LeagueWithSettings struct { - ID int64 - Name string - CompanyID int64 - CountryCode ValidString - Bet365ID ValidInt32 - SportID int32 - IsActive bool - IsFeatured bool - UpdatedAt time.Time + ID int64 + Name string + CompanyID int64 + CountryCode ValidString + Bet365ID ValidInt32 + SportID int32 + IsActive bool + IsFeatured bool + DefaultIsActive bool + DefaultIsFeatured bool + UpdatedAt time.Time } type LeagueWithSettingsRes struct { - ID int64 `json:"id" example:"1"` - Name string `json:"name" example:"BPL"` - CountryCode string `json:"cc" example:"uk"` - 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"` + ID int64 `json:"id" example:"1"` + Name string `json:"name" example:"BPL"` + CompanyID int64 `json:"company_id" example:"1"` + CountryCode string `json:"cc" example:"uk"` + 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"` + DefaultIsActive bool `json:"default_is_active" example:"false"` + DefaultIsFeatured bool `json:"default_is_featured" example:"false"` + UpdatedAt time.Time `json:"updated_at"` } type BaseLeague struct { ID int64 @@ -82,6 +88,7 @@ type UpdateLeague struct { } type LeagueFilter struct { + Query ValidString CountryCode ValidString SportID ValidInt32 IsActive ValidBool @@ -137,12 +144,12 @@ func ConvertDBBaseLeagues(leagues []dbgen.League) []BaseLeague { return result } -func ConvertDBLeagueWithSetting(lws dbgen.LeagueWithSetting) LeagueWithSettings { +func ConvertDBLeagueWithSetting(lws dbgen.GetAllLeaguesWithSettingsRow) LeagueWithSettings { return LeagueWithSettings{ ID: lws.ID, Name: lws.Name, - CompanyID: lws.CompanyID, - CountryCode: ValidString{ + CompanyID: lws.CompanyID.Int64, + CountryCode: ValidString{ Value: lws.CountryCode.String, Valid: lws.CountryCode.Valid, }, @@ -154,10 +161,13 @@ func ConvertDBLeagueWithSetting(lws dbgen.LeagueWithSetting) LeagueWithSettings SportID: lws.SportID, IsFeatured: lws.IsFeatured, UpdatedAt: lws.UpdatedAt.Time, + + DefaultIsActive: lws.DefaultIsActive, + DefaultIsFeatured: lws.DefaultIsFeatured, } } -func ConvertDBLeagueWithSettings(lws []dbgen.LeagueWithSetting) []LeagueWithSettings { +func ConvertDBLeagueWithSettings(lws []dbgen.GetAllLeaguesWithSettingsRow) []LeagueWithSettings { result := make([]LeagueWithSettings, len(lws)) for i, league := range lws { result[i] = ConvertDBLeagueWithSetting(league) @@ -174,3 +184,50 @@ func ConvertUpdateLeague(updateLeague UpdateLeague) dbgen.UpdateLeagueParams { SportID: updateLeague.SportID.ToPG(), } } + +func ConvertLeagueWithSettingRes(lws LeagueWithSettings) LeagueWithSettingsRes { + return LeagueWithSettingsRes{ + ID: lws.ID, + Name: lws.Name, + CompanyID: lws.CompanyID, + CountryCode: lws.CountryCode.Value, + Bet365ID: lws.Bet365ID.Value, + IsActive: lws.IsActive, + SportID: lws.SportID, + IsFeatured: lws.IsFeatured, + UpdatedAt: lws.UpdatedAt, + DefaultIsActive: lws.DefaultIsActive, + DefaultIsFeatured: lws.DefaultIsFeatured, + } +} + +func ConvertLeagueWithSettingResList(leagues []LeagueWithSettings) []LeagueWithSettingsRes { + result := make([]LeagueWithSettingsRes, len(leagues)) + + for i, lws := range leagues { + result[i] = ConvertLeagueWithSettingRes(lws) + } + + return result +} + +func ConvertBaseLeagueRes(league BaseLeague) BaseLeagueRes { + return BaseLeagueRes{ + ID: league.ID, + Name: league.Name, + CountryCode: league.CountryCode.Value, + Bet365ID: league.Bet365ID.Value, + SportID: league.SportID, + DefaultIsActive: league.DefaultIsActive, + DefaultIsFeatured: league.DefaultIsFeatured, + } +} + +func ConvertBaseLeagueResList(leagues []BaseLeague) []BaseLeagueRes { + result := make([]BaseLeagueRes, len(leagues)) + for i, league := range leagues { + result[i] = ConvertBaseLeagueRes(league) + } + + return result +} diff --git a/internal/domain/notification.go b/internal/domain/notification.go index d10f3d7..97fc9d1 100644 --- a/internal/domain/notification.go +++ b/internal/domain/notification.go @@ -32,6 +32,7 @@ const ( NOTIFICATION_TYPE_BET_RESULT NotificationType = "bet_result" NOTIFICATION_TYPE_TRANSFER_REJECTED NotificationType = "transfer_rejected" NOTIFICATION_TYPE_APPROVAL_REQUIRED NotificationType = "approval_required" + NOTIFICATION_TYPE_BONUS_AWARDED NotificationType = "bonus_awarded" NotificationRecieverSideAdmin NotificationRecieverSide = "admin" NotificationRecieverSideCustomer NotificationRecieverSide = "customer" @@ -73,7 +74,7 @@ type Notification struct { RecipientID int64 `json:"recipient_id"` Type NotificationType `json:"type"` Level NotificationLevel `json:"level"` - ErrorSeverity *NotificationErrorSeverity `json:"error_severity"` + ErrorSeverity NotificationErrorSeverity `json:"error_severity"` Reciever NotificationRecieverSide `json:"reciever"` IsRead bool `json:"is_read"` DeliveryStatus NotificationDeliveryStatus `json:"delivery_status,omitempty"` diff --git a/internal/domain/oddres.go b/internal/domain/oddres.go index 2334b16..19c2cb2 100644 --- a/internal/domain/oddres.go +++ b/internal/domain/oddres.go @@ -27,7 +27,7 @@ type RawOdd struct { // The Market ID for the json data can be either string / int which is causing problems when UnMarshalling type OddsMarket struct { - ID ValidInt64 `json:"id"` + ID StringOrNumber `json:"id"` Name string `json:"name"` Odds []json.RawMessage `json:"odds"` Header string `json:"header,omitempty"` diff --git a/internal/domain/odds.go b/internal/domain/odds.go index c56d320..88092e1 100644 --- a/internal/domain/odds.go +++ b/internal/domain/odds.go @@ -61,6 +61,17 @@ type CreateOddMarketSettings struct { CustomRawOdds []map[string]interface{} } +type CustomOdd struct { + OddID int64 `json:"odd_id"` + OddValue float32 `json:"odd_value"` +} + +type CreateOddMarketSettingsReq struct { + OddMarketID int64 `json:"odd_market_id"` + IsActive *bool `json:"is_active,omitempty"` + CustomOdd []CustomOdd `json:"custom_odd,omitempty"` +} + type RawOddsByMarketID struct { ID int64 `json:"id"` MarketName string `json:"market_name"` @@ -136,12 +147,12 @@ func ConvertCreateOddMarket(oddMarket CreateOddMarket) (dbgen.InsertOddsMarketPa }, nil } -func ConvertCreateOddMarketSetting(oms CreateOddMarketSettings) (dbgen.InsertOddSettingsParams, error) { +func ConvertCreateOddMarketSetting(oms CreateOddMarketSettings) (dbgen.SaveOddSettingsParams, error) { rawOddsBytes, err := json.Marshal(oms.CustomRawOdds) if err != nil { - return dbgen.InsertOddSettingsParams{}, err + return dbgen.SaveOddSettingsParams{}, err } - return dbgen.InsertOddSettingsParams{ + return dbgen.SaveOddSettingsParams{ CompanyID: oms.CompanyID, OddsMarketID: oms.OddMarketID, IsActive: oms.IsActive.ToPG(), diff --git a/internal/domain/raffle.go b/internal/domain/raffle.go new file mode 100644 index 0000000..a8307b8 --- /dev/null +++ b/internal/domain/raffle.go @@ -0,0 +1,83 @@ +package domain + +import "time" + +type Raffle struct { + ID int32 + CompanyID int32 + Name string + CreatedAt time.Time + ExpiresAt time.Time + Type string + Status string +} + +type RaffleFilter struct { + // requireds will depend on type of raffle (sport or game) + Type string `json:"type" validate:"required,oneof=sport game"` + RaffleID int32 `json:"raffle_id" validate:"required"` + SportID int32 `json:"sport_id" validate:"required_if=Type sport"` + LeagueID int32 `json:"league_id" validate:"required_if=Type sport"` + GameID string `json:"game_id" validate:"required_if=Type game"` +} + +type RaffleStanding struct { + UserID int64 + RaffleID int32 + FirstName string + LastName string + PhoneNumber string + Email string + TicketCount int64 +} + +type RaffleStandingRes struct { + UserID int64 `json:"user_id"` + RaffleID int32 `json:"raffle_id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + PhoneNumber string `json:"phone_number"` + Email string `json:"email"` + TicketCount int64 `json:"ticket_count"` +} + +type RaffleWinnerParams struct { + RaffleID int32 + UserID int32 + Rank int32 +} + +type RaffleTicket struct { + ID int32 + RaffleID int32 + UserID int32 + IsActive bool +} + +type RaffleTicketRes struct { + TicketID int32 + UserID int32 + Name string + Type string + ExpiresAt time.Time + Status string +} + +type CreateRaffle struct { + CompanyID int32 `json:"company_id" validate:"required"` + Name string `json:"name" validate:"required"` + ExpiresAt *time.Time `json:"expires_at" validate:"required"` + Type string `json:"type" validate:"required"` +} + +type CreateRaffleTicket struct { + RaffleID int32 `json:"raffle_id" validate:"required"` + UserID int32 `json:"user_id" validate:"required"` +} + +// aside from ID, atleast one of the fields should be required +type UpdateRaffleParams struct { + ID int32 `json:"id" validate:"required"` + Name string `json:"name" validate:"required_without_all=ExpiresAt"` + ExpiresAt *time.Time `json:"expires_at" validate:"required_without_all=Name"` +} diff --git a/internal/domain/referal.go b/internal/domain/referal.go index 1e528a4..bb9e1bb 100644 --- a/internal/domain/referal.go +++ b/internal/domain/referal.go @@ -1,73 +1,186 @@ package domain import ( - "database/sql/driver" - "fmt" "time" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" ) -type ReferralStatus string - -const ( - ReferralPending ReferralStatus = "PENDING" - ReferralCompleted ReferralStatus = "COMPLETED" - ReferralExpired ReferralStatus = "EXPIRED" - ReferralCancelled ReferralStatus = "CANCELLED" -) - -func (rs *ReferralStatus) Scan(src interface{}) error { - switch s := src.(type) { - case []byte: - *rs = ReferralStatus(s) - case string: - *rs = ReferralStatus(s) - default: - return fmt.Errorf("unsupported scan type for ReferralStatus: %T", src) - } - return nil +type ReferralCode struct { + ID int64 + ReferrerID int64 + ReferralCode string + CompanyID int64 + NumberOfReferrals int64 + RewardAmount Currency + CreatedAt time.Time + UpdatedAt time.Time } -func (rs ReferralStatus) Value() (driver.Value, error) { - return string(rs), nil +type ReferralCodeRes struct { + ID int64 `json:"id"` + ReferrerID int64 `json:"referrer_id"` + ReferralCode string `json:"referral_code"` + CompanyID int64 `json:"company_id"` + NumberOfReferrals int64 `json:"number_of_referrals"` + RewardAmount float32 `json:"reward_amount"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type CreateReferralCode struct { + ReferrerID int64 + ReferralCode string + CompanyID int64 + NumberOfReferrals int64 + RewardAmount Currency +} + +type UserReferral struct { + ReferredID int64 + ReferralCodeID int64 +} + +type CreateUserReferrals struct { + ReferredID int64 + ReferralCodeID int64 +} + +type UpdateReferralCode struct { + ID int64 + IsActive bool + ReferralCode string + RewardAmount Currency + NumberOfReferrals int64 } type ReferralStats struct { - TotalReferrals int - CompletedReferrals int - TotalRewardEarned float64 - PendingRewards float64 + TotalReferrals int64 + TotalRewardEarned Currency } -type ReferralSettings struct { - ID int64 - ReferralRewardAmount float64 - CashbackPercentage float64 - BetReferralBonusPercentage float64 - MaxReferrals int32 - ExpiresAfterDays int32 - UpdatedBy string - CreatedAt time.Time - UpdatedAt time.Time - Version int32 +type ReferralStatsRes struct { + TotalReferrals int64 `json:"total_referrals"` + TotalRewardEarned float32 `json:"total_reward_earned"` } -type ReferralSettingsReq struct { - ReferralRewardAmount float64 `json:"referral_reward_amount" validate:"required"` - CashbackPercentage float64 `json:"cashback_percentage" validate:"required"` - MaxReferrals int32 `json:"max_referrals" validate:"required"` - ExpiresAfterDays int32 `json:"expires_afterdays" validate:"required"` - UpdatedBy string `json:"updated_by" validate:"required"` +// type ReferralSettings struct { +// ID int64 +// ReferralRewardAmount float64 +// CashbackPercentage float64 +// BetReferralBonusPercentage float64 +// MaxReferrals int32 +// ExpiresAfterDays int32 +// UpdatedBy string +// CreatedAt time.Time +// UpdatedAt time.Time +// Version int32 +// } + +// type ReferralSettingsReq struct { +// ReferralRewardAmount float64 `json:"referral_reward_amount" validate:"required"` +// CashbackPercentage float64 `json:"cashback_percentage" validate:"required"` +// MaxReferrals int32 `json:"max_referrals" validate:"required"` +// UpdatedBy string `json:"updated_by" validate:"required"` +// } + +func ConvertCreateReferralCode(code CreateReferralCode) dbgen.CreateReferralCodeParams { + return dbgen.CreateReferralCodeParams{ + ReferralCode: code.ReferralCode, + ReferrerID: code.ReferrerID, + CompanyID: code.CompanyID, + NumberOfReferrals: code.NumberOfReferrals, + RewardAmount: int64(code.RewardAmount), + } } -type Referral struct { - ID int64 - ReferralCode string - ReferrerID string - ReferredID *string - Status ReferralStatus - RewardAmount float64 - CashbackAmount float64 - CreatedAt time.Time - UpdatedAt time.Time - ExpiresAt time.Time + func ConvertDBReferralCode(code dbgen.ReferralCode) ReferralCode { + return ReferralCode{ + ID: code.ID, + ReferrerID: code.ReferrerID, + ReferralCode: code.ReferralCode, + NumberOfReferrals: code.NumberOfReferrals, + RewardAmount: Currency(code.RewardAmount), + CompanyID: code.CompanyID, + CreatedAt: code.CreatedAt.Time, + UpdatedAt: code.UpdatedAt.Time, + } +} + +func ConvertDBReferralCodes(codes []dbgen.ReferralCode) []ReferralCode { + result := make([]ReferralCode, len(codes)) + for i, code := range codes { + result[i] = ConvertDBReferralCode(code) + } + return result +} + +func ConvertCreateUserReferral(referral CreateUserReferrals) dbgen.CreateUserReferralParams { + return dbgen.CreateUserReferralParams{ + ReferredID: referral.ReferredID, + ReferralCodeID: referral.ReferralCodeID, + } +} + +func ConvertDBUserReferral(referral dbgen.UserReferral) UserReferral { + return UserReferral{ + ReferredID: referral.ReferredID, + ReferralCodeID: referral.ReferralCodeID, + } +} + +func ConvertDBUserReferrals(referrals []dbgen.UserReferral) []UserReferral { + result := make([]UserReferral, len(referrals)) + for i, referral := range referrals { + result[i] = ConvertDBUserReferral(referral) + } + + return result +} + +func ConvertUpdateReferralCode(referralCode UpdateReferralCode) dbgen.UpdateReferralCodeParams { + return dbgen.UpdateReferralCodeParams{ + ID: referralCode.ID, + IsActive: referralCode.IsActive, + ReferralCode: referralCode.ReferralCode, + NumberOfReferrals: referralCode.NumberOfReferrals, + RewardAmount: int64(referralCode.RewardAmount), + } +} + +func ConvertDBReferralStats(stats dbgen.GetReferralStatsRow) ReferralStats { + return ReferralStats{ + TotalReferrals: stats.TotalReferrals, + TotalRewardEarned: Currency(stats.TotalRewardEarned), + } +} + +func ConvertReferralCodeRes(referral ReferralCode) ReferralCodeRes { + return ReferralCodeRes{ + ID: referral.ID, + ReferrerID: referral.ReferrerID, + ReferralCode: referral.ReferralCode, + CompanyID: referral.CompanyID, + NumberOfReferrals: referral.NumberOfReferrals, + RewardAmount: referral.RewardAmount.Float32(), + CreatedAt: referral.CreatedAt, + UpdatedAt: referral.UpdatedAt, + } +} + +func ConvertReferralCodeResList(referrals []ReferralCode) []ReferralCodeRes { + result := make([]ReferralCodeRes, len(referrals)) + + for i, referral := range referrals { + result[i] = ConvertReferralCodeRes(referral) + } + + return result +} + +func ConvertReferralStatsRes(stats ReferralStats) ReferralStatsRes { + return ReferralStatsRes{ + TotalReferrals: stats.TotalReferrals, + TotalRewardEarned: stats.TotalRewardEarned.Float32(), + } } diff --git a/internal/domain/role.go b/internal/domain/role.go index dcd2c57..f1ddfc4 100644 --- a/internal/domain/role.go +++ b/internal/domain/role.go @@ -3,16 +3,17 @@ package domain type Role string const ( - RoleSuperAdmin Role = "super_admin" - RoleAdmin Role = "admin" - RoleBranchManager Role = "branch_manager" - RoleCustomer Role = "customer" - RoleCashier Role = "cashier" + RoleSuperAdmin Role = "super_admin" + RoleAdmin Role = "admin" + RoleBranchManager Role = "branch_manager" + RoleCustomer Role = "customer" + RoleCashier Role = "cashier" + RoleTransactionApprover Role = "transaction_approver" ) func (r Role) IsValid() bool { switch r { - case RoleSuperAdmin, RoleAdmin, RoleBranchManager, RoleCustomer, RoleCashier: + case RoleSuperAdmin, RoleAdmin, RoleBranchManager, RoleCustomer, RoleCashier, RoleTransactionApprover: return true default: return false diff --git a/internal/domain/setting_list.go b/internal/domain/setting_list.go index 5f4a20e..5a5c86e 100644 --- a/internal/domain/setting_list.go +++ b/internal/domain/setting_list.go @@ -16,67 +16,170 @@ var ( ) type SettingList struct { - SMSProvider SMSProvider `json:"sms_provider"` - 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"` - AmountForBetReferral Currency `json:"amount_for_bet_referral"` - CashbackAmountCap Currency `json:"cashback_amount_cap"` + SMSProvider SMSProvider `json:"sms_provider"` + 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"` + AmountForBetReferral Currency `json:"amount_for_bet_referral"` + CashbackAmountCap Currency `json:"cashback_amount_cap"` + DefaultWinningLimit int64 `json:"default_winning_limit"` + ReferralRewardAmount Currency `json:"referral_reward_amount"` + CashbackPercentage float32 `json:"cashback_percentage"` + DefaultMaxReferrals int64 `json:"default_max_referrals"` + MinimumBetAmount Currency `json:"minimum_bet_amount"` + BetDuplicateLimit int64 `json:"bet_duplicate_limit"` + SendEmailOnBetFinish bool `json:"send_email_on_bet_finish"` + SendSMSOnBetFinish bool `json:"send_sms_on_bet_finish"` + WelcomeBonusActive bool `json:"welcome_bonus_active"` + WelcomeBonusMultiplier float32 `json:"welcome_bonus_multiplier"` + WelcomeBonusCap Currency `json:"welcome_bonus_cap"` + WelcomeBonusCount int64 `json:"welcome_bonus_count"` + WelcomeBonusExpire int64 `json:"welcome_bonus_expiry"` } type SettingListRes struct { - SMSProvider SMSProvider `json:"sms_provider"` - MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"` - BetAmountLimit float32 `json:"bet_amount_limit"` - DailyTicketPerIP int64 `json:"daily_ticket_limit"` - TotalWinningLimit float32 `json:"total_winning_limit"` - AmountForBetReferral float32 `json:"amount_for_bet_referral"` - CashbackAmountCap float32 `json:"cashback_amount_cap"` + SMSProvider SMSProvider `json:"sms_provider"` + MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"` + BetAmountLimit float32 `json:"bet_amount_limit"` + DailyTicketPerIP int64 `json:"daily_ticket_limit"` + TotalWinningLimit float32 `json:"total_winning_limit"` + AmountForBetReferral float32 `json:"amount_for_bet_referral"` + CashbackAmountCap float32 `json:"cashback_amount_cap"` + DefaultWinningLimit int64 `json:"default_winning_limit"` + ReferralRewardAmount float32 `json:"referral_reward_amount"` + CashbackPercentage float32 `json:"cashback_percentage"` + DefaultMaxReferrals int64 `json:"default_max_referrals"` + MinimumBetAmount float32 `json:"minimum_bet_amount"` + BetDuplicateLimit int64 `json:"bet_duplicate_limit"` + SendEmailOnBetFinish bool `json:"send_email_on_bet_finish"` + SendSMSOnBetFinish bool `json:"send_sms_on_bet_finish"` + WelcomeBonusActive bool `json:"welcome_bonus_active"` + WelcomeBonusMultiplier float32 `json:"welcome_bonus_multiplier"` + WelcomeBonusCap float32 `json:"welcome_bonus_cap"` + WelcomeBonusCount int64 `json:"welcome_bonus_count"` + WelcomeBonusExpire int64 `json:"welcome_bonus_expiry"` +} + +func ConvertSettingListRes(settings SettingList) SettingListRes { + return SettingListRes{ + SMSProvider: settings.SMSProvider, + MaxNumberOfOutcomes: settings.MaxNumberOfOutcomes, + BetAmountLimit: settings.BetAmountLimit.Float32(), + DailyTicketPerIP: settings.DailyTicketPerIP, + TotalWinningLimit: settings.TotalWinningLimit.Float32(), + AmountForBetReferral: settings.AmountForBetReferral.Float32(), + CashbackAmountCap: settings.CashbackAmountCap.Float32(), + DefaultWinningLimit: settings.DefaultWinningLimit, + ReferralRewardAmount: settings.ReferralRewardAmount.Float32(), + CashbackPercentage: settings.CashbackPercentage, + DefaultMaxReferrals: settings.DefaultMaxReferrals, + MinimumBetAmount: settings.MinimumBetAmount.Float32(), + BetDuplicateLimit: settings.BetDuplicateLimit, + SendEmailOnBetFinish: settings.SendEmailOnBetFinish, + SendSMSOnBetFinish: settings.SendSMSOnBetFinish, + WelcomeBonusActive: settings.WelcomeBonusActive, + WelcomeBonusMultiplier: settings.WelcomeBonusMultiplier, + WelcomeBonusCap: settings.WelcomeBonusCap.Float32(), + WelcomeBonusCount: settings.WelcomeBonusCount, + WelcomeBonusExpire: settings.WelcomeBonusExpire, + } } type SaveSettingListReq struct { - SMSProvider *string `json:"sms_provider,omitempty"` - MaxNumberOfOutcomes *int64 `json:"max_number_of_outcomes,omitempty"` - BetAmountLimit *float32 `json:"bet_amount_limit,omitempty"` - DailyTicketPerIP *int64 `json:"daily_ticket_limit,omitempty"` - TotalWinningLimit *float32 `json:"total_winning_limit,omitempty"` - AmountForBetReferral *float32 `json:"amount_for_bet_referral,omitempty"` - CashbackAmountCap *float32 `json:"cashback_amount_cap,omitempty"` + SMSProvider *string `json:"sms_provider,omitempty"` + MaxNumberOfOutcomes *int64 `json:"max_number_of_outcomes,omitempty"` + BetAmountLimit *float32 `json:"bet_amount_limit,omitempty"` + DailyTicketPerIP *int64 `json:"daily_ticket_limit,omitempty"` + TotalWinningLimit *float32 `json:"total_winning_limit,omitempty"` + AmountForBetReferral *float32 `json:"amount_for_bet_referral,omitempty"` + CashbackAmountCap *float32 `json:"cashback_amount_cap,omitempty"` + DefaultWinningLimit *int64 `json:"default_winning_limit,omitempty"` + ReferralRewardAmount *float32 `json:"referral_reward_amount"` + CashbackPercentage *float32 `json:"cashback_percentage"` + DefaultMaxReferrals *int64 `json:"default_max_referrals"` + MinimumBetAmount *float32 `json:"minimum_bet_amount"` + BetDuplicateLimit *int64 `json:"bet_duplicate_limit"` + SendEmailOnBetFinish *bool `json:"send_email_on_bet_finish"` + SendSMSOnBetFinish *bool `json:"send_sms_on_bet_finish"` + WelcomeBonusActive *bool `json:"welcome_bonus_active"` + WelcomeBonusMultiplier *float32 `json:"welcome_bonus_multiplier"` + WelcomeBonusCap *float32 `json:"welcome_bonus_cap"` + WelcomeBonusCount *int64 `json:"welcome_bonus_count"` + WelcomeBonusExpire *int64 `json:"welcome_bonus_expiry"` +} + +type ValidSettingList struct { + SMSProvider ValidString + MaxNumberOfOutcomes ValidInt64 + BetAmountLimit ValidCurrency + DailyTicketPerIP ValidInt64 + TotalWinningLimit ValidCurrency + AmountForBetReferral ValidCurrency + CashbackAmountCap ValidCurrency + DefaultWinningLimit ValidInt64 + ReferralRewardAmount ValidCurrency + CashbackPercentage ValidFloat32 + DefaultMaxReferrals ValidInt64 + MinimumBetAmount ValidCurrency + BetDuplicateLimit ValidInt64 + SendEmailOnBetFinish ValidBool + SendSMSOnBetFinish ValidBool + WelcomeBonusActive ValidBool + WelcomeBonusMultiplier ValidFloat32 + WelcomeBonusCap ValidCurrency + WelcomeBonusCount ValidInt64 + WelcomeBonusExpire ValidInt64 } func ConvertSaveSettingListReq(settings SaveSettingListReq) ValidSettingList { return ValidSettingList{ - SMSProvider: ConvertStringPtr(settings.SMSProvider), - MaxNumberOfOutcomes: ConvertInt64Ptr(settings.MaxNumberOfOutcomes), - BetAmountLimit: ConvertFloat32PtrToCurrency(settings.BetAmountLimit), - DailyTicketPerIP: ConvertInt64Ptr(settings.DailyTicketPerIP), - TotalWinningLimit: ConvertFloat32PtrToCurrency(settings.TotalWinningLimit), - AmountForBetReferral: ConvertFloat32PtrToCurrency(settings.AmountForBetReferral), - CashbackAmountCap: ConvertFloat32PtrToCurrency(settings.CashbackAmountCap), + SMSProvider: ConvertStringPtr(settings.SMSProvider), + MaxNumberOfOutcomes: ConvertInt64Ptr(settings.MaxNumberOfOutcomes), + BetAmountLimit: ConvertFloat32PtrToCurrency(settings.BetAmountLimit), + DailyTicketPerIP: ConvertInt64Ptr(settings.DailyTicketPerIP), + TotalWinningLimit: ConvertFloat32PtrToCurrency(settings.TotalWinningLimit), + AmountForBetReferral: ConvertFloat32PtrToCurrency(settings.AmountForBetReferral), + CashbackAmountCap: ConvertFloat32PtrToCurrency(settings.CashbackAmountCap), + DefaultWinningLimit: ConvertInt64Ptr(settings.DefaultWinningLimit), + ReferralRewardAmount: ConvertFloat32PtrToCurrency(settings.ReferralRewardAmount), + CashbackPercentage: ConvertFloat32Ptr(settings.CashbackPercentage), + DefaultMaxReferrals: ConvertInt64Ptr(settings.DefaultMaxReferrals), + MinimumBetAmount: ConvertFloat32PtrToCurrency(settings.MinimumBetAmount), + BetDuplicateLimit: ConvertInt64Ptr(settings.BetDuplicateLimit), + SendEmailOnBetFinish: ConvertBoolPtr(settings.SendEmailOnBetFinish), + SendSMSOnBetFinish: ConvertBoolPtr(settings.SendSMSOnBetFinish), + WelcomeBonusActive: ConvertBoolPtr(settings.WelcomeBonusActive), + WelcomeBonusMultiplier: ConvertFloat32Ptr(settings.WelcomeBonusMultiplier), + WelcomeBonusCap: ConvertFloat32PtrToCurrency(settings.WelcomeBonusCap), + WelcomeBonusCount: ConvertInt64Ptr(settings.WelcomeBonusCount), + WelcomeBonusExpire: ConvertInt64Ptr(settings.WelcomeBonusExpire), } } -type ValidSettingList struct { - SMSProvider ValidString - MaxNumberOfOutcomes ValidInt64 - BetAmountLimit ValidCurrency - DailyTicketPerIP ValidInt64 - TotalWinningLimit ValidCurrency - AmountForBetReferral ValidCurrency - CashbackAmountCap ValidCurrency -} - // Always make sure to run the validation before converting this func (vsl *ValidSettingList) ToSettingList() SettingList { return SettingList{ - SMSProvider: SMSProvider(vsl.SMSProvider.Value), - MaxNumberOfOutcomes: vsl.MaxNumberOfOutcomes.Value, - BetAmountLimit: Currency(vsl.BetAmountLimit.Value), - DailyTicketPerIP: vsl.DailyTicketPerIP.Value, - TotalWinningLimit: Currency(vsl.TotalWinningLimit.Value), - AmountForBetReferral: Currency(vsl.AmountForBetReferral.Value), - CashbackAmountCap: Currency(vsl.CashbackAmountCap.Value), + SMSProvider: SMSProvider(vsl.SMSProvider.Value), + MaxNumberOfOutcomes: vsl.MaxNumberOfOutcomes.Value, + BetAmountLimit: vsl.BetAmountLimit.Value, + DailyTicketPerIP: vsl.DailyTicketPerIP.Value, + TotalWinningLimit: vsl.TotalWinningLimit.Value, + AmountForBetReferral: vsl.AmountForBetReferral.Value, + CashbackAmountCap: vsl.CashbackAmountCap.Value, + DefaultWinningLimit: vsl.DefaultWinningLimit.Value, + ReferralRewardAmount: vsl.ReferralRewardAmount.Value, + CashbackPercentage: vsl.CashbackPercentage.Value, + DefaultMaxReferrals: vsl.DefaultMaxReferrals.Value, + MinimumBetAmount: vsl.MinimumBetAmount.Value, + BetDuplicateLimit: vsl.BetDuplicateLimit.Value, + SendEmailOnBetFinish: vsl.SendEmailOnBetFinish.Value, + SendSMSOnBetFinish: vsl.SendSMSOnBetFinish.Value, + WelcomeBonusActive: vsl.WelcomeBonusActive.Value, + WelcomeBonusMultiplier: vsl.WelcomeBonusMultiplier.Value, + WelcomeBonusCap: vsl.WelcomeBonusCap.Value, + WelcomeBonusCount: vsl.WelcomeBonusCount.Value, + WelcomeBonusExpire: vsl.WelcomeBonusExpire.Value, } } @@ -92,6 +195,11 @@ func (vsl *ValidSettingList) GetInt64SettingsMap() map[string]*ValidInt64 { return map[string]*ValidInt64{ "max_number_of_outcomes": &vsl.MaxNumberOfOutcomes, "daily_ticket_limit": &vsl.DailyTicketPerIP, + "default_winning_limit": &vsl.DefaultWinningLimit, + "default_max_referrals": &vsl.DefaultMaxReferrals, + "bet_duplicate_limit": &vsl.BetDuplicateLimit, + "welcome_bonus_count": &vsl.WelcomeBonusCount, + "welcome_bonus_expiry": &vsl.WelcomeBonusExpire, } } @@ -101,6 +209,9 @@ func (vsl *ValidSettingList) GetCurrencySettingsMap() map[string]*ValidCurrency "total_winnings_limit": &vsl.TotalWinningLimit, "amount_for_bet_referral": &vsl.AmountForBetReferral, "cashback_amount_cap": &vsl.CashbackAmountCap, + "referral_reward_amount": &vsl.ReferralRewardAmount, + "minimum_bet_amount": &vsl.MinimumBetAmount, + "welcome_bonus_cap": &vsl.WelcomeBonusCap, } } @@ -111,17 +222,26 @@ func (vsl *ValidSettingList) GetStringSettingsMap() map[string]*ValidString { } func (vsl *ValidSettingList) GetBoolSettingsMap() map[string]*ValidBool { - return map[string]*ValidBool{} + return map[string]*ValidBool{ + "send_email_on_bet_finish": &vsl.SendEmailOnBetFinish, + "send_sms_on_bet_finish": &vsl.SendSMSOnBetFinish, + "welcome_bonus_active": &vsl.WelcomeBonusActive, + } } func (vsl *ValidSettingList) GetFloat32SettingsMap() map[string]*ValidFloat32 { - return map[string]*ValidFloat32{} + return map[string]*ValidFloat32{ + "cashback_percentage": &vsl.CashbackPercentage, + "welcome_bonus_multiplier": &vsl.WelcomeBonusMultiplier, + } } func (vsl *ValidSettingList) GetTimeSettingsMap() map[string]*ValidTime { return map[string]*ValidTime{} } +// Setting Functions + func (vsl *ValidSettingList) GetTotalSettings() int { return len(vsl.GetInt64SettingsMap()) + len(vsl.GetCurrencySettingsMap()) + @@ -157,57 +277,105 @@ func (vsl *ValidSettingList) GetAllValid() map[string]*bool { return settingValid } -func setValidSetting[T any](settings map[string]*T, searchKey string, setVal T) error { - for key, setting := range settings { +// func setValidSetting[T any](settings map[string]*T, searchKey string, searchVal string, setVal func(string) (T, error)) error { +// for key, setting := range settings { + +// if key == searchKey { +// s, err := setVal(searchVal) +// if err != nil { +// return err +// } +// *setting = s +// } +// return nil +// } +// return ErrSettingNotFound +// } +func (vsl *ValidSettingList) SetInt64Setting(searchKey string, searchVal string) error { + for key, setting := range vsl.GetInt64SettingsMap() { if key == searchKey { - *setting = setVal + value, err := strconv.ParseInt(searchVal, 10, 64) + if err != nil { + return err + } + *setting = ValidInt64{Value: value, Valid: true} + return nil } - return nil } return ErrSettingNotFound } -func (vsl *ValidSettingList) SetInt64Setting(searchKey string, searchVal string) error { - value, err := strconv.ParseInt(searchVal, 10, 64) - if err != nil { - return err - } - return setValidSetting(vsl.GetInt64SettingsMap(), searchKey, ValidInt64{Value: value, Valid: true}) -} func (vsl *ValidSettingList) SetCurrencySetting(searchKey string, searchVal string) error { - value, err := strconv.ParseInt(searchVal, 10, 64) - if err != nil { - return err + for key, setting := range vsl.GetCurrencySettingsMap() { + if key == searchKey { + value, err := strconv.ParseInt(searchVal, 10, 64) + if err != nil { + return err + } + *setting = ValidCurrency{Value: Currency(value), Valid: true} + return nil + } } - return setValidSetting(vsl.GetCurrencySettingsMap(), searchKey, ValidCurrency{Value: Currency(value), Valid: true}) + + return ErrSettingNotFound } func (vsl *ValidSettingList) SetStringSetting(searchKey string, searchVal string) error { - return setValidSetting(vsl.GetStringSettingsMap(), searchKey, ValidString{Value: searchVal, Valid: true}) + for key, setting := range vsl.GetStringSettingsMap() { + if key == searchKey { + *setting = ValidString{Value: searchVal, Valid: true} + return nil + } + } + + return ErrSettingNotFound } func (vsl *ValidSettingList) SetBoolSetting(searchKey string, searchVal string) error { - value, err := strconv.ParseBool(searchVal) - if err != nil { - return err + for key, setting := range vsl.GetBoolSettingsMap() { + + if key == searchKey { + value, err := strconv.ParseBool(searchVal) + if err != nil { + return err + } + + *setting = ValidBool{Value: value, Valid: true} + return nil + } + } - return setValidSetting(vsl.GetBoolSettingsMap(), searchKey, ValidBool{Value: value, Valid: true}) + + return ErrSettingNotFound } func (vsl *ValidSettingList) SetFloat32Setting(searchKey string, searchVal string) error { - value, err := strconv.ParseFloat(searchVal, 32) - if err != nil { - return err + for key, setting := range vsl.GetFloat32SettingsMap() { + if key == searchKey { + value, err := strconv.ParseFloat(searchVal, 32) + if err != nil { + return err + } + *setting = ValidFloat32{Value: float32(value), Valid: true} + return nil + } } - return setValidSetting(vsl.GetFloat32SettingsMap(), searchKey, ValidFloat32{Value: float32(value), Valid: true}) + + return ErrSettingNotFound } func (vsl *ValidSettingList) SetTimeSetting(searchKey string, searchVal string) error { - value, err := time.Parse(time.RFC3339, searchVal) - if err != nil { - return err + for key, setting := range vsl.GetTimeSettingsMap() { + if key == searchKey { + value, err := time.Parse(time.RFC3339, searchVal) + if err != nil { + return err + } + *setting = ValidTime{Value: value, Valid: true} + return nil + } } - return setValidSetting(vsl.GetTimeSettingsMap(), searchKey, ValidTime{Value: value, Valid: true}) + return ErrSettingNotFound } func (vsl *ValidSettingList) SetSetting(searchKey string, searchVal string) error { @@ -223,9 +391,10 @@ func (vsl *ValidSettingList) SetSetting(searchKey string, searchVal string) erro for _, setter := range setters { if err := setter(searchKey, searchVal); err != nil { if err == ErrSettingNotFound { + // fmt.Printf("setting is not found %v \n", searchKey) continue // not this setter, try the next } - return fmt.Errorf("error while processing setting %q: %w", searchKey, err) + return fmt.Errorf("error while processing setting %q: %w \n", searchKey, err) } return nil // successfully set } @@ -306,6 +475,7 @@ func validateSettings[T any]( var errs []string for key, s := range settings { if !customValidator(s) { + errs = append(errs, fmt.Sprintf("%v is invalid", key)) } } @@ -378,6 +548,7 @@ func (vsl *ValidSettingList) ValidateAllSettings() error { for _, validator := range validators { if err := validator(); err != nil { + errs = append(errs, err.Error()) } } @@ -410,12 +581,12 @@ func ConvertDBGlobalSettingList(settings []dbgen.GlobalSetting) (SettingList, er if err == ErrSettingNotFound { MongoDBLogger.Warn("unknown setting found on database", zap.String("setting", setting.Key)) } + MongoDBLogger.Error("unknown error while fetching settings", zap.Error(err)) } } if err := dbSettingList.ValidateAllSettings(); err != nil { - fmt.Printf("setting validation error: %v \n", err) - MongoDBLogger.Warn("setting validation error", zap.Error(err)) + MongoDBLogger.Warn("setting validation error", zap.Error(err), zap.Any("db_setting_list", dbSettingList)) return SettingList{}, err } @@ -436,7 +607,6 @@ func ConvertDBOverrideSettingList(settings []dbgen.GetOverrideSettingsRow) (Sett } if err := dbSettingList.ValidateAllSettings(); err != nil { - fmt.Printf("setting validation error: %v \n", err) MongoDBLogger.Warn("setting validation error", zap.Error(err)) return SettingList{}, err } diff --git a/internal/domain/transfer.go b/internal/domain/transfer.go index cf629c3..dd32c18 100644 --- a/internal/domain/transfer.go +++ b/internal/domain/transfer.go @@ -105,3 +105,10 @@ type CreateTransfer struct { Status string `json:"status"` CashierID ValidInt64 `json:"cashier_id"` } + +type TransferStats struct { + TotalTransfer int64 + TotalDeposits int64 + TotalWithdraws int64 + TotalWalletToWallet int64 +} diff --git a/internal/domain/user.go b/internal/domain/user.go index 194a89b..73920a5 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -23,7 +23,7 @@ type User struct { UpdatedAt time.Time SuspendedAt time.Time Suspended bool - CompanyID ValidInt64 //This should be null + CompanyID ValidInt64 } type UserFilter struct { @@ -36,7 +36,6 @@ type UserFilter struct { CreatedAfter ValidTime } - type RegisterUserReq struct { FirstName string LastName string @@ -65,6 +64,7 @@ type ResetPasswordReq struct { Password string Otp string OtpMedium OtpMedium + CompanyID int64 } type UpdateUserReq struct { UserId int64 diff --git a/internal/domain/validtypes.go b/internal/domain/validtypes.go index c20f6c3..99ad794 100644 --- a/internal/domain/validtypes.go +++ b/internal/domain/validtypes.go @@ -31,6 +31,7 @@ func (n *ValidInt64) UnmarshalJSON(data []byte) error { } v, err := strconv.ParseInt(s, 10, 64) if err != nil { + fmt.Printf("Failed to parse the value of %v \n\n", s) return err } n.Value, n.Valid = v, true @@ -42,7 +43,7 @@ func (n *ValidInt64) UnmarshalJSON(data []byte) error { n.Value, n.Valid = v, true return nil } - + fmt.Printf("Failed to parse the value of %v", s) return fmt.Errorf("invalid int64 value: %s", string(data)) } diff --git a/internal/domain/wallet.go b/internal/domain/wallet.go index aec3895..947f3c8 100644 --- a/internal/domain/wallet.go +++ b/internal/domain/wallet.go @@ -76,9 +76,10 @@ type CreateCustomerWallet struct { type WalletType string const ( - CustomerWalletType WalletType = "customer_wallet" - BranchWalletType WalletType = "branch_wallet" - CompanyWalletType WalletType = "company_wallet" + RegularWalletType WalletType = "regular_wallet" + StaticWalletType WalletType = "static_wallet" + BranchWalletType WalletType = "branch_wallet" + CompanyWalletType WalletType = "company_wallet" ) // domain/wallet.go @@ -92,18 +93,18 @@ const ( ) type DirectDeposit struct { - ID int64 - CustomerID int64 - WalletID int64 - Wallet Wallet // Joined data - Amount Currency - BankReference string - SenderAccount string - Status DirectDepositStatus - CreatedAt time.Time - VerifiedBy *int64 // Nullable - VerificationNotes string - VerifiedAt *time.Time // Nullable + ID int64 `json:"id"` + CustomerID int64 `json:"customer_id"` + WalletID int64 `json:"wallet_id"` + Wallet Wallet `json:"wallet"` + Amount Currency `json:"amount"` + BankReference string `json:"bank_reference"` + SenderAccount string `json:"sender_account"` + Status DirectDepositStatus `json:"status"` + CreatedAt time.Time `json:"created_at"` + VerifiedBy *int64 `json:"verified_by"` + VerificationNotes string `json:"verification_notes"` + VerifiedAt *time.Time `json:"verified_at"` } type CreateDirectDeposit struct { @@ -124,14 +125,14 @@ type UpdateDirectDeposit struct { } type DirectDepositRequest struct { - CustomerID int64 `json:"customer_id" binding:"required"` - Amount Currency `json:"amount" binding:"required,gt=0"` - BankReference string `json:"bank_reference" binding:"required"` - SenderAccount string `json:"sender_account" binding:"required"` + CustomerID int64 `json:"customer_id" binding:"required"` + Amount Currency `json:"amount" binding:"required,gt=0"` + BankReference string `json:"bank_reference" binding:"required"` + SenderAccount string `json:"sender_account" binding:"required"` } type VerifyDirectDepositRequest struct { - DepositID int64 `json:"deposit_id" binding:"required"` - IsVerified bool `json:"is_verified" binding:"required"` - Notes string `json:"notes"` + DepositID int64 `json:"deposit_id" binding:"required"` + IsVerified bool `json:"is_verified" binding:"required"` + Notes string `json:"notes"` } diff --git a/internal/pkgs/helpers/helpers.go b/internal/pkgs/helpers/helpers.go index d9be84a..cb4a4ec 100644 --- a/internal/pkgs/helpers/helpers.go +++ b/internal/pkgs/helpers/helpers.go @@ -1,10 +1,13 @@ package helpers import ( + random "crypto/rand" "fmt" - "math/rand/v2" + "strings" "github.com/google/uuid" + "math/big" + "math/rand/v2" ) func GenerateID() string { @@ -24,3 +27,34 @@ func GenerateFastCode() string { } return code } + +func GenerateCashoutID() (string, error) { + const chars = "abcdefghijklmnopqrstuvwxyz0123456789" + const length int = 13 + charLen := big.NewInt(int64(len(chars))) + result := make([]byte, length) + + for i := 0; i < length; i++ { + index, err := random.Int(random.Reader, charLen) + if err != nil { + return "", err + } + result[i] = chars[index.Int64()] + } + + return string(result), nil +} + +func MaskPhone(phone string) string { + if phone == "" { + return "" + } + return phone[:4] + "**" + phone[len(phone)-2:] +} + +func MaskEmail(email string) string { + if email == "" { + return "" + } + return email[:3] + "**" + email[strings.Index(email, "@"):] +} diff --git a/internal/repository/bet.go b/internal/repository/bet.go index b1d1e52..09a667b 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -10,6 +10,7 @@ import ( dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" "go.uber.org/zap" ) @@ -220,6 +221,46 @@ func (s *Store) UpdateStatus(ctx context.Context, id int64, status domain.Outcom return err } +func (s *Store) SettleWinningBet(ctx context.Context, betID int64, userID int64, amount domain.Currency, status domain.OutcomeStatus) error { + tx, err := s.conn.BeginTx(ctx, pgx.TxOptions{}) + if err != nil { + return err + } + qtx := s.queries.WithTx(tx) + + wallet, err := qtx.GetCustomerWallet(ctx, userID) + if err != nil { + tx.Rollback(ctx) + return err + } + + // 1. Update wallet + newAmount := wallet.RegularBalance + int64(amount) + if err := qtx.UpdateBalance(ctx, dbgen.UpdateBalanceParams{ + Balance: newAmount, + ID: wallet.RegularID, + }); err != nil { + tx.Rollback(ctx) + return err + } + + // 2. Update bet + if err := qtx.UpdateStatus(ctx, dbgen.UpdateStatusParams{ + Status: int32(status), + ID: betID, + }); err != nil { + tx.Rollback(ctx) + return err + } + + // 3. Commit both together + if err := tx.Commit(ctx); err != nil { + return err + } + + return nil +} + func (s *Store) GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error) { outcomes, err := s.queries.GetBetOutcomeByEventID(ctx, dbgen.GetBetOutcomeByEventIDParams{ diff --git a/internal/repository/bonus.go b/internal/repository/bonus.go index c4f57ac..6b95b3e 100644 --- a/internal/repository/bonus.go +++ b/internal/repository/bonus.go @@ -4,27 +4,80 @@ import ( "context" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) -func (s *Store) CreateBonusMultiplier(ctx context.Context, multiplier float32, balance_cap int64) error { - return s.queries.CreateBonusMultiplier(ctx, dbgen.CreateBonusMultiplierParams{ - Multiplier: multiplier, - BalanceCap: balance_cap, +func (s *Store) CreateUserBonus(ctx context.Context, bonus domain.CreateBonus) (domain.UserBonus, error) { + newBonus, err := s.queries.CreateUserBonus(ctx, domain.ConvertCreateBonus(bonus)) + + if err != nil { + return domain.UserBonus{}, err + } + + return domain.ConvertDBBonus(newBonus), nil +} + +func (s *Store) GetAllUserBonuses(ctx context.Context, filter domain.BonusFilter) ([]domain.UserBonus, error) { + bonuses, err := s.queries.GetAllUserBonuses(ctx, dbgen.GetAllUserBonusesParams{ + UserID: filter.UserID.ToPG(), + Offset: filter.Offset.ToPG(), + Limit: filter.Limit.ToPG(), }) + + if err != nil { + return nil, err + } + + return domain.ConvertDBBonuses(bonuses), nil } -func (s *Store) GetBonusMultiplier(ctx context.Context) ([]dbgen.GetBonusMultiplierRow, error) { - return s.queries.GetBonusMultiplier(ctx) +func (s *Store) GetBonusCount(ctx context.Context, filter domain.BonusFilter) (int64, error) { + count, err := s.queries.GetBonusCount(ctx, filter.UserID.ToPG()) + if err != nil { + return 0, err + } + return count, nil } -func (s *Store) GetBonusBalanceCap(ctx context.Context) ([]dbgen.GetBonusBalanceCapRow, error) { - return s.queries.GetBonusBalanceCap(ctx) +func (s *Store) GetBonusByID(ctx context.Context, bonusID int64) (domain.UserBonus, error) { + bonus, err := s.queries.GetUserBonusByID(ctx, bonusID) + if err != nil { + return domain.UserBonus{}, err + } + return domain.ConvertDBBonus(bonus), nil } -func (s *Store) UpdateBonusMultiplier(ctx context.Context, id int64, mulitplier float32, balance_cap int64) error { - return s.queries.UpdateBonusMultiplier(ctx, dbgen.UpdateBonusMultiplierParams{ - ID: id, - Multiplier: mulitplier, - BalanceCap: balance_cap, + + + +func (s *Store) GetBonusStats(ctx context.Context, filter domain.BonusFilter) (domain.BonusStats, error) { + bonus, err := s.queries.GetBonusStats(ctx, dbgen.GetBonusStatsParams{ + CompanyID: filter.CompanyID.ToPG(), + UserID: filter.UserID.ToPG(), }) + if err != nil { + return domain.BonusStats{}, err + } + return domain.ConvertDBBonusStats(bonus), nil +} + +func (s *Store) UpdateUserBonus(ctx context.Context, bonusID int64, IsClaimed bool) (error) { + err := s.queries.UpdateUserBonus(ctx, dbgen.UpdateUserBonusParams{ + ID: bonusID, + IsClaimed: IsClaimed, + }) + + if err != nil { + return err + } + return nil +} + + +func (s *Store) DeleteUserBonus(ctx context.Context, bonusID int64) (error) { + err := s.queries.DeleteUserBonus(ctx, bonusID) + if err != nil { + return err + } + return nil } diff --git a/internal/repository/company.go b/internal/repository/company.go index dc440e9..08f5251 100644 --- a/internal/repository/company.go +++ b/internal/repository/company.go @@ -2,12 +2,12 @@ package repository import ( "context" - "database/sql" "errors" "fmt" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) @@ -15,11 +15,11 @@ func (s *Store) CreateCompany(ctx context.Context, company domain.CreateCompany) baseSlug := helpers.GenerateSlug(company.Name) uniqueSlug := baseSlug i := 1 - + for { _, err := s.queries.GetCompanyIDUsingSlug(ctx, uniqueSlug) if err != nil { - if errors.Is(err, sql.ErrNoRows) { + if errors.Is(err, pgx.ErrNoRows) { // slug is unique break } else { diff --git a/internal/repository/event.go b/internal/repository/event.go index d0798d7..8b4e87e 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -16,10 +16,6 @@ func (s *Store) SaveEvent(ctx context.Context, e domain.CreateEvent) error { return s.queries.InsertEvent(ctx, domain.ConvertCreateEvent(e)) } -func (s *Store) InsertEventSettings(ctx context.Context, eventSetting domain.CreateEventSettings) error { - return s.queries.InsertEventSettings(ctx, domain.ConvertCreateEventSettings(eventSetting)) -} - func (s *Store) GetLiveEventIDs(ctx context.Context) ([]string, error) { return s.queries.ListLiveEvents(ctx) } @@ -89,6 +85,8 @@ func (s *Store) GetEventsWithSettings(ctx context.Context, companyID int64, filt FirstStartTime: filter.FirstStartTime.ToPG(), LastStartTime: filter.LastStartTime.ToPG(), CountryCode: filter.CountryCode.ToPG(), + IsFeatured: filter.Featured.ToPG(), + IsActive: filter.Active.ToPG(), }) if err != nil { @@ -103,13 +101,70 @@ func (s *Store) GetEventsWithSettings(ctx context.Context, companyID int64, filt FirstStartTime: filter.FirstStartTime.ToPG(), LastStartTime: filter.LastStartTime.ToPG(), CountryCode: filter.CountryCode.ToPG(), + IsFeatured: filter.Featured.ToPG(), + IsActive: filter.Active.ToPG(), }) if err != nil { return nil, 0, err } numberOfPages := math.Ceil(float64(totalCount) / float64(filter.Limit.Value)) - return domain.ConvertDBEventWithSettings(events), int64(numberOfPages), nil + + result := make([]domain.EventWithSettings, len(events)) + + for i, event := range events { + result[i] = domain.EventWithSettings{ + ID: event.ID, + SportID: event.SportID, + MatchName: event.MatchName, + HomeTeam: event.HomeTeam, + AwayTeam: event.AwayTeam, + HomeTeamID: event.HomeTeamID, + AwayTeamID: event.AwayTeamID, + HomeTeamImage: event.HomeKitImage, + AwayTeamImage: event.AwayKitImage, + LeagueID: event.LeagueID, + LeagueName: event.LeagueName, + LeagueCC: domain.ValidString{ + Value: event.LeagueCc.String, + Valid: event.LeagueCc.Valid, + }, + StartTime: event.StartTime.Time.UTC(), + Source: domain.EventSource(event.Source), + Status: domain.EventStatus(event.Status), + IsFeatured: event.IsFeatured, + IsMonitored: event.IsMonitored, + IsActive: event.IsActive, + DefaultIsFeatured: event.DefaultIsFeatured, + DefaultIsActive: event.DefaultIsActive, + DefaultWinningUpperLimit: event.DefaultWinningUpperLimit, + Score: domain.ValidString{ + Value: event.Score.String, + Valid: event.Score.Valid, + }, + MatchMinute: domain.ValidInt{ + Value: int(event.MatchMinute.Int32), + Valid: event.MatchMinute.Valid, + }, + TimerStatus: domain.ValidString{ + Value: event.TimerStatus.String, + Valid: event.TimerStatus.Valid, + }, + AddedTime: domain.ValidInt{ + Value: int(event.AddedTime.Int32), + Valid: event.AddedTime.Valid, + }, + MatchPeriod: domain.ValidInt{ + Value: int(event.MatchPeriod.Int32), + Valid: event.MatchPeriod.Valid, + }, + IsLive: event.IsLive, + UpdatedAt: event.UpdatedAt.Time, + FetchedAt: event.FetchedAt.Time, + } + } + + return result, int64(numberOfPages), nil } func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.BaseEvent, error) { event, err := s.queries.GetUpcomingByID(ctx, ID) @@ -121,14 +176,63 @@ func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.Bas } func (s *Store) GetEventWithSettingByID(ctx context.Context, ID string, companyID int64) (domain.EventWithSettings, error) { event, err := s.queries.GetEventWithSettingByID(ctx, dbgen.GetEventWithSettingByIDParams{ - ID: ID, + ID: ID, CompanyID: companyID, }) if err != nil { return domain.EventWithSettings{}, err } - return domain.ConvertDBEventWithSetting(event), nil + res := domain.EventWithSettings{ + ID: event.ID, + SportID: event.SportID, + MatchName: event.MatchName, + HomeTeam: event.HomeTeam, + AwayTeam: event.AwayTeam, + HomeTeamID: event.HomeTeamID, + AwayTeamID: event.AwayTeamID, + HomeTeamImage: event.HomeKitImage, + AwayTeamImage: event.AwayKitImage, + LeagueID: event.LeagueID, + LeagueName: event.LeagueName, + LeagueCC: domain.ValidString{ + Value: event.LeagueCc.String, + Valid: event.LeagueCc.Valid, + }, + StartTime: event.StartTime.Time.UTC(), + Source: domain.EventSource(event.Source), + Status: domain.EventStatus(event.Status), + IsFeatured: event.IsFeatured, + IsMonitored: event.IsMonitored, + IsActive: event.IsActive, + DefaultIsFeatured: event.DefaultIsFeatured, + DefaultIsActive: event.DefaultIsActive, + DefaultWinningUpperLimit: event.DefaultWinningUpperLimit, + Score: domain.ValidString{ + Value: event.Score.String, + Valid: event.Score.Valid, + }, + MatchMinute: domain.ValidInt{ + Value: int(event.MatchMinute.Int32), + Valid: event.MatchMinute.Valid, + }, + TimerStatus: domain.ValidString{ + Value: event.TimerStatus.String, + Valid: event.TimerStatus.Valid, + }, + AddedTime: domain.ValidInt{ + Value: int(event.AddedTime.Int32), + Valid: event.AddedTime.Valid, + }, + MatchPeriod: domain.ValidInt{ + Value: int(event.MatchPeriod.Int32), + Valid: event.MatchPeriod.Valid, + }, + IsLive: event.IsLive, + UpdatedAt: event.UpdatedAt.Time, + FetchedAt: event.FetchedAt.Time, + } + return res, nil } func (s *Store) UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error { params := dbgen.UpdateMatchResultParams{ @@ -176,7 +280,7 @@ func (s *Store) UpdateEventMonitored(ctx context.Context, eventID string, IsMoni } func (s *Store) UpdateEventSettings(ctx context.Context, event domain.CreateEventSettings) error { - return s.queries.UpdateEventSettings(ctx, domain.ConvertUpdateEventSettings(event)) + return s.queries.SaveEventSettings(ctx, domain.ConvertUpdateEventSettings(event)) } func (s *Store) DeleteEvent(ctx context.Context, eventID string) error { @@ -186,3 +290,13 @@ func (s *Store) DeleteEvent(ctx context.Context, eventID string) error { } return nil } + +func (s *Store) GetSportAndLeagueIDs(ctx context.Context, eventID string) ([]int64, error) { + sportAndLeagueIDs, err := s.queries.GetSportAndLeagueIDs(ctx, eventID) + if err != nil { + return nil, err + } + + res := []int64{int64(sportAndLeagueIDs.SportID), sportAndLeagueIDs.LeagueID} + return res, err +} diff --git a/internal/repository/league.go b/internal/repository/league.go index f003fd9..ae0a4d5 100644 --- a/internal/repository/league.go +++ b/internal/repository/league.go @@ -18,6 +18,7 @@ func (s *Store) SaveLeagueSettings(ctx context.Context, leagueSettings domain.Cr func (s *Store) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.BaseLeague, error) { l, err := s.queries.GetAllLeagues(ctx, dbgen.GetAllLeaguesParams{ + Query: filter.Query.ToPG(), CountryCode: filter.CountryCode.ToPG(), SportID: filter.SportID.ToPG(), Limit: pgtype.Int4{ @@ -36,8 +37,10 @@ func (s *Store) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ( return domain.ConvertDBBaseLeagues(l), nil } -func (s *Store) GetAllLeaguesByCompany(ctx context.Context, companyID int64, filter domain.LeagueFilter) ([]domain.LeagueWithSettings, error) { +func (s *Store) GetAllLeaguesByCompany(ctx context.Context, companyID int64, filter domain.LeagueFilter) ([]domain.LeagueWithSettings, int64, error) { + l, err := s.queries.GetAllLeaguesWithSettings(ctx, dbgen.GetAllLeaguesWithSettingsParams{ + Query: filter.Query.ToPG(), CompanyID: companyID, CountryCode: filter.CountryCode.ToPG(), SportID: filter.SportID.ToPG(), @@ -49,13 +52,27 @@ func (s *Store) GetAllLeaguesByCompany(ctx context.Context, companyID int64, fil Int32: int32(filter.Offset.Value * filter.Limit.Value), Valid: filter.Offset.Valid, }, + IsFeatured: filter.IsFeatured.ToPG(), + IsActive: filter.IsActive.ToPG(), }) if err != nil { - return nil, err + return nil, 0, err } - return domain.ConvertDBLeagueWithSettings(l), nil + total, err := s.queries.GetTotalLeaguesWithSettings(ctx, dbgen.GetTotalLeaguesWithSettingsParams{ + Query: filter.Query.ToPG(), + CompanyID: companyID, + CountryCode: filter.CountryCode.ToPG(), + SportID: filter.SportID.ToPG(), + IsFeatured: filter.IsFeatured.ToPG(), + IsActive: filter.IsActive.ToPG(), + }) + + if err != nil { + return nil, 0, err + } + return domain.ConvertDBLeagueWithSettings(l), total, nil } func (s *Store) CheckLeagueSupport(ctx context.Context, leagueID int64, companyID int64) (bool, error) { diff --git a/internal/repository/notification.go b/internal/repository/notification.go index 1034bfc..29e7b8c 100644 --- a/internal/repository/notification.go +++ b/internal/repository/notification.go @@ -39,8 +39,8 @@ func (s *Store) DisconnectWebSocket(recipientID int64) { func (r *Repository) CreateNotification(ctx context.Context, notification *domain.Notification) (*domain.Notification, error) { var errorSeverity pgtype.Text - if notification.ErrorSeverity != nil { - errorSeverity.String = string(*notification.ErrorSeverity) + if notification.ErrorSeverity != "" { + errorSeverity.String = string(notification.ErrorSeverity) errorSeverity.Valid = true } @@ -155,10 +155,12 @@ func (r *Repository) ListRecipientIDs(ctx context.Context, receiver domain.Notif } func (r *Repository) mapDBToDomain(dbNotif *dbgen.Notification) *domain.Notification { - var errorSeverity *domain.NotificationErrorSeverity + var errorSeverity domain.NotificationErrorSeverity if dbNotif.ErrorSeverity.Valid { - s := domain.NotificationErrorSeverity(dbNotif.ErrorSeverity.String) - errorSeverity = &s + errorSeverity = domain.NotificationErrorSeverity(dbNotif.ErrorSeverity.String) + + } else { + errorSeverity = "" } var deliveryChannel domain.DeliveryChannel @@ -317,8 +319,6 @@ func (s *Store) CountUnreadNotifications(ctx context.Context, userID int64) (int return count, 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/odds.go b/internal/repository/odds.go index cb684ce..3e09a91 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -88,13 +88,51 @@ func (s *Store) GetAllOddsWithSettings(ctx context.Context, companyID int64, fil return nil, err } - domainOdds, err := domain.ConvertDBOddMarketWithSettings(odds) + // domainOdds, err := domain.ConvertDBOddMarketWithSettings(odds) + // if err != nil { + // return nil, err + // } - if err != nil { - return nil, err + result := make([]domain.OddMarketWithSettings, len(odds)) + for i, o := range odds { + var rawOdds []json.RawMessage + if len(o.RawOdds) > 0 { + if err := json.Unmarshal(o.RawOdds, &rawOdds); err != nil { + return nil, err + } + } else { + rawOdds = []json.RawMessage{} // explicit empty slice + } + + result[i] = domain.OddMarketWithSettings{ + ID: o.ID, + EventID: o.EventID, + MarketType: o.MarketType, + MarketName: o.MarketName, + MarketCategory: o.MarketCategory, + MarketID: o.MarketID, + RawOdds: rawOdds, + FetchedAt: o.FetchedAt.Time, + ExpiresAt: o.ExpiresAt.Time, + IsActive: o.IsActive, + } } - return domainOdds, nil + return result, nil +} + +func (s *Store) GetOddByID(ctx context.Context, id int64) (domain.OddMarket, error) { + odd, err := s.queries.GetOddByID(ctx, id) + if err != nil { + return domain.OddMarket{}, err + } + + convertedOdd, err := domain.ConvertDBOddMarket(odd) + + if err != nil { + return domain.OddMarket{}, err + } + return convertedOdd, nil } func (s *Store) GetOddsByMarketID(ctx context.Context, marketID string, eventID string) (domain.OddMarket, error) { @@ -126,12 +164,76 @@ func (s *Store) GetOddsWithSettingsByMarketID(ctx context.Context, marketID stri return domain.OddMarketWithSettings{}, err } - convertedOdd, err := domain.ConvertDBOddMarketWithSetting(odds) + // convertedOdd, err := domain.ConvertDBOddMarketWithSetting(odds) + + // if err != nil { + // return domain.OddMarketWithSettings{}, err + // } + + var rawOdds []json.RawMessage + if len(odds.RawOdds) > 0 { + if err := json.Unmarshal(odds.RawOdds, &rawOdds); err != nil { + return domain.OddMarketWithSettings{}, err + } + } else { + rawOdds = []json.RawMessage{} // explicit empty slice + } + + converted := domain.OddMarketWithSettings{ + ID: odds.ID, + EventID: odds.EventID, + MarketType: odds.MarketType, + MarketName: odds.MarketName, + MarketCategory: odds.MarketCategory, + MarketID: odds.MarketID, + RawOdds: rawOdds, + FetchedAt: odds.FetchedAt.Time, + ExpiresAt: odds.ExpiresAt.Time, + IsActive: odds.IsActive, + } + return converted, nil +} + +func (s *Store) GetOddsWithSettingsByID(ctx context.Context, ID int64, companyID int64) (domain.OddMarketWithSettings, error) { + + odds, err := s.queries.GetOddsWithSettingsByID(ctx, dbgen.GetOddsWithSettingsByIDParams{ + ID: ID, + CompanyID: companyID, + }) if err != nil { return domain.OddMarketWithSettings{}, err } - return convertedOdd, nil + + // convertedOdd, err := domain.ConvertDBOddMarketWithSetting(odds) + + // if err != nil { + // return domain.OddMarketWithSettings{}, err + // } + + var rawOdds []json.RawMessage + if len(odds.RawOdds) > 0 { + if err := json.Unmarshal(odds.RawOdds, &rawOdds); err != nil { + return domain.OddMarketWithSettings{}, err + } + } else { + rawOdds = []json.RawMessage{} // explicit empty slice + } + + converted := domain.OddMarketWithSettings{ + ID: odds.ID, + EventID: odds.EventID, + MarketType: odds.MarketType, + MarketName: odds.MarketName, + MarketCategory: odds.MarketCategory, + MarketID: odds.MarketID, + RawOdds: rawOdds, + FetchedAt: odds.FetchedAt.Time, + ExpiresAt: odds.ExpiresAt.Time, + IsActive: odds.IsActive, + } + + return converted, nil } func (s *Store) GetOddsByEventID(ctx context.Context, upcomingID string, filter domain.OddMarketWithEventFilter) ([]domain.OddMarket, error) { @@ -174,14 +276,49 @@ func (s *Store) GetOddsWithSettingsByEventID(ctx context.Context, upcomingID str } // Map the results to domain.Odd - domainOdds, err := domain.ConvertDBOddMarketWithSettings(odds) - if err != nil { - return nil, err + // domainOdds, err := domain.ConvertDBOddMarketWithSettings(odds) + // if err != nil { + // return nil, err + // } + + result := make([]domain.OddMarketWithSettings, len(odds)) + for i, o := range odds { + var rawOdds []json.RawMessage + if len(o.RawOdds) > 0 { + if err := json.Unmarshal(o.RawOdds, &rawOdds); err != nil { + return nil, err + } + } else { + rawOdds = []json.RawMessage{} // explicit empty slice + } + + result[i] = domain.OddMarketWithSettings{ + ID: o.ID, + EventID: o.EventID, + MarketType: o.MarketType, + MarketName: o.MarketName, + MarketCategory: o.MarketCategory, + MarketID: o.MarketID, + RawOdds: rawOdds, + FetchedAt: o.FetchedAt.Time, + ExpiresAt: o.ExpiresAt.Time, + IsActive: o.IsActive, + } } - return domainOdds, nil + return result, nil } func (s *Store) DeleteOddsForEvent(ctx context.Context, eventID string) error { return s.queries.DeleteOddsForEvent(ctx, eventID) } + +func (s *Store) SaveOddsSetting(ctx context.Context, odd domain.CreateOddMarketSettings) error { + + res, err := domain.ConvertCreateOddMarketSetting(odd) + + if err != nil { + return nil + } + return s.queries.SaveOddSettings(ctx, res) +} diff --git a/internal/repository/raffel.go b/internal/repository/raffel.go new file mode 100644 index 0000000..6e37013 --- /dev/null +++ b/internal/repository/raffel.go @@ -0,0 +1,193 @@ +package repository + +import ( + "context" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/jackc/pgx/v5/pgtype" +) + +func convertRaffleOutcome(raffle dbgen.Raffle) domain.Raffle { + return domain.Raffle{ + ID: raffle.ID, + CompanyID: raffle.CompanyID, + Name: raffle.Name, + CreatedAt: raffle.CreatedAt.Time, + ExpiresAt: raffle.ExpiresAt.Time, + Type: raffle.Type, + Status: raffle.Status, + } +} + +func convertRaffleTicketOutcome(raffle dbgen.RaffleTicket) domain.RaffleTicket { + return domain.RaffleTicket{ + ID: raffle.ID, + RaffleID: raffle.RaffleID, + UserID: raffle.UserID, + IsActive: raffle.IsActive.Bool, + } +} + +func convertJoinedRaffleTicketOutcome(raffle dbgen.GetUserRaffleTicketsRow) domain.RaffleTicketRes { + return domain.RaffleTicketRes{ + TicketID: raffle.TicketID, + UserID: raffle.UserID, + Name: raffle.Name, + Type: raffle.Type, + ExpiresAt: raffle.ExpiresAt.Time, + Status: raffle.Status, + } +} + +func convertCreateRaffle(raffle domain.CreateRaffle) dbgen.CreateRaffleParams { + return dbgen.CreateRaffleParams{ + CompanyID: raffle.CompanyID, + Name: raffle.Name, + ExpiresAt: pgtype.Timestamp{ + Time: *raffle.ExpiresAt, + Valid: true, + }, + Type: raffle.Type, + } +} + +func convertRaffleStanding(raffleStanding dbgen.GetRaffleStandingRow) domain.RaffleStanding { + return domain.RaffleStanding{ + UserID: raffleStanding.UserID, + RaffleID: raffleStanding.RaffleID, + FirstName: raffleStanding.FirstName, + LastName: raffleStanding.LastName, + PhoneNumber: raffleStanding.PhoneNumber.String, + Email: raffleStanding.Email.String, + TicketCount: raffleStanding.TicketCount, + } +} + +func (s *Store) CreateRaffle(ctx context.Context, raffle domain.CreateRaffle) (domain.Raffle, error) { + raffleRes, err := s.queries.CreateRaffle(ctx, convertCreateRaffle(raffle)) + if err != nil { + return domain.Raffle{}, err + } + + return convertRaffleOutcome(raffleRes), nil +} + +func (s *Store) DeleteRaffle(ctx context.Context, raffleID int32) (domain.Raffle, error) { + raffleRes, err := s.queries.DeleteRaffle(ctx, raffleID) + if err != nil { + return domain.Raffle{}, err + } + + return convertRaffleOutcome(raffleRes), nil +} + +func (s *Store) GetRafflesOfCompany(ctx context.Context, companyID int32) ([]dbgen.Raffle, error) { + raffles, err := s.queries.GetRafflesOfCompany(ctx, companyID) + if err != nil { + return nil, err + } + + return raffles, nil +} + +func (s *Store) CreateRaffleTicket(ctx context.Context, raffleTicketParams domain.CreateRaffleTicket) (domain.RaffleTicket, error) { + raffleTicket, err := s.queries.CreateRaffleTicket(ctx, dbgen.CreateRaffleTicketParams{ + RaffleID: raffleTicketParams.RaffleID, + UserID: raffleTicketParams.UserID, + }) + if err != nil { + return domain.RaffleTicket{}, err + } + + return convertRaffleTicketOutcome(raffleTicket), nil +} + +func (s *Store) GetUserRaffleTickets(ctx context.Context, userID int32) ([]domain.RaffleTicketRes, error) { + raffleTickets, err := s.queries.GetUserRaffleTickets(ctx, userID) + if err != nil { + return nil, err + } + + res := []domain.RaffleTicketRes{} + for _, raffle := range raffleTickets { + res = append(res, convertJoinedRaffleTicketOutcome(raffle)) + } + + return res, nil +} + +func (s *Store) SuspendRaffleTicket(ctx context.Context, raffleTicketID int32) error { + return s.queries.UpdateRaffleTicketStatus(ctx, dbgen.UpdateRaffleTicketStatusParams{ + ID: raffleTicketID, + IsActive: pgtype.Bool{ + Bool: false, + Valid: true, + }, + }) +} + +func (s *Store) UnSuspendRaffleTicket(ctx context.Context, raffleID int32) error { + return s.queries.UpdateRaffleTicketStatus(ctx, dbgen.UpdateRaffleTicketStatusParams{ + ID: raffleID, + IsActive: pgtype.Bool{ + Bool: true, + Valid: true, + }, + }) +} + +// TODO: could also add -> suspend a specific user's raffle tickets + +func (s *Store) GetRaffleStanding(ctx context.Context, raffleID, limit int32) ([]domain.RaffleStanding, error) { + raffleStanding, err := s.queries.GetRaffleStanding(ctx, dbgen.GetRaffleStandingParams{ + RaffleID: raffleID, + Limit: limit, + }) + if err != nil { + return nil, err + } + + res := []domain.RaffleStanding{} + for _, standing := range raffleStanding { + res = append(res, convertRaffleStanding(standing)) + } + + return res, nil +} + +func (s *Store) CreateRaffleWinner(ctx context.Context, raffleWinnerParams domain.RaffleWinnerParams) error { + _, err := s.queries.CreateRaffleWinner(ctx, dbgen.CreateRaffleWinnerParams{ + RaffleID: raffleWinnerParams.RaffleID, + UserID: raffleWinnerParams.UserID, + Rank: raffleWinnerParams.Rank, + }) + + return err +} + +func (s *Store) SetRaffleComplete(ctx context.Context, raffleID int32) error { + return s.queries.SetRaffleComplete(ctx, raffleID) +} + +func (s *Store) AddSportRaffleFilter(ctx context.Context, raffleID int32, sportID, leagueID int64) error { + _, err := s.queries.AddSportRaffleFilter(ctx, dbgen.AddSportRaffleFilterParams{ + RaffleID: raffleID, + SportID: sportID, + LeagueID: leagueID, + }) + return err +} + +func (s *Store) CheckValidSportRaffleFilter(ctx context.Context, raffleID int32, sportID, leagueID int64) (bool, error) { + res, err := s.queries.CheckValidSportRaffleFilter(ctx, dbgen.CheckValidSportRaffleFilterParams{ + RaffleID: raffleID, + SportID: sportID, + LeagueID: leagueID, + }) + if err != nil { + return false, err + } + + return res, nil +} diff --git a/internal/repository/referal.go b/internal/repository/referal.go index d214c54..cda22e2 100644 --- a/internal/repository/referal.go +++ b/internal/repository/referal.go @@ -2,28 +2,21 @@ package repository import ( "context" - "database/sql" - "errors" - "fmt" - "strconv" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - "github.com/jackc/pgx/v5/pgtype" ) type ReferralRepository interface { - CreateReferral(ctx context.Context, referral *domain.Referral) error - GetReferralByCode(ctx context.Context, code string) (*domain.Referral, error) - UpdateReferral(ctx context.Context, referral *domain.Referral) error - GetReferralStats(ctx context.Context, userID string) (*domain.ReferralStats, error) - GetSettings(ctx context.Context) (*domain.ReferralSettings, error) - UpdateSettings(ctx context.Context, settings *domain.ReferralSettings) error - CreateSettings(ctx context.Context, settings *domain.ReferralSettings) error - GetReferralByReferredID(ctx context.Context, referredID string) (*domain.Referral, error) // New method - GetReferralCountByID(ctx context.Context, referrerID string) (int64, error) - GetActiveReferralByReferrerID(ctx context.Context, referrerID string) (*domain.Referral, error) - UpdateUserReferalCode(ctx context.Context, codedata domain.UpdateUserReferalCode) error + CreateReferralCode(ctx context.Context, referralCode domain.CreateReferralCode) (domain.ReferralCode, error) + CreateUserReferral(ctx context.Context, referral domain.CreateUserReferrals) (domain.UserReferral, error) + GetReferralCodesByUser(ctx context.Context, userID int64) ([]domain.ReferralCode, error) + GetReferralCode(ctx context.Context, code string) (domain.ReferralCode, error) + UpdateReferralCode(ctx context.Context, referral domain.UpdateReferralCode) error + GetReferralStats(ctx context.Context, userID int64, companyID int64) (domain.ReferralStats, error) + GetUserReferral(ctx context.Context, referredID int64) (domain.UserReferral, error) + GetUserReferralsByCode(ctx context.Context, code string) ([]domain.UserReferral, error) + GetUserReferralCount(ctx context.Context, referrerID int64) (int64, error) } type ReferralRepo struct { @@ -34,248 +27,159 @@ func NewReferralRepository(store *Store) ReferralRepository { return &ReferralRepo{store: store} } -func (r *ReferralRepo) UpdateUserReferalCode(ctx context.Context, codedata domain.UpdateUserReferalCode) error { - params := dbgen.UpdateReferralCodeParams{ - ID: codedata.UserID, - ReferralCode: pgtype.Text{ - String: codedata.Code, - Valid: true, - }, - } +func (r *ReferralRepo) CreateReferralCode(ctx context.Context, referralCode domain.CreateReferralCode) (domain.ReferralCode, error) { + newReferralCode, err := r.store.queries.CreateReferralCode(ctx, domain.ConvertCreateReferralCode(referralCode)) - return r.store.queries.UpdateReferralCode(ctx, params) -} - -func (r *ReferralRepo) CreateReferral(ctx context.Context, referral *domain.Referral) error { - rewardAmount := pgtype.Numeric{} - if err := rewardAmount.Scan(strconv.Itoa(int(referral.RewardAmount))); err != nil { - return err - } - - params := dbgen.CreateReferralParams{ - ReferralCode: referral.ReferralCode, - ReferrerID: referral.ReferrerID, - Status: dbgen.Referralstatus(referral.Status), - RewardAmount: rewardAmount, - ExpiresAt: pgtype.Timestamptz{Time: referral.ExpiresAt, Valid: true}, - } - - _, err := r.store.queries.CreateReferral(ctx, params) - return err -} - -func (r *ReferralRepo) GetReferralByCode(ctx context.Context, code string) (*domain.Referral, error) { - dbReferral, err := r.store.queries.GetReferralByCode(ctx, code) if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, nil - } - return nil, err + return domain.ReferralCode{}, err } - return r.mapToDomainReferral(&dbReferral), nil + return domain.ConvertDBReferralCode(newReferralCode), nil } -func (r *ReferralRepo) UpdateReferral(ctx context.Context, referral *domain.Referral) error { - var referredID pgtype.Text - if referral.ReferredID != nil { - referredID = pgtype.Text{String: *referral.ReferredID, Valid: true} +func (r *ReferralRepo) CreateUserReferral(ctx context.Context, referral domain.CreateUserReferrals) (domain.UserReferral, error) { + newReferral, err := r.store.queries.CreateUserReferral(ctx, domain.ConvertCreateUserReferral(referral)) + + if err != nil { + return domain.UserReferral{}, err } - params := dbgen.UpdateReferralParams{ - ID: referral.ID, - ReferredID: referredID, - Status: dbgen.Referralstatus(referral.Status), - } - - _, err := r.store.queries.UpdateReferral(ctx, params) - return err + return domain.ConvertDBUserReferral(newReferral), nil } -func (r *ReferralRepo) GetReferralStats(ctx context.Context, userID string) (*domain.ReferralStats, error) { - stats, err := r.store.queries.GetReferralStats(ctx, userID) +func (r *ReferralRepo) GetReferralCodesByUser(ctx context.Context, userID int64) ([]domain.ReferralCode, error) { + codes, err := r.store.queries.GetReferralCodeByUser(ctx, userID) + if err != nil { return nil, err } - return &domain.ReferralStats{ - TotalReferrals: int(stats.TotalReferrals), - CompletedReferrals: int(stats.CompletedReferrals), - TotalRewardEarned: stats.TotalRewardEarned.(float64), - PendingRewards: stats.PendingRewards.(float64), - }, nil + return domain.ConvertDBReferralCodes(codes), nil } -func (r *ReferralRepo) GetSettings(ctx context.Context) (*domain.ReferralSettings, error) { - settings, err := r.store.queries.GetReferralSettings(ctx) +func (r *ReferralRepo) GetReferralCode(ctx context.Context, code string) (domain.ReferralCode, error) { + referralCode, err := r.store.queries.GetReferralCode(ctx, code) + + if err != nil { + return domain.ReferralCode{}, err + } + + return domain.ConvertDBReferralCode(referralCode), nil + +} +func (r *ReferralRepo) UpdateReferralCode(ctx context.Context, referral domain.UpdateReferralCode) error { + err := r.store.queries.UpdateReferralCode(ctx, domain.ConvertUpdateReferralCode(referral)) + + if err != nil { + return err + } + + return nil +} + +func (r *ReferralRepo) GetReferralStats(ctx context.Context, userID int64, companyID int64) (domain.ReferralStats, error) { + stats, err := r.store.queries.GetReferralStats(ctx, dbgen.GetReferralStatsParams{ + ReferrerID: userID, + CompanyID: companyID, + }) + if err != nil { + return domain.ReferralStats{}, err + } + + return domain.ConvertDBReferralStats(stats), nil +} + +func (r *ReferralRepo) GetUserReferral(ctx context.Context, referredID int64) (domain.UserReferral, error) { + dbReferral, err := r.store.queries.GetUserReferral(ctx, referredID) + if err != nil { + return domain.UserReferral{}, err + } + return domain.ConvertDBUserReferral(dbReferral), nil +} + +func (r *ReferralRepo) GetUserReferralsByCode(ctx context.Context, code string) ([]domain.UserReferral, error) { + dbReferrals, err := r.store.queries.GetUserReferralsByCode(ctx, code) + if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, nil - } return nil, err } - return r.mapToDomainSettings(&settings), nil + + return domain.ConvertDBUserReferrals(dbReferrals), nil } -func (r *ReferralRepo) UpdateSettings(ctx context.Context, settings *domain.ReferralSettings) error { - rewardAmount := pgtype.Numeric{} - if err := rewardAmount.Scan(settings.ReferralRewardAmount); err != nil { - return err - } - - cashbackPercentage := pgtype.Numeric{} - if err := cashbackPercentage.Scan(settings.CashbackPercentage); err != nil { - return err - } - - betReferralBonusPercentage := pgtype.Numeric{} - if err := betReferralBonusPercentage.Scan(settings.BetReferralBonusPercentage); err != nil { - return err - } - - params := dbgen.UpdateReferralSettingsParams{ - ID: settings.ID, - ReferralRewardAmount: rewardAmount, - CashbackPercentage: cashbackPercentage, - BetReferralBonusPercentage: betReferralBonusPercentage, // New field - MaxReferrals: settings.MaxReferrals, - ExpiresAfterDays: settings.ExpiresAfterDays, - UpdatedBy: settings.UpdatedBy, - } - - _, err := r.store.queries.UpdateReferralSettings(ctx, params) - return err -} - -func (r *ReferralRepo) CreateSettings(ctx context.Context, settings *domain.ReferralSettings) error { - rewardAmount := pgtype.Numeric{} - if err := rewardAmount.Scan(fmt.Sprintf("%f", settings.ReferralRewardAmount)); err != nil { - return err - } - - cashbackPercentage := pgtype.Numeric{} - if err := cashbackPercentage.Scan(fmt.Sprintf("%f", settings.CashbackPercentage)); err != nil { - return err - } - - betReferralBonusPercentage := pgtype.Numeric{} - if err := betReferralBonusPercentage.Scan(fmt.Sprintf("%f", settings.BetReferralBonusPercentage)); err != nil { - return err - } - - params := dbgen.CreateReferralSettingsParams{ - ReferralRewardAmount: rewardAmount, - CashbackPercentage: cashbackPercentage, - BetReferralBonusPercentage: betReferralBonusPercentage, // New field - MaxReferrals: settings.MaxReferrals, - ExpiresAfterDays: settings.ExpiresAfterDays, - UpdatedBy: settings.UpdatedBy, - } - - _, err := r.store.queries.CreateReferralSettings(ctx, params) - return err -} - -func (r *ReferralRepo) GetReferralByReferredID(ctx context.Context, referredID string) (*domain.Referral, error) { - dbReferral, err := r.store.queries.GetReferralByReferredID(ctx, pgtype.Text{String: referredID, Valid: true}) +func (r *ReferralRepo) GetUserReferralCount(ctx context.Context, referrerID int64) (int64, error) { + count, err := r.store.queries.GetUserReferralsCount(ctx, referrerID) if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, nil - } - return nil, err - } - return r.mapToDomainReferral(&dbReferral), nil -} - -func (r *ReferralRepo) GetReferralCountByID(ctx context.Context, referrerID string) (int64, error) { - count, err := r.store.queries.GetReferralCountByID(ctx, referrerID) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return 0, nil - } return 0, err } - return count, nil } -func (r *ReferralRepo) GetActiveReferralByReferrerID(ctx context.Context, referrerID string) (*domain.Referral, error) { - referral, err := r.store.queries.GetActiveReferralByReferrerID(ctx, referrerID) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return &domain.Referral{}, nil - } - return &domain.Referral{}, err - } +// func (r *ReferralRepo) mapToDomainReferral(dbRef *dbgen.Referral) *domain.Referral { +// var referredID *int64 +// if dbRef.ReferredID.Valid { +// referredID = &dbRef.ReferredID.Int64 +// } - return r.mapToDomainReferral(&referral), nil -} +// rewardAmount := 0.0 +// if dbRef.RewardAmount.Valid { +// if f8, err := dbRef.RewardAmount.Float64Value(); err == nil { +// rewardAmount = f8.Float64 +// } +// } -func (r *ReferralRepo) mapToDomainReferral(dbRef *dbgen.Referral) *domain.Referral { - var referredID *string - if dbRef.ReferredID.Valid { - referredID = &dbRef.ReferredID.String - } +// cashbackAmount := 0.0 +// if dbRef.CashbackAmount.Valid { +// if f8, err := dbRef.CashbackAmount.Float64Value(); err == nil { +// cashbackAmount = f8.Float64 +// } +// } - rewardAmount := 0.0 - if dbRef.RewardAmount.Valid { - if f8, err := dbRef.RewardAmount.Float64Value(); err == nil { - rewardAmount = f8.Float64 - } - } +// return &domain.Referral{ +// ID: dbRef.ID, +// ReferralCode: dbRef.ReferralCode, +// ReferrerID: dbRef.ReferrerID, +// ReferredID: referredID, +// Status: domain.ReferralStatus(dbRef.Status), +// RewardAmount: rewardAmount, +// CashbackAmount: cashbackAmount, +// CreatedAt: dbRef.CreatedAt.Time, +// UpdatedAt: dbRef.UpdatedAt.Time, +// ExpiresAt: dbRef.ExpiresAt.Time, +// } +// } - cashbackAmount := 0.0 - if dbRef.CashbackAmount.Valid { - if f8, err := dbRef.CashbackAmount.Float64Value(); err == nil { - cashbackAmount = f8.Float64 - } - } +// func (r *ReferralRepo) mapToDomainSettings(dbSettings *dbgen.ReferralSetting) *domain.ReferralSettings { +// rewardAmount := 0.0 +// if dbSettings.ReferralRewardAmount.Valid { +// if f8, err := dbSettings.ReferralRewardAmount.Float64Value(); err == nil { +// rewardAmount = f8.Float64 +// } +// } - return &domain.Referral{ - ID: dbRef.ID, - ReferralCode: dbRef.ReferralCode, - ReferrerID: dbRef.ReferrerID, - ReferredID: referredID, - Status: domain.ReferralStatus(dbRef.Status), - RewardAmount: rewardAmount, - CashbackAmount: cashbackAmount, - CreatedAt: dbRef.CreatedAt.Time, - UpdatedAt: dbRef.UpdatedAt.Time, - ExpiresAt: dbRef.ExpiresAt.Time, - } -} +// cashbackPercentage := 0.0 +// if dbSettings.CashbackPercentage.Valid { +// if f8, err := dbSettings.CashbackPercentage.Float64Value(); err == nil { +// cashbackPercentage = f8.Float64 +// } +// } -func (r *ReferralRepo) mapToDomainSettings(dbSettings *dbgen.ReferralSetting) *domain.ReferralSettings { - rewardAmount := 0.0 - if dbSettings.ReferralRewardAmount.Valid { - if f8, err := dbSettings.ReferralRewardAmount.Float64Value(); err == nil { - rewardAmount = f8.Float64 - } - } +// betReferralBonusPercentage := 0.0 +// if dbSettings.BetReferralBonusPercentage.Valid { +// if f8, err := dbSettings.BetReferralBonusPercentage.Float64Value(); err == nil { +// betReferralBonusPercentage = f8.Float64 +// } +// } - cashbackPercentage := 0.0 - if dbSettings.CashbackPercentage.Valid { - if f8, err := dbSettings.CashbackPercentage.Float64Value(); err == nil { - cashbackPercentage = f8.Float64 - } - } - - betReferralBonusPercentage := 0.0 - if dbSettings.BetReferralBonusPercentage.Valid { - if f8, err := dbSettings.BetReferralBonusPercentage.Float64Value(); err == nil { - betReferralBonusPercentage = f8.Float64 - } - } - - return &domain.ReferralSettings{ - ID: dbSettings.ID, - ReferralRewardAmount: rewardAmount, - CashbackPercentage: cashbackPercentage, - BetReferralBonusPercentage: betReferralBonusPercentage, // New field - MaxReferrals: dbSettings.MaxReferrals, - ExpiresAfterDays: dbSettings.ExpiresAfterDays, - UpdatedBy: dbSettings.UpdatedBy, - CreatedAt: dbSettings.CreatedAt.Time, - UpdatedAt: dbSettings.UpdatedAt.Time, - Version: dbSettings.Version, - } -} +// return &domain.ReferralSettings{ +// ID: dbSettings.ID, +// ReferralRewardAmount: rewardAmount, +// CashbackPercentage: cashbackPercentage, +// BetReferralBonusPercentage: betReferralBonusPercentage, // New field +// MaxReferrals: dbSettings.MaxReferrals, +// ExpiresAfterDays: dbSettings.ExpiresAfterDays, +// UpdatedBy: dbSettings.UpdatedBy, +// CreatedAt: dbSettings.CreatedAt.Time, +// UpdatedAt: dbSettings.UpdatedAt.Time, +// Version: dbSettings.Version, +// } +// } diff --git a/internal/repository/settings.go b/internal/repository/settings.go index 333f280..f456bff 100644 --- a/internal/repository/settings.go +++ b/internal/repository/settings.go @@ -165,9 +165,9 @@ func (s *Store) GetOverrideSettingsList(ctx context.Context, companyID int64) (d func (s *Store) DeleteCompanySetting(ctx context.Context, companyID int64, key string) error { return s.queries.DeleteCompanySetting(ctx, dbgen.DeleteCompanySettingParams{ CompanyID: companyID, - Key: key, + Key: key, }) } -func (s *Store) DeleteAllCompanySetting(ctx context.Context, companyID int64,) error { +func (s *Store) DeleteAllCompanySetting(ctx context.Context, companyID int64) error { return s.queries.DeleteAllCompanySetting(ctx, companyID) } diff --git a/internal/repository/shop_bet.go b/internal/repository/shop_bet.go index 6896640..66e3e63 100644 --- a/internal/repository/shop_bet.go +++ b/internal/repository/shop_bet.go @@ -2,6 +2,7 @@ package repository import ( "context" + "fmt" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -62,7 +63,7 @@ func (s *Store) CreateShopBet(ctx context.Context, bet domain.CreateShopBet) (do if err != nil { return domain.ShopBet{}, err } - + return convertDBShopBet(newShopBet), err } @@ -104,8 +105,10 @@ func (s *Store) GetAllShopBet(ctx context.Context, filter domain.ShopBetFilter) func (s *Store) GetShopBetByID(ctx context.Context, id int64) (domain.ShopBetDetail, error) { bet, err := s.queries.GetShopBetByID(ctx, id) if err != nil { + fmt.Printf("GetShopBetByID Repo BetID %d err %v \n", id, err.Error()) return domain.ShopBetDetail{}, err } + return convertDBShopBetDetail(bet), nil } diff --git a/internal/repository/ticket.go b/internal/repository/ticket.go index ac140bf..1e2ef36 100644 --- a/internal/repository/ticket.go +++ b/internal/repository/ticket.go @@ -41,6 +41,7 @@ func convertDBTicketOutcomes(ticket dbgen.TicketWithOutcome) domain.GetTicket { } return domain.GetTicket{ ID: ticket.ID, + CompanyID: ticket.CompanyID, Amount: domain.Currency(ticket.Amount), TotalOdds: ticket.TotalOdds, Outcomes: outcomes, diff --git a/internal/repository/transfer.go b/internal/repository/transfer.go index cad330e..3b5e5a9 100644 --- a/internal/repository/transfer.go +++ b/internal/repository/transfer.go @@ -148,6 +148,24 @@ func (s *Store) GetTransferByID(ctx context.Context, id int64) (domain.TransferD return convertDBTransferDetail(transfer), nil } +func (s *Store) GetTransferStats(ctx context.Context, walletID int64) (domain.TransferStats, error) { + stats, err := s.queries.GetTransferStats(ctx, pgtype.Int8{ + Int64: walletID, + Valid: true, + }) + + if err != nil { + return domain.TransferStats{}, err + } + + return domain.TransferStats{ + TotalTransfer: stats.TotalTransfers, + TotalDeposits: stats.TotalDeposits, + TotalWithdraws: stats.TotalWithdraw, + TotalWalletToWallet: stats.TotalWalletToWallet, + }, nil +} + func (s *Store) UpdateTransferVerification(ctx context.Context, id int64, verified bool) error { err := s.queries.UpdateTransferVerification(ctx, dbgen.UpdateTransferVerificationParams{ ID: id, diff --git a/internal/repository/user.go b/internal/repository/user.go index 4198b7d..50c6593 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -9,6 +9,7 @@ import ( dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) @@ -73,7 +74,7 @@ func (s *Store) CreateUser(ctx context.Context, user domain.User, usedOtpId int6 func (s *Store) GetUserByID(ctx context.Context, id int64) (domain.User, error) { user, err := s.queries.GetUserByID(ctx, id) if err != nil { - if errors.Is(err, sql.ErrNoRows) { + if errors.Is(err, pgx.ErrNoRows) { return domain.User{}, domain.ErrUserNotFound } return domain.User{}, err @@ -428,7 +429,7 @@ func (s *Store) GetUserByPhone(ctx context.Context, phoneNum string, companyID d }, nil } -func (s *Store) UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64) error { +func (s *Store) UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64, companyId int64) error { err := s.queries.MarkOtpAsUsed(ctx, dbgen.MarkOtpAsUsedParams{ ID: usedOtpId, UsedAt: pgtype.Timestamptz{ @@ -449,6 +450,10 @@ func (s *Store) UpdatePassword(ctx context.Context, identifier string, password String: identifier, Valid: true, }, + CompanyID: pgtype.Int8{ + Int64: companyId, + Valid: true, + }, }) if err != nil { return err diff --git a/internal/services/bet/notification.go b/internal/services/bet/notification.go new file mode 100644 index 0000000..2d4de4e --- /dev/null +++ b/internal/services/bet/notification.go @@ -0,0 +1,247 @@ +package bet + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "go.uber.org/zap" +) + +func newBetResultNotification(userID int64, level domain.NotificationLevel, channel domain.DeliveryChannel, headline, message string, metadata any) *domain.Notification { + raw, _ := json.Marshal(metadata) + return &domain.Notification{ + RecipientID: userID, + DeliveryStatus: domain.DeliveryStatusPending, + IsRead: false, + Type: domain.NOTIFICATION_TYPE_BET_RESULT, + Level: level, + Reciever: domain.NotificationRecieverSideCustomer, + DeliveryChannel: channel, + Payload: domain.NotificationPayload{ + Headline: headline, + Message: message, + }, + Priority: 2, + Metadata: raw, + } +} + +type SendResultNotificationParam struct { + BetID int64 + Status domain.OutcomeStatus + UserID int64 + WinningAmount domain.Currency + Extra string + SendEmail bool + SendSMS bool +} + +func (p SendResultNotificationParam) Validate() error { + if p.BetID == 0 { + return errors.New("BetID is required") + } + if p.UserID == 0 { + return errors.New("UserID is required") + } + return nil +} + +func shouldSend(channel domain.DeliveryChannel, sendEmail, sendSMS bool) bool { + switch { + case channel == domain.DeliveryChannelEmail && sendEmail: + return true + case channel == domain.DeliveryChannelSMS && sendSMS: + return true + case channel == domain.DeliveryChannelInApp: + return true + default: + return false + } +} + +func (s *Service) SendWinningStatusNotification(ctx context.Context, param SendResultNotificationParam) error { + if err := param.Validate(); err != nil { + return err + } + + var headline string + var message string + + switch param.Status { + case domain.OUTCOME_STATUS_WIN: + headline = fmt.Sprintf("Bet #%v Won!", param.BetID) + message = fmt.Sprintf( + "Congratulations! Your bet #%v has won. %.2f has been credited to your wallet.", + param.BetID, + param.WinningAmount.Float32(), + ) + case domain.OUTCOME_STATUS_HALF: + headline = fmt.Sprintf("Bet #%v Half-Win", param.BetID) + message = fmt.Sprintf( + "Your bet #%v resulted in a half-win. %.2f has been credited to your wallet.", + param.BetID, + param.WinningAmount.Float32(), + ) + case domain.OUTCOME_STATUS_VOID: + headline = fmt.Sprintf("Bet #%v Refunded", param.BetID) + message = fmt.Sprintf( + "Your bet #%v has been voided. %.2f has been refunded to your wallet.", + param.BetID, + param.WinningAmount.Float32(), + ) + + default: + return fmt.Errorf("unsupported status: %v", param.Status) + } + + for _, channel := range []domain.DeliveryChannel{ + domain.DeliveryChannelInApp, + domain.DeliveryChannelEmail, + domain.DeliveryChannelSMS, + } { + if !shouldSend(channel, param.SendEmail, param.SendSMS) { + continue + } + n := newBetResultNotification(param.UserID, domain.NotificationLevelSuccess, channel, headline, message, map[string]any{ + "winning_amount": param.WinningAmount.Float32(), + "status": param.Status, + "more": param.Extra, + }) + if err := s.notificationSvc.SendNotification(ctx, n); err != nil { + return err + } + } + + return nil +} + +func (s *Service) SendLosingStatusNotification(ctx context.Context, param SendResultNotificationParam) error { + if err := param.Validate(); err != nil { + return err + } + + var headline string + var message string + + switch param.Status { + case domain.OUTCOME_STATUS_LOSS: + headline = fmt.Sprintf("Bet #%v Lost", param.BetID) + message = "Unfortunately, your bet did not win this time. Better luck next time!" + default: + return fmt.Errorf("unsupported status: %v", param.Status) + } + + for _, channel := range []domain.DeliveryChannel{ + domain.DeliveryChannelInApp, + domain.DeliveryChannelEmail, + domain.DeliveryChannelSMS, + } { + if !shouldSend(channel, param.SendEmail, param.SendSMS) { + continue + } + n := newBetResultNotification(param.UserID, domain.NotificationLevelWarning, channel, headline, message, map[string]any{ + "status": param.Status, + "more": param.Extra, + }) + if err := s.notificationSvc.SendNotification(ctx, n); err != nil { + return err + } + } + + return nil +} + +func (s *Service) SendErrorStatusNotification(ctx context.Context, betID int64, status domain.OutcomeStatus, userID int64, extra string) error { + + var headline string + var message string + + switch status { + case domain.OUTCOME_STATUS_ERROR, domain.OUTCOME_STATUS_PENDING: + headline = fmt.Sprintf("Bet #%v Processing Issue", betID) + message = "We encountered a problem while processing your bet. Our team is working to resolve it as soon as possible." + + default: + return fmt.Errorf("unsupported status: %v", status) + } + + for _, channel := range []domain.DeliveryChannel{ + domain.DeliveryChannelInApp, + domain.DeliveryChannelEmail, + } { + n := newBetResultNotification(userID, domain.NotificationLevelError, channel, headline, message, map[string]any{ + "status": status, + "more": extra, + }) + if err := s.notificationSvc.SendNotification(ctx, n); err != nil { + return err + } + } + return nil +} + +func (s *Service) SendAdminAlertNotification(ctx context.Context, betID int64, status domain.OutcomeStatus, extra string, companyID int64) error { + + var headline string + var message string + + switch status { + case domain.OUTCOME_STATUS_ERROR, domain.OUTCOME_STATUS_PENDING: + headline = fmt.Sprintf("Processing Error for Bet #%v", betID) + message = "A processing error occurred with this bet. Please review and take corrective action." + + default: + return fmt.Errorf("unsupported status: %v", status) + } + + super_admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{ + Role: string(domain.RoleSuperAdmin), + }) + + if err != nil { + s.mongoLogger.Error("failed to get super_admin recipients", + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return err + } + + admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{ + Role: string(domain.RoleAdmin), + CompanyID: domain.ValidInt64{ + Value: companyID, + Valid: true, + }, + }) + + if err != nil { + s.mongoLogger.Error("failed to get admin recipients", + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return err + } + + users := append(super_admin_users, admin_users...) + + for _, user := range users { + for _, channel := range []domain.DeliveryChannel{ + domain.DeliveryChannelInApp, + domain.DeliveryChannelEmail, + } { + n := newBetResultNotification(user.ID, domain.NotificationLevelError, channel, headline, message, map[string]any{ + "status": status, + "more": extra, + }) + if err := s.notificationSvc.SendNotification(ctx, n); err != nil { + return err + } + } + } + + return nil +} diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index e026110..4ba3a66 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -31,20 +31,20 @@ import ( ) var ( - ErrNoEventsAvailable = errors.New("Not enough events available with the given filters") - 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") + ErrNoEventsAvailable = errors.New("not enough events available with the given filters") + 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") + 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") - ErrInvalidAmount = errors.New("Invalid amount") - ErrBetAmountTooHigh = errors.New("Cannot create a bet with an amount above limit") - ErrBetWinningTooHigh = errors.New("Total Winnings over set limit") + ErrInvalidAmount = errors.New("invalid amount") + ErrBetAmountTooHigh = errors.New("cannot create a bet with an amount above limit") + ErrBetWinningTooHigh = errors.New("total Winnings over set limit") ) type Service struct { @@ -97,10 +97,6 @@ func (s *Service) GenerateCashoutID() (string, error) { for i := 0; i < length; i++ { index, err := rand.Int(rand.Reader, charLen) if err != nil { - s.mongoLogger.Error("failed to generate random index for cashout ID", - zap.Int("position", i), - zap.Error(err), - ) return "", err } result[i] = chars[index.Int64()] @@ -221,7 +217,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID if err != nil { return domain.CreateBetRes{}, err } - if req.Amount < 1 { + if req.Amount < settingsList.MinimumBetAmount.Float32() { return domain.CreateBetRes{}, ErrInvalidAmount } @@ -283,8 +279,9 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID ) return domain.CreateBetRes{}, err } - if count >= 2 { - return domain.CreateBetRes{}, fmt.Errorf("bet already placed twice") + + if role == domain.RoleCustomer && count >= settingsList.BetDuplicateLimit { + return domain.CreateBetRes{}, fmt.Errorf("max user limit for duplicate bet") } fastCode := helpers.GenerateFastCode() @@ -340,17 +337,20 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID return domain.CreateBetRes{}, err } - if branch.BranchManagerID != userID { - s.mongoLogger.Warn("unauthorized branch for branch manager", - zap.Int64("branch_id", *req.BranchID), - zap.Error(err), - ) - return domain.CreateBetRes{}, err + if role == domain.RoleBranchManager { + if branch.BranchManagerID != userID { + s.mongoLogger.Warn("unauthorized branch for branch manager", + zap.Int64("branch_id", *req.BranchID), + zap.Error(err), + ) + return domain.CreateBetRes{}, err + } } - - if branch.CompanyID == companyID { + if branch.CompanyID != companyID { s.mongoLogger.Warn("unauthorized company", zap.Int64("branch_id", *req.BranchID), + zap.Int64("branch_company_id", branch.CompanyID), + zap.Int64("company_id", companyID), zap.Error(err), ) } @@ -376,13 +376,13 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID ) return domain.CreateBetRes{}, err } - // + // default: s.mongoLogger.Error("unknown role type", zap.String("role", string(role)), zap.Int64("user_id", userID), ) - return domain.CreateBetRes{}, fmt.Errorf("Unknown Role Type") + return domain.CreateBetRes{}, fmt.Errorf("unknown role type") } bet, err := s.CreateBet(ctx, newBet) @@ -583,25 +583,21 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string, var newOdds []domain.CreateBetOutcome var totalOdds float32 = 1 + eventLogger := s.mongoLogger.With( + zap.String("eventID", eventID), + zap.Int32("sportID", sportID), + zap.String("homeTeam", HomeTeam), + zap.String("awayTeam", AwayTeam), + ) markets, err := s.prematchSvc.GetOddsByEventID(ctx, eventID, domain.OddMarketWithEventFilter{}) + if err != nil { - s.logger.Error("failed to get odds for event", "event id", eventID, "error", err) - s.mongoLogger.Error("failed to get odds for event", - zap.String("eventID", eventID), - zap.Int32("sportID", sportID), - zap.String("homeTeam", HomeTeam), - zap.String("awayTeam", AwayTeam), - zap.Error(err)) + eventLogger.Error("failed to get odds for event", zap.Error(err)) return nil, 0, err } if len(markets) == 0 { - s.logger.Error("empty odds for event", "event id", eventID) - s.mongoLogger.Warn("empty odds for event", - zap.String("eventID", eventID), - zap.Int32("sportID", sportID), - zap.String("homeTeam", HomeTeam), - zap.String("awayTeam", AwayTeam)) + eventLogger.Warn("empty odds for event") return nil, 0, fmt.Errorf("empty odds or event %v", eventID) } @@ -630,19 +626,13 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string, err = json.Unmarshal(rawBytes, &selectedOdd) if err != nil { - s.logger.Error("Failed to unmarshal raw odd", "error", err) - s.mongoLogger.Warn("Failed to unmarshal raw odd", - zap.String("eventID", eventID), - zap.Int32("sportID", sportID), - zap.Error(err)) + eventLogger.Warn("Failed to unmarshal raw odd", zap.Error(err)) continue } parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32) if err != nil { - s.logger.Error("Failed to parse odd", "error", err) - s.mongoLogger.Warn("Failed to parse odd", - zap.String("eventID", eventID), + eventLogger.Warn("Failed to parse odd", zap.String("oddValue", selectedOdd.Odds), zap.Error(err)) continue @@ -650,17 +640,13 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string, eventIDInt, err := strconv.ParseInt(eventID, 10, 64) if err != nil { - s.logger.Error("Failed to parse eventID", "error", err) - s.mongoLogger.Warn("Failed to parse eventID", - zap.String("eventID", eventID), - zap.Error(err)) + eventLogger.Warn("Failed to parse eventID", zap.Error(err)) continue } oddID, err := strconv.ParseInt(selectedOdd.ID, 10, 64) if err != nil { - s.logger.Error("Failed to parse oddID", "error", err) - s.mongoLogger.Warn("Failed to parse oddID", + eventLogger.Warn("Failed to parse oddID", zap.String("oddID", selectedOdd.ID), zap.Error(err)) continue @@ -668,8 +654,7 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string, marketID, err := strconv.ParseInt(market.MarketID, 10, 64) if err != nil { - s.logger.Error("Failed to parse marketID", "error", err) - s.mongoLogger.Warn("Failed to parse marketID", + eventLogger.Warn("Failed to parse marketID", zap.String("marketID", market.MarketID), zap.Error(err)) continue @@ -696,22 +681,12 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string, } if len(newOdds) == 0 { - s.logger.Error("Bet Outcomes is empty for market", "selectedMarkets", len(selectedMarkets)) - s.mongoLogger.Error("Bet Outcomes is empty for market", - zap.String("eventID", eventID), - zap.Int32("sportID", sportID), - zap.String("homeTeam", HomeTeam), - zap.String("awayTeam", AwayTeam), - zap.Int("selectedMarkets", len(selectedMarkets))) + eventLogger.Error("Bet Outcomes is empty for market", zap.Int("selectedMarkets", len(selectedMarkets))) return nil, 0, ErrGenerateRandomOutcome } // ✅ Final success log (optional) - s.mongoLogger.Info("Random bet outcomes generated successfully", - zap.String("eventID", eventID), - zap.Int32("sportID", sportID), - zap.Int("numOutcomes", len(newOdds)), - zap.Float32("totalOdds", totalOdds)) + eventLogger.Info("Random bet outcomes generated successfully", zap.Int("numOutcomes", len(newOdds)), zap.Float32("totalOdds", totalOdds)) return newOdds, totalOdds, nil } @@ -719,7 +694,15 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string, func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID, companyID int64, leagueID domain.ValidInt64, sportID domain.ValidInt32, firstStartTime, lastStartTime domain.ValidTime) (domain.CreateBetRes, error) { // Get a unexpired event id - + randomBetLogger := s.mongoLogger.With( + zap.Int64("userID", userID), + zap.Int64("branchID", branchID), + zap.Int64("companyID", companyID), + zap.Any("leagueID", leagueID), + zap.Any("sportID", sportID), + zap.Any("firstStartTime", firstStartTime), + zap.Any("lastStartTime", lastStartTime), + ) events, _, err := s.eventSvc.GetPaginatedUpcomingEvents(ctx, domain.EventFilter{ SportID: sportID, @@ -729,17 +712,12 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID, companyI }) if err != nil { - s.mongoLogger.Error("failed to get paginated upcoming events", - zap.Int64("userID", userID), - zap.Int64("branchID", branchID), - zap.Error(err)) + randomBetLogger.Error("failed to get paginated upcoming events", zap.Error(err)) return domain.CreateBetRes{}, err } if len(events) == 0 { - s.mongoLogger.Warn("no events available for random bet", - zap.Int64("userID", userID), - zap.Int64("branchID", branchID)) + randomBetLogger.Warn("no events available for random bet") return domain.CreateBetRes{}, ErrNoEventsAvailable } @@ -765,12 +743,7 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID, companyI newOdds, total, err := s.GenerateRandomBetOutcomes(ctx, event.ID, event.SportID, event.HomeTeam, event.AwayTeam, event.StartTime, numMarketsPerBet) if err != nil { - s.logger.Error("failed to generate random bet outcome", "event id", event.ID, "error", err) - s.mongoLogger.Error("failed to generate random bet outcome", - zap.Int64("userID", userID), - zap.Int64("branchID", branchID), - zap.String("eventID", event.ID), - zap.String("error", fmt.Sprintf("%v", err))) + s.mongoLogger.Error("failed to generate random bet outcome", zap.String("eventID", event.ID), zap.Error(err)) continue } @@ -779,10 +752,7 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID, companyI } if len(randomOdds) == 0 { - s.logger.Error("Failed to generate random any outcomes for all events") - s.mongoLogger.Error("Failed to generate random any outcomes for all events", - zap.Int64("userID", userID), - zap.Int64("branchID", branchID)) + randomBetLogger.Error("Failed to generate random any outcomes for all events") return domain.CreateBetRes{}, ErrGenerateRandomOutcome } @@ -790,20 +760,13 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID, companyI outcomesHash, err := generateOutcomeHash(randomOdds) if err != nil { - s.mongoLogger.Error("failed to generate outcome hash", - zap.Int64("user_id", userID), - zap.Error(err), - ) + randomBetLogger.Error("failed to generate outcome hash", zap.Error(err)) return domain.CreateBetRes{}, err } count, err := s.GetBetCountByUserID(ctx, userID, outcomesHash) if err != nil { - s.mongoLogger.Error("failed to get bet count", - zap.Int64("user_id", userID), - zap.String("outcome_hash", outcomesHash), - zap.Error(err), - ) + randomBetLogger.Error("failed to get bet count", zap.String("outcome_hash", outcomesHash), zap.Error(err)) return domain.CreateBetRes{}, err } @@ -825,10 +788,7 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID, companyI bet, err := s.CreateBet(ctx, newBet) if err != nil { - s.mongoLogger.Error("Failed to create a new random bet", - zap.Int64("userID", userID), - zap.Int64("branchID", branchID), - zap.String("bet", fmt.Sprintf("%+v", newBet))) + randomBetLogger.Error("Failed to create a new random bet", zap.Error(err)) return domain.CreateBetRes{}, err } @@ -838,19 +798,13 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID, companyI rows, err := s.betStore.CreateBetOutcome(ctx, randomOdds) if err != nil { - s.mongoLogger.Error("Failed to create a new random bet outcome", - zap.Int64("userID", userID), - zap.Int64("branchID", branchID), - zap.String("randomOdds", fmt.Sprintf("%+v", randomOdds))) + randomBetLogger.Error("Failed to create a new random bet outcome", zap.Any("randomOdds", randomOdds)) return domain.CreateBetRes{}, err } res := domain.ConvertCreateBetRes(bet, rows) - s.mongoLogger.Info("Random bets placed successfully", - zap.Int64("userID", userID), - zap.Int64("branchID", branchID), - zap.String("response", fmt.Sprintf("%+v", res))) + randomBetLogger.Info("Random bets placed successfully") return res, nil } @@ -897,53 +851,73 @@ func (s *Service) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) e return s.betStore.UpdateCashOut(ctx, id, cashedOut) } -func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error { - bet, err := s.GetBetByID(ctx, id) +func (s *Service) UpdateStatus(ctx context.Context, betId int64, status domain.OutcomeStatus) error { + + updateLogger := s.mongoLogger.With( + zap.Int64("bet_id", betId), + zap.String("status", status.String()), + ) + bet, err := s.GetBetByID(ctx, betId) if err != nil { - s.mongoLogger.Error("failed to update bet status: invalid bet ID", - zap.Int64("bet_id", id), - zap.Error(err), - ) + updateLogger.Error("failed to update bet status: invalid bet ID", zap.Error(err)) + return err + } + + settingsList, err := s.settingSvc.GetOverrideSettingsList(ctx, bet.CompanyID) + if err != nil { + updateLogger.Error("failed to get settings", zap.Error(err)) return err } if status == domain.OUTCOME_STATUS_ERROR || status == domain.OUTCOME_STATUS_PENDING { - s.SendAdminErrorAlertNotification(ctx, status, "") - s.SendErrorStatusNotification(ctx, status, bet.UserID, "") - s.mongoLogger.Error("Bet Status is error", - zap.Int64("bet_id", id), - zap.Error(err), - ) - return s.betStore.UpdateStatus(ctx, id, status) + if err := s.SendAdminAlertNotification(ctx, betId, status, "", bet.CompanyID); err != nil { + updateLogger.Error("failed to send admin notification", zap.Error(err)) + return err + } + + if err := s.SendErrorStatusNotification(ctx, betId, status, bet.UserID, ""); err != nil { + updateLogger.Error("failed to send error notification to user", zap.Error(err)) + return err + } + updateLogger.Error("bet entered error/pending state") + return s.betStore.UpdateStatus(ctx, betId, status) } if bet.IsShopBet { - return s.betStore.UpdateStatus(ctx, id, status) + return s.betStore.UpdateStatus(ctx, betId, status) } - customerWallet, err := s.walletSvc.GetCustomerWallet(ctx, id) + // After this point the bet is known to be a online customer bet + + customerWallet, err := s.walletSvc.GetCustomerWallet(ctx, bet.UserID) if err != nil { - s.mongoLogger.Error("failed to get customer wallet", - zap.Int64("bet_id", id), - zap.Error(err), - ) + updateLogger.Error("failed to get customer wallet", zap.Error(err)) return err } + resultNotification := SendResultNotificationParam{ + BetID: betId, + Status: status, + UserID: bet.UserID, + SendEmail: settingsList.SendEmailOnBetFinish, + SendSMS: settingsList.SendSMSOnBetFinish, + } var amount domain.Currency switch status { case domain.OUTCOME_STATUS_LOSS: - s.SendLosingStatusNotification(ctx, status, bet.UserID, "") - return s.betStore.UpdateStatus(ctx, id, status) + err := s.SendLosingStatusNotification(ctx, resultNotification) + if err != nil { + updateLogger.Error("failed to send notification", zap.Error(err)) + return err + } + return s.betStore.UpdateStatus(ctx, betId, status) case domain.OUTCOME_STATUS_WIN: amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds) - s.SendWinningStatusNotification(ctx, status, bet.UserID, amount, "") case domain.OUTCOME_STATUS_HALF: amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds) / 2 - s.SendWinningStatusNotification(ctx, status, bet.UserID, amount, "") case domain.OUTCOME_STATUS_VOID: amount = bet.Amount - s.SendWinningStatusNotification(ctx, status, bet.UserID, amount, "") default: + updateLogger.Error("invalid outcome status") return fmt.Errorf("invalid outcome status") } @@ -951,7 +925,7 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc domain.TRANSFER_DIRECT, domain.PaymentDetails{}, fmt.Sprintf("Added %v to wallet by system for winning a bet", amount.Float32())) if err != nil { - s.mongoLogger.Error("failed to add winnings to wallet", + updateLogger.Error("failed to add winnings to wallet", zap.Int64("wallet_id", customerWallet.RegularID), zap.Float32("amount", float32(amount)), zap.Error(err), @@ -959,231 +933,23 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc return err } - return s.betStore.UpdateStatus(ctx, id, status) -} - -func (s *Service) SendWinningStatusNotification(ctx context.Context, status domain.OutcomeStatus, userID int64, winningAmount domain.Currency, extra string) error { - - var headline string - var message string - - switch status { - case domain.OUTCOME_STATUS_WIN: - headline = "You Bet Has Won!" - message = fmt.Sprintf( - "You have been awarded %.2f", - winningAmount.Float32(), - ) - case domain.OUTCOME_STATUS_HALF: - headline = "You have a half win" - message = fmt.Sprintf( - "You have been awarded %.2f", - winningAmount.Float32(), - ) - case domain.OUTCOME_STATUS_VOID: - headline = "Your bet has been refunded" - message = fmt.Sprintf( - "You have been awarded %.2f", - winningAmount.Float32(), - ) - } - - betNotification := &domain.Notification{ - RecipientID: userID, - DeliveryStatus: domain.DeliveryStatusPending, - IsRead: false, - Type: domain.NOTIFICATION_TYPE_BET_RESULT, - Level: domain.NotificationLevelSuccess, - Reciever: domain.NotificationRecieverSideCustomer, - DeliveryChannel: domain.DeliveryChannelInApp, - Payload: domain.NotificationPayload{ - Headline: headline, - Message: message, - }, - Priority: 2, - Metadata: fmt.Appendf(nil, `{ - "winning_amount":%.2f, - "status":%v - "more": %v - }`, winningAmount.Float32(), status, extra), - } - - if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil { - return err - } - - betNotification.DeliveryChannel = domain.DeliveryChannelEmail - if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil { - return err - } - - return nil -} - -func (s *Service) SendLosingStatusNotification(ctx context.Context, status domain.OutcomeStatus, userID int64, extra string) error { - - var headline string - var message string - - switch status { - case domain.OUTCOME_STATUS_LOSS: - headline = "Your bet has lost" - message = "Better luck next time" - } - - betNotification := &domain.Notification{ - RecipientID: userID, - DeliveryStatus: domain.DeliveryStatusPending, - IsRead: false, - Type: domain.NOTIFICATION_TYPE_BET_RESULT, - Level: domain.NotificationLevelSuccess, - Reciever: domain.NotificationRecieverSideCustomer, - DeliveryChannel: domain.DeliveryChannelInApp, - Payload: domain.NotificationPayload{ - Headline: headline, - Message: message, - }, - Priority: 2, - Metadata: fmt.Appendf(nil, `{ - "status":%v - "more": %v - }`, status, extra), - } - - if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil { - return err - } - - betNotification.DeliveryChannel = domain.DeliveryChannelEmail - if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil { - return err - } - - return nil -} - -func (s *Service) SendErrorStatusNotification(ctx context.Context, status domain.OutcomeStatus, userID int64, extra string) error { - - var headline string - var message string - - switch status { - case domain.OUTCOME_STATUS_ERROR, domain.OUTCOME_STATUS_PENDING: - headline = "There was an error with your bet" - message = "We have encounter an error with your bet. We will fix it as soon as we can" - } - - errorSeverityLevel := domain.NotificationErrorSeverityFatal - - betNotification := &domain.Notification{ - RecipientID: userID, - DeliveryStatus: domain.DeliveryStatusPending, - IsRead: false, - Type: domain.NOTIFICATION_TYPE_BET_RESULT, - Level: domain.NotificationLevelSuccess, - Reciever: domain.NotificationRecieverSideCustomer, - DeliveryChannel: domain.DeliveryChannelInApp, - Payload: domain.NotificationPayload{ - Headline: headline, - Message: message, - }, - Priority: 1, - ErrorSeverity: &errorSeverityLevel, - Metadata: fmt.Appendf(nil, `{ - "status":%v - "more": %v - }`, status, extra), - } - - if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil { - return err - } - - betNotification.DeliveryChannel = domain.DeliveryChannelEmail - if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil { - return err - } - return nil -} - -func (s *Service) SendAdminErrorAlertNotification(ctx context.Context, status domain.OutcomeStatus, extra string) error { - - var headline string - var message string - - switch status { - case domain.OUTCOME_STATUS_ERROR, domain.OUTCOME_STATUS_PENDING: - headline = "There was an error processing bet" - message = "We have encounter an error with bet. We will fix it as soon as we can" - } - - errorSeverity := domain.NotificationErrorSeverityHigh - betNotification := &domain.Notification{ - ErrorSeverity: &errorSeverity, - DeliveryStatus: domain.DeliveryStatusPending, - IsRead: false, - Type: domain.NOTIFICATION_TYPE_BET_RESULT, - Level: domain.NotificationLevelSuccess, - Reciever: domain.NotificationRecieverSideCustomer, - DeliveryChannel: domain.DeliveryChannelEmail, - Payload: domain.NotificationPayload{ - Headline: headline, - Message: message, - }, - Priority: 2, - Metadata: fmt.Appendf(nil, `{ - "status":%v - "more": %v - }`, status, extra), - } - - super_admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{ - Role: string(domain.RoleSuperAdmin), - }) - - if err != nil { - s.mongoLogger.Error("failed to get super_admin recipients", + if err := s.betStore.UpdateStatus(ctx, betId, status); err != nil { + updateLogger.Error("failed to update bet status", + zap.String("status", status.String()), zap.Error(err), - zap.Time("timestamp", time.Now()), ) return err } - admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{ - Role: string(domain.RoleAdmin), - }) + resultNotification.WinningAmount = amount + if err := s.SendWinningStatusNotification(ctx, resultNotification); err != nil { - if err != nil { - s.mongoLogger.Error("failed to get admin recipients", + updateLogger.Error("failed to send winning notification", zap.Error(err), - zap.Time("timestamp", time.Now()), ) return err } - users := append(super_admin_users, admin_users...) - - for _, user := range users { - betNotification.RecipientID = user.ID - if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil { - s.mongoLogger.Error("failed to send admin notification", - zap.Int64("admin_id", user.ID), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return err - } - betNotification.DeliveryChannel = domain.DeliveryChannelEmail - if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil { - s.mongoLogger.Error("failed to send email admin notification", - zap.Int64("admin_id", user.ID), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return err - } - } - return nil } @@ -1313,7 +1079,7 @@ func (s *Service) SetBetToRemoved(ctx context.Context, id int64) error { } func (s *Service) ProcessBetCashback(ctx context.Context) error { - + bets, err := s.betStore.GetBetsForCashback(ctx) if err != nil { s.mongoLogger.Error("failed to fetch bets", @@ -1322,7 +1088,6 @@ func (s *Service) ProcessBetCashback(ctx context.Context) error { return err } - for _, bet := range bets { shouldProcess := true loseCount := 0 @@ -1365,6 +1130,14 @@ func (s *Service) ProcessBetCashback(ctx context.Context) error { } settingsList, err := s.settingSvc.GetOverrideSettingsList(ctx, bet.CompanyID) + if err != nil { + s.mongoLogger.Error("Failed to get settings", + zap.Int64("userID", bet.UserID), + zap.Error(err)) + + return err + } + cashbackAmount := math.Min(float64(settingsList.CashbackAmountCap.Float32()), float64(calculateCashbackAmount(bet.Amount.Float32(), bet.TotalOdds))) _, err = s.walletSvc.AddToWallet(ctx, wallets.StaticID, domain.ToCurrency(float32(cashbackAmount)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, diff --git a/internal/services/bonus/notification.go b/internal/services/bonus/notification.go new file mode 100644 index 0000000..8497a53 --- /dev/null +++ b/internal/services/bonus/notification.go @@ -0,0 +1,83 @@ +package bonus + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type SendBonusNotificationParam struct { + BonusID int64 + UserID int64 + Type domain.BonusType + Amount domain.Currency + SendEmail bool + SendSMS bool +} + +func shouldSend(channel domain.DeliveryChannel, sendEmail, sendSMS bool) bool { + switch { + case channel == domain.DeliveryChannelEmail && sendEmail: + return true + case channel == domain.DeliveryChannelSMS && sendSMS: + return true + case channel == domain.DeliveryChannelInApp: + return true + default: + return false + } +} + +func (s *Service) SendBonusNotification(ctx context.Context, param SendBonusNotificationParam) error { + + var headline string + var message string + + switch param.Type { + case domain.WelcomeBonus: + headline = "You've been awarded a welcome bonus!" + message = fmt.Sprintf( + "Congratulations! A you've been given %.2f as a welcome bonus for you to bet on.", + param.Amount, + ) + default: + return fmt.Errorf("unsupported bonus type: %v", param.Type) + } + for _, channel := range []domain.DeliveryChannel{ + domain.DeliveryChannelInApp, + domain.DeliveryChannelEmail, + domain.DeliveryChannelSMS, + } { + if !shouldSend(channel, param.SendEmail, param.SendSMS) { + continue + } + + raw, _ := json.Marshal(map[string]any{ + "bonus_id": param.BonusID, + "type": param.Type, + }) + + n := &domain.Notification{ + RecipientID: param.UserID, + DeliveryStatus: domain.DeliveryStatusPending, + IsRead: false, + Type: domain.NOTIFICATION_TYPE_BONUS_AWARDED, + Level: domain.NotificationLevelSuccess, + Reciever: domain.NotificationRecieverSideCustomer, + DeliveryChannel: channel, + Payload: domain.NotificationPayload{ + Headline: headline, + Message: message, + }, + Priority: 2, + Metadata: raw, + } + if err := s.notificationSvc.SendNotification(ctx, n); err != nil { + return err + } + } + + return nil +} diff --git a/internal/services/bonus/port.go b/internal/services/bonus/port.go index 2147b51..4bbd877 100644 --- a/internal/services/bonus/port.go +++ b/internal/services/bonus/port.go @@ -3,12 +3,15 @@ package bonus import ( "context" - dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) type BonusStore interface { - CreateBonusMultiplier(ctx context.Context, multiplier float32, balance_cap int64) error - GetBonusMultiplier(ctx context.Context) ([]dbgen.GetBonusMultiplierRow, error) - GetBonusBalanceCap(ctx context.Context) ([]dbgen.GetBonusBalanceCapRow, error) - UpdateBonusMultiplier(ctx context.Context, id int64, mulitplier float32, balance_cap int64) error + CreateUserBonus(ctx context.Context, bonus domain.CreateBonus) (domain.UserBonus, error) + GetAllUserBonuses(ctx context.Context, filter domain.BonusFilter) ([]domain.UserBonus, error) + GetBonusCount(ctx context.Context, filter domain.BonusFilter) (int64, error) + GetBonusByID(ctx context.Context, bonusID int64) (domain.UserBonus, error) + GetBonusStats(ctx context.Context, filter domain.BonusFilter) (domain.BonusStats, error) + UpdateUserBonus(ctx context.Context, bonusID int64, IsClaimed bool) error + DeleteUserBonus(ctx context.Context, bonusID int64) error } diff --git a/internal/services/bonus/service.go b/internal/services/bonus/service.go index 51e008a..3daaf71 100644 --- a/internal/services/bonus/service.go +++ b/internal/services/bonus/service.go @@ -2,32 +2,162 @@ package bonus import ( "context" + "errors" + "fmt" + "math" + "time" - dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" + "go.uber.org/zap" ) type Service struct { - bonusStore BonusStore + bonusStore BonusStore + walletSvc *wallet.Service + settingSvc *settings.Service + notificationSvc *notificationservice.Service + mongoLogger *zap.Logger } -func NewService(bonusStore BonusStore) *Service { +func NewService(bonusStore BonusStore, walletSvc *wallet.Service, settingSvc *settings.Service, notificationSvc *notificationservice.Service, mongoLogger *zap.Logger) *Service { return &Service{ - bonusStore: bonusStore, + bonusStore: bonusStore, + walletSvc: walletSvc, + settingSvc: settingSvc, + notificationSvc: notificationSvc, + mongoLogger: mongoLogger, } } -func (s *Service) CreateBonusMultiplier(ctx context.Context, multiplier float32, balance_cap int64) error { - return s.bonusStore.CreateBonusMultiplier(ctx, multiplier, balance_cap) +var ( + ErrWelcomeBonusNotActive = errors.New("welcome bonus is not active") + ErrWelcomeBonusCountReached = errors.New("welcome bonus max deposit count reached") +) + +func (s *Service) CreateWelcomeBonus(ctx context.Context, amount domain.Currency, companyID int64, userID int64) error { + settingsList, err := s.settingSvc.GetOverrideSettingsList(ctx, companyID) + if err != nil { + s.mongoLogger.Error("Failed to get settings", + zap.Int64("companyID", companyID), + zap.Error(err)) + + return err + } + + if !settingsList.WelcomeBonusActive { + return ErrWelcomeBonusNotActive + } + + wallet, err := s.walletSvc.GetCustomerWallet(ctx, userID) + if err != nil { + return err + } + + stats, err := s.walletSvc.GetTransferStats(ctx, wallet.ID) + + if err != nil { + return err + } + + if stats.TotalDeposits > settingsList.WelcomeBonusCount { + return ErrWelcomeBonusCountReached + } + + newBalance := math.Min(float64(amount)*float64(settingsList.WelcomeBonusMultiplier), float64(settingsList.WelcomeBonusCap)) + + bonus, err := s.CreateUserBonus(ctx, domain.CreateBonus{ + Name: "Welcome Bonus", + Description: fmt.Sprintf("Awarded for deposit number (%v / %v)", stats.TotalDeposits, settingsList.WelcomeBonusCount), + UserID: userID, + Type: domain.WelcomeBonus, + RewardAmount: domain.Currency(newBalance), + ExpiresAt: time.Now().Add(time.Duration(settingsList.WelcomeBonusExpire) * 24 * time.Hour), + }) + + if err != nil { + return err + } + + err = s.SendBonusNotification(ctx, SendBonusNotificationParam{ + BonusID: bonus.ID, + UserID: userID, + Type: domain.DepositBonus, + Amount: domain.Currency(newBalance), + SendEmail: true, + SendSMS: false, + }) + + if err != nil { + return err + } + + return nil } -func (s *Service) GetBonusMultiplier(ctx context.Context) ([]dbgen.GetBonusMultiplierRow, error) { - return s.bonusStore.GetBonusMultiplier(ctx) +var ( + ErrBonusIsAlreadyClaimed = errors.New("bonus is already claimed") + ErrBonusUserIDNotMatch = errors.New("bonus user id is not a match") +) + +func (s *Service) ProcessBonusClaim(ctx context.Context, bonusID, userID int64) error { + + bonus, err := s.GetBonusByID(ctx, bonusID) + + if err != nil { + return err + } + + if bonus.UserID != userID { + + } + if bonus.IsClaimed { + return ErrBonusIsAlreadyClaimed + } + + wallet, err := s.walletSvc.GetCustomerWallet(ctx, bonus.UserID) + + if err != nil { + return err + } + + _, err = s.walletSvc.AddToWallet( + ctx, wallet.StaticID, bonus.RewardAmount, + domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, + fmt.Sprintf("Added %v to bonus wallet due to %v", bonus.RewardAmount, bonus.Type), + ) + if err != nil { + return err + } + + if err := s.UpdateUserBonus(ctx, bonusID, true); err != nil { + return err + } + + return nil } -func (s *Service) GetBonusBalanceCap(ctx context.Context) ([]dbgen.GetBonusBalanceCapRow, error) { - return s.bonusStore.GetBonusBalanceCap(ctx) +func (s *Service) CreateUserBonus(ctx context.Context, bonus domain.CreateBonus) (domain.UserBonus, error) { + return s.bonusStore.CreateUserBonus(ctx, bonus) +} +func (s *Service) GetAllUserBonuses(ctx context.Context, filter domain.BonusFilter) ([]domain.UserBonus, error) { + return s.bonusStore.GetAllUserBonuses(ctx, filter) } -func (s *Service) UpdateBonusMultiplier(ctx context.Context, id int64, mulitplier float32, balance_cap int64) error { - return s.bonusStore.UpdateBonusMultiplier(ctx, id, mulitplier, balance_cap) +func (s *Service) GetBonusCount(ctx context.Context, filter domain.BonusFilter) (int64, error) { + return s.bonusStore.GetBonusCount(ctx, filter) +} +func (s *Service) GetBonusByID(ctx context.Context, bonusID int64) (domain.UserBonus, error) { + return s.bonusStore.GetBonusByID(ctx, bonusID) +} +func (s *Service) GetBonusStats(ctx context.Context, filter domain.BonusFilter) (domain.BonusStats, error) { + return s.bonusStore.GetBonusStats(ctx, filter) +} +func (s *Service) UpdateUserBonus(ctx context.Context, bonusID int64, IsClaimed bool) error { + return s.bonusStore.UpdateUserBonus(ctx, bonusID, IsClaimed) +} +func (s *Service) DeleteUserBonus(ctx context.Context, bonusID int64) error { + return s.bonusStore.DeleteUserBonus(ctx, bonusID) } diff --git a/internal/services/event/port.go b/internal/services/event/port.go index e02be83..74305f9 100644 --- a/internal/services/event/port.go +++ b/internal/services/event/port.go @@ -19,6 +19,7 @@ type Service interface { IsEventMonitored(ctx context.Context, eventID string) (bool, error) UpdateEventMonitored(ctx context.Context, eventID string, IsMonitored bool) error GetEventsWithSettings(ctx context.Context, companyID int64, filter domain.EventFilter) ([]domain.EventWithSettings, int64, error) - GetEventWithSettingByID(ctx context.Context, ID string, companyID int64) (domain.EventWithSettings, error) + GetEventWithSettingByID(ctx context.Context, ID string, companyID int64) (domain.EventWithSettings, error) UpdateEventSettings(ctx context.Context, event domain.CreateEventSettings) error + GetSportAndLeagueIDs(ctx context.Context, eventID string) ([]int64, error) } diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 6dcf6ee..4f56c2a 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -12,8 +12,11 @@ import ( "sync" "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/settings" + "github.com/jackc/pgx/v5" "go.uber.org/zap" // "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" ) @@ -21,14 +24,18 @@ import ( type service struct { token string store *repository.Store + settingSvc settings.Service mongoLogger *zap.Logger + cfg *config.Config } -func New(token string, store *repository.Store, mongoLogger *zap.Logger) Service { +func New(token string, store *repository.Store, settingSvc settings.Service, mongoLogger *zap.Logger, cfg *config.Config) Service { return &service{ token: token, store: store, + settingSvc: settingSvc, mongoLogger: mongoLogger, + cfg: cfg, } } @@ -206,22 +213,39 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { } func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_url string, source domain.EventSource) { - const pageLimit int = 200 - sportIDs := []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91} - // sportIDs := []int{1} + settingsList, err := s.settingSvc.GetGlobalSettingList(ctx) + + if err != nil { + s.mongoLogger.Error("Failed to fetch event data for page", zap.Error(err)) + return + } + + var pageLimit int + var sportIDs []int + + // Restricting the page to 1 on development, which drastically reduces the amount of events that is fetched + if s.cfg.Env == "development" { + pageLimit = 1 + sportIDs = []int{1} + } else { + pageLimit = 200 + sportIDs = []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91} + } + + var skippedLeague []string + var totalEvents = 0 + nilAway := 0 for sportIndex, sportID := range sportIDs { var totalPages int = 1 var page int = 0 - var count int = 0 - var skippedLeague []string - var totalEvents = 0 + var pageCount int = 0 + var sportEvents = 0 logger := s.mongoLogger.With( zap.String("source", string(source)), zap.Int("sport_id", sportID), zap.String("sport_name", domain.Sport(sportID).String()), - zap.Int("count", count), - zap.Int("totalEvents", totalEvents), + zap.Int("count", pageCount), zap.Int("Skipped leagues", len(skippedLeague)), ) for page <= totalPages { @@ -301,34 +325,33 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_ur // } event := domain.CreateEvent{ - ID: ev.ID, - SportID: convertInt32(ev.SportID), - MatchName: "", - HomeTeam: ev.Home.Name, - AwayTeam: "", // handle nil safely - HomeTeamID: convertInt64(ev.Home.ID), - AwayTeamID: 0, - HomeTeamImage: "", - AwayTeamImage: "", - LeagueID: convertInt64(ev.League.ID), - LeagueName: ev.League.Name, - StartTime: time.Unix(startUnix, 0).UTC(), - Source: source, - IsLive: false, - Status: domain.STATUS_PENDING, + ID: ev.ID, + SportID: convertInt32(ev.SportID), + HomeTeam: ev.Home.Name, + AwayTeam: "", // handle nil safely + HomeTeamID: convertInt64(ev.Home.ID), + AwayTeamID: 0, + LeagueID: convertInt64(ev.League.ID), + LeagueName: ev.League.Name, + StartTime: time.Unix(startUnix, 0).UTC(), + Source: source, + IsLive: false, + Status: domain.STATUS_PENDING, + DefaultWinningUpperLimit: settingsList.DefaultWinningLimit, } if ev.Away != nil { - dataLogger.Info("event away is empty") event.AwayTeam = ev.Away.Name event.AwayTeamID = convertInt64(ev.Away.ID) event.MatchName = ev.Home.Name + " vs " + ev.Away.Name + } else { + nilAway += 1 } - ok, err := s.CheckAndInsertEventHistory(ctx, event) + ok, _ := s.CheckAndInsertEventHistory(ctx, event) - if err != nil { - dataLogger.Error("failed to check and insert event history", zap.Error(err)) - } + // if err != nil { + // dataLogger.Error("failed to check and insert event history", zap.Error(err)) + // } if ok { dataLogger.Info("event history has been recorded") @@ -338,7 +361,8 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_ur if err != nil { dataLogger.Error("failed to save upcoming event", zap.Error(err)) } - totalEvents += 1 + sportEvents += 1 + } // log.Printf("⚠️ Skipped leagues %v", len(skippedLeague)) @@ -346,26 +370,26 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_ur totalPages = data.Pager.Total / data.Pager.PerPage - if count >= pageLimit { + if pageCount >= pageLimit { break } if page > totalPages { break } - count++ + pageCount++ } - s.mongoLogger.Info( - "Successfully fetched upcoming events", - zap.String("source", string(source)), - zap.Int("totalEvents", totalEvents), - zap.Int("sport_id", sportID), - zap.String("sport_name", domain.Sport(sportID).String()), - zap.Int("page", page), - zap.Int("total_pages", totalPages), - zap.Int("Skipped leagues", len(skippedLeague)), - ) + logger.Info("Completed adding sport", zap.Int("number_of_events_in_sport", sportEvents)) + totalEvents += sportEvents } + + s.mongoLogger.Info( + "Successfully fetched upcoming events", + zap.String("source", string(source)), + zap.Int("totalEvents", totalEvents), + zap.Int("Skipped leagues", len(skippedLeague)), + zap.Int("Events with empty away data", nilAway), + ) } func (s *service) CheckAndInsertEventHistory(ctx context.Context, event domain.CreateEvent) (bool, error) { @@ -379,7 +403,9 @@ func (s *service) CheckAndInsertEventHistory(ctx context.Context, event domain.C ) if err != nil { - eventLogger.Error("failed to get event is_monitored", zap.Error(err)) + if err != pgx.ErrNoRows { + eventLogger.Info("failed to get event is_monitored", zap.Error(err)) + } return false, err } @@ -479,9 +505,14 @@ func (s *service) GetEventsWithSettings(ctx context.Context, companyID int64, fi return s.store.GetEventsWithSettings(ctx, companyID, filter) } -func (s *service) GetEventWithSettingByID(ctx context.Context, ID string, companyID int64) (domain.EventWithSettings, error) { +func (s *service) GetEventWithSettingByID(ctx context.Context, ID string, companyID int64) (domain.EventWithSettings, error) { return s.store.GetEventWithSettingByID(ctx, ID, companyID) } + func (s *service) UpdateEventSettings(ctx context.Context, event domain.CreateEventSettings) error { return s.store.UpdateEventSettings(ctx, event) } + +func (s *service) GetSportAndLeagueIDs(ctx context.Context, eventID string) ([]int64, error) { + return s.store.GetSportAndLeagueIDs(ctx, eventID) +} diff --git a/internal/services/league/port.go b/internal/services/league/port.go index b203c4f..54dc626 100644 --- a/internal/services/league/port.go +++ b/internal/services/league/port.go @@ -10,7 +10,7 @@ type Service interface { SaveLeague(ctx context.Context, league domain.CreateLeague) error SaveLeagueSettings(ctx context.Context, leagueSettings domain.CreateLeagueSettings) error GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.BaseLeague, error) - GetAllLeaguesByCompany(ctx context.Context, companyID int64, filter domain.LeagueFilter) ([]domain.LeagueWithSettings, error) + GetAllLeaguesByCompany(ctx context.Context, companyID int64, filter domain.LeagueFilter) ([]domain.LeagueWithSettings, int64, error) CheckLeagueSupport(ctx context.Context, leagueID int64, companyID int64) (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 d82b38f..9a3e1a3 100644 --- a/internal/services/league/service.go +++ b/internal/services/league/service.go @@ -29,7 +29,7 @@ func (s *service) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) return s.store.GetAllLeagues(ctx, filter) } -func (s *service) GetAllLeaguesByCompany(ctx context.Context, companyID int64, filter domain.LeagueFilter) ([]domain.LeagueWithSettings, error) { +func (s *service) GetAllLeaguesByCompany(ctx context.Context, companyID int64, filter domain.LeagueFilter) ([]domain.LeagueWithSettings, int64, error) { return s.store.GetAllLeaguesByCompany(ctx, companyID, filter) } diff --git a/internal/services/messenger/email.go b/internal/services/messenger/email.go index ddb3542..a99d2fe 100644 --- a/internal/services/messenger/email.go +++ b/internal/services/messenger/email.go @@ -3,10 +3,9 @@ package messenger import ( "context" "github.com/resend/resend-go/v2" - ) -func (s *Service) SendEmail(ctx context.Context, receiverEmail, message string, subject string) error { +func (s *Service) SendEmail(ctx context.Context, receiverEmail, message string, messageHTML string, subject string) error { apiKey := s.config.ResendApiKey client := resend.NewClient(apiKey) formattedSenderEmail := "FortuneBets <" + s.config.ResendSenderEmail + ">" @@ -15,6 +14,7 @@ func (s *Service) SendEmail(ctx context.Context, receiverEmail, message string, To: []string{receiverEmail}, Subject: subject, Text: message, + Html: messageHTML, } _, err := client.Emails.Send(params) diff --git a/internal/services/notfication/service.go b/internal/services/notfication/service.go deleted file mode 100644 index c6a5457..0000000 --- a/internal/services/notfication/service.go +++ /dev/null @@ -1,475 +0,0 @@ -package notificationservice - -import ( - "context" - "encoding/json" - "errors" - "log/slog" - "sync" - "time" - - "github.com/SamuelTariku/FortuneBet-Backend/internal/config" - "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 - // 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, - logger: logger, - connections: sync.Map{}, - 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 -} - -func (s *Service) addConnection(recipientID int64, c *websocket.Conn) { - if c == nil { - s.logger.Warn("[NotificationSvc.AddConnection] Attempted to add nil WebSocket connection", "recipientID", recipientID) - return - } - - s.connections.Store(recipientID, c) - s.logger.Info("[NotificationSvc.AddConnection] Added WebSocket connection", "recipientID", recipientID) -} - -func (s *Service) SendNotification(ctx context.Context, notification *domain.Notification) error { - notification.ID = helpers.GenerateID() - notification.Timestamp = time.Now() - notification.DeliveryStatus = domain.DeliveryStatusPending - - created, err := s.repo.CreateNotification(ctx, notification) - if err != nil { - s.logger.Error("[NotificationSvc.SendNotification] Failed to create notification", "id", notification.ID, "error", err) - return err - } - - notification = created - - if notification.DeliveryChannel == domain.DeliveryChannelInApp { - s.Hub.Broadcast <- map[string]interface{}{ - "type": "CREATED_NOTIFICATION", - "recipient_id": notification.RecipientID, - "payload": notification, - } - } - - select { - case s.notificationCh <- notification: - default: - s.logger.Warn("[NotificationSvc.SendNotification] Notification channel full, dropping notification", "id", notification.ID) - } - - return nil -} - -func (s *Service) MarkAsRead(ctx context.Context, notificationIDs []string, recipientID int64) error { - for _, notificationID := range notificationIDs { - _, err := s.repo.UpdateNotificationStatus(ctx, notificationID, string(domain.DeliveryStatusSent), true, nil) - if err != nil { - s.logger.Error("[NotificationSvc.MarkAsRead] Failed to mark notification as read", "notificationID", notificationID, "recipientID", recipientID, "error", err) - return err - } - - // count, err := s.repo.CountUnreadNotifications(ctx, recipientID) - // if err != nil { - // s.logger.Error("[NotificationSvc.MarkAsRead] Failed to count unread notifications", "recipientID", recipientID, "error", err) - // return err - // } - - // s.Hub.Broadcast <- map[string]interface{}{ - // "type": "COUNT_NOT_OPENED_NOTIFICATION", - // "recipient_id": recipientID, - // "payload": map[string]int{ - // "not_opened_notifications_count": int(count), - // }, - // } - - s.logger.Info("[NotificationSvc.MarkAsRead] Notification marked as read", "notificationID", notificationID, "recipientID", recipientID) - } - - return nil -} - -func (s *Service) ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error) { - notifications, err := s.repo.ListNotifications(ctx, recipientID, limit, offset) - if err != nil { - s.logger.Error("[NotificationSvc.ListNotifications] Failed to list notifications", "recipientID", recipientID, "limit", limit, "offset", offset, "error", err) - return nil, err - } - s.logger.Info("[NotificationSvc.ListNotifications] Successfully listed notifications", "recipientID", recipientID, "count", len(notifications)) - return notifications, nil -} - -func (s *Service) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) { - notifications, err := s.repo.GetAllNotifications(ctx, limit, offset) - if err != nil { - s.logger.Error("[NotificationSvc.ListNotifications] Failed to get all notifications") - return nil, err - } - s.logger.Info("[NotificationSvc.ListNotifications] Successfully retrieved all notifications", "count", len(notifications)) - return notifications, nil -} - -func (s *Service) ConnectWebSocket(ctx context.Context, recipientID int64, c *websocket.Conn) error { - s.addConnection(recipientID, c) - s.logger.Info("[NotificationSvc.ConnectWebSocket] WebSocket connection established", "recipientID", recipientID) - return nil -} - -func (s *Service) DisconnectWebSocket(recipientID int64) { - if conn, loaded := s.connections.LoadAndDelete(recipientID); loaded { - conn.(*websocket.Conn).Close() - s.logger.Info("[NotificationSvc.DisconnectWebSocket] Disconnected WebSocket", "recipientID", recipientID) - } -} - -func (s *Service) SendSMS(ctx context.Context, recipientID int64, message string) error { - s.logger.Info("[NotificationSvc.SendSMS] SMS notification requested", "recipientID", recipientID, "message", message) - - apiKey := s.config.AFRO_SMS_API_KEY - senderName := s.config.AFRO_SMS_SENDER_NAME - receiverPhone := s.config.AFRO_SMS_RECEIVER_PHONE_NUMBER - hostURL := s.config.ADRO_SMS_HOST_URL - endpoint := "/api/send" - - request := afro.GetRequest(apiKey, endpoint, hostURL) - request.Method = "GET" - request.Sender(senderName) - request.To(receiverPhone, message) - - response, err := afro.MakeRequestWithContext(ctx, request) - if err != nil { - s.logger.Error("[NotificationSvc.SendSMS] Failed to send SMS", "recipientID", recipientID, "error", err) - return err - } - - if response["acknowledge"] == "success" { - s.logger.Info("[NotificationSvc.SendSMS] SMS sent successfully", "recipientID", recipientID) - } else { - s.logger.Error("[NotificationSvc.SendSMS] Failed to send SMS", "recipientID", recipientID, "response", response["response"]) - return errors.New("SMS delivery failed: " + response["response"].(string)) - } - - return nil -} - -func (s *Service) SendEmail(ctx context.Context, recipientID int64, subject, message string) error { - s.logger.Info("[NotificationSvc.SendEmail] Email notification requested", "recipientID", recipientID, "subject", subject) - return nil -} - -func (s *Service) startWorker() { - for { - select { - case notification := <-s.notificationCh: - s.handleNotification(notification) - case <-s.stopCh: - s.logger.Info("[NotificationSvc.StartWorker] Worker stopped") - return - } - } -} - -func (s *Service) ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) { - return s.repo.ListRecipientIDs(ctx, receiver) -} - -func (s *Service) handleNotification(notification *domain.Notification) { - ctx := context.Background() - - switch notification.DeliveryChannel { - case domain.DeliveryChannelSMS: - err := s.SendSMS(ctx, notification.RecipientID, notification.Payload.Message) - if err != nil { - notification.DeliveryStatus = domain.DeliveryStatusFailed - } else { - notification.DeliveryStatus = domain.DeliveryStatusSent - } - case domain.DeliveryChannelEmail: - err := s.SendEmail(ctx, notification.RecipientID, notification.Payload.Headline, notification.Payload.Message) - if err != nil { - notification.DeliveryStatus = domain.DeliveryStatusFailed - } else { - notification.DeliveryStatus = domain.DeliveryStatusSent - } - default: - if notification.DeliveryChannel != domain.DeliveryChannelInApp { - s.logger.Warn("[NotificationSvc.HandleNotification] Unsupported delivery channel", "channel", notification.DeliveryChannel) - notification.DeliveryStatus = domain.DeliveryStatusFailed - } - } - - if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil { - s.logger.Error("[NotificationSvc.HandleNotification] Failed to update notification status", "id", notification.ID, "error", err) - } -} - -func (s *Service) startRetryWorker() { - ticker := time.NewTicker(1 * time.Minute) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - s.retryFailedNotifications() - case <-s.stopCh: - s.logger.Info("[NotificationSvc.StartRetryWorker] Retry worker stopped") - return - } - } -} - -func (s *Service) retryFailedNotifications() { - ctx := context.Background() - failedNotifications, err := s.repo.ListFailedNotifications(ctx, 100) - if err != nil { - s.logger.Error("[NotificationSvc.RetryFailedNotifications] Failed to list failed notifications", "error", err) - return - } - - for _, n := range failedNotifications { - notification := &n - go func(notification *domain.Notification) { - for attempt := 0; attempt < 3; attempt++ { - time.Sleep(time.Duration(attempt) * time.Second) - 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 { - s.logger.Error("[NotificationSvc.RetryFailedNotifications] Failed to update after retry", "id", notification.ID, "error", err) - } - s.logger.Info("[NotificationSvc.RetryFailedNotifications] Successfully retried notification", "id", notification.ID) - return - } - 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 { - s.logger.Error("[NotificationSvc.RetryFailedNotifications] Failed to update after retry", "id", notification.ID, "error", err) - } - s.logger.Info("[NotificationSvc.RetryFailedNotifications] Successfully retried notification", "id", notification.ID) - return - } - } - } - s.logger.Error("[NotificationSvc.RetryFailedNotifications] Max retries reached for notification", "id", notification.ID) - }(notification) - } -} - -func (s *Service) CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) { - return s.repo.CountUnreadNotifications(ctx, recipient_id) -} - -// 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) UpdateLiveMetricForWallet(ctx context.Context, wallet domain.Wallet) { - var ( - payload domain.LiveWalletMetrics - event map[string]interface{} - key = "live_metrics" - ) - - // Try company first - company, companyErr := s.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.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.GetCompanyByWalletID(ctx, walletID) -} - -func (s *Service) GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error) { - return s.GetBranchByWalletID(ctx, walletID) -} diff --git a/internal/services/notification/service.go b/internal/services/notification/service.go index ae0b990..6ba4044 100644 --- a/internal/services/notification/service.go +++ b/internal/services/notification/service.go @@ -93,7 +93,7 @@ func (s *Service) addConnection(recipientID int64, c *websocket.Conn) error { } func (s *Service) SendNotification(ctx context.Context, notification *domain.Notification) error { - + notification.ID = helpers.GenerateID() notification.Timestamp = time.Now() notification.DeliveryStatus = domain.DeliveryStatusPending @@ -300,7 +300,7 @@ func (s *Service) handleNotification(notification *domain.Notification) { } case domain.DeliveryChannelEmail: - err := s.SendNotificationEmail(ctx, notification.RecipientID, notification.Payload.Headline, notification.Payload.Message) + err := s.SendNotificationEmail(ctx, notification.RecipientID, notification.Payload.Message, notification.Payload.Headline) if err != nil { notification.DeliveryStatus = domain.DeliveryStatusFailed } else { @@ -334,14 +334,22 @@ func (s *Service) SendNotificationSMS(ctx context.Context, recipientID int64, me } if !user.PhoneVerified { - return fmt.Errorf("Cannot send notification to unverified phone number") + return fmt.Errorf("cannot send notification to unverified phone number") } if user.PhoneNumber == "" { - return fmt.Errorf("Phone Number is invalid") + return fmt.Errorf("phone Number is invalid") } err = s.messengerSvc.SendSMS(ctx, user.PhoneNumber, message, user.CompanyID) if err != nil { + s.mongoLogger.Error("[NotificationSvc.HandleNotification] Failed to send notification SMS", + zap.Int64("recipient_id", recipientID), + zap.String("user_phone_number", user.PhoneNumber), + zap.String("message", message), + zap.Int64("company_id", user.CompanyID.Value), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return err } @@ -357,14 +365,22 @@ func (s *Service) SendNotificationEmail(ctx context.Context, recipientID int64, } if !user.EmailVerified { - return fmt.Errorf("Cannot send notification to unverified email") + return fmt.Errorf("cannot send notification to unverified email") } - if user.PhoneNumber == "" { - return fmt.Errorf("Email is invalid") + if user.Email == "" { + return fmt.Errorf("email is invalid") } - err = s.messengerSvc.SendEmail(ctx, user.PhoneNumber, message, subject) + err = s.messengerSvc.SendEmail(ctx, user.Email, message, message, subject) if err != nil { + s.mongoLogger.Error("[NotificationSvc.HandleNotification] Failed to send notification SMS", + zap.Int64("recipient_id", recipientID), + zap.String("user_email", user.Email), + zap.String("message", message), + zap.Int64("company_id", user.CompanyID.Value), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) return err } @@ -424,7 +440,7 @@ func (s *Service) retryFailedNotifications() { return } case domain.DeliveryChannelEmail: - if err := s.SendNotificationEmail(ctx, notification.RecipientID, notification.Payload.Headline, notification.Payload.Message); err == nil { + if err := s.SendNotificationEmail(ctx, notification.RecipientID, notification.Payload.Message, notification.Payload.Headline); err == nil { notification.DeliveryStatus = domain.DeliveryStatusSent if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil { s.mongoLogger.Error("[NotificationSvc.RetryFailedNotifications] Failed to update after retry", diff --git a/internal/services/odds/port.go b/internal/services/odds/port.go index d1026aa..3ec57b9 100644 --- a/internal/services/odds/port.go +++ b/internal/services/odds/port.go @@ -17,6 +17,11 @@ type Service interface { GetALLPrematchOdds(ctx context.Context) ([]domain.OddMarket, error) // GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) (domain.OddMarket, error) DeleteOddsForEvent(ctx context.Context, eventID string) error + + GetOddByID(ctx context.Context, id int64) (domain.OddMarket, error) + GetOddsWithSettingsByID(ctx context.Context, ID int64, companyID int64) (domain.OddMarketWithSettings, error) + // Settings + SaveOddsSetting(ctx context.Context, odd domain.CreateOddMarketSettings) error // Odd History InsertOddHistory(ctx context.Context, odd domain.CreateOddHistory) (domain.OddHistory, error) diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 33e8db8..b52bf0e 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -88,67 +88,49 @@ func (s *ServiceImpl) fetchBet365Odds(ctx context.Context) error { return err } - var errs []error - for index, event := range eventIDs { - log.Printf("📡 Fetching prematch odds for event ID: %v (%d/%d) ", event.ID, index, len(eventIDs)) + if s.config.Env == "development" { + log.Printf("📡 Fetching prematch odds for event ID: %v (%d/%d) ", event.ID, index, len(eventIDs)) + } + eventLogger := s.mongoLogger.With( + zap.String("eventID", event.ID), + zap.Int32("sportID", event.SportID), + ) oddsData, err := s.FetchNonLiveOddsByEventID(ctx, event.ID) if err != nil || oddsData.Success != 1 { - s.mongoLogger.Error( - "Failed to fetch prematch odds", - zap.String("eventID", event.ID), - zap.Error(err), - ) - errs = append(errs, fmt.Errorf("failed to fetch prematch odds for event %v: %w", event.ID, err)) + eventLogger.Error("Failed to fetch prematch odds", zap.Error(err)) continue } parsedOddSections, err := s.ParseOddSections(ctx, oddsData.Results[0], event.SportID) if err != nil { - s.mongoLogger.Error( - "Failed to parse odd section", - zap.String("eventID", event.ID), - zap.Int32("sportID", event.SportID), - zap.Error(err), - ) - errs = append(errs, fmt.Errorf("failed to parse odd section for event %v: %w", event.ID, err)) + eventLogger.Error("Failed to parse odd section", zap.Error(err)) continue } + parsedOddLogger := eventLogger.With( + zap.String("parsedOddSectionFI", parsedOddSections.EventFI), + zap.Int("main_sections_count", len(parsedOddSections.Sections)), + zap.Int("other_sections_count", len(parsedOddSections.OtherRes)), + ) + if parsedOddSections.EventFI == "" { - s.mongoLogger.Error( - "Skipping result with no valid Event FI field", - zap.String("FI", parsedOddSections.EventFI), - zap.String("eventID", event.ID), - zap.Int32("sportID", event.SportID), - zap.Error(err), - ) - errs = append(errs, errors.New("event FI is empty")) + parsedOddLogger.Error("Skipping result with no valid Event FI field", zap.Error(err)) continue } + if len(parsedOddSections.Sections) == 0 { + parsedOddLogger.Warn("Event has no odds in main sections", zap.Error(err)) + } for oddCategory, section := range parsedOddSections.Sections { if err := s.storeSection(ctx, event.ID, parsedOddSections.EventFI, oddCategory, section); err != nil { - s.mongoLogger.Error( - "Error storing odd section", - zap.String("eventID", event.ID), - zap.String("odd", oddCategory), - zap.Int32("sportID", event.SportID), - zap.Error(err), - ) - errs = append(errs, err) + parsedOddLogger.Error("Error storing odd section", zap.String("odd", oddCategory), zap.Error(err)) } } for _, section := range parsedOddSections.OtherRes { if err := s.storeSection(ctx, event.ID, parsedOddSections.EventFI, "others", section); err != nil { - s.mongoLogger.Error( - "Error storing odd other section", - zap.String("eventID", event.ID), - zap.Int32("sportID", event.SportID), - zap.Error(err), - ) - errs = append(errs, err) + parsedOddLogger.Error("Error storing odd other section", zap.Error(err)) continue } } @@ -157,10 +139,6 @@ func (s *ServiceImpl) fetchBet365Odds(ctx context.Context) error { } - for err := range errs { - log.Printf("❌ Error: %v", err) - } - return nil } @@ -345,6 +323,7 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage, if err := json.Unmarshal(res, &footballRes); err != nil { s.mongoLogger.Error( "Failed to unmarshal football result", + zap.Error(err), ) return domain.ParseOddSectionsRes{}, err @@ -534,6 +513,10 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage, func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName string, section domain.OddsSection) error { if len(section.Sp) == 0 { + s.mongoLogger.Warn("Event Section is empty", + zap.String("eventID", eventID), + zap.String("sectionName", sectionName), + ) return nil } @@ -542,41 +525,36 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName var errs []error for marketType, market := range section.Sp { + marketLogger := s.mongoLogger.With( + zap.String("eventID", eventID), + zap.String("sectionName", sectionName), + zap.String("market_id", string(market.ID)), + zap.String("marketType", marketType), + zap.String("marketName", market.Name), + ) if len(market.Odds) == 0 { + // marketLogger.Warn("Skipping market with no odds") continue } - // Check if the market id is a string - marketIDint := market.ID.Value - // if err != nil { - // s.mongoLogger.Error( - // "Invalid market id", - // zap.Int64("market_id", marketIDint), - // zap.String("market_name", market.Name), - // zap.String("eventID", eventID), - // zap.Error(err), - // ) - // continue - // } + marketIDint, err := strconv.ParseInt(string(market.ID), 10, 64) + if err != nil { + marketLogger.Warn("skipping market section where market_id is not int") + continue + } marketIDstr := strconv.FormatInt(marketIDint, 10) isSupported, ok := domain.SupportedMarkets[marketIDint] if !ok || !isSupported { - // s.logger.Info("Unsupported market_id", "marketID", marketIDint, "marketName", market.Name) + // marketLogger.Warn("skipping market that isn't supported", zap.Bool("is_market_found", ok)) continue } marketOdds, err := convertRawMessage(market.Odds) if err != nil { - s.mongoLogger.Error( - "failed to convert market.Odds to json.RawMessage to []map[string]interface{}", - zap.String("market_id", marketIDstr), - zap.String("market_name", market.Name), - zap.String("eventID", eventID), - zap.Error(err), - ) + marketLogger.Error("failed to convert market.Odds to json.RawMessage to []map[string]interface{}", zap.Error(err)) errs = append(errs, err) continue } @@ -593,25 +571,13 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName } if err := s.CheckAndInsertOddHistory(ctx, marketRecord); err != nil { - s.mongoLogger.Error( - "failed to check and insert odd history", - zap.String("market_id", marketIDstr), - zap.String("market_name", market.Name), - zap.String("eventID", eventID), - zap.Error(err), - ) + marketLogger.Error("failed to check and insert odd history", zap.Error(err)) continue } err = s.store.SaveOddMarket(ctx, marketRecord) if err != nil { - s.mongoLogger.Error( - "failed to save market", - zap.String("market_id", marketIDstr), - zap.String("market_name", market.Name), - zap.String("eventID", eventID), - zap.Error(err), - ) + marketLogger.Error("failed to save market", zap.Error(err)) errs = append(errs, fmt.Errorf("market %v: %w", market.ID, err)) continue } @@ -705,6 +671,46 @@ func (s *ServiceImpl) GetAllOddsWithSettings(ctx context.Context, companyID int6 return s.store.GetAllOddsWithSettings(ctx, companyID, filter) } +func (s *ServiceImpl) GetOddByID(ctx context.Context, id int64) (domain.OddMarket, error) { + return s.store.GetOddByID(ctx, id) +} + +func (s *ServiceImpl) SaveOddsSetting(ctx context.Context, odd domain.CreateOddMarketSettings) error { + return s.store.SaveOddsSetting(ctx, odd) +} + +func (s *ServiceImpl) SaveOddsSettingReq(ctx context.Context, companyID int64, req domain.CreateOddMarketSettingsReq) error { + + odd, err := s.GetOddsWithSettingsByID(ctx, req.OddMarketID, companyID) + if err != nil { + return err + } + + newOdds, err := convertRawMessage(odd.RawOdds) + if err != nil { + return err + } + + if len(req.CustomOdd) != 0 { + for _, customOdd := range req.CustomOdd { + for _, newOdd := range newOdds { + oldRawOddID := getInt(newOdd["id"]) + + if oldRawOddID == int(customOdd.OddID) { + newOdd["odds"] = customOdd.OddValue + } + } + } + } + + return s.SaveOddsSetting(ctx, domain.CreateOddMarketSettings{ + CompanyID: companyID, + OddMarketID: req.OddMarketID, + IsActive: domain.ConvertBoolPtr(req.IsActive), + CustomRawOdds: newOdds, + }) +} + func (s *ServiceImpl) GetOddsByMarketID(ctx context.Context, marketID string, eventID string) (domain.OddMarket, error) { rows, err := s.store.GetOddsByMarketID(ctx, marketID, eventID) if err != nil { @@ -722,6 +728,10 @@ func (s *ServiceImpl) GetOddsWithSettingsByMarketID(ctx context.Context, marketI return rows, nil } +func (s *ServiceImpl) GetOddsWithSettingsByID(ctx context.Context, ID int64, companyID int64) (domain.OddMarketWithSettings, error) { + return s.store.GetOddsWithSettingsByID(ctx, ID, companyID) +} + func (s *ServiceImpl) GetOddsByEventID(ctx context.Context, upcomingID string, filter domain.OddMarketWithEventFilter) ([]domain.OddMarket, error) { return s.store.GetOddsByEventID(ctx, upcomingID, filter) } diff --git a/internal/services/raffle/port.go b/internal/services/raffle/port.go new file mode 100644 index 0000000..39f5bfa --- /dev/null +++ b/internal/services/raffle/port.go @@ -0,0 +1,24 @@ +package raffle + +import ( + "context" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type RaffleStore interface { + CreateRaffle(ctx context.Context, raffle domain.CreateRaffle) (domain.Raffle, error) + AddSportRaffleFilter(ctx context.Context, raffleID int32, sportID, leagueID int64) error + DeleteRaffle(ctx context.Context, raffleID int32) (domain.Raffle, error) + GetRafflesOfCompany(ctx context.Context, companyID int32) ([]dbgen.Raffle, error) + GetRaffleStanding(ctx context.Context, raffleID, limit int32) ([]domain.RaffleStanding, error) + CreateRaffleWinner(ctx context.Context, raffleWinnerParams domain.RaffleWinnerParams) error + SetRaffleComplete(ctx context.Context, raffleID int32) error + CheckValidSportRaffleFilter(ctx context.Context, raffleID int32, sportID, leagueID int64) (bool, error) + + CreateRaffleTicket(ctx context.Context, raffleTicketParams domain.CreateRaffleTicket) (domain.RaffleTicket, error) + GetUserRaffleTickets(ctx context.Context, userID int32) ([]domain.RaffleTicketRes, error) + SuspendRaffleTicket(ctx context.Context, raffleTicketID int32) error + UnSuspendRaffleTicket(ctx context.Context, raffleID int32) error +} diff --git a/internal/services/raffle/service.go b/internal/services/raffle/service.go new file mode 100644 index 0000000..017d164 --- /dev/null +++ b/internal/services/raffle/service.go @@ -0,0 +1,66 @@ +package raffle + +import ( + "context" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type Service struct { + raffleStore RaffleStore +} + +func NewService(raffleStore RaffleStore) *Service { + return &Service{ + raffleStore: raffleStore, + } +} + +func (s *Service) CreateRaffle(ctx context.Context, raffle domain.CreateRaffle) (domain.Raffle, error) { + return s.raffleStore.CreateRaffle(ctx, raffle) +} + +func (s *Service) AddSportRaffleFilter(ctx context.Context, raffleID int32, sportID, leagueID int64) error { + return s.raffleStore.AddSportRaffleFilter(ctx, raffleID, sportID, leagueID) +} + +func (s *Service) DeleteRaffle(ctx context.Context, raffleID int32) (domain.Raffle, error) { + return s.raffleStore.DeleteRaffle(ctx, raffleID) +} + +func (s *Service) GetRafflesOfCompany(ctx context.Context, companyID int32) ([]dbgen.Raffle, error) { + return s.raffleStore.GetRafflesOfCompany(ctx, companyID) +} + +func (s *Service) GetRaffleStanding(ctx context.Context, raffleID, limit int32) ([]domain.RaffleStanding, error) { + return s.raffleStore.GetRaffleStanding(ctx, raffleID, limit) +} + +func (s *Service) CreateRaffleWinner(ctx context.Context, raffleWinnerParams domain.RaffleWinnerParams) error { + return s.raffleStore.CreateRaffleWinner(ctx, raffleWinnerParams) +} + +func (s *Service) SetRaffleComplete(ctx context.Context, raffleID int32) error { + return s.raffleStore.SetRaffleComplete(ctx, raffleID) +} + +func (s *Service) CreateRaffleTicket(ctx context.Context, raffleTicketParams domain.CreateRaffleTicket) (domain.RaffleTicket, error) { + return s.raffleStore.CreateRaffleTicket(ctx, raffleTicketParams) +} + +func (s *Service) GetUserRaffleTickets(ctx context.Context, userID int32) ([]domain.RaffleTicketRes, error) { + return s.raffleStore.GetUserRaffleTickets(ctx, userID) +} + +func (s *Service) SuspendRaffleTicket(ctx context.Context, raffleTicketID int32) error { + return s.raffleStore.SuspendRaffleTicket(ctx, raffleTicketID) +} + +func (s *Service) UnSuspendRaffleTicket(ctx context.Context, raffleID int32) error { + return s.raffleStore.UnSuspendRaffleTicket(ctx, raffleID) +} + +func (s *Service) CheckValidSportRaffleFilter(ctx context.Context, raffleID int32, sportID, leagueID int64) (bool, error) { + return s.raffleStore.CheckValidSportRaffleFilter(ctx, raffleID, sportID, leagueID) +} diff --git a/internal/services/referal/port.go b/internal/services/referal/port.go index 6add199..6930c0e 100644 --- a/internal/services/referal/port.go +++ b/internal/services/referal/port.go @@ -8,13 +8,10 @@ import ( type ReferralStore interface { GenerateReferralCode() (string, error) - CreateReferral(ctx context.Context, userID int64) error - ProcessReferral(ctx context.Context, referredPhone, referralCode string, companyID int64) error - ProcessDepositBonus(ctx context.Context, userID string, amount float64) error - GetReferralStats(ctx context.Context, userID string) (*domain.ReferralStats, error) - CreateReferralSettings(ctx context.Context, req domain.ReferralSettingsReq) error - UpdateReferralSettings(ctx context.Context, settings *domain.ReferralSettings) error - GetReferralSettings(ctx context.Context) (*domain.ReferralSettings, error) - GetReferralCountByID(ctx context.Context, referrerID string) (int64, error) - ProcessBetReferral(ctx context.Context, userPhone string, betAmount float64) error + CreateReferral(ctx context.Context, userID int64, companyID int64) error + ProcessReferral(ctx context.Context, referredPhone, referralCode string, companyID int64) error + ProcessDepositBonus(ctx context.Context, userPhone string, amount float64) error + ProcessBetReferral(ctx context.Context, userId int64, betAmount float64) error + GetReferralStats(ctx context.Context, userID int64, companyID int64) (*domain.ReferralStats, error) + GetReferralCountByID(ctx context.Context, referrerID int64) (int64, error) } diff --git a/internal/services/referal/service.go b/internal/services/referal/service.go index d89b023..eb5b021 100644 --- a/internal/services/referal/service.go +++ b/internal/services/referal/service.go @@ -7,333 +7,252 @@ import ( "errors" "fmt" "log/slog" - "strconv" - "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/settings" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" + "go.uber.org/zap" ) type Service struct { - repo repository.ReferralRepository - walletSvc wallet.Service - store *repository.Store - config *config.Config - logger *slog.Logger + repo repository.ReferralRepository + walletSvc wallet.Service + settingSvc settings.Service + config *config.Config + logger *slog.Logger + mongoLogger *zap.Logger } -func New(repo repository.ReferralRepository, walletSvc wallet.Service, store *repository.Store, cfg *config.Config, logger *slog.Logger) *Service { +func New(repo repository.ReferralRepository, walletSvc wallet.Service, settingSvc settings.Service, cfg *config.Config, logger *slog.Logger, mongoLogger *zap.Logger) *Service { return &Service{ - repo: repo, - walletSvc: walletSvc, - store: store, - config: cfg, - logger: logger, + repo: repo, + walletSvc: walletSvc, + settingSvc: settingSvc, + config: cfg, + logger: logger, + mongoLogger: mongoLogger, } } var ( - ErrInvalidReferral = errors.New("invalid or expired referral") - ErrInvalidReferralSignup = errors.New("referral requires phone signup") - ErrUserNotFound = errors.New("user not found") - ErrNoReferralFound = errors.New("no referral found for this user") + ErrInvalidReferral = errors.New("invalid or expired referral") + ErrUserNotFound = errors.New("user not found") + ErrNoReferralFound = errors.New("no referral found for this user") + ErrUserAlreadyHasReferralCode = errors.New("user already has an active referral code") + ErrMaxReferralCountLimitReached = errors.New("referral count limit has been reached") ) func (s *Service) GenerateReferralCode() (string, error) { b := make([]byte, 8) if _, err := rand.Read(b); err != nil { - s.logger.Error("Failed to generate random bytes for referral code", "error", err) + s.mongoLogger.Error("Failed to generate random bytes for referral code", zap.Error(err)) return "", err } code := base32.StdEncoding.EncodeToString(b)[:10] - s.logger.Debug("Generated referral code", "code", code) + s.mongoLogger.Debug("Generated referral code", zap.String("code", code)) return code, nil } -func (s *Service) CreateReferral(ctx context.Context, userID int64) error { - s.logger.Info("Creating referral code for user", "userID", userID) +func (s *Service) CreateReferralCode(ctx context.Context, userID int64, companyID int64) (domain.ReferralCode, error) { + + settingsList, err := s.settingSvc.GetOverrideSettingsList(ctx, companyID) + + if err != nil { + s.mongoLogger.Error("Failed to fetch settings", zap.Error(err)) + return domain.ReferralCode{}, err + } // check if user already has an active referral code - referral, err := s.repo.GetActiveReferralByReferrerID(ctx, fmt.Sprintf("%d", userID)) + referralCodes, err := s.repo.GetReferralCodesByUser(ctx, userID) if err != nil { - s.logger.Error("Failed to check if user alredy has active referral code", "error", err) - return err + s.mongoLogger.Error("Failed to check if user already has active referral code", zap.Int64("userID", userID), zap.Error(err)) + return domain.ReferralCode{}, err } - if referral != nil && referral.Status == domain.ReferralPending && referral.ExpiresAt.After(time.Now()) { - s.logger.Error("user already has an active referral code", "error", err) - return err - } - - settings, err := s.GetReferralSettings(ctx) - if err != nil || settings == nil { - s.logger.Error("Failed to fetch referral settings", "error", err) - return err - } - - // check referral count limit - referralCount, err := s.GetReferralCountByID(ctx, fmt.Sprintf("%d", userID)) - if err != nil { - s.logger.Error("Failed to get referral count", "userID", userID, "error", err) - return err - } - - fmt.Println("referralCount: ", referralCount) - if referralCount == int64(settings.MaxReferrals) { - s.logger.Error("referral count limit has been reached", "referralCount", referralCount, "error", err) - return err + if len(referralCodes) != 0 { + s.mongoLogger.Error("user already has an active referral code", zap.Int64("userID", userID), zap.Any("codes", referralCodes), zap.Error(err)) + return domain.ReferralCode{}, ErrUserAlreadyHasReferralCode } code, err := s.GenerateReferralCode() if err != nil { - s.logger.Error("Failed to generate referral code", "error", err) - return err + return domain.ReferralCode{}, err } - var rewardAmount float64 = settings.ReferralRewardAmount - var expireDuration time.Time = time.Now().Add(time.Duration((24 * settings.ExpiresAfterDays)) * time.Hour) + newReferralCode, err := s.repo.CreateReferralCode(ctx, domain.CreateReferralCode{ + ReferrerID: userID, + ReferralCode: code, + CompanyID: companyID, + NumberOfReferrals: settingsList.DefaultMaxReferrals, + RewardAmount: settingsList.ReferralRewardAmount, + }) - if err := s.repo.CreateReferral(ctx, &domain.Referral{ - ReferralCode: code, - ReferrerID: fmt.Sprintf("%d", userID), - Status: domain.ReferralPending, - RewardAmount: rewardAmount, - ExpiresAt: expireDuration, - }); err != nil { - return err + if err != nil { + return domain.ReferralCode{}, err } - return nil + return newReferralCode, nil } -func (s *Service) ProcessReferral(ctx context.Context, referredPhone, referralCode string, companyID int64) error { - s.logger.Info("Processing referral", "referredPhone", referredPhone, "referralCode", referralCode) - - referral, err := s.repo.GetReferralByCode(ctx, referralCode) - if err != nil || referral == nil { - s.logger.Error("Failed to get referral by code", "referralCode", referralCode, "error", err) - return err - } - - if referral.Status != domain.ReferralPending || referral.ExpiresAt.Before(time.Now()) { - s.logger.Warn("Invalid or expired referral", "referralCode", referralCode, "status", referral.Status) - return ErrInvalidReferral - } - - user, err := s.store.GetUserByPhone(ctx, referredPhone, domain.ValidInt64{ - Value: companyID, - Valid: true, - }) +func (s *Service) ProcessReferral(ctx context.Context, referredID int64, referralCode string, companyID int64) error { + paramLogger := s.mongoLogger.With( + zap.Int64("referredID", referredID), + zap.String("referralCode", referralCode), + zap.Int64("companyID", companyID), + ) + referral, err := s.repo.GetReferralCode(ctx, referralCode) if err != nil { - if errors.Is(err, domain.ErrUserNotFound) { - s.logger.Warn("User not found for referral", "referredPhone", referredPhone) - return ErrUserNotFound - } - s.logger.Error("Failed to get user by phone", "referredPhone", referredPhone, "error", err) - return err - } - if !user.PhoneVerified { - s.logger.Warn("Phone not verified for referral", "referredPhone", referredPhone) - return ErrInvalidReferralSignup - } - - referral.ReferredID = &referredPhone - referral.Status = domain.ReferralCompleted - referral.UpdatedAt = time.Now() - - if err := s.repo.UpdateReferral(ctx, referral); err != nil { - s.logger.Error("Failed to update referral", "referralCode", referralCode, "error", err) + paramLogger.Error("Failed to get referral by code", zap.Error(err)) return err } - referrerId, err := strconv.Atoi(referral.ReferrerID) + wallets, err := s.walletSvc.GetCustomerWallet(ctx, referral.ReferrerID) if err != nil { - s.logger.Error("Failed to convert referrer id", "referrerId", referral.ReferrerID, "error", err) - return err - } - - wallets, err := s.store.GetCustomerWallet(ctx, int64(referrerId)) - if err != nil { - s.logger.Error("Failed to get referrer wallets", "referrerId", referral.ReferrerID, "error", err) + paramLogger.Error("Failed to get referrer wallets", zap.Error(err)) return err } _, err = s.walletSvc.AddToWallet(ctx, wallets.StaticID, - domain.ToCurrency(float32(referral.RewardAmount)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, - fmt.Sprintf("Added %v to static wallet because of referral ID %v", referral.RewardAmount, referrerId), + referral.RewardAmount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, + fmt.Sprintf("Added %v to static wallet due to %v referral code being used", referral.RewardAmount, referral.ReferralCode), ) if err != nil { - s.logger.Error("Failed to add referral reward to static wallet", "walletID", wallets.StaticID, "referrer phone number", referredPhone, "error", err) + paramLogger.Error("Failed to add referral reward to static wallet", zap.Int64("static_wallet_id", wallets.StaticID), zap.Error(err)) return err } - s.logger.Info("Referral processed successfully", "referredPhone", referredPhone, "referralCode", referralCode, "rewardAmount", referral.RewardAmount) + _, err = s.repo.CreateUserReferral(ctx, domain.CreateUserReferrals{ + ReferredID: referredID, + ReferralCodeID: referral.ID, + }) + + if err != nil { + paramLogger.Error("Failed to create user referral", zap.Error(err)) + return err + } + + paramLogger.Info("Referral processed successfully", zap.String("rewardAmount", referral.ReferralCode)) return nil } -func (s *Service) ProcessDepositBonus(ctx context.Context, userPhone string, amount float64) error { - s.logger.Info("Processing deposit bonus", "userPhone", userPhone, "amount", amount) +func (s *Service) GetReferralStats(ctx context.Context, userID int64, companyID int64) (domain.ReferralStats, error) { + paramLogger := s.mongoLogger.With(zap.Int64("userID", userID), zap.Int64("companyID", companyID)) - settings, err := s.repo.GetSettings(ctx) + stats, err := s.repo.GetReferralStats(ctx, userID, companyID) if err != nil { - s.logger.Error("Failed to get referral settings", "error", err) - return err + paramLogger.Error("Failed to get referral stats", zap.Error(err)) + return domain.ReferralStats{}, err } - userID, err := strconv.ParseInt(userPhone, 10, 64) - if err != nil { - s.logger.Error("Invalid phone number format", "userPhone", userPhone, "error", err) - return errors.New("invalid phone number format") - } - - wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID) - if err != nil { - s.logger.Error("Failed to get wallets for user", "userID", userID, "error", err) - return err - } - if len(wallets) == 0 { - s.logger.Error("User has no wallet", "userID", userID) - return errors.New("user has no wallet") - } - - walletID := wallets[0].ID - bonus := amount * (settings.CashbackPercentage / 100) - currentBonus := float64(wallets[0].Balance) - _, err = s.walletSvc.AddToWallet(ctx, walletID, domain.ToCurrency(float32(currentBonus+bonus)), domain.ValidInt64{}, - domain.TRANSFER_DIRECT, domain.PaymentDetails{}, - fmt.Sprintf("Added %v to static wallet because of Deposit Cashback Bonus %d", currentBonus+bonus, bonus)) - if err != nil { - s.logger.Error("Failed to add deposit bonus to wallet", "walletID", walletID, "userID", userID, "bonus", bonus, "error", err) - return err - } - - s.logger.Info("Deposit bonus processed successfully", "userPhone", userPhone, "bonus", bonus) - return nil -} - -func (s *Service) ProcessBetReferral(ctx context.Context, userPhone string, betAmount float64) error { - s.logger.Info("Processing bet referral", "userPhone", userPhone, "betAmount", betAmount) - - settings, err := s.repo.GetSettings(ctx) - if err != nil { - s.logger.Error("Failed to get referral settings", "error", err) - return err - } - - referral, err := s.repo.GetReferralByReferredID(ctx, userPhone) - if err != nil { - s.logger.Error("Failed to get referral by referred ID", "userPhone", userPhone, "error", err) - return err - } - if referral == nil || referral.Status != domain.ReferralCompleted { - s.logger.Warn("No valid referral found", "userPhone", userPhone, "status", referral.Status) - return ErrNoReferralFound - } - - referrerID, err := strconv.ParseInt(referral.ReferrerID, 10, 64) - if err != nil { - s.logger.Error("Invalid referrer phone number format", "referrerID", referral.ReferrerID, "error", err) - return errors.New("invalid referrer phone number format") - } - - wallets, err := s.walletSvc.GetWalletsByUser(ctx, referrerID) - if err != nil { - s.logger.Error("Failed to get wallets for referrer", "referrerID", referrerID, "error", err) - return err - } - if len(wallets) == 0 { - s.logger.Error("Referrer has no wallet", "referrerID", referrerID) - return errors.New("referrer has no wallet") - } - - bonusPercentage := settings.BetReferralBonusPercentage - if bonusPercentage == 0 { - bonusPercentage = 5.0 - s.logger.Debug("Using default bet referral bonus percentage", "percentage", bonusPercentage) - } - bonus := betAmount * (bonusPercentage / 100) - - walletID := wallets[0].ID - currentBalance := float64(wallets[0].Balance) - _, err = s.walletSvc.AddToWallet(ctx, walletID, domain.ToCurrency(float32(currentBalance+bonus)), domain.ValidInt64{}, - domain.TRANSFER_DIRECT, domain.PaymentDetails{}, - fmt.Sprintf("Added %v to static wallet because of bet referral", referral.RewardAmount)) - if err != nil { - s.logger.Error("Failed to add bet referral bonus to wallet", "walletID", walletID, "referrerID", referrerID, "bonus", bonus, "error", err) - return err - } - - s.logger.Info("Bet referral processed successfully", "userPhone", userPhone, "referrerID", referrerID, "bonus", bonus) - return nil -} - -func (s *Service) GetReferralStats(ctx context.Context, userPhone string) (*domain.ReferralStats, error) { - s.logger.Info("Fetching referral stats", "userPhone", userPhone) - - stats, err := s.repo.GetReferralStats(ctx, userPhone) - if err != nil { - s.logger.Error("Failed to get referral stats", "userPhone", userPhone, "error", err) - return nil, err - } - - s.logger.Info("Referral stats retrieved successfully", "userPhone", userPhone, "totalReferrals", stats.TotalReferrals) return stats, nil } -func (s *Service) CreateReferralSettings(ctx context.Context, req domain.ReferralSettingsReq) error { - s.logger.Info("Creating referral setting") - - if err := s.repo.CreateSettings(ctx, &domain.ReferralSettings{ - ReferralRewardAmount: req.ReferralRewardAmount, - CashbackPercentage: req.CashbackPercentage, - MaxReferrals: req.MaxReferrals, - ExpiresAfterDays: req.ExpiresAfterDays, - UpdatedBy: req.UpdatedBy, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - }); err != nil { - s.logger.Error("Failed to create referral setting", "error", err) - return err - } - - s.logger.Info("Referral setting created succesfully") - return nil -} - -func (s *Service) UpdateReferralSettings(ctx context.Context, settings *domain.ReferralSettings) error { - s.logger.Info("Updating referral settings", "settingsID", settings.ID) - - settings.UpdatedAt = time.Now() - err := s.repo.UpdateSettings(ctx, settings) +func (s *Service) GetUserReferralCount(ctx context.Context, referrerID int64) (int64, error) { + count, err := s.repo.GetUserReferralCount(ctx, referrerID) if err != nil { - s.logger.Error("Failed to update referral settings", "settingsID", settings.ID, "error", err) - return err - } - - s.logger.Info("Referral settings updated successfully", "settingsID", settings.ID) - return nil -} - -func (s *Service) GetReferralSettings(ctx context.Context) (*domain.ReferralSettings, error) { - s.logger.Info("Fetching referral settings") - - settings, err := s.repo.GetSettings(ctx) - if err != nil { - s.logger.Error("Failed to get referral settings", "error", err) - return nil, err - } - - s.logger.Info("Referral settings retrieved successfully", "settings", settings) - return settings, nil -} - -func (s *Service) GetReferralCountByID(ctx context.Context, referrerID string) (int64, error) { - count, err := s.repo.GetReferralCountByID(ctx, referrerID) - if err != nil { - s.logger.Error("Failed to get referral count", "userID", referrerID, "error", err) + s.mongoLogger.Error("Failed to get referral count", zap.Int64("referrerID", referrerID), zap.Error(err)) return 0, err } return count, nil } + +func (s *Service) GetReferralCodesByUser(ctx context.Context, userID int64) ([]domain.ReferralCode, error) { + return s.repo.GetReferralCodesByUser(ctx, userID) +} + +func (s *Service) GetReferralCode(ctx context.Context, code string) (domain.ReferralCode, error) { + return s.repo.GetReferralCode(ctx, code) +} + +func (s *Service) UpdateReferralCode(ctx context.Context, referral domain.UpdateReferralCode) error { + return s.repo.UpdateReferralCode(ctx, referral) +} + +func (s *Service) GetUserReferral(ctx context.Context, referrerID int64) (domain.UserReferral, error) { + return s.repo.GetUserReferral(ctx, referrerID) +} + +func (s *Service) GetUserReferralsByCode(ctx context.Context, code string) ([]domain.UserReferral, error) { + return s.repo.GetUserReferralsByCode(ctx, code) +} + +// func (s *Service) ProcessDepositBonus(ctx context.Context, userID int64, amount float32, companyID int64) error { +// settingsList, err := s.settingSvc.GetOverrideSettingsList(ctx, companyID) +// if err != nil { +// s.logger.Error("Failed to fetch settings", "error", err) +// return err +// } + +// s.logger.Info("Processing deposit bonus", "amount", amount) + +// customerWallet, err := s.walletSvc.GetCustomerWallet(ctx, userID) +// if err != nil { +// s.logger.Error("Failed to get wallets for user", "userID", userID, "error", err) +// return err +// } + +// bonus := amount * settingsList.CashbackPercentage + +// _, err = s.walletSvc.AddToWallet(ctx, customerWallet.StaticID, domain.ToCurrency(bonus), domain.ValidInt64{}, +// domain.TRANSFER_DIRECT, domain.PaymentDetails{}, +// fmt.Sprintf("Added to bonus wallet because of Deposit Cashback Bonus %d", bonus)) +// if err != nil { +// s.logger.Error("Failed to add deposit bonus to wallet", "staticWalletID", customerWallet.StaticID, "userID", userID, "bonus", bonus, "error", err) +// return err +// } + +// s.logger.Info("Deposit bonus processed successfully", "bonus", bonus) +// return nil +// } + +// func (s *Service) ProcessBetReferral(ctx context.Context, userId int64, betAmount float64) error { +// s.logger.Info("Processing bet referral", "userID", userId, "betAmount", betAmount) + +// settings, err := s.repo.GetSettings(ctx) +// if err != nil { +// s.logger.Error("Failed to get referral settings", "error", err) +// return err +// } + +// referral, err := s.repo.GetReferralByReferredID(ctx, userId) +// if err != nil { +// s.logger.Error("Failed to get referral by referred ID", "userId", userId, "error", err) +// return err +// } +// if referral == nil || referral.Status != domain.ReferralCompleted { +// s.logger.Warn("No valid referral found", "userId", userId, "status", referral.Status) +// return ErrNoReferralFound +// } + +// wallets, err := s.walletSvc.GetWalletsByUser(ctx, referral.ReferrerID) +// if err != nil { +// s.logger.Error("Failed to get wallets for referrer", "referrerID", referral.ReferrerID, "error", err) +// return err +// } +// if len(wallets) == 0 { +// s.logger.Error("Referrer has no wallet", "referrerID", referral.ReferrerID) +// return errors.New("referrer has no wallet") +// } + +// bonusPercentage := settings.BetReferralBonusPercentage +// if bonusPercentage == 0 { +// bonusPercentage = 5.0 +// s.logger.Debug("Using default bet referral bonus percentage", "percentage", bonusPercentage) +// } +// bonus := betAmount * (bonusPercentage / 100) + +// walletID := wallets[0].ID +// currentBalance := float64(wallets[0].Balance) +// _, err = s.walletSvc.AddToWallet(ctx, walletID, domain.ToCurrency(float32(currentBalance+bonus)), domain.ValidInt64{}, +// domain.TRANSFER_DIRECT, domain.PaymentDetails{}, +// fmt.Sprintf("Added %v to static wallet because of bet referral", referral.RewardAmount)) +// if err != nil { +// s.logger.Error("Failed to add bet referral bonus to wallet", "walletID", walletID, "referrerID", referral.ReferrerID, "bonus", bonus, "error", err) +// return err +// } + +// s.logger.Info("Bet referral processed successfully", "referrer ID", referral.ReferrerID, "referrerID", referral.ReferrerID, "bonus", bonus) +// return nil +// } diff --git a/internal/services/result/service.go b/internal/services/result/service.go index ad1f1c5..cb19963 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -16,6 +16,7 @@ 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" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/messenger" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" @@ -33,6 +34,7 @@ type Service struct { eventSvc event.Service leagueSvc league.Service notificationSvc *notificationservice.Service + messengerSvc *messenger.Service userSvc user.Service } @@ -46,6 +48,7 @@ func NewService( eventSvc event.Service, leagueSvc league.Service, notificationSvc *notificationservice.Service, + messengerSvc *messenger.Service, userSvc user.Service, ) *Service { return &Service{ @@ -59,6 +62,7 @@ func NewService( eventSvc: eventSvc, leagueSvc: leagueSvc, notificationSvc: notificationSvc, + messengerSvc: messengerSvc, userSvc: userSvc, } } @@ -491,6 +495,7 @@ func (s *Service) CheckAndSendResultNotifications(ctx context.Context, createdAf } func buildHeadlineAndMessage(counts domain.ResultLog) (string, string) { + totalIssues := counts.StatusNotFinishedCount + counts.StatusToBeFixedCount + counts.StatusPostponedCount + counts.StatusRemovedCount totalBets := counts.StatusEndedBets + counts.StatusNotFinishedBets + counts.StatusPostponedBets + counts.StatusRemovedBets + counts.StatusToBeFixedBets if totalIssues == 0 { @@ -517,10 +522,123 @@ func buildHeadlineAndMessage(counts domain.ResultLog) (string, string) { } headline := "⚠️ Issues Found Processing Event Results" - message := fmt.Sprintf("Processed expired event results: %s. Please review pending entries.", strings.Join(parts, ", ")) + message := fmt.Sprintf("Processed expired event results: %s. Please review pending entries.", strings.Join(parts, ", ")) return headline, message } +func buildHeadlineAndMessageEmail(counts domain.ResultLog, user domain.User) (string, string, string) { + totalIssues := counts.StatusNotFinishedCount + counts.StatusToBeFixedCount + + counts.StatusPostponedCount + counts.StatusRemovedCount + totalEvents := counts.StatusEndedCount + counts.StatusNotFinishedCount + + counts.StatusToBeFixedCount + counts.StatusPostponedCount + counts.StatusRemovedCount + totalBets := counts.StatusEndedBets + counts.StatusNotFinishedBets + + counts.StatusPostponedBets + counts.StatusRemovedBets + counts.StatusToBeFixedBets + + greeting := fmt.Sprintf("Hi %s %s,", user.FirstName, user.LastName) + + if totalIssues == 0 { + headline := "✅ Daily Results Report — All Events Processed Successfully" + plain := fmt.Sprintf(`%s + +Daily Results Summary: +- %d Ended Events +- %d Total Bets + +All events were processed successfully, and no issues were detected. + +Best regards, +The System`, greeting, counts.StatusEndedCount, totalBets) + + html := fmt.Sprintf(`

%s

+

Daily Results Summary

+ +

All events were processed successfully, and no issues were detected.

+

Best regards,
The System

`, + greeting, counts.StatusEndedCount, totalBets) + + return headline, plain, html + } + + partsPlain := []string{} + partsHTML := []string{} + + if counts.StatusNotFinishedCount > 0 { + partsPlain = append(partsPlain, + fmt.Sprintf("- %d Incomplete Events (%d Bets)", counts.StatusNotFinishedCount, counts.StatusNotFinishedBets)) + partsHTML = append(partsHTML, + fmt.Sprintf("
  • %d Incomplete Events (%d Bets)
  • ", counts.StatusNotFinishedCount, counts.StatusNotFinishedBets)) + } + if counts.StatusToBeFixedCount > 0 { + partsPlain = append(partsPlain, + fmt.Sprintf("- %d Requires Review (%d Bets)", counts.StatusToBeFixedCount, counts.StatusToBeFixedBets)) + partsHTML = append(partsHTML, + fmt.Sprintf("
  • %d Requires Review (%d Bets)
  • ", counts.StatusToBeFixedCount, counts.StatusToBeFixedBets)) + } + if counts.StatusPostponedCount > 0 { + partsPlain = append(partsPlain, + fmt.Sprintf("- %d Postponed Events (%d Bets)", counts.StatusPostponedCount, counts.StatusPostponedBets)) + partsHTML = append(partsHTML, + fmt.Sprintf("
  • %d Postponed Events (%d Bets)
  • ", counts.StatusPostponedCount, counts.StatusPostponedBets)) + } + if counts.StatusRemovedCount > 0 { + partsPlain = append(partsPlain, + fmt.Sprintf("- %d Discarded Events (%d Bets)", counts.StatusRemovedCount, counts.StatusRemovedBets)) + partsHTML = append(partsHTML, + fmt.Sprintf("
  • %d Discarded Events (%d Bets)
  • ", counts.StatusRemovedCount, counts.StatusRemovedBets)) + } + if counts.StatusEndedCount > 0 { + partsPlain = append(partsPlain, + fmt.Sprintf("- %d Successfully Ended Events (%d Bets)", counts.StatusEndedCount, counts.StatusEndedBets)) + partsHTML = append(partsHTML, + fmt.Sprintf("
  • %d Successfully Ended Events (%d Bets)
  • ", counts.StatusEndedCount, counts.StatusEndedBets)) + } + + headline := "⚠️ Daily Results Report — Review Required" + + plain := fmt.Sprintf(`%s + +Daily Results Summary: +%s + +Totals: +- %d Events Processed +- %d Total Bets + +Next Steps: +Some events require your attention. Please log into the admin dashboard to review pending issues. + +Best regards, +The System`, + greeting, + strings.Join(partsPlain, "\n"), + totalEvents, + totalBets, + ) + + html := fmt.Sprintf(`

    %s

    +

    Daily Results Summary

    + +

    Totals

    + +

    Next Steps:
    Some events require your attention. Please log into the admin dashboard to review pending issues.

    +

    Best regards,
    The System

    `, + greeting, + strings.Join(partsHTML, "\n"), + totalEvents, + totalBets, + ) + + return headline, plain, html +} + func (s *Service) SendAdminResultStatusErrorNotification( ctx context.Context, counts domain.ResultLog, @@ -541,9 +659,9 @@ func (s *Service) SendAdminResultStatusErrorNotification( } headline, message := buildHeadlineAndMessage(counts) - errorSeverity := domain.NotificationErrorSeverityLow + notification := &domain.Notification{ - ErrorSeverity: &errorSeverity, + ErrorSeverity: domain.NotificationErrorSeverityHigh, DeliveryStatus: domain.DeliveryStatusPending, IsRead: false, Type: domain.NOTIFICATION_TYPE_BET_RESULT, @@ -568,9 +686,14 @@ func (s *Service) SendAdminResultStatusErrorNotification( ) sendErrors = append(sendErrors, err) } - notification.DeliveryChannel = domain.DeliveryChannelEmail - if err := s.notificationSvc.SendNotification(ctx, notification); err != nil { - s.mongoLogger.Error("failed to send admin email notification", + // notification.DeliveryChannel = domain.DeliveryChannelEmail + if user.Email == "" { + continue + } + + subject, plain, html := buildHeadlineAndMessageEmail(counts, user) + if err := s.messengerSvc.SendEmail(ctx, user.Email, plain, html, subject); err != nil { + s.mongoLogger.Error("failed to send admin result report email", zap.Int64("admin_id", user.ID), zap.Error(err), ) @@ -884,17 +1007,17 @@ func (s *Service) GetResultsForEvent(ctx context.Context, eventID string) (json. outcomes := make([]domain.BetOutcome, 0) for _, section := range parsedOddSections.Sections { for _, market := range section.Sp { - marketIDint := market.ID.Value - // if err != nil { - // s.mongoLogger.Error( - // "Invalid market id", - // zap.Int64("market_id", marketIDint), - // zap.String("market_name", market.Name), - // zap.String("eventID", eventID), - // zap.Error(err), - // ) - // continue - // } + marketIDint, err := strconv.ParseInt(string(market.ID), 10, 64) + if err != nil { + s.mongoLogger.Warn( + "Invalid market id", + zap.Int64("market_id", marketIDint), + zap.String("market_name", market.Name), + zap.String("eventID", eventID), + zap.Error(err), + ) + continue + } isSupported, ok := domain.SupportedMarkets[marketIDint] diff --git a/internal/services/transaction/shop_bet.go b/internal/services/transaction/shop_bet.go index 6c1ed20..372726f 100644 --- a/internal/services/transaction/shop_bet.go +++ b/internal/services/transaction/shop_bet.go @@ -52,6 +52,7 @@ func (s *Service) CreateShopBet(ctx context.Context, userID int64, role domain.R newBet, err := s.betSvc.PlaceBet(ctx, domain.CreateBetReq{ Outcomes: req.Outcomes, Amount: req.Amount, + BranchID: branchID, }, userID, role, *companyID) if err != nil { @@ -94,6 +95,10 @@ func (s *Service) CreateShopBet(ctx context.Context, userID int64, role domain.R }, }) + if err != nil { + return domain.ShopBet{}, err + } + return s.transactionStore.CreateShopBet(ctx, domain.CreateShopBet{ ShopTransactionID: newTransaction.ID, CashoutID: cashoutID, diff --git a/internal/services/user/common.go b/internal/services/user/common.go index c14403c..40b1814 100644 --- a/internal/services/user/common.go +++ b/internal/services/user/common.go @@ -31,7 +31,7 @@ func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpF return fmt.Errorf("invalid sms provider: %s", provider) } case domain.OtpMediumEmail: - if err := s.messengerSvc.SendEmail(ctx, sentTo, message, "FortuneBets - One Time Password"); err != nil { + if err := s.messengerSvc.SendEmail(ctx, sentTo, message, message, "FortuneBets - One Time Password"); err != nil { return err } } diff --git a/internal/services/user/direct.go b/internal/services/user/direct.go index bbad0b6..9f1a40f 100644 --- a/internal/services/user/direct.go +++ b/internal/services/user/direct.go @@ -19,6 +19,7 @@ func (s *Service) CreateUser(ctx context.Context, User domain.CreateUserReq, is_ // User.BranchID = branchId // User.Role = string(domain.RoleBranchManager) // } + hashedPassword, err := hashPassword(User.Password) if err != nil { return domain.User{}, err diff --git a/internal/services/user/port.go b/internal/services/user/port.go index 8625f35..321ee77 100644 --- a/internal/services/user/port.go +++ b/internal/services/user/port.go @@ -23,7 +23,7 @@ type UserStore interface { GetUserByEmail(ctx context.Context, email string, companyID domain.ValidInt64) (domain.User, error) GetUserByPhone(ctx context.Context, phoneNum string, companyID domain.ValidInt64) (domain.User, error) SearchUserByNameOrPhone(ctx context.Context, searchString string, role *domain.Role, companyID domain.ValidInt64) ([]domain.User, error) - UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64) error // identifier verified email or phone + UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64, companyId int64) error GetCustomerCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) GetCustomerDetails(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.CustomerDetail, error) diff --git a/internal/services/user/reset.go b/internal/services/user/reset.go index 0b5cc0f..4039e68 100644 --- a/internal/services/user/reset.go +++ b/internal/services/user/reset.go @@ -58,7 +58,7 @@ func (s *Service) ResetPassword(ctx context.Context, resetReq domain.ResetPasswo } // reset pass and mark otp as used - err = s.userStore.UpdatePassword(ctx, sentTo, hashedPassword, otp.ID) + err = s.userStore.UpdatePassword(ctx, sentTo, hashedPassword, otp.ID, resetReq.CompanyID) if err != nil { return err } diff --git a/internal/services/virtualGame/game_orchestration.go b/internal/services/virtualGame/game_orchestration.go index 69a463c..73972b0 100644 --- a/internal/services/virtualGame/game_orchestration.go +++ b/internal/services/virtualGame/game_orchestration.go @@ -33,10 +33,22 @@ func (s *service) ListProviders(ctx context.Context, limit, offset int32) ([]dom // Convert []dbgen.VirtualGameProvider to []domain.VirtualGameProvider domainProviders := make([]domain.VirtualGameProvider, len(providers)) for i, p := range providers { + var logoDark *string + if p.LogoDark.Valid { + logoDark = &p.LogoDark.String + } + + var logoLight *string + if p.LogoLight.Valid { + logoLight = &p.LogoLight.String + } + domainProviders[i] = domain.VirtualGameProvider{ ProviderID: p.ProviderID, ProviderName: p.ProviderName, Enabled: p.Enabled, + LogoDark: logoDark, + LogoLight: logoLight, CreatedAt: p.CreatedAt.Time, UpdatedAt: &p.UpdatedAt.Time, // Add other fields as needed diff --git a/internal/services/virtualGame/veli/game_orchestration.go b/internal/services/virtualGame/veli/game_orchestration.go index 18621c1..a371296 100644 --- a/internal/services/virtualGame/veli/game_orchestration.go +++ b/internal/services/virtualGame/veli/game_orchestration.go @@ -7,11 +7,16 @@ import ( dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/jackc/pgx/v5/pgtype" + "go.uber.org/zap" ) func (s *Service) AddProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) { + + logger := s.mongoLogger.With(zap.String("service", "AddProviders"), zap.Any("ProviderRequest", req)) + // 0. Remove all existing providers first if err := s.repo.DeleteAllVirtualGameProviders(ctx); err != nil { + logger.Error("failed to delete all virtual game providers", zap.Error(err)) return nil, fmt.Errorf("failed to clear existing providers: %w", err) } @@ -21,6 +26,7 @@ func (s *Service) AddProviders(ctx context.Context, req domain.ProviderRequest) } // Optional fields + sigParams["extraData"] = fmt.Sprintf("%t", req.ExtraData) // false is still included if req.Size > 0 { sigParams["size"] = fmt.Sprintf("%d", req.Size) } else { @@ -50,6 +56,7 @@ func (s *Service) AddProviders(ctx context.Context, req domain.ProviderRequest) } if _, err := s.repo.CreateVirtualGameProvider(ctx, createParams); err != nil { + logger.Error("failed to add provider", zap.Error(err)) return nil, fmt.Errorf("failed to add provider %s: %w", p.ProviderID, err) } } @@ -58,8 +65,8 @@ func (s *Service) AddProviders(ctx context.Context, req domain.ProviderRequest) popokParams := dbgen.CreateVirtualGameProviderParams{ ProviderID: "popok", ProviderName: "Popok Gaming", - LogoDark: pgtype.Text{String: "/static/logos/popok-dark.png", Valid: true}, // adjust as needed - LogoLight: pgtype.Text{String: "/static/logos/popok-light.png", Valid: true}, // adjust as needed + LogoDark: pgtype.Text{String: fmt.Sprintf("%v/static/logos/popok-dark.png", s.cfg.PopOK.CallbackURL), Valid: true}, // adjust as needed + LogoLight: pgtype.Text{String: fmt.Sprintf("%v/static/logos/popok-light.png", s.cfg.PopOK.CallbackURL), Valid: true}, // adjust as needed Enabled: true, } @@ -72,6 +79,7 @@ func (s *Service) AddProviders(ctx context.Context, req domain.ProviderRequest) } if _, err := s.repo.CreateVirtualGameProvider(ctx, popokParams); err != nil { + logger.Error("failed to add popok provider", zap.Any("popokParams", popokParams), zap.Error(err)) return nil, fmt.Errorf("failed to add popok provider: %w", err) } @@ -92,9 +100,10 @@ func (s *Service) AddProviders(ctx context.Context, req domain.ProviderRequest) func (s *Service) GetAllVirtualGames(ctx context.Context, params dbgen.GetAllVirtualGamesParams) ([]domain.UnifiedGame, error) { // Build params for repo call - + logger := s.mongoLogger.With(zap.String("service", "GetAllVirtualGames"), zap.Any("params", params)) rows, err := s.repo.ListAllVirtualGames(ctx, params) if err != nil { + logger.Error("[GetAllVirtualGames] Failed to fetch virtual games", zap.Error(err)) return nil, fmt.Errorf("failed to fetch virtual games: %w", err) } @@ -140,16 +149,29 @@ func (s *Service) GetAllVirtualGames(ctx context.Context, params dbgen.GetAllVir } func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.ProviderRequest, currency string) ([]domain.UnifiedGame, error) { - var allGames []domain.UnifiedGame + logger := s.mongoLogger.With( + zap.String("service", "FetchAndStoreAllVirtualGames"), + zap.Any("ProviderRequest", req), + ) + + // This is necessary, since the provider is a foreign key + _, err := s.AddProviders(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to add providers to database: %w", err) + } + + var allGames []domain.UnifiedGame // --- 1. Get providers from external API --- providersRes, err := s.GetProviders(ctx, req) if err != nil { + logger.Error("Failed to fetch provider", zap.Error(err)) return nil, fmt.Errorf("failed to fetch providers: %w", err) } // --- 2. Fetch games for each provider --- for _, p := range providersRes.Items { + // Violates foreign key if the provider isn't added games, err := s.GetGames(ctx, domain.GameListRequest{ BrandID: s.cfg.VeliGames.BrandID, ProviderID: p.ProviderID, @@ -157,6 +179,7 @@ func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.P Size: req.Size, }) if err != nil { + logger.Error("failed to get veli games", zap.String("ProviderID", p.ProviderID), zap.Error(err)) continue // skip failing provider but continue others } @@ -176,7 +199,7 @@ func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.P allGames = append(allGames, unified) // --- Save to DB --- - _, _ = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{ + _, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{ GameID: g.GameID, ProviderID: g.ProviderID, Name: g.Name, @@ -202,18 +225,23 @@ func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.P // Thumbnail: g.Thumbnail, // Status: g.Status, }) + + if err != nil { + logger.Error("failed to create virtual game", zap.Error(err)) + } } } // --- 3. Handle PopOK separately --- popokGames, err := s.virtualGameSvc.ListGames(ctx, currency) if err != nil { + logger.Error("failed to fetch PopOk games", zap.Error(err)) return nil, fmt.Errorf("failed to fetch PopOK games: %w", err) } for _, g := range popokGames { unified := domain.UnifiedGame{ - GameID: fmt.Sprintf("popok-%d", g.ID), + GameID: fmt.Sprintf("%d", g.ID), ProviderID: "popok", Provider: "PopOK", Name: g.GameName, @@ -233,8 +261,8 @@ func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.P } // --- Save to DB --- - _, _ = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{ - GameID: fmt.Sprintf("popok-%d", g.ID), + _, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{ + GameID: fmt.Sprintf("%d", g.ID), //The id here needs to be clean for me to access ProviderID: "popok", Name: g.GameName, Bets: betsNumeric, @@ -247,6 +275,10 @@ func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.P Valid: true, }, }) + + if err != nil { + logger.Error("failed to create virtual game", zap.Error(err)) + } } return allGames, nil diff --git a/internal/services/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go index 0bf78a7..3f41633 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -11,6 +11,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" + "go.uber.org/zap" ) var ( @@ -26,16 +27,26 @@ type Service struct { client *Client walletSvc *wallet.Service transfetStore wallet.TransferStore + mongoLogger *zap.Logger cfg *config.Config } -func New(virtualGameSvc virtualgameservice.VirtualGameService, repo repository.VirtualGameRepository, client *Client, walletSvc *wallet.Service, transferStore wallet.TransferStore, cfg *config.Config) *Service { +func New( + virtualGameSvc virtualgameservice.VirtualGameService, + repo repository.VirtualGameRepository, + client *Client, + walletSvc *wallet.Service, + transferStore wallet.TransferStore, + mongoLogger *zap.Logger, + cfg *config.Config, +) *Service { return &Service{ virtualGameSvc: virtualGameSvc, repo: repo, client: client, walletSvc: walletSvc, transfetStore: transferStore, + mongoLogger: mongoLogger, cfg: cfg, } } @@ -66,20 +77,22 @@ func (s *Service) GetProviders(ctx context.Context, req domain.ProviderRequest) func (s *Service) GetGames(ctx context.Context, req domain.GameListRequest) ([]domain.GameEntity, error) { // 1. Check if provider is enabled in DB - provider, err := s.repo.GetVirtualGameProviderByID(ctx, req.ProviderID) - if err != nil { - return nil, fmt.Errorf("failed to check provider %s: %w", req.ProviderID, err) - } + // provider, err := s.repo.GetVirtualGameProviderByID(ctx, req.ProviderID) + // if err != nil { + // return nil, fmt.Errorf("failed to check provider %s: %w", req.ProviderID, err) + // } - if !provider.Enabled { - // Provider exists but is disabled → return empty list (or error if you prefer) - return nil, fmt.Errorf("provider %s is disabled", req.ProviderID) - } + // if !provider.Enabled { + // // Provider exists but is disabled → return empty list (or error if you prefer) + // return nil, fmt.Errorf("provider %s is disabled", req.ProviderID) + // } // 2. Prepare signature params sigParams := map[string]any{ "brandId": req.BrandID, "providerId": req.ProviderID, + "size": req.Size, + "page": req.Page, } // 3. Call external API @@ -95,15 +108,15 @@ func (s *Service) GetGames(ctx context.Context, req domain.GameListRequest) ([]d func (s *Service) StartGame(ctx context.Context, req domain.GameStartRequest) (*domain.GameStartResponse, error) { // 1. Check if provider is enabled in DB - provider, err := s.repo.GetVirtualGameProviderByID(ctx, req.ProviderID) - if err != nil { - return nil, fmt.Errorf("failed to check provider %s: %w", req.ProviderID, err) - } + // provider, err := s.repo.GetVirtualGameProviderByID(ctx, req.ProviderID) + // if err != nil { + // return nil, fmt.Errorf("failed to check provider %s: %w", req.ProviderID, err) + // } - if !provider.Enabled { - // Provider exists but is disabled → return error - return nil, fmt.Errorf("provider %s is disabled", req.ProviderID) - } + // if !provider.Enabled { + // // Provider exists but is disabled → return error + // return nil, fmt.Errorf("provider %s is disabled", req.ProviderID) + // } // 2. Prepare signature params sigParams := map[string]any{ @@ -130,15 +143,15 @@ func (s *Service) StartGame(ctx context.Context, req domain.GameStartRequest) (* func (s *Service) StartDemoGame(ctx context.Context, req domain.DemoGameRequest) (*domain.GameStartResponse, error) { // 1. Check if provider is enabled in DB - provider, err := s.repo.GetVirtualGameProviderByID(ctx, req.ProviderID) - if err != nil { - return nil, fmt.Errorf("failed to check provider %s: %w", req.ProviderID, err) - } + // provider, err := s.repo.GetVirtualGameProviderByID(ctx, req.ProviderID) + // if err != nil { + // return nil, fmt.Errorf("failed to check provider %s: %w", req.ProviderID, err) + // } - if !provider.Enabled { - // Provider exists but is disabled → return error - return nil, fmt.Errorf("provider %s is disabled", req.ProviderID) - } + // if !provider.Enabled { + // // Provider exists but is disabled → return error + // return nil, fmt.Errorf("provider %s is disabled", req.ProviderID) + // } // 2. Prepare signature params sigParams := map[string]any{ diff --git a/internal/services/wallet/direct_deposit.go b/internal/services/wallet/direct_deposit.go index fc25861..a049d66 100644 --- a/internal/services/wallet/direct_deposit.go +++ b/internal/services/wallet/direct_deposit.go @@ -148,7 +148,7 @@ func (s *Service) notifyCashiersForVerification(ctx context.Context, depositID, Metadata: metadataJSON, } - if err := s.notificationStore.SendNotification(ctx, notification); err != nil { + if err := s.notificationSvc.SendNotification(ctx, notification); err != nil { s.logger.Error("failed to send verification notification", "cashier_id", cashier.ID, "error", err) @@ -199,7 +199,7 @@ func (s *Service) notifyCustomerVerificationResult(ctx context.Context, deposit Metadata: metadataJSON, } - if err := s.notificationStore.SendNotification(ctx, notification); err != nil { + if err := s.notificationSvc.SendNotification(ctx, notification); err != nil { s.logger.Error("failed to send deposit result notification", "customer_id", deposit.CustomerID, "error", err) diff --git a/internal/services/wallet/port.go b/internal/services/wallet/port.go index 29e21e1..e5687d4 100644 --- a/internal/services/wallet/port.go +++ b/internal/services/wallet/port.go @@ -30,6 +30,7 @@ type TransferStore interface { GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.TransferDetail, error) GetTransferByReference(ctx context.Context, reference string) (domain.TransferDetail, error) GetTransferByID(ctx context.Context, id int64) (domain.TransferDetail, error) + GetTransferStats(ctx context.Context, walletID int64) (domain.TransferStats, error) UpdateTransferVerification(ctx context.Context, id int64, verified bool) error UpdateTransferStatus(ctx context.Context, id int64, status string) error // InitiateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) @@ -47,9 +48,9 @@ type ApprovalStore interface { } type DirectDepositStore interface { - CreateDirectDeposit(ctx context.Context, deposit domain.CreateDirectDeposit) (domain.DirectDeposit, error) - GetDirectDeposit(ctx context.Context, id int64) (domain.DirectDeposit, error) - UpdateDirectDeposit(ctx context.Context, deposit domain.UpdateDirectDeposit) (domain.DirectDeposit, error) - GetDirectDepositsByStatus(ctx context.Context, status domain.DirectDepositStatus) ([]domain.DirectDeposit, error) - GetCustomerDirectDeposits(ctx context.Context, customerID int64) ([]domain.DirectDeposit, error) + CreateDirectDeposit(ctx context.Context, deposit domain.CreateDirectDeposit) (domain.DirectDeposit, error) + GetDirectDeposit(ctx context.Context, id int64) (domain.DirectDeposit, error) + UpdateDirectDeposit(ctx context.Context, deposit domain.UpdateDirectDeposit) (domain.DirectDeposit, error) + GetDirectDepositsByStatus(ctx context.Context, status domain.DirectDepositStatus) ([]domain.DirectDeposit, error) + GetCustomerDirectDeposits(ctx context.Context, customerID int64) ([]domain.DirectDeposit, error) } diff --git a/internal/services/wallet/service.go b/internal/services/wallet/service.go index 8b0d216..2e248a5 100644 --- a/internal/services/wallet/service.go +++ b/internal/services/wallet/service.go @@ -14,7 +14,6 @@ type Service struct { walletStore WalletStore transferStore TransferStore directDepositStore DirectDepositStore - notificationStore notificationservice.NotificationStore notificationSvc *notificationservice.Service userSvc *user.Service mongoLogger *zap.Logger @@ -26,7 +25,6 @@ func NewService( walletStore WalletStore, transferStore TransferStore, directDepositStore DirectDepositStore, - notificationStore notificationservice.NotificationStore, notificationSvc *notificationservice.Service, userSvc *user.Service, mongoLogger *zap.Logger, @@ -38,7 +36,6 @@ func NewService( transferStore: transferStore, directDepositStore: directDepositStore, // approvalStore: approvalStore, - notificationStore: notificationStore, notificationSvc: notificationSvc, userSvc: userSvc, mongoLogger: mongoLogger, diff --git a/internal/services/wallet/transfer.go b/internal/services/wallet/transfer.go index ad3ef1d..461b51a 100644 --- a/internal/services/wallet/transfer.go +++ b/internal/services/wallet/transfer.go @@ -35,6 +35,9 @@ func (s *Service) GetTransfersByWallet(ctx context.Context, walletID int64) ([]d return s.transferStore.GetTransfersByWallet(ctx, walletID) } +func (s *Service) GetTransferStats(ctx context.Context, walletID int64) (domain.TransferStats, error) { + return s.transferStore.GetTransferStats(ctx, walletID) +} func (s *Service) UpdateTransferVerification(ctx context.Context, id int64, verified bool) error { return s.transferStore.UpdateTransferVerification(ctx, id, verified) } @@ -135,7 +138,7 @@ func (s *Service) SendTransferNotification(ctx context.Context, senderWallet dom } // Sender notifications - if err := s.notificationStore.SendNotification(ctx, senderNotify); err != nil { + if err := s.notificationSvc.SendNotification(ctx, senderNotify); err != nil { s.logger.Error("failed to send sender notification", "user_id", "", "error", err) @@ -163,7 +166,7 @@ func (s *Service) SendTransferNotification(ctx context.Context, senderWallet dom }`, amount, receiverWallet.Balance, receiverWallet.ID)), } // Sender notifications - if err := s.notificationStore.SendNotification(ctx, receiverNotify); err != nil { + if err := s.notificationSvc.SendNotification(ctx, receiverNotify); err != nil { s.logger.Error("failed to send sender notification", "user_id", "", "error", err) diff --git a/internal/services/wallet/wallet.go b/internal/services/wallet/wallet.go index 7d7bd5b..7d04160 100644 --- a/internal/services/wallet/wallet.go +++ b/internal/services/wallet/wallet.go @@ -26,6 +26,7 @@ func (s *Service) CreateCustomerWallet(ctx context.Context, customerID int64) (d IsBettable: true, IsTransferable: true, UserID: customerID, + Type: domain.RegularWalletType, }) if err != nil { @@ -37,6 +38,7 @@ func (s *Service) CreateCustomerWallet(ctx context.Context, customerID int64) (d IsBettable: true, IsTransferable: true, UserID: customerID, + Type: domain.StaticWalletType, }) if err != nil { @@ -213,6 +215,9 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain. 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) @@ -304,10 +309,10 @@ func (s *Service) GetAdminNotificationRecipients(ctx context.Context, walletID i } func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWallet domain.Wallet) error { - errorSeverity := domain.NotificationErrorSeverityLow + // Send notification to admin team adminNotification := &domain.Notification{ - ErrorSeverity: &errorSeverity, + ErrorSeverity: "low", IsRead: false, DeliveryStatus: domain.DeliveryStatusPending, RecipientID: adminWallet.UserID, @@ -343,7 +348,7 @@ func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWalle for _, adminID := range adminRecipients { adminNotification.RecipientID = adminID - if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil { + if err := s.notificationSvc.SendNotification(ctx, adminNotification); err != nil { s.mongoLogger.Error("failed to send admin notification", zap.Int64("admin_id", adminID), zap.Error(err), @@ -353,7 +358,7 @@ func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWalle adminNotification.DeliveryChannel = domain.DeliveryChannelEmail - if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil { + if err := s.notificationSvc.SendNotification(ctx, adminNotification); err != nil { s.mongoLogger.Error("failed to send email admin notification", zap.Int64("admin_id", adminID), zap.Error(err), @@ -366,11 +371,9 @@ func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWalle } func (s *Service) SendAdminWalletInsufficientNotification(ctx context.Context, adminWallet domain.Wallet, amount domain.Currency) error { - - errorSeverity := domain.NotificationErrorSeverityLow // Send notification to admin team adminNotification := &domain.Notification{ - ErrorSeverity: &errorSeverity, + ErrorSeverity: domain.NotificationErrorSeverityLow, IsRead: false, DeliveryStatus: domain.DeliveryStatusPending, RecipientID: adminWallet.UserID, @@ -408,7 +411,7 @@ func (s *Service) SendAdminWalletInsufficientNotification(ctx context.Context, a } for _, adminID := range recipients { adminNotification.RecipientID = adminID - if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil { + if err := s.notificationSvc.SendNotification(ctx, adminNotification); err != nil { s.mongoLogger.Error("failed to send admin notification", zap.Int64("admin_id", adminID), zap.Error(err), @@ -417,7 +420,7 @@ func (s *Service) SendAdminWalletInsufficientNotification(ctx context.Context, a } adminNotification.DeliveryChannel = domain.DeliveryChannelEmail - if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil { + if err := s.notificationSvc.SendNotification(ctx, adminNotification); err != nil { s.mongoLogger.Error("failed to send email admin notification", zap.Int64("admin_id", adminID), zap.Error(err), @@ -431,10 +434,9 @@ func (s *Service) SendAdminWalletInsufficientNotification(ctx context.Context, a } func (s *Service) SendCustomerWalletInsufficientNotification(ctx context.Context, customerWallet domain.Wallet, amount domain.Currency) error { - errorSeverity := domain.NotificationErrorSeverityLow // Send notification to admin team customerNotification := &domain.Notification{ - ErrorSeverity: &errorSeverity, + ErrorSeverity: domain.NotificationErrorSeverityLow, IsRead: false, DeliveryStatus: domain.DeliveryStatusPending, RecipientID: customerWallet.UserID, @@ -460,7 +462,7 @@ func (s *Service) SendCustomerWalletInsufficientNotification(ctx context.Context }`, customerWallet.ID, customerWallet.Balance, amount.Float32()), } - if err := s.notificationStore.SendNotification(ctx, customerNotification); err != nil { + if err := s.notificationSvc.SendNotification(ctx, customerNotification); err != nil { s.mongoLogger.Error("failed to create customer notification", zap.Int64("customer_id", customerWallet.UserID), zap.Error(err), diff --git a/internal/web_server/app.go b/internal/web_server/app.go index fe03649..a836706 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -19,6 +19,7 @@ import ( 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/raffle" "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" @@ -60,7 +61,8 @@ type App struct { cfg *config.Config logger *slog.Logger NotidicationStore *notificationservice.Service - referralSvc referralservice.ReferralStore + referralSvc *referralservice.Service + raffleSvc raffle.RaffleStore bonusSvc *bonus.Service port int settingSvc *settings.Service @@ -113,7 +115,8 @@ func NewApp( prematchSvc *odds.ServiceImpl, eventSvc event.Service, leagueSvc league.Service, - referralSvc referralservice.ReferralStore, + referralSvc *referralservice.Service, + raffleSvc raffle.RaffleStore, bonusSvc *bonus.Service, virtualGameSvc virtualgameservice.VirtualGameService, aleaVirtualGameService alea.AleaVirtualGameService, @@ -132,11 +135,13 @@ func NewApp( app.Use(cors.New(cors.Config{ AllowOrigins: "*", - AllowMethods: "GET,POST,PUT,DELETE,OPTIONS", + AllowMethods: "GET,POST,PUT,PATCH,DELETE,OPTIONS", AllowHeaders: "Content-Type,Authorization,platform", // AllowCredentials: true, })) + app.Static("/static", "./static") + s := &App{ enetPulseSvc: enetPulseSvc, atlasVirtualGameService: atlasVirtualGameService, @@ -166,6 +171,7 @@ func NewApp( companySvc: companySvc, NotidicationStore: notidicationStore, referralSvc: referralSvc, + raffleSvc: raffleSvc, bonusSvc: bonusSvc, Logger: logger, prematchSvc: prematchSvc, diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 9df7279..1cf19c6 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -26,32 +26,32 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S spec string task func() }{ - // { - // spec: "0 0 * * * *", // Every 1 hour - // task: func() { - // mongoLogger.Info("Began fetching upcoming events cron task") - // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { - // mongoLogger.Error("Failed to fetch upcoming events", - // zap.Error(err), - // ) - // } else { - // mongoLogger.Info("Completed fetching upcoming events without errors") - // } - // }, - // }, - // { - // spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events) - // task: func() { - // mongoLogger.Info("Began fetching non live odds cron task") - // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { - // mongoLogger.Error("Failed to fetch non live odds", - // zap.Error(err), - // ) - // } else { - // mongoLogger.Info("Completed fetching non live odds without errors") - // } - // }, - // }, + { + spec: "0 0 * * * *", // Every 1 hour + task: func() { + mongoLogger.Info("Began fetching upcoming events cron task") + if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { + mongoLogger.Error("Failed to fetch upcoming events", + zap.Error(err), + ) + } else { + mongoLogger.Info("Completed fetching upcoming events without errors") + } + }, + }, + { + spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events) + task: func() { + mongoLogger.Info("Began fetching non live odds cron task") + if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { + mongoLogger.Error("Failed to fetch non live odds", + zap.Error(err), + ) + } else { + mongoLogger.Info("Completed fetching non live odds without errors") + } + }, + }, { spec: "0 */5 * * * *", // Every 5 Minutes task: func() { @@ -65,21 +65,21 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S } }, }, - // { - // spec: "0 */15 * * * *", // Every 15 Minutes - // task: func() { - // mongoLogger.Info("Began fetching results for upcoming events cron task") - // if err := resultService.FetchAndProcessResults(context.Background()); err != nil { - // mongoLogger.Error("Failed to process result", - // zap.Error(err), - // ) - // } else { - // mongoLogger.Info("Completed processing all event result outcomes without errors") - // } - // }, - // }, { - spec: "0 0 * * * *", // Every Day + spec: "0 */15 * * * *", // Every 15 Minutes + task: func() { + mongoLogger.Info("Began fetching results for upcoming events cron task") + if err := resultService.FetchAndProcessResults(context.Background()); err != nil { + mongoLogger.Error("Failed to process result", + zap.Error(err), + ) + } else { + mongoLogger.Info("Completed processing all event result outcomes without errors") + } + }, + }, + { + spec: "0 0 0 * * *", // Every Day task: func() { mongoLogger.Info("Began Send daily result notification cron task") if err := resultService.CheckAndSendResultNotifications(context.Background(), time.Now().Add(-24*time.Hour)); err != nil { @@ -154,10 +154,10 @@ func SetupReportandVirtualGameCronJobs( spec string period string }{ - { - spec: "*/60 * * * * *", // Every 60 seconds for testing - period: "test", - }, + // { + // spec: "*/60 * * * * *", // Every 1 minute for testing + // period: "test", + // }, { spec: "0 0 0 * * *", // Daily at midnight period: "daily", diff --git a/internal/web_server/handlers/admin.go b/internal/web_server/handlers/admin.go index 48843c6..6eb6122 100644 --- a/internal/web_server/handlers/admin.go +++ b/internal/web_server/handlers/admin.go @@ -196,11 +196,13 @@ func (h *Handler) GetAllAdmins(c *fiber.Ctx) error { Valid: true, } } + + companyFilter := int64(c.QueryInt("company_id")) filter := domain.UserFilter{ Role: string(domain.RoleAdmin), CompanyID: domain.ValidInt64{ - Value: int64(c.QueryInt("company_id")), - Valid: false, + Value: companyFilter, + Valid: companyFilter != 0, }, Page: domain.ValidInt{ Value: c.QueryInt("page", 1) - 1, diff --git a/internal/web_server/handlers/auth_handler.go b/internal/web_server/handlers/auth_handler.go index 57081c1..a64f285 100644 --- a/internal/web_server/handlers/auth_handler.go +++ b/internal/web_server/handlers/auth_handler.go @@ -107,7 +107,13 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusForbidden, "Only customers are allowed to login ") } - accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, successRes.CompanyID, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry) + accessToken, err := jwtutil.CreateJwt( + successRes.UserId, + successRes.Role, + successRes.CompanyID, + h.jwtConfig.JwtAccessKey, + h.jwtConfig.JwtAccessExpiry, + ) if err != nil { h.mongoLoggerSvc.Error("Failed to create access token", zap.Int("status_code", fiber.StatusInternalServerError), @@ -189,22 +195,22 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error { switch { case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound): h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials", - zap.Int("status_code", fiber.StatusUnauthorized), + zap.Int("status_code", fiber.StatusBadRequest), zap.String("email", req.Email), zap.String("phone", req.PhoneNumber), zap.Error(err), zap.Time("timestamp", time.Now()), ) - return fiber.NewError(fiber.StatusUnauthorized, "Invalid credentials") + return fiber.NewError(fiber.StatusBadRequest, "Invalid credentials") case errors.Is(err, authentication.ErrUserSuspended): h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked", - zap.Int("status_code", fiber.StatusUnauthorized), + zap.Int("status_code", fiber.StatusForbidden), zap.String("email", req.Email), zap.String("phone", req.PhoneNumber), zap.Error(err), zap.Time("timestamp", time.Now()), ) - return fiber.NewError(fiber.StatusUnauthorized, "User login has been locked") + return fiber.NewError(fiber.StatusForbidden, "User login has been locked") default: h.mongoLoggerSvc.Error("Login failed", zap.Int("status_code", fiber.StatusInternalServerError), @@ -290,22 +296,22 @@ func (h *Handler) LoginSuper(c *fiber.Ctx) error { switch { case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound): h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials", - zap.Int("status_code", fiber.StatusUnauthorized), + zap.Int("status_code", fiber.StatusBadRequest), zap.String("email", req.Email), zap.String("phone", req.PhoneNumber), zap.Error(err), zap.Time("timestamp", time.Now()), ) - return fiber.NewError(fiber.StatusUnauthorized, "Invalid credentials") + return fiber.NewError(fiber.StatusBadRequest, "Invalid credentials") case errors.Is(err, authentication.ErrUserSuspended): h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked", - zap.Int("status_code", fiber.StatusUnauthorized), + zap.Int("status_code", fiber.StatusForbidden), zap.String("email", req.Email), zap.String("phone", req.PhoneNumber), zap.Error(err), zap.Time("timestamp", time.Now()), ) - return fiber.NewError(fiber.StatusUnauthorized, "User login has been locked") + return fiber.NewError(fiber.StatusForbidden, "User login has been locked") default: h.mongoLoggerSvc.Error("Login failed", zap.Int("status_code", fiber.StatusInternalServerError), diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index 2cfd49c..c090de3 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -7,7 +7,6 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" - "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" @@ -46,6 +45,19 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error { res, err := h.CreateBetInternal(c, req, userID, role, companyID.Value) if err != nil { + switch err { + case bet.ErrEventHasBeenRemoved, bet.ErrEventHasNotEnded, bet.ErrRawOddInvalid, bet.ErrTotalBalanceNotEnough: + h.mongoLoggerSvc.Info("PlaceBet failed", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Int64("userID", userID), + zap.Int64("companyID", companyID.Value), + zap.String("role", string(role)), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + h.mongoLoggerSvc.Error("Failed to create bet", zap.Int("status_code", fiber.StatusInternalServerError), zap.Int64("user_id", userID), @@ -200,7 +212,7 @@ func (h *Handler) CreateBetInternal(c *fiber.Ctx, req domain.CreateBetReq, userI res, err := h.betSvc.PlaceBet(c.Context(), req, userID, role, companyID) if err != nil { switch err { - case bet.ErrEventHasBeenRemoved, bet.ErrEventHasNotEnded, bet.ErrRawOddInvalid, wallet.ErrBalanceInsufficient: + case bet.ErrEventHasBeenRemoved, bet.ErrEventHasNotEnded, bet.ErrRawOddInvalid, bet.ErrTotalBalanceNotEnough: h.mongoLoggerSvc.Info("PlaceBet failed", zap.Int("status_code", fiber.StatusBadRequest), zap.Int64("userID", userID), @@ -224,6 +236,73 @@ func (h *Handler) CreateBetInternal(c *fiber.Ctx, req domain.CreateBetReq, userI return domain.CreateBetRes{}, err } + // create raffle tickets + raffles, err := h.raffleSvc.GetRafflesOfCompany(c.Context(), int32(companyID)) + if err != nil { + h.mongoLoggerSvc.Error("Failed to fetch raffle of company", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Int64("userID", userID), + zap.Int64("companyID", companyID), + zap.String("role", string(role)), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + } + + sportAndLeagueIDs := [][]int64{} + for _, outcome := range req.Outcomes { + ids, err := h.eventSvc.GetSportAndLeagueIDs(c.Context(), fmt.Sprintf("%d", outcome.EventID)) + if err != nil { + continue + } + + sportAndLeagueIDs = append(sportAndLeagueIDs, ids) + } + + fmt.Println("sportAndLeagueIDs: ", sportAndLeagueIDs) + + for _, raffle := range raffles { + // TODO: only fetch pending raffles from db + if raffle.Status == "completed" { + continue + } + + // only require one sport and league combo to be valide to make the raffle ticket + foundValid := false + for _, sportAndLeagueID := range sportAndLeagueIDs { + res, err := h.raffleSvc.CheckValidSportRaffleFilter(c.Context(), raffle.ID, sportAndLeagueID[0], sportAndLeagueID[1]) + if err != nil { + continue + } + + fmt.Println(sportAndLeagueID, res) + + foundValid = foundValid || res + } + + if !foundValid { + continue + } + + raffleTicket := domain.CreateRaffleTicket{ + RaffleID: raffle.ID, + UserID: int32(userID), + } + + _, err := h.raffleSvc.CreateRaffleTicket(c.Context(), raffleTicket) + if err != nil { + h.mongoLoggerSvc.Error("Failed to create raffle ticket", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Int64("raffleID", int64(raffle.ID)), + zap.Int64("userID", userID), + zap.Int64("companyID", companyID), + zap.String("role", string(role)), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + } + } + return res, nil } @@ -379,9 +458,110 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error { // @Failure 500 {object} response.APIResponse // @Router /api/v1/{tenant_slug}/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.Info("failed to parse is_shop_bet", + zap.Int("status_code", fiber.StatusBadRequest), + zap.String("is_shop", isShopBetQuery), + 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.mongoLoggerSvc.Info("invalid created_before format", + zap.String("time", createdBeforeQuery), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid created_before format") + } + 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.mongoLoggerSvc.Info("invalid created_after format", + zap.String("created_after", createdAfterQuery), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid created_after format") + } + createdAfter = domain.ValidTime{ + Value: createdAfterParsed, + Valid: true, + } + } + + bets, err := h.betSvc.GetAllBets(c.Context(), domain.BetFilter{ + IsShopBet: isShopBet, + Query: searchString, + CreatedBefore: createdBefore, + CreatedAfter: createdAfter, + }) + if err != nil { + h.mongoLoggerSvc.Error("Failed to get all bets", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve bets፡"+err.Error()) + } + + res := make([]domain.BetRes, len(bets)) + for i, bet := range bets { + res[i] = domain.ConvertBet(bet) + } + + return response.WriteJSON(c, fiber.StatusOK, "All bets retrieved successfully", res, nil) +} + +// GetAllTenants godoc +// @Summary Gets all bets +// @Description Gets all the bets +// @Tags bet +// @Accept json +// @Produce json +// @Success 200 {array} domain.BetRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/{tenant_slug}/sport/bet [get] +func (h *Handler) GetAllTenantBets(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) if !companyID.Valid { - h.BadRequestLogger().Error("invalid company id") + h.BadRequestLogger().Error("invalid company id", zap.Any("company_id", companyID)) return fiber.NewError(fiber.StatusBadRequest, "invalid company id") } role := c.Locals("role").(domain.Role) @@ -485,8 +665,54 @@ func (h *Handler) GetAllBet(c *fiber.Ctx) error { // @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/{tenant_slug}/sport/bet/{id} [get] +// @Router /api/v1/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.mongoLoggerSvc.Info("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 { + h.mongoLoggerSvc.Info("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) + + // 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) +} + +// GetTenantBetByID godoc +// @Summary Gets bet by id +// @Description Gets a single bet by id +// @Tags bet +// @Accept json +// @Produce json +// @Param id path int true "Bet ID" +// @Success 200 {object} domain.BetRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/{tenant_slug}/sport/bet/{id} [get] +func (h *Handler) GetTenantBetByID(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) if !companyID.Valid { h.BadRequestLogger().Error("invalid company id") @@ -696,8 +922,52 @@ func (h *Handler) UpdateCashOut(c *fiber.Ctx) error { // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/{tenant_slug}/sport/bet/{id} [delete] +// @Router /api/v1/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.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.SetBetToRemoved(c.Context(), id) + if err != nil { + 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:"+err.Error()) + } + + 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) +} + +// DeleteTenantBet godoc +// @Summary Deletes bet by id +// @Description Deletes bet by id +// @Tags bet +// @Accept json +// @Produce json +// @Param id path int true "Bet ID" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/{tenant_slug}/sport/bet/{id} [delete] +func (h *Handler) DeleteTenantBet(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) if !companyID.Valid { h.BadRequestLogger().Error("invalid company id") diff --git a/internal/web_server/handlers/bonus.go b/internal/web_server/handlers/bonus.go index f796827..a273dff 100644 --- a/internal/web_server/handlers/bonus.go +++ b/internal/web_server/handlers/bonus.go @@ -1,98 +1,206 @@ package handlers import ( - "time" + "strconv" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" "go.uber.org/zap" ) -func (h *Handler) CreateBonusMultiplier(c *fiber.Ctx) error { - var req struct { - Multiplier float32 `json:"multiplier"` - BalanceCap int64 `json:"balance_cap"` - } +// func (h *Handler) CreateBonusMultiplier(c *fiber.Ctx) error { +// var req struct { +// Multiplier float32 `json:"multiplier"` +// BalanceCap int64 `json:"balance_cap"` +// } - if err := c.BodyParser(&req); err != nil { - h.logger.Error("failed to parse bonus multiplier request", "error", err) - h.mongoLoggerSvc.Info("failed to parse bonus multiplier", - zap.Int("status_code", fiber.StatusBadRequest), - zap.Error(err), - zap.Time("timestamp", time.Now()), +// if err := c.BodyParser(&req); err != nil { +// h.logger.Error("failed to parse bonus multiplier request", "error", err) +// h.mongoLoggerSvc.Info("failed to parse bonus multiplier", +// zap.Int("status_code", fiber.StatusBadRequest), +// zap.Error(err), +// zap.Time("timestamp", time.Now()), +// ) +// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error()) +// } + +// // currently only one multiplier is allowed +// // we can add an active bool in the db and have mulitple bonus if needed +// multipliers, err := h.bonusSvc.GetBonusMultiplier(c.Context()) +// if err != nil { +// h.logger.Error("failed to get bonus multiplier", "error", err) +// h.mongoLoggerSvc.Info("Failed to get bonus multiplier", +// zap.Int("status_code", fiber.StatusBadRequest), +// zap.Error(err), +// zap.Time("timestamp", time.Now()), +// ) +// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error()) +// } + +// if len(multipliers) > 0 { +// return fiber.NewError(fiber.StatusBadRequest, "only one multiplier is allowed") +// } + +// if err := h.bonusSvc.CreateBonusMultiplier(c.Context(), req.Multiplier, req.BalanceCap); err != nil { +// h.mongoLoggerSvc.Error("failed to create bonus multiplier", +// zap.Int("status_code", fiber.StatusInternalServerError), +// zap.Error(err), +// zap.Time("timestamp", time.Now()), +// ) +// return fiber.NewError(fiber.StatusInternalServerError, "failed to create bonus multiplier"+err.Error()) +// } + +// return response.WriteJSON(c, fiber.StatusOK, "Create bonus multiplier successfully", nil, nil) +// } + +// func (h *Handler) GetBonusMultiplier(c *fiber.Ctx) error { +// multipliers, err := h.bonusSvc.GetBonusMultiplier(c.Context()) +// if err != nil { +// h.mongoLoggerSvc.Info("failed to get bonus multiplier", +// zap.Int("status_code", fiber.StatusBadRequest), +// zap.Error(err), +// zap.Time("timestamp", time.Now()), +// ) +// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body"+err.Error()) +// } + +// return response.WriteJSON(c, fiber.StatusOK, "Fetched bonus multiplier successfully", multipliers, nil) +// } + +// func (h *Handler) UpdateBonusMultiplier(c *fiber.Ctx) error { +// var req struct { +// ID int64 `json:"id"` +// Multiplier float32 `json:"multiplier"` +// BalanceCap int64 `json:"balance_cap"` +// } + +// if err := c.BodyParser(&req); err != nil { +// h.mongoLoggerSvc.Info("failed to parse bonus multiplier", +// zap.Int("status_code", fiber.StatusBadRequest), +// zap.Error(err), +// zap.Time("timestamp", time.Now()), +// ) +// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error()) +// } + +// if err := h.bonusSvc.UpdateBonusMultiplier(c.Context(), req.ID, req.Multiplier, req.BalanceCap); err != nil { +// h.logger.Error("failed to update bonus multiplier", "error", err) +// h.mongoLoggerSvc.Error("failed to update bonus multiplier", +// zap.Int64("id", req.ID), +// zap.Int("status_code", fiber.StatusInternalServerError), +// zap.Error(err), +// zap.Time("timestamp", time.Now()), +// ) +// return fiber.NewError(fiber.StatusInternalServerError, "failed to update bonus multiplier:"+err.Error()) +// } + +// return response.WriteJSON(c, fiber.StatusOK, "Updated bonus multiplier successfully", nil, nil) +// } + +func (h *Handler) GetBonusesByUserID(c *fiber.Ctx) error { + userID, ok := c.Locals("user_id").(int64) + if !ok || userID == 0 { + h.InternalServerErrorLogger().Error("Invalid user ID in context", + zap.Int64("userID", userID), ) - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error()) + return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification") } - // currently only one multiplier is allowed - // we can add an active bool in the db and have mulitple bonus if needed - multipliers, err := h.bonusSvc.GetBonusMultiplier(c.Context()) + page := c.QueryInt("page", 1) + pageSize := c.QueryInt("page_size", 10) + limit := domain.ValidInt{ + Value: pageSize, + Valid: true, + } + offset := domain.ValidInt{ + Value: page - 1, + Valid: true, + } + + filter := domain.BonusFilter{ + UserID: domain.ValidInt64{ + Value: userID, + Valid: true, + }, + Limit: limit, + Offset: offset, + } + + bonuses, err := h.bonusSvc.GetAllUserBonuses(c.Context(), filter) + if err != nil { - h.logger.Error("failed to get bonus multiplier", "error", err) - h.mongoLoggerSvc.Info("Failed to get bonus multiplier", - zap.Int("status_code", fiber.StatusBadRequest), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error()) + h.InternalServerErrorLogger().Error("Failed to bonus by userID", zap.Int64("userId", userID)) + return fiber.NewError(fiber.StatusInternalServerError, "failed to get bonus by user ID") } - if len(multipliers) > 0 { - return fiber.NewError(fiber.StatusBadRequest, "only one multiplier is allowed") - } - - if err := h.bonusSvc.CreateBonusMultiplier(c.Context(), req.Multiplier, req.BalanceCap); err != nil { - h.mongoLoggerSvc.Error("failed to create bonus multiplier", - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusInternalServerError, "failed to create bonus multiplier"+err.Error()) - } - - return response.WriteJSON(c, fiber.StatusOK, "Create bonus multiplier successfully", nil, nil) -} - -func (h *Handler) GetBonusMultiplier(c *fiber.Ctx) error { - multipliers, err := h.bonusSvc.GetBonusMultiplier(c.Context()) + count, err := h.bonusSvc.GetBonusCount(c.Context(), filter) if err != nil { - h.mongoLoggerSvc.Info("failed to get bonus multiplier", - zap.Int("status_code", fiber.StatusBadRequest), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body"+err.Error()) + h.InternalServerErrorLogger().Error("Failed to get bonus count", zap.Int64("userId", userID)) + return fiber.NewError(fiber.StatusInternalServerError, "failed to get bonus count by user ID") } - return response.WriteJSON(c, fiber.StatusOK, "Fetched bonus multiplier successfully", multipliers, nil) + res := domain.ConvertToBonusResList(bonuses) + + return response.WritePaginatedJSON(c, fiber.StatusOK, "Fetched User Bonuses", res, nil, page, int(count)) + } -func (h *Handler) UpdateBonusMultiplier(c *fiber.Ctx) error { - var req struct { - ID int64 `json:"id"` - Multiplier float32 `json:"multiplier"` - BalanceCap int64 `json:"balance_cap"` - } - - if err := c.BodyParser(&req); err != nil { - h.mongoLoggerSvc.Info("failed to parse bonus multiplier", - zap.Int("status_code", fiber.StatusBadRequest), - zap.Error(err), - zap.Time("timestamp", time.Now()), +func (h *Handler) GetBonusStats(c *fiber.Ctx) error { + userID, ok := c.Locals("user_id").(int64) + if !ok || userID == 0 { + h.InternalServerErrorLogger().Error("Invalid user ID in context", + zap.Int64("userID", userID), ) - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error()) + return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification") } - if err := h.bonusSvc.UpdateBonusMultiplier(c.Context(), req.ID, req.Multiplier, req.BalanceCap); err != nil { - h.logger.Error("failed to update bonus multiplier", "error", err) - h.mongoLoggerSvc.Error("failed to update bonus multiplier", - zap.Int64("id", req.ID), - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Error(err), - zap.Time("timestamp", time.Now()), + stats, err := h.bonusSvc.GetBonusStats(c.Context(), domain.BonusFilter{ + UserID: domain.ValidInt64{ + Value: userID, + Valid: true, + }, + }) + + if err != nil { + h.InternalServerErrorLogger().Error("Failed to get bonus stats", + zap.Int64("userID", userID), ) - return fiber.NewError(fiber.StatusInternalServerError, "failed to update bonus multiplier:"+err.Error()) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get bonus stats") } - return response.WriteJSON(c, fiber.StatusOK, "Updated bonus multiplier successfully", nil, nil) + res := domain.ConvertToBonusStatsRes(stats) + + return response.WriteJSON(c, fiber.StatusOK, "Get Bonus Stats", res, nil) +} + +// bonus/:id/claim +func (h *Handler) ClaimBonus(c *fiber.Ctx) error { + bonusIDParam := c.Params("id") + bonusID, err := strconv.ParseInt(bonusIDParam, 10, 64) + if err != nil { + h.BadRequestLogger().Error("Invalid bonus ID", + zap.Int64("bonusID", bonusID), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid bonus id") + } + + userID, ok := c.Locals("user_id").(int64) + if !ok || userID == 0 { + h.InternalServerErrorLogger().Error("Invalid user ID in context", + zap.Int64("userID", userID), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification") + } + + if err := h.bonusSvc.ProcessBonusClaim(c.Context(), bonusID, userID); err != nil { + h.InternalServerErrorLogger().Error("Failed to update bonus claim", + zap.Int64("userID", userID), + zap.Int64("bonusID", bonusID), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update bonus claim") + } + + return response.WriteJSON(c, fiber.StatusOK, "Bonus has successfully been claimed", nil, nil) + } diff --git a/internal/web_server/handlers/branch_handler.go b/internal/web_server/handlers/branch_handler.go index f65a7f8..d187b21 100644 --- a/internal/web_server/handlers/branch_handler.go +++ b/internal/web_server/handlers/branch_handler.go @@ -86,6 +86,7 @@ func (h *Handler) CreateBranch(c *fiber.Ctx) error { IsBettable: true, IsTransferable: true, UserID: req.BranchManagerID, + Type: domain.BranchWalletType, }) if err != nil { @@ -295,6 +296,108 @@ func (h *Handler) GetBranchByID(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Branch retrieved successfully", res, nil) } +// ReturnBranchWallet godoc +// @Summary Unassign the branch wallet to company +// @Description Unassign the branch wallet to company +// @Tags branch +// @Accept json +// @Produce json +// @Param id path int true "Branch ID" +// @Success 200 {object} domain.BranchDetailRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/branch/{id}/return [post] +func (h *Handler) ReturnBranchWallet(c *fiber.Ctx) error { + userID := c.Locals("user_id").(int64) + + branchID := c.Params("id") + id, err := strconv.ParseInt(branchID, 10, 64) + if err != nil { + h.mongoLoggerSvc.Info("Invalid branch ID", + zap.String("branch", branchID), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid branch ID") + } + + branch, err := h.branchSvc.GetBranchByID(c.Context(), id) + + if err != nil { + h.mongoLoggerSvc.Info("Failed to get branch by ID", + zap.Int64("branchID", id), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + company, err := h.companySvc.GetCompanyByID(c.Context(), branch.CompanyID) + + if err != nil { + h.mongoLoggerSvc.Info("Failed to get company by ID", + zap.Int64("branchID", id), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + branchWallet, err := h.walletSvc.GetWalletByID(c.Context(), branch.WalletID) + if err != nil { + h.mongoLoggerSvc.Info("Failed to get wallet by ID", + zap.Int64("branchID", id), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + _, err = h.walletSvc.AddToWallet(c.Context(), + company.WalletID, + branchWallet.Balance, + domain.ValidInt64{Value: userID, Valid: true}, + domain.TRANSFER_DIRECT, + domain.PaymentDetails{}, + fmt.Sprintf("Returning Branch %v Wallet to Company", branch.Name)) + + if err != nil { + h.mongoLoggerSvc.Info("Failed to add to company wallet", + zap.Int64("branchID", id), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + _, err = h.walletSvc.DeductFromWallet(c.Context(), + branchWallet.ID, + branchWallet.Balance, + domain.ValidInt64{Value: userID, Valid: true}, + domain.TRANSFER_DIRECT, + "Branch Wallet Balance has been returned to Company", + ) + + if err != nil { + h.mongoLoggerSvc.Info("Failed to deduct from branch wallet", + zap.Int64("branchID", id), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + res := domain.ConvertBranchDetail(branch) + + return response.WriteJSON(c, fiber.StatusOK, "Branch Wallet Has been emptied", res, nil) +} + // GetBranchByManagerID godoc // @Summary Gets branches by manager id // @Description Gets a branches by manager id diff --git a/internal/web_server/handlers/chapa.go b/internal/web_server/handlers/chapa.go index 00b771a..9a2a7c2 100644 --- a/internal/web_server/handlers/chapa.go +++ b/internal/web_server/handlers/chapa.go @@ -2,7 +2,6 @@ package handlers import ( "fmt" - "math" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/gofiber/fiber/v2" @@ -53,36 +52,39 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error { } // get static wallet of user - wallet, err := h.walletSvc.GetCustomerWallet(c.Context(), userID) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ - Error: err.Error(), - Message: "Failed to initiate Chapa deposit", - }) - } + // wallet, err := h.walletSvc.GetCustomerWallet(c.Context(), userID) + // if err != nil { + // return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + // Error: err.Error(), + // Message: "Failed to initiate Chapa deposit", + // }) + // } - var multiplier float32 = 1 - bonusMultiplier, err := h.bonusSvc.GetBonusMultiplier(c.Context()) - if err == nil { - multiplier = bonusMultiplier[0].Multiplier - } + // var multiplier float32 = 1 + // bonusMultiplier, err := h.bonusSvc.GetBonusMultiplier(c.Context()) + // if err == nil { + // multiplier = bonusMultiplier[0].Multiplier + // } - var balanceCap int64 = 0 - bonusBalanceCap, err := h.bonusSvc.GetBonusBalanceCap(c.Context()) - if err == nil { - balanceCap = bonusBalanceCap[0].BalanceCap - } + // var balanceCap int64 = 0 + // bonusBalanceCap, err := h.bonusSvc.GetBonusBalanceCap(c.Context()) + // if err == nil { + // balanceCap = bonusBalanceCap[0].BalanceCap + // } - capedBalanceAmount := domain.Currency((math.Min(req.Amount, float64(balanceCap)) * float64(multiplier)) * 100) + // capedBalanceAmount := domain.Currency((math.Min(req.Amount, float64(balanceCap)) * float64(multiplier)) * 100) - _, err = h.walletSvc.AddToWallet(c.Context(), wallet.StaticID, capedBalanceAmount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, - fmt.Sprintf("Added %v to static wallet because of deposit bonus using multiplier %v", capedBalanceAmount, multiplier), - ) - if err != nil { - h.logger.Error("Failed to add bonus to static wallet", "walletID", wallet.StaticID, "user id", userID, "error", err) - return err - } + // _, err = h.walletSvc.AddToWallet(c.Context(), wallet.StaticID, capedBalanceAmount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, + // fmt.Sprintf("Added %v to static wallet because of deposit bonus using multiplier %v", capedBalanceAmount, multiplier), + // ) + // if err != nil { + // h.logger.Error("Failed to add bonus to static wallet", "walletID", wallet.StaticID, "user id", userID, "error", err) + // return err + // } + // if err := h.bonusSvc.ProcessWelcomeBonus(c.Context(), domain.ToCurrency(float32(req.Amount)), 0, userID); err != nil { + // return err + // } return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Chapa deposit process initiated successfully", Data: checkoutURL, diff --git a/internal/web_server/handlers/company_handler.go b/internal/web_server/handlers/company_handler.go index 8eb731c..64fd401 100644 --- a/internal/web_server/handlers/company_handler.go +++ b/internal/web_server/handlers/company_handler.go @@ -64,6 +64,7 @@ func (h *Handler) CreateCompany(c *fiber.Ctx) error { IsBettable: true, IsTransferable: true, UserID: req.AdminID, + Type: domain.CompanyWalletType, }) if err != nil { diff --git a/internal/web_server/handlers/customer.go b/internal/web_server/handlers/customer.go index af444c3..ec24fa4 100644 --- a/internal/web_server/handlers/customer.go +++ b/internal/web_server/handlers/customer.go @@ -26,6 +26,8 @@ type CustomersRes struct { LastLogin time.Time `json:"last_login"` SuspendedAt time.Time `json:"suspended_at"` Suspended bool `json:"suspended"` + CompanyID int64 `json:"company_id"` + CompanyName string `json:"company_name"` } // GetAllCustomers godoc @@ -156,6 +158,30 @@ func (h *Handler) GetAllCustomers(c *fiber.Ctx) error { "Failed to retrieve user last login:"+err.Error()) } } + + if !customer.CompanyID.Valid { + h.mongoLoggerSvc.Error("Invalid user company ID", + zap.Int64("userID", customer.ID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, + "Failed to retrieve company id for customer:") + } + + company, err := h.companySvc.GetCompanyByID(c.Context(), customer.CompanyID.Value) + + if err != nil { + h.mongoLoggerSvc.Error("Invalid user company value", + zap.Int64("userID", customer.ID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, + "Failed to fetch company for customer:") + } result[index] = CustomersRes{ ID: customer.ID, FirstName: customer.FirstName, @@ -170,6 +196,176 @@ func (h *Handler) GetAllCustomers(c *fiber.Ctx) error { SuspendedAt: customer.SuspendedAt, Suspended: customer.Suspended, LastLogin: *lastLogin, + CompanyID: company.ID, + CompanyName: company.Name, + } + } + + return response.WritePaginatedJSON(c, fiber.StatusOK, "Customers retrieved successfully", result, nil, filter.Page.Value, int(total)) + +} + +// GetAllTenantCustomers godoc +// @Summary Get all Customers +// @Description Get all Customers +// @Tags customer +// @Accept json +// @Produce json +// @Param page query int false "Page number" +// @Param page_size query int false "Page size" +// @Success 200 {object} CustomersRes +// @Failure 400 {object} response.APIResponse +// @Failure 401 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/tenant/{tenant_slug}/customer [get] +func (h *Handler) GetAllTenantCustomers(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + + 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.mongoLoggerSvc.Info("invalid created_before format", + zap.String("createdBefore", createdBeforeQuery), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid created_before format") + } + 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.mongoLoggerSvc.Info("invalid created_after format", + zap.String("createdAfter", createdAfterQuery), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid created_after format") + } + createdAfter = domain.ValidTime{ + Value: createdAfterParsed, + Valid: true, + } + } + + filter := domain.UserFilter{ + Role: string(domain.RoleCustomer), + CompanyID: companyID, + Page: domain.ValidInt{ + Value: c.QueryInt("page", 1) - 1, + Valid: true, + }, + PageSize: domain.ValidInt{ + Value: c.QueryInt("page_size", 10), + Valid: true, + }, + Query: searchString, + CreatedBefore: createdBefore, + CreatedAfter: createdAfter, + } + valErrs, ok := h.validator.Validate(c, filter) + if !ok { + var errMsg string + for field, msg := range valErrs { + errMsg += fmt.Sprintf("%s: %s; ", field, msg) + } + h.mongoLoggerSvc.Info("Failed to validate GetAllCustomer filters", + zap.Any("filter", filter), + zap.Int("status_code", fiber.StatusBadRequest), + zap.String("errMsg", errMsg), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, errMsg) + } + customers, total, err := h.userSvc.GetAllUsers(c.Context(), filter) + if err != nil { + h.mongoLoggerSvc.Error("GetAllCustomers failed to get all users", + zap.Any("filter", filter), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get Customers:"+err.Error()) + } + + var result []CustomersRes = make([]CustomersRes, len(customers)) + for index, customer := range customers { + lastLogin, err := h.authSvc.GetLastLogin(c.Context(), customer.ID) + if err != nil { + if err == authentication.ErrRefreshTokenNotFound { + lastLogin = &customer.CreatedAt + } else { + h.mongoLoggerSvc.Error("Failed to get user last login", + zap.Int64("userID", customer.ID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, + "Failed to retrieve user last login:"+err.Error()) + } + } + + if !customer.CompanyID.Valid { + h.mongoLoggerSvc.Error("Invalid user company ID", + zap.Int64("userID", customer.ID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, + "Failed to retrieve company id for customer:") + } + + company, err := h.companySvc.GetCompanyByID(c.Context(), customer.CompanyID.Value) + + if err != nil { + h.mongoLoggerSvc.Error("Invalid user company value", + zap.Int64("userID", customer.ID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, + "Failed to fetch company for customer:") + } + result[index] = CustomersRes{ + ID: customer.ID, + FirstName: customer.FirstName, + LastName: customer.LastName, + Email: customer.Email, + PhoneNumber: customer.PhoneNumber, + Role: customer.Role, + EmailVerified: customer.EmailVerified, + PhoneVerified: customer.PhoneVerified, + CreatedAt: customer.CreatedAt, + UpdatedAt: customer.UpdatedAt, + SuspendedAt: customer.SuspendedAt, + Suspended: customer.Suspended, + LastLogin: *lastLogin, + CompanyID: company.ID, + CompanyName: company.Name, } } @@ -222,6 +418,29 @@ func (h *Handler) GetCustomerByID(c *fiber.Ctx) error { lastLogin = &user.CreatedAt } + if !user.CompanyID.Valid { + h.mongoLoggerSvc.Error("Invalid user company ID", + zap.Int64("userID", user.ID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, + "Failed to retrieve company id for customer:") + } + + company, err := h.companySvc.GetCompanyByID(c.Context(), user.CompanyID.Value) + + if err != nil { + h.mongoLoggerSvc.Error("Invalid user company value", + zap.Int64("userID", user.ID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, + "Failed to fetch company for customer:") + } res := CustomersRes{ ID: user.ID, FirstName: user.FirstName, @@ -236,16 +455,226 @@ func (h *Handler) GetCustomerByID(c *fiber.Ctx) error { SuspendedAt: user.SuspendedAt, Suspended: user.Suspended, LastLogin: *lastLogin, + CompanyID: company.ID, + CompanyName: company.Name, } return response.WriteJSON(c, fiber.StatusOK, "User retrieved successfully", res, nil) } +// GetCustomerByID godoc +// @Summary Get customer by id +// @Description Get a single customer by id +// @Tags customer +// @Accept json +// @Produce json +// @Param id path int true "User ID" +// @Success 200 {object} CustomersRes +// @Failure 400 {object} response.APIResponse +// @Failure 401 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/tenant/{tenant_slug}/customer/{id} [get] +func (h *Handler) GetTenantCustomerByID(c *fiber.Ctx) error { + + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + + userIDstr := c.Params("id") + userID, err := strconv.ParseInt(userIDstr, 10, 64) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid customers ID") + } + + user, err := h.userSvc.GetUserByID(c.Context(), userID) + if err != nil { + h.mongoLoggerSvc.Error("Failed to get customers", + zap.Int64("userID", userID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get customers:"+err.Error()) + } + + lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID) + if err != nil { + if err != authentication.ErrRefreshTokenNotFound { + h.mongoLoggerSvc.Error("Failed to get user last login", + zap.Int64("userID", userID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login:"+err.Error()) + } + + lastLogin = &user.CreatedAt + } + + if !user.CompanyID.Valid || user.CompanyID.Value != companyID.Value { + h.mongoLoggerSvc.Error("Failed to get customer", + zap.Int64("userID", userID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get customer:"+err.Error()) + } + + company, err := h.companySvc.GetCompanyByID(c.Context(), user.CompanyID.Value) + + if err != nil { + h.mongoLoggerSvc.Error("Invalid user company value", + zap.Int64("userID", user.ID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, + "Failed to fetch company for customer:") + } + res := CustomersRes{ + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email, + PhoneNumber: user.PhoneNumber, + Role: user.Role, + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + SuspendedAt: user.SuspendedAt, + Suspended: user.Suspended, + LastLogin: *lastLogin, + CompanyID: company.ID, + CompanyName: company.Name, + } + + return response.WriteJSON(c, fiber.StatusOK, "User retrieved successfully", res, nil) +} + +// GetTenantCustomerBets godoc +// @Summary Get tenant customer bets +// @Description Get tenant customer bets +// @Tags customer +// @Accept json +// @Produce json +// @Param id path int true "User ID" +// @Success 200 {object} CustomersRes +// @Failure 400 {object} response.APIResponse +// @Failure 401 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/tenant/{tenant_slug}/customer/{id}/bets [get] +func (h *Handler) GetTenantCustomerBets(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + + userIDstr := c.Params("id") + userID, err := strconv.ParseInt(userIDstr, 10, 64) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid customers ID") + } + + user, err := h.userSvc.GetUserByID(c.Context(), userID) + if err != nil { + h.mongoLoggerSvc.Error("Failed to get customers", + zap.Int64("userID", userID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get customers:"+err.Error()) + } + + if !user.CompanyID.Valid || user.CompanyID.Value != companyID.Value { + h.mongoLoggerSvc.Warn("User Attempt to access another companies customer", + zap.Int64("userID", userID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get customer bet") + } + + bets, err := h.betSvc.GetBetByUserID(c.Context(), user.ID) + if err != nil { + h.mongoLoggerSvc.Error("Failed to get user bets", + zap.Int64("userID", userID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get user bet:"+err.Error()) + } + + res := make([]domain.BetRes, len(bets)) + for i, bet := range bets { + res[i] = domain.ConvertBet(bet) + } + + return response.WriteJSON(c, fiber.StatusOK, "User's Bets retrieved successfully", res, nil) +} + +// GetCustomerBets godoc +// @Summary Get customer bets +// @Description Get customer bets +// @Tags customer +// @Accept json +// @Produce json +// @Param id path int true "User ID" +// @Success 200 {object} CustomersRes +// @Failure 400 {object} response.APIResponse +// @Failure 401 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/customer/{id}/bets [get] +func (h *Handler) GetCustomerBets(c *fiber.Ctx) error { + userIDstr := c.Params("id") + userID, err := strconv.ParseInt(userIDstr, 10, 64) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid customers ID") + } + + user, err := h.userSvc.GetUserByID(c.Context(), userID) + if err != nil { + h.mongoLoggerSvc.Error("Failed to get customers", + zap.Int64("userID", userID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get customers:"+err.Error()) + } + + bets, err := h.betSvc.GetBetByUserID(c.Context(), user.ID) + if err != nil { + h.mongoLoggerSvc.Error("Failed to get user bets", + zap.Int64("userID", userID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get user bet:"+err.Error()) + } + + res := make([]domain.BetRes, len(bets)) + for i, bet := range bets { + res[i] = domain.ConvertBet(bet) + } + + return response.WriteJSON(c, fiber.StatusOK, "User's Bets retrieved successfully", res, nil) +} + type updateCustomerReq struct { FirstName string `json:"first_name" example:"John"` LastName string `json:"last_name" example:"Doe"` Suspended bool `json:"suspended" example:"false"` - CompanyID *int64 `json:"company_id,omitempty" example:"1"` } // UpdateCustomers godoc @@ -341,3 +770,108 @@ func (h *Handler) UpdateCustomer(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Customers updated successfully", nil, nil) } + +// UpdateTenantCustomer godoc +// @Summary Update Customers +// @Description Update Customers +// @Tags customer +// @Accept json +// @Produce json +// @Param Customers body updateCustomerReq true "Update Customers" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 401 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/tenant/{tenant_slug}/customer/{id} [put] +func (h *Handler) UpdateTenantCustomer(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + var req updateCustomerReq + + if err := c.BodyParser(&req); err != nil { + h.mongoLoggerSvc.Error("UpdateCustomers invalid request body", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body"+err.Error()) + } + + valErrs, ok := h.validator.Validate(c, req) + + if !ok { + var errMsg string + for field, msg := range valErrs { + errMsg += fmt.Sprintf("%s: %s; ", field, msg) + } + h.mongoLoggerSvc.Info("Failed to validate UpdateCustomerReq", + zap.Any("request", req), + zap.Int("status_code", fiber.StatusBadRequest), + zap.String("ErrMsg", errMsg), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, errMsg) + } + CustomersIdStr := c.Params("id") + CustomersId, err := strconv.ParseInt(CustomersIdStr, 10, 64) + if err != nil { + h.mongoLoggerSvc.Info("Invalid Customers ID", + zap.String("userID", CustomersIdStr), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid Customers ID") + } + + user, err := h.userSvc.GetUserByID(c.Context(), CustomersId) + if err != nil { + h.mongoLoggerSvc.Info("Customers Not Found", + zap.String("userID", CustomersIdStr), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Customers Not Found") + } + + if user.CompanyID.Value != companyID.Value { + h.mongoLoggerSvc.Warn("User Attempt to update another companies customer", + zap.String("userID", CustomersIdStr), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Customers Not Found") + } + err = h.userSvc.UpdateUser(c.Context(), domain.UpdateUserReq{ + UserId: CustomersId, + FirstName: domain.ValidString{ + Value: req.FirstName, + Valid: req.FirstName != "", + }, + LastName: domain.ValidString{ + Value: req.LastName, + Valid: req.LastName != "", + }, + Suspended: domain.ValidBool{ + Value: req.Suspended, + Valid: true, + }, + }, + ) + if err != nil { + h.mongoLoggerSvc.Error("Failed to update Customers", + zap.Int64("userID", CustomersId), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update Customers:"+err.Error()) + } + return response.WriteJSON(c, fiber.StatusOK, "Customers updated successfully", nil, nil) + +} diff --git a/internal/web_server/handlers/event_handler.go b/internal/web_server/handlers/event_handler.go index e0914a3..a149870 100644 --- a/internal/web_server/handlers/event_handler.go +++ b/internal/web_server/handlers/event_handler.go @@ -303,6 +303,7 @@ func (h *Handler) GetTenantUpcomingEvents(c *fiber.Ctx) error { Offset: offset, CountryCode: countryCode, Featured: isFeatured, + Active: domain.ValidBool{Value: true, Valid: true}, }) // fmt.Printf("League ID: %v", leagueID) @@ -347,7 +348,7 @@ func (h *Handler) GetTopLeagues(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "invalid company id") } - leagues, err := h.leagueSvc.GetAllLeaguesByCompany(c.Context(), companyID.Value, domain.LeagueFilter{ + leagues, _, err := h.leagueSvc.GetAllLeaguesByCompany(c.Context(), companyID.Value, domain.LeagueFilter{ IsFeatured: domain.ValidBool{ Value: true, Valid: true, @@ -370,6 +371,7 @@ func (h *Handler) GetTopLeagues(c *fiber.Ctx) error { Value: league.ID, Valid: true, }, + Active: domain.ValidBool{Value: true, Valid: true}, }) if err != nil { h.InternalServerErrorLogger().Warn("Error while fetching events for top league", @@ -511,7 +513,7 @@ type UpdateEventSettingsReq struct { // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/{tenant_slug}/events/{id}/settings [put] +// @Router /api/v1/tenant/{tenant_slug}/events/{id}/settings [put] func (h *Handler) UpdateEventSettings(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) if !companyID.Valid { @@ -523,7 +525,7 @@ func (h *Handler) UpdateEventSettings(c *fiber.Ctx) error { var req UpdateEventSettingsReq if err := c.BodyParser(&req); err != nil { - h.BadRequestLogger().Info("Failed to parse user id", + h.BadRequestLogger().Info("Failed to parse event id", zap.String("eventID", eventID), zap.Error(err), ) @@ -543,7 +545,7 @@ func (h *Handler) UpdateEventSettings(c *fiber.Ctx) error { for field, msg := range valErrs { errMsg += fmt.Sprintf("%s: %s; ", field, msg) } - h.BadRequestLogger().Error("Failed to update event featured", + h.BadRequestLogger().Error("Failed to update event settings", append(logFields, zap.String("errMsg", errMsg))..., ) return fiber.NewError(fiber.StatusBadRequest, errMsg) @@ -557,7 +559,62 @@ func (h *Handler) UpdateEventSettings(c *fiber.Ctx) error { }) if err != nil { - h.InternalServerErrorLogger().Error("Failed to update event featured", append(logFields, zap.Error(err))...) + h.InternalServerErrorLogger().Error("Failed to update event settings", append(logFields, zap.Error(err))...) + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + return response.WriteJSON(c, fiber.StatusOK, "Event updated successfully", nil, nil) + +} + +type SetEventIsMonitoredReq struct { + IsMonitored bool `json:"is_monitored"` +} + +// SetEventIsMonitored godoc +// @Summary update the event is_monitored +// @Description Update the event is_monitored +// @Tags event +// @Accept json +// @Produce json +// @Param id path int true "Event ID" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/events/{id}/is_monitored [patch] +func (h *Handler) SetEventIsMonitored(c *fiber.Ctx) error { + eventID := c.Params("id") + + var req SetEventIsMonitoredReq + + if err := c.BodyParser(&req); err != nil { + h.BadRequestLogger().Info("Failed to parse bet id", + zap.String("eventID", eventID), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + logFields := []zap.Field{ + zap.String("eventID", eventID), + zap.Any("is_featured", req.IsMonitored), + } + valErrs, ok := h.validator.Validate(c, req) + if !ok { + var errMsg string + for field, msg := range valErrs { + errMsg += fmt.Sprintf("%s: %s; ", field, msg) + } + h.BadRequestLogger().Error("Failed to update event featured", + append(logFields, zap.String("errMsg", errMsg))..., + ) + return fiber.NewError(fiber.StatusBadRequest, errMsg) + } + + err := h.eventSvc.UpdateEventMonitored(c.Context(), eventID, req.IsMonitored) + + if err != nil { + h.InternalServerErrorLogger().Error("Failed to update event is_monitored", append(logFields, zap.Error(err))...) return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index 41cd9a6..48f60f7 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -19,6 +19,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/raffle" "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" @@ -51,7 +52,8 @@ type Handler struct { settingSvc *settings.Service notificationSvc *notificationservice.Service userSvc *user.Service - referralSvc referralservice.ReferralStore + referralSvc *referralservice.Service + raffleSvc raffle.RaffleStore bonusSvc *bonus.Service reportSvc report.ReportStore chapaSvc *chapa.Service @@ -92,7 +94,8 @@ func New( reportSvc report.ReportStore, chapaSvc *chapa.Service, walletSvc *wallet.Service, - referralSvc referralservice.ReferralStore, + referralSvc *referralservice.Service, + raffleSvc raffle.RaffleStore, bonusSvc *bonus.Service, virtualGameSvc virtualgameservice.VirtualGameService, aleaVirtualGameSvc alea.AleaVirtualGameService, @@ -129,6 +132,7 @@ func New( chapaSvc: chapaSvc, walletSvc: walletSvc, referralSvc: referralSvc, + raffleSvc: raffleSvc, bonusSvc: bonusSvc, validator: validator, userSvc: userSvc, diff --git a/internal/web_server/handlers/leagues.go b/internal/web_server/handlers/leagues.go index 48274fd..73abbf8 100644 --- a/internal/web_server/handlers/leagues.go +++ b/internal/web_server/handlers/leagues.go @@ -33,6 +33,12 @@ func (h *Handler) GetAllLeagues(c *fiber.Ctx) error { Valid: true, } + searchQuery := c.Query("query") + searchString := domain.ValidString{ + Value: searchQuery, + Valid: searchQuery != "", + } + countryCodeQuery := c.Query("cc") countryCode := domain.ValidString{ Value: countryCodeQuery, @@ -73,6 +79,7 @@ func (h *Handler) GetAllLeagues(c *fiber.Ctx) error { zap.Bool("is_active_valid", isActive.Valid), zap.String("cc_query", countryCodeQuery), zap.String("cc", countryCode.Value), + zap.String("query", searchQuery), zap.Bool("cc_valid", countryCode.Valid), zap.String("sport_id_query", sportIDQuery), zap.Int32("sport_id", sportID.Value), @@ -85,6 +92,7 @@ func (h *Handler) GetAllLeagues(c *fiber.Ctx) error { SportID: sportID, Limit: limit, Offset: offset, + Query: searchString, }) if err != nil { @@ -93,7 +101,10 @@ func (h *Handler) GetAllLeagues(c *fiber.Ctx) error { ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to get leagues:"+err.Error()) } - return response.WriteJSON(c, fiber.StatusOK, "All leagues retrieved", leagues, nil) + + res := domain.ConvertBaseLeagueResList(leagues) + + return response.WriteJSON(c, fiber.StatusOK, "All leagues retrieved", res, nil) } // GetAllLeaguesForTenant godoc @@ -125,6 +136,11 @@ func (h *Handler) GetAllLeaguesForTenant(c *fiber.Ctx) error { Valid: true, } + searchQuery := c.Query("query") + searchString := domain.ValidString{ + Value: searchQuery, + Valid: searchQuery != "", + } countryCodeQuery := c.Query("cc") countryCode := domain.ValidString{ Value: countryCodeQuery, @@ -173,12 +189,13 @@ func (h *Handler) GetAllLeaguesForTenant(c *fiber.Ctx) error { zap.Bool("sport_id_valid", sportID.Valid), } - leagues, err := h.leagueSvc.GetAllLeaguesByCompany(c.Context(), companyID.Value, domain.LeagueFilter{ + leagues, total, err := h.leagueSvc.GetAllLeaguesByCompany(c.Context(), companyID.Value, domain.LeagueFilter{ CountryCode: countryCode, IsActive: isActive, SportID: sportID, Limit: limit, Offset: offset, + Query: searchString, }) if err != nil { @@ -186,7 +203,10 @@ func (h *Handler) GetAllLeaguesForTenant(c *fiber.Ctx) error { h.InternalServerErrorLogger().Error("Failed to get all leagues", append(queryLogFields, zap.Error(err))...) return fiber.NewError(fiber.StatusInternalServerError, "Failed to get leagues:"+err.Error()) } - return response.WriteJSON(c, fiber.StatusOK, "All leagues retrieved", leagues, nil) + + res := domain.ConvertLeagueWithSettingResList(leagues) + + return response.WritePaginatedJSON(c, fiber.StatusOK, "All leagues retrieved", res, nil, page, int(total)) } type SetLeagueActiveReq struct { diff --git a/internal/web_server/handlers/notification_handler.go b/internal/web_server/handlers/notification_handler.go index e278581..b822e6d 100644 --- a/internal/web_server/handlers/notification_handler.go +++ b/internal/web_server/handlers/notification_handler.go @@ -208,12 +208,17 @@ func (h *Handler) CreateAndSendNotification(c *fiber.Ctx) error { // return fiber.NewError(fiber.StatusForbidden, "Unauthorized to send notification to this recipient") // } + errorSeverity := domain.NotificationErrorSeverityMedium + if req.ErrorSeverity != nil { + errorSeverity = *req.ErrorSeverity + } + notification := &domain.Notification{ ID: "", RecipientID: req.RecipientID, Type: req.Type, Level: req.Level, - ErrorSeverity: req.ErrorSeverity, + ErrorSeverity: errorSeverity, Reciever: req.Reciever, IsRead: false, DeliveryStatus: domain.DeliveryStatusPending, @@ -257,12 +262,17 @@ func (h *Handler) CreateAndSendNotification(c *fiber.Ctx) error { notificationIDs := make([]string, 0, len(recipients)) for _, user := range recipients { + errorSeverity := domain.NotificationErrorSeverityMedium + if req.ErrorSeverity != nil { + errorSeverity = *req.ErrorSeverity + } + notification := &domain.Notification{ ID: "", RecipientID: user.ID, Type: req.Type, Level: req.Level, - ErrorSeverity: req.ErrorSeverity, + ErrorSeverity: errorSeverity, Reciever: req.Reciever, IsRead: false, DeliveryStatus: domain.DeliveryStatusPending, diff --git a/internal/web_server/handlers/odd_handler.go b/internal/web_server/handlers/odd_handler.go index 15585ff..192875c 100644 --- a/internal/web_server/handlers/odd_handler.go +++ b/internal/web_server/handlers/odd_handler.go @@ -1,6 +1,7 @@ package handlers import ( + "fmt" "strconv" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -300,3 +301,49 @@ func (h *Handler) GetTenantOddsByUpcomingID(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Odds retrieved successfully", odds, nil) } + +func (h *Handler) SaveOddsSetting(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + var req domain.CreateOddMarketSettingsReq + + if err := c.BodyParser(&req); err != nil { + h.BadRequestLogger().Info("Failed to parse event id", + zap.Int64("CompanyID", companyID.Value), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + logFields := []zap.Field{ + zap.Int64("companyID", companyID.Value), + zap.Any("is_active", req.IsActive), + zap.Any("custom_odd", req.CustomOdd), + } + + valErrs, ok := h.validator.Validate(c, req) + if !ok { + var errMsg string + for field, msg := range valErrs { + errMsg += fmt.Sprintf("%s: %s; ", field, msg) + } + h.BadRequestLogger().Error("Failed to insert odd settings", + append(logFields, zap.String("errMsg", errMsg))..., + ) + return fiber.NewError(fiber.StatusBadRequest, errMsg) + } + + err := h.prematchSvc.SaveOddsSettingReq(c.Context(), companyID.Value, req) + + if err != nil { + logFields = append(logFields, zap.Error(err)) + h.InternalServerErrorLogger().Error("Failed to save odds settings", append(logFields, zap.Error(err))...) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to save odds settings"+err.Error()) + } + + return response.WriteJSON(c, fiber.StatusOK, "Odds Settings saved successfully", nil, nil) + +} diff --git a/internal/web_server/handlers/raffle_handler.go b/internal/web_server/handlers/raffle_handler.go new file mode 100644 index 0000000..4c316f8 --- /dev/null +++ b/internal/web_server/handlers/raffle_handler.go @@ -0,0 +1,377 @@ +package handlers + +import ( + "errors" + "fmt" + "strconv" + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers" + "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" + "github.com/gofiber/fiber/v2" + "go.uber.org/zap" +) + +func (h *Handler) CreateRaffle(c *fiber.Ctx) error { + var req domain.CreateRaffle + if err := c.BodyParser(&req); err != nil { + h.mongoLoggerSvc.Info("Failed to parse raffle 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 { + var errMsg string + for field, msg := range valErrs { + errMsg += fmt.Sprintf("%s: %s; ", field, msg) + } + h.mongoLoggerSvc.Info("Failed to validate settings", + zap.String("errMsg", errMsg), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, errMsg) + } + + raffle, err := h.raffleSvc.CreateRaffle(c.Context(), req) + if err != nil { + h.mongoLoggerSvc.Error("Failed to create raffle", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create raffle") + } + + return response.WriteJSON(c, fiber.StatusOK, "Raffle created successfully", raffle, nil) +} + +func (h *Handler) AddRaffleFilter(c *fiber.Ctx) error { + var filter domain.RaffleFilter + + if err := c.BodyParser(&filter); err != nil { + h.mongoLoggerSvc.Info("Failed to parse raffle filter 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, filter); !ok { + var errMsg string + for field, msg := range valErrs { + errMsg += fmt.Sprintf("%s: %s; ", field, msg) + } + h.mongoLoggerSvc.Info("Failed to validate raffle filter", + zap.String("errMsg", errMsg), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, errMsg) + } + + switch filter.Type { + case "sport": + err := h.raffleSvc.AddSportRaffleFilter(c.Context(), filter.RaffleID, int64(filter.SportID), int64(filter.LeagueID)) + if err != nil { + h.mongoLoggerSvc.Error("Failed to add raffle filter", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to add raffle filter") + } + + // handle game case below + // there won't be a default case since its being handled in the validator + } + + return response.WriteJSON(c, fiber.StatusOK, "Raffle filter added successfully", nil, nil) +} + +func (h *Handler) DeleteRaffle(c *fiber.Ctx) error { + stringRaffleID := c.Params("id") + raffleID, err := strconv.Atoi(stringRaffleID) + if err != nil { + h.mongoLoggerSvc.Info("failed to parse raffle id", + zap.String("stringRaffleID", stringRaffleID), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid raffle id") + } + + raffle, err := h.raffleSvc.DeleteRaffle(c.Context(), int32(raffleID)) + if err != nil { + fmt.Println("raffle delete error: ", err) + h.mongoLoggerSvc.Error("Failed to delete raffle", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete raffle") + } + + return response.WriteJSON(c, fiber.StatusOK, "Raffle deleted successfully", raffle, nil) +} + +func (h *Handler) GetRafflesOfCompany(c *fiber.Ctx) error { + stringCompanyID := c.Params("id") + companyID, err := strconv.Atoi(stringCompanyID) + if err != nil || companyID == 0 { + h.mongoLoggerSvc.Info("failed to parse company id", + zap.String("stringCompanyID", stringCompanyID), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid company ID") + } + + companyRaffles, err := h.raffleSvc.GetRafflesOfCompany(c.Context(), int32(companyID)) + if err != nil { + h.mongoLoggerSvc.Error("Failed to fetch company raffle", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch company raffle") + } + + return response.WriteJSON(c, fiber.StatusOK, "Company Raffles fetched successfully", companyRaffles, nil) +} + +func (h *Handler) GetTenantRaffles(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + + companyRaffles, err := h.raffleSvc.GetRafflesOfCompany(c.Context(), int32(companyID.Value)) + if err != nil { + h.mongoLoggerSvc.Error("Failed to fetch company raffle", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch company raffle") + } + + return response.WriteJSON(c, fiber.StatusOK, "Company Raffles fetched successfully", companyRaffles, nil) +} + +func (h *Handler) GetRaffleStanding(c *fiber.Ctx) error { + raffleIDStr := c.Params("id") + limitStr := c.Params("limit") + + // if error happens while parsing, it just uses zero values + // resulting in empty standing + raffleID, _ := strconv.Atoi(raffleIDStr) + limit, _ := strconv.Atoi(limitStr) + + raffleStanding, err := h.raffleSvc.GetRaffleStanding(c.Context(), int32(raffleID), int32(limit)) + if err != nil { + h.mongoLoggerSvc.Error("Failed to fetch raffle standing", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch raffle standing") + } + + maskedRaffleStanding := make([]domain.RaffleStandingRes, 0, len(raffleStanding)) + + for _, standing := range raffleStanding { + maskedStanding := domain.RaffleStandingRes{ + UserID: standing.UserID, + RaffleID: standing.RaffleID, + FirstName: standing.FirstName, + LastName: standing.LastName, + PhoneNumber: helpers.MaskPhone(standing.PhoneNumber), + Email: helpers.MaskEmail(standing.Email), + TicketCount: standing.TicketCount, + } + + maskedRaffleStanding = append(maskedRaffleStanding, maskedStanding) + } + + return response.WriteJSON(c, fiber.StatusOK, "Raffles standing fetched successfully", maskedRaffleStanding, nil) +} + +func (h *Handler) GetRaffleWinners(c *fiber.Ctx) error { + raffleIDStr := c.Params("id") + limitStr := c.Params("limit") + + // if error happens while parsing, it just uses zero values + // resulting in empty standing + raffleID, _ := strconv.Atoi(raffleIDStr) + limit, _ := strconv.Atoi(limitStr) + + raffleStanding, err := h.raffleSvc.GetRaffleStanding(c.Context(), int32(raffleID), int32(limit)) + if err != nil { + h.mongoLoggerSvc.Error("Failed to fetch raffle standing", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch raffle standing") + } + + // set raffle as complete + if err := h.raffleSvc.SetRaffleComplete(c.Context(), int32(raffleID)); err != nil { + h.mongoLoggerSvc.Error("Failed to set raffle complete", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to set raffle complete") + } + + // add winners to table + var errs []error + for i, standing := range raffleStanding { + err = h.raffleSvc.CreateRaffleWinner(c.Context(), domain.RaffleWinnerParams{ + RaffleID: standing.RaffleID, + UserID: int32(standing.UserID), + Rank: int32(i + 1), + }) + + if err != nil { + errs = append(errs, err) + } + } + + if len(errs) != 0 { + h.mongoLoggerSvc.Error("Failed to create raffle winners", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(errors.Join(errs...)), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create raffle winners") + } + + return nil +} + +func (h *Handler) CreateRaffleTicket(c *fiber.Ctx) error { + var req domain.CreateRaffleTicket + if err := c.BodyParser(&req); err != nil { + fmt.Println("parser error: ", err) + h.mongoLoggerSvc.Info("Failed to parse raffle ticket 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 { + var errMsg string + for field, msg := range valErrs { + errMsg += fmt.Sprintf("%s: %s; ", field, msg) + } + h.mongoLoggerSvc.Info("Failed to validate settings", + zap.String("errMsg", errMsg), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, errMsg) + } + + raffleTicket, err := h.raffleSvc.CreateRaffleTicket(c.Context(), req) + if err != nil { + fmt.Println("raffle ticket create error: ", err) + h.mongoLoggerSvc.Error("Failed to create raffle ticket", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create raffle ticket") + } + + return response.WriteJSON(c, fiber.StatusOK, "Raffle created successfully", raffleTicket, nil) +} + +func (h *Handler) GetUserRaffleTickets(c *fiber.Ctx) error { + stringUserID := c.Params("id") + userID, err := strconv.Atoi(stringUserID) + if err != nil { + h.mongoLoggerSvc.Info("failed to parse company id", + zap.String("stringUserID", stringUserID), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid user ID") + } + + raffleTickets, err := h.raffleSvc.GetUserRaffleTickets(c.Context(), int32(userID)) + if err != nil { + h.mongoLoggerSvc.Error("Failed to fetch user raffle tickets", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch user raffle tickets") + } + + return response.WriteJSON(c, fiber.StatusOK, "User raffle tickets fetched successfully", raffleTickets, nil) +} + +func (h *Handler) SuspendRaffleTicket(c *fiber.Ctx) error { + stringRaffleTicketID := c.Params("id") + raffleTicketID, err := strconv.Atoi(stringRaffleTicketID) + if err != nil { + h.mongoLoggerSvc.Info("failed to parse raffle ticket id", + zap.String("stringUserID", stringRaffleTicketID), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid raffel ticket id") + } + + if err := h.raffleSvc.SuspendRaffleTicket(c.Context(), int32(raffleTicketID)); err != nil { + h.mongoLoggerSvc.Error("Failed to suspend raffle ticket", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to suspend raffle ticket") + } + + return response.WriteJSON(c, fiber.StatusOK, "User raffle tickets suspended successfully", nil, nil) +} + +func (h *Handler) UnSuspendRaffleTicket(c *fiber.Ctx) error { + stringRaffleTicketID := c.Params("id") + raffleTicketID, err := strconv.Atoi(stringRaffleTicketID) + if err != nil { + h.mongoLoggerSvc.Info("failed to parse raffle ticket id", + zap.String("stringUserID", stringRaffleTicketID), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid raffel ticket id") + } + + if err := h.raffleSvc.UnSuspendRaffleTicket(c.Context(), int32(raffleTicketID)); err != nil { + h.mongoLoggerSvc.Error("Failed to unsuspend raffle ticket", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to unsuspend raffle ticket") + } + + return response.WriteJSON(c, fiber.StatusOK, "User raffle tickets unsuspended successfully", nil, nil) + +} diff --git a/internal/web_server/handlers/referal_handlers.go b/internal/web_server/handlers/referal_handlers.go index ef8cb06..7ca3856 100644 --- a/internal/web_server/handlers/referal_handlers.go +++ b/internal/web_server/handlers/referal_handlers.go @@ -11,6 +11,12 @@ import ( ) func (h *Handler) CreateReferralCode(c *fiber.Ctx) error { + + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } userID, ok := c.Locals("user_id").(int64) if !ok || userID == 0 { h.mongoLoggerSvc.Info("Invalid user ID in context", @@ -20,8 +26,9 @@ func (h *Handler) CreateReferralCode(c *fiber.Ctx) error { ) return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification") } + referralCode, err := h.referralSvc.CreateReferralCode(c.Context(), userID, companyID.Value) - if err := h.referralSvc.CreateReferral(c.Context(), userID); err != nil { + if err != nil { h.mongoLoggerSvc.Error("Failed to create referral", zap.Int64("userID", userID), zap.Int("status_code", fiber.StatusInternalServerError), @@ -31,78 +38,19 @@ func (h *Handler) CreateReferralCode(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create referral") } - return response.WriteJSON(c, fiber.StatusOK, "Referral created successfully", nil, nil) + fmt.Printf("Successfully created referral!") + + res := domain.ConvertReferralCodeRes(referralCode) + + return response.WriteJSON(c, fiber.StatusOK, "Referral created successfully", res, nil) } -func (h *Handler) CreateReferralSettings(c *fiber.Ctx) error { - var req domain.ReferralSettingsReq - if err := c.BodyParser(&req); err != nil { - h.mongoLoggerSvc.Info("Failed to parse settings", - zap.Int("status_code", fiber.StatusBadRequest), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") +func (h *Handler) GetReferralCode(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") } - - if valErrs, ok := h.validator.Validate(c, req); !ok { - var errMsg string - for field, msg := range valErrs { - errMsg += fmt.Sprintf("%s: %s; ", field, msg) - } - h.mongoLoggerSvc.Info("Failed to validate settings", - zap.String("errMsg", errMsg), - zap.Int("status_code", fiber.StatusBadRequest), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusBadRequest, errMsg) - } - - settings, err := h.referralSvc.GetReferralSettings(c.Context()) - if err != nil { - h.mongoLoggerSvc.Error("Failed to fetch previous referral setting", - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to create referral") - } - - // only allow one referral setting for now - // for future it can be multiple and be able to choose from them - if settings != nil { - h.mongoLoggerSvc.Error("referral setting already exists", - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusInternalServerError, "referral setting already exists") - } - - if err := h.referralSvc.CreateReferralSettings(c.Context(), req); err != nil { - h.mongoLoggerSvc.Error("Failed to create referral setting", - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to create referral") - } - - return response.WriteJSON(c, fiber.StatusOK, "Referral created successfully", nil, nil) -} - -// GetReferralStats godoc -// @Summary Get referral statistics -// @Description Retrieves referral statistics for the authenticated user -// @Tags referral -// @Accept json -// @Produce json -// @Success 200 {object} domain.ReferralStats -// @Failure 401 {object} response.APIResponse -// @Failure 500 {object} response.APIResponse -// @Security Bearer -// @Router /api/v1/referral/stats [get] -func (h *Handler) GetReferralStats(c *fiber.Ctx) error { userID, ok := c.Locals("user_id").(int64) if !ok || userID == 0 { h.mongoLoggerSvc.Error("Invalid user ID in context", @@ -124,38 +72,54 @@ func (h *Handler) GetReferralStats(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user") } - stats, err := h.referralSvc.GetReferralStats(c.Context(), user.PhoneNumber) - if err != nil { - h.mongoLoggerSvc.Error("Failed to get referral stats", + if !user.CompanyID.Valid || user.CompanyID.Value != companyID.Value { + h.mongoLoggerSvc.Warn("User attempt to login to different company", zap.Int64("userID", userID), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve referral stats") + return fiber.NewError(fiber.StatusBadRequest, "Failed to retrieve user") } - return response.WriteJSON(c, fiber.StatusOK, "Referral stats retrieved successfully", stats, nil) + referrals, err := h.referralSvc.GetReferralCodesByUser(c.Context(), user.ID) + + if err != nil { + h.mongoLoggerSvc.Error("Failed to get user referrals", + zap.Int64("userID", userID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user referral codes") + } + + result := domain.ConvertReferralCodeResList(referrals) + + return response.WriteJSON(c, fiber.StatusOK, "Referral Code Fetched Successfully", result, nil) + } -// UpdateReferralSettings godoc -// @Summary Update referral settings -// @Description Updates referral settings (admin only) +// GetReferralStats godoc +// @Summary Get referral statistics +// @Description Retrieves referral statistics for the authenticated user // @Tags referral // @Accept json // @Produce json -// @Param settings body domain.ReferralSettings true "Referral settings" -// @Success 200 {object} response.APIResponse +// @Success 200 {object} domain.ReferralStats // @Failure 401 {object} response.APIResponse -// @Failure 403 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Security Bearer -// @Router /api/v1/referral/settings [put] -func (h *Handler) UpdateReferralSettings(c *fiber.Ctx) error { +// @Router /api/v1/tenant/{tenant_slug}/referral/stats [get] +func (h *Handler) GetReferralStats(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } userID, ok := c.Locals("user_id").(int64) if !ok || userID == 0 { - h.logger.Error("Invalid user ID in context") - h.mongoLoggerSvc.Error("Failed to delete user", + h.mongoLoggerSvc.Error("Invalid user ID in context", zap.Int64("userID", userID), zap.Int("status_code", fiber.StatusInternalServerError), zap.Time("timestamp", time.Now()), @@ -163,72 +127,6 @@ func (h *Handler) UpdateReferralSettings(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Invalid user id") } - user, err := h.userSvc.GetUserByID(c.Context(), userID) - if err != nil { - h.mongoLoggerSvc.Error("Failed to get user", - zap.Int64("userID", userID), - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } - - if user.Role != domain.RoleAdmin { - h.mongoLoggerSvc.Error("Access Forbidden", - zap.Int64("userID", userID), - zap.Int("status_code", fiber.StatusForbidden), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusForbidden, "Admin access required") - } - - var settings domain.ReferralSettings - if err := c.BodyParser(&settings); err != nil { - h.mongoLoggerSvc.Info("Failed to parse settings", - zap.Int64("userID", userID), - zap.Int("status_code", fiber.StatusBadRequest), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") - } - - settings.UpdatedBy = user.PhoneNumber - if err := h.referralSvc.UpdateReferralSettings(c.Context(), &settings); err != nil { - h.mongoLoggerSvc.Error("Failed to update referral settings", - zap.Int64("userID", userID), - zap.Int("status_code", fiber.StatusInternalServerError), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } - - return response.WriteJSON(c, fiber.StatusOK, "Referral settings updated successfully", nil, nil) -} - -// GetReferralSettings godoc -// @Summary Get referral settings -// @Description Retrieves current referral settings (admin only) -// @Tags referral -// @Accept json -// @Produce json -// @Success 200 {object} domain.ReferralSettings -// @Failure 401 {object} response.APIResponse -// @Failure 403 {object} response.APIResponse -// @Failure 500 {object} response.APIResponse -// @Security Bearer -// @Router /api/v1/referral/settings [get] -func (h *Handler) GetReferralSettings(c *fiber.Ctx) error { - // userID, ok := c.Locals("user_id").(int64) - // if !ok || userID == 0 { - // h.logger.Error("Invalid user ID in context") - // return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification") - // } - userID := int64(2) - user, err := h.userSvc.GetUserByID(c.Context(), userID) if err != nil { h.mongoLoggerSvc.Error("Failed to get user", @@ -240,26 +138,130 @@ func (h *Handler) GetReferralSettings(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user") } - if user.Role != domain.RoleAdmin { - h.mongoLoggerSvc.Error("Admin access required", - zap.Int64("userID", userID), - zap.Int("status_code", fiber.StatusForbidden), - zap.Error(err), - zap.Time("timestamp", time.Now()), - ) - return fiber.NewError(fiber.StatusForbidden, "Admin access required") - } - - settings, err := h.referralSvc.GetReferralSettings(c.Context()) - if err != nil { - h.mongoLoggerSvc.Error("Failed to get referral settings", + if !user.CompanyID.Valid || user.CompanyID.Value != companyID.Value { + h.mongoLoggerSvc.Warn("User attempt to login to different company", zap.Int64("userID", userID), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + return fiber.NewError(fiber.StatusBadRequest, "Failed to retrieve user") } - return response.WriteJSON(c, fiber.StatusOK, "Referral settings retrieved successfully", settings, nil) + stats, err := h.referralSvc.GetReferralStats(c.Context(), user.ID, companyID.Value) + if err != nil { + h.mongoLoggerSvc.Error("Failed to get referral stats", + zap.Int64("userID", userID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve referral stats") + } + + res := domain.ConvertReferralStatsRes(stats) + + return response.WriteJSON(c, fiber.StatusOK, "Referral stats retrieved successfully", res, nil) } + +// // UpdateReferralSettings godoc +// // @Summary Update referral settings +// // @Description Updates referral settings (admin only) +// // @Tags referral +// // @Accept json +// // @Produce json +// // @Param settings body domain.ReferralSettings true "Referral settings" +// // @Success 200 {object} response.APIResponse +// // @Failure 401 {object} response.APIResponse +// // @Failure 403 {object} response.APIResponse +// // @Failure 500 {object} response.APIResponse +// // @Security Bearer +// // @Router /api/v1/referral/settings [put] +// func (h *Handler) UpdateReferralSettings(c *fiber.Ctx) error { +// userID, ok := c.Locals("user_id").(int64) +// if !ok || userID == 0 { +// h.logger.Error("Invalid user ID in context") +// h.mongoLoggerSvc.Error("Failed to delete user", +// zap.Int64("userID", userID), +// zap.Int("status_code", fiber.StatusInternalServerError), +// zap.Time("timestamp", time.Now()), +// ) +// return fiber.NewError(fiber.StatusInternalServerError, "Invalid user id") +// } + +// user, err := h.userSvc.GetUserByID(c.Context(), userID) +// if err != nil { +// h.mongoLoggerSvc.Error("Failed to get user", +// zap.Int64("userID", userID), +// zap.Int("status_code", fiber.StatusInternalServerError), +// zap.Error(err), +// zap.Time("timestamp", time.Now()), +// ) +// return fiber.NewError(fiber.StatusInternalServerError, err.Error()) +// } + +// if user.Role != domain.RoleAdmin { +// h.mongoLoggerSvc.Error("Access Forbidden", +// zap.Int64("userID", userID), +// zap.Int("status_code", fiber.StatusForbidden), +// zap.Error(err), +// zap.Time("timestamp", time.Now()), +// ) +// return fiber.NewError(fiber.StatusForbidden, "Admin access required") +// } + +// var settings domain.ReferralSettings +// if err := c.BodyParser(&settings); err != nil { +// h.mongoLoggerSvc.Info("Failed to parse settings", +// zap.Int64("userID", userID), +// zap.Int("status_code", fiber.StatusBadRequest), +// zap.Error(err), +// zap.Time("timestamp", time.Now()), +// ) +// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") +// } + +// settings.UpdatedBy = user.PhoneNumber +// if err := h.referralSvc.UpdateReferralSettings(c.Context(), &settings); err != nil { +// h.mongoLoggerSvc.Error("Failed to update referral settings", +// zap.Int64("userID", userID), +// zap.Int("status_code", fiber.StatusInternalServerError), +// zap.Error(err), +// zap.Time("timestamp", time.Now()), +// ) +// return fiber.NewError(fiber.StatusInternalServerError, err.Error()) +// } + +// return response.WriteJSON(c, fiber.StatusOK, "Referral settings updated successfully", nil, nil) +// } + +// // GetReferralSettings godoc +// // @Summary Get referral settings +// // @Description Retrieves current referral settings (admin only) +// // @Tags referral +// // @Accept json +// // @Produce json +// // @Success 200 {object} domain.ReferralSettings +// // @Failure 401 {object} response.APIResponse +// // @Failure 403 {object} response.APIResponse +// // @Failure 500 {object} response.APIResponse +// // @Security Bearer +// // @Router /api/v1/referral/settings [get] +// func (h *Handler) GetReferralSettings(c *fiber.Ctx) error { +// // userID, ok := c.Locals("user_id").(int64) +// // if !ok || userID == 0 { +// // h.logger.Error("Invalid user ID in context") +// // return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification") +// // } +// settings, err := h.referralSvc.GetReferralSettings(c.Context()) +// if err != nil { +// h.mongoLoggerSvc.Error("Failed to get referral settings", +// zap.Int("status_code", fiber.StatusInternalServerError), +// zap.Error(err), +// zap.Time("timestamp", time.Now()), +// ) +// return fiber.NewError(fiber.StatusInternalServerError, err.Error()) +// } + +// return response.WriteJSON(c, fiber.StatusOK, "Referral settings retrieved successfully", settings, nil) +// } diff --git a/internal/web_server/handlers/settings_handler.go b/internal/web_server/handlers/settings_handler.go index bc30195..13e9890 100644 --- a/internal/web_server/handlers/settings_handler.go +++ b/internal/web_server/handlers/settings_handler.go @@ -17,12 +17,15 @@ func (h *Handler) GetGlobalSettingList(c *fiber.Ctx) error { h.mongoLoggerSvc.Error("Failed to fetch settings", zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), + zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to get setting list:"+err.Error()) } - return response.WriteJSON(c, fiber.StatusOK, "All Settings retrieved successfully", settingsList, nil) + res := domain.ConvertSettingListRes(settingsList) + + return response.WriteJSON(c, fiber.StatusOK, "All Settings retrieved successfully", res, nil) } func (h *Handler) GetGlobalSettingByKey(c *fiber.Ctx) error { @@ -100,7 +103,9 @@ func (h *Handler) UpdateGlobalSettingList(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to get setting list:"+err.Error()) } - return response.WriteJSON(c, fiber.StatusOK, "setting updated", settingsList, nil) + res := domain.ConvertSettingListRes(settingsList) + + return response.WriteJSON(c, fiber.StatusOK, "setting updated", res, nil) } func (h *Handler) SaveCompanySettingList(c *fiber.Ctx) error { @@ -178,7 +183,9 @@ func (h *Handler) GetCompanySettingList(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to get setting list:"+err.Error()) } - return response.WriteJSON(c, fiber.StatusOK, "All Settings retrieved successfully", settingsList, nil) + res := domain.ConvertSettingListRes(settingsList) + + return response.WriteJSON(c, fiber.StatusOK, "All Settings retrieved successfully", res, nil) } // /api/v1/{tenant_slug}/settings/{key} diff --git a/internal/web_server/handlers/shop_handler.go b/internal/web_server/handlers/shop_handler.go index 7a25615..ff363b8 100644 --- a/internal/web_server/handlers/shop_handler.go +++ b/internal/web_server/handlers/shop_handler.go @@ -27,7 +27,7 @@ func (h *Handler) CreateShopBet(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) role := c.Locals("role").(domain.Role) company_id := c.Locals("company_id").(domain.ValidInt64) - + var req domain.ShopBetReq if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Info("CreateBetReq failed to parse request", @@ -65,7 +65,7 @@ func (h *Handler) CreateShopBet(c *fiber.Ctx) error { zap.Error(err), zap.Time("timestamp", time.Now()), ) - return fiber.NewError(statusCode, "failed to create shop bet"+err.Error()) + return fiber.NewError(statusCode, err.Error()) } res := domain.ConvertShopBet(shopBet) @@ -464,6 +464,7 @@ func (h *Handler) GetAllTransactions(c *fiber.Ctx) error { // role := c.Locals("role").(domain.Role) companyID := c.Locals("company_id").(domain.ValidInt64) branchID := c.Locals("branch_id").(domain.ValidInt64) + role := c.Locals("role").(domain.Role) searchQuery := c.Query("query") searchString := domain.ValidString{ @@ -509,6 +510,14 @@ func (h *Handler) GetAllTransactions(c *fiber.Ctx) error { } } + companyFilter := int64(c.QueryInt("company_id")) + if role == domain.RoleSuperAdmin { + companyID = domain.ValidInt64{ + Value: companyFilter, + Valid: companyFilter != 0, + } + } + // Check user role and fetch transactions accordingly transactions, err := h.transactionSvc.GetAllShopTransactions(c.Context(), domain.ShopTransactionFilter{ CompanyID: companyID, diff --git a/internal/web_server/handlers/ticket_handler.go b/internal/web_server/handlers/ticket_handler.go index f3b108a..8f335c3 100644 --- a/internal/web_server/handlers/ticket_handler.go +++ b/internal/web_server/handlers/ticket_handler.go @@ -123,6 +123,10 @@ func (h *Handler) GetTicketByID(c *fiber.Ctx) error { if ticket.CompanyID != companyID.Value { h.mongoLoggerSvc.Warn("User attempt to access another company ticket", zap.Int64("ticketID", id), + zap.Int64("ticket CompanyID", ticket.CompanyID), + zap.Int64("companyID", companyID.Value), + zap.Bool("companyID Valid", companyID.Valid), + zap.Int("status_code", fiber.StatusNotFound), zap.Error(err), zap.Time("timestamp", time.Now()), diff --git a/internal/web_server/handlers/transaction_approver.go b/internal/web_server/handlers/transaction_approver.go new file mode 100644 index 0000000..e949a9d --- /dev/null +++ b/internal/web_server/handlers/transaction_approver.go @@ -0,0 +1,438 @@ +package handlers + +import ( + "fmt" + "strconv" + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" + "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" + "github.com/gofiber/fiber/v2" + "go.uber.org/zap" +) + +type CreateTransactionApproverReq struct { + FirstName string `json:"first_name" example:"John"` + LastName string `json:"last_name" example:"Doe"` + Email string `json:"email" example:"john.doe@example.com"` + PhoneNumber string `json:"phone_number" example:"1234567890"` + Password string `json:"password" example:"password123"` + CompanyID int64 `json:"company_id" example:"1"` +} + +// CreateTransactionApprover godoc +// @Summary Create transaction approver +// @Description Create transaction approver +// @Tags admin +// @Accept json +// @Produce json +// @Param manger body CreateTransactionApproverReq true "Create transaction approver" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 401 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/admin [post] +func (h *Handler) CreateTransactionApprover(c *fiber.Ctx) error { + var companyID domain.ValidInt64 + var req CreateTransactionApproverReq + + if err := c.BodyParser(&req); err != nil { + h.mongoLoggerSvc.Info("failed to parse CreateAdmin request", + zap.Int64("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request:"+err.Error()) + } + + valErrs, ok := h.validator.Validate(c, req) + if !ok { + var errMsg string + for field, msg := range valErrs { + errMsg += fmt.Sprintf("%s: %s; ", field, msg) + } + 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 fiber.NewError(fiber.StatusBadRequest, errMsg) + } + + _, err := h.companySvc.GetCompanyByID(c.Context(), req.CompanyID) + if err != nil { + 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 fiber.NewError(fiber.StatusInternalServerError, "Company ID is invalid:"+err.Error()) + } + companyID = domain.ValidInt64{ + Value: req.CompanyID, + Valid: true, + } + + user := domain.CreateUserReq{ + FirstName: req.FirstName, + LastName: req.LastName, + Email: req.Email, + PhoneNumber: req.PhoneNumber, + Password: req.Password, + Role: string(domain.RoleTransactionApprover), + CompanyID: companyID, + } + + newUser, err := h.userSvc.CreateUser(c.Context(), user, true) + if err != nil { + 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 fiber.NewError(fiber.StatusInternalServerError, "Failed to create admin:"+err.Error()) + } + + h.mongoLoggerSvc.Info("transaction_approver created successfully", + zap.Int64("transaction_approver_id", newUser.ID), + zap.String("email", newUser.Email), + zap.Time("timestamp", time.Now()), + ) + + return response.WriteJSON(c, fiber.StatusOK, "Transaction Approver created successfully", nil, nil) +} + +type TransactionApproverRes struct { + ID int64 `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` + PhoneNumber string `json:"phone_number"` + Role domain.Role `json:"role"` + EmailVerified bool `json:"email_verified"` + PhoneVerified bool `json:"phone_verified"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LastLogin time.Time `json:"last_login"` + SuspendedAt time.Time `json:"suspended_at"` + Suspended bool `json:"suspended"` +} + +// GetAllAdmins godoc +// @Summary Get all Admins +// @Description Get all Admins +// @Tags admin +// @Accept json +// @Produce json +// @Param page query int false "Page number" +// @Param page_size query int false "Page size" +// @Success 200 {object} AdminRes +// @Failure 400 {object} response.APIResponse +// @Failure 401 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/t-approver [get] +func (h *Handler) GetAllTransactionApprovers(c *fiber.Ctx) error { + role := c.Locals("role").(domain.Role) + companyID := c.Locals("company_id").(domain.ValidInt64) + + 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.Info("invalid start_time format", "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid start_time format") + } + 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.Info("invalid start_time format", "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid start_time format") + } + createdAfter = domain.ValidTime{ + Value: createdAfterParsed, + Valid: true, + } + } + + var companyIDFilter domain.ValidInt64 + if role == domain.RoleSuperAdmin { + companyIDQuery := int64(c.QueryInt("company_id")) + companyIDFilter = domain.ValidInt64{ + Value: companyIDQuery, + Valid: companyIDQuery != 0, + } + } else { + if !companyID.Valid { + h.logger.Info("invalid companyID") + return fiber.NewError(fiber.StatusBadRequest, "Unable to get company ID") + } + + companyIDFilter = companyID + } + + filter := domain.UserFilter{ + Role: string(domain.RoleTransactionApprover), + CompanyID: companyIDFilter, + Page: domain.ValidInt{ + Value: c.QueryInt("page", 1) - 1, + Valid: true, + }, + PageSize: domain.ValidInt{ + Value: c.QueryInt("page_size", 10), + Valid: true, + }, + Query: searchString, + CreatedBefore: createdBefore, + CreatedAfter: createdAfter, + } + + valErrs, ok := h.validator.Validate(c, filter) + if !ok { + var errMsg string + for field, msg := range valErrs { + errMsg += fmt.Sprintf("%s: %s; ", field, msg) + } + h.mongoLoggerSvc.Info("invalid filter values in GetAllAdmins request", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Any("validation_errors", valErrs), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, errMsg) + } + + users, total, err := h.userSvc.GetAllUsers(c.Context(), filter) + if err != nil { + h.mongoLoggerSvc.Error("failed to get users from user service", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Any("filter", filter), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get users"+err.Error()) + } + + result := make([]TransactionApproverRes, len(users)) + for index, admin := range users { + lastLogin, err := h.authSvc.GetLastLogin(c.Context(), admin.ID) + if err != nil { + if err == authentication.ErrRefreshTokenNotFound { + lastLogin = &admin.CreatedAt + } else { + 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"+err.Error()) + } + } + + result[index] = TransactionApproverRes{ + ID: admin.ID, + FirstName: admin.FirstName, + LastName: admin.LastName, + Email: admin.Email, + PhoneNumber: admin.PhoneNumber, + Role: admin.Role, + EmailVerified: admin.EmailVerified, + PhoneVerified: admin.PhoneVerified, + CreatedAt: admin.CreatedAt, + UpdatedAt: admin.UpdatedAt, + SuspendedAt: admin.SuspendedAt, + Suspended: admin.Suspended, + LastLogin: *lastLogin, + } + } + + h.mongoLoggerSvc.Info("approvers 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)) +} + +// GetAdminByID godoc +// @Summary Get admin by id +// @Description Get a single admin by id +// @Tags admin +// @Accept json +// @Produce json +// @Param id path int true "User ID" +// @Success 200 {object} AdminRes +// @Failure 400 {object} response.APIResponse +// @Failure 401 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/t-approver/{id} [get] +func (h *Handler) GetTransactionApproverByID(c *fiber.Ctx) error { + userIDstr := c.Params("id") + userID, err := strconv.ParseInt(userIDstr, 10, 64) + if err != nil { + 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 fiber.NewError(fiber.StatusBadRequest, "Invalid admin ID") + } + + user, err := h.userSvc.GetUserByID(c.Context(), userID) + if err != nil { + 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 fiber.NewError(fiber.StatusInternalServerError, "Failed to get admin"+err.Error()) + } + + lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID) + 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:"+err.Error()) + } + if err == authentication.ErrRefreshTokenNotFound { + lastLogin = &user.CreatedAt + } + + res := TransactionApproverRes{ + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email, + PhoneNumber: user.PhoneNumber, + Role: user.Role, + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + SuspendedAt: user.SuspendedAt, + Suspended: user.Suspended, + LastLogin: *lastLogin, + } + + 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 updateTransactionApproverReq struct { + FirstName string `json:"first_name" example:"John"` + LastName string `json:"last_name" example:"Doe"` + Suspended bool `json:"suspended" example:"false"` +} + +// UpdateAdmin godoc +// @Summary Update Admin +// @Description Update Admin +// @Tags admin +// @Accept json +// @Produce json +// @Param admin body updateAdminReq true "Update Admin" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 401 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/t-approver/{id} [put] +func (h *Handler) UpdateTransactionApprover(c *fiber.Ctx) error { + var req updateTransactionApproverReq + if err := c.BodyParser(&req); err != nil { + h.mongoLoggerSvc.Error("UpdateAdmin failed - invalid request body", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error()) + } + + valErrs, ok := h.validator.Validate(c, req) + if !ok { + var errMsg string + for field, msg := range valErrs { + errMsg += fmt.Sprintf("%s: %s; ", field, msg) + } + h.mongoLoggerSvc.Error("UpdateAdmin failed - validation errors", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Any("validation_errors", valErrs), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, errMsg) + } + + ApproverIDStr := c.Params("id") + ApproverID, err := strconv.ParseInt(ApproverIDStr, 10, 64) + if err != nil { + h.mongoLoggerSvc.Info("UpdateAdmin failed - invalid Admin ID param", + zap.Int("status_code", fiber.StatusBadRequest), + zap.String("admin_id_param", ApproverIDStr), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid Admin ID") + } + + err = h.userSvc.UpdateUser(c.Context(), domain.UpdateUserReq{ + UserId: ApproverID, + FirstName: domain.ValidString{ + Value: req.FirstName, + Valid: req.FirstName != "", + }, + LastName: domain.ValidString{ + Value: req.LastName, + Valid: req.LastName != "", + }, + Suspended: domain.ValidBool{ + Value: req.Suspended, + Valid: true, + }, + }) + if err != nil { + h.mongoLoggerSvc.Error("UpdateAdmin failed - user service error", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Int64("admin_id", ApproverID), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update admin:"+err.Error()) + } + + h.mongoLoggerSvc.Info("UpdateAdmin succeeded", + zap.Int("status_code", fiber.StatusOK), + zap.Int64("admin_id", ApproverID), + zap.Time("timestamp", time.Now()), + ) + + return response.WriteJSON(c, fiber.StatusOK, "Managers updated successfully", nil, nil) +} diff --git a/internal/web_server/handlers/user.go b/internal/web_server/handlers/user.go index decf177..5c185fc 100644 --- a/internal/web_server/handlers/user.go +++ b/internal/web_server/handlers/user.go @@ -143,13 +143,13 @@ func (h *Handler) SendRegisterCode(c *fiber.Ctx) error { } type RegisterUserReq struct { - FirstName string `json:"first_name" example:"John"` - LastName string `json:"last_name" example:"Doe"` - Email string `json:"email" example:"john.doe@example.com"` - PhoneNumber string `json:"phone_number" example:"1234567890"` - Password string `json:"password" example:"password123"` - Otp string `json:"otp" example:"123456"` - ReferalCode string `json:"referal_code" example:"ABC123"` + FirstName string `json:"first_name" example:"John"` + LastName string `json:"last_name" example:"Doe"` + Email string `json:"email" example:"john.doe@example.com"` + PhoneNumber string `json:"phone_number" example:"1234567890"` + Password string `json:"password" example:"password123"` + Otp string `json:"otp" example:"123456"` + ReferralCode string `json:"referral_code" example:"ABC123"` } // RegisterUser godoc @@ -193,7 +193,7 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error { PhoneNumber: req.PhoneNumber, Password: req.Password, Otp: req.Otp, - ReferralCode: req.ReferalCode, + ReferralCode: req.ReferralCode, OtpMedium: domain.OtpMediumEmail, CompanyID: companyID, Role: string(domain.RoleCustomer), @@ -247,12 +247,12 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create user wallet:"+err.Error()) } - if req.ReferalCode != "" { - err = h.referralSvc.ProcessReferral(c.Context(), req.PhoneNumber, req.ReferalCode, companyID.Value) + if req.ReferralCode != "" { + err = h.referralSvc.ProcessReferral(c.Context(), newUser.ID, req.ReferralCode, companyID.Value) if err != nil { h.mongoLoggerSvc.Error("Failed to process referral during registration", zap.String("phone", req.PhoneNumber), - zap.String("code", req.ReferalCode), + zap.String("code", req.ReferralCode), zap.Error(err), zap.Time("timestamp", time.Now()), ) @@ -293,8 +293,70 @@ type ResetCodeReq struct { // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/{tenant_slug}/user/sendResetCode [post] +// @Router /api/v1/user/sendResetCode [post] func (h *Handler) SendResetCode(c *fiber.Ctx) error { + var req ResetCodeReq + if err := c.BodyParser(&req); err != nil { + h.mongoLoggerSvc.Info("Failed to parse SendResetCode request", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error()) + } + + if valErrs, ok := h.validator.Validate(c, req); !ok { + var errMsg string + for field, msg := range valErrs { + errMsg += fmt.Sprintf("%s: %s; ", field, msg) + } + return fiber.NewError(fiber.StatusBadRequest, errMsg) + } + + var sentTo string + var medium domain.OtpMedium + if req.Email != "" { + sentTo = req.Email + medium = domain.OtpMediumEmail + } else if req.PhoneNumber != "" { + sentTo = req.PhoneNumber + medium = domain.OtpMediumSms + } else { + h.mongoLoggerSvc.Info("Email or PhoneNumber must be provided", + zap.String("Email", req.Email), + zap.String("Phone Number", req.PhoneNumber), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided") + } + + if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo, domain.AfroMessage, domain.ValidInt64{}); err != nil { + h.mongoLoggerSvc.Error("Failed to send reset code", + zap.String("medium", string(medium)), + zap.String("sentTo", string(sentTo)), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to send reset code:"+err.Error()) + } + + return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil) +} + +// SendTenantResetCode godoc +// @Summary Send reset code +// @Description Send reset code +// @Tags user +// @Accept json +// @Produce json +// @Param resetCode body ResetCodeReq true "Send reset code" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/{tenant_slug}/user/sendResetCode [post] +func (h *Handler) SendTenantResetCode(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) if !companyID.Valid { h.BadRequestLogger().Error("invalid company id") @@ -367,7 +429,7 @@ type ResetPasswordReq struct { // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/{tenant_slug}/user/resetPassword [post] +// @Router /api/v1/user/resetPassword [post] func (h *Handler) ResetPassword(c *fiber.Ctx) error { var req ResetPasswordReq @@ -421,6 +483,76 @@ func (h *Handler) ResetPassword(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Password reset successful", nil, nil) } +// ResetTenantPassword godoc +// @Summary Reset tenant password +// @Description Reset tenant password +// @Tags user +// @Accept json +// @Produce json +// @Param resetPassword body ResetPasswordReq true "Reset password" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/{tenant_slug}/user/resetPassword [post] +func (h *Handler) ResetTenantPassword(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + + var req ResetPasswordReq + if err := c.BodyParser(&req); err != nil { + h.mongoLoggerSvc.Info("Failed to parse ResetPassword request", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error()) + } + + if valErrs, ok := h.validator.Validate(c, req); !ok { + var errMsg string + for field, msg := range valErrs { + errMsg += fmt.Sprintf("%s: %s; ", field, msg) + } + return fiber.NewError(fiber.StatusBadRequest, errMsg) + } + + medium, err := getMedium(req.Email, req.PhoneNumber) + if err != nil { + h.mongoLoggerSvc.Info("Failed to determine medium for ResetPassword", + zap.String("Email", req.Email), + zap.String("Phone Number", req.PhoneNumber), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + resetReq := domain.ResetPasswordReq{ + Email: req.Email, + PhoneNumber: req.PhoneNumber, + Password: req.Password, + Otp: req.Otp, + OtpMedium: medium, + CompanyID: companyID.Value, + } + + if err := h.userSvc.ResetPassword(c.Context(), resetReq); err != nil { + h.mongoLoggerSvc.Error("Failed to reset password", + zap.Any("userID", resetReq), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset password:"+err.Error()) + } + + return response.WriteJSON(c, fiber.StatusOK, "Password reset successful", nil, nil) +} + type UserProfileRes struct { ID int64 `json:"id"` FirstName string `json:"first_name"` @@ -503,6 +635,7 @@ func (h *Handler) CustomerProfile(c *fiber.Ctx) error { lastLogin = &user.CreatedAt } + res := CustomerProfileRes{ ID: user.ID, FirstName: user.FirstName, diff --git a/internal/web_server/handlers/veli_games.go b/internal/web_server/handlers/veli_games.go index b5f5f02..c8adbd2 100644 --- a/internal/web_server/handlers/veli_games.go +++ b/internal/web_server/handlers/veli_games.go @@ -4,12 +4,12 @@ import ( "context" "errors" "fmt" - "log" "strings" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/gofiber/fiber/v2" + "go.uber.org/zap" ) // GetProviders godoc @@ -37,7 +37,10 @@ func (h *Handler) GetProviders(c *fiber.Ctx) error { } res, err := h.veliVirtualGameSvc.GetProviders(context.Background(), req) if err != nil { - log.Println("GetProviders error:", err) + h.InternalServerErrorLogger().Error("Failed to [VeliGameHandler]GetProviders", + zap.Any("request", req), + zap.Error(err), + ) return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ Message: "Failed to retrieve providers", Error: err.Error(), @@ -78,8 +81,10 @@ func (h *Handler) GetGamesByProvider(c *fiber.Ctx) error { res, err := h.veliVirtualGameSvc.GetGames(context.Background(), req) if err != nil { - log.Println("GetGames error:", err) - + h.InternalServerErrorLogger().Error("Failed to [VeliGameHandler]GetGames", + zap.Any("request", req), + zap.Error(err), + ) // Handle provider disabled case specifically if strings.Contains(err.Error(), "is disabled") { return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{ @@ -89,7 +94,7 @@ func (h *Handler) GetGamesByProvider(c *fiber.Ctx) error { } // Fallback for other errors - return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to retrieve games", Error: err.Error(), }) @@ -130,7 +135,9 @@ func (h *Handler) StartGame(c *fiber.Ctx) error { Error: err.Error(), }) } - + + // There needs to be a way to generate a sessionID + // Attach user ID to request req.PlayerID = fmt.Sprintf("%d", userId) @@ -139,10 +146,14 @@ func (h *Handler) StartGame(c *fiber.Ctx) error { req.BrandID = h.Cfg.VeliGames.BrandID } + req.IP = c.IP() + res, err := h.veliVirtualGameSvc.StartGame(context.Background(), req) if err != nil { - log.Println("StartGame error:", err) - + h.InternalServerErrorLogger().Error("Failed to [VeliGameHandler]StartGame", + zap.Any("request", req), + zap.Error(err), + ) // Handle provider disabled case specifically if strings.Contains(err.Error(), "is disabled") { return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{ @@ -191,10 +202,14 @@ func (h *Handler) StartDemoGame(c *fiber.Ctx) error { req.BrandID = h.Cfg.VeliGames.BrandID } + req.IP = c.IP() + res, err := h.veliVirtualGameSvc.StartDemoGame(context.Background(), req) if err != nil { - log.Println("StartDemoGame error:", err) - + h.InternalServerErrorLogger().Error("Failed to [VeliGameHandler]StartDemoGame", + zap.Any("request", req), + zap.Error(err), + ) // Handle provider disabled case specifically if strings.Contains(err.Error(), "is disabled") { return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{ @@ -341,7 +356,10 @@ func (h *Handler) GetGamingActivity(c *fiber.Ctx) error { resp, err := h.veliVirtualGameSvc.GetGamingActivity(c.Context(), req) if err != nil { - log.Println("GetGamingActivity error:", err) + h.InternalServerErrorLogger().Error("Failed to [VeliGameHandler]GetGamingActivity", + zap.Any("request", req), + zap.Error(err), + ) return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to retrieve gaming activity", Error: err.Error(), @@ -378,7 +396,10 @@ func (h *Handler) GetHugeWins(c *fiber.Ctx) error { resp, err := h.veliVirtualGameSvc.GetHugeWins(c.Context(), req) if err != nil { - log.Println("GetHugeWins error:", err) + h.InternalServerErrorLogger().Error("Failed to [VeliGameHandler]GetHugeWins", + zap.Any("request", req), + zap.Error(err), + ) return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to retrieve huge wins", Error: err.Error(), @@ -416,7 +437,10 @@ func (h *Handler) GetCreditBalances(c *fiber.Ctx) error { res, err := h.veliVirtualGameSvc.GetCreditBalances(c.Context(), brandID) if err != nil { - log.Println("GetCreditBalances error:", err) + h.InternalServerErrorLogger().Error("Failed to [VeliGameHandler]GetCreditBalances", + zap.String("brandID", brandID), + zap.Error(err), + ) return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ Message: "Failed to fetch credit balances", Error: err.Error(), diff --git a/internal/web_server/handlers/virtual_games_hadlers.go b/internal/web_server/handlers/virtual_games_hadlers.go index 058f4e0..c077906 100644 --- a/internal/web_server/handlers/virtual_games_hadlers.go +++ b/internal/web_server/handlers/virtual_games_hadlers.go @@ -12,6 +12,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" + "github.com/jackc/pgx/v5/pgtype" ) type launchVirtualGameReq struct { @@ -50,12 +51,29 @@ func (h *Handler) ListVirtualGames(c *fiber.Ctx) error { } category := c.Query("category", "") search := c.Query("search", "") + providerID := c.Query("providerID", "") params := dbgen.GetAllVirtualGamesParams{ - Column1: category, - Column2: search, - Limit: int32(limit), - Offset: int32(offset), + Category: pgtype.Text{ + String: category, + Valid: category != "", + }, + Name: pgtype.Text{ + String: search, + Valid: search != "", + }, + ProviderID: pgtype.Text{ + String: providerID, + Valid: providerID != "", + }, + Offset: pgtype.Int4{ + Int32: int32(offset), + Valid: offset != 0, + }, + Limit: pgtype.Int4{ + Int32: int32(limit), + Valid: limit != 0, + }, } // --- Call service method --- diff --git a/internal/web_server/jwt/jwt.go b/internal/web_server/jwt/jwt.go index 8a3f0b3..b9313ae 100644 --- a/internal/web_server/jwt/jwt.go +++ b/internal/web_server/jwt/jwt.go @@ -2,6 +2,7 @@ package jwtutil import ( "errors" + "fmt" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -15,22 +16,23 @@ var ( ErrRefreshTokenNotFound = errors.New("refresh token not found") ) + type UserClaim struct { jwt.RegisteredClaims UserId int64 Role domain.Role - CompanyID domain.ValidInt64 + CompanyID domain.NullJwtInt64 } 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"` - CompanyID domain.ValidInt64 `json:"company_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.NullJwtInt64 `json:"company_id"` } type JwtConfig struct { @@ -41,15 +43,18 @@ type JwtConfig struct { func CreateJwt(userId int64, Role domain.Role, CompanyID domain.ValidInt64, key string, expiry int) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaim{ RegisteredClaims: jwt.RegisteredClaims{ - Issuer: "github.com/lafetz/snippitstash", + Issuer: "fortune-bet", IssuedAt: jwt.NewNumericDate(time.Now()), - Audience: jwt.ClaimStrings{"fortune.com"}, + Audience: jwt.ClaimStrings{"api.fortunebets.net"}, NotBefore: jwt.NewNumericDate(time.Now()), ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expiry) * time.Second)), }, - UserId: userId, - Role: Role, - CompanyID: CompanyID, + UserId: userId, + Role: Role, + CompanyID: domain.NullJwtInt64{ + Value: CompanyID.Value, + Valid: CompanyID.Valid, + }, }) jwtToken, err := token.SignedString([]byte(key)) return jwtToken, err @@ -70,7 +75,10 @@ func CreatePopOKJwt(userID int64, CompanyID domain.ValidInt64, username, currenc Lang: lang, Mode: mode, SessionID: sessionID, - CompanyID: CompanyID, + CompanyID: domain.NullJwtInt64{ + Value: CompanyID.Value, + Valid: CompanyID.Valid, + }, }) return token.SignedString([]byte(key)) } @@ -84,6 +92,7 @@ func ParseJwt(jwtToken string, key string) (*UserClaim, error) { return nil, ErrExpiredToken } if errors.Is(err, jwt.ErrTokenMalformed) { + fmt.Printf("error %v", err.Error()) return nil, ErrMalformedToken } return nil, err diff --git a/internal/web_server/middleware.go b/internal/web_server/middleware.go index da941d2..68d4889 100644 --- a/internal/web_server/middleware.go +++ b/internal/web_server/middleware.go @@ -2,6 +2,7 @@ package httpserver import ( "errors" + "fmt" "strings" "time" @@ -56,6 +57,7 @@ func (a *App) authMiddleware(c *fiber.Ctx) error { zap.String("ip_address", ip), zap.String("user_agent", userAgent), zap.Time("timestamp", time.Now()), + zap.Error(err), ) return fiber.NewError(fiber.StatusUnauthorized, "Invalid access token") } @@ -79,7 +81,10 @@ func (a *App) authMiddleware(c *fiber.Ctx) error { } c.Locals("user_id", claim.UserId) c.Locals("role", claim.Role) - c.Locals("company_id", claim.CompanyID) + c.Locals("company_id", domain.ValidInt64{ + Value: claim.CompanyID.Value, + Valid: claim.CompanyID.Valid, + }) c.Locals("refresh_token", refreshToken) var branchID domain.ValidInt64 @@ -198,6 +203,7 @@ func (a *App) WebsocketAuthMiddleware(c *fiber.Ctx) error { zap.String("ip_address", ip), zap.String("user_agent", userAgent), zap.Time("timestamp", time.Now()), + zap.Error(err), ) return fiber.NewError(fiber.StatusUnauthorized, "Invalid token") } @@ -222,17 +228,20 @@ func (a *App) WebsocketAuthMiddleware(c *fiber.Ctx) error { } func (a *App) TenantMiddleware(c *fiber.Ctx) error { - if tokenCID, ok := c.Locals("company_id").(domain.ValidInt64); ok && tokenCID.Valid { - return c.Next() - } - tenantSlug := c.Params("tenant_slug") if tenantSlug == "" { + a.mongoLoggerSvc.Info("blank tenant param", + zap.Time("timestamp", time.Now()), + ) return fiber.NewError(fiber.StatusBadRequest, "tenant is required for this route") } companyID, err := a.companySvc.GetCompanyIDBySlug(c.Context(), tenantSlug) if err != nil { + a.mongoLoggerSvc.Info("failed to resolve tenant", + zap.String("tenant_slug", tenantSlug), + zap.Time("timestamp", time.Now()), + ) return fiber.NewError(fiber.StatusBadRequest, "failed to resolve tenant") } @@ -242,3 +251,35 @@ func (a *App) TenantMiddleware(c *fiber.Ctx) error { }) return c.Next() } + +func (a *App) TenantAuthMiddleware(c *fiber.Ctx) error { + slugID, ok := c.Locals("tenant_id").(domain.ValidInt64) + + if !ok || !slugID.Valid { + a.mongoLoggerSvc.Info("invalid tenant slug", + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "invalid tenant slug") + } + + tokenCID, ok := c.Locals("company_id").(domain.ValidInt64) + if !ok || !tokenCID.Valid { + a.mongoLoggerSvc.Error("invalid company id in token", + zap.Time("timestamp", time.Now()), + zap.Bool("tokenCID Valid", tokenCID.Valid), + zap.Bool("ValidInt64 Type Check", ok), + ) + return fiber.NewError(fiber.StatusInternalServerError, "invalid company id in token") + } + + if slugID.Value != tokenCID.Value { + a.mongoLoggerSvc.Error("token company-id doesn't match the slug company_id", + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "invalid company_id") + } + + fmt.Printf("\nTenant successfully authenticated!\n") + + return c.Next() +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 36f1c45..561d3e7 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -35,6 +35,7 @@ func (a *App) initAppRoutes() { a.chapaSvc, a.walletSvc, a.referralSvc, + a.raffleSvc, a.bonusSvc, a.virtualGameSvc, a.aleaVirtualGameService, @@ -60,15 +61,41 @@ 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.0dev12", + "version": "1.0.dev15", }) }) + a.fiber.Get("/routes", func(c *fiber.Ctx) error { + return c.JSON(a.fiber.Stack()) // prints all registered routes + }) + // Groups groupV1 := a.fiber.Group("/api/v1") - tenant := groupV1.Group("/:tenant_slug", a.TenantMiddleware) - tenantAuth := groupV1.Group("/:tenant_slug", a.authMiddleware, a.TenantMiddleware) + tenant := groupV1.Group("/tenant/:tenant_slug", a.TenantMiddleware) + tenant.Get("/test", a.authMiddleware, a.authMiddleware, func(c *fiber.Ctx) error { + fmt.Printf("\nTest Route %v\n", c.Route().Path) + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + fmt.Printf("In the tenant auth test \n") + return c.JSON(fiber.Map{ + "message": "Is is fine", + }) + }) + tenant.Get("/", func(c *fiber.Ctx) error { + fmt.Printf("\nTenant Route %v\n", c.Route().Path) + companyID := c.Locals("company_id").(domain.ValidInt64) + if !companyID.Valid { + h.BadRequestLogger().Error("invalid company id") + return fiber.NewError(fiber.StatusBadRequest, "invalid company id") + } + return c.JSON(fiber.Map{ + "message": "Company Tenant Active", + }) + }) //Direct_deposit groupV1.Post("/direct_deposit", a.authMiddleware, h.InitiateDirectDeposit) groupV1.Post("/direct_deposit/verify", a.authMiddleware, h.VerifyDirectDeposit) @@ -143,47 +170,80 @@ func (a *App) initAppRoutes() { // groupV1.Get("/arifpay/session-id/verify-transaction/:session_id", a.authMiddleware, h.ArifpayVerifyBySessionIDHandler) // User Routes - tenant.Post("/user/resetPassword", h.ResetPassword) - tenant.Post("/user/sendResetCode", h.SendResetCode) + groupV1.Post("/user/resetPassword", h.ResetPassword) + groupV1.Post("/user/sendResetCode", h.SendResetCode) + + tenant.Post("/user/resetPassword", h.ResetTenantPassword) + tenant.Post("/user/sendResetCode", h.SendTenantResetCode) tenant.Post("/user/register", h.RegisterUser) tenant.Post("/user/sendRegisterCode", h.SendRegisterCode) tenant.Post("/user/checkPhoneEmailExist", h.CheckPhoneEmailExist) - tenantAuth.Get("/user/customer-profile", h.CustomerProfile) - tenantAuth.Get("/user/admin-profile", h.AdminProfile) - tenantAuth.Get("/user/bets", h.GetBetByUserID) + + tenant.Get("/user/customer-profile", a.authMiddleware, h.CustomerProfile) + tenant.Get("/user/admin-profile", a.authMiddleware, h.AdminProfile) + tenant.Get("/user/bets", a.authMiddleware, h.GetBetByUserID) groupV1.Get("/user/single/:id", a.authMiddleware, h.GetUserByID) groupV1.Post("/user/suspend", a.authMiddleware, h.UpdateUserSuspend) groupV1.Delete("/user/delete/:id", a.authMiddleware, h.DeleteUser) - tenantAuth.Get("/user/wallet", a.authMiddleware, h.GetCustomerWallet) - tenantAuth.Post("/user/search", a.authMiddleware, h.SearchUserByNameOrPhone) + tenant.Get("/user/wallet", a.authMiddleware, h.GetCustomerWallet) + tenant.Post("/user/search", a.authMiddleware, h.SearchUserByNameOrPhone) // Referral Routes - groupV1.Post("/referral/create", a.authMiddleware, h.CreateReferralCode) - groupV1.Get("/referral/stats", a.authMiddleware, h.GetReferralStats) - groupV1.Post("/referral/settings", a.authMiddleware, h.CreateReferralSettings) - groupV1.Get("/referral/settings", a.authMiddleware, h.GetReferralSettings) - groupV1.Patch("/referral/settings", a.authMiddleware, h.UpdateReferralSettings) + tenant.Post("/referral/create", a.authMiddleware, h.CreateReferralCode) + tenant.Get("/referral/code", a.authMiddleware, h.GetReferralCode) + tenant.Get("/referral/stats", a.authMiddleware, h.GetReferralStats) + + // groupV1.Post("/referral/settings", a.authMiddleware, h.CreateReferralSettings) + // groupV1.Get("/referral/settings", a.authMiddleware, h.GetReferralSettings) + // groupV1.Patch("/referral/settings", a.authMiddleware, h.UpdateReferralSettings) + + // Raffle Routes + tenant.Get("/raffle/list", h.GetTenantRaffles) + a.fiber.Get("/raffle/standing/:id/:limit", h.GetRaffleStanding) //This needs to be accessible by non-login user + a.fiber.Post("/raffle/create", a.authMiddleware, h.CreateRaffle) + a.fiber.Post("/raffle/add-filter", a.authMiddleware, h.AddRaffleFilter) + a.fiber.Get("/raffle/delete/:id", a.authMiddleware, h.DeleteRaffle) + a.fiber.Get("/raffle/company/:id", a.authMiddleware, h.GetRafflesOfCompany) + a.fiber.Get("raffle/winners/:id/:limit", a.authMiddleware, h.GetRaffleWinners) + + a.fiber.Post("/raffle-ticket/create", a.authMiddleware, h.CreateRaffleTicket) + a.fiber.Get("/raffle-ticket/:id", a.authMiddleware, h.GetUserRaffleTickets) + a.fiber.Get("/raffle-ticket/suspend/:id", a.authMiddleware, h.SuspendRaffleTicket) + a.fiber.Get("/raffle-ticket/unsuspend/:id", a.authMiddleware, h.UnSuspendRaffleTicket) // Bonus Routes - groupV1.Get("/bonus", a.authMiddleware, h.GetBonusMultiplier) - groupV1.Post("/bonus/create", a.authMiddleware, h.CreateBonusMultiplier) - groupV1.Put("/bonus/update", a.authMiddleware, h.UpdateBonusMultiplier) + tenant.Get("/bonus", a.authMiddleware, h.GetBonusesByUserID) + tenant.Get("/bonus/stats", a.authMiddleware, h.GetBonusStats) + tenant.Post("/bonus/claim/:id", a.authMiddleware, h.ClaimBonus) + // groupV1.Post("/bonus/create", a.authMiddleware, h.CreateBonusMultiplier) + // groupV1.Put("/bonus/update", a.authMiddleware, h.UpdateBonusMultiplier) groupV1.Get("/cashiers", a.authMiddleware, h.GetAllCashiers) groupV1.Get("/cashiers/:id", a.authMiddleware, h.GetCashierByID) groupV1.Post("/cashiers", a.authMiddleware, h.CreateCashier) groupV1.Put("/cashiers/:id", a.authMiddleware, h.UpdateCashier) + tenant.Get("/customer", a.authMiddleware, h.GetAllTenantCustomers) + tenant.Get("/customer/:id", a.authMiddleware, h.GetTenantCustomerByID) + tenant.Put("/customer/:id", a.authMiddleware, h.UpdateTenantCustomer) + tenant.Get("/customer/:id/bets", a.authMiddleware, h.GetTenantCustomerBets) + groupV1.Get("/customer", a.authMiddleware, a.SuperAdminOnly, h.GetAllCustomers) groupV1.Get("/customer/:id", a.authMiddleware, a.SuperAdminOnly, h.GetCustomerByID) groupV1.Put("/customer/:id", a.authMiddleware, a.SuperAdminOnly, h.UpdateCustomer) + groupV1.Get("/customer/:id/bets", a.authMiddleware, h.GetCustomerBets) groupV1.Get("/admin", a.authMiddleware, h.GetAllAdmins) groupV1.Get("/admin/:id", a.authMiddleware, h.GetAdminByID) groupV1.Post("/admin", a.authMiddleware, h.CreateAdmin) groupV1.Put("/admin/:id", a.authMiddleware, h.UpdateAdmin) + groupV1.Get("/t-approver", a.authMiddleware, h.GetAllTransactionApprovers) + groupV1.Get("/t-approver/:id", a.authMiddleware, h.GetTransactionApproverByID) + groupV1.Post("/t-approver", a.authMiddleware, h.CreateTransactionApprover) + groupV1.Put("/t-approver/:id", a.authMiddleware, h.UpdateTransactionApprover) + groupV1.Get("/managers", a.authMiddleware, h.GetAllManagers) groupV1.Get("/managers/:id", a.authMiddleware, h.GetManagerByID) groupV1.Post("/managers", a.authMiddleware, h.CreateManager) @@ -197,10 +257,12 @@ func (a *App) initAppRoutes() { tenant.Get("/odds", h.GetAllTenantOdds) tenant.Get("/odds/upcoming/:upcoming_id", h.GetTenantOddsByUpcomingID) tenant.Get("/odds/upcoming/:upcoming_id/market/:market_id", h.GetTenantOddsByMarketID) + tenant.Post("/odds/settings", a.CompanyOnly, h.SaveOddsSetting) - groupV1.Get("/events", a.authMiddleware, a.SuperAdminOnly, h.GetAllUpcomingEvents) - groupV1.Get("/events/:id", a.authMiddleware, a.SuperAdminOnly, h.GetUpcomingEventByID) + groupV1.Get("/events", a.authMiddleware, h.GetAllUpcomingEvents) + groupV1.Get("/events/:id", a.authMiddleware, h.GetUpcomingEventByID) groupV1.Delete("/events/:id", a.authMiddleware, a.SuperAdminOnly, h.SetEventStatusToRemoved) + groupV1.Patch("/events/:id/is_monitored", a.authMiddleware, a.SuperAdminOnly, h.SetEventIsMonitored) tenant.Get("/events", h.GetTenantUpcomingEvents) tenant.Get("/events/:id", h.GetTenantEventByID) @@ -222,6 +284,7 @@ func (a *App) initAppRoutes() { groupV1.Post("/branch", a.authMiddleware, h.CreateBranch) groupV1.Get("/branch", a.authMiddleware, h.GetAllBranches) groupV1.Get("/branch/:id", a.authMiddleware, h.GetBranchByID) + groupV1.Post("/branch/:id/return", a.authMiddleware, h.ReturnBranchWallet) groupV1.Get("/branch/:id/bets", a.authMiddleware, h.GetBetByBranchID) groupV1.Put("/branch/:id", a.authMiddleware, h.UpdateBranch) groupV1.Put("/branch/:id/set-active", a.authMiddleware, h.UpdateBranchStatus) @@ -259,15 +322,19 @@ func (a *App) initAppRoutes() { tenant.Get("/ticket/:id", h.GetTicketByID) // Bet Routes - tenantAuth.Post("/sport/bet", h.CreateBet) - tenantAuth.Post("/sport/bet/fastcode", h.CreateBetWithFastCode) + tenant.Post("/sport/bet", a.authMiddleware, h.CreateBet) + tenant.Post("/sport/bet/fastcode", a.authMiddleware, h.CreateBetWithFastCode) tenant.Get("/sport/bet/fastcode/:fast_code", h.GetBetByFastCode) - tenantAuth.Get("/sport/bet", h.GetAllBet) - tenantAuth.Get("/sport/bet/:id", h.GetBetByID) - tenantAuth.Patch("/sport/bet/:id", h.UpdateCashOut) - tenantAuth.Delete("/sport/bet/:id", h.DeleteBet) + tenant.Get("/sport/bet", a.authMiddleware, h.GetAllTenantBets) + tenant.Get("/sport/bet/:id", a.authMiddleware, h.GetTenantBetByID) + tenant.Patch("/sport/bet/:id", a.authMiddleware, h.UpdateCashOut) + tenant.Delete("/sport/bet/:id", a.authMiddleware, h.DeleteTenantBet) - tenantAuth.Post("/sport/random/bet", h.RandomBet) + groupV1.Get("/sport/bet", a.authMiddleware, a.SuperAdminOnly, h.GetAllBet) + groupV1.Get("/sport/bet/:id", a.authMiddleware, a.SuperAdminOnly, h.GetBetByID) + groupV1.Delete("/sport/bet/:id", a.authMiddleware, a.SuperAdminOnly, h.DeleteBet) + + tenant.Post("/sport/random/bet", a.authMiddleware, h.RandomBet) // Wallet groupV1.Get("/wallet", h.GetAllWallets) @@ -304,8 +371,8 @@ func (a *App) initAppRoutes() { groupV1.Post("/webhooks/alea-play", a.authMiddleware, h.HandleAleaCallback) //Veli Virtual Game Routes - groupV1.Post("/veli/providers", a.authMiddleware, h.GetProviders) - groupV1.Post("/veli/games-list", a.authMiddleware, h.GetGamesByProvider) + groupV1.Post("/veli/providers", h.GetProviders) + groupV1.Post("/veli/games-list", h.GetGamesByProvider) groupV1.Post("/veli/start-game", a.authMiddleware, h.StartGame) groupV1.Post("/veli/start-demo-game", h.StartDemoGame) a.fiber.Post("/balance", h.GetBalance) @@ -386,9 +453,9 @@ func (a *App) initAppRoutes() { groupV1.Get("/settings/:key", a.authMiddleware, a.SuperAdminOnly, h.GetGlobalSettingByKey) groupV1.Put("/settings", a.authMiddleware, a.SuperAdminOnly, h.UpdateGlobalSettingList) - tenantAuth.Post("/settings", h.SaveCompanySettingList) - tenantAuth.Get("/settings", h.GetCompanySettingList) - tenantAuth.Delete("/settings/:key", h.DeleteCompanySetting) - tenantAuth.Delete("/settings", h.DeleteAllCompanySetting) + tenant.Get("/settings", a.authMiddleware, h.GetCompanySettingList) + tenant.Put("/settings", a.authMiddleware, h.SaveCompanySettingList) + tenant.Delete("/settings/:key", a.authMiddleware, h.DeleteCompanySetting) + tenant.Delete("/settings", a.authMiddleware, h.DeleteAllCompanySetting) } diff --git a/makefile b/makefile index d8ef6a1..e5e4fae 100644 --- a/makefile +++ b/makefile @@ -56,14 +56,24 @@ restore: restore_file: @echo "Restoring latest backup..." gunzip -c $(file) | docker exec -i fortunebet-backend-postgres-1 psql -U root -d gh + +.PHONY: seed_data seed_data: - cat db/data/seed_data.sql | docker exec -i fortunebet-backend-postgres-1 psql -U root -d gh + + @echo "Waiting for PostgreSQL to be ready..." + @until docker exec fortunebet-backend-postgres-1 pg_isready -U root -d gh; do \ + echo "PostgreSQL is not ready yet..."; \ + sleep 1; \ + done + @for file in db/data/*.sql; do \ + echo "Seeding $$file..."; \ + cat $$file | docker exec -i fortunebet-backend-postgres-1 psql -U root -d gh; \ + done postgres_log: docker logs fortunebet-backend-postgres-1 .PHONY: swagger swagger: @swag init -g cmd/main.go - .PHONY: db-up logs: @mkdir -p logs @@ -73,8 +83,8 @@ db-up: | logs @docker logs fortunebet-backend-postgres-1 > logs/postgres.log 2>&1 & .PHONY: db-down db-down: - @docker compose down - @docker volume rm fortunebet-backend_postgres_data + @docker compose down -v +# @docker volume rm fortunebet-backend_postgres_data .PHONY: sqlc-gen sqlc-gen: @sqlc generate