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: