From 910d592bef336deeff9aa9d7501d7de22fe738b9 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Sun, 31 Aug 2025 13:30:26 +0300 Subject: [PATCH 01/39] fixes while integrating --- db/data/001_initial_seed_data.sql | 343 +++++++++++ db/data/002_veli_user.sql | 148 +++++ db/data/seed_data.sql | 222 -------- db/migrations/000001_fortune.up.sql | 160 +++--- db/migrations/000003_referal.up.sql | 14 +- db/migrations/000007_setting_data.up.sql | 22 +- db/migrations/000009_location_data.up.sql | 150 ++--- db/migrations/000010_seed_data.up.sql | 438 +++++++------- db/query/referal.sql | 116 ++-- db/query/user.sql | 2 +- gen/db/models.go | 5 +- gen/db/referal.sql.go | 132 +++-- gen/db/user.sql.go | 4 +- internal/domain/auth.go | 7 + internal/domain/referal.go | 5 +- internal/domain/role.go | 13 +- internal/domain/user.go | 4 +- internal/domain/wallet.go | 45 +- internal/repository/referal.go | 32 +- internal/repository/user.go | 6 +- internal/services/referal/port.go | 12 +- internal/services/referal/service.go | 58 +- internal/services/user/port.go | 2 +- internal/services/user/reset.go | 2 +- internal/web_server/cron.go | 10 +- internal/web_server/handlers/admin.go | 6 +- internal/web_server/handlers/auth_handler.go | 8 +- internal/web_server/handlers/bet_handler.go | 197 ++++++- .../web_server/handlers/branch_handler.go | 103 ++++ internal/web_server/handlers/customer.go | 536 +++++++++++++++++- .../web_server/handlers/referal_handlers.go | 102 +++- .../handlers/transaction_approver.go | 438 ++++++++++++++ internal/web_server/handlers/user.go | 159 +++++- internal/web_server/jwt/jwt.go | 37 +- internal/web_server/middleware.go | 51 +- internal/web_server/routes.go | 98 +++- makefile | 18 +- 37 files changed, 2795 insertions(+), 910 deletions(-) create mode 100644 db/data/001_initial_seed_data.sql create mode 100644 db/data/002_veli_user.sql delete mode 100644 db/data/seed_data.sql create mode 100644 internal/web_server/handlers/transaction_approver.go diff --git a/db/data/001_initial_seed_data.sql b/db/data/001_initial_seed_data.sql new file mode 100644 index 0000000..9081dee --- /dev/null +++ b/db/data/001_initial_seed_data.sql @@ -0,0 +1,343 @@ +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') 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, + 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 + ) 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; \ 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..5323bac --- /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, + 10000, + 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/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 d05dcbe..53f61ba 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -18,17 +18,21 @@ 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 ); @@ -40,7 +44,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 @@ -96,7 +107,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 ( @@ -220,9 +231,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, @@ -232,7 +243,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, @@ -246,7 +257,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 @@ -263,12 +274,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, @@ -289,21 +297,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, is_monitored BOOLEAN NOT NULL DEFAULT FALSE, - UNIQUE(id, source) + UNIQUE (id, source) ); CREATE TABLE event_history ( id BIGSERIAL PRIMARY KEY, @@ -319,7 +321,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, @@ -330,13 +332,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, @@ -358,7 +360,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, @@ -408,7 +410,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, @@ -425,7 +427,7 @@ 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, @@ -439,10 +441,10 @@ CREATE TABLE bonus ( ); 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 ( @@ -458,20 +460,20 @@ 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); +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 VIEW companies_details AS SELECT companies.*, @@ -486,7 +488,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 @@ -500,9 +502,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 @@ -512,7 +514,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; @@ -566,7 +568,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 @@ -643,47 +645,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; \ No newline at end of file diff --git a/db/migrations/000003_referal.up.sql b/db/migrations/000003_referal.up.sql index 521e3e3..dc51f2a 100644 --- a/db/migrations/000003_referal.up.sql +++ b/db/migrations/000003_referal.up.sql @@ -18,17 +18,19 @@ CREATE TABLE IF NOT EXISTS referral_settings ( ); CREATE TABLE IF NOT EXISTS referrals ( id BIGSERIAL PRIMARY KEY, + company_id BIGINT NOT NULL REFERENCES companies (id) ON +DELETE CASCADE, referral_code VARCHAR(10) NOT NULL UNIQUE, - referrer_id VARCHAR(255) NOT NULL, - referred_id VARCHAR(255) UNIQUE, + referrer_id BIGINT NOT NULL REFERENCES users (id), + referred_id BIGINT UNIQUE REFERENCES users (id), 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, 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), + 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) ); @@ -38,7 +40,7 @@ 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 +-- 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, 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/referal.sql b/db/query/referal.sql index 206606e..6c60722 100644 --- a/db/query/referal.sql +++ b/db/query/referal.sql @@ -1,77 +1,87 @@ -- name: CreateReferral :one INSERT INTO referrals ( - referral_code, - referrer_id, - status, - reward_amount, - expires_at -) VALUES ( - $1, $2, $3, $4, $5 -) RETURNING *; - + referral_code, + referrer_id, + company_id, + status, + reward_amount, + expires_at + ) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING *; -- name: GetReferralByCode :one -SELECT * FROM referrals +SELECT * +FROM referrals WHERE referral_code = $1; - -- name: UpdateReferral :one UPDATE referrals -SET - referred_id = $2, - status = $3, - updated_at = CURRENT_TIMESTAMP +SET referred_id = $2, + status = $3, + updated_at = CURRENT_TIMESTAMP WHERE id = $1 RETURNING *; - -- name: UpdateReferralCode :exec UPDATE users -SET - referral_code = $2, - updated_at = CURRENT_TIMESTAMP +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 +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; - +WHERE referrer_id = $1 + AND company_id = $2; -- name: GetReferralSettings :one -SELECT * FROM referral_settings +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 +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 *; - + 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; - +SELECT * +FROM referrals +WHERE referred_id = $1 +LIMIT 1; -- name: GetActiveReferralByReferrerID :one -SELECT * FROM referrals WHERE referrer_id = $1 AND status = 'PENDING' LIMIT 1; - +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 +SELECT COUNT(*) +FROM referrals +WHERE referrer_id = $1; \ No newline at end of file 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/gen/db/models.go b/gen/db/models.go index d91961f..a323eb8 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -540,9 +540,10 @@ type Otp struct { type Referral struct { ID int64 `json:"id"` + CompanyID int64 `json:"company_id"` ReferralCode string `json:"referral_code"` - ReferrerID string `json:"referrer_id"` - ReferredID pgtype.Text `json:"referred_id"` + ReferrerID int64 `json:"referrer_id"` + ReferredID pgtype.Int8 `json:"referred_id"` Status Referralstatus `json:"status"` RewardAmount pgtype.Numeric `json:"reward_amount"` CashbackAmount pgtype.Numeric `json:"cashback_amount"` diff --git a/gen/db/referal.sql.go b/gen/db/referal.sql.go index b5ceeed..6db003a 100644 --- a/gen/db/referal.sql.go +++ b/gen/db/referal.sql.go @@ -13,19 +13,21 @@ import ( 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 + referral_code, + referrer_id, + company_id, + status, + reward_amount, + expires_at + ) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING id, company_id, referral_code, referrer_id, referred_id, status, reward_amount, cashback_amount, created_at, updated_at, expires_at ` type CreateReferralParams struct { ReferralCode string `json:"referral_code"` - ReferrerID string `json:"referrer_id"` + ReferrerID int64 `json:"referrer_id"` + CompanyID int64 `json:"company_id"` Status Referralstatus `json:"status"` RewardAmount pgtype.Numeric `json:"reward_amount"` ExpiresAt pgtype.Timestamptz `json:"expires_at"` @@ -35,6 +37,7 @@ func (q *Queries) CreateReferral(ctx context.Context, arg CreateReferralParams) row := q.db.QueryRow(ctx, CreateReferral, arg.ReferralCode, arg.ReferrerID, + arg.CompanyID, arg.Status, arg.RewardAmount, arg.ExpiresAt, @@ -42,6 +45,7 @@ func (q *Queries) CreateReferral(ctx context.Context, arg CreateReferralParams) var i Referral err := row.Scan( &i.ID, + &i.CompanyID, &i.ReferralCode, &i.ReferrerID, &i.ReferredID, @@ -57,15 +61,15 @@ func (q *Queries) CreateReferral(ctx context.Context, arg CreateReferralParams) 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 + 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 ` type CreateReferralSettingsParams struct { @@ -103,14 +107,19 @@ func (q *Queries) CreateReferralSettings(ctx context.Context, arg CreateReferral } 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 +SELECT id, company_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) { +func (q *Queries) GetActiveReferralByReferrerID(ctx context.Context, referrerID int64) (Referral, error) { row := q.db.QueryRow(ctx, GetActiveReferralByReferrerID, referrerID) var i Referral err := row.Scan( &i.ID, + &i.CompanyID, &i.ReferralCode, &i.ReferrerID, &i.ReferredID, @@ -125,7 +134,8 @@ func (q *Queries) GetActiveReferralByReferrerID(ctx context.Context, referrerID } 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 +SELECT id, company_id, referral_code, referrer_id, referred_id, status, reward_amount, cashback_amount, created_at, updated_at, expires_at +FROM referrals WHERE referral_code = $1 ` @@ -134,6 +144,7 @@ func (q *Queries) GetReferralByCode(ctx context.Context, referralCode string) (R var i Referral err := row.Scan( &i.ID, + &i.CompanyID, &i.ReferralCode, &i.ReferrerID, &i.ReferredID, @@ -148,14 +159,18 @@ func (q *Queries) GetReferralByCode(ctx context.Context, referralCode string) (R } 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 +SELECT id, company_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 ` -func (q *Queries) GetReferralByReferredID(ctx context.Context, referredID pgtype.Text) (Referral, error) { +func (q *Queries) GetReferralByReferredID(ctx context.Context, referredID pgtype.Int8) (Referral, error) { row := q.db.QueryRow(ctx, GetReferralByReferredID, referredID) var i Referral err := row.Scan( &i.ID, + &i.CompanyID, &i.ReferralCode, &i.ReferrerID, &i.ReferredID, @@ -170,10 +185,12 @@ func (q *Queries) GetReferralByReferredID(ctx context.Context, referredID pgtype } const GetReferralCountByID = `-- name: GetReferralCountByID :one -SELECT count(*) FROM referrals WHERE referrer_id = $1 +SELECT COUNT(*) +FROM referrals +WHERE referrer_id = $1 ` -func (q *Queries) GetReferralCountByID(ctx context.Context, referrerID string) (int64, error) { +func (q *Queries) GetReferralCountByID(ctx context.Context, referrerID int64) (int64, error) { row := q.db.QueryRow(ctx, GetReferralCountByID, referrerID) var count int64 err := row.Scan(&count) @@ -181,7 +198,8 @@ func (q *Queries) GetReferralCountByID(ctx context.Context, referrerID string) ( } 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 +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 ` @@ -204,15 +222,31 @@ func (q *Queries) GetReferralSettings(ctx context.Context) (ReferralSetting, 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 +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 + 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"` CompletedReferrals int64 `json:"completed_referrals"` @@ -220,8 +254,8 @@ type GetReferralStatsRow struct { PendingRewards interface{} `json:"pending_rewards"` } -func (q *Queries) GetReferralStats(ctx context.Context, referrerID string) (GetReferralStatsRow, error) { - row := q.db.QueryRow(ctx, GetReferralStats, referrerID) +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, @@ -234,17 +268,16 @@ func (q *Queries) GetReferralStats(ctx context.Context, referrerID string) (GetR const UpdateReferral = `-- name: UpdateReferral :one UPDATE referrals -SET - referred_id = $2, - status = $3, - updated_at = CURRENT_TIMESTAMP +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 +RETURNING id, company_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"` + ReferredID pgtype.Int8 `json:"referred_id"` Status Referralstatus `json:"status"` } @@ -253,6 +286,7 @@ func (q *Queries) UpdateReferral(ctx context.Context, arg UpdateReferralParams) var i Referral err := row.Scan( &i.ID, + &i.CompanyID, &i.ReferralCode, &i.ReferrerID, &i.ReferredID, @@ -268,9 +302,8 @@ func (q *Queries) UpdateReferral(ctx context.Context, arg UpdateReferralParams) const UpdateReferralCode = `-- name: UpdateReferralCode :exec UPDATE users -SET - referral_code = $2, - updated_at = CURRENT_TIMESTAMP +SET referral_code = $2, + updated_at = CURRENT_TIMESTAMP WHERE id = $1 ` @@ -286,14 +319,13 @@ func (q *Queries) UpdateReferralCode(ctx context.Context, arg UpdateReferralCode 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 +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 ` diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index 0d4c33b..43d9156 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -587,7 +587,7 @@ SET password = $1, WHERE ( email = $2 OR phone_number = $3 - AND company_id = $4 + AND company_id = $5 ) ` @@ -596,6 +596,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 +605,7 @@ func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) arg.Email, arg.PhoneNumber, arg.UpdatedAt, + arg.CompanyID, ) return 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/referal.go b/internal/domain/referal.go index 1e528a4..b8f61bf 100644 --- a/internal/domain/referal.go +++ b/internal/domain/referal.go @@ -62,8 +62,9 @@ type ReferralSettingsReq struct { type Referral struct { ID int64 ReferralCode string - ReferrerID string - ReferredID *string + ReferrerID int64 + CompanyID int64 + ReferredID *int64 Status ReferralStatus RewardAmount float64 CashbackAmount float64 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/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/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/repository/referal.go b/internal/repository/referal.go index d214c54..8b44c8a 100644 --- a/internal/repository/referal.go +++ b/internal/repository/referal.go @@ -16,13 +16,13 @@ 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) + GetReferralStats(ctx context.Context, userID int64, companyID int64) (*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) + GetReferralByReferredID(ctx context.Context, referredID int64) (*domain.Referral, error) + GetReferralCountByID(ctx context.Context, referrerID int64) (int64, error) + GetActiveReferralByReferrerID(ctx context.Context, referrerID int64) (*domain.Referral, error) UpdateUserReferalCode(ctx context.Context, codedata domain.UpdateUserReferalCode) error } @@ -58,6 +58,7 @@ func (r *ReferralRepo) CreateReferral(ctx context.Context, referral *domain.Refe Status: dbgen.Referralstatus(referral.Status), RewardAmount: rewardAmount, ExpiresAt: pgtype.Timestamptz{Time: referral.ExpiresAt, Valid: true}, + CompanyID: referral.CompanyID, } _, err := r.store.queries.CreateReferral(ctx, params) @@ -76,9 +77,9 @@ func (r *ReferralRepo) GetReferralByCode(ctx context.Context, code string) (*dom } func (r *ReferralRepo) UpdateReferral(ctx context.Context, referral *domain.Referral) error { - var referredID pgtype.Text + var referredID pgtype.Int8 if referral.ReferredID != nil { - referredID = pgtype.Text{String: *referral.ReferredID, Valid: true} + referredID = pgtype.Int8{Int64: *referral.ReferredID, Valid: true} } params := dbgen.UpdateReferralParams{ @@ -91,8 +92,11 @@ func (r *ReferralRepo) UpdateReferral(ctx context.Context, referral *domain.Refe return err } -func (r *ReferralRepo) GetReferralStats(ctx context.Context, userID string) (*domain.ReferralStats, error) { - stats, err := r.store.queries.GetReferralStats(ctx, userID) +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 nil, err } @@ -175,8 +179,8 @@ func (r *ReferralRepo) CreateSettings(ctx context.Context, settings *domain.Refe 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) GetReferralByReferredID(ctx context.Context, referredID int64) (*domain.Referral, error) { + dbReferral, err := r.store.queries.GetReferralByReferredID(ctx, pgtype.Int8{Int64: referredID, Valid: true}) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, nil @@ -186,7 +190,7 @@ func (r *ReferralRepo) GetReferralByReferredID(ctx context.Context, referredID s return r.mapToDomainReferral(&dbReferral), nil } -func (r *ReferralRepo) GetReferralCountByID(ctx context.Context, referrerID string) (int64, error) { +func (r *ReferralRepo) GetReferralCountByID(ctx context.Context, referrerID int64) (int64, error) { count, err := r.store.queries.GetReferralCountByID(ctx, referrerID) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -198,7 +202,7 @@ func (r *ReferralRepo) GetReferralCountByID(ctx context.Context, referrerID stri return count, nil } -func (r *ReferralRepo) GetActiveReferralByReferrerID(ctx context.Context, referrerID string) (*domain.Referral, error) { +func (r *ReferralRepo) GetActiveReferralByReferrerID(ctx context.Context, referrerID int64) (*domain.Referral, error) { referral, err := r.store.queries.GetActiveReferralByReferrerID(ctx, referrerID) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -211,9 +215,9 @@ func (r *ReferralRepo) GetActiveReferralByReferrerID(ctx context.Context, referr } func (r *ReferralRepo) mapToDomainReferral(dbRef *dbgen.Referral) *domain.Referral { - var referredID *string + var referredID *int64 if dbRef.ReferredID.Valid { - referredID = &dbRef.ReferredID.String + referredID = &dbRef.ReferredID.Int64 } rewardAmount := 0.0 diff --git a/internal/repository/user.go b/internal/repository/user.go index 703e745..83eff1f 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -428,7 +428,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 +449,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/referal/port.go b/internal/services/referal/port.go index 6add199..1b2278a 100644 --- a/internal/services/referal/port.go +++ b/internal/services/referal/port.go @@ -8,13 +8,13 @@ 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) + 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) 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 + GetReferralCountByID(ctx context.Context, referrerID int64) (int64, error) } diff --git a/internal/services/referal/service.go b/internal/services/referal/service.go index d89b023..159d494 100644 --- a/internal/services/referal/service.go +++ b/internal/services/referal/service.go @@ -52,11 +52,11 @@ func (s *Service) GenerateReferralCode() (string, error) { return code, nil } -func (s *Service) CreateReferral(ctx context.Context, userID int64) error { +func (s *Service) CreateReferral(ctx context.Context, userID int64, companyID int64) error { s.logger.Info("Creating referral code for user", "userID", userID) // check if user already has an active referral code - referral, err := s.repo.GetActiveReferralByReferrerID(ctx, fmt.Sprintf("%d", userID)) + referral, err := s.repo.GetActiveReferralByReferrerID(ctx, userID) if err != nil { s.logger.Error("Failed to check if user alredy has active referral code", "error", err) return err @@ -73,7 +73,7 @@ func (s *Service) CreateReferral(ctx context.Context, userID int64) error { } // check referral count limit - referralCount, err := s.GetReferralCountByID(ctx, fmt.Sprintf("%d", userID)) + referralCount, err := s.GetReferralCountByID(ctx, userID) if err != nil { s.logger.Error("Failed to get referral count", "userID", userID, "error", err) return err @@ -96,10 +96,11 @@ func (s *Service) CreateReferral(ctx context.Context, userID int64) error { if err := s.repo.CreateReferral(ctx, &domain.Referral{ ReferralCode: code, - ReferrerID: fmt.Sprintf("%d", userID), + ReferrerID: userID, Status: domain.ReferralPending, RewardAmount: rewardAmount, ExpiresAt: expireDuration, + CompanyID: companyID, }); err != nil { return err } @@ -138,7 +139,7 @@ func (s *Service) ProcessReferral(ctx context.Context, referredPhone, referralCo return ErrInvalidReferralSignup } - referral.ReferredID = &referredPhone + referral.ReferredID = &user.ID referral.Status = domain.ReferralCompleted referral.UpdatedAt = time.Now() @@ -147,13 +148,7 @@ func (s *Service) ProcessReferral(ctx context.Context, referredPhone, referralCo return err } - referrerId, err := strconv.Atoi(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)) + wallets, err := s.store.GetCustomerWallet(ctx, referral.ReferrerID) if err != nil { s.logger.Error("Failed to get referrer wallets", "referrerId", referral.ReferrerID, "error", err) return err @@ -161,7 +156,7 @@ func (s *Service) ProcessReferral(ctx context.Context, referredPhone, referralCo _, 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), + fmt.Sprintf("Added %v to static wallet because of referral ID %v", referral.RewardAmount, referral.ReferrerID), ) if err != nil { s.logger.Error("Failed to add referral reward to static wallet", "walletID", wallets.StaticID, "referrer phone number", referredPhone, "error", err) @@ -212,8 +207,8 @@ func (s *Service) ProcessDepositBonus(ctx context.Context, userPhone string, amo return nil } -func (s *Service) ProcessBetReferral(ctx context.Context, userPhone string, betAmount float64) error { - s.logger.Info("Processing bet referral", "userPhone", userPhone, "betAmount", betAmount) +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 { @@ -221,29 +216,24 @@ func (s *Service) ProcessBetReferral(ctx context.Context, userPhone string, betA return err } - referral, err := s.repo.GetReferralByReferredID(ctx, userPhone) + referral, err := s.repo.GetReferralByReferredID(ctx, userId) if err != nil { - s.logger.Error("Failed to get referral by referred ID", "userPhone", userPhone, "error", err) + 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", "userPhone", userPhone, "status", referral.Status) + s.logger.Warn("No valid referral found", "userId", userId, "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) + wallets, err := s.walletSvc.GetWalletsByUser(ctx, referral.ReferrerID) if err != nil { - s.logger.Error("Failed to get wallets for referrer", "referrerID", referrerID, "error", err) + 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", referrerID) + s.logger.Error("Referrer has no wallet", "referrerID", referral.ReferrerID) return errors.New("referrer has no wallet") } @@ -260,24 +250,24 @@ func (s *Service) ProcessBetReferral(ctx context.Context, userPhone string, betA 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) + 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", "userPhone", userPhone, "referrerID", referrerID, "bonus", bonus) + s.logger.Info("Bet referral processed successfully", "referrer ID", referral.ReferrerID, "referrerID", referral.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) +func (s *Service) GetReferralStats(ctx context.Context, userID int64, companyID int64) (*domain.ReferralStats, error) { + s.logger.Info("Fetching referral stats", "userID", userID) - stats, err := s.repo.GetReferralStats(ctx, userPhone) + stats, err := s.repo.GetReferralStats(ctx, userID, companyID) if err != nil { - s.logger.Error("Failed to get referral stats", "userPhone", userPhone, "error", err) + s.logger.Error("Failed to get referral stats", "userID", userID, "error", err) return nil, err } - s.logger.Info("Referral stats retrieved successfully", "userPhone", userPhone, "totalReferrals", stats.TotalReferrals) + s.logger.Info("Referral stats retrieved successfully", "userID", userID, "totalReferrals", stats.TotalReferrals) return stats, nil } @@ -328,7 +318,7 @@ func (s *Service) GetReferralSettings(ctx context.Context) (*domain.ReferralSett return settings, nil } -func (s *Service) GetReferralCountByID(ctx context.Context, referrerID string) (int64, error) { +func (s *Service) GetReferralCountByID(ctx context.Context, referrerID int64) (int64, error) { count, err := s.repo.GetReferralCountByID(ctx, referrerID) if err != nil { s.logger.Error("Failed to get referral count", "userID", referrerID, "error", 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/web_server/cron.go b/internal/web_server/cron.go index c18bbe5..4900794 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -156,10 +156,10 @@ func SetupReportandVirtualGameCronJobs( spec string period string }{ - { - spec: "*/60 * * * * *", // Every 1 minute for testing - period: "test", - }, + // { + // spec: "*/60 * * * * *", // Every 1 minute for testing + // period: "test", + // }, { spec: "0 0 0 * * *", // Daily at midnight period: "daily", @@ -254,8 +254,6 @@ func SetupReportandVirtualGameCronJobs( log.Printf("Cron jobs started. Reports will be saved to: %s", outputDir) } - - func ProcessBetCashback(ctx context.Context, betService *betSvc.Service) { c := cron.New(cron.WithSeconds()) 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..0ee2f01 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), diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index d9cea40..5c5ec04 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -379,9 +379,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 +586,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 +843,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/branch_handler.go b/internal/web_server/handlers/branch_handler.go index f65a7f8..7314ccb 100644 --- a/internal/web_server/handlers/branch_handler.go +++ b/internal/web_server/handlers/branch_handler.go @@ -254,6 +254,7 @@ func (h *Handler) CreateBranchOperation(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Branch Operation Created", nil, nil) } + // GetBranchByID godoc // @Summary Gets branch by id // @Description Gets a single branch by id @@ -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/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/referal_handlers.go b/internal/web_server/handlers/referal_handlers.go index ef8cb06..3105a4a 100644 --- a/internal/web_server/handlers/referal_handlers.go +++ b/internal/web_server/handlers/referal_handlers.go @@ -11,6 +11,11 @@ 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", @@ -21,7 +26,7 @@ func (h *Handler) CreateReferralCode(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification") } - if err := h.referralSvc.CreateReferral(c.Context(), userID); err != nil { + if err := h.referralSvc.CreateReferral(c.Context(), userID, companyID.Value); err != nil { h.mongoLoggerSvc.Error("Failed to create referral", zap.Int64("userID", userID), zap.Int("status_code", fiber.StatusInternalServerError), @@ -35,6 +40,7 @@ func (h *Handler) CreateReferralCode(c *fiber.Ctx) error { } 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", @@ -91,6 +97,57 @@ func (h *Handler) CreateReferralSettings(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Referral created successfully", nil, nil) } +// 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") +// } +// userID, ok := c.Locals("user_id").(int64) +// if !ok || userID == 0 { +// h.mongoLoggerSvc.Error("Invalid user ID in context", +// 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, "Failed to retrieve user") +// } + +// 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.StatusBadRequest, "Failed to retrieve user") +// } + +// // referrals, err := h.referralSvc.GetReferralStats(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") +// } + +// } + // GetReferralStats godoc // @Summary Get referral statistics // @Description Retrieves referral statistics for the authenticated user @@ -101,8 +158,13 @@ func (h *Handler) CreateReferralSettings(c *fiber.Ctx) error { // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Security Bearer -// @Router /api/v1/referral/stats [get] +// @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.mongoLoggerSvc.Error("Invalid user ID in context", @@ -124,7 +186,17 @@ 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 !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.StatusBadRequest, "Failed to retrieve user") + } + + 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), @@ -227,33 +299,9 @@ func (h *Handler) GetReferralSettings(c *fiber.Ctx) error { // 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", - 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") - } - - 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", - zap.Int64("userID", userID), zap.Int("status_code", fiber.StatusInternalServerError), 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..81f8223 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(), req.PhoneNumber, 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/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 c6c1f7f..0b55e6b 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -62,16 +62,41 @@ func (a *App) initAppRoutes() { }) }) + 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) groupV1.Get("/direct_deposit/pending", a.authMiddleware, h.GetPendingDirectDeposits) - // Swagger a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler()) @@ -141,28 +166,29 @@ func (a *App) initAppRoutes() { // groupV1.Post("/arifpay/transaction-id/verify-transaction", a.authMiddleware, h.ArifpayVerifyByTransactionIDHandler) // 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) + tenant.Post("/referral/create", a.authMiddleware, h.CreateReferralCode) + 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) @@ -177,15 +203,26 @@ func (a *App) initAppRoutes() { 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) + tenant.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) @@ -200,8 +237,8 @@ func (a *App) initAppRoutes() { tenant.Get("/odds/upcoming/:upcoming_id", h.GetTenantOddsByUpcomingID) tenant.Get("/odds/upcoming/:upcoming_id/market/:market_id", h.GetTenantOddsByMarketID) - 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) tenant.Get("/events", h.GetTenantUpcomingEvents) @@ -221,6 +258,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) @@ -258,15 +296,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) @@ -373,9 +415,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.Post("/settings", a.authMiddleware, h.SaveCompanySettingList) + tenant.Get("/settings", a.authMiddleware, h.GetCompanySettingList) + tenant.Delete("/settings/:key", a.authMiddleware, h.DeleteCompanySetting) + tenant.Delete("/settings", a.authMiddleware, h.DeleteAllCompanySetting) } diff --git a/makefile b/makefile index d8ef6a1..638a748 100644 --- a/makefile +++ b/makefile @@ -56,8 +56,19 @@ 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 @@ -69,11 +80,12 @@ logs: @mkdir -p logs db-up: | logs @mkdir -p logs - @docker compose up -d postgres migrate mongo redis + @docker compose up -d postgres migrate mongo redis --wait migrate + @make seed_data @docker logs fortunebet-backend-postgres-1 > logs/postgres.log 2>&1 & .PHONY: db-down db-down: - @docker compose down + @docker compose down -v @docker volume rm fortunebet-backend_postgres_data .PHONY: sqlc-gen sqlc-gen: From 89e3d7de781dd03240dcc520c340a4a435c73752 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Mon, 1 Sep 2025 23:47:27 +0300 Subject: [PATCH 02/39] raffle service structure --- db/migrations/000001_fortune.up.sql | 18 ++- db/query/raffle.sql | 36 ++++++ gen/db/models.go | 17 +++ gen/db/raffle.sql.go | 190 ++++++++++++++++++++++++++++ internal/domain/raffle.go | 36 ++++++ internal/repository/reffel.go | 123 ++++++++++++++++++ internal/services/raffle/port.go | 18 +++ internal/services/raffle/service.go | 45 +++++++ internal/web_server/routes.go | 4 - 9 files changed, 482 insertions(+), 5 deletions(-) create mode 100644 db/query/raffle.sql create mode 100644 gen/db/raffle.sql.go create mode 100644 internal/domain/raffle.go create mode 100644 internal/repository/reffel.go create mode 100644 internal/services/raffle/port.go create mode 100644 internal/services/raffle/service.go diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index d05dcbe..f2f852d 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -498,6 +498,22 @@ CREATE TABLE IF NOT EXISTS supported_operations ( name VARCHAR(255) NOT NULL, description VARCHAR(255) NOT NULL ); +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, + UNIQUE (raffle_id, user_id) +); CREATE VIEW bet_with_outcomes AS SELECT bets.*, CONCAT(users.first_name, ' ', users.last_name) AS full_name, @@ -686,4 +702,4 @@ ADD CONSTRAINT fk_event_settings_company FOREIGN KEY (company_id) REFERENCES com 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_odds_market FOREIGN KEY (odds_market_id) REFERENCES odds_market(id) ON DELETE CASCADE; diff --git a/db/query/raffle.sql b/db/query/raffle.sql new file mode 100644 index 0000000..4e900fd --- /dev/null +++ b/db/query/raffle.sql @@ -0,0 +1,36 @@ +-- 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: UpdateRaffle :exec +UPDATE raffles +SET name = $1, + expires_at = $2, + status = $3 +WHERE id = $4; + +-- 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; diff --git a/gen/db/models.go b/gen/db/models.go index d91961f..0d18e73 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -538,6 +538,23 @@ type Otp struct { 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 RaffleTicket struct { + ID int32 `json:"id"` + RaffleID int32 `json:"raffle_id"` + UserID int32 `json:"user_id"` + IsActive pgtype.Bool `json:"is_active"` +} + type Referral struct { ID int64 `json:"id"` ReferralCode string `json:"referral_code"` diff --git a/gen/db/raffle.sql.go b/gen/db/raffle.sql.go new file mode 100644 index 0000000..64fdd88 --- /dev/null +++ b/gen/db/raffle.sql.go @@ -0,0 +1,190 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: raffle.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +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 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 UpdateRaffle = `-- name: UpdateRaffle :exec +UPDATE raffles +SET name = $1, + expires_at = $2, + status = $3 +WHERE id = $4 +` + +type UpdateRaffleParams struct { + Name string `json:"name"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + Status string `json:"status"` + ID int32 `json:"id"` +} + +func (q *Queries) UpdateRaffle(ctx context.Context, arg UpdateRaffleParams) error { + _, err := q.db.Exec(ctx, UpdateRaffle, + arg.Name, + arg.ExpiresAt, + arg.Status, + arg.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/internal/domain/raffle.go b/internal/domain/raffle.go new file mode 100644 index 0000000..b4b1e70 --- /dev/null +++ b/internal/domain/raffle.go @@ -0,0 +1,36 @@ +package domain + +import "time" + +type Raffle struct { + ID int32 + CompanyID int32 + Name string + CreatedAt time.Time + ExpiresAt time.Time + Type string + Status string +} + +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 + Name string + ExpiresAt time.Time + Type string +} diff --git a/internal/repository/reffel.go b/internal/repository/reffel.go new file mode 100644 index 0000000..a28096a --- /dev/null +++ b/internal/repository/reffel.go @@ -0,0 +1,123 @@ +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 (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) 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) UpdateRaffle(ctx context.Context, raffleParams dbgen.UpdateRaffleParams) error { + return s.queries.UpdateRaffle(ctx, raffleParams) +} + +func (s *Store) SuspendRaffleTicket(ctx context.Context, raffleID int32) error { + return s.queries.UpdateRaffleTicketStatus(ctx, dbgen.UpdateRaffleTicketStatusParams{ + ID: raffleID, + 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) CreateRaffleTicket(ctx context.Context, raffleID, userID int32) (domain.RaffleTicket, error) { + raffleTicket, err := s.queries.CreateRaffleTicket(ctx, dbgen.CreateRaffleTicketParams{ + RaffleID: raffleID, + UserID: 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 +} diff --git a/internal/services/raffle/port.go b/internal/services/raffle/port.go new file mode 100644 index 0000000..a457f66 --- /dev/null +++ b/internal/services/raffle/port.go @@ -0,0 +1,18 @@ +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) + GetRafflesOfCompany(ctx context.Context, companyID int32) ([]dbgen.Raffle, error) + UpdateRaffle(ctx context.Context, raffleParams dbgen.UpdateRaffleParams) error + SuspendRaffleTicket(ctx context.Context, raffleID int32) error + UnSuspendRaffleTicket(ctx context.Context, raffleID int32) error + CreateRaffleTicket(ctx context.Context, raffleID, userID int32) (domain.RaffleTicket, error) + GetUserRaffleTickets(ctx context.Context, userID int32) ([]domain.RaffleTicketRes, error) +} diff --git a/internal/services/raffle/service.go b/internal/services/raffle/service.go new file mode 100644 index 0000000..6048ca7 --- /dev/null +++ b/internal/services/raffle/service.go @@ -0,0 +1,45 @@ +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) GetRafflesOfCompany(ctx context.Context, companyID int32) ([]dbgen.Raffle, error) { + return s.GetRafflesOfCompany(ctx, companyID) +} +func (s *Service) UpdateRaffle(ctx context.Context, raffleParams dbgen.UpdateRaffleParams) error { + return s.raffleStore.UpdateRaffle(ctx, raffleParams) +} + +func (s *Service) SuspendRaffleTicket(ctx context.Context, raffleID int32) error { + return s.raffleStore.SuspendRaffleTicket(ctx, raffleID) +} + +func (s *Service) UnSuspendRaffleTicket(ctx context.Context, raffleID int32) error { + return s.raffleStore.UnSuspendRaffleTicket(ctx, raffleID) +} + +func (s *Service) CreateRaffleTicket(ctx context.Context, raffleID, userID int32) (domain.RaffleTicket, error) { + return s.raffleStore.CreateRaffleTicket(ctx, raffleID, userID) +} + +func (s *Service) GetUserRaffleTickets(ctx context.Context, userID int32) ([]domain.RaffleTicketRes, error) { + return s.raffleStore.GetUserRaffleTickets(ctx, userID) +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index c6c1f7f..82e49e8 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -71,7 +71,6 @@ func (a *App) initAppRoutes() { groupV1.Post("/direct_deposit", a.authMiddleware, h.InitiateDirectDeposit) groupV1.Post("/direct_deposit/verify", a.authMiddleware, h.VerifyDirectDeposit) groupV1.Get("/direct_deposit/pending", a.authMiddleware, h.GetPendingDirectDeposits) - // Swagger a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler()) @@ -141,9 +140,6 @@ func (a *App) initAppRoutes() { // groupV1.Post("/arifpay/transaction-id/verify-transaction", a.authMiddleware, h.ArifpayVerifyByTransactionIDHandler) // 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) From 6ecc6f042892f4ee2225781de829397621632c01 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Tue, 2 Sep 2025 04:24:37 +0300 Subject: [PATCH 03/39] Refactor event and league domain models to include default settings, enhance odds handling, improve API routes, fixed get global settings and Incremented API version to 1.0.dev13. --- db/query/events.sql | 16 +- db/query/leagues.sql | 9 + db/query/odds.sql | 11 +- gen/db/events.sql.go | 102 +++------ gen/db/leagues.sql.go | 17 +- gen/db/odds.sql.go | 121 ++++++++--- internal/domain/event.go | 203 +++++++++--------- internal/domain/league.go | 89 ++++++-- internal/domain/odds.go | 17 +- internal/domain/setting_list.go | 126 ++++++++--- internal/repository/event.go | 7 +- internal/repository/league.go | 2 + internal/repository/odds.go | 44 ++++ internal/services/odds/port.go | 5 + internal/services/odds/service.go | 44 ++++ internal/web_server/handlers/event_handler.go | 64 +++++- internal/web_server/handlers/leagues.go | 24 ++- internal/web_server/handlers/odd_handler.go | 47 ++++ .../web_server/handlers/settings_handler.go | 13 +- internal/web_server/routes.go | 4 +- 20 files changed, 688 insertions(+), 277 deletions(-) diff --git a/db/query/events.sql b/db/query/events.sql index 1a61445..82c7ed8 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -54,7 +54,7 @@ SET sport_id = EXCLUDED.sport_id, is_live = EXCLUDED.is_live, source = EXCLUDED.source, fetched_at = now(); --- name: InsertEventSettings :exec +-- name: SaveEventSettings :exec INSERT INTO company_event_settings ( company_id, event_id, @@ -243,19 +243,7 @@ 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 diff --git a/db/query/leagues.sql b/db/query/leagues.sql index fbbc562..1063480 100644 --- a/db/query/leagues.sql +++ b/db/query/leagues.sql @@ -36,6 +36,10 @@ 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 @@ -58,6 +62,11 @@ WHERE (company_id = $1) is_featured = sqlc.narg('is_featured') OR sqlc.narg('is_featured') IS NULL ) + AND ( + name ILIKE '%' || sqlc.narg('query') || '%' + OR league_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..0a4916e 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, @@ -46,6 +46,10 @@ SELECT * FROM odds_market_with_settings WHERE 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 @@ -57,6 +61,11 @@ FROM odds_market_with_settings WHERE market_id = $1 AND event_id = $2 AND company_id = $3; +-- name: GetOddsWithSettingsByID :one +SELECT * +FROM odds_market_with_settings +WHERE id = $1 + AND company_id = $2; -- name: GetOddsByEventID :many SELECT * FROM odds_market_with_event diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index 313b240..0932419 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -654,40 +654,6 @@ func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error 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, - ) - return err -} - const IsEventMonitored = `-- name: IsEventMonitored :one SELECT is_monitored FROM events @@ -727,6 +693,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 +743,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/leagues.sql.go b/gen/db/leagues.sql.go index 5d49d4d..ff9ea19 100644 --- a/gen/db/leagues.sql.go +++ b/gen/db/leagues.sql.go @@ -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, ) @@ -109,9 +115,14 @@ WHERE (company_id = $1) is_featured = $5 OR $5 IS NULL ) + AND ( + name ILIKE '%' || $6 || '%' + OR league_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,6 +131,7 @@ 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"` } @@ -131,6 +143,7 @@ func (q *Queries) GetAllLeaguesWithSettings(ctx context.Context, arg GetAllLeagu arg.SportID, arg.IsActive, arg.IsFeatured, + arg.Query, arg.Offset, arg.Limit, ) diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index 33fcde8..a0bb5a9 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -114,6 +114,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 @@ -272,6 +300,39 @@ func (q *Queries) GetOddsWithSettingsByEventID(ctx context.Context, arg GetOddsW return items, nil } +const GetOddsWithSettingsByID = `-- name: GetOddsWithSettingsByID :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 id = $1 + AND company_id = $2 +` + +type GetOddsWithSettingsByIDParams struct { + ID int64 `json:"id"` + CompanyID int64 `json:"company_id"` +} + +func (q *Queries) GetOddsWithSettingsByID(ctx context.Context, arg GetOddsWithSettingsByIDParams) (OddsMarketWithSetting, error) { + row := q.db.QueryRow(ctx, GetOddsWithSettingsByID, arg.ID, arg.CompanyID) + var i OddsMarketWithSetting + 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 i, err +} + 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 @@ -307,36 +368,6 @@ 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 -` - -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"` -} - -func (q *Queries) InsertOddSettings(ctx context.Context, arg InsertOddSettingsParams) error { - _, err := q.db.Exec(ctx, InsertOddSettings, - arg.CompanyID, - arg.OddsMarketID, - arg.IsActive, - arg.CustomRawOdds, - ) - return err -} - const InsertOddsMarket = `-- name: InsertOddsMarket :exec INSERT INTO odds_market ( event_id, @@ -391,3 +422,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/internal/domain/event.go b/internal/domain/event.go index 0906918..6bae5a1 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -98,42 +98,45 @@ type BaseEventRes struct { DefaultIsFeatured bool `json:"default_is_featured"` DefaultIsActive bool `json:"default_is_active"` DefaultWinningUpperLimit int32 `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"` + 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 - 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 + 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 int32 + Score ValidString + MatchMinute ValidInt + TimerStatus ValidString + AddedTime ValidInt + MatchPeriod ValidInt + IsLive bool + UpdatedAt time.Time + FetchedAt time.Time } type CreateEvent struct { @@ -155,33 +158,36 @@ type CreateEvent struct { } 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"` + 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 int32 `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 EventSettings struct { @@ -291,8 +297,8 @@ func ConvertCreateEvent(e CreateEvent) dbgen.InsertEventParams { } } -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 +324,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 +367,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 +417,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/league.go b/internal/domain/league.go index a44ae70..88862b3 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 @@ -154,6 +161,9 @@ func ConvertDBLeagueWithSetting(lws dbgen.LeagueWithSetting) LeagueWithSettings SportID: lws.SportID, IsFeatured: lws.IsFeatured, UpdatedAt: lws.UpdatedAt.Time, + + DefaultIsActive: lws.DefaultIsActive, + DefaultIsFeatured: lws.DefaultIsFeatured, } } @@ -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/odds.go b/internal/domain/odds.go index fa30718..cb67b82 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/setting_list.go b/internal/domain/setting_list.go index 5f4a20e..5f90621 100644 --- a/internal/domain/setting_list.go +++ b/internal/domain/setting_list.go @@ -35,6 +35,18 @@ type SettingListRes struct { CashbackAmountCap float32 `json:"cashback_amount_cap"` } +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(), + } +} + type SaveSettingListReq struct { SMSProvider *string `json:"sms_provider,omitempty"` MaxNumberOfOutcomes *int64 `json:"max_number_of_outcomes,omitempty"` @@ -157,57 +169,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 +283,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 +367,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 +440,7 @@ func (vsl *ValidSettingList) ValidateAllSettings() error { for _, validator := range validators { if err := validator(); err != nil { + errs = append(errs, err.Error()) } } @@ -410,12 +473,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 +499,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/repository/event.go b/internal/repository/event.go index d0798d7..dc593bc 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -16,9 +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) @@ -121,7 +118,7 @@ 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 { @@ -176,7 +173,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 { diff --git a/internal/repository/league.go b/internal/repository/league.go index f003fd9..52b8aef 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{ @@ -38,6 +39,7 @@ func (s *Store) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ( func (s *Store) GetAllLeaguesByCompany(ctx context.Context, companyID int64, filter domain.LeagueFilter) ([]domain.LeagueWithSettings, error) { l, err := s.queries.GetAllLeaguesWithSettings(ctx, dbgen.GetAllLeaguesWithSettingsParams{ + Query: filter.Query.ToPG(), CompanyID: companyID, CountryCode: filter.CountryCode.ToPG(), SportID: filter.SportID.ToPG(), diff --git a/internal/repository/odds.go b/internal/repository/odds.go index cb684ce..976c007 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -97,6 +97,21 @@ func (s *Store) GetAllOddsWithSettings(ctx context.Context, companyID int64, fil return domainOdds, 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) { odds, err := s.queries.GetOddsByMarketID(ctx, dbgen.GetOddsByMarketIDParams{ @@ -134,6 +149,25 @@ func (s *Store) GetOddsWithSettingsByMarketID(ctx context.Context, marketID stri return convertedOdd, 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 + } + + convertedOdd, err := domain.ConvertDBOddMarketWithSetting(odds) + + if err != nil { + return domain.OddMarketWithSettings{}, err + } + return convertedOdd, nil +} + func (s *Store) GetOddsByEventID(ctx context.Context, upcomingID string, filter domain.OddMarketWithEventFilter) ([]domain.OddMarket, error) { odds, err := s.queries.GetOddsByEventID(ctx, dbgen.GetOddsByEventIDParams{ EventID: upcomingID, @@ -185,3 +219,13 @@ func (s *Store) GetOddsWithSettingsByEventID(ctx context.Context, upcomingID str 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/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..dff3fdd 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -705,6 +705,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 +762,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/web_server/handlers/event_handler.go b/internal/web_server/handlers/event_handler.go index e0914a3..6c3c286 100644 --- a/internal/web_server/handlers/event_handler.go +++ b/internal/web_server/handlers/event_handler.go @@ -511,7 +511,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 +523,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 +543,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 +557,63 @@ 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/leagues.go b/internal/web_server/handlers/leagues.go index 48274fd..4363e27 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, @@ -179,6 +195,7 @@ func (h *Handler) GetAllLeaguesForTenant(c *fiber.Ctx) error { 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.WriteJSON(c, fiber.StatusOK, "All leagues retrieved", res, nil) } type SetLeagueActiveReq struct { diff --git a/internal/web_server/handlers/odd_handler.go b/internal/web_server/handlers/odd_handler.go index bf89c84..9f23dff 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/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/routes.go b/internal/web_server/routes.go index 8ca5173..c1fc9fe 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -58,7 +58,7 @@ func (a *App) initAppRoutes() { a.fiber.Get("/", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{ "message": "Welcome to the FortuneBet API", - "version": "1.0dev12", + "version": "1.0.dev13", }) }) @@ -236,10 +236,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, 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) From 1c7c7e5731a7698d07549ce2ad184ed537be069a Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Tue, 2 Sep 2025 04:27:29 +0300 Subject: [PATCH 04/39] uncommenting crons for deployment --- internal/web_server/cron.go | 78 ++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 9ce3113..8946f17 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,19 +65,19 @@ 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 */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 task: func() { From 6b09c3c8d2698a6543c845fe5fe678d86796d8c5 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 3 Sep 2025 03:16:37 +0300 Subject: [PATCH 05/39] fix: resolved issue on event, odds and creating a bet --- cmd/main.go | 7 +- db/data/001_initial_seed_data.sql | 10 +- db/data/002_veli_user.sql | 4 +- db/migrations/000001_fortune.up.sql | 32 ++- db/query/events.sql | 72 +++++-- db/query/leagues.sql | 13 +- db/query/odds.sql | 80 ++++++-- gen/db/events.sql.go | 187 ++++++++++++++---- gen/db/leagues.sql.go | 34 +++- gen/db/models.go | 12 +- gen/db/odds.sql.go | 160 ++++++++++++--- internal/domain/bet.go | 1 + internal/domain/event.go | 70 +++---- internal/domain/league.go | 4 +- internal/domain/notification.go | 2 +- internal/domain/oddres.go | 2 +- internal/domain/setting_list.go | 28 ++- internal/repository/company.go | 6 +- internal/repository/event.go | 113 ++++++++++- internal/repository/league.go | 28 ++- internal/repository/notification.go | 14 +- internal/repository/odds.go | 137 ++++++++++--- internal/repository/shop_bet.go | 5 +- internal/repository/ticket.go | 1 + internal/services/bet/service.go | 37 ++-- internal/services/event/service.go | 103 +++++----- internal/services/notification/service.go | 30 ++- internal/services/result/service.go | 3 +- internal/services/transaction/shop_bet.go | 5 + internal/services/wallet/direct_deposit.go | 4 +- internal/services/wallet/service.go | 3 - internal/services/wallet/transfer.go | 4 +- internal/services/wallet/wallet.go | 21 +- internal/web_server/handlers/bet_handler.go | 16 +- internal/web_server/handlers/event_handler.go | 1 + .../handlers/notification_handler.go | 14 +- internal/web_server/handlers/shop_handler.go | 13 +- .../web_server/handlers/ticket_handler.go | 4 + 38 files changed, 952 insertions(+), 328 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 38797ae..fe8f4f1 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -112,13 +112,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) 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 @@ -130,7 +128,6 @@ func main() { wallet.WalletStore(store), wallet.TransferStore(store), wallet.DirectDepositStore(store), - notificatioStore, notificationSvc, userSvc, domain.MongoDBLogger, @@ -153,7 +150,7 @@ func main() { 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), cfg) recommendationSvc := recommendation.NewService(recommendationRepo) chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY) diff --git a/db/data/001_initial_seed_data.sql b/db/data/001_initial_seed_data.sql index 9081dee..eb64f5c 100644 --- a/db/data/001_initial_seed_data.sql +++ b/db/data/001_initial_seed_data.sql @@ -1,5 +1,4 @@ CREATE EXTENSION IF NOT EXISTS pgcrypto; - -- Locations Initial Data INSERT INTO branch_locations (key, value) VALUES ('addis_ababa', 'Addis Ababa'), @@ -75,7 +74,6 @@ VALUES ('addis_ababa', 'Addis Ababa'), ('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'), @@ -84,8 +82,8 @@ VALUES ('sms_provider', 'afro_message'), ('daily_ticket_limit', '50'), ('total_winnings_limit', '1000000'), ('amount_for_bet_referral', '1000000'), - ('cashback_amount_cap', '1000') ON CONFLICT (key) DO NOTHING; - + ('cashback_amount_cap', '1000'), + ('default_winning_limit', '5000000') ON CONFLICT (key) DO NOTHING; -- Users INSERT INTO users ( id, @@ -224,7 +222,7 @@ VALUES ( ), ( 3, - 20000, + 100000000 , TRUE, TRUE, TRUE, @@ -237,7 +235,7 @@ VALUES ( ), ( 4, - 15000, + 50000000, TRUE, TRUE, TRUE, diff --git a/db/data/002_veli_user.sql b/db/data/002_veli_user.sql index 5323bac..1dfe96a 100644 --- a/db/data/002_veli_user.sql +++ b/db/data/002_veli_user.sql @@ -33,7 +33,7 @@ VALUES ( 6, 'Kirubel', 'Kibru', - 'modernkibru @gmail.com', + 'modernkibru@gmail.com', NULL, crypt('password@123', gen_salt('bf'))::bytea, 'customer', @@ -98,7 +98,7 @@ VALUES ( ), ( 7, - 10000, + 1000000, TRUE, TRUE, TRUE, diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 6af0cb2..898d587 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -36,28 +36,24 @@ CREATE TABLE IF NOT EXISTS virtual_game_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, @@ -325,7 +321,7 @@ CREATE TABLE events ( ), 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) ); @@ -621,7 +617,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, @@ -634,7 +630,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.*, @@ -656,7 +652,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, diff --git a/db/query/events.sql b/db/query/events.sql index 82c7ed8..61f5a86 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,6 +54,7 @@ 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: SaveEventSettings :exec INSERT INTO company_event_settings ( @@ -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,29 @@ 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 sqlc.narg('is_featured') 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 +212,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 +229,12 @@ 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 sqlc.narg('is_featured') IS NULL +) ORDER BY start_time ASC LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- name: GetUpcomingByID :one @@ -223,10 +245,21 @@ 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; @@ -243,7 +276,6 @@ WHERE id = $1; UPDATE events SET is_monitored = $1 WHERE id = $2; - -- name: DeleteEvent :exec DELETE FROM events WHERE id = $1; \ No newline at end of file diff --git a/db/query/leagues.sql b/db/query/leagues.sql index 1063480..21765b9 100644 --- a/db/query/leagues.sql +++ b/db/query/leagues.sql @@ -43,10 +43,14 @@ WHERE ( 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 ( +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 ) @@ -64,7 +68,6 @@ WHERE (company_id = $1) ) AND ( name ILIKE '%' || sqlc.narg('query') || '%' - OR league_name ILIKE '%' || sqlc.narg('query') || '%' OR sqlc.narg('query') IS NULL ) ORDER BY is_featured DESC, diff --git a/db/query/odds.sql b/db/query/odds.sql index 0a4916e..dc467c6 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -42,9 +42,22 @@ 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 * @@ -56,16 +69,42 @@ 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 * -FROM odds_market_with_settings -WHERE id = $1 - AND company_id = $2; +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 @@ -84,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/gen/db/events.sql.go b/gen/db/events.sql.go index 0932419..3ee77ad 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -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,14 @@ 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 $8 IS NULL +) ORDER BY start_time ASC -LIMIT $9 OFFSET $8 +LIMIT $10 OFFSET $9 ` type GetEventsWithSettingsParams struct { @@ -175,11 +234,46 @@ 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"` 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 +282,7 @@ func (q *Queries) GetEventsWithSettings(ctx context.Context, arg GetEventsWithSe arg.LastStartTime, arg.FirstStartTime, arg.CountryCode, + arg.IsFeatured, arg.Offset, arg.Limit, ) @@ -195,9 +290,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, @@ -403,16 +498,18 @@ func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginat 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 +526,13 @@ 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 $8 IS NULL + ) ` type GetTotalCompanyEventsParams struct { @@ -442,6 +543,7 @@ 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"` } func (q *Queries) GetTotalCompanyEvents(ctx context.Context, arg GetTotalCompanyEventsParams) (int64, error) { @@ -453,6 +555,7 @@ func (q *Queries) GetTotalCompanyEvents(ctx context.Context, arg GetTotalCompany arg.LastStartTime, arg.FirstStartTime, arg.CountryCode, + arg.IsFeatured, ) var count int64 err := row.Scan(&count) @@ -573,7 +676,8 @@ INSERT INTO events ( start_time, is_live, status, - source + source, + default_winning_upper_limit ) VALUES ( $1, @@ -590,7 +694,8 @@ VALUES ( $12, $13, $14, - $15 + $15, + $16 ) ON CONFLICT (id) DO UPDATE SET sport_id = EXCLUDED.sport_id, @@ -603,7 +708,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 +716,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,6 +756,7 @@ func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error arg.IsLive, arg.Status, arg.Source, + arg.DefaultWinningUpperLimit, ) return err } diff --git a/gen/db/leagues.sql.go b/gen/db/leagues.sql.go index ff9ea19..57d3c28 100644 --- a/gen/db/leagues.sql.go +++ b/gen/db/leagues.sql.go @@ -96,10 +96,14 @@ 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 ) @@ -117,7 +121,6 @@ WHERE (company_id = $1) ) AND ( name ILIKE '%' || $6 || '%' - OR league_name ILIKE '%' || $6 || '%' OR $6 IS NULL ) ORDER BY is_featured DESC, @@ -136,7 +139,22 @@ type GetAllLeaguesWithSettingsParams struct { 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, @@ -151,9 +169,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, diff --git a/gen/db/models.go b/gen/db/models.go index 428889c..e41c659 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -321,7 +321,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 +356,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 +385,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 +447,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 +520,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"` diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index a0bb5a9..79da894 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -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, @@ -247,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 ` @@ -261,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, @@ -272,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, @@ -301,10 +359,23 @@ func (q *Queries) GetOddsWithSettingsByEventID(ctx context.Context, arg GetOddsW } const GetOddsWithSettingsByID = `-- name: GetOddsWithSettingsByID :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 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 o.id = $1 ` type GetOddsWithSettingsByIDParams struct { @@ -312,9 +383,25 @@ type GetOddsWithSettingsByIDParams struct { CompanyID int64 `json:"company_id"` } -func (q *Queries) GetOddsWithSettingsByID(ctx context.Context, arg GetOddsWithSettingsByIDParams) (OddsMarketWithSetting, error) { +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 OddsMarketWithSetting + var i GetOddsWithSettingsByIDRow err := row.Scan( &i.ID, &i.EventID, @@ -334,11 +421,24 @@ func (q *Queries) GetOddsWithSettingsByID(ctx context.Context, arg GetOddsWithSe } 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 +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 ` type GetOddsWithSettingsByMarketIDParams struct { @@ -347,9 +447,25 @@ type GetOddsWithSettingsByMarketIDParams struct { CompanyID int64 `json:"company_id"` } -func (q *Queries) GetOddsWithSettingsByMarketID(ctx context.Context, arg GetOddsWithSettingsByMarketIDParams) (OddsMarketWithSetting, error) { +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 OddsMarketWithSetting + var i GetOddsWithSettingsByMarketIDRow err := row.Scan( &i.ID, &i.EventID, 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/event.go b/internal/domain/event.go index 6bae5a1..57a1d32 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,7 +97,7 @@ 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"` @@ -128,7 +128,7 @@ type EventWithSettings struct { WinningUpperLimit int32 DefaultIsFeatured bool DefaultIsActive bool - DefaultWinningUpperLimit int32 + DefaultWinningUpperLimit int64 Score ValidString MatchMinute ValidInt TimerStatus ValidString @@ -140,21 +140,22 @@ type EventWithSettings struct { } 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 + 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 { @@ -179,7 +180,7 @@ type EventWithSettingsRes struct { WinningUpperLimit int32 `json:"winning_upper_limit"` 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,omitempty"` MatchMinute int `json:"match_minute,omitempty"` TimerStatus string `json:"timer_status,omitempty"` @@ -279,21 +280,22 @@ 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, } } diff --git a/internal/domain/league.go b/internal/domain/league.go index 88862b3..0e0d65c 100644 --- a/internal/domain/league.go +++ b/internal/domain/league.go @@ -148,8 +148,8 @@ func ConvertDBLeagueWithSetting(lws dbgen.LeagueWithSetting) 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, }, diff --git a/internal/domain/notification.go b/internal/domain/notification.go index d10f3d7..28dbed2 100644 --- a/internal/domain/notification.go +++ b/internal/domain/notification.go @@ -73,7 +73,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..266aa11 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 ValidInt64 `json:"id"` Name string `json:"name"` Odds []json.RawMessage `json:"odds"` Header string `json:"header,omitempty"` diff --git a/internal/domain/setting_list.go b/internal/domain/setting_list.go index 5f90621..52ae2a4 100644 --- a/internal/domain/setting_list.go +++ b/internal/domain/setting_list.go @@ -23,6 +23,7 @@ type SettingList struct { 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"` } type SettingListRes struct { @@ -33,6 +34,7 @@ type SettingListRes struct { 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"` } func ConvertSettingListRes(settings SettingList) SettingListRes { @@ -44,6 +46,7 @@ func ConvertSettingListRes(settings SettingList) SettingListRes { TotalWinningLimit: settings.TotalWinningLimit.Float32(), AmountForBetReferral: settings.AmountForBetReferral.Float32(), CashbackAmountCap: settings.CashbackAmountCap.Float32(), + DefaultWinningLimit: settings.DefaultWinningLimit, } } @@ -55,6 +58,18 @@ type SaveSettingListReq struct { 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"` +} + +type ValidSettingList struct { + SMSProvider ValidString + MaxNumberOfOutcomes ValidInt64 + BetAmountLimit ValidCurrency + DailyTicketPerIP ValidInt64 + TotalWinningLimit ValidCurrency + AmountForBetReferral ValidCurrency + CashbackAmountCap ValidCurrency + DefaultWinningLimit ValidInt64 } func ConvertSaveSettingListReq(settings SaveSettingListReq) ValidSettingList { @@ -66,19 +81,10 @@ func ConvertSaveSettingListReq(settings SaveSettingListReq) ValidSettingList { TotalWinningLimit: ConvertFloat32PtrToCurrency(settings.TotalWinningLimit), AmountForBetReferral: ConvertFloat32PtrToCurrency(settings.AmountForBetReferral), CashbackAmountCap: ConvertFloat32PtrToCurrency(settings.CashbackAmountCap), + DefaultWinningLimit: ConvertInt64Ptr(settings.DefaultWinningLimit), } } -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{ @@ -89,6 +95,7 @@ func (vsl *ValidSettingList) ToSettingList() SettingList { TotalWinningLimit: Currency(vsl.TotalWinningLimit.Value), AmountForBetReferral: Currency(vsl.AmountForBetReferral.Value), CashbackAmountCap: Currency(vsl.CashbackAmountCap.Value), + DefaultWinningLimit: vsl.DefaultWinningLimit.Value, } } @@ -104,6 +111,7 @@ 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, } } 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 dc593bc..faf073e 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -16,7 +16,6 @@ func (s *Store) SaveEvent(ctx context.Context, e domain.CreateEvent) error { return s.queries.InsertEvent(ctx, domain.ConvertCreateEvent(e)) } - func (s *Store) GetLiveEventIDs(ctx context.Context) ([]string, error) { return s.queries.ListLiveEvents(ctx) } @@ -86,6 +85,7 @@ 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(), }) if err != nil { @@ -100,13 +100,69 @@ 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(), }) 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) @@ -125,7 +181,56 @@ func (s *Store) GetEventWithSettingByID(ctx context.Context, ID string, companyI 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{ @@ -173,7 +278,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.SaveEventSettings(ctx, domain.ConvertUpdateEventSettings(event)); + return s.queries.SaveEventSettings(ctx, domain.ConvertUpdateEventSettings(event)) } func (s *Store) DeleteEvent(ctx context.Context, eventID string) error { diff --git a/internal/repository/league.go b/internal/repository/league.go index 52b8aef..4000b00 100644 --- a/internal/repository/league.go +++ b/internal/repository/league.go @@ -51,13 +51,39 @@ 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 domain.ConvertDBLeagueWithSettings(l), nil + result := make([]domain.LeagueWithSettings, len(l)) + for i, league := range l { + result[i] = domain.LeagueWithSettings{ + ID: league.ID, + Name: league.Name, + CompanyID: league.CompanyID.Int64, + CountryCode: domain.ValidString{ + Value: league.CountryCode.String, + Valid: league.CountryCode.Valid, + }, + Bet365ID: domain.ValidInt32{ + Value: league.Bet365ID.Int32, + Valid: league.Bet365ID.Valid, + }, + IsActive: league.IsActive, + SportID: league.SportID, + IsFeatured: league.IsFeatured, + UpdatedAt: league.UpdatedAt.Time, + + DefaultIsActive: league.DefaultIsActive, + DefaultIsFeatured: league.DefaultIsFeatured, + } + } + + return result, 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 976c007..3e09a91 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -88,23 +88,46 @@ 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) + convertedOdd, err := domain.ConvertDBOddMarket(odd) if err != nil { return domain.OddMarket{}, err @@ -141,31 +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 + // 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 } - return convertedOdd, nil + + 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, + ID: ID, CompanyID: companyID, }) - - if err != nil { - return domain.OddMarketWithSettings{}, err - } - - convertedOdd, err := domain.ConvertDBOddMarketWithSetting(odds) 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) { @@ -208,12 +276,37 @@ 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 { 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/services/bet/service.go b/internal/services/bet/service.go index e026110..b2ef38f 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -283,8 +283,10 @@ 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") + + // TODO: Make this a setting + if role == domain.RoleCustomer && count >= 10 { + return domain.CreateBetRes{}, fmt.Errorf("max user limit for single outcome") } fastCode := helpers.GenerateFastCode() @@ -340,17 +342,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,7 +381,7 @@ 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)), @@ -1073,8 +1078,6 @@ func (s *Service) SendErrorStatusNotification(ctx context.Context, status domain 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, @@ -1088,7 +1091,7 @@ func (s *Service) SendErrorStatusNotification(ctx context.Context, status domain Message: message, }, Priority: 1, - ErrorSeverity: &errorSeverityLevel, + ErrorSeverity: domain.NotificationErrorSeverityHigh, Metadata: fmt.Appendf(nil, `{ "status":%v "more": %v @@ -1117,9 +1120,8 @@ func (s *Service) SendAdminErrorAlertNotification(ctx context.Context, status do 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, + ErrorSeverity: domain.NotificationErrorSeverityHigh, DeliveryStatus: domain.DeliveryStatusPending, IsRead: false, Type: domain.NOTIFICATION_TYPE_BET_RESULT, @@ -1313,7 +1315,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 +1324,6 @@ func (s *Service) ProcessBetCashback(ctx context.Context) error { return err } - for _, bet := range bets { shouldProcess := true loseCount := 0 diff --git a/internal/services/event/service.go b/internal/services/event/service.go index ab227bc..fa53e92 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -14,6 +14,8 @@ import ( "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,13 +23,15 @@ import ( type service struct { token string store *repository.Store + settingSvc settings.Service mongoLogger *zap.Logger } -func New(token string, store *repository.Store, mongoLogger *zap.Logger) Service { +func New(token string, store *repository.Store, settingSvc settings.Service, mongoLogger *zap.Logger) Service { return &service{ token: token, store: store, + settingSvc: settingSvc, mongoLogger: mongoLogger, } } @@ -206,22 +210,31 @@ 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 + } + + const pageLimit int = 1 + // sportIDs := []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91} + sportIDs := []int{1} + + 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 +314,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 +350,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 +359,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 +392,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 } @@ -478,7 +493,7 @@ 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 { diff --git a/internal/services/notification/service.go b/internal/services/notification/service.go index ae0b990..76531db 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 @@ -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, 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 } diff --git a/internal/services/result/service.go b/internal/services/result/service.go index ad1f1c5..a872924 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -541,9 +541,8 @@ 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, 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/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/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..fd6bc04 100644 --- a/internal/services/wallet/transfer.go +++ b/internal/services/wallet/transfer.go @@ -135,7 +135,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 +163,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..df67111 100644 --- a/internal/services/wallet/wallet.go +++ b/internal/services/wallet/wallet.go @@ -304,10 +304,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 +343,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 +353,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 +366,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 +406,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 +415,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 +429,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 +457,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/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index 5ea6c56..664a1f0 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), diff --git a/internal/web_server/handlers/event_handler.go b/internal/web_server/handlers/event_handler.go index 6c3c286..2de000d 100644 --- a/internal/web_server/handlers/event_handler.go +++ b/internal/web_server/handlers/event_handler.go @@ -352,6 +352,7 @@ func (h *Handler) GetTopLeagues(c *fiber.Ctx) error { Value: true, Valid: true, }, + }) if err != nil { 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/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()), From cc31c6660efd26b148acf5ae51e6853e6ea2f46f Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 3 Sep 2025 03:20:43 +0300 Subject: [PATCH 06/39] fix: removed docker volume rm on db-down --- makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/makefile b/makefile index 638a748..2a98b47 100644 --- a/makefile +++ b/makefile @@ -86,7 +86,7 @@ db-up: | logs .PHONY: db-down db-down: @docker compose down -v - @docker volume rm fortunebet-backend_postgres_data +# @docker volume rm fortunebet-backend_postgres_data .PHONY: sqlc-gen sqlc-gen: @sqlc generate From 180a940b58666a4ffc5d46fc69907d8955117430 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 3 Sep 2025 03:22:00 +0300 Subject: [PATCH 07/39] chore: changed the verison to v1.0dev13.1 --- internal/web_server/routes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index c1fc9fe..c337f4a 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -58,7 +58,7 @@ func (a *App) initAppRoutes() { a.fiber.Get("/", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{ "message": "Welcome to the FortuneBet API", - "version": "1.0.dev13", + "version": "1.0.dev13.1", }) }) From c09bf5ca3dd76f83039a677eb7f489f75fdd2b68 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 3 Sep 2025 03:30:55 +0300 Subject: [PATCH 09/39] fix: removed the seed_data from db-up in the makefile --- makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/makefile b/makefile index 2a98b47..f837eea 100644 --- a/makefile +++ b/makefile @@ -81,7 +81,6 @@ logs: db-up: | logs @mkdir -p logs @docker compose up -d postgres migrate mongo redis --wait migrate - @make seed_data @docker logs fortunebet-backend-postgres-1 > logs/postgres.log 2>&1 & .PHONY: db-down db-down: From 6f4229d556f4d054e2f1b95cca8855910854195f Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 3 Sep 2025 03:35:34 +0300 Subject: [PATCH 10/39] fix: fetching full event list --- internal/services/event/service.go | 6 +++--- makefile | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/services/event/service.go b/internal/services/event/service.go index fa53e92..09673d3 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -218,9 +218,9 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_ur return } - const pageLimit int = 1 - // sportIDs := []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91} - sportIDs := []int{1} + const pageLimit int = 200 + sportIDs := []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91} + // sportIDs := []int{1} var skippedLeague []string var totalEvents = 0 diff --git a/makefile b/makefile index f837eea..7c57002 100644 --- a/makefile +++ b/makefile @@ -74,7 +74,6 @@ postgres_log: .PHONY: swagger swagger: @swag init -g cmd/main.go - .PHONY: db-up logs: @mkdir -p logs From edc15952c8575b3230a58367c03968b750556d83 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 3 Sep 2025 09:55:25 +0300 Subject: [PATCH 13/39] hotfix: commenting out providers for demo --- internal/services/virtualGame/veli/service.go | 16 ++++++------ internal/web_server/app.go | 20 +++++++------- internal/web_server/cron.go | 8 +++--- internal/web_server/handlers/auth_handler.go | 26 +++++++++---------- 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/internal/services/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go index 2de2eb4..347c608 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -66,15 +66,15 @@ 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{ diff --git a/internal/web_server/app.go b/internal/web_server/app.go index fa016c0..02e84e1 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -126,21 +126,21 @@ 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, })) s := &App{ - veliVirtualGameService: veliVirtualGameService, - telebirrSvc: telebirrSvc, - arifpaySvc: arifpaySvc, - santimpaySvc: santimpaySvc, - issueReportingSvc: issueReportingSvc, - instSvc: instSvc, - currSvc: currSvc, - fiber: app, - port: port, + veliVirtualGameService: veliVirtualGameService, + telebirrSvc: telebirrSvc, + arifpaySvc: arifpaySvc, + santimpaySvc: santimpaySvc, + issueReportingSvc: issueReportingSvc, + instSvc: instSvc, + currSvc: currSvc, + fiber: app, + port: port, settingSvc: settingSvc, authSvc: authSvc, diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 8946f17..53acbb1 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -154,10 +154,10 @@ func SetupReportandVirtualGameCronJobs( spec string period string }{ - // { - // spec: "*/60 * * * * *", // Every 1 minute 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/auth_handler.go b/internal/web_server/handlers/auth_handler.go index 0ee2f01..a64f285 100644 --- a/internal/web_server/handlers/auth_handler.go +++ b/internal/web_server/handlers/auth_handler.go @@ -108,12 +108,12 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error { } accessToken, err := jwtutil.CreateJwt( - successRes.UserId, - successRes.Role, - successRes.CompanyID, - h.jwtConfig.JwtAccessKey, + 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), @@ -195,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), @@ -296,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), From c77355ad4c07e73c5a8cee24d9e7f0112d6abaa7 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 3 Sep 2025 10:01:22 +0300 Subject: [PATCH 14/39] hotfix:commenting provider code for demo --- internal/services/virtualGame/veli/service.go | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/internal/services/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go index 347c608..d7dc327 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -95,15 +95,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{ @@ -131,15 +131,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{ From d78a100f27a0f09ef9506af68ba7f6230b1f399b Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Wed, 3 Sep 2025 16:06:01 +0300 Subject: [PATCH 15/39] raffle service implementation --- cmd/main.go | 3 + db/query/raffle.sql | 10 +- gen/db/auth.sql.go | 2 +- gen/db/bet.sql.go | 2 +- gen/db/bet_stat.sql.go | 2 +- gen/db/bonus.sql.go | 2 +- gen/db/branch.sql.go | 2 +- gen/db/cashier.sql.go | 2 +- gen/db/company.sql.go | 2 +- gen/db/copyfrom.go | 2 +- gen/db/db.go | 2 +- gen/db/direct_deposit.sql.go | 2 +- gen/db/disabled_odds.sql.go | 2 +- gen/db/event_history.sql.go | 2 +- gen/db/events.sql.go | 2 +- gen/db/events_stat.sql.go | 2 +- gen/db/flags.sql.go | 2 +- gen/db/institutions.sql.go | 2 +- gen/db/issue_reporting.sql.go | 2 +- gen/db/leagues.sql.go | 2 +- gen/db/location.sql.go | 2 +- gen/db/models.go | 2 +- gen/db/monitor.sql.go | 2 +- gen/db/notification.sql.go | 2 +- gen/db/odd_history.sql.go | 2 +- gen/db/odds.sql.go | 2 +- gen/db/otp.sql.go | 2 +- gen/db/raffle.sql.go | 48 ++-- gen/db/referal.sql.go | 2 +- gen/db/report.sql.go | 2 +- gen/db/result.sql.go | 2 +- gen/db/result_log.sql.go | 2 +- gen/db/settings.sql.go | 2 +- gen/db/shop_transactions.sql.go | 2 +- gen/db/ticket.sql.go | 2 +- gen/db/transfer.sql.go | 2 +- gen/db/user.sql.go | 2 +- gen/db/virtual_games.sql.go | 2 +- gen/db/wallet.sql.go | 2 +- internal/domain/raffle.go | 20 +- internal/repository/{reffel.go => raffel.go} | 65 +++--- internal/repository/settings.go | 4 +- internal/services/raffle/port.go | 8 +- internal/services/raffle/service.go | 29 +-- internal/web_server/app.go | 22 +- internal/web_server/cron.go | 2 - internal/web_server/handlers/bet_handler.go | 2 + internal/web_server/handlers/handlers.go | 4 + .../web_server/handlers/raffle_handler.go | 216 ++++++++++++++++++ internal/web_server/routes.go | 11 +- 50 files changed, 382 insertions(+), 134 deletions(-) rename internal/repository/{reffel.go => raffel.go} (82%) create mode 100644 internal/web_server/handlers/raffle_handler.go diff --git a/cmd/main.go b/cmd/main.go index fe39dff..9d25945 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -45,6 +45,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" @@ -150,6 +151,7 @@ func main() { recommendationRepo := repository.NewRecommendationRepository(store) referalSvc := referralservice.New(referalRepo, *walletSvc, store, cfg, logger) + raffleSvc := raffle.NewService(store) virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger) aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger) veliCLient := veli.NewClient(cfg, walletSvc) @@ -274,6 +276,7 @@ func main() { eventSvc, leagueSvc, referalSvc, + raffleSvc, bonusSvc, virtualGameSvc, aleaService, diff --git a/db/query/raffle.sql b/db/query/raffle.sql index 4e900fd..4c43a10 100644 --- a/db/query/raffle.sql +++ b/db/query/raffle.sql @@ -6,12 +6,10 @@ RETURNING *; -- name: GetRafflesOfCompany :many SELECT * FROM raffles WHERE company_id = $1; --- name: UpdateRaffle :exec -UPDATE raffles -SET name = $1, - expires_at = $2, - status = $3 -WHERE id = $4; +-- name: DeleteRaffle :one +DELETE FROM raffles +WHERE id = $1 +RETURNING *; -- name: UpdateRaffleTicketStatus :exec UPDATE raffle_tickets diff --git a/gen/db/auth.sql.go b/gen/db/auth.sql.go index 1817514..c5984b9 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 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..f62227b 100644 --- a/gen/db/bonus.sql.go +++ b/gen/db/bonus.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: bonus.sql package dbgen 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..e262575 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 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..2bdb5b8 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 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..6c0ae07 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 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 0d18e73..91e56e7 100644 --- a/gen/db/models.go +++ b/gen/db/models.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/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..f7dec2b 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 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 index 64fdd88..4b77106 100644 --- a/gen/db/raffle.sql.go +++ b/gen/db/raffle.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: raffle.sql package dbgen @@ -67,6 +67,27 @@ func (q *Queries) CreateRaffleTicket(ctx context.Context, arg CreateRaffleTicket 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 GetRafflesOfCompany = `-- name: GetRafflesOfCompany :many SELECT id, company_id, name, created_at, expires_at, type, status FROM raffles WHERE company_id = $1 ` @@ -148,31 +169,6 @@ func (q *Queries) GetUserRaffleTickets(ctx context.Context, userID int32) ([]Get return items, nil } -const UpdateRaffle = `-- name: UpdateRaffle :exec -UPDATE raffles -SET name = $1, - expires_at = $2, - status = $3 -WHERE id = $4 -` - -type UpdateRaffleParams struct { - Name string `json:"name"` - ExpiresAt pgtype.Timestamp `json:"expires_at"` - Status string `json:"status"` - ID int32 `json:"id"` -} - -func (q *Queries) UpdateRaffle(ctx context.Context, arg UpdateRaffleParams) error { - _, err := q.db.Exec(ctx, UpdateRaffle, - arg.Name, - arg.ExpiresAt, - arg.Status, - arg.ID, - ) - return err -} - const UpdateRaffleTicketStatus = `-- name: UpdateRaffleTicketStatus :exec UPDATE raffle_tickets SET is_active = $1 diff --git a/gen/db/referal.sql.go b/gen/db/referal.sql.go index b5ceeed..3313a9b 100644 --- a/gen/db/referal.sql.go +++ b/gen/db/referal.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: referal.sql package dbgen 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..eb50e0e 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 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..926fc8c 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 diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index 0d4c33b..b15d8a2 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 diff --git a/gen/db/virtual_games.sql.go b/gen/db/virtual_games.sql.go index 79bce9e..51da52d 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 diff --git a/gen/db/wallet.sql.go b/gen/db/wallet.sql.go index 7e3eb7f..f41a944 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 diff --git a/internal/domain/raffle.go b/internal/domain/raffle.go index b4b1e70..c6adc2c 100644 --- a/internal/domain/raffle.go +++ b/internal/domain/raffle.go @@ -29,8 +29,20 @@ type RaffleTicketRes struct { } type CreateRaffle struct { - CompanyID int32 - Name string - ExpiresAt time.Time - Type string + 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/repository/reffel.go b/internal/repository/raffel.go similarity index 82% rename from internal/repository/reffel.go rename to internal/repository/raffel.go index a28096a..db230bd 100644 --- a/internal/repository/reffel.go +++ b/internal/repository/raffel.go @@ -45,7 +45,7 @@ func convertCreateRaffle(raffle domain.CreateRaffle) dbgen.CreateRaffleParams { CompanyID: raffle.CompanyID, Name: raffle.Name, ExpiresAt: pgtype.Timestamp{ - Time: raffle.ExpiresAt, + Time: *raffle.ExpiresAt, Valid: true, }, Type: raffle.Type, @@ -61,6 +61,15 @@ func (s *Store) CreateRaffle(ctx context.Context, raffle domain.CreateRaffle) (d 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 { @@ -70,36 +79,10 @@ func (s *Store) GetRafflesOfCompany(ctx context.Context, companyID int32) ([]dbg return raffles, nil } -func (s *Store) UpdateRaffle(ctx context.Context, raffleParams dbgen.UpdateRaffleParams) error { - return s.queries.UpdateRaffle(ctx, raffleParams) -} - -func (s *Store) SuspendRaffleTicket(ctx context.Context, raffleID int32) error { - return s.queries.UpdateRaffleTicketStatus(ctx, dbgen.UpdateRaffleTicketStatusParams{ - ID: raffleID, - 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) CreateRaffleTicket(ctx context.Context, raffleID, userID int32) (domain.RaffleTicket, error) { +func (s *Store) CreateRaffleTicket(ctx context.Context, raffleTicketParams domain.CreateRaffleTicket) (domain.RaffleTicket, error) { raffleTicket, err := s.queries.CreateRaffleTicket(ctx, dbgen.CreateRaffleTicketParams{ - RaffleID: raffleID, - UserID: userID, + RaffleID: raffleTicketParams.RaffleID, + UserID: raffleTicketParams.UserID, }) if err != nil { return domain.RaffleTicket{}, err @@ -121,3 +104,25 @@ func (s *Store) GetUserRaffleTickets(ctx context.Context, userID int32) ([]domai 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 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/services/raffle/port.go b/internal/services/raffle/port.go index a457f66..c8c4b8e 100644 --- a/internal/services/raffle/port.go +++ b/internal/services/raffle/port.go @@ -9,10 +9,10 @@ import ( type RaffleStore interface { CreateRaffle(ctx context.Context, raffle domain.CreateRaffle) (domain.Raffle, error) + DeleteRaffle(ctx context.Context, raffleID int32) (domain.Raffle, error) GetRafflesOfCompany(ctx context.Context, companyID int32) ([]dbgen.Raffle, error) - UpdateRaffle(ctx context.Context, raffleParams dbgen.UpdateRaffleParams) error - SuspendRaffleTicket(ctx context.Context, raffleID int32) error - UnSuspendRaffleTicket(ctx context.Context, raffleID int32) error - CreateRaffleTicket(ctx context.Context, raffleID, userID int32) (domain.RaffleTicket, 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 index 6048ca7..1246fb7 100644 --- a/internal/services/raffle/service.go +++ b/internal/services/raffle/service.go @@ -21,25 +21,26 @@ func (s *Service) CreateRaffle(ctx context.Context, raffle domain.CreateRaffle) return s.raffleStore.CreateRaffle(ctx, raffle) } +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.GetRafflesOfCompany(ctx, companyID) -} -func (s *Service) UpdateRaffle(ctx context.Context, raffleParams dbgen.UpdateRaffleParams) error { - return s.raffleStore.UpdateRaffle(ctx, raffleParams) + return s.raffleStore.GetRafflesOfCompany(ctx, companyID) } -func (s *Service) SuspendRaffleTicket(ctx context.Context, raffleID int32) error { - return s.raffleStore.SuspendRaffleTicket(ctx, raffleID) -} - -func (s *Service) UnSuspendRaffleTicket(ctx context.Context, raffleID int32) error { - return s.raffleStore.UnSuspendRaffleTicket(ctx, raffleID) -} - -func (s *Service) CreateRaffleTicket(ctx context.Context, raffleID, userID int32) (domain.RaffleTicket, error) { - return s.raffleStore.CreateRaffleTicket(ctx, raffleID, userID) +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) +} diff --git a/internal/web_server/app.go b/internal/web_server/app.go index fa016c0..1d7adf6 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -18,6 +18,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" @@ -57,6 +58,7 @@ type App struct { logger *slog.Logger NotidicationStore *notificationservice.Service referralSvc referralservice.ReferralStore + raffleSvc raffle.RaffleStore bonusSvc *bonus.Service port int settingSvc *settings.Service @@ -108,6 +110,7 @@ func NewApp( eventSvc event.Service, leagueSvc league.Service, referralSvc referralservice.ReferralStore, + raffleSvc raffle.RaffleStore, bonusSvc *bonus.Service, virtualGameSvc virtualgameservice.VirtualGameService, aleaVirtualGameService alea.AleaVirtualGameService, @@ -132,15 +135,15 @@ func NewApp( })) s := &App{ - veliVirtualGameService: veliVirtualGameService, - telebirrSvc: telebirrSvc, - arifpaySvc: arifpaySvc, - santimpaySvc: santimpaySvc, - issueReportingSvc: issueReportingSvc, - instSvc: instSvc, - currSvc: currSvc, - fiber: app, - port: port, + veliVirtualGameService: veliVirtualGameService, + telebirrSvc: telebirrSvc, + arifpaySvc: arifpaySvc, + santimpaySvc: santimpaySvc, + issueReportingSvc: issueReportingSvc, + instSvc: instSvc, + currSvc: currSvc, + fiber: app, + port: port, settingSvc: settingSvc, authSvc: authSvc, @@ -158,6 +161,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 c18bbe5..ea828e5 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -254,8 +254,6 @@ func SetupReportandVirtualGameCronJobs( log.Printf("Cron jobs started. Reports will be saved to: %s", outputDir) } - - func ProcessBetCashback(ctx context.Context, betService *betSvc.Service) { c := cron.New(cron.WithSeconds()) diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index 31dac1b..b081512 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -224,6 +224,8 @@ func (h *Handler) CreateBetInternal(c *fiber.Ctx, req domain.CreateBetReq, userI return domain.CreateBetRes{}, err } + // create raffle ticket here + return res, nil } diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index a59dbc9..673a871 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -18,6 +18,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" @@ -49,6 +50,7 @@ type Handler struct { notificationSvc *notificationservice.Service userSvc *user.Service referralSvc referralservice.ReferralStore + raffleSvc raffle.RaffleStore bonusSvc *bonus.Service reportSvc report.ReportStore chapaSvc *chapa.Service @@ -88,6 +90,7 @@ func New( chapaSvc *chapa.Service, walletSvc *wallet.Service, referralSvc referralservice.ReferralStore, + raffleSvc raffle.RaffleStore, bonusSvc *bonus.Service, virtualGameSvc virtualgameservice.VirtualGameService, aleaVirtualGameSvc alea.AleaVirtualGameService, @@ -122,6 +125,7 @@ func New( chapaSvc: chapaSvc, walletSvc: walletSvc, referralSvc: referralSvc, + raffleSvc: raffleSvc, bonusSvc: bonusSvc, validator: validator, userSvc: userSvc, diff --git a/internal/web_server/handlers/raffle_handler.go b/internal/web_server/handlers/raffle_handler.go new file mode 100644 index 0000000..d1b59ba --- /dev/null +++ b/internal/web_server/handlers/raffle_handler.go @@ -0,0 +1,216 @@ +package handlers + +import ( + "fmt" + "strconv" + "time" + + "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) 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) 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) CreateRaffleTicket(c *fiber.Ctx) error { + var req domain.CreateRaffleTicket + if err := c.BodyParser(&req); err != nil { + 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 { + 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/routes.go b/internal/web_server/routes.go index 82e49e8..d9bfdf0 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -34,6 +34,7 @@ func (a *App) initAppRoutes() { a.chapaSvc, a.walletSvc, a.referralSvc, + a.raffleSvc, a.bonusSvc, a.virtualGameSvc, a.aleaVirtualGameService, @@ -163,6 +164,15 @@ func (a *App) initAppRoutes() { groupV1.Get("/referral/settings", a.authMiddleware, h.GetReferralSettings) groupV1.Patch("/referral/settings", a.authMiddleware, h.UpdateReferralSettings) + // Raffle Routes + a.fiber.Post("/raffle/create", a.authMiddleware, h.CreateRaffle) + a.fiber.Get("/raffle/delete/:id", a.authMiddleware, h.DeleteRaffle) + a.fiber.Get("/raffle/company/:id", a.authMiddleware, h.GetRafflesOfCompany) + 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) @@ -254,7 +264,6 @@ 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.Get("/sport/bet/fastcode/:fast_code", h.GetBetByFastCode) tenantAuth.Get("/sport/bet", h.GetAllBet) From 144cb0a42c894d039d94e8fce6b4acb6adfdf65a Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 3 Sep 2025 22:26:12 +0300 Subject: [PATCH 16/39] fix: sending daily result report instead of hourly --- internal/services/messenger/email.go | 4 +- internal/services/notification/service.go | 6 +- internal/services/result/service.go | 134 +++++++++++++++++- internal/services/user/common.go | 2 +- internal/services/virtualGame/veli/service.go | 16 +-- internal/web_server/cron.go | 2 +- 6 files changed, 145 insertions(+), 19 deletions(-) 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/notification/service.go b/internal/services/notification/service.go index 76531db..6ba4044 100644 --- a/internal/services/notification/service.go +++ b/internal/services/notification/service.go @@ -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 { @@ -371,7 +371,7 @@ func (s *Service) SendNotificationEmail(ctx context.Context, recipientID int64, if user.Email == "" { return fmt.Errorf("email is invalid") } - err = s.messengerSvc.SendEmail(ctx, user.Email, 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), @@ -440,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/result/service.go b/internal/services/result/service.go index a872924..c8e69b9 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,124 @@ 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

+
    +
  • %d Ended Events
  • +
  • %d Total Bets
  • +
+

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 Unresolved Events (%d Bets)", counts.StatusNotFinishedCount, counts.StatusNotFinishedBets)) + partsHTML = append(partsHTML, + fmt.Sprintf("
  • %d Unresolved 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

    +
      +%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(partsHTML, "\n"), + totalEvents, + totalBets, + ) + + return headline, plain, html +} + + func (s *Service) SendAdminResultStatusErrorNotification( ctx context.Context, counts domain.ResultLog, @@ -541,6 +660,8 @@ func (s *Service) SendAdminResultStatusErrorNotification( } headline, message := buildHeadlineAndMessage(counts) + + notification := &domain.Notification{ ErrorSeverity: domain.NotificationErrorSeverityHigh, DeliveryStatus: domain.DeliveryStatusPending, @@ -567,9 +688,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), ) 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/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go index d7dc327..9104da4 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -131,15 +131,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/web_server/cron.go b/internal/web_server/cron.go index 53acbb1..1e0b967 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -79,7 +79,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S }, }, { - spec: "0 0 * * * *", // Every Day + 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 { From 9d3362519836fd28dc9da6fd8407d7032e46d83b Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 3 Sep 2025 22:30:24 +0300 Subject: [PATCH 17/39] fix: minor fix --- cmd/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/main.go b/cmd/main.go index fe8f4f1..ef59a25 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -140,7 +140,7 @@ 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) + resultSvc := result.NewService(store, cfg, logger, domain.MongoDBLogger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc, messengerSvc, *userSvc) bonusSvc := bonus.NewService(store) referalRepo := repository.NewReferralRepository(store) vitualGameRepo := repository.NewVirtualGameRepository(store) From 9900113e33d3de05851829c0fad544f7480c7d4b Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Sat, 6 Sep 2025 13:42:27 +0300 Subject: [PATCH 18/39] fix: result report email --- internal/services/result/service.go | 124 ++++++++++++++-------------- makefile | 2 +- 2 files changed, 62 insertions(+), 64 deletions(-) diff --git a/internal/services/result/service.go b/internal/services/result/service.go index c8e69b9..6e5075e 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -527,18 +527,18 @@ func buildHeadlineAndMessage(counts domain.ResultLog) (string, string) { } 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 + 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) + 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 + if totalIssues == 0 { + headline := "✅ Daily Results Report — All Events Processed Successfully" + plain := fmt.Sprintf(`%s Daily Results Summary: - %d Ended Events @@ -549,7 +549,7 @@ All events were processed successfully, and no issues were detected. Best regards, The System`, greeting, counts.StatusEndedCount, totalBets) - html := fmt.Sprintf(`

    %s

    + html := fmt.Sprintf(`

    %s

    Daily Results Summary

    • %d Ended Events
    • @@ -557,48 +557,48 @@ The System`, greeting, counts.StatusEndedCount, totalBets)

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

    Best regards,
    The System

    `, - greeting, counts.StatusEndedCount, totalBets) + greeting, counts.StatusEndedCount, totalBets) - return headline, plain, html - } + return headline, plain, html + } - partsPlain := []string{} - partsHTML := []string{} + partsPlain := []string{} + partsHTML := []string{} - if counts.StatusNotFinishedCount > 0 { - partsPlain = append(partsPlain, - fmt.Sprintf("- %d Unresolved Events (%d Bets)", counts.StatusNotFinishedCount, counts.StatusNotFinishedBets)) - partsHTML = append(partsHTML, - fmt.Sprintf("
  • %d Unresolved 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)) - } + if counts.StatusNotFinishedCount > 0 { + partsPlain = append(partsPlain, + fmt.Sprintf("- %d Unresolved Events (%d Bets)", counts.StatusNotFinishedCount, counts.StatusNotFinishedBets)) + partsHTML = append(partsHTML, + fmt.Sprintf("
  • %d Unresolved 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" + headline := "⚠️ Daily Results Report — Review Required" - plain := fmt.Sprintf(`%s + plain := fmt.Sprintf(`%s Daily Results Summary: %s @@ -612,13 +612,13 @@ Some events require your attention. Please log into the admin dashboard to revie Best regards, The System`, - greeting, - strings.Join(partsPlain, "\n"), - totalEvents, - totalBets, - ) + greeting, + strings.Join(partsPlain, "\n"), + totalEvents, + totalBets, + ) - html := fmt.Sprintf(`

    %s

    + html := fmt.Sprintf(`

    %s

    Daily Results Summary

      %s @@ -628,18 +628,17 @@ The System`,
    • %d Events Processed
    • %d Total Bets
    -

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

    +

    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, - ) + greeting, + strings.Join(partsHTML, "\n"), + totalEvents, + totalBets, + ) - return headline, plain, html + return headline, plain, html } - func (s *Service) SendAdminResultStatusErrorNotification( ctx context.Context, counts domain.ResultLog, @@ -660,7 +659,6 @@ func (s *Service) SendAdminResultStatusErrorNotification( } headline, message := buildHeadlineAndMessage(counts) - notification := &domain.Notification{ ErrorSeverity: domain.NotificationErrorSeverityHigh, diff --git a/makefile b/makefile index 7c57002..e5e4fae 100644 --- a/makefile +++ b/makefile @@ -79,7 +79,7 @@ logs: @mkdir -p logs db-up: | logs @mkdir -p logs - @docker compose up -d postgres migrate mongo redis --wait migrate + @docker compose up -d postgres migrate mongo redis @docker logs fortunebet-backend-postgres-1 > logs/postgres.log 2>&1 & .PHONY: db-down db-down: From e229ac911e616143e116d345efe7a067c357b3c3 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Mon, 8 Sep 2025 17:05:09 +0300 Subject: [PATCH 19/39] fix: refactoring referral --- cmd/main.go | 2 +- db/migrations/000001_fortune.up.sql | 3 +- db/migrations/000003_referal.up.sql | 74 ++- db/query/referal.sql | 104 ++--- gen/db/auth.sql.go | 4 +- gen/db/cashier.sql.go | 16 +- gen/db/models.go | 93 +--- gen/db/referal.sql.go | 429 +++++++----------- gen/db/user.sql.go | 8 +- gen/db/wallet.sql.go | 16 +- internal/domain/referal.go | 176 ++++--- internal/domain/setting_list.go | 38 +- internal/repository/referal.go | 368 ++++++--------- internal/repository/user.go | 3 +- internal/services/referal/port.go | 3 - internal/services/referal/service.go | 387 ++++++---------- internal/services/result/service.go | 4 +- internal/web_server/app.go | 4 +- internal/web_server/cron.go | 86 ++-- internal/web_server/handlers/handlers.go | 4 +- .../web_server/handlers/referal_handlers.go | 2 + 21 files changed, 744 insertions(+), 1080 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index ef59a25..1a336b9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -146,7 +146,7 @@ func main() { vitualGameRepo := repository.NewVirtualGameRepository(store) recommendationRepo := repository.NewRecommendationRepository(store) - referalSvc := referralservice.New(referalRepo, *walletSvc, store, cfg, logger) + referalSvc := referralservice.New(referalRepo, *walletSvc, *settingSvc, cfg, logger) virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger) aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger) veliCLient := veli.NewClient(cfg, walletSvc) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 898d587..ed75d50 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -193,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, diff --git a/db/migrations/000003_referal.up.sql b/db/migrations/000003_referal.up.sql index dc51f2a..c6cbd92 100644 --- a/db/migrations/000003_referal.up.sql +++ b/db/migrations/000003_referal.up.sql @@ -1,48 +1,40 @@ -CREATE TYPE ReferralStatus AS ENUM ('PENDING', 'COMPLETED', 'EXPIRED', 'CANCELLED'); -CREATE TABLE IF NOT EXISTS referral_settings ( +-- 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_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 ( - id BIGSERIAL PRIMARY KEY, - company_id BIGINT NOT NULL REFERENCES companies (id) ON -DELETE CASCADE, referral_code VARCHAR(10) NOT NULL UNIQUE, - referrer_id BIGINT NOT NULL REFERENCES users (id), - referred_id BIGINT UNIQUE REFERENCES users (id), - 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 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 +); diff --git a/db/query/referal.sql b/db/query/referal.sql index 6c60722..10e5781 100644 --- a/db/query/referal.sql +++ b/db/query/referal.sql @@ -1,87 +1,51 @@ --- name: CreateReferral :one -INSERT INTO referrals ( +-- name: CreateReferralCode :one +INSERT INTO referral_codes ( referral_code, referrer_id, company_id, - status, - reward_amount, - expires_at + number_of_referrals, + reward_amount ) -VALUES ($1, $2, $3, $4, $5, $6) +VALUES ($1, $2, $3, $4, $5) RETURNING *; --- name: GetReferralByCode :one +-- 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: GetReferralCode :one SELECT * -FROM referrals +FROM referral_codes WHERE referral_code = $1; --- name: UpdateReferral :one -UPDATE referrals -SET referred_id = $2, - status = $3, - updated_at = CURRENT_TIMESTAMP -WHERE id = $1 -RETURNING *; -- name: UpdateReferralCode :exec -UPDATE users -SET referral_code = $2, +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, - 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 + SUM(reward_amount) 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: GetReferralSettings :one +-- name: GetUserReferral :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 +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 referrals +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/gen/db/auth.sql.go b/gen/db/auth.sql.go index 1817514..7d8d59d 100644 --- a/gen/db/auth.sql.go +++ b/gen/db/auth.sql.go @@ -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/cashier.sql.go b/gen/db/cashier.sql.go index c15f497..fc4a7f8 100644 --- a/gen/db/cashier.sql.go +++ b/gen/db/cashier.sql.go @@ -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/models.go b/gen/db/models.go index e41c659..3af1538 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -5,56 +5,9 @@ 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"` @@ -538,31 +491,16 @@ type Otp struct { ExpiresAt pgtype.Timestamptz `json:"expires_at"` } -type Referral struct { - ID int64 `json:"id"` - CompanyID int64 `json:"company_id"` - ReferralCode string `json:"referral_code"` - ReferrerID int64 `json:"referrer_id"` - ReferredID pgtype.Int8 `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 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 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 { @@ -795,8 +733,6 @@ 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 UserGameInteraction struct { @@ -809,6 +745,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"` @@ -897,8 +840,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/referal.sql.go b/gen/db/referal.sql.go index 6db003a..621692d 100644 --- a/gen/db/referal.sql.go +++ b/gen/db/referal.sql.go @@ -7,237 +7,138 @@ package dbgen import ( "context" - - "github.com/jackc/pgx/v5/pgtype" ) -const CreateReferral = `-- name: CreateReferral :one -INSERT INTO referrals ( +const CreateReferralCode = `-- name: CreateReferralCode :one +INSERT INTO referral_codes ( referral_code, referrer_id, company_id, - status, - reward_amount, - expires_at + number_of_referrals, + reward_amount ) -VALUES ($1, $2, $3, $4, $5, $6) -RETURNING id, company_id, referral_code, referrer_id, referred_id, status, reward_amount, cashback_amount, created_at, updated_at, expires_at +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 int64 `json:"referrer_id"` - CompanyID int64 `json:"company_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.CompanyID, - arg.Status, + arg.NumberOfReferrals, arg.RewardAmount, - arg.ExpiresAt, ) - var i Referral + var i ReferralCode err := row.Scan( &i.ID, - &i.CompanyID, &i.ReferralCode, &i.ReferrerID, - &i.ReferredID, - &i.Status, - &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 -` - -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"` -} - -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 - 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, company_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 int64) (Referral, error) { - row := q.db.QueryRow(ctx, GetActiveReferralByReferrerID, referrerID) - var i Referral - err := row.Scan( - &i.ID, &i.CompanyID, - &i.ReferralCode, - &i.ReferrerID, - &i.ReferredID, - &i.Status, + &i.IsActive, + &i.NumberOfReferrals, &i.RewardAmount, - &i.CashbackAmount, &i.CreatedAt, &i.UpdatedAt, - &i.ExpiresAt, ) return i, err } -const GetReferralByCode = `-- name: GetReferralByCode :one -SELECT id, company_id, referral_code, referrer_id, referred_id, status, reward_amount, cashback_amount, created_at, updated_at, expires_at -FROM referrals +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 CreateUserReferralParams struct { + ReferredID int64 `json:"referred_id"` + ReferralCodeID int64 `json:"referral_code_id"` +} + +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.ReferredID, + &i.ReferralCodeID, + &i.CreatedAt, + ) + return i, err +} + +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.CompanyID, &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, company_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 -` - -func (q *Queries) GetReferralByReferredID(ctx context.Context, referredID pgtype.Int8) (Referral, error) { - row := q.db.QueryRow(ctx, GetReferralByReferredID, referredID) - var i Referral - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.ReferralCode, - &i.ReferrerID, - &i.ReferredID, - &i.Status, - &i.RewardAmount, - &i.CashbackAmount, - &i.CreatedAt, - &i.UpdatedAt, - &i.ExpiresAt, - ) - return i, err -} - -const GetReferralCountByID = `-- name: GetReferralCountByID :one -SELECT COUNT(*) -FROM referrals +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) GetReferralCountByID(ctx context.Context, referrerID int64) (int64, error) { - row := q.db.QueryRow(ctx, GetReferralCountByID, 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 +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, - 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 + SUM(reward_amount) 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 ` @@ -248,120 +149,106 @@ type GetReferralStatsParams struct { } type GetReferralStatsRow struct { - TotalReferrals int64 `json:"total_referrals"` - CompletedReferrals int64 `json:"completed_referrals"` - TotalRewardEarned interface{} `json:"total_reward_earned"` - PendingRewards interface{} `json:"pending_rewards"` + 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.CompletedReferrals, - &i.TotalRewardEarned, - &i.PendingRewards, - ) + err := row.Scan(&i.TotalReferrals, &i.TotalRewardEarned) 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, company_id, referral_code, referrer_id, referred_id, status, reward_amount, cashback_amount, created_at, updated_at, expires_at +const GetUserReferral = `-- name: GetUserReferral :one +SELECT id, referred_id, referral_code_id, created_at +FROM user_referrals +WHERE referred_id = $1 ` -type UpdateReferralParams struct { - ID int64 `json:"id"` - ReferredID pgtype.Int8 `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 +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.CompanyID, - &i.ReferralCode, - &i.ReferrerID, &i.ReferredID, - &i.Status, - &i.RewardAmount, - &i.CashbackAmount, + &i.ReferralCodeID, &i.CreatedAt, - &i.UpdatedAt, - &i.ExpiresAt, ) return i, err } +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) 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 UpdateReferralCode = `-- name: UpdateReferralCode :exec -UPDATE users -SET referral_code = $2, +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/user.sql.go b/gen/db/user.sql.go index 43d9156..999f169 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -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 } diff --git a/gen/db/wallet.sql.go b/gen/db/wallet.sql.go index 7e3eb7f..fcde631 100644 --- a/gen/db/wallet.sql.go +++ b/gen/db/wallet.sql.go @@ -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/referal.go b/internal/domain/referal.go index b8f61bf..5e384f1 100644 --- a/internal/domain/referal.go +++ b/internal/domain/referal.go @@ -1,74 +1,138 @@ 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 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 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 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"` +func ConvertDBReferralCode(code dbgen.ReferralCode) ReferralCode { + return ReferralCode{ + ID: code.ID, + ReferrerID: code.ReferrerID, + ReferralCode: code.ReferralCode, + CompanyID: code.CompanyID, + CreatedAt: code.CreatedAt.Time, + UpdatedAt: code.UpdatedAt.Time, + } } -type Referral struct { - ID int64 - ReferralCode string - ReferrerID int64 - CompanyID int64 - ReferredID *int64 - Status ReferralStatus - RewardAmount float64 - CashbackAmount float64 - CreatedAt time.Time - UpdatedAt time.Time - ExpiresAt time.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), + } } diff --git a/internal/domain/setting_list.go b/internal/domain/setting_list.go index 52ae2a4..96ffe1a 100644 --- a/internal/domain/setting_list.go +++ b/internal/domain/setting_list.go @@ -24,6 +24,9 @@ type SettingList struct { 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"` } type SettingListRes struct { @@ -35,6 +38,9 @@ type SettingListRes struct { 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"` } func ConvertSettingListRes(settings SettingList) SettingListRes { @@ -47,6 +53,9 @@ func ConvertSettingListRes(settings SettingList) SettingListRes { AmountForBetReferral: settings.AmountForBetReferral.Float32(), CashbackAmountCap: settings.CashbackAmountCap.Float32(), DefaultWinningLimit: settings.DefaultWinningLimit, + ReferralRewardAmount: settings.ReferralRewardAmount.Float32(), + CashbackPercentage: settings.CashbackPercentage, + DefaultMaxReferrals: settings.DefaultMaxReferrals, } } @@ -59,6 +68,9 @@ type SaveSettingListReq struct { 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"` } type ValidSettingList struct { @@ -70,6 +82,9 @@ type ValidSettingList struct { AmountForBetReferral ValidCurrency CashbackAmountCap ValidCurrency DefaultWinningLimit ValidInt64 + ReferralRewardAmount ValidCurrency + CashbackPercentage ValidFloat32 + DefaultMaxReferrals ValidInt64 } func ConvertSaveSettingListReq(settings SaveSettingListReq) ValidSettingList { @@ -82,6 +97,9 @@ func ConvertSaveSettingListReq(settings SaveSettingListReq) ValidSettingList { AmountForBetReferral: ConvertFloat32PtrToCurrency(settings.AmountForBetReferral), CashbackAmountCap: ConvertFloat32PtrToCurrency(settings.CashbackAmountCap), DefaultWinningLimit: ConvertInt64Ptr(settings.DefaultWinningLimit), + ReferralRewardAmount: ConvertFloat32PtrToCurrency(settings.ReferralRewardAmount), + CashbackPercentage: ConvertFloat32Ptr(settings.CashbackPercentage), + DefaultMaxReferrals: ConvertInt64Ptr(settings.DefaultMaxReferrals), } } @@ -90,12 +108,15 @@ func (vsl *ValidSettingList) ToSettingList() SettingList { return SettingList{ SMSProvider: SMSProvider(vsl.SMSProvider.Value), MaxNumberOfOutcomes: vsl.MaxNumberOfOutcomes.Value, - BetAmountLimit: Currency(vsl.BetAmountLimit.Value), + BetAmountLimit: vsl.BetAmountLimit.Value, DailyTicketPerIP: vsl.DailyTicketPerIP.Value, - TotalWinningLimit: Currency(vsl.TotalWinningLimit.Value), - AmountForBetReferral: Currency(vsl.AmountForBetReferral.Value), - CashbackAmountCap: Currency(vsl.CashbackAmountCap.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, } } @@ -112,6 +133,7 @@ func (vsl *ValidSettingList) GetInt64SettingsMap() map[string]*ValidInt64 { "max_number_of_outcomes": &vsl.MaxNumberOfOutcomes, "daily_ticket_limit": &vsl.DailyTicketPerIP, "default_winning_limit": &vsl.DefaultWinningLimit, + "default_max_referrals": &vsl.DefaultMaxReferrals, } } @@ -121,6 +143,7 @@ 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, } } @@ -135,13 +158,18 @@ func (vsl *ValidSettingList) GetBoolSettingsMap() map[string]*ValidBool { } func (vsl *ValidSettingList) GetFloat32SettingsMap() map[string]*ValidFloat32 { - return map[string]*ValidFloat32{} + return map[string]*ValidFloat32{ + "cashback_percentage": &vsl.CashbackPercentage, + } } 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()) + diff --git a/internal/repository/referal.go b/internal/repository/referal.go index 8b44c8a..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 int64, companyID int64) (*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 int64) (*domain.Referral, error) - GetReferralCountByID(ctx context.Context, referrerID int64) (int64, error) - GetActiveReferralByReferrerID(ctx context.Context, referrerID int64) (*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,252 +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) + if err != nil { + return domain.ReferralCode{}, err + } + return domain.ConvertDBReferralCode(newReferralCode), nil } -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 { +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 + } + + return domain.ConvertDBUserReferral(newReferral), nil +} + +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.ConvertDBReferralCodes(codes), nil +} + +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 } - params := dbgen.CreateReferralParams{ - ReferralCode: referral.ReferralCode, - ReferrerID: referral.ReferrerID, - Status: dbgen.Referralstatus(referral.Status), - RewardAmount: rewardAmount, - ExpiresAt: pgtype.Timestamptz{Time: referral.ExpiresAt, Valid: true}, - CompanyID: referral.CompanyID, - } - - _, err := r.store.queries.CreateReferral(ctx, params) - return err + return nil } -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 r.mapToDomainReferral(&dbReferral), nil -} - -func (r *ReferralRepo) UpdateReferral(ctx context.Context, referral *domain.Referral) error { - var referredID pgtype.Int8 - if referral.ReferredID != nil { - referredID = pgtype.Int8{Int64: *referral.ReferredID, Valid: true} - } - - params := dbgen.UpdateReferralParams{ - ID: referral.ID, - ReferredID: referredID, - Status: dbgen.Referralstatus(referral.Status), - } - - _, err := r.store.queries.UpdateReferral(ctx, params) - return err -} - -func (r *ReferralRepo) GetReferralStats(ctx context.Context, userID int64, companyID int64) (*domain.ReferralStats, error) { +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 { 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.ConvertDBUserReferrals(dbReferrals), nil } -func (r *ReferralRepo) GetSettings(ctx context.Context) (*domain.ReferralSettings, error) { - settings, err := r.store.queries.GetReferralSettings(ctx) +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.mapToDomainSettings(&settings), 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 int64) (*domain.Referral, error) { - dbReferral, err := r.store.queries.GetReferralByReferredID(ctx, pgtype.Int8{Int64: referredID, Valid: true}) - 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 int64) (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 int64) (*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 *int64 - if dbRef.ReferredID.Valid { - referredID = &dbRef.ReferredID.Int64 - } +// 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/user.go b/internal/repository/user.go index 0c72347..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 diff --git a/internal/services/referal/port.go b/internal/services/referal/port.go index 1b2278a..6930c0e 100644 --- a/internal/services/referal/port.go +++ b/internal/services/referal/port.go @@ -13,8 +13,5 @@ type ReferralStore interface { 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) - 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 int64) (int64, error) } diff --git a/internal/services/referal/service.go b/internal/services/referal/service.go index 159d494..a3b2bf5 100644 --- a/internal/services/referal/service.go +++ b/internal/services/referal/service.go @@ -7,38 +7,38 @@ 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" ) 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 } -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) *Service { return &Service{ - repo: repo, - walletSvc: walletSvc, - store: store, - config: cfg, - logger: logger, + repo: repo, + walletSvc: walletSvc, + settingSvc: settingSvc, + config: cfg, + logger: logger, } } 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) { @@ -52,274 +52,100 @@ func (s *Service) GenerateReferralCode() (string, error) { return code, nil } -func (s *Service) CreateReferral(ctx context.Context, userID int64, companyID int64) error { +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.logger.Error("Failed to fetch settings", "error", err) + return domain.ReferralCode{}, err + } + s.logger.Info("Creating referral code for user", "userID", userID) // check if user already has an active referral code - referral, err := s.repo.GetActiveReferralByReferrerID(ctx, 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 + return domain.ReferralCode{}, err } - if referral != nil && referral.Status == domain.ReferralPending && referral.ExpiresAt.After(time.Now()) { + if referralCodes != nil { 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, 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 + 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: userID, - Status: domain.ReferralPending, - RewardAmount: rewardAmount, - ExpiresAt: expireDuration, - CompanyID: companyID, - }); 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) +func (s *Service) ProcessReferral(ctx context.Context, referredID int64, referralCode string, companyID int64) error { + s.logger.Info("Processing referral", "referralCode", referralCode) - referral, err := s.repo.GetReferralByCode(ctx, referralCode) - if err != nil || referral == nil { + referral, err := s.repo.GetReferralCode(ctx, referralCode) + + if err != 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, - }) - 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 = &user.ID - 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) - return err - } - - wallets, err := s.store.GetCustomerWallet(ctx, referral.ReferrerID) + wallets, err := s.walletSvc.GetCustomerWallet(ctx, referral.ReferrerID) if err != nil { s.logger.Error("Failed to get referrer wallets", "referrerId", referral.ReferrerID, "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, referral.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) + s.logger.Error("Failed to add referral reward to static wallet", "walletID", wallets.StaticID, "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 { + s.logger.Error("Failed to add referral reward to static wallet", "referredID", referredID, "error", err) + return err + } + + s.logger.Info("Referral processed successfully", "referralCode", referralCode, "rewardAmount", referral.RewardAmount) return nil } -func (s *Service) ProcessDepositBonus(ctx context.Context, userPhone string, amount float64) error { - s.logger.Info("Processing deposit bonus", "userPhone", userPhone, "amount", amount) - - settings, err := s.repo.GetSettings(ctx) - if err != nil { - s.logger.Error("Failed to get referral settings", "error", err) - return 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, 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 -} - -func (s *Service) GetReferralStats(ctx context.Context, userID int64, companyID int64) (*domain.ReferralStats, error) { - s.logger.Info("Fetching referral stats", "userID", userID) - +func (s *Service) GetReferralStats(ctx context.Context, userID int64, companyID int64) (domain.ReferralStats, error) { stats, err := s.repo.GetReferralStats(ctx, userID, companyID) if err != nil { s.logger.Error("Failed to get referral stats", "userID", userID, "error", err) - return nil, err + return domain.ReferralStats{}, err } s.logger.Info("Referral stats retrieved successfully", "userID", userID, "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) - 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 int64) (int64, error) { - count, err := s.repo.GetReferralCountByID(ctx, referrerID) +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 get referral count", "userID", referrerID, "error", err) return 0, err @@ -327,3 +153,88 @@ func (s *Service) GetReferralCountByID(ctx context.Context, referrerID int64) (i return count, nil } + + + + + + +// 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 6e5075e..4ae5bf3 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -567,9 +567,9 @@ The System`, greeting, counts.StatusEndedCount, totalBets) if counts.StatusNotFinishedCount > 0 { partsPlain = append(partsPlain, - fmt.Sprintf("- %d Unresolved Events (%d Bets)", counts.StatusNotFinishedCount, counts.StatusNotFinishedBets)) + fmt.Sprintf("- %d Incomplete Events (%d Bets)", counts.StatusNotFinishedCount, counts.StatusNotFinishedBets)) partsHTML = append(partsHTML, - fmt.Sprintf("
  • %d Unresolved Events (%d Bets)
  • ", counts.StatusNotFinishedCount, counts.StatusNotFinishedBets)) + fmt.Sprintf("
  • %d Incomplete Events (%d Bets)
  • ", counts.StatusNotFinishedCount, counts.StatusNotFinishedBets)) } if counts.StatusToBeFixedCount > 0 { partsPlain = append(partsPlain, diff --git a/internal/web_server/app.go b/internal/web_server/app.go index 02e84e1..2517d71 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -56,7 +56,7 @@ type App struct { cfg *config.Config logger *slog.Logger NotidicationStore *notificationservice.Service - referralSvc referralservice.ReferralStore + referralSvc *referralservice.Service bonusSvc *bonus.Service port int settingSvc *settings.Service @@ -107,7 +107,7 @@ func NewApp( prematchSvc *odds.ServiceImpl, eventSvc event.Service, leagueSvc league.Service, - referralSvc referralservice.ReferralStore, + referralSvc *referralservice.Service, bonusSvc *bonus.Service, virtualGameSvc virtualgameservice.VirtualGameService, aleaVirtualGameService alea.AleaVirtualGameService, diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 1e0b967..1bebede 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() { @@ -78,19 +78,19 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S } }, }, - { - 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 { - mongoLogger.Error("Failed to process result", - zap.Error(err), - ) - } else { - mongoLogger.Info("Completed sending daily result notification 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 { + // mongoLogger.Error("Failed to process result", + // zap.Error(err), + // ) + // } else { + // mongoLogger.Info("Completed sending daily result notification without errors") + // } + // }, + // }, } for _, job := range schedule { @@ -154,10 +154,10 @@ func SetupReportandVirtualGameCronJobs( spec string period string }{ - { - spec: "*/60 * * * * *", // Every 1 minute 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/handlers.go b/internal/web_server/handlers/handlers.go index a59dbc9..3a425ee 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -48,7 +48,7 @@ type Handler struct { settingSvc *settings.Service notificationSvc *notificationservice.Service userSvc *user.Service - referralSvc referralservice.ReferralStore + referralSvc *referralservice.Service bonusSvc *bonus.Service reportSvc report.ReportStore chapaSvc *chapa.Service @@ -87,7 +87,7 @@ func New( reportSvc report.ReportStore, chapaSvc *chapa.Service, walletSvc *wallet.Service, - referralSvc referralservice.ReferralStore, + referralSvc *referralservice.Service, bonusSvc *bonus.Service, virtualGameSvc virtualgameservice.VirtualGameService, aleaVirtualGameSvc alea.AleaVirtualGameService, diff --git a/internal/web_server/handlers/referal_handlers.go b/internal/web_server/handlers/referal_handlers.go index 3105a4a..ceca245 100644 --- a/internal/web_server/handlers/referal_handlers.go +++ b/internal/web_server/handlers/referal_handlers.go @@ -11,6 +11,7 @@ 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") @@ -36,6 +37,7 @@ func (h *Handler) CreateReferralCode(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create referral") } + fmt.Printf("Successfully created referral!") return response.WriteJSON(c, fiber.StatusOK, "Referral created successfully", nil, nil) } From 895c652ae0252b8b6f8706f347c63ea78f113985 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Mon, 8 Sep 2025 17:08:39 +0300 Subject: [PATCH 20/39] fix: commenting out providers check --- internal/services/virtualGame/veli/service.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/services/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go index 9104da4..d7dc327 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -131,15 +131,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{ From 3313c3833bb5d425414f44684cc5328c23c18e2a Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Tue, 9 Sep 2025 19:19:35 +0300 Subject: [PATCH 21/39] fix: more referral fixes --- internal/services/referal/service.go | 6 -- .../web_server/handlers/referal_handlers.go | 63 +------------------ 2 files changed, 3 insertions(+), 66 deletions(-) diff --git a/internal/services/referal/service.go b/internal/services/referal/service.go index a3b2bf5..2dc8a73 100644 --- a/internal/services/referal/service.go +++ b/internal/services/referal/service.go @@ -154,11 +154,6 @@ func (s *Service) GetUserReferralCount(ctx context.Context, referrerID int64) (i return count, nil } - - - - - // func (s *Service) ProcessDepositBonus(ctx context.Context, userID int64, amount float32, companyID int64) error { // settingsList, err := s.settingSvc.GetOverrideSettingsList(ctx, companyID) // if err != nil { @@ -237,4 +232,3 @@ func (s *Service) GetUserReferralCount(ctx context.Context, referrerID int64) (i // s.logger.Info("Bet referral processed successfully", "referrer ID", referral.ReferrerID, "referrerID", referral.ReferrerID, "bonus", bonus) // return nil // } - diff --git a/internal/web_server/handlers/referal_handlers.go b/internal/web_server/handlers/referal_handlers.go index ceca245..7683466 100644 --- a/internal/web_server/handlers/referal_handlers.go +++ b/internal/web_server/handlers/referal_handlers.go @@ -26,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, companyID.Value); err != nil { + if err != nil { h.mongoLoggerSvc.Error("Failed to create referral", zap.Int64("userID", userID), zap.Int("status_code", fiber.StatusInternalServerError), @@ -38,65 +39,7 @@ func (h *Handler) CreateReferralCode(c *fiber.Ctx) error { } fmt.Printf("Successfully created referral!") - return response.WriteJSON(c, fiber.StatusOK, "Referral created successfully", nil, 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") - } - - 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) + return response.WriteJSON(c, fiber.StatusOK, "Referral created successfully", referralCode, nil) } // func (h *Handler) GetReferralCode(c *fiber.Ctx) error { From 5c482b4d62c0bc18968203791692bb37cf0190e6 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Tue, 9 Sep 2025 19:21:43 +0300 Subject: [PATCH 22/39] fix: remove auth from veli games --- internal/web_server/routes.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 9e68e79..21803b5 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -357,8 +357,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) From 2ac66308d3422ddcc847ed47e6352039f6e223dd Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Tue, 9 Sep 2025 19:25:08 +0300 Subject: [PATCH 23/39] fix: merge fixes --- gen/db/auth.sql.go | 2 +- gen/db/bet.sql.go | 2 +- gen/db/bet_stat.sql.go | 2 +- gen/db/bonus.sql.go | 2 +- gen/db/branch.sql.go | 2 +- gen/db/cashier.sql.go | 2 +- gen/db/company.sql.go | 2 +- gen/db/copyfrom.go | 2 +- gen/db/db.go | 2 +- gen/db/direct_deposit.sql.go | 2 +- gen/db/disabled_odds.sql.go | 2 +- gen/db/event_history.sql.go | 2 +- gen/db/events.sql.go | 2 +- gen/db/events_stat.sql.go | 2 +- gen/db/flags.sql.go | 2 +- gen/db/institutions.sql.go | 2 +- gen/db/issue_reporting.sql.go | 2 +- gen/db/leagues.sql.go | 2 +- gen/db/location.sql.go | 2 +- gen/db/models.go | 20 +------------------- gen/db/monitor.sql.go | 2 +- gen/db/notification.sql.go | 2 +- gen/db/odd_history.sql.go | 2 +- gen/db/odds.sql.go | 2 +- gen/db/otp.sql.go | 2 +- gen/db/raffle.sql.go | 2 +- gen/db/referal.sql.go | 2 +- gen/db/report.sql.go | 2 +- gen/db/result.sql.go | 2 +- gen/db/result_log.sql.go | 2 +- gen/db/settings.sql.go | 2 +- gen/db/shop_transactions.sql.go | 2 +- gen/db/ticket.sql.go | 2 +- gen/db/transfer.sql.go | 2 +- gen/db/user.sql.go | 2 +- gen/db/virtual_games.sql.go | 2 +- gen/db/wallet.sql.go | 2 +- 37 files changed, 37 insertions(+), 55 deletions(-) diff --git a/gen/db/auth.sql.go b/gen/db/auth.sql.go index 8dd2280..7d8d59d 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.30.0 +// sqlc v1.29.0 // source: auth.sql package dbgen diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index 573c4c2..ff64087 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.30.0 +// sqlc v1.29.0 // source: bet.sql package dbgen diff --git a/gen/db/bet_stat.sql.go b/gen/db/bet_stat.sql.go index 9a7b494..275ef07 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.30.0 +// sqlc v1.29.0 // source: bet_stat.sql package dbgen diff --git a/gen/db/bonus.sql.go b/gen/db/bonus.sql.go index f62227b..12677b8 100644 --- a/gen/db/bonus.sql.go +++ b/gen/db/bonus.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: bonus.sql package dbgen diff --git a/gen/db/branch.sql.go b/gen/db/branch.sql.go index 89d2959..a9a57b8 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.30.0 +// sqlc v1.29.0 // source: branch.sql package dbgen diff --git a/gen/db/cashier.sql.go b/gen/db/cashier.sql.go index 55e69d2..fc4a7f8 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.30.0 +// sqlc v1.29.0 // source: cashier.sql package dbgen diff --git a/gen/db/company.sql.go b/gen/db/company.sql.go index 18bc509..506eaca 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.30.0 +// sqlc v1.29.0 // source: company.sql package dbgen diff --git a/gen/db/copyfrom.go b/gen/db/copyfrom.go index f7a4793..1212253 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.30.0 +// sqlc v1.29.0 // source: copyfrom.go package dbgen diff --git a/gen/db/db.go b/gen/db/db.go index 8134784..84de07c 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.30.0 +// sqlc v1.29.0 package dbgen diff --git a/gen/db/direct_deposit.sql.go b/gen/db/direct_deposit.sql.go index ff5a3b2..be02750 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.30.0 +// sqlc v1.29.0 // source: direct_deposit.sql package dbgen diff --git a/gen/db/disabled_odds.sql.go b/gen/db/disabled_odds.sql.go index 917acce..85dcd2e 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.30.0 +// sqlc v1.29.0 // source: disabled_odds.sql package dbgen diff --git a/gen/db/event_history.sql.go b/gen/db/event_history.sql.go index 64762c3..ab29359 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.30.0 +// sqlc v1.29.0 // source: event_history.sql package dbgen diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index 1c7a0c3..3ee77ad 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.30.0 +// sqlc v1.29.0 // source: events.sql package dbgen diff --git a/gen/db/events_stat.sql.go b/gen/db/events_stat.sql.go index 615e2fa..677fa2a 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.30.0 +// sqlc v1.29.0 // source: events_stat.sql package dbgen diff --git a/gen/db/flags.sql.go b/gen/db/flags.sql.go index 4b82cac..653543f 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.30.0 +// sqlc v1.29.0 // source: flags.sql package dbgen diff --git a/gen/db/institutions.sql.go b/gen/db/institutions.sql.go index 61ca108..324ac3e 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.30.0 +// sqlc v1.29.0 // source: institutions.sql package dbgen diff --git a/gen/db/issue_reporting.sql.go b/gen/db/issue_reporting.sql.go index e35fba1..7fcb4af 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.30.0 +// sqlc v1.29.0 // source: issue_reporting.sql package dbgen diff --git a/gen/db/leagues.sql.go b/gen/db/leagues.sql.go index 1fed73e..57d3c28 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.30.0 +// sqlc v1.29.0 // source: leagues.sql package dbgen diff --git a/gen/db/location.sql.go b/gen/db/location.sql.go index 254c73a..008aa61 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.30.0 +// sqlc v1.29.0 // source: location.sql package dbgen diff --git a/gen/db/models.go b/gen/db/models.go index f49a147..53adfa3 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 package dbgen @@ -501,24 +501,6 @@ type Raffle struct { Status string `json:"status"` } -type RaffleTicket struct { - ID int32 `json:"id"` - RaffleID int32 `json:"raffle_id"` - UserID int32 `json:"user_id"` - IsActive pgtype.Bool `json:"is_active"` -} - -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 RaffleTicket struct { ID int32 `json:"id"` RaffleID int32 `json:"raffle_id"` diff --git a/gen/db/monitor.sql.go b/gen/db/monitor.sql.go index b5f248f..a9a7ecb 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.30.0 +// sqlc v1.29.0 // source: monitor.sql package dbgen diff --git a/gen/db/notification.sql.go b/gen/db/notification.sql.go index 9ce7e42..ba9882b 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.30.0 +// sqlc v1.29.0 // source: notification.sql package dbgen diff --git a/gen/db/odd_history.sql.go b/gen/db/odd_history.sql.go index dd69a51..0a0333d 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.30.0 +// sqlc v1.29.0 // source: odd_history.sql package dbgen diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index d194d14..79da894 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.30.0 +// sqlc v1.29.0 // source: odds.sql package dbgen diff --git a/gen/db/otp.sql.go b/gen/db/otp.sql.go index c96aaaa..7dba175 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.30.0 +// sqlc v1.29.0 // source: otp.sql package dbgen diff --git a/gen/db/raffle.sql.go b/gen/db/raffle.sql.go index 4b77106..b5705fb 100644 --- a/gen/db/raffle.sql.go +++ b/gen/db/raffle.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: raffle.sql package dbgen diff --git a/gen/db/referal.sql.go b/gen/db/referal.sql.go index 250f782..621692d 100644 --- a/gen/db/referal.sql.go +++ b/gen/db/referal.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: referal.sql package dbgen diff --git a/gen/db/report.sql.go b/gen/db/report.sql.go index d6193c1..1a1ccde 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.30.0 +// sqlc v1.29.0 // source: report.sql package dbgen diff --git a/gen/db/result.sql.go b/gen/db/result.sql.go index 899561b..bff7b1e 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.30.0 +// sqlc v1.29.0 // source: result.sql package dbgen diff --git a/gen/db/result_log.sql.go b/gen/db/result_log.sql.go index 3f11e16..468795e 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.30.0 +// sqlc v1.29.0 // source: result_log.sql package dbgen diff --git a/gen/db/settings.sql.go b/gen/db/settings.sql.go index eb50e0e..f67fecc 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.30.0 +// sqlc v1.29.0 // source: settings.sql package dbgen diff --git a/gen/db/shop_transactions.sql.go b/gen/db/shop_transactions.sql.go index 7664dbb..bcd884e 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.30.0 +// sqlc v1.29.0 // source: shop_transactions.sql package dbgen diff --git a/gen/db/ticket.sql.go b/gen/db/ticket.sql.go index 45603ba..bc9bb5f 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.30.0 +// sqlc v1.29.0 // source: ticket.sql package dbgen diff --git a/gen/db/transfer.sql.go b/gen/db/transfer.sql.go index 926fc8c..35e38d4 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.30.0 +// sqlc v1.29.0 // source: transfer.sql package dbgen diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index 9f9cd95..999f169 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.30.0 +// sqlc v1.29.0 // source: user.sql package dbgen diff --git a/gen/db/virtual_games.sql.go b/gen/db/virtual_games.sql.go index cef2965..33697d7 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.30.0 +// sqlc v1.29.0 // source: virtual_games.sql package dbgen diff --git a/gen/db/wallet.sql.go b/gen/db/wallet.sql.go index ccb2d37..fcde631 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.30.0 +// sqlc v1.29.0 // source: wallet.sql package dbgen From 5595600ede1627eebea93fa7c482674a30336d11 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Tue, 9 Sep 2025 23:44:02 +0300 Subject: [PATCH 24/39] fix: referral handlers and integration issues --- cmd/main.go | 2 +- db/data/001_initial_seed_data.sql | 10 +- db/data/003_fix_autoincrement_desync.sql | 31 ++ db/migrations/000003_referal.up.sql | 7 +- db/query/referal.sql | 8 +- gen/db/referal.sql.go | 8 +- internal/domain/referal.go | 64 +++- internal/services/referal/service.go | 86 +++-- internal/services/wallet/wallet.go | 2 + .../web_server/handlers/branch_handler.go | 2 +- .../web_server/handlers/company_handler.go | 1 + .../web_server/handlers/referal_handlers.go | 295 +++++++++--------- internal/web_server/handlers/user.go | 2 +- internal/web_server/routes.go | 8 +- 14 files changed, 322 insertions(+), 204 deletions(-) create mode 100644 db/data/003_fix_autoincrement_desync.sql diff --git a/cmd/main.go b/cmd/main.go index 29425b3..1019bb5 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -147,7 +147,7 @@ func main() { vitualGameRepo := repository.NewVirtualGameRepository(store) recommendationRepo := repository.NewRecommendationRepository(store) - referalSvc := referralservice.New(referalRepo, *walletSvc, *settingSvc, 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) diff --git a/db/data/001_initial_seed_data.sql b/db/data/001_initial_seed_data.sql index eb64f5c..0441221 100644 --- a/db/data/001_initial_seed_data.sql +++ b/db/data/001_initial_seed_data.sql @@ -83,7 +83,10 @@ VALUES ('sms_provider', 'afro_message'), ('total_winnings_limit', '1000000'), ('amount_for_bet_referral', '1000000'), ('cashback_amount_cap', '1000'), - ('default_winning_limit', '5000000') ON CONFLICT (key) DO NOTHING; + ('default_winning_limit', '5000000'), + ('referral_reward_amount', '10000'), + ('cashback_percentage', '0.2'), + ('default_max_referrals', '15') ON CONFLICT (key) DO NOTHING; -- Users INSERT INTO users ( id, @@ -222,7 +225,7 @@ VALUES ( ), ( 3, - 100000000 , + 100000000, TRUE, TRUE, TRUE, @@ -338,4 +341,5 @@ SET name = EXCLUDED.name, profit_percent = EXCLUDED.profit_percent, is_active = EXCLUDED.is_active, created_at = EXCLUDED.created_at, - updated_at = EXCLUDED.updated_at; \ No newline at end of file + updated_at = EXCLUDED.updated_at; + 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/migrations/000003_referal.up.sql b/db/migrations/000003_referal.up.sql index c6cbd92..badf443 100644 --- a/db/migrations/000003_referal.up.sql +++ b/db/migrations/000003_referal.up.sql @@ -16,7 +16,6 @@ -- AND cashback_percentage <= 100 -- ) -- ); - CREATE TABLE IF NOT EXISTS referral_codes ( id BIGSERIAL PRIMARY KEY, referral_code VARCHAR(10) NOT NULL UNIQUE, @@ -29,12 +28,10 @@ CREATE TABLE IF NOT EXISTS referral_codes ( updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT reward_amount_positive CHECK (reward_amount >= 0) ); -CREATE INDEX idx_referrals_referrer_id ON referrals (referrer_id); -CREATE INDEX idx_referrals_status ON referrals (status); - +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/query/referal.sql b/db/query/referal.sql index 10e5781..3dbe00e 100644 --- a/db/query/referal.sql +++ b/db/query/referal.sql @@ -30,9 +30,9 @@ SET is_active = $2, WHERE id = $1; -- name: GetReferralStats :one SELECT COUNT(*) AS total_referrals, - SUM(reward_amount) AS total_reward_earned + COALESCE(SUM(reward_amount), 0)::bigint AS total_reward_earned FROM user_referrals - JOIN referral_codes ON referral_codes.id == referral_code_id + JOIN referral_codes ON referral_codes.id = referral_code_id WHERE referrer_id = $1 AND company_id = $2; -- name: GetUserReferral :one @@ -42,10 +42,10 @@ WHERE referred_id = $1; -- name: GetUserReferralsByCode :many SELECT user_referrals.* FROM user_referrals - JOIN referral_codes ON referral_codes.id == referral_code_id + 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 + JOIN referral_codes ON referral_codes.id = referral_code_id WHERE referrer_id = $1; \ No newline at end of file diff --git a/gen/db/referal.sql.go b/gen/db/referal.sql.go index 621692d..caaa01a 100644 --- a/gen/db/referal.sql.go +++ b/gen/db/referal.sql.go @@ -136,9 +136,9 @@ func (q *Queries) GetReferralCodeByUser(ctx context.Context, referrerID int64) ( const GetReferralStats = `-- name: GetReferralStats :one SELECT COUNT(*) AS total_referrals, - SUM(reward_amount) AS total_reward_earned + COALESCE(SUM(reward_amount), 0)::bigint AS total_reward_earned FROM user_referrals - JOIN referral_codes ON referral_codes.id == referral_code_id + JOIN referral_codes ON referral_codes.id = referral_code_id WHERE referrer_id = $1 AND company_id = $2 ` @@ -181,7 +181,7 @@ func (q *Queries) GetUserReferral(ctx context.Context, referredID int64) (UserRe 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 + JOIN referral_codes ON referral_codes.id = referral_code_id WHERE referral_code = $1 ` @@ -213,7 +213,7 @@ func (q *Queries) GetUserReferralsByCode(ctx context.Context, referralCode strin const GetUserReferralsCount = `-- name: GetUserReferralsCount :one SELECT COUNT(*) FROM user_referrals - JOIN referral_codes ON referral_codes.id == referral_code_id + JOIN referral_codes ON referral_codes.id = referral_code_id WHERE referrer_id = $1 ` diff --git a/internal/domain/referal.go b/internal/domain/referal.go index 5e384f1..bb9e1bb 100644 --- a/internal/domain/referal.go +++ b/internal/domain/referal.go @@ -17,6 +17,17 @@ type ReferralCode struct { UpdatedAt time.Time } +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 @@ -48,6 +59,11 @@ type ReferralStats struct { TotalRewardEarned Currency } +type ReferralStatsRes struct { + TotalReferrals int64 `json:"total_referrals"` + TotalRewardEarned float32 `json:"total_reward_earned"` +} + // type ReferralSettings struct { // ID int64 // ReferralRewardAmount float64 @@ -78,14 +94,16 @@ func ConvertCreateReferralCode(code CreateReferralCode) dbgen.CreateReferralCode } } -func ConvertDBReferralCode(code dbgen.ReferralCode) ReferralCode { + func ConvertDBReferralCode(code dbgen.ReferralCode) ReferralCode { return ReferralCode{ - ID: code.ID, - ReferrerID: code.ReferrerID, - ReferralCode: code.ReferralCode, - CompanyID: code.CompanyID, - CreatedAt: code.CreatedAt.Time, - UpdatedAt: code.UpdatedAt.Time, + 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, } } @@ -132,7 +150,37 @@ func ConvertUpdateReferralCode(referralCode UpdateReferralCode) dbgen.UpdateRefe func ConvertDBReferralStats(stats dbgen.GetReferralStatsRow) ReferralStats { return ReferralStats{ - TotalReferrals: stats.TotalReferrals, + 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/services/referal/service.go b/internal/services/referal/service.go index 2dc8a73..eb5b021 100644 --- a/internal/services/referal/service.go +++ b/internal/services/referal/service.go @@ -13,23 +13,26 @@ import ( "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 - settingSvc settings.Service - 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, settingSvc settings.Service, 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, - settingSvc: settingSvc, - config: cfg, - logger: logger, + repo: repo, + walletSvc: walletSvc, + settingSvc: settingSvc, + config: cfg, + logger: logger, + mongoLogger: mongoLogger, } } @@ -44,38 +47,36 @@ var ( 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) CreateReferralCode(ctx context.Context, userID int64, companyID int64) (domain.ReferralCode, error) { settingsList, err := s.settingSvc.GetOverrideSettingsList(ctx, companyID) + if err != nil { - s.logger.Error("Failed to fetch settings", "error", err) + s.mongoLogger.Error("Failed to fetch settings", zap.Error(err)) return domain.ReferralCode{}, err } - s.logger.Info("Creating referral code for user", "userID", userID) - // check if user already has an active referral code 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) + 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 referralCodes != nil { - s.logger.Error("user already has an active referral code", "error", 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 domain.ReferralCode{}, err } @@ -95,18 +96,20 @@ func (s *Service) CreateReferralCode(ctx context.Context, userID int64, companyI } func (s *Service) ProcessReferral(ctx context.Context, referredID int64, referralCode string, companyID int64) error { - s.logger.Info("Processing referral", "referralCode", referralCode) - + 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 { - s.logger.Error("Failed to get referral by code", "referralCode", referralCode, "error", err) + paramLogger.Error("Failed to get referral by code", zap.Error(err)) return err } wallets, err := s.walletSvc.GetCustomerWallet(ctx, referral.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 } @@ -115,7 +118,7 @@ func (s *Service) ProcessReferral(ctx context.Context, referredID int64, referra 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, "error", err) + paramLogger.Error("Failed to add referral reward to static wallet", zap.Int64("static_wallet_id", wallets.StaticID), zap.Error(err)) return err } @@ -125,35 +128,56 @@ func (s *Service) ProcessReferral(ctx context.Context, referredID int64, referra }) if err != nil { - s.logger.Error("Failed to add referral reward to static wallet", "referredID", referredID, "error", err) + paramLogger.Error("Failed to create user referral", zap.Error(err)) return err } - s.logger.Info("Referral processed successfully", "referralCode", referralCode, "rewardAmount", referral.RewardAmount) + paramLogger.Info("Referral processed successfully", zap.String("rewardAmount", referral.ReferralCode)) return nil } 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)) + stats, err := s.repo.GetReferralStats(ctx, userID, companyID) if err != nil { - s.logger.Error("Failed to get referral stats", "userID", userID, "error", err) + paramLogger.Error("Failed to get referral stats", zap.Error(err)) return domain.ReferralStats{}, err } - s.logger.Info("Referral stats retrieved successfully", "userID", userID, "totalReferrals", stats.TotalReferrals) return stats, nil } 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 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 { diff --git a/internal/services/wallet/wallet.go b/internal/services/wallet/wallet.go index df67111..5865f75 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 { diff --git a/internal/web_server/handlers/branch_handler.go b/internal/web_server/handlers/branch_handler.go index 7314ccb..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 { @@ -254,7 +255,6 @@ func (h *Handler) CreateBranchOperation(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Branch Operation Created", nil, nil) } - // GetBranchByID godoc // @Summary Gets branch by id // @Description Gets a single branch by id 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/referal_handlers.go b/internal/web_server/handlers/referal_handlers.go index 7683466..7ca3856 100644 --- a/internal/web_server/handlers/referal_handlers.go +++ b/internal/web_server/handlers/referal_handlers.go @@ -26,9 +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); + referralCode, err := h.referralSvc.CreateReferralCode(c.Context(), userID, companyID.Value) - if err != nil { + if err != nil { h.mongoLoggerSvc.Error("Failed to create referral", zap.Int64("userID", userID), zap.Int("status_code", fiber.StatusInternalServerError), @@ -39,59 +39,66 @@ func (h *Handler) CreateReferralCode(c *fiber.Ctx) error { } fmt.Printf("Successfully created referral!") - return response.WriteJSON(c, fiber.StatusOK, "Referral created successfully", referralCode, nil) + + res := domain.ConvertReferralCodeRes(referralCode) + + return response.WriteJSON(c, fiber.StatusOK, "Referral created successfully", res, nil) } -// 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") -// } -// userID, ok := c.Locals("user_id").(int64) -// if !ok || userID == 0 { -// h.mongoLoggerSvc.Error("Invalid user ID in context", -// zap.Int64("userID", userID), -// zap.Int("status_code", fiber.StatusInternalServerError), -// zap.Time("timestamp", time.Now()), -// ) -// return fiber.NewError(fiber.StatusInternalServerError, "Invalid user id") -// } +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") + } + userID, ok := c.Locals("user_id").(int64) + if !ok || userID == 0 { + h.mongoLoggerSvc.Error("Invalid user ID in context", + 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, "Failed to retrieve user") -// } + 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, "Failed to retrieve user") + } -// 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.StatusBadRequest, "Failed to retrieve user") -// } + 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.StatusBadRequest, "Failed to retrieve user") + } -// // referrals, err := h.referralSvc.GetReferralStats(c.Context(), user.ID) + 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") -// } + 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) + +} // GetReferralStats godoc // @Summary Get referral statistics @@ -152,107 +159,109 @@ func (h *Handler) GetReferralStats(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve referral stats") } - return response.WriteJSON(c, fiber.StatusOK, "Referral stats retrieved successfully", stats, nil) + 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") - } +// // 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()) - } +// 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") - } +// 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") - } +// 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()) - } +// 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) -} +// 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()) - } +// // 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) -} +// return response.WriteJSON(c, fiber.StatusOK, "Referral settings retrieved successfully", settings, nil) +// } diff --git a/internal/web_server/handlers/user.go b/internal/web_server/handlers/user.go index 81f8223..5c185fc 100644 --- a/internal/web_server/handlers/user.go +++ b/internal/web_server/handlers/user.go @@ -248,7 +248,7 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error { } if req.ReferralCode != "" { - err = h.referralSvc.ProcessReferral(c.Context(), req.PhoneNumber, req.ReferralCode, companyID.Value) + 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), diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 21803b5..4a7f4b7 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -189,10 +189,12 @@ func (a *App) initAppRoutes() { // Referral Routes 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) + + // 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 a.fiber.Post("/raffle/create", a.authMiddleware, h.CreateRaffle) From 215eb5a1d8d42a24fda5c7e3bc72cbc7b2389385 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 10 Sep 2025 23:27:11 +0300 Subject: [PATCH 25/39] fix: refactoring bonus --- cmd/main.go | 2 +- db/data/001_initial_seed_data.sql | 8 +- db/migrations/000001_fortune.up.sql | 17 +- db/query/bonus.sql | 69 +++- db/query/transfer.sql | 13 + gen/db/bonus.sql.go | 214 ++++++++--- gen/db/models.go | 19 +- gen/db/transfer.sql.go | 34 ++ internal/domain/bonus.go | 136 +++++++ internal/domain/setting_list.go | 63 ++- internal/domain/transfer.go | 7 + internal/repository/bet.go | 41 ++ internal/repository/bonus.go | 82 +++- internal/repository/transfer.go | 18 + internal/services/bet/service.go | 529 +++++++++++++------------- internal/services/bonus/port.go | 13 +- internal/services/bonus/service.go | 108 +++++- internal/services/wallet/port.go | 11 +- internal/services/wallet/transfer.go | 3 + internal/services/wallet/wallet.go | 3 + internal/web_server/handlers/bonus.go | 166 ++++---- internal/web_server/handlers/chapa.go | 54 +-- internal/web_server/routes.go | 6 +- 23 files changed, 1116 insertions(+), 500 deletions(-) create mode 100644 internal/domain/bonus.go diff --git a/cmd/main.go b/cmd/main.go index 1019bb5..2a5d805 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -142,7 +142,7 @@ func main() { 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, messengerSvc, *userSvc) - bonusSvc := bonus.NewService(store) + bonusSvc := bonus.NewService(store, walletSvc, settingSvc, domain.MongoDBLogger) referalRepo := repository.NewReferralRepository(store) vitualGameRepo := repository.NewVirtualGameRepository(store) recommendationRepo := repository.NewRecommendationRepository(store) diff --git a/db/data/001_initial_seed_data.sql b/db/data/001_initial_seed_data.sql index 0441221..55a3650 100644 --- a/db/data/001_initial_seed_data.sql +++ b/db/data/001_initial_seed_data.sql @@ -86,7 +86,10 @@ VALUES ('sms_provider', 'afro_message'), ('default_winning_limit', '5000000'), ('referral_reward_amount', '10000'), ('cashback_percentage', '0.2'), - ('default_max_referrals', '15') ON CONFLICT (key) DO NOTHING; + ('default_max_referrals', '15'), + ('minimum_bet_amount', '100'), + ('send_email_on_bet_finish', 'true'), + ('send_sms_on_bet_finish', 'false') ON CONFLICT (key) DO NOTHING; -- Users INSERT INTO users ( id, @@ -341,5 +344,4 @@ SET name = EXCLUDED.name, profit_percent = EXCLUDED.profit_percent, is_active = EXCLUDED.is_active, created_at = EXCLUDED.created_at, - updated_at = EXCLUDED.updated_at; - + updated_at = EXCLUDED.updated_at; \ No newline at end of file diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 13b3702..c6eefde 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -453,10 +453,17 @@ CREATE TABLE IF NOT EXISTS company_settings ( updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (company_id, key) ); -CREATE TABLE bonus ( - multiplier REAL NOT NULL, - id BIGSERIAL PRIMARY KEY, - balance_cap BIGINT NOT NULL DEFAULT 0 +CREATE TABLE user_bonuses ( + id BIGINT NOT NULL, + name TEXT NOT NULL, + description TEXT NOT NULL, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + bonus_code TEXT NOT NULL UNIQUE, + reward_amount BIGINT NOT NULL, + is_claimed BOOLEAN NOT NULL DEFAULT false, + expires_at TIMESTAMP NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE flags ( id BIGSERIAL PRIMARY KEY, @@ -723,4 +730,4 @@ ADD CONSTRAINT fk_event_settings_company FOREIGN KEY (company_id) REFERENCES com 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; + 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 diff --git a/db/query/bonus.sql b/db/query/bonus.sql index 82b3113..4b07761 100644 --- a/db/query/bonus.sql +++ b/db/query/bonus.sql @@ -1,17 +1,52 @@ --- name: CreateBonusMultiplier :exec -INSERT INTO bonus (multiplier, balance_cap) -VALUES ($1, $2); - --- 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: CreateUserBonus :one +INSERT INTO user_bonuses ( + name, + description, + user_id, + bonus_code, + reward_amount, + expires_at + ) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING *; +-- name: GetAllUserBonuses :many +SELECT * +FROM user_bonuses; +-- name: GetUserBonusByID :one +SELECT * +FROM user_bonuses +WHERE id = $1; +-- name: GetBonusesByUserID :many +SELECT * +FROM user_bonuses +WHERE user_id = $1; +-- 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/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/gen/db/bonus.sql.go b/gen/db/bonus.sql.go index 12677b8..fe0b99b 100644 --- a/gen/db/bonus.sql.go +++ b/gen/db/bonus.sql.go @@ -7,43 +7,93 @@ 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, + user_id, + bonus_code, + reward_amount, + expires_at + ) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING id, name, description, user_id, bonus_code, reward_amount, is_claimed, expires_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"` + UserID int64 `json:"user_id"` + BonusCode string `json:"bonus_code"` + 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.UserID, + arg.BonusCode, + arg.RewardAmount, + arg.ExpiresAt, + ) + var i UserBonuse + err := row.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.UserID, + &i.BonusCode, + &i.RewardAmount, + &i.IsClaimed, + &i.ExpiresAt, + &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, user_id, bonus_code, reward_amount, is_claimed, expires_at, created_at, updated_at +FROM user_bonuses ` -type GetBonusBalanceCapRow struct { - ID int64 `json:"id"` - BalanceCap int64 `json:"balance_cap"` -} - -func (q *Queries) GetBonusBalanceCap(ctx context.Context) ([]GetBonusBalanceCapRow, error) { - rows, err := q.db.Query(ctx, GetBonusBalanceCap) +func (q *Queries) GetAllUserBonuses(ctx context.Context) ([]UserBonuse, error) { + rows, err := q.db.Query(ctx, GetAllUserBonuses) 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.UserID, + &i.BonusCode, + &i.RewardAmount, + &i.IsClaimed, + &i.ExpiresAt, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { return nil, err } items = append(items, i) @@ -54,26 +104,82 @@ func (q *Queries) GetBonusBalanceCap(ctx context.Context) ([]GetBonusBalanceCapR return items, nil } -const GetBonusMultiplier = `-- name: GetBonusMultiplier :many -SELECT id, multiplier -FROM bonus +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 GetBonusMultiplierRow struct { - ID int64 `json:"id"` - Multiplier float32 `json:"multiplier"` +type GetBonusStatsParams struct { + CompanyID pgtype.Int8 `json:"company_id"` + UserID pgtype.Int8 `json:"user_id"` } -func (q *Queries) GetBonusMultiplier(ctx context.Context) ([]GetBonusMultiplierRow, error) { - rows, err := q.db.Query(ctx, GetBonusMultiplier) +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 GetBonusesByUserID = `-- name: GetBonusesByUserID :many +SELECT id, name, description, user_id, bonus_code, reward_amount, is_claimed, expires_at, created_at, updated_at +FROM user_bonuses +WHERE user_id = $1 +` + +func (q *Queries) GetBonusesByUserID(ctx context.Context, userID int64) ([]UserBonuse, error) { + rows, err := q.db.Query(ctx, GetBonusesByUserID, userID) if err != nil { return nil, err } defer rows.Close() - var items []GetBonusMultiplierRow + var items []UserBonuse for rows.Next() { - var i GetBonusMultiplierRow - if err := rows.Scan(&i.ID, &i.Multiplier); err != nil { + var i UserBonuse + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.UserID, + &i.BonusCode, + &i.RewardAmount, + &i.IsClaimed, + &i.ExpiresAt, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { return nil, err } items = append(items, i) @@ -84,20 +190,42 @@ func (q *Queries) GetBonusMultiplier(ctx context.Context) ([]GetBonusMultiplierR return items, nil } -const UpdateBonusMultiplier = `-- name: UpdateBonusMultiplier :exec -UPDATE bonus -SET multiplier = $1, - balance_cap = $2 -WHERE id = $3 +const GetUserBonusByID = `-- name: GetUserBonusByID :one +SELECT id, name, description, user_id, bonus_code, reward_amount, is_claimed, expires_at, created_at, updated_at +FROM user_bonuses +WHERE id = $1 ` -type UpdateBonusMultiplierParams struct { - Multiplier float32 `json:"multiplier"` - BalanceCap int64 `json:"balance_cap"` - ID int64 `json:"id"` +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.UserID, + &i.BonusCode, + &i.RewardAmount, + &i.IsClaimed, + &i.ExpiresAt, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err } -func (q *Queries) UpdateBonusMultiplier(ctx context.Context, arg UpdateBonusMultiplierParams) error { - _, err := q.db.Exec(ctx, UpdateBonusMultiplier, arg.Multiplier, arg.BalanceCap, arg.ID) +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/models.go b/gen/db/models.go index 53adfa3..c206de5 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -79,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"` @@ -752,6 +746,19 @@ type User struct { Suspended bool `json:"suspended"` } +type UserBonuse struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + UserID int64 `json:"user_id"` + BonusCode string `json:"bonus_code"` + RewardAmount int64 `json:"reward_amount"` + IsClaimed bool `json:"is_claimed"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` +} + type UserGameInteraction struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` diff --git a/gen/db/transfer.sql.go b/gen/db/transfer.sql.go index 35e38d4..b2a1066 100644 --- a/gen/db/transfer.sql.go +++ b/gen/db/transfer.sql.go @@ -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/internal/domain/bonus.go b/internal/domain/bonus.go new file mode 100644 index 0000000..94d88a7 --- /dev/null +++ b/internal/domain/bonus.go @@ -0,0 +1,136 @@ +package domain + +import ( + "time" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/jackc/pgx/v5/pgtype" +) + +type UserBonus struct { + ID int64 + Name string + Description string + UserID int64 + BonusCode string + 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"` + BonusCode string `json:"bonus_code"` + 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, + UserID: bonus.UserID, + BonusCode: bonus.BonusCode, + RewardAmount: bonus.RewardAmount.Float32(), + IsClaimed: bonus.IsClaimed, + ExpiresAt: bonus.ExpiresAt, + CreatedAt: bonus.CreatedAt, + UpdatedAt: bonus.UpdatedAt, + } +} + +type CreateBonus struct { + Name string + Description string + UserID int64 + BonusCode string + RewardAmount Currency + ExpiresAt time.Time +} + +type CreateBonusReq struct { + Name string `json:"name"` + Description string `json:"description"` + UserID int64 `json:"user_id"` + BonusCode string `json:"bonus_code"` + 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, + UserID: bonus.UserID, + BonusCode: bonus.BonusCode, + RewardAmount: ToCurrency(bonus.RewardAmount), + ExpiresAt: bonus.ExpiresAt, + } +} + +func ConvertCreateBonus(bonus CreateBonus) dbgen.CreateUserBonusParams { + return dbgen.CreateUserBonusParams{ + Name: bonus.Name, + Description: bonus.Description, + UserID: bonus.UserID, + BonusCode: bonus.BonusCode, + 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, + UserID: bonus.UserID, + BonusCode: bonus.BonusCode, + 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 +} + +type BonusStats struct { + TotalBonus int64 + TotalRewardAmount Currency + ClaimedBonuses int64 + ExpiredBonuses int64 +} + +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/setting_list.go b/internal/domain/setting_list.go index 96ffe1a..3b376ec 100644 --- a/internal/domain/setting_list.go +++ b/internal/domain/setting_list.go @@ -16,17 +16,26 @@ 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"` - DefaultWinningLimit int64 `json:"default_winning_limit"` - ReferralRewardAmount Currency `json:"referral_reward_amount"` - CashbackPercentage float32 `json:"cashback_percentage"` - DefaultMaxReferrals int64 `json:"default_max_referrals"` + 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 { @@ -41,6 +50,10 @@ type SettingListRes struct { 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"` } func ConvertSettingListRes(settings SettingList) SettingListRes { @@ -56,6 +69,10 @@ func ConvertSettingListRes(settings SettingList) SettingListRes { ReferralRewardAmount: settings.ReferralRewardAmount.Float32(), CashbackPercentage: settings.CashbackPercentage, DefaultMaxReferrals: settings.DefaultMaxReferrals, + MinimumBetAmount: settings.MinimumBetAmount.Float32(), + BetDuplicateLimit: settings.BetDuplicateLimit, + SendEmailOnBetFinish: settings.SendEmailOnBetFinish, + SendSMSOnBetFinish: settings.SendSMSOnBetFinish, } } @@ -71,6 +88,10 @@ type SaveSettingListReq struct { 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"` } type ValidSettingList struct { @@ -85,6 +106,10 @@ type ValidSettingList struct { ReferralRewardAmount ValidCurrency CashbackPercentage ValidFloat32 DefaultMaxReferrals ValidInt64 + MinimumBetAmount ValidCurrency + BetDuplicateLimit ValidInt64 + SendEmailOnBetFinish ValidBool + SendSMSOnBetFinish ValidBool } func ConvertSaveSettingListReq(settings SaveSettingListReq) ValidSettingList { @@ -100,6 +125,10 @@ func ConvertSaveSettingListReq(settings SaveSettingListReq) ValidSettingList { 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), } } @@ -117,6 +146,10 @@ func (vsl *ValidSettingList) ToSettingList() SettingList { 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, } } @@ -134,6 +167,7 @@ func (vsl *ValidSettingList) GetInt64SettingsMap() map[string]*ValidInt64 { "daily_ticket_limit": &vsl.DailyTicketPerIP, "default_winning_limit": &vsl.DefaultWinningLimit, "default_max_referrals": &vsl.DefaultMaxReferrals, + "bet_duplicate_limit": &vsl.BetDuplicateLimit, } } @@ -144,6 +178,7 @@ func (vsl *ValidSettingList) GetCurrencySettingsMap() map[string]*ValidCurrency "amount_for_bet_referral": &vsl.AmountForBetReferral, "cashback_amount_cap": &vsl.CashbackAmountCap, "referral_reward_amount": &vsl.ReferralRewardAmount, + "minimum_bet_amount": &vsl.MinimumBetAmount, } } @@ -154,7 +189,10 @@ 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, + } } func (vsl *ValidSettingList) GetFloat32SettingsMap() map[string]*ValidFloat32 { @@ -167,7 +205,6 @@ func (vsl *ValidSettingList) GetTimeSettingsMap() map[string]*ValidTime { return map[string]*ValidTime{} } - // Setting Functions func (vsl *ValidSettingList) GetTotalSettings() int { 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/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..8f16e04 100644 --- a/internal/repository/bonus.go +++ b/internal/repository/bonus.go @@ -4,27 +4,75 @@ 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) ([]domain.UserBonus, error) { + bonuses, err := s.queries.GetAllUserBonuses(ctx) + + if err != nil { + return nil, err + } + + return domain.ConvertDBBonuses(bonuses), nil +} + +func (s *Store) GetBonusesByUserID(ctx context.Context, userID int64) ([]domain.UserBonus, error) { + bonuses, err := s.queries.GetBonusesByUserID(ctx, userID) + if err != nil { + return nil, err + } + + return domain.ConvertDBBonuses(bonuses), nil +} + +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) 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) GetBonusMultiplier(ctx context.Context) ([]dbgen.GetBonusMultiplierRow, error) { - return s.queries.GetBonusMultiplier(ctx) -} - -func (s *Store) GetBonusBalanceCap(ctx context.Context) ([]dbgen.GetBonusBalanceCapRow, error) { - return s.queries.GetBonusBalanceCap(ctx) -} - -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) 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/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/services/bet/service.go b/internal/services/bet/service.go index b2ef38f..65e361f 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 { @@ -221,7 +221,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 } @@ -284,9 +284,8 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID return domain.CreateBetRes{}, err } - // TODO: Make this a setting - if role == domain.RoleCustomer && count >= 10 { - return domain.CreateBetRes{}, fmt.Errorf("max user limit for single outcome") + if role == domain.RoleCustomer && count >= settingsList.BetDuplicateLimit { + return domain.CreateBetRes{}, fmt.Errorf("max user limit for duplicate bet") } fastCode := helpers.GenerateFastCode() @@ -387,7 +386,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID 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) @@ -588,25 +587,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) } @@ -635,19 +630,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 @@ -655,17 +644,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 @@ -673,8 +658,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 @@ -701,22 +685,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 } @@ -724,7 +698,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, @@ -734,17 +716,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 } @@ -770,12 +747,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 } @@ -784,10 +756,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 } @@ -795,20 +764,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 } @@ -830,10 +792,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 } @@ -843,19 +802,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 } @@ -902,53 +855,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") } @@ -956,7 +929,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), @@ -964,179 +937,211 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc return err } - return s.betStore.UpdateStatus(ctx, id, status) + 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), + ) + return err + } + + resultNotification.WinningAmount = amount + if err := s.SendWinningStatusNotification(ctx, resultNotification); err != nil { + + updateLogger.Error("failed to send winning notification", + zap.Error(err), + ) + return err + } + + return nil } -func (s *Service) SendWinningStatusNotification(ctx context.Context, status domain.OutcomeStatus, userID int64, winningAmount domain.Currency, extra string) error { +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 status { + switch param.Status { case domain.OUTCOME_STATUS_WIN: - headline = "You Bet Has Won!" + headline = fmt.Sprintf("Bet #%v Won!", param.BetID) message = fmt.Sprintf( - "You have been awarded %.2f", - winningAmount.Float32(), + "Congratulations! Your bet #%v has won. %.2f has been credited to your wallet.", + param.BetID, + param.WinningAmount.Float32(), ) case domain.OUTCOME_STATUS_HALF: - headline = "You have a half win" + headline = fmt.Sprintf("Bet #%v Half-Win", param.BetID) message = fmt.Sprintf( - "You have been awarded %.2f", - winningAmount.Float32(), + "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 = "Your bet has been refunded" + headline = fmt.Sprintf("Bet #%v Refunded", param.BetID) message = fmt.Sprintf( - "You have been awarded %.2f", - winningAmount.Float32(), + "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) } - 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 + 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, status domain.OutcomeStatus, userID int64, extra string) error { +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 status { + switch param.Status { case domain.OUTCOME_STATUS_LOSS: - headline = "Your bet has lost" - message = "Better luck next time" + 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) } - 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 + 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, status domain.OutcomeStatus, userID int64, extra string) error { +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 = "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" + 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) } - 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: domain.NotificationErrorSeverityHigh, - 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 + 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) SendAdminErrorAlertNotification(ctx context.Context, status domain.OutcomeStatus, extra string) error { +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 = "There was an error processing bet" - message = "We have encounter an error with bet. We will fix it as soon as we can" - } + headline = fmt.Sprintf("Processing Error for Bet #%v", betID) + message = "A processing error occurred with this bet. Please review and take corrective action." - betNotification := &domain.Notification{ - ErrorSeverity: domain.NotificationErrorSeverityHigh, - 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), + default: + return fmt.Errorf("unsupported status: %v", status) } super_admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{ @@ -1153,6 +1158,10 @@ func (s *Service) SendAdminErrorAlertNotification(ctx context.Context, status do admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{ Role: string(domain.RoleAdmin), + CompanyID: domain.ValidInt64{ + Value: companyID, + Valid: true, + }, }) if err != nil { @@ -1166,23 +1175,17 @@ func (s *Service) SendAdminErrorAlertNotification(ctx context.Context, status do 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 + 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 + } } } @@ -1366,6 +1369,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/port.go b/internal/services/bonus/port.go index 2147b51..3dafb67 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) ([]domain.UserBonus, error) + GetBonusesByUserID(ctx context.Context, userID int64) ([]domain.UserBonus, 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..089c1c7 100644 --- a/internal/services/bonus/service.go +++ b/internal/services/bonus/service.go @@ -2,32 +2,112 @@ package bonus import ( "context" + "errors" + "math" + "time" - dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers" + "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 + mongoLogger *zap.Logger } -func NewService(bonusStore BonusStore) *Service { +func NewService(bonusStore BonusStore, walletSvc *wallet.Service, settingSvc *settings.Service, mongoLogger *zap.Logger) *Service { return &Service{ - bonusStore: bonusStore, + bonusStore: bonusStore, + walletSvc: walletSvc, + settingSvc: settingSvc, + 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) ProcessWelcomeBonus(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)) + + _, err = s.CreateUserBonus(ctx, domain.CreateBonus{ + Name: "Welcome Bonus", + Description: "Awarded when the user logged in for the first time", + UserID: userID, + BonusCode: helpers.GenerateFastCode(), + RewardAmount: domain.Currency(newBalance), + ExpiresAt: time.Now().Add(time.Duration(settingsList.WelcomeBonusExpire) * 24 * time.Hour), + }) + + if err != nil { + return err + } + + // TODO: Add a claim function that adds to the static wallet when the user inputs his bonus code + // _, err = s.walletSvc.AddToWallet(ctx, wallet.StaticID, domain.ToCurrency(float32(newBalance)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, + // fmt.Sprintf("Added %v to static wallet because of deposit bonus using multiplier %v", newBalance, settingsList.WelcomeBonusMultiplier), + // ) + // if err != nil { + // return err + // } + + return nil + } -func (s *Service) GetBonusMultiplier(ctx context.Context) ([]dbgen.GetBonusMultiplierRow, error) { - return s.bonusStore.GetBonusMultiplier(ctx) +func (s *Service) CreateUserBonus(ctx context.Context, bonus domain.CreateBonus) (domain.UserBonus, error) { + return s.bonusStore.CreateUserBonus(ctx, bonus) } - -func (s *Service) GetBonusBalanceCap(ctx context.Context) ([]dbgen.GetBonusBalanceCapRow, error) { - return s.bonusStore.GetBonusBalanceCap(ctx) +func (s *Service) GetAllUserBonuses(ctx context.Context) ([]domain.UserBonus, error) { + return s.bonusStore.GetAllUserBonuses(ctx) } - -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) GetBonusesByUserID(ctx context.Context, userID int64) ([]domain.UserBonus, error) { + return s.bonusStore.GetBonusesByUserID(ctx, userID) +} +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/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/transfer.go b/internal/services/wallet/transfer.go index fd6bc04..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) } diff --git a/internal/services/wallet/wallet.go b/internal/services/wallet/wallet.go index 5865f75..7d04160 100644 --- a/internal/services/wallet/wallet.go +++ b/internal/services/wallet/wallet.go @@ -215,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) diff --git a/internal/web_server/handlers/bonus.go b/internal/web_server/handlers/bonus.go index f796827..80374cc 100644 --- a/internal/web_server/handlers/bonus.go +++ b/internal/web_server/handlers/bonus.go @@ -1,98 +1,98 @@ package handlers -import ( - "time" +// import ( +// "time" - "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" - "github.com/gofiber/fiber/v2" - "go.uber.org/zap" -) +// "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()), - ) - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error()) - } +// 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()) - } +// // 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 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()) - } +// 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) -} +// 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()) - } +// 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) -} +// 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"` - } +// 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 := 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()) - } +// 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) -} +// return response.WriteJSON(c, fiber.StatusOK, "Updated bonus multiplier successfully", nil, nil) +// } 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/routes.go b/internal/web_server/routes.go index 4a7f4b7..28b31e4 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -206,9 +206,9 @@ func (a *App) initAppRoutes() { 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) + // groupV1.Get("/bonus", a.authMiddleware, h.GetBonusMultiplier) + // 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) From e5f42f192801f30a6e8f4d40f170634920a47ded Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Fri, 12 Sep 2025 16:28:39 +0300 Subject: [PATCH 26/39] fix: refactor bonus and bonus settings; added welcome bonus --- cmd/main.go | 2 +- db/data/001_initial_seed_data.sql | 8 +- db/migrations/000001_fortune.up.sql | 37 +- db/migrations/000002_notification.up.sql | 3 +- db/query/bonus.sql | 21 +- gen/db/bonus.sql.go | 91 ++-- gen/db/models.go | 3 +- internal/domain/bonus.go | 92 ++-- internal/domain/notification.go | 3 +- internal/domain/setting_list.go | 217 ++++---- internal/pkgs/helpers/helpers.go | 22 +- internal/repository/bonus.go | 19 +- internal/services/bet/notification.go | 247 +++++++++ internal/services/bet/service.go | 239 --------- internal/services/bonus/notification.go | 83 +++ internal/services/bonus/port.go | 4 +- internal/services/bonus/service.go | 99 +++- internal/services/notfication/service.go | 475 ------------------ internal/services/virtualGame/veli/service.go | 28 +- internal/web_server/cron.go | 52 +- internal/web_server/handlers/bonus.go | 120 ++++- internal/web_server/routes.go | 4 +- 22 files changed, 875 insertions(+), 994 deletions(-) create mode 100644 internal/services/bet/notification.go create mode 100644 internal/services/bonus/notification.go delete mode 100644 internal/services/notfication/service.go diff --git a/cmd/main.go b/cmd/main.go index 2a5d805..bfa197b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -142,7 +142,7 @@ func main() { 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, messengerSvc, *userSvc) - bonusSvc := bonus.NewService(store, walletSvc, settingSvc, domain.MongoDBLogger) + bonusSvc := bonus.NewService(store, walletSvc, settingSvc, notificationSvc, domain.MongoDBLogger) referalRepo := repository.NewReferralRepository(store) vitualGameRepo := repository.NewVirtualGameRepository(store) recommendationRepo := repository.NewRecommendationRepository(store) diff --git a/db/data/001_initial_seed_data.sql b/db/data/001_initial_seed_data.sql index 55a3650..479027e 100644 --- a/db/data/001_initial_seed_data.sql +++ b/db/data/001_initial_seed_data.sql @@ -88,8 +88,14 @@ VALUES ('sms_provider', 'afro_message'), ('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') ON CONFLICT (key) DO NOTHING; + ('send_sms_on_bet_finish', 'false'), + ('welcome_bonus_active', 'false'), + ('welcome_bonus_multiplier', '1.5'), + ('welcome_bonus_multiplier', '100000'), + ('welcome_bonus_count', '3'), + ('welcome_bonus_expiry', '10') ON CONFLICT (key) DO NOTHING; -- Users INSERT INTO users ( id, diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index c6eefde..966e2b2 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -457,11 +457,12 @@ CREATE TABLE user_bonuses ( id BIGINT NOT NULL, name TEXT NOT NULL, description TEXT NOT NULL, + type TEXT NOT NULL, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - bonus_code TEXT NOT NULL UNIQUE, reward_amount BIGINT NOT NULL, is_claimed BOOLEAN NOT NULL DEFAULT false, expires_at TIMESTAMP NOT NULL, + claimed_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); @@ -500,7 +501,23 @@ CREATE TABLE direct_deposits ( 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 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, + UNIQUE (raffle_id, user_id) +); +------ Views CREATE VIEW companies_details AS SELECT companies.*, wallets.balance, @@ -526,22 +543,6 @@ CREATE TABLE IF NOT EXISTS supported_operations ( name VARCHAR(255) NOT NULL, description VARCHAR(255) NOT NULL ); -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, - UNIQUE (raffle_id, user_id) -); CREATE VIEW bet_with_outcomes AS SELECT bets.*, CONCAT (users.first_name, ' ', users.last_name) AS full_name, 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/query/bonus.sql b/db/query/bonus.sql index 4b07761..216528a 100644 --- a/db/query/bonus.sql +++ b/db/query/bonus.sql @@ -2,8 +2,8 @@ INSERT INTO user_bonuses ( name, description, + type, user_id, - bonus_code, reward_amount, expires_at ) @@ -11,15 +11,24 @@ VALUES ($1, $2, $3, $4, $5, $6) RETURNING *; -- name: GetAllUserBonuses :many SELECT * -FROM user_bonuses; +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: GetBonusesByUserID :many -SELECT * + +-- name: GetBonusCount :one +SELECT COUNT(*) FROM user_bonuses -WHERE user_id = $1; +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, @@ -30,7 +39,7 @@ SELECT COUNT(*) AS total_bonuses, ) AS claimed_bonuses, COUNT( CASE - WHEN expires_at > now() THEN 1 + WHEN expires_at < now() THEN 1 END ) AS expired_bonuses FROM user_bonuses diff --git a/gen/db/bonus.sql.go b/gen/db/bonus.sql.go index fe0b99b..7c6f168 100644 --- a/gen/db/bonus.sql.go +++ b/gen/db/bonus.sql.go @@ -15,20 +15,20 @@ const CreateUserBonus = `-- name: CreateUserBonus :one INSERT INTO user_bonuses ( name, description, + type, user_id, - bonus_code, reward_amount, expires_at ) VALUES ($1, $2, $3, $4, $5, $6) -RETURNING id, name, description, user_id, bonus_code, reward_amount, is_claimed, expires_at, created_at, updated_at +RETURNING id, name, description, type, user_id, reward_amount, is_claimed, expires_at, claimed_at, created_at, updated_at ` type CreateUserBonusParams struct { Name string `json:"name"` Description string `json:"description"` + Type string `json:"type"` UserID int64 `json:"user_id"` - BonusCode string `json:"bonus_code"` RewardAmount int64 `json:"reward_amount"` ExpiresAt pgtype.Timestamp `json:"expires_at"` } @@ -37,8 +37,8 @@ func (q *Queries) CreateUserBonus(ctx context.Context, arg CreateUserBonusParams row := q.db.QueryRow(ctx, CreateUserBonus, arg.Name, arg.Description, + arg.Type, arg.UserID, - arg.BonusCode, arg.RewardAmount, arg.ExpiresAt, ) @@ -47,11 +47,12 @@ func (q *Queries) CreateUserBonus(ctx context.Context, arg CreateUserBonusParams &i.ID, &i.Name, &i.Description, + &i.Type, &i.UserID, - &i.BonusCode, &i.RewardAmount, &i.IsClaimed, &i.ExpiresAt, + &i.ClaimedAt, &i.CreatedAt, &i.UpdatedAt, ) @@ -69,12 +70,23 @@ func (q *Queries) DeleteUserBonus(ctx context.Context, id int64) error { } const GetAllUserBonuses = `-- name: GetAllUserBonuses :many -SELECT id, name, description, user_id, bonus_code, reward_amount, is_claimed, expires_at, created_at, updated_at +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 ` -func (q *Queries) GetAllUserBonuses(ctx context.Context) ([]UserBonuse, error) { - rows, err := q.db.Query(ctx, GetAllUserBonuses) +type GetAllUserBonusesParams struct { + UserID pgtype.Int8 `json:"user_id"` + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` +} + +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 } @@ -86,11 +98,12 @@ func (q *Queries) GetAllUserBonuses(ctx context.Context) ([]UserBonuse, error) { &i.ID, &i.Name, &i.Description, + &i.Type, &i.UserID, - &i.BonusCode, &i.RewardAmount, &i.IsClaimed, &i.ExpiresAt, + &i.ClaimedAt, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -104,6 +117,22 @@ func (q *Queries) GetAllUserBonuses(ctx context.Context) ([]UserBonuse, error) { return items, nil } +const GetBonusCount = `-- name: GetBonusCount :one +SELECT COUNT(*) +FROM user_bonuses +WHERE ( + user_id = $1 + OR $1 IS NULL + ) +` + +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 +} + const GetBonusStats = `-- name: GetBonusStats :one SELECT COUNT(*) AS total_bonuses, COALESCE(SUM(reward_amount), 0)::bigint AS total_reward_earned, @@ -114,7 +143,7 @@ SELECT COUNT(*) AS total_bonuses, ) AS claimed_bonuses, COUNT( CASE - WHEN expires_at > now() THEN 1 + WHEN expires_at < now() THEN 1 END ) AS expired_bonuses FROM user_bonuses @@ -153,45 +182,8 @@ func (q *Queries) GetBonusStats(ctx context.Context, arg GetBonusStatsParams) (G return i, err } -const GetBonusesByUserID = `-- name: GetBonusesByUserID :many -SELECT id, name, description, user_id, bonus_code, reward_amount, is_claimed, expires_at, created_at, updated_at -FROM user_bonuses -WHERE user_id = $1 -` - -func (q *Queries) GetBonusesByUserID(ctx context.Context, userID int64) ([]UserBonuse, error) { - rows, err := q.db.Query(ctx, GetBonusesByUserID, userID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []UserBonuse - for rows.Next() { - var i UserBonuse - if err := rows.Scan( - &i.ID, - &i.Name, - &i.Description, - &i.UserID, - &i.BonusCode, - &i.RewardAmount, - &i.IsClaimed, - &i.ExpiresAt, - &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 GetUserBonusByID = `-- name: GetUserBonusByID :one -SELECT id, name, description, user_id, bonus_code, reward_amount, is_claimed, expires_at, created_at, updated_at +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 ` @@ -203,11 +195,12 @@ func (q *Queries) GetUserBonusByID(ctx context.Context, id int64) (UserBonuse, e &i.ID, &i.Name, &i.Description, + &i.Type, &i.UserID, - &i.BonusCode, &i.RewardAmount, &i.IsClaimed, &i.ExpiresAt, + &i.ClaimedAt, &i.CreatedAt, &i.UpdatedAt, ) diff --git a/gen/db/models.go b/gen/db/models.go index c206de5..b8bbdc9 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -750,11 +750,12 @@ type UserBonuse struct { ID int64 `json:"id"` Name string `json:"name"` Description string `json:"description"` + Type string `json:"type"` UserID int64 `json:"user_id"` - BonusCode string `json:"bonus_code"` 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"` } diff --git a/internal/domain/bonus.go b/internal/domain/bonus.go index 94d88a7..f436381 100644 --- a/internal/domain/bonus.go +++ b/internal/domain/bonus.go @@ -7,12 +7,19 @@ import ( "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 - BonusCode string + Type BonusType RewardAmount Currency IsClaimed bool ExpiresAt time.Time @@ -25,7 +32,7 @@ type UserBonusRes struct { Name string `json:"name"` Description string `json:"description"` UserID int64 `json:"user_id"` - BonusCode string `json:"bonus_code"` + Type BonusType `json:"type"` RewardAmount float32 `json:"reward_amount"` IsClaimed bool `json:"is_claimed"` ExpiresAt time.Time `json:"expires_at"` @@ -38,8 +45,8 @@ func ConvertToBonusRes(bonus UserBonus) UserBonusRes { ID: bonus.ID, Name: bonus.Name, Description: bonus.Description, + Type: bonus.Type, UserID: bonus.UserID, - BonusCode: bonus.BonusCode, RewardAmount: bonus.RewardAmount.Float32(), IsClaimed: bonus.IsClaimed, ExpiresAt: bonus.ExpiresAt, @@ -48,41 +55,51 @@ func ConvertToBonusRes(bonus UserBonus) UserBonusRes { } } +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 - BonusCode string RewardAmount Currency ExpiresAt time.Time } -type CreateBonusReq struct { - Name string `json:"name"` - Description string `json:"description"` - UserID int64 `json:"user_id"` - BonusCode string `json:"bonus_code"` - RewardAmount float32 `json:"reward_amount"` - ExpiresAt time.Time `json:"expires_at"` -} +// 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, - UserID: bonus.UserID, - BonusCode: bonus.BonusCode, - RewardAmount: ToCurrency(bonus.RewardAmount), - ExpiresAt: bonus.ExpiresAt, - } -} +// 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, - BonusCode: bonus.BonusCode, RewardAmount: int64(bonus.RewardAmount), ExpiresAt: pgtype.Timestamp{ Time: bonus.ExpiresAt, @@ -93,11 +110,12 @@ func ConvertCreateBonus(bonus CreateBonus) dbgen.CreateUserBonusParams { func ConvertDBBonus(bonus dbgen.UserBonuse) UserBonus { return UserBonus{ - ID: bonus.ID, - Name: bonus.Name, - Description: bonus.Description, - UserID: bonus.UserID, - BonusCode: bonus.BonusCode, + 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, @@ -117,6 +135,8 @@ func ConvertDBBonuses(bonuses []dbgen.UserBonuse) []UserBonus { type BonusFilter struct { UserID ValidInt64 CompanyID ValidInt64 + Limit ValidInt + Offset ValidInt } type BonusStats struct { @@ -126,6 +146,22 @@ type BonusStats struct { 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, diff --git a/internal/domain/notification.go b/internal/domain/notification.go index 28dbed2..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/setting_list.go b/internal/domain/setting_list.go index 3b376ec..5a5c86e 100644 --- a/internal/domain/setting_list.go +++ b/internal/domain/setting_list.go @@ -39,117 +39,147 @@ type SettingList struct { } 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"` - 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"` + 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, + 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"` - 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"` + 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 + 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), - 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), + 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), } } // 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: 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, + 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, } } @@ -168,6 +198,8 @@ func (vsl *ValidSettingList) GetInt64SettingsMap() map[string]*ValidInt64 { "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, } } @@ -179,6 +211,7 @@ func (vsl *ValidSettingList) GetCurrencySettingsMap() map[string]*ValidCurrency "cashback_amount_cap": &vsl.CashbackAmountCap, "referral_reward_amount": &vsl.ReferralRewardAmount, "minimum_bet_amount": &vsl.MinimumBetAmount, + "welcome_bonus_cap": &vsl.WelcomeBonusCap, } } @@ -192,12 +225,14 @@ func (vsl *ValidSettingList) GetBoolSettingsMap() 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{ - "cashback_percentage": &vsl.CashbackPercentage, + "cashback_percentage": &vsl.CashbackPercentage, + "welcome_bonus_multiplier": &vsl.WelcomeBonusMultiplier, } } diff --git a/internal/pkgs/helpers/helpers.go b/internal/pkgs/helpers/helpers.go index d9be84a..f336b1b 100644 --- a/internal/pkgs/helpers/helpers.go +++ b/internal/pkgs/helpers/helpers.go @@ -1,10 +1,11 @@ package helpers import ( + random "crypto/rand" "fmt" - "math/rand/v2" - "github.com/google/uuid" + "math/big" + "math/rand/v2" ) func GenerateID() string { @@ -24,3 +25,20 @@ 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 +} diff --git a/internal/repository/bonus.go b/internal/repository/bonus.go index 8f16e04..6b95b3e 100644 --- a/internal/repository/bonus.go +++ b/internal/repository/bonus.go @@ -17,8 +17,12 @@ func (s *Store) CreateUserBonus(ctx context.Context, bonus domain.CreateBonus) ( return domain.ConvertDBBonus(newBonus), nil } -func (s *Store) GetAllUserBonuses(ctx context.Context) ([]domain.UserBonus, error) { - bonuses, err := s.queries.GetAllUserBonuses(ctx) +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 @@ -27,13 +31,12 @@ func (s *Store) GetAllUserBonuses(ctx context.Context) ([]domain.UserBonus, erro return domain.ConvertDBBonuses(bonuses), nil } -func (s *Store) GetBonusesByUserID(ctx context.Context, userID int64) ([]domain.UserBonus, error) { - bonuses, err := s.queries.GetBonusesByUserID(ctx, userID) +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 nil, err + return 0, err } - - return domain.ConvertDBBonuses(bonuses), nil + return count, nil } func (s *Store) GetBonusByID(ctx context.Context, bonusID int64) (domain.UserBonus, error) { @@ -45,6 +48,8 @@ func (s *Store) GetBonusByID(ctx context.Context, bonusID int64) (domain.UserBon } + + 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(), 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 65e361f..4ba3a66 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -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()] @@ -957,241 +953,6 @@ func (s *Service) UpdateStatus(ctx context.Context, betId int64, status domain.O return nil } -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 -} - func (s *Service) CheckBetOutcomeForBet(ctx context.Context, betID int64) (domain.OutcomeStatus, error) { betOutcomes, err := s.betStore.GetBetOutcomeByBetID(ctx, betID) if err != nil { 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 3dafb67..4bbd877 100644 --- a/internal/services/bonus/port.go +++ b/internal/services/bonus/port.go @@ -8,8 +8,8 @@ import ( type BonusStore interface { CreateUserBonus(ctx context.Context, bonus domain.CreateBonus) (domain.UserBonus, error) - GetAllUserBonuses(ctx context.Context) ([]domain.UserBonus, error) - GetBonusesByUserID(ctx context.Context, userID int64) ([]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 diff --git a/internal/services/bonus/service.go b/internal/services/bonus/service.go index 089c1c7..047fb7c 100644 --- a/internal/services/bonus/service.go +++ b/internal/services/bonus/service.go @@ -3,29 +3,32 @@ package bonus import ( "context" "errors" + "fmt" "math" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - "github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers" + 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 - walletSvc *wallet.Service - settingSvc *settings.Service - mongoLogger *zap.Logger + bonusStore BonusStore + walletSvc *wallet.Service + settingSvc *settings.Service + notificationSvc *notificationservice.Service + mongoLogger *zap.Logger } -func NewService(bonusStore BonusStore, walletSvc *wallet.Service, settingSvc *settings.Service, mongoLogger *zap.Logger) *Service { +func NewService(bonusStore BonusStore, walletSvc *wallet.Service, settingSvc *settings.Service, notificationSvc *notificationservice.Service, mongoLogger *zap.Logger) *Service { return &Service{ - bonusStore: bonusStore, - walletSvc: walletSvc, - settingSvc: settingSvc, - mongoLogger: mongoLogger, + bonusStore: bonusStore, + walletSvc: walletSvc, + settingSvc: settingSvc, + notificationSvc: notificationSvc, + mongoLogger: mongoLogger, } } @@ -34,7 +37,7 @@ var ( ErrWelcomeBonusCountReached = errors.New("welcome bonus max deposit count reached") ) -func (s *Service) ProcessWelcomeBonus(ctx context.Context, amount domain.Currency, companyID int64, userID int64) error { +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", @@ -65,11 +68,10 @@ func (s *Service) ProcessWelcomeBonus(ctx context.Context, amount domain.Currenc newBalance := math.Min(float64(amount)*float64(settingsList.WelcomeBonusMultiplier), float64(settingsList.WelcomeBonusCap)) - _, err = s.CreateUserBonus(ctx, domain.CreateBonus{ + bonus, err := s.CreateUserBonus(ctx, domain.CreateBonus{ Name: "Welcome Bonus", - Description: "Awarded when the user logged in for the first time", + Description: fmt.Sprintf("Awarded for deposit number (%v / %v)", stats.TotalDeposits, settingsList.WelcomeBonusCount), UserID: userID, - BonusCode: helpers.GenerateFastCode(), RewardAmount: domain.Currency(newBalance), ExpiresAt: time.Now().Add(time.Duration(settingsList.WelcomeBonusExpire) * 24 * time.Hour), }) @@ -78,26 +80,73 @@ func (s *Service) ProcessWelcomeBonus(ctx context.Context, amount domain.Currenc return err } - // TODO: Add a claim function that adds to the static wallet when the user inputs his bonus code - // _, err = s.walletSvc.AddToWallet(ctx, wallet.StaticID, domain.ToCurrency(float32(newBalance)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, - // fmt.Sprintf("Added %v to static wallet because of deposit bonus using multiplier %v", newBalance, settingsList.WelcomeBonusMultiplier), - // ) - // 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 +} +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) CreateUserBonus(ctx context.Context, bonus domain.CreateBonus) (domain.UserBonus, error) { return s.bonusStore.CreateUserBonus(ctx, bonus) } -func (s *Service) GetAllUserBonuses(ctx context.Context) ([]domain.UserBonus, error) { - return s.bonusStore.GetAllUserBonuses(ctx) +func (s *Service) GetAllUserBonuses(ctx context.Context, filter domain.BonusFilter) ([]domain.UserBonus, error) { + return s.bonusStore.GetAllUserBonuses(ctx, filter) } -func (s *Service) GetBonusesByUserID(ctx context.Context, userID int64) ([]domain.UserBonus, error) { - return s.bonusStore.GetBonusesByUserID(ctx, userID) + +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) 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/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go index d7dc327..df0ac27 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -22,23 +22,23 @@ var ( type Service struct { virtualGameSvc virtualgameservice.VirtualGameService - repo repository.VirtualGameRepository - client *Client - walletSvc *wallet.Service - transfetStore wallet.TransferStore - cfg *config.Config + repo repository.VirtualGameRepository + client *Client + walletSvc *wallet.Service + transfetStore wallet.TransferStore + 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, cfg *config.Config) *Service { return &Service{ virtualGameSvc: virtualGameSvc, - repo: repo, - client: client, - walletSvc: walletSvc, - transfetStore: transferStore, - cfg: cfg, + repo: repo, + client: client, + walletSvc: walletSvc, + transfetStore: transferStore, + cfg: cfg, } -} +} func (s *Service) GetProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) { // Always mirror request body fields into sigParams @@ -80,6 +80,8 @@ func (s *Service) GetGames(ctx context.Context, req domain.GameListRequest) ([]d sigParams := map[string]any{ "brandId": req.BrandID, "providerId": req.ProviderID, + "size": req.Size, + "page": req.Page, } // 3. Call external API @@ -128,7 +130,6 @@ func (s *Service) StartGame(ctx context.Context, req domain.GameStartRequest) (* return &res, nil } - 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) @@ -160,7 +161,6 @@ func (s *Service) StartDemoGame(ctx context.Context, req domain.DemoGameRequest) return &res, nil } - func (s *Service) GetBalance(ctx context.Context, req domain.BalanceRequest) (*domain.BalanceResponse, error) { // Retrieve player's real balance from wallet Service playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64) diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 1bebede..1655050 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() { diff --git a/internal/web_server/handlers/bonus.go b/internal/web_server/handlers/bonus.go index 80374cc..a273dff 100644 --- a/internal/web_server/handlers/bonus.go +++ b/internal/web_server/handlers/bonus.go @@ -1,12 +1,13 @@ package handlers -// import ( -// "time" +import ( + "strconv" -// "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" -// "github.com/gofiber/fiber/v2" -// "go.uber.org/zap" -// ) + "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 { @@ -96,3 +97,110 @@ package handlers // 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.StatusInternalServerError, "Invalid user identification") + } + + 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.InternalServerErrorLogger().Error("Failed to bonus by userID", zap.Int64("userId", userID)) + return fiber.NewError(fiber.StatusInternalServerError, "failed to get bonus by user ID") + } + + count, err := h.bonusSvc.GetBonusCount(c.Context(), filter) + if err != nil { + 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") + } + + res := domain.ConvertToBonusResList(bonuses) + + return response.WritePaginatedJSON(c, fiber.StatusOK, "Fetched User Bonuses", res, nil, page, int(count)) + +} + +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.StatusInternalServerError, "Invalid user identification") + } + + 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 get bonus stats") + } + + 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/routes.go b/internal/web_server/routes.go index 28b31e4..07aeddc 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -206,7 +206,9 @@ func (a *App) initAppRoutes() { a.fiber.Get("/raffle-ticket/unsuspend/:id", a.authMiddleware, h.UnSuspendRaffleTicket) // Bonus Routes - // groupV1.Get("/bonus", a.authMiddleware, h.GetBonusMultiplier) + groupV1.Get("/bonus", a.authMiddleware, h.GetBonusesByUserID) + groupV1.Get("/bonus/stats", a.authMiddleware, h.GetBonusStats) + groupV1.Post("/bonus/claim/:id", a.authMiddleware, h.ClaimBonus) // groupV1.Post("/bonus/create", a.authMiddleware, h.CreateBonusMultiplier) // groupV1.Put("/bonus/update", a.authMiddleware, h.UpdateBonusMultiplier) From 90f69210828d83d2f9fad72ab065a732be9c1ee1 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Fri, 12 Sep 2025 16:45:37 +0300 Subject: [PATCH 27/39] chore: updating version to 1.0.dev14 --- internal/web_server/cron.go | 26 +++++++++++++------------- internal/web_server/routes.go | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 1655050..1cf19c6 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -78,19 +78,19 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S } }, }, - // { - // 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 { - // mongoLogger.Error("Failed to process result", - // zap.Error(err), - // ) - // } else { - // mongoLogger.Info("Completed sending daily result notification 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 { + mongoLogger.Error("Failed to process result", + zap.Error(err), + ) + } else { + mongoLogger.Info("Completed sending daily result notification without errors") + } + }, + }, } for _, job := range schedule { diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 16cc0b8..044a501 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -60,7 +60,7 @@ func (a *App) initAppRoutes() { a.fiber.Get("/", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{ "message": "Welcome to the FortuneBet API", - "version": "1.0.dev13.1", + "version": "1.0.dev14", }) }) From c49e191657aba4e0f06f64c311f3b4d07b59c6c6 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Fri, 12 Sep 2025 17:11:13 +0300 Subject: [PATCH 28/39] fix: change logger in veli handler --- internal/web_server/handlers/veli_games.go | 42 +++++++++++++++------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/internal/web_server/handlers/veli_games.go b/internal/web_server/handlers/veli_games.go index b5f5f02..dbc57ea 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(), }) @@ -141,8 +146,10 @@ func (h *Handler) StartGame(c *fiber.Ctx) error { 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{ @@ -193,8 +200,10 @@ func (h *Handler) StartDemoGame(c *fiber.Ctx) error { 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 +350,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 +390,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 +431,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(), From ab6c1acdc6217b51f04cda0ef9476e75e05b8390 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Fri, 12 Sep 2025 20:56:07 +0300 Subject: [PATCH 29/39] fix veli orchestration error --- cmd/main.go | 2 +- db/data/001_initial_seed_data.sql | 2 +- db/query/virtual_games.sql | 260 ++++++++++++----- gen/db/virtual_games.sql.go | 276 +++++++++++++----- internal/services/event/service.go | 2 +- .../virtualGame/veli/game_orchestration.go | 40 ++- internal/services/virtualGame/veli/service.go | 13 +- .../handlers/virtual_games_hadlers.go | 26 +- 8 files changed, 450 insertions(+), 171 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 9fcfad3..07471fe 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -153,7 +153,7 @@ func main() { 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) diff --git a/db/data/001_initial_seed_data.sql b/db/data/001_initial_seed_data.sql index 479027e..159a658 100644 --- a/db/data/001_initial_seed_data.sql +++ b/db/data/001_initial_seed_data.sql @@ -93,7 +93,7 @@ VALUES ('sms_provider', 'afro_message'), ('send_sms_on_bet_finish', 'false'), ('welcome_bonus_active', 'false'), ('welcome_bonus_multiplier', '1.5'), - ('welcome_bonus_multiplier', '100000'), + ('welcome_bonus_cap', '100000'), ('welcome_bonus_count', '3'), ('welcome_bonus_expiry', '10') ON CONFLICT (key) DO NOTHING; -- 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/virtual_games.sql.go b/gen/db/virtual_games.sql.go index 33697d7..5a2809a 100644 --- a/gen/db/virtual_games.sql.go +++ b/gen/db/virtual_games.sql.go @@ -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/internal/services/event/service.go b/internal/services/event/service.go index 09673d3..798a925 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -217,9 +217,9 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_ur s.mongoLogger.Error("Failed to fetch event data for page", zap.Error(err)) return } - const pageLimit int = 200 sportIDs := []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91} + // const pageLimit int = 1 // sportIDs := []int{1} var skippedLeague []string diff --git a/internal/services/virtualGame/veli/game_orchestration.go b/internal/services/virtualGame/veli/game_orchestration.go index 55d312d..4728119 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) } } @@ -64,6 +71,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) } @@ -80,9 +88,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) } @@ -128,16 +137,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, @@ -145,6 +167,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 } @@ -164,7 +187,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, @@ -190,12 +213,17 @@ 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) } @@ -221,7 +249,7 @@ func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.P } // --- Save to DB --- - _, _ = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{ + _, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{ GameID: fmt.Sprintf("popok-%d", g.ID), ProviderID: "popok", Name: g.GameName, @@ -235,6 +263,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 49eb716..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, } } 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 --- From a5dfd6c70d9e46dea70579c6549f34efd6d37b33 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Sat, 13 Sep 2025 21:06:56 +0300 Subject: [PATCH 30/39] raffle standing and winners per point --- db/migrations/000001_fortune.up.sql | 10 +- db/query/raffle.sql | 27 ++++++ gen/db/models.go | 8 ++ gen/db/raffle.sql.go | 97 +++++++++++++++++++ internal/domain/raffle.go | 16 +++ internal/pkgs/helpers/helpers.go | 15 +++ internal/repository/raffel.go | 43 ++++++++ internal/services/raffle/port.go | 3 + internal/services/raffle/service.go | 12 +++ .../web_server/handlers/raffle_handler.go | 96 ++++++++++++++++++ internal/web_server/routes.go | 2 + 11 files changed, 327 insertions(+), 2 deletions(-) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 28a61da..ba7f7bb 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -531,8 +531,14 @@ 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, - UNIQUE (raffle_id, user_id) + 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 VIEW bet_with_outcomes AS SELECT bets.*, diff --git a/db/query/raffle.sql b/db/query/raffle.sql index 4c43a10..089022f 100644 --- a/db/query/raffle.sql +++ b/db/query/raffle.sql @@ -32,3 +32,30 @@ SELECT 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; diff --git a/gen/db/models.go b/gen/db/models.go index 133b635..3605f88 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -555,6 +555,14 @@ type RaffleTicket struct { 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 Referral struct { ID int64 `json:"id"` CompanyID int64 `json:"company_id"` diff --git a/gen/db/raffle.sql.go b/gen/db/raffle.sql.go index 4b77106..480c30c 100644 --- a/gen/db/raffle.sql.go +++ b/gen/db/raffle.sql.go @@ -67,6 +67,31 @@ func (q *Queries) CreateRaffleTicket(ctx context.Context, arg CreateRaffleTicket 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 @@ -88,6 +113,67 @@ func (q *Queries) DeleteRaffle(ctx context.Context, id int32) (Raffle, error) { 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 ` @@ -169,6 +255,17 @@ func (q *Queries) GetUserRaffleTickets(ctx context.Context, userID int32) ([]Get 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 diff --git a/internal/domain/raffle.go b/internal/domain/raffle.go index c6adc2c..7366b0d 100644 --- a/internal/domain/raffle.go +++ b/internal/domain/raffle.go @@ -12,6 +12,22 @@ type Raffle struct { Status string } +type RaffleStanding struct { + UserID int64 + RaffleID int32 + FirstName string + LastName string + PhoneNumber string + Email string + TicketCount int64 +} + +type RaffleWinnerParams struct { + RaffleID int32 + UserID int32 + Rank int32 +} + type RaffleTicket struct { ID int32 RaffleID int32 diff --git a/internal/pkgs/helpers/helpers.go b/internal/pkgs/helpers/helpers.go index d9be84a..7a7b1fe 100644 --- a/internal/pkgs/helpers/helpers.go +++ b/internal/pkgs/helpers/helpers.go @@ -3,6 +3,7 @@ package helpers import ( "fmt" "math/rand/v2" + "strings" "github.com/google/uuid" ) @@ -24,3 +25,17 @@ func GenerateFastCode() string { } return code } + +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/raffel.go b/internal/repository/raffel.go index db230bd..f458c2c 100644 --- a/internal/repository/raffel.go +++ b/internal/repository/raffel.go @@ -52,6 +52,18 @@ func convertCreateRaffle(raffle domain.CreateRaffle) dbgen.CreateRaffleParams { } } +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 { @@ -126,3 +138,34 @@ func (s *Store) UnSuspendRaffleTicket(ctx context.Context, raffleID int32) error } // 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) +} diff --git a/internal/services/raffle/port.go b/internal/services/raffle/port.go index c8c4b8e..ee41045 100644 --- a/internal/services/raffle/port.go +++ b/internal/services/raffle/port.go @@ -11,6 +11,9 @@ type RaffleStore interface { CreateRaffle(ctx context.Context, raffle domain.CreateRaffle) (domain.Raffle, 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 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 diff --git a/internal/services/raffle/service.go b/internal/services/raffle/service.go index 1246fb7..3ca6b0e 100644 --- a/internal/services/raffle/service.go +++ b/internal/services/raffle/service.go @@ -29,6 +29,18 @@ func (s *Service) GetRafflesOfCompany(ctx context.Context, companyID int32) ([]d 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) } diff --git a/internal/web_server/handlers/raffle_handler.go b/internal/web_server/handlers/raffle_handler.go index d1b59ba..6f64a9b 100644 --- a/internal/web_server/handlers/raffle_handler.go +++ b/internal/web_server/handlers/raffle_handler.go @@ -1,11 +1,13 @@ 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" @@ -101,9 +103,102 @@ func (h *Handler) GetRafflesOfCompany(c *fiber.Ctx) error { 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 := []domain.RaffleStanding{} + for _, standing := range raffleStanding { + maskedStanding := domain.RaffleStanding{ + 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), @@ -127,6 +222,7 @@ func (h *Handler) CreateRaffleTicket(c *fiber.Ctx) error { 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), diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 9e68e79..9ee0244 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -198,6 +198,8 @@ func (a *App) initAppRoutes() { a.fiber.Post("/raffle/create", a.authMiddleware, h.CreateRaffle) a.fiber.Get("/raffle/delete/:id", a.authMiddleware, h.DeleteRaffle) a.fiber.Get("/raffle/company/:id", a.authMiddleware, h.GetRafflesOfCompany) + a.fiber.Get("/raffle/standing/:id/:limit", a.authMiddleware, h.GetRaffleStanding) + 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) From b7f3170d20ef6fda01e916da922fa42a53e5a8f8 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Mon, 15 Sep 2025 13:41:40 +0300 Subject: [PATCH 31/39] fix: removed 'popok' from the virtual game orchestrator game_id --- internal/services/virtualGame/veli/game_orchestration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/services/virtualGame/veli/game_orchestration.go b/internal/services/virtualGame/veli/game_orchestration.go index 4728119..ac8971d 100644 --- a/internal/services/virtualGame/veli/game_orchestration.go +++ b/internal/services/virtualGame/veli/game_orchestration.go @@ -229,7 +229,7 @@ func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.P 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, From 28b59e80810e655595c13496a69b6d314c3566ae Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Wed, 17 Sep 2025 12:07:16 +0300 Subject: [PATCH 32/39] create raffle ticket on every bet --- internal/web_server/handlers/bet_handler.go | 38 ++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index de240a3..cfe6343 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -236,7 +236,43 @@ func (h *Handler) CreateBetInternal(c *fiber.Ctx, req domain.CreateBetReq, userI return domain.CreateBetRes{}, err } - // create raffle ticket here + // 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()), + ) + } + + for _, raffle := range raffles { + // TODO: only fetch pending raffles from db + if raffle.Status == "completed" { + 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 } From 999b8d65918193438992e135f0fe20f3a4f0d0dc Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 17 Sep 2025 12:57:37 +0300 Subject: [PATCH 33/39] fix: minor changes --- .../virtualGame/game_orchestration.go | 12 ++++++++++ .../virtualGame/veli/game_orchestration.go | 4 ++-- internal/web_server/app.go | 22 ++++++++++--------- internal/web_server/handlers/veli_games.go | 8 ++++++- 4 files changed, 33 insertions(+), 13 deletions(-) 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 ac8971d..2bf3182 100644 --- a/internal/services/virtualGame/veli/game_orchestration.go +++ b/internal/services/virtualGame/veli/game_orchestration.go @@ -65,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, } diff --git a/internal/web_server/app.go b/internal/web_server/app.go index d4299a1..d2cf4db 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -60,7 +60,7 @@ type App struct { logger *slog.Logger NotidicationStore *notificationservice.Service referralSvc *referralservice.Service - raffleSvc raffle.RaffleStore + raffleSvc raffle.RaffleStore bonusSvc *bonus.Service port int settingSvc *settings.Service @@ -137,17 +137,19 @@ func NewApp( // AllowCredentials: true, })) + app.Static("/static", "./static") + s := &App{ atlasVirtualGameService: atlasVirtualGameService, - veliVirtualGameService: veliVirtualGameService, - telebirrSvc: telebirrSvc, - arifpaySvc: arifpaySvc, - santimpaySvc: santimpaySvc, - issueReportingSvc: issueReportingSvc, - instSvc: instSvc, - currSvc: currSvc, - fiber: app, - port: port, + veliVirtualGameService: veliVirtualGameService, + telebirrSvc: telebirrSvc, + arifpaySvc: arifpaySvc, + santimpaySvc: santimpaySvc, + issueReportingSvc: issueReportingSvc, + instSvc: instSvc, + currSvc: currSvc, + fiber: app, + port: port, settingSvc: settingSvc, authSvc: authSvc, diff --git a/internal/web_server/handlers/veli_games.go b/internal/web_server/handlers/veli_games.go index dbc57ea..c8adbd2 100644 --- a/internal/web_server/handlers/veli_games.go +++ b/internal/web_server/handlers/veli_games.go @@ -135,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) @@ -144,6 +146,8 @@ 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 { h.InternalServerErrorLogger().Error("Failed to [VeliGameHandler]StartGame", @@ -198,6 +202,8 @@ 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 { h.InternalServerErrorLogger().Error("Failed to [VeliGameHandler]StartDemoGame", From 2875e9b85c2804e684c1355f4a2d5e6bda09fe86 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 17 Sep 2025 15:43:27 +0300 Subject: [PATCH 34/39] fix: top-league not showing up --- db/migrations/000001_fortune.up.sql | 4 +- db/query/events.sql | 21 ++- db/query/leagues.sql | 44 +++++- gen/db/events.sql.go | 27 +++- gen/db/leagues.sql.go | 57 +++++++- gen/db/models.go | 16 +-- internal/domain/event.go | 1 + internal/domain/league.go | 4 +- internal/repository/event.go | 2 + internal/repository/league.go | 41 ++---- internal/services/league/port.go | 2 +- internal/services/league/service.go | 2 +- internal/web_server/cron.go | 130 +++++++++--------- internal/web_server/handlers/event_handler.go | 6 +- internal/web_server/handlers/leagues.go | 6 +- 15 files changed, 235 insertions(+), 128 deletions(-) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 75f2ed2..20dfd81 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -514,7 +514,7 @@ 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, + is_active BOOL DEFAULT true ); CREATE TABLE IF NOT EXISTS raffle_winners ( id SERIAL PRIMARY KEY, @@ -737,4 +737,4 @@ ADD CONSTRAINT fk_event_settings_company FOREIGN KEY (company_id) REFERENCES com 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; + 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 diff --git a/db/query/events.sql b/db/query/events.sql index 61f5a86..ba80192 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -187,7 +187,13 @@ WHERE is_live = false ) 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 e.*, @@ -231,10 +237,17 @@ WHERE start_time > now() AND ( l.country_code = sqlc.narg('country_code') OR sqlc.narg('country_code') IS NULL - ) AND ( - ces.is_featured = sqlc.narg('is_featured') - OR sqlc.narg('is_featured') 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 diff --git a/db/query/leagues.sql b/db/query/leagues.sql index 21765b9..476c3e8 100644 --- a/db/query/leagues.sql +++ b/db/query/leagues.sql @@ -42,14 +42,11 @@ WHERE ( ) ORDER BY name ASC LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); --- 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 +-- name: GetTotalLeaguesWithSettings :one +SELECT COUNT(*) FROM leagues l - LEFT JOIN company_league_settings cls ON l.id = cls.league_id AND company_id = $1 + 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 @@ -60,10 +57,43 @@ WHERE ( ) 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 ( diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index 3ee77ad..279cc8d 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -218,12 +218,19 @@ WHERE start_time > now() AND ( l.country_code = $7 OR $7 IS NULL - ) AND ( - ces.is_featured = $8 - OR $8 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 $10 OFFSET $9 +LIMIT $11 OFFSET $10 ` type GetEventsWithSettingsParams struct { @@ -235,6 +242,7 @@ type GetEventsWithSettingsParams struct { 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"` } @@ -283,6 +291,7 @@ func (q *Queries) GetEventsWithSettings(ctx context.Context, arg GetEventsWithSe arg.FirstStartTime, arg.CountryCode, arg.IsFeatured, + arg.IsActive, arg.Offset, arg.Limit, ) @@ -531,8 +540,14 @@ WHERE is_live = false ) 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 { @@ -544,6 +559,7 @@ type GetTotalCompanyEventsParams struct { 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) { @@ -556,6 +572,7 @@ func (q *Queries) GetTotalCompanyEvents(ctx context.Context, arg GetTotalCompany arg.FirstStartTime, arg.CountryCode, arg.IsFeatured, + arg.IsActive, ) var count int64 err := row.Scan(&count) diff --git a/gen/db/leagues.sql.go b/gen/db/leagues.sql.go index 57d3c28..1d2800b 100644 --- a/gen/db/leagues.sql.go +++ b/gen/db/leagues.sql.go @@ -102,7 +102,8 @@ SELECT l.id, l.name, l.img_url, l.country_code, l.bet365_id, l.sport_id, l.defau 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 + LEFT JOIN company_league_settings cls ON l.id = cls.league_id + AND company_id = $1 WHERE ( country_code = $2 OR $2 IS NULL @@ -113,10 +114,12 @@ WHERE ( ) 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 ( @@ -196,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/models.go b/gen/db/models.go index 863dc14..ae21984 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -502,6 +502,14 @@ type RaffleTicket struct { 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"` @@ -514,14 +522,6 @@ type ReferralCode struct { UpdatedAt pgtype.Timestamptz `json:"updated_at"` } -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 RefreshToken struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` diff --git a/internal/domain/event.go b/internal/domain/event.go index 57a1d32..152f1de 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -219,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 { diff --git a/internal/domain/league.go b/internal/domain/league.go index 0e0d65c..6743b6e 100644 --- a/internal/domain/league.go +++ b/internal/domain/league.go @@ -144,7 +144,7 @@ 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, @@ -167,7 +167,7 @@ func ConvertDBLeagueWithSetting(lws dbgen.LeagueWithSetting) LeagueWithSettings } } -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) diff --git a/internal/repository/event.go b/internal/repository/event.go index faf073e..9602796 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -86,6 +86,7 @@ func (s *Store) GetEventsWithSettings(ctx context.Context, companyID int64, filt LastStartTime: filter.LastStartTime.ToPG(), CountryCode: filter.CountryCode.ToPG(), IsFeatured: filter.Featured.ToPG(), + IsActive: filter.Active.ToPG(), }) if err != nil { @@ -101,6 +102,7 @@ func (s *Store) GetEventsWithSettings(ctx context.Context, companyID int64, filt LastStartTime: filter.LastStartTime.ToPG(), CountryCode: filter.CountryCode.ToPG(), IsFeatured: filter.Featured.ToPG(), + IsActive: filter.Active.ToPG(), }) if err != nil { return nil, 0, err diff --git a/internal/repository/league.go b/internal/repository/league.go index 4000b00..ae0a4d5 100644 --- a/internal/repository/league.go +++ b/internal/repository/league.go @@ -37,7 +37,8 @@ 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, @@ -52,38 +53,26 @@ func (s *Store) GetAllLeaguesByCompany(ctx context.Context, companyID int64, fil Valid: filter.Offset.Valid, }, IsFeatured: filter.IsFeatured.ToPG(), - IsActive: filter.IsActive.ToPG(), + IsActive: filter.IsActive.ToPG(), }) if err != nil { - return nil, err + return nil, 0, err } - result := make([]domain.LeagueWithSettings, len(l)) - for i, league := range l { - result[i] = domain.LeagueWithSettings{ - ID: league.ID, - Name: league.Name, - CompanyID: league.CompanyID.Int64, - CountryCode: domain.ValidString{ - Value: league.CountryCode.String, - Valid: league.CountryCode.Valid, - }, - Bet365ID: domain.ValidInt32{ - Value: league.Bet365ID.Int32, - Valid: league.Bet365ID.Valid, - }, - IsActive: league.IsActive, - SportID: league.SportID, - IsFeatured: league.IsFeatured, - UpdatedAt: league.UpdatedAt.Time, + 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(), + }) - DefaultIsActive: league.DefaultIsActive, - DefaultIsFeatured: league.DefaultIsFeatured, - } + if err != nil { + return nil, 0, err } - - return result, nil + return domain.ConvertDBLeagueWithSettings(l), total, nil } func (s *Store) CheckLeagueSupport(ctx context.Context, leagueID int64, companyID int64) (bool, error) { 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/web_server/cron.go b/internal/web_server/cron.go index 1cf19c6..3c39dfc 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -26,71 +26,71 @@ 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 */5 * * * *", // Every 5 Minutes - task: func() { - mongoLogger.Info("Began update all expired events status cron task") - if _, err := resultService.CheckAndUpdateExpiredEvents(context.Background()); err != nil { - mongoLogger.Error("Failed to update expired events status", - zap.Error(err), - ) - } else { - mongoLogger.Info("Completed expired events without errors") - } - }, - }, - { - 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 { - mongoLogger.Error("Failed to process result", - zap.Error(err), - ) - } else { - mongoLogger.Info("Completed sending daily result notification 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() { + // mongoLogger.Info("Began update all expired events status cron task") + // if _, err := resultService.CheckAndUpdateExpiredEvents(context.Background()); err != nil { + // mongoLogger.Error("Failed to update expired events status", + // zap.Error(err), + // ) + // } else { + // mongoLogger.Info("Completed expired events without errors") + // } + // }, + // }, + // { + // 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 { + // mongoLogger.Error("Failed to process result", + // zap.Error(err), + // ) + // } else { + // mongoLogger.Info("Completed sending daily result notification without errors") + // } + // }, + // }, } for _, job := range schedule { diff --git a/internal/web_server/handlers/event_handler.go b/internal/web_server/handlers/event_handler.go index 2de000d..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,12 +348,11 @@ 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, }, - }) if err != nil { @@ -371,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", @@ -570,7 +571,6 @@ type SetEventIsMonitoredReq struct { IsMonitored bool `json:"is_monitored"` } - // SetEventIsMonitored godoc // @Summary update the event is_monitored // @Description Update the event is_monitored diff --git a/internal/web_server/handlers/leagues.go b/internal/web_server/handlers/leagues.go index 4363e27..73abbf8 100644 --- a/internal/web_server/handlers/leagues.go +++ b/internal/web_server/handlers/leagues.go @@ -103,7 +103,7 @@ func (h *Handler) GetAllLeagues(c *fiber.Ctx) error { } res := domain.ConvertBaseLeagueResList(leagues) - + return response.WriteJSON(c, fiber.StatusOK, "All leagues retrieved", res, nil) } @@ -189,7 +189,7 @@ 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, @@ -206,7 +206,7 @@ func (h *Handler) GetAllLeaguesForTenant(c *fiber.Ctx) error { res := domain.ConvertLeagueWithSettingResList(leagues) - return response.WriteJSON(c, fiber.StatusOK, "All leagues retrieved", res, nil) + return response.WritePaginatedJSON(c, fiber.StatusOK, "All leagues retrieved", res, nil, page, int(total)) } type SetLeagueActiveReq struct { From af1cb4023e8ca663835cf10bb68bd7fff9580447 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 17 Sep 2025 17:25:49 +0300 Subject: [PATCH 35/39] fixing issues with fetching event --- cmd/main.go | 6 +- internal/domain/jsontypes.go | 27 +++++++ internal/domain/oddres.go | 2 +- internal/domain/validtypes.go | 3 +- internal/services/event/service.go | 21 +++-- internal/services/odds/service.go | 120 ++++++++++------------------ internal/services/result/service.go | 22 ++--- 7 files changed, 103 insertions(+), 98 deletions(-) create mode 100644 internal/domain/jsontypes.go diff --git a/cmd/main.go b/cmd/main.go index 07471fe..75f8265 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -114,7 +114,7 @@ func main() { authSvc := authentication.NewService(store, store, cfg.RefreshExpiry) userSvc := user.NewService(store, store, messengerSvc, cfg) - eventSvc := event.New(cfg.Bet365Token, store, *settingSvc, 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) @@ -143,7 +143,7 @@ func main() { 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, messengerSvc, *userSvc) - bonusSvc := bonus.NewService(store, walletSvc, settingSvc, notificationSvc, domain.MongoDBLogger) + bonusSvc := bonus.NewService(store, walletSvc, settingSvc, notificationSvc, domain.MongoDBLogger) referalRepo := repository.NewReferralRepository(store) vitualGameRepo := repository.NewVirtualGameRepository(store) recommendationRepo := repository.NewRecommendationRepository(store) @@ -153,7 +153,7 @@ func main() { 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), domain.MongoDBLogger, 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) 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/oddres.go b/internal/domain/oddres.go index 266aa11..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/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/services/event/service.go b/internal/services/event/service.go index 798a925..13f9897 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -12,6 +12,7 @@ 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" @@ -25,14 +26,16 @@ type service struct { store *repository.Store settingSvc settings.Service mongoLogger *zap.Logger + cfg *config.Config } -func New(token string, store *repository.Store, settingSvc settings.Service, 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, } } @@ -217,10 +220,18 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_ur s.mongoLogger.Error("Failed to fetch event data for page", zap.Error(err)) return } - const pageLimit int = 200 - sportIDs := []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91} - // const pageLimit int = 1 - // sportIDs := []int{1} + + 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 diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index dff3fdd..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 } diff --git a/internal/services/result/service.go b/internal/services/result/service.go index 4ae5bf3..cb19963 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -1007,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] From 65df6b9ff18c1cf00be0309e9c7688f43f225cf1 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 17 Sep 2025 21:19:48 +0300 Subject: [PATCH 36/39] fix: popok-id on orchestrator --- internal/services/virtualGame/veli/game_orchestration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/services/virtualGame/veli/game_orchestration.go b/internal/services/virtualGame/veli/game_orchestration.go index 2bf3182..e374cf0 100644 --- a/internal/services/virtualGame/veli/game_orchestration.go +++ b/internal/services/virtualGame/veli/game_orchestration.go @@ -250,7 +250,7 @@ func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.P // --- Save to DB --- _, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{ - GameID: fmt.Sprintf("popok-%d", g.ID), + GameID: fmt.Sprintf("%d", g.ID), //The id here needs to be clean for me to access ProviderID: "popok", Name: g.GameName, Bets: betsNumeric, From 723ca34660c229ccd76718f477f1eeb4b2777a90 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Thu, 18 Sep 2025 22:34:56 +0300 Subject: [PATCH 37/39] fix: fixed company override settings and raffle routes --- db/data/001_initial_seed_data.sql | 28 +++- db/migrations/000001_fortune.up.sql | 4 +- db/query/settings.sql | 4 +- gen/db/settings.sql.go | 10 +- internal/domain/raffle.go | 10 ++ internal/services/bonus/service.go | 1 + internal/services/user/direct.go | 1 + internal/web_server/cron.go | 130 +++++++++--------- .../web_server/handlers/raffle_handler.go | 25 +++- internal/web_server/routes.go | 15 +- 10 files changed, 145 insertions(+), 83 deletions(-) diff --git a/db/data/001_initial_seed_data.sql b/db/data/001_initial_seed_data.sql index 159a658..95a21d8 100644 --- a/db/data/001_initial_seed_data.sql +++ b/db/data/001_initial_seed_data.sql @@ -350,4 +350,30 @@ SET name = EXCLUDED.name, profit_percent = EXCLUDED.profit_percent, is_active = EXCLUDED.is_active, created_at = EXCLUDED.created_at, - updated_at = EXCLUDED.updated_at; \ No newline at end of file + 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/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 20dfd81..f8f509d 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -454,7 +454,7 @@ CREATE TABLE IF NOT EXISTS company_settings ( PRIMARY KEY (company_id, key) ); CREATE TABLE user_bonuses ( - id BIGINT NOT NULL, + id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL, description TEXT NOT NULL, type TEXT NOT NULL, @@ -462,7 +462,7 @@ CREATE TABLE user_bonuses ( reward_amount BIGINT NOT NULL, is_claimed BOOLEAN NOT NULL DEFAULT false, expires_at TIMESTAMP NOT NULL, - claimed_at TIMESTAMP NOT NULL, + claimed_at TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); 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/gen/db/settings.sql.go b/gen/db/settings.sql.go index f67fecc..96ea916 100644 --- a/gen/db/settings.sql.go +++ b/gen/db/settings.sql.go @@ -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/internal/domain/raffle.go b/internal/domain/raffle.go index 7366b0d..0767b5d 100644 --- a/internal/domain/raffle.go +++ b/internal/domain/raffle.go @@ -22,6 +22,16 @@ type RaffleStanding struct { 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 diff --git a/internal/services/bonus/service.go b/internal/services/bonus/service.go index 047fb7c..3daaf71 100644 --- a/internal/services/bonus/service.go +++ b/internal/services/bonus/service.go @@ -72,6 +72,7 @@ func (s *Service) CreateWelcomeBonus(ctx context.Context, amount domain.Currency 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), }) 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/web_server/cron.go b/internal/web_server/cron.go index 3c39dfc..1cf19c6 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -26,71 +26,71 @@ 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 */5 * * * *", // Every 5 Minutes - // task: func() { - // mongoLogger.Info("Began update all expired events status cron task") - // if _, err := resultService.CheckAndUpdateExpiredEvents(context.Background()); err != nil { - // mongoLogger.Error("Failed to update expired events status", - // zap.Error(err), - // ) - // } else { - // mongoLogger.Info("Completed expired events without errors") - // } - // }, - // }, - // { - // 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 { - // mongoLogger.Error("Failed to process result", - // zap.Error(err), - // ) - // } else { - // mongoLogger.Info("Completed sending daily result notification 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() { + mongoLogger.Info("Began update all expired events status cron task") + if _, err := resultService.CheckAndUpdateExpiredEvents(context.Background()); err != nil { + mongoLogger.Error("Failed to update expired events status", + zap.Error(err), + ) + } else { + mongoLogger.Info("Completed expired events without errors") + } + }, + }, + { + 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 { + mongoLogger.Error("Failed to process result", + zap.Error(err), + ) + } else { + mongoLogger.Info("Completed sending daily result notification without errors") + } + }, + }, } for _, job := range schedule { diff --git a/internal/web_server/handlers/raffle_handler.go b/internal/web_server/handlers/raffle_handler.go index 6f64a9b..6dc9041 100644 --- a/internal/web_server/handlers/raffle_handler.go +++ b/internal/web_server/handlers/raffle_handler.go @@ -103,6 +103,26 @@ func (h *Handler) GetRafflesOfCompany(c *fiber.Ctx) error { 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") @@ -122,9 +142,10 @@ func (h *Handler) GetRaffleStanding(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch raffle standing") } - maskedRaffleStanding := []domain.RaffleStanding{} + maskedRaffleStanding := make([]domain.RaffleStandingRes, 0, len(raffleStanding)) + for _, standing := range raffleStanding { - maskedStanding := domain.RaffleStanding{ + maskedStanding := domain.RaffleStandingRes{ UserID: standing.UserID, RaffleID: standing.RaffleID, FirstName: standing.FirstName, diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index f94cf4b..94924e7 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -60,7 +60,7 @@ func (a *App) initAppRoutes() { a.fiber.Get("/", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{ "message": "Welcome to the FortuneBet API", - "version": "1.0.dev14", + "version": "1.0.dev15", }) }) @@ -198,10 +198,11 @@ func (a *App) initAppRoutes() { // 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.Get("/raffle/delete/:id", a.authMiddleware, h.DeleteRaffle) a.fiber.Get("/raffle/company/:id", a.authMiddleware, h.GetRafflesOfCompany) - a.fiber.Get("/raffle/standing/:id/:limit", a.authMiddleware, h.GetRaffleStanding) 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) @@ -209,9 +210,9 @@ func (a *App) initAppRoutes() { a.fiber.Get("/raffle-ticket/unsuspend/:id", a.authMiddleware, h.UnSuspendRaffleTicket) // Bonus Routes - groupV1.Get("/bonus", a.authMiddleware, h.GetBonusesByUserID) - groupV1.Get("/bonus/stats", a.authMiddleware, h.GetBonusStats) - groupV1.Post("/bonus/claim/:id", a.authMiddleware, h.ClaimBonus) + 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) @@ -228,7 +229,7 @@ func (a *App) initAppRoutes() { 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) - tenant.Get("/customer/:id/bets", a.authMiddleware, h.GetCustomerBets) + groupV1.Get("/customer/:id/bets", a.authMiddleware, h.GetCustomerBets) groupV1.Get("/admin", a.authMiddleware, h.GetAllAdmins) groupV1.Get("/admin/:id", a.authMiddleware, h.GetAdminByID) @@ -446,8 +447,8 @@ func (a *App) initAppRoutes() { groupV1.Get("/settings/:key", a.authMiddleware, a.SuperAdminOnly, h.GetGlobalSettingByKey) groupV1.Put("/settings", a.authMiddleware, a.SuperAdminOnly, h.UpdateGlobalSettingList) - tenant.Post("/settings", a.authMiddleware, h.SaveCompanySettingList) 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) From ee27ec7f1098163a97588a6ff5d41207fc24593b Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Sun, 21 Sep 2025 16:14:08 +0300 Subject: [PATCH 38/39] add raffle filters --- db/migrations/000001_fortune.up.sql | 15 ++++++- db/query/raffle.sql | 5 +++ gen/db/auth.sql.go | 2 +- gen/db/bet.sql.go | 2 +- gen/db/bet_stat.sql.go | 2 +- gen/db/bonus.sql.go | 2 +- gen/db/branch.sql.go | 2 +- gen/db/cashier.sql.go | 2 +- gen/db/company.sql.go | 2 +- gen/db/copyfrom.go | 2 +- gen/db/db.go | 2 +- gen/db/direct_deposit.sql.go | 2 +- gen/db/disabled_odds.sql.go | 2 +- gen/db/event_history.sql.go | 2 +- gen/db/events.sql.go | 2 +- gen/db/events_stat.sql.go | 2 +- gen/db/flags.sql.go | 2 +- gen/db/institutions.sql.go | 2 +- gen/db/issue_reporting.sql.go | 2 +- gen/db/leagues.sql.go | 2 +- gen/db/location.sql.go | 2 +- gen/db/models.go | 15 ++++++- gen/db/monitor.sql.go | 2 +- gen/db/notification.sql.go | 2 +- gen/db/odd_history.sql.go | 2 +- gen/db/odds.sql.go | 2 +- gen/db/otp.sql.go | 2 +- gen/db/raffle.sql.go | 26 ++++++++++- gen/db/referal.sql.go | 2 +- gen/db/report.sql.go | 2 +- gen/db/result.sql.go | 2 +- gen/db/result_log.sql.go | 2 +- gen/db/settings.sql.go | 2 +- gen/db/shop_transactions.sql.go | 2 +- gen/db/ticket.sql.go | 2 +- gen/db/transfer.sql.go | 2 +- gen/db/user.sql.go | 2 +- gen/db/virtual_games.sql.go | 2 +- gen/db/wallet.sql.go | 2 +- internal/domain/raffle.go | 9 ++++ internal/repository/raffel.go | 9 ++++ internal/services/raffle/port.go | 2 + internal/services/raffle/service.go | 4 ++ .../web_server/handlers/raffle_handler.go | 44 +++++++++++++++++++ internal/web_server/routes.go | 2 + 45 files changed, 163 insertions(+), 38 deletions(-) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index f8f509d..c6c550d 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -523,6 +523,19 @@ CREATE TABLE IF NOT EXISTS raffle_winners ( 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.*, @@ -737,4 +750,4 @@ ADD CONSTRAINT fk_event_settings_company FOREIGN KEY (company_id) REFERENCES com 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_odds_market FOREIGN KEY (odds_market_id) REFERENCES odds_market (id) ON DELETE CASCADE; diff --git a/db/query/raffle.sql b/db/query/raffle.sql index 089022f..fecb50d 100644 --- a/db/query/raffle.sql +++ b/db/query/raffle.sql @@ -59,3 +59,8 @@ RETURNING *; 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 *; diff --git a/gen/db/auth.sql.go b/gen/db/auth.sql.go index 7d8d59d..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 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 7c6f168..1a5d8e9 100644 --- a/gen/db/bonus.sql.go +++ b/gen/db/bonus.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: bonus.sql package dbgen 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 fc4a7f8..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 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 279cc8d..73f929a 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 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 1d2800b..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 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 ae21984..1e27632 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 package dbgen @@ -495,6 +495,19 @@ type Raffle struct { Status string `json:"status"` } +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"` 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 79da894..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 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 index 7d0bab1..8aa8b34 100644 --- a/gen/db/raffle.sql.go +++ b/gen/db/raffle.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: raffle.sql package dbgen @@ -11,6 +11,30 @@ import ( "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 CreateRaffle = `-- name: CreateRaffle :one INSERT INTO raffles (company_id, name, expires_at, type) VALUES ($1, $2, $3, $4) diff --git a/gen/db/referal.sql.go b/gen/db/referal.sql.go index caaa01a..99d8bb2 100644 --- a/gen/db/referal.sql.go +++ b/gen/db/referal.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: referal.sql package dbgen 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 96ea916..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 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 b2a1066..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 diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index 999f169..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 diff --git a/gen/db/virtual_games.sql.go b/gen/db/virtual_games.sql.go index 5a2809a..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 diff --git a/gen/db/wallet.sql.go b/gen/db/wallet.sql.go index fcde631..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 diff --git a/internal/domain/raffle.go b/internal/domain/raffle.go index 0767b5d..a8307b8 100644 --- a/internal/domain/raffle.go +++ b/internal/domain/raffle.go @@ -12,6 +12,15 @@ type Raffle struct { 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 diff --git a/internal/repository/raffel.go b/internal/repository/raffel.go index f458c2c..02631f9 100644 --- a/internal/repository/raffel.go +++ b/internal/repository/raffel.go @@ -169,3 +169,12 @@ func (s *Store) CreateRaffleWinner(ctx context.Context, raffleWinnerParams domai 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 +} diff --git a/internal/services/raffle/port.go b/internal/services/raffle/port.go index ee41045..198b844 100644 --- a/internal/services/raffle/port.go +++ b/internal/services/raffle/port.go @@ -9,11 +9,13 @@ import ( 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 + 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 diff --git a/internal/services/raffle/service.go b/internal/services/raffle/service.go index 3ca6b0e..651534e 100644 --- a/internal/services/raffle/service.go +++ b/internal/services/raffle/service.go @@ -21,6 +21,10 @@ func (s *Service) CreateRaffle(ctx context.Context, raffle domain.CreateRaffle) 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) } diff --git a/internal/web_server/handlers/raffle_handler.go b/internal/web_server/handlers/raffle_handler.go index 6dc9041..4c316f8 100644 --- a/internal/web_server/handlers/raffle_handler.go +++ b/internal/web_server/handlers/raffle_handler.go @@ -50,6 +50,50 @@ func (h *Handler) CreateRaffle(c *fiber.Ctx) error { 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) diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 94924e7..0310a81 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -201,9 +201,11 @@ func (a *App) initAppRoutes() { 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) From 34109b6af448c5730224610fac81043b44e57090 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Mon, 22 Sep 2025 10:06:30 +0300 Subject: [PATCH 39/39] create raffle ticket with filter --- db/query/events.sql | 5 +++- db/query/raffle.sql | 7 +++++ gen/db/events.sql.go | 17 ++++++++++++ gen/db/raffle.sql.go | 21 +++++++++++++++ internal/repository/event.go | 10 +++++++ internal/repository/raffel.go | 13 +++++++++ internal/services/event/port.go | 3 ++- internal/services/event/service.go | 7 ++++- internal/services/raffle/port.go | 1 + internal/services/raffle/service.go | 4 +++ internal/web_server/handlers/bet_handler.go | 29 +++++++++++++++++++++ 11 files changed, 114 insertions(+), 3 deletions(-) diff --git a/db/query/events.sql b/db/query/events.sql index ba80192..c5ec974 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -276,6 +276,9 @@ 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, @@ -291,4 +294,4 @@ SET is_monitored = $1 WHERE 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/raffle.sql b/db/query/raffle.sql index fecb50d..55f302c 100644 --- a/db/query/raffle.sql +++ b/db/query/raffle.sql @@ -64,3 +64,10 @@ WHERE id = $1; 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/gen/db/events.sql.go b/gen/db/events.sql.go index 73f929a..9c9afe7 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -505,6 +505,23 @@ 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 events e diff --git a/gen/db/raffle.sql.go b/gen/db/raffle.sql.go index 8aa8b34..a4888f9 100644 --- a/gen/db/raffle.sql.go +++ b/gen/db/raffle.sql.go @@ -35,6 +35,27 @@ func (q *Queries) AddSportRaffleFilter(ctx context.Context, arg AddSportRaffleFi 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) diff --git a/internal/repository/event.go b/internal/repository/event.go index 9602796..8b4e87e 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -290,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/raffel.go b/internal/repository/raffel.go index 02631f9..6e37013 100644 --- a/internal/repository/raffel.go +++ b/internal/repository/raffel.go @@ -178,3 +178,16 @@ func (s *Store) AddSportRaffleFilter(ctx context.Context, raffleID int32, sportI }) 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/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 13f9897..76755c1 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -223,7 +223,7 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_ur 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 @@ -507,6 +507,11 @@ func (s *service) GetEventsWithSettings(ctx context.Context, companyID int64, fi 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/raffle/port.go b/internal/services/raffle/port.go index 198b844..39f5bfa 100644 --- a/internal/services/raffle/port.go +++ b/internal/services/raffle/port.go @@ -15,6 +15,7 @@ type RaffleStore interface { 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) diff --git a/internal/services/raffle/service.go b/internal/services/raffle/service.go index 651534e..017d164 100644 --- a/internal/services/raffle/service.go +++ b/internal/services/raffle/service.go @@ -60,3 +60,7 @@ func (s *Service) SuspendRaffleTicket(ctx context.Context, raffleTicketID int32) 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/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index cfe6343..c090de3 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -249,12 +249,41 @@ func (h *Handler) CreateBetInternal(c *fiber.Ctx, req domain.CreateBetReq, userI ) } + 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),